diff --git a/cmd/gocmd/cmd_commands.go b/cmd/gocmd/cmd_commands.go deleted file mode 100644 index 5b2943a..0000000 --- a/cmd/gocmd/cmd_commands.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package gocmd provides Go development commands with enhanced output. -// -// Note: Package named gocmd because 'go' is a reserved keyword. -// -// Commands: -// - test: Run tests with colour-coded coverage summary -// - cov: Run tests with detailed coverage reports (HTML, thresholds) -// - fmt: Format code using goimports or gofmt -// - lint: Run golangci-lint -// - install: Install binary to $GOPATH/bin -// - mod: Module management (tidy, download, verify, graph) -// - work: Workspace management (sync, init, use) -// -// Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS. -package gocmd diff --git a/cmd/gocmd/cmd_format.go b/cmd/gocmd/cmd_format.go deleted file mode 100644 index 5eb643b..0000000 --- a/cmd/gocmd/cmd_format.go +++ /dev/null @@ -1,177 +0,0 @@ -package gocmd - -import ( - "bufio" - "os" - "os/exec" - "path/filepath" - "strings" - - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-i18n" -) - -var ( - fmtFix bool - fmtDiff bool - fmtCheck bool - fmtAll bool -) - -func addGoFmtCommand(parent *cli.Command) { - fmtCmd := &cli.Command{ - Use: "fmt", - Short: "Format Go code", - Long: "Format Go code using goimports or gofmt. By default only checks changed files.", - RunE: func(cmd *cli.Command, args []string) error { - // Get list of files to check - var files []string - if fmtAll { - // Check all Go files - files = []string{"."} - } else { - // Only check changed Go files (git-aware) - files = getChangedGoFiles() - if len(files) == 0 { - cli.Print("%s\n", i18n.T("cmd.go.fmt.no_changes")) - return nil - } - } - - // Validate flag combinations - if fmtCheck && fmtFix { - return cli.Err("--check and --fix are mutually exclusive") - } - - fmtArgs := []string{} - if fmtFix { - fmtArgs = append(fmtArgs, "-w") - } - if fmtDiff { - fmtArgs = append(fmtArgs, "-d") - } - if !fmtFix && !fmtDiff { - fmtArgs = append(fmtArgs, "-l") - } - fmtArgs = append(fmtArgs, files...) - - // Try goimports first, fall back to gofmt - var execCmd *exec.Cmd - if _, err := exec.LookPath("goimports"); err == nil { - execCmd = exec.Command("goimports", fmtArgs...) - } else { - execCmd = exec.Command("gofmt", fmtArgs...) - } - - // For --check mode, capture output to detect unformatted files - if fmtCheck { - output, err := execCmd.CombinedOutput() - if err != nil { - _, _ = os.Stderr.Write(output) - return err - } - if len(output) > 0 { - _, _ = os.Stdout.Write(output) - return cli.Err("files need formatting (use --fix)") - } - return nil - } - - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("common.flag.fix")) - fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("common.flag.diff")) - fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, i18n.T("cmd.go.fmt.flag.check")) - fmtCmd.Flags().BoolVar(&fmtAll, "all", false, i18n.T("cmd.go.fmt.flag.all")) - - parent.AddCommand(fmtCmd) -} - -// getChangedGoFiles returns Go files that have been modified, staged, or are untracked. -func getChangedGoFiles() []string { - var files []string - - // Get modified and staged files - cmd := exec.Command("git", "diff", "--name-only", "--diff-filter=ACMR", "HEAD") - output, err := cmd.Output() - if err == nil { - files = append(files, filterGoFiles(string(output))...) - } - - // Get untracked files - cmd = exec.Command("git", "ls-files", "--others", "--exclude-standard") - output, err = cmd.Output() - if err == nil { - files = append(files, filterGoFiles(string(output))...) - } - - // Deduplicate - seen := make(map[string]bool) - var unique []string - for _, f := range files { - if !seen[f] { - seen[f] = true - // Verify file exists (might have been deleted) - if _, err := os.Stat(f); err == nil { - unique = append(unique, f) - } - } - } - - return unique -} - -// filterGoFiles filters a newline-separated list of files to only include .go files. -func filterGoFiles(output string) []string { - var goFiles []string - scanner := bufio.NewScanner(strings.NewReader(output)) - for scanner.Scan() { - file := strings.TrimSpace(scanner.Text()) - if file != "" && filepath.Ext(file) == ".go" { - goFiles = append(goFiles, file) - } - } - return goFiles -} - -var ( - lintFix bool - lintAll bool -) - -func addGoLintCommand(parent *cli.Command) { - lintCmd := &cli.Command{ - Use: "lint", - Short: "Run golangci-lint", - Long: "Run golangci-lint for comprehensive static analysis. By default only lints changed files.", - RunE: func(cmd *cli.Command, args []string) error { - lintArgs := []string{"run"} - if lintFix { - lintArgs = append(lintArgs, "--fix") - } - - if !lintAll { - // Use --new-from-rev=HEAD to only report issues in uncommitted changes - // This is golangci-lint's native way to handle incremental linting - lintArgs = append(lintArgs, "--new-from-rev=HEAD") - } - - // Always lint all packages - lintArgs = append(lintArgs, "./...") - - execCmd := exec.Command("golangci-lint", lintArgs...) - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - lintCmd.Flags().BoolVar(&lintFix, "fix", false, i18n.T("common.flag.fix")) - lintCmd.Flags().BoolVar(&lintAll, "all", false, i18n.T("cmd.go.lint.flag.all")) - - parent.AddCommand(lintCmd) -} diff --git a/cmd/gocmd/cmd_fuzz.go b/cmd/gocmd/cmd_fuzz.go deleted file mode 100644 index 1f4ed0a..0000000 --- a/cmd/gocmd/cmd_fuzz.go +++ /dev/null @@ -1,169 +0,0 @@ -package gocmd - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-i18n" -) - -var ( - fuzzDuration time.Duration - fuzzPkg string - fuzzRun string - fuzzVerbose bool -) - -func addGoFuzzCommand(parent *cli.Command) { - fuzzCmd := &cli.Command{ - Use: "fuzz", - Short: "Run Go fuzz tests", - Long: `Run Go fuzz tests with configurable duration. - -Discovers Fuzz* functions across the project and runs each with go test -fuzz. - -Examples: - core go fuzz # Run all fuzz targets for 10s each - core go fuzz --duration=30s # Run each target for 30s - core go fuzz --pkg=./pkg/... # Fuzz specific package - core go fuzz --run=FuzzE # Run only matching fuzz targets`, - RunE: func(cmd *cli.Command, args []string) error { - return runGoFuzz(fuzzDuration, fuzzPkg, fuzzRun, fuzzVerbose) - }, - } - - fuzzCmd.Flags().DurationVar(&fuzzDuration, "duration", 10*time.Second, "Duration per fuzz target") - fuzzCmd.Flags().StringVar(&fuzzPkg, "pkg", "", "Package to fuzz (default: auto-discover)") - fuzzCmd.Flags().StringVar(&fuzzRun, "run", "", "Only run fuzz targets matching pattern") - fuzzCmd.Flags().BoolVarP(&fuzzVerbose, "verbose", "v", false, "Verbose output") - - parent.AddCommand(fuzzCmd) -} - -// fuzzTarget represents a discovered fuzz function and its package. -type fuzzTarget struct { - Pkg string - Name string -} - -func runGoFuzz(duration time.Duration, pkg, run string, verbose bool) error { - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("fuzz")), i18n.ProgressSubject("run", "fuzz tests")) - cli.Blank() - - targets, err := discoverFuzzTargets(pkg, run) - if err != nil { - return cli.Wrap(err, "discover fuzz targets") - } - - if len(targets) == 0 { - cli.Print(" %s no fuzz targets found\n", dimStyle.Render("—")) - return nil - } - - cli.Print(" %s %d target(s), %s each\n", dimStyle.Render(i18n.Label("targets")), len(targets), duration) - cli.Blank() - - passed := 0 - failed := 0 - - for _, t := range targets { - cli.Print(" %s %s in %s\n", dimStyle.Render("→"), t.Name, t.Pkg) - - args := []string{ - "test", - fmt.Sprintf("-fuzz=^%s$", t.Name), - fmt.Sprintf("-fuzztime=%s", duration), - "-run=^$", // Don't run unit tests - } - if verbose { - args = append(args, "-v") - } - args = append(args, t.Pkg) - - cmd := exec.Command("go", args...) - cmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0", "CGO_ENABLED=0") - cmd.Dir, _ = os.Getwd() - - output, runErr := cmd.CombinedOutput() - outputStr := string(output) - - if runErr != nil { - failed++ - cli.Print(" %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), runErr.Error()) - if outputStr != "" { - cli.Text(outputStr) - } - } else { - passed++ - cli.Print(" %s %s\n", successStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.done.pass")) - if verbose && outputStr != "" { - cli.Text(outputStr) - } - } - } - - cli.Blank() - if failed > 0 { - cli.Print("%s %d passed, %d failed\n", errorStyle.Render(cli.Glyph(":cross:")), passed, failed) - return cli.Err("fuzz: %d target(s) failed", failed) - } - - cli.Print("%s %d passed\n", successStyle.Render(cli.Glyph(":check:")), passed) - return nil -} - -// discoverFuzzTargets scans for Fuzz* functions in test files. -func discoverFuzzTargets(pkg, pattern string) ([]fuzzTarget, error) { - root := "." - if pkg != "" { - // Convert Go package pattern to filesystem path - root = strings.TrimPrefix(pkg, "./") - root = strings.TrimSuffix(root, "/...") - } - - fuzzRe := regexp.MustCompile(`^func\s+(Fuzz\w+)\s*\(\s*\w+\s+\*testing\.F\s*\)`) - var matchRe *regexp.Regexp - if pattern != "" { - var err error - matchRe, err = regexp.Compile(pattern) - if err != nil { - return nil, fmt.Errorf("invalid --run pattern: %w", err) - } - } - - var targets []fuzzTarget - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() || !strings.HasSuffix(info.Name(), "_test.go") { - return nil - } - - data, readErr := os.ReadFile(path) - if readErr != nil { - return nil - } - - dir := "./" + filepath.Dir(path) - for line := range strings.SplitSeq(string(data), "\n") { - m := fuzzRe.FindStringSubmatch(line) - if m == nil { - continue - } - name := m[1] - if matchRe != nil && !matchRe.MatchString(name) { - continue - } - targets = append(targets, fuzzTarget{Pkg: dir, Name: name}) - } - return nil - }) - return targets, err -} diff --git a/cmd/gocmd/cmd_go.go b/cmd/gocmd/cmd_go.go deleted file mode 100644 index f272e5c..0000000 --- a/cmd/gocmd/cmd_go.go +++ /dev/null @@ -1,36 +0,0 @@ -// Package gocmd provides Go development commands. -// -// Note: Package named gocmd because 'go' is a reserved keyword. -package gocmd - -import ( - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-i18n" -) - -// Style aliases for shared styles -var ( - successStyle = cli.SuccessStyle - errorStyle = cli.ErrorStyle - dimStyle = cli.DimStyle -) - -// AddGoCommands adds Go development commands. -func AddGoCommands(root *cli.Command) { - goCmd := &cli.Command{ - Use: "go", - Short: i18n.T("cmd.go.short"), - Long: i18n.T("cmd.go.long"), - } - - root.AddCommand(goCmd) - addGoQACommand(goCmd) - addGoTestCommand(goCmd) - addGoCovCommand(goCmd) - addGoFmtCommand(goCmd) - addGoLintCommand(goCmd) - addGoInstallCommand(goCmd) - addGoModCommand(goCmd) - addGoWorkCommand(goCmd) - addGoFuzzCommand(goCmd) -} diff --git a/cmd/gocmd/cmd_gotest.go b/cmd/gocmd/cmd_gotest.go deleted file mode 100644 index 20594df..0000000 --- a/cmd/gocmd/cmd_gotest.go +++ /dev/null @@ -1,430 +0,0 @@ -package gocmd - -import ( - "bufio" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "strconv" - "strings" - - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-i18n" -) - -var ( - testCoverage bool - testPkg string - testRun string - testShort bool - testRace bool - testJSON bool - testVerbose bool -) - -func addGoTestCommand(parent *cli.Command) { - testCmd := &cli.Command{ - Use: "test", - Short: "Run Go tests", - Long: "Run Go tests with optional coverage, filtering, and race detection", - RunE: func(cmd *cli.Command, args []string) error { - return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose) - }, - } - - testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Generate coverage report") - testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package to test") - testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching pattern") - testCmd.Flags().BoolVar(&testShort, "short", false, "Run only short tests") - testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector") - testCmd.Flags().BoolVar(&testJSON, "json", false, "Output as JSON") - testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, "Verbose output") - - parent.AddCommand(testCmd) -} - -func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose bool) error { - if pkg == "" { - pkg = "./..." - } - - args := []string{"test"} - - var covPath string - if coverage { - args = append(args, "-cover", "-covermode=atomic") - covFile, err := os.CreateTemp("", "coverage-*.out") - if err == nil { - covPath = covFile.Name() - _ = covFile.Close() - args = append(args, "-coverprofile="+covPath) - defer os.Remove(covPath) - } - } - - if run != "" { - args = append(args, "-run", run) - } - if short { - args = append(args, "-short") - } - if race { - args = append(args, "-race") - } - if verbose { - args = append(args, "-v") - } - - args = append(args, pkg) - - if !jsonOut { - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("test")), i18n.ProgressSubject("run", "tests")) - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), pkg) - cli.Blank() - } - - cmd := exec.Command("go", args...) - cmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0", "CGO_ENABLED=0") - cmd.Dir, _ = os.Getwd() - - output, err := cmd.CombinedOutput() - outputStr := string(output) - - // Filter linker warnings - lines := strings.Split(outputStr, "\n") - var filtered []string - for _, line := range lines { - if !strings.Contains(line, "ld: warning:") { - filtered = append(filtered, line) - } - } - outputStr = strings.Join(filtered, "\n") - - // Parse results - passed, failed, skipped := parseTestResults(outputStr) - cov := parseOverallCoverage(outputStr) - - if jsonOut { - cli.Print(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`, - passed, failed, skipped, cov, cmd.ProcessState.ExitCode()) - cli.Blank() - return err - } - - // Print filtered output if verbose or failed - if verbose || err != nil { - cli.Text(outputStr) - } - - // Summary - if err == nil { - cli.Print(" %s %s\n", successStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass")) - } else { - cli.Print(" %s %s, %s\n", errorStyle.Render(cli.Glyph(":cross:")), - i18n.T("i18n.count.test", passed)+" "+i18n.T("i18n.done.pass"), - i18n.T("i18n.count.test", failed)+" "+i18n.T("i18n.done.fail")) - } - - if cov > 0 { - cli.Print("\n %s %s\n", cli.KeyStyle.Render(i18n.Label("statements")), formatCoverage(cov)) - if covPath != "" { - branchCov, err := calculateBlockCoverage(covPath) - if err != nil { - cli.Print(" %s %s\n", cli.KeyStyle.Render(i18n.Label("branches")), cli.ErrorStyle.Render("unable to calculate")) - } else { - cli.Print(" %s %s\n", cli.KeyStyle.Render(i18n.Label("branches")), formatCoverage(branchCov)) - } - } - } - - if err == nil { - cli.Print("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) - } else { - cli.Print("\n%s\n", errorStyle.Render(i18n.T("i18n.done.fail"))) - } - - return err -} - -func parseTestResults(output string) (passed, failed, skipped int) { - passRe := regexp.MustCompile(`(?m)^ok\s+`) - failRe := regexp.MustCompile(`(?m)^FAIL\s+`) - skipRe := regexp.MustCompile(`(?m)^\?\s+`) - - passed = len(passRe.FindAllString(output, -1)) - failed = len(failRe.FindAllString(output, -1)) - skipped = len(skipRe.FindAllString(output, -1)) - return -} - -func parseOverallCoverage(output string) float64 { - re := regexp.MustCompile(`coverage:\s+([\d.]+)%`) - matches := re.FindAllStringSubmatch(output, -1) - if len(matches) == 0 { - return 0 - } - - var total float64 - for _, m := range matches { - var cov float64 - _, _ = fmt.Sscanf(m[1], "%f", &cov) - total += cov - } - return total / float64(len(matches)) -} - -var ( - covPkg string - covHTML bool - covOpen bool - covThreshold float64 - covBranchThreshold float64 - covOutput string -) - -func addGoCovCommand(parent *cli.Command) { - covCmd := &cli.Command{ - Use: "cov", - Short: "Run tests with coverage report", - Long: "Run tests with detailed coverage reports, HTML output, and threshold checking", - RunE: func(cmd *cli.Command, args []string) error { - pkg := covPkg - if pkg == "" { - // Auto-discover packages with tests - pkgs, err := findTestPackages(".") - if err != nil { - return cli.Wrap(err, i18n.T("i18n.fail.find", "test packages")) - } - if len(pkgs) == 0 { - return errors.New("no test packages found") - } - pkg = strings.Join(pkgs, " ") - } - - // Create temp file for coverage data - covFile, err := os.CreateTemp("", "coverage-*.out") - if err != nil { - return cli.Wrap(err, i18n.T("i18n.fail.create", "coverage file")) - } - covPath := covFile.Name() - _ = covFile.Close() - defer func() { - if covOutput == "" { - _ = os.Remove(covPath) - } else { - // Copy to output destination before removing - src, _ := os.Open(covPath) - dst, _ := os.Create(covOutput) - if src != nil && dst != nil { - _, _ = io.Copy(dst, src) - _ = src.Close() - _ = dst.Close() - } - _ = os.Remove(covPath) - } - }() - - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("coverage")), i18n.ProgressSubject("run", "tests")) - // Truncate package list if too long for display - displayPkg := pkg - if len(displayPkg) > 60 { - displayPkg = displayPkg[:57] + "..." - } - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("package")), displayPkg) - cli.Blank() - - // Run tests with coverage - // We need to split pkg into individual arguments if it contains spaces - pkgArgs := strings.Fields(pkg) - cmdArgs := append([]string{"test", "-coverprofile=" + covPath, "-covermode=atomic"}, pkgArgs...) - - goCmd := exec.Command("go", cmdArgs...) - goCmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0") - goCmd.Stdout = os.Stdout - goCmd.Stderr = os.Stderr - - testErr := goCmd.Run() - - // Get coverage percentage - coverCmd := exec.Command("go", "tool", "cover", "-func="+covPath) - covOutput, err := coverCmd.Output() - if err != nil { - if testErr != nil { - return testErr - } - return cli.Wrap(err, i18n.T("i18n.fail.get", "coverage")) - } - - // Parse total coverage from last line - lines := strings.Split(strings.TrimSpace(string(covOutput)), "\n") - var statementCov float64 - if len(lines) > 0 { - lastLine := lines[len(lines)-1] - // Format: "total: (statements) XX.X%" - if strings.Contains(lastLine, "total:") { - parts := strings.Fields(lastLine) - if len(parts) >= 3 { - covStr := strings.TrimSuffix(parts[len(parts)-1], "%") - _, _ = fmt.Sscanf(covStr, "%f", &statementCov) - } - } - } - - // Calculate branch coverage (block coverage) - branchCov, err := calculateBlockCoverage(covPath) - if err != nil { - return cli.Wrap(err, "calculate branch coverage") - } - - // Print coverage summary - cli.Blank() - cli.Print(" %s %s\n", cli.KeyStyle.Render(i18n.Label("statements")), formatCoverage(statementCov)) - cli.Print(" %s %s\n", cli.KeyStyle.Render(i18n.Label("branches")), formatCoverage(branchCov)) - - // Generate HTML if requested - if covHTML || covOpen { - htmlPath := "coverage.html" - htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) - if err := htmlCmd.Run(); err != nil { - return cli.Wrap(err, i18n.T("i18n.fail.generate", "HTML")) - } - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("html")), htmlPath) - - if covOpen { - // Open in browser - var openCmd *exec.Cmd - switch { - case exec.Command("which", "open").Run() == nil: - openCmd = exec.Command("open", htmlPath) - case exec.Command("which", "xdg-open").Run() == nil: - openCmd = exec.Command("xdg-open", htmlPath) - default: - cli.Print(" %s\n", dimStyle.Render("Open coverage.html in your browser")) - } - if openCmd != nil { - _ = openCmd.Run() - } - } - } - - // Check thresholds - if covThreshold > 0 && statementCov < covThreshold { - cli.Print("\n%s Statements: %.1f%% < %.1f%%\n", errorStyle.Render(i18n.T("i18n.fail.meet", "threshold")), statementCov, covThreshold) - return errors.New("statement coverage below threshold") - } - if covBranchThreshold > 0 && branchCov < covBranchThreshold { - cli.Print("\n%s Branches: %.1f%% < %.1f%%\n", errorStyle.Render(i18n.T("i18n.fail.meet", "threshold")), branchCov, covBranchThreshold) - return errors.New("branch coverage below threshold") - } - - if testErr != nil { - return testErr - } - - cli.Print("\n%s\n", successStyle.Render(i18n.T("i18n.done.pass"))) - return nil - }, - } - - covCmd.Flags().StringVar(&covPkg, "pkg", "", "Package to test") - covCmd.Flags().BoolVar(&covHTML, "html", false, "Generate HTML report") - covCmd.Flags().BoolVar(&covOpen, "open", false, "Open HTML report in browser") - covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, "Minimum statement coverage percentage") - covCmd.Flags().Float64Var(&covBranchThreshold, "branch-threshold", 0, "Minimum branch coverage percentage") - covCmd.Flags().StringVarP(&covOutput, "output", "o", "", "Output file for coverage profile") - - parent.AddCommand(covCmd) -} - -// calculateBlockCoverage parses a Go coverage profile and returns the percentage of basic -// blocks that have a non-zero execution count. Go's coverage profile contains one line per -// basic block, where the last field is the execution count, not explicit branch coverage. -// The resulting block coverage is used here only as a proxy for branch coverage; computing -// true branch coverage would require more detailed control-flow analysis. -func calculateBlockCoverage(path string) (float64, error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - var totalBlocks, coveredBlocks int - - // Skip the first line (mode: atomic/set/count) - if !scanner.Scan() { - return 0, nil - } - - for scanner.Scan() { - line := scanner.Text() - if line == "" { - continue - } - fields := strings.Fields(line) - if len(fields) < 3 { - continue - } - - // Last field is the count - count, err := strconv.Atoi(fields[len(fields)-1]) - if err != nil { - continue - } - - totalBlocks++ - if count > 0 { - coveredBlocks++ - } - } - - if err := scanner.Err(); err != nil { - return 0, err - } - - if totalBlocks == 0 { - return 0, nil - } - - return (float64(coveredBlocks) / float64(totalBlocks)) * 100, nil -} - -func findTestPackages(root string) ([]string, error) { - pkgMap := make(map[string]bool) - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if !info.IsDir() && strings.HasSuffix(info.Name(), "_test.go") { - dir := filepath.Dir(path) - if !strings.HasPrefix(dir, ".") { - dir = "./" + dir - } - pkgMap[dir] = true - } - return nil - }) - if err != nil { - return nil, err - } - - var pkgs []string - for pkg := range pkgMap { - pkgs = append(pkgs, pkg) - } - return pkgs, nil -} - -func formatCoverage(cov float64) string { - s := fmt.Sprintf("%.1f%%", cov) - if cov >= 80 { - return cli.SuccessStyle.Render(s) - } else if cov >= 50 { - return cli.WarningStyle.Render(s) - } - return cli.ErrorStyle.Render(s) -} diff --git a/cmd/gocmd/cmd_qa.go b/cmd/gocmd/cmd_qa.go deleted file mode 100644 index 1d04760..0000000 --- a/cmd/gocmd/cmd_qa.go +++ /dev/null @@ -1,629 +0,0 @@ -package gocmd - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "regexp" - "strings" - "time" - - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-devops/cmd/qa" - "forge.lthn.ai/core/go-i18n" -) - -// QA command flags - comprehensive options for all agents -var ( - qaFix bool - qaChanged bool - qaAll bool - qaSkip string - qaOnly string - qaCoverage bool - qaThreshold float64 - qaBranchThreshold float64 - qaDocblockThreshold float64 - qaJSON bool - qaVerbose bool - qaQuiet bool - qaTimeout time.Duration - qaShort bool - qaRace bool - qaBench bool - qaFailFast bool - qaMod bool - qaCI bool -) - -func addGoQACommand(parent *cli.Command) { - qaCmd := &cli.Command{ - Use: "qa", - Short: "Run QA checks", - Long: `Run comprehensive code quality checks for Go projects. - -Checks available: fmt, vet, lint, test, race, fuzz, vuln, sec, bench, docblock - -Examples: - core go qa # Default: fmt, lint, test - core go qa --fix # Auto-fix formatting and lint issues - core go qa --only=test # Only run tests - core go qa --skip=vuln,sec # Skip vulnerability and security scans - core go qa --coverage --threshold=80 # Require 80% coverage - core go qa --changed # Only check changed files (git-aware) - core go qa --ci # CI mode: strict, coverage, fail-fast - core go qa --race --short # Quick tests with race detection - core go qa --json # Output results as JSON`, - RunE: runGoQA, - } - - // Fix and modification flags (persistent so subcommands inherit them) - qaCmd.PersistentFlags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible") - qaCmd.PersistentFlags().BoolVar(&qaMod, "mod", false, "Run go mod tidy before checks") - - // Scope flags - qaCmd.PersistentFlags().BoolVar(&qaChanged, "changed", false, "Only check changed files (git-aware)") - qaCmd.PersistentFlags().BoolVar(&qaAll, "all", false, "Check all files (override git-aware)") - qaCmd.PersistentFlags().StringVar(&qaSkip, "skip", "", "Skip checks (comma-separated: fmt,vet,lint,test,race,fuzz,vuln,sec,bench)") - qaCmd.PersistentFlags().StringVar(&qaOnly, "only", "", "Only run these checks (comma-separated)") - - // Coverage flags - qaCmd.PersistentFlags().BoolVar(&qaCoverage, "coverage", false, "Include coverage reporting") - qaCmd.PersistentFlags().BoolVarP(&qaCoverage, "cov", "c", false, "Include coverage reporting (shorthand)") - qaCmd.PersistentFlags().Float64Var(&qaThreshold, "threshold", 0, "Minimum statement coverage threshold (0-100), fail if below") - qaCmd.PersistentFlags().Float64Var(&qaBranchThreshold, "branch-threshold", 0, "Minimum branch coverage threshold (0-100), fail if below") - qaCmd.PersistentFlags().Float64Var(&qaDocblockThreshold, "docblock-threshold", 80, "Minimum docblock coverage threshold (0-100)") - - // Test flags - qaCmd.PersistentFlags().BoolVar(&qaShort, "short", false, "Run tests with -short flag") - qaCmd.PersistentFlags().BoolVar(&qaRace, "race", false, "Include race detection in tests") - qaCmd.PersistentFlags().BoolVar(&qaBench, "bench", false, "Include benchmarks") - - // Output flags - qaCmd.PersistentFlags().BoolVar(&qaJSON, "json", false, "Output results as JSON") - qaCmd.PersistentFlags().BoolVarP(&qaVerbose, "verbose", "v", false, "Show verbose output") - qaCmd.PersistentFlags().BoolVarP(&qaQuiet, "quiet", "q", false, "Only show errors") - - // Control flags - qaCmd.PersistentFlags().DurationVar(&qaTimeout, "timeout", 10*time.Minute, "Timeout for all checks") - qaCmd.PersistentFlags().BoolVar(&qaFailFast, "fail-fast", false, "Stop on first failure") - qaCmd.PersistentFlags().BoolVar(&qaCI, "ci", false, "CI mode: strict checks, coverage required, fail-fast") - - // Preset subcommands for convenience - qaCmd.AddCommand(&cli.Command{ - Use: "quick", - Short: "Quick QA: fmt, vet, lint (no tests)", - RunE: func(cmd *cli.Command, args []string) error { qaOnly = "fmt,vet,lint"; return runGoQA(cmd, args) }, - }) - - qaCmd.AddCommand(&cli.Command{ - Use: "full", - Short: "Full QA: all checks including race, vuln, sec", - RunE: func(cmd *cli.Command, args []string) error { - qaOnly = "fmt,vet,lint,test,race,vuln,sec" - return runGoQA(cmd, args) - }, - }) - - qaCmd.AddCommand(&cli.Command{ - Use: "pre-commit", - Short: "Pre-commit checks: fmt --fix, lint --fix, test --short", - RunE: func(cmd *cli.Command, args []string) error { - qaFix = true - qaShort = true - qaOnly = "fmt,lint,test" - return runGoQA(cmd, args) - }, - }) - - qaCmd.AddCommand(&cli.Command{ - Use: "pr", - Short: "PR checks: full QA with coverage threshold", - RunE: func(cmd *cli.Command, args []string) error { - qaCoverage = true - if qaThreshold == 0 { - qaThreshold = 50 // Default PR threshold - } - qaOnly = "fmt,vet,lint,test" - return runGoQA(cmd, args) - }, - }) - - parent.AddCommand(qaCmd) -} - -// QAResult holds the result of a QA run for JSON output -type QAResult struct { - Success bool `json:"success"` - Duration string `json:"duration"` - Checks []CheckResult `json:"checks"` - Coverage *float64 `json:"coverage,omitempty"` - BranchCoverage *float64 `json:"branch_coverage,omitempty"` - Threshold *float64 `json:"threshold,omitempty"` - BranchThreshold *float64 `json:"branch_threshold,omitempty"` -} - -// CheckResult holds the result of a single check -type CheckResult struct { - Name string `json:"name"` - Passed bool `json:"passed"` - Duration string `json:"duration"` - Error string `json:"error,omitempty"` - Output string `json:"output,omitempty"` - FixHint string `json:"fix_hint,omitempty"` -} - -func runGoQA(cmd *cli.Command, args []string) error { - // Apply CI mode defaults - if qaCI { - qaCoverage = true - qaFailFast = true - if qaThreshold == 0 { - qaThreshold = 50 - } - } - - cwd, err := os.Getwd() - if err != nil { - return cli.Wrap(err, i18n.T("i18n.fail.get", "working directory")) - } - - // Detect if this is a Go project - if _, err := os.Stat("go.mod"); os.IsNotExist(err) { - return cli.Err("not a Go project (no go.mod found)") - } - - // Determine which checks to run - checkNames := determineChecks() - - if !qaJSON && !qaQuiet { - cli.Print("%s %s\n\n", cli.DimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "Go QA")) - } - - // Run go mod tidy if requested - if qaMod { - if !qaQuiet { - cli.Print("%s %s\n", cli.DimStyle.Render("→"), "Running go mod tidy...") - } - modCmd := exec.Command("go", "mod", "tidy") - modCmd.Dir = cwd - if err := modCmd.Run(); err != nil { - return cli.Wrap(err, "go mod tidy failed") - } - } - - ctx, cancel := context.WithTimeout(context.Background(), qaTimeout) - defer cancel() - - startTime := time.Now() - checks := buildChecks(checkNames) - results := make([]CheckResult, 0, len(checks)) - passed := 0 - failed := 0 - - for _, check := range checks { - checkStart := time.Now() - - if !qaJSON && !qaQuiet { - cli.Print("%s %s\n", cli.DimStyle.Render("→"), i18n.Progress(check.Name)) - } - - output, err := runCheckCapture(ctx, cwd, check) - checkDuration := time.Since(checkStart) - - result := CheckResult{ - Name: check.Name, - Duration: checkDuration.Round(time.Millisecond).String(), - } - - if err != nil { - result.Passed = false - result.Error = err.Error() - if qaVerbose { - result.Output = output - } - result.FixHint = fixHintFor(check.Name, output) - failed++ - - if !qaJSON && !qaQuiet { - cli.Print(" %s %s\n", cli.ErrorStyle.Render(cli.Glyph(":cross:")), err.Error()) - if qaVerbose && output != "" { - cli.Text(output) - } - if result.FixHint != "" { - cli.Hint("fix", result.FixHint) - } - } - - if qaFailFast { - results = append(results, result) - break - } - } else { - result.Passed = true - if qaVerbose { - result.Output = output - } - passed++ - - if !qaJSON && !qaQuiet { - cli.Print(" %s %s\n", cli.SuccessStyle.Render(cli.Glyph(":check:")), i18n.T("i18n.done.pass")) - } - } - - results = append(results, result) - } - - // Run coverage if requested - var coverageVal *float64 - var branchVal *float64 - if qaCoverage && !qaFailFast || (qaCoverage && failed == 0) { - cov, branch, err := runCoverage(ctx, cwd) - if err == nil { - coverageVal = &cov - branchVal = &branch - if !qaJSON && !qaQuiet { - cli.Print("\n%s %.1f%%\n", cli.DimStyle.Render("Statement Coverage:"), cov) - cli.Print("%s %.1f%%\n", cli.DimStyle.Render("Branch Coverage:"), branch) - } - if qaThreshold > 0 && cov < qaThreshold { - failed++ - if !qaJSON && !qaQuiet { - cli.Print(" %s Statement coverage %.1f%% below threshold %.1f%%\n", - cli.ErrorStyle.Render(cli.Glyph(":cross:")), cov, qaThreshold) - } - } - if qaBranchThreshold > 0 && branch < qaBranchThreshold { - failed++ - if !qaJSON && !qaQuiet { - cli.Print(" %s Branch coverage %.1f%% below threshold %.1f%%\n", - cli.ErrorStyle.Render(cli.Glyph(":cross:")), branch, qaBranchThreshold) - } - } - - if failed > 0 && !qaJSON && !qaQuiet { - cli.Hint("fix", "Run 'core go cov --open' to see uncovered lines, then add tests.") - } - } - } - - duration := time.Since(startTime).Round(time.Millisecond) - - // JSON output - if qaJSON { - qaResult := QAResult{ - Success: failed == 0, - Duration: duration.String(), - Checks: results, - Coverage: coverageVal, - BranchCoverage: branchVal, - } - if qaThreshold > 0 { - qaResult.Threshold = &qaThreshold - } - if qaBranchThreshold > 0 { - qaResult.BranchThreshold = &qaBranchThreshold - } - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(qaResult) - } - - // Summary - if !qaQuiet { - cli.Blank() - if failed > 0 { - cli.Print("%s %s, %s (%s)\n", - cli.ErrorStyle.Render(cli.Glyph(":cross:")), - i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), - i18n.T("i18n.count.check", failed)+" "+i18n.T("i18n.done.fail"), - duration) - } else { - cli.Print("%s %s (%s)\n", - cli.SuccessStyle.Render(cli.Glyph(":check:")), - i18n.T("i18n.count.check", passed)+" "+i18n.T("i18n.done.pass"), - duration) - } - } - - if failed > 0 { - return cli.Err("QA checks failed: %d passed, %d failed", passed, failed) - } - return nil -} - -func determineChecks() []string { - // If --only is specified, use those - if qaOnly != "" { - return strings.Split(qaOnly, ",") - } - - // Default checks - checks := []string{"fmt", "lint", "test", "fuzz", "docblock"} - - // Add race if requested - if qaRace { - // Replace test with race (which includes test) - for i, c := range checks { - if c == "test" { - checks[i] = "race" - break - } - } - } - - // Add bench if requested - if qaBench { - checks = append(checks, "bench") - } - - // Remove skipped checks - if qaSkip != "" { - skipMap := make(map[string]bool) - for _, s := range strings.Split(qaSkip, ",") { - skipMap[strings.TrimSpace(s)] = true - } - filtered := make([]string, 0, len(checks)) - for _, c := range checks { - if !skipMap[c] { - filtered = append(filtered, c) - } - } - checks = filtered - } - - return checks -} - -// QACheck represents a single QA check. -type QACheck struct { - Name string - Command string - Args []string -} - -func buildChecks(names []string) []QACheck { - var checks []QACheck - for _, name := range names { - name = strings.TrimSpace(name) - check := buildCheck(name) - if check.Command != "" { - checks = append(checks, check) - } - } - return checks -} - -func buildCheck(name string) QACheck { - switch name { - case "fmt", "format": - args := []string{"-l", "."} - if qaFix { - args = []string{"-w", "."} - } - return QACheck{Name: "format", Command: "gofmt", Args: args} - - case "vet": - return QACheck{Name: "vet", Command: "go", Args: []string{"vet", "./..."}} - - case "lint": - args := []string{"run"} - if qaFix { - args = append(args, "--fix") - } - if qaChanged && !qaAll { - args = append(args, "--new-from-rev=HEAD") - } - args = append(args, "./...") - return QACheck{Name: "lint", Command: "golangci-lint", Args: args} - - case "test": - args := []string{"test"} - if qaShort { - args = append(args, "-short") - } - if qaVerbose { - args = append(args, "-v") - } - args = append(args, "./...") - return QACheck{Name: "test", Command: "go", Args: args} - - case "race": - args := []string{"test", "-race"} - if qaShort { - args = append(args, "-short") - } - if qaVerbose { - args = append(args, "-v") - } - args = append(args, "./...") - return QACheck{Name: "race", Command: "go", Args: args} - - case "bench": - args := []string{"test", "-bench=.", "-benchmem", "-run=^$"} - args = append(args, "./...") - return QACheck{Name: "bench", Command: "go", Args: args} - - case "vuln": - return QACheck{Name: "vuln", Command: "govulncheck", Args: []string{"./..."}} - - case "sec": - return QACheck{Name: "sec", Command: "gosec", Args: []string{"-quiet", "./..."}} - - case "fuzz": - return QACheck{Name: "fuzz", Command: "_internal_"} - - case "docblock": - // Special internal check - handled separately - return QACheck{Name: "docblock", Command: "_internal_"} - - default: - return QACheck{} - } -} - -// fixHintFor returns an actionable fix instruction for a given check failure. -func fixHintFor(checkName, output string) string { - switch checkName { - case "format", "fmt": - return "Run 'core go qa fmt --fix' to auto-format." - case "vet": - return "Fix the issues reported by go vet — typically genuine bugs." - case "lint": - return "Run 'core go qa lint --fix' for auto-fixable issues." - case "test": - if name := extractFailingTest(output); name != "" { - return fmt.Sprintf("Run 'go test -run %s -v ./...' to debug.", name) - } - return "Run 'go test -run -v ./path/' to debug." - case "race": - return "Data race detected. Add mutex, channel, or atomic to synchronise shared state." - case "bench": - return "Benchmark regression. Run 'go test -bench=. -benchmem' to reproduce." - case "vuln": - return "Run 'govulncheck ./...' for details. Update affected deps with 'go get -u'." - case "sec": - return "Review gosec findings. Common fixes: validate inputs, parameterised queries." - case "fuzz": - return "Add a regression test for the crashing input in testdata/fuzz//." - case "docblock": - return "Add doc comments to exported symbols: '// Name does X.' before each declaration." - default: - return "" - } -} - -var failTestRe = regexp.MustCompile(`--- FAIL: (\w+)`) - -// extractFailingTest parses the first failing test name from go test output. -func extractFailingTest(output string) string { - if m := failTestRe.FindStringSubmatch(output); len(m) > 1 { - return m[1] - } - return "" -} - -func runCheckCapture(ctx context.Context, dir string, check QACheck) (string, error) { - // Handle internal checks - if check.Command == "_internal_" { - return runInternalCheck(check) - } - - // Check if command exists - if _, err := exec.LookPath(check.Command); err != nil { - return "", cli.Err("%s: not installed", check.Command) - } - - cmd := exec.CommandContext(ctx, check.Command, check.Args...) - cmd.Dir = dir - - // For gofmt -l, capture output to check if files need formatting - if check.Name == "format" && len(check.Args) > 0 && check.Args[0] == "-l" { - output, err := cmd.Output() - if err != nil { - return string(output), err - } - if len(output) > 0 { - // Show files that need formatting - if !qaQuiet && !qaJSON { - cli.Text(string(output)) - } - return string(output), cli.Err("files need formatting (use --fix)") - } - return "", nil - } - - // For other commands, stream or capture based on quiet mode - if qaQuiet || qaJSON { - output, err := cmd.CombinedOutput() - return string(output), err - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return "", cmd.Run() -} - -func runCoverage(ctx context.Context, dir string) (float64, float64, error) { - // Create temp file for coverage data - covFile, err := os.CreateTemp("", "coverage-*.out") - if err != nil { - return 0, 0, err - } - covPath := covFile.Name() - _ = covFile.Close() - defer os.Remove(covPath) - - args := []string{"test", "-cover", "-covermode=atomic", "-coverprofile=" + covPath} - if qaShort { - args = append(args, "-short") - } - args = append(args, "./...") - - cmd := exec.CommandContext(ctx, "go", args...) - cmd.Dir = dir - if !qaQuiet && !qaJSON { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - - if err := cmd.Run(); err != nil { - return 0, 0, err - } - - // Parse statement coverage - coverCmd := exec.CommandContext(ctx, "go", "tool", "cover", "-func="+covPath) - output, err := coverCmd.Output() - if err != nil { - return 0, 0, err - } - - // Parse last line for total coverage - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - var statementPct float64 - if len(lines) > 0 { - lastLine := lines[len(lines)-1] - fields := strings.Fields(lastLine) - if len(fields) >= 3 { - // Parse percentage (e.g., "45.6%") - pctStr := strings.TrimSuffix(fields[len(fields)-1], "%") - _, _ = fmt.Sscanf(pctStr, "%f", &statementPct) - } - } - - // Parse branch coverage - branchPct, err := calculateBlockCoverage(covPath) - if err != nil { - return statementPct, 0, err - } - - return statementPct, branchPct, nil -} - -// runInternalCheck runs internal Go-based checks (not external commands). -func runInternalCheck(check QACheck) (string, error) { - switch check.Name { - case "fuzz": - // Short burst fuzz in QA (3s per target) - duration := 3 * time.Second - if qaTimeout > 0 && qaTimeout < 30*time.Second { - duration = 2 * time.Second - } - return "", runGoFuzz(duration, "", "", qaVerbose) - - case "docblock": - result, err := qa.CheckDocblockCoverage([]string{"./..."}) - if err != nil { - return "", err - } - if result.Coverage < qaDocblockThreshold { - return "", cli.Err("docblock coverage %.1f%% below threshold %.1f%%", result.Coverage, qaDocblockThreshold) - } - return fmt.Sprintf("docblock coverage: %.1f%% (%d/%d)", result.Coverage, result.Documented, result.Total), nil - - default: - return "", cli.Err("unknown internal check: %s", check.Name) - } -} diff --git a/cmd/gocmd/cmd_tools.go b/cmd/gocmd/cmd_tools.go deleted file mode 100644 index cfa7981..0000000 --- a/cmd/gocmd/cmd_tools.go +++ /dev/null @@ -1,236 +0,0 @@ -package gocmd - -import ( - "errors" - "os" - "os/exec" - "path/filepath" - - "forge.lthn.ai/core/cli/pkg/cli" - "forge.lthn.ai/core/go-i18n" -) - -var ( - installVerbose bool - installNoCgo bool -) - -func addGoInstallCommand(parent *cli.Command) { - installCmd := &cli.Command{ - Use: "install [path]", - Short: "Install Go binary", - Long: "Install Go binary to $GOPATH/bin", - RunE: func(cmd *cli.Command, args []string) error { - // Get install path from args or default to current dir - installPath := "./..." - if len(args) > 0 { - installPath = args[0] - } - - // Detect if we're in a module with cmd/ subdirectories or a root main.go - if installPath == "./..." { - if _, err := os.Stat("core.go"); err == nil { - installPath = "." - } else if entries, err := os.ReadDir("cmd"); err == nil && len(entries) > 0 { - installPath = "./cmd/..." - } else if _, err := os.Stat("main.go"); err == nil { - installPath = "." - } - } - - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("install")), i18n.Progress("install")) - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("path")), installPath) - if installNoCgo { - cli.Print(" %s %s\n", dimStyle.Render(i18n.Label("cgo")), "disabled") - } - - cmdArgs := []string{"install"} - if installVerbose { - cmdArgs = append(cmdArgs, "-v") - } - cmdArgs = append(cmdArgs, installPath) - - execCmd := exec.Command("go", cmdArgs...) - if installNoCgo { - execCmd.Env = append(os.Environ(), "CGO_ENABLED=0") - } - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - - if err := execCmd.Run(); err != nil { - cli.Print("\n%s\n", errorStyle.Render(i18n.T("i18n.fail.install", "binary"))) - return err - } - - // Show where it was installed - gopath := os.Getenv("GOPATH") - if gopath == "" { - home, _ := os.UserHomeDir() - gopath = filepath.Join(home, "go") - } - binDir := filepath.Join(gopath, "bin") - - cli.Print("\n%s %s\n", successStyle.Render(i18n.T("i18n.done.install")), binDir) - return nil - }, - } - - installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, "Verbose output") - installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, "Disable CGO") - - parent.AddCommand(installCmd) -} - -func addGoModCommand(parent *cli.Command) { - modCmd := &cli.Command{ - Use: "mod", - Short: "Module management", - Long: "Go module management commands", - } - - // tidy - tidyCmd := &cli.Command{ - Use: "tidy", - Short: "Run go mod tidy", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "mod", "tidy") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - // download - downloadCmd := &cli.Command{ - Use: "download", - Short: "Download module dependencies", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "mod", "download") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - // verify - verifyCmd := &cli.Command{ - Use: "verify", - Short: "Verify module checksums", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "mod", "verify") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - // graph - graphCmd := &cli.Command{ - Use: "graph", - Short: "Print module dependency graph", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "mod", "graph") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - modCmd.AddCommand(tidyCmd) - modCmd.AddCommand(downloadCmd) - modCmd.AddCommand(verifyCmd) - modCmd.AddCommand(graphCmd) - parent.AddCommand(modCmd) -} - -func addGoWorkCommand(parent *cli.Command) { - workCmd := &cli.Command{ - Use: "work", - Short: "Workspace management", - Long: "Go workspace management commands", - } - - // sync - syncCmd := &cli.Command{ - Use: "sync", - Short: "Sync workspace modules", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "work", "sync") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - // init - initCmd := &cli.Command{ - Use: "init", - Short: "Initialise a new workspace", - RunE: func(cmd *cli.Command, args []string) error { - execCmd := exec.Command("go", "work", "init") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - if err := execCmd.Run(); err != nil { - return err - } - // Auto-add current module if go.mod exists - if _, err := os.Stat("go.mod"); err == nil { - execCmd = exec.Command("go", "work", "use", ".") - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - } - return nil - }, - } - - // use - useCmd := &cli.Command{ - Use: "use [modules...]", - Short: "Add modules to workspace", - RunE: func(cmd *cli.Command, args []string) error { - if len(args) == 0 { - // Auto-detect modules - modules := findGoModules(".") - if len(modules) == 0 { - return errors.New("no Go modules found") - } - for _, mod := range modules { - execCmd := exec.Command("go", "work", "use", mod) - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - if err := execCmd.Run(); err != nil { - return err - } - cli.Print("%s %s\n", successStyle.Render(i18n.T("i18n.done.add")), mod) - } - return nil - } - - cmdArgs := append([]string{"work", "use"}, args...) - execCmd := exec.Command("go", cmdArgs...) - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - return execCmd.Run() - }, - } - - workCmd.AddCommand(syncCmd) - workCmd.AddCommand(initCmd) - workCmd.AddCommand(useCmd) - parent.AddCommand(workCmd) -} - -func findGoModules(root string) []string { - var modules []string - _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.Name() == "go.mod" && path != "go.mod" { - modules = append(modules, filepath.Dir(path)) - } - return nil - }) - return modules -} diff --git a/cmd/gocmd/coverage_test.go b/cmd/gocmd/coverage_test.go deleted file mode 100644 index f48c6a9..0000000 --- a/cmd/gocmd/coverage_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package gocmd - -import ( - "os" - "testing" - - "forge.lthn.ai/core/cli/pkg/cli" - "github.com/stretchr/testify/assert" -) - -func TestCalculateBlockCoverage(t *testing.T) { - // Create a dummy coverage profile - content := `mode: set -forge.lthn.ai/core/go/pkg/foo.go:1.2,3.4 5 1 -forge.lthn.ai/core/go/pkg/foo.go:5.6,7.8 2 0 -forge.lthn.ai/core/go/pkg/bar.go:10.1,12.20 10 5 -` - tmpfile, err := os.CreateTemp("", "test-coverage-*.out") - assert.NoError(t, err) - defer os.Remove(tmpfile.Name()) - - _, err = tmpfile.Write([]byte(content)) - assert.NoError(t, err) - err = tmpfile.Close() - assert.NoError(t, err) - - // Test calculation - // 3 blocks total, 2 covered (count > 0) - // Expect (2/3) * 100 = 66.666... - pct, err := calculateBlockCoverage(tmpfile.Name()) - assert.NoError(t, err) - assert.InDelta(t, 66.67, pct, 0.01) - - // Test empty file (only header) - contentEmpty := "mode: atomic\n" - tmpfileEmpty, _ := os.CreateTemp("", "test-coverage-empty-*.out") - defer os.Remove(tmpfileEmpty.Name()) - tmpfileEmpty.Write([]byte(contentEmpty)) - tmpfileEmpty.Close() - - pct, err = calculateBlockCoverage(tmpfileEmpty.Name()) - assert.NoError(t, err) - assert.Equal(t, 0.0, pct) - - // Test non-existent file - pct, err = calculateBlockCoverage("non-existent-file") - assert.Error(t, err) - assert.Equal(t, 0.0, pct) - - // Test malformed file - contentMalformed := `mode: set -forge.lthn.ai/core/go/pkg/foo.go:1.2,3.4 5 -forge.lthn.ai/core/go/pkg/foo.go:1.2,3.4 5 notanumber -` - tmpfileMalformed, _ := os.CreateTemp("", "test-coverage-malformed-*.out") - defer os.Remove(tmpfileMalformed.Name()) - tmpfileMalformed.Write([]byte(contentMalformed)) - tmpfileMalformed.Close() - - pct, err = calculateBlockCoverage(tmpfileMalformed.Name()) - assert.NoError(t, err) - assert.Equal(t, 0.0, pct) - - // Test malformed file - missing fields - contentMalformed2 := `mode: set -forge.lthn.ai/core/go/pkg/foo.go:1.2,3.4 5 -` - tmpfileMalformed2, _ := os.CreateTemp("", "test-coverage-malformed2-*.out") - defer os.Remove(tmpfileMalformed2.Name()) - tmpfileMalformed2.Write([]byte(contentMalformed2)) - tmpfileMalformed2.Close() - - pct, err = calculateBlockCoverage(tmpfileMalformed2.Name()) - assert.NoError(t, err) - assert.Equal(t, 0.0, pct) - - // Test completely empty file - tmpfileEmpty2, _ := os.CreateTemp("", "test-coverage-empty2-*.out") - defer os.Remove(tmpfileEmpty2.Name()) - tmpfileEmpty2.Close() - pct, err = calculateBlockCoverage(tmpfileEmpty2.Name()) - assert.NoError(t, err) - assert.Equal(t, 0.0, pct) -} - -func TestParseOverallCoverage(t *testing.T) { - output := `ok forge.lthn.ai/core/go/pkg/foo 0.100s coverage: 50.0% of statements -ok forge.lthn.ai/core/go/pkg/bar 0.200s coverage: 100.0% of statements -` - pct := parseOverallCoverage(output) - assert.Equal(t, 75.0, pct) - - outputNoCov := "ok forge.lthn.ai/core/go/pkg/foo 0.100s" - pct = parseOverallCoverage(outputNoCov) - assert.Equal(t, 0.0, pct) -} - -func TestFormatCoverage(t *testing.T) { - assert.Contains(t, formatCoverage(85.0), "85.0%") - assert.Contains(t, formatCoverage(65.0), "65.0%") - assert.Contains(t, formatCoverage(25.0), "25.0%") -} - -func TestAddGoCovCommand(t *testing.T) { - cmd := &cli.Command{Use: "test"} - addGoCovCommand(cmd) - assert.True(t, cmd.HasSubCommands()) - sub := cmd.Commands()[0] - assert.Equal(t, "cov", sub.Name()) -} - -func TestAddGoQACommand(t *testing.T) { - cmd := &cli.Command{Use: "test"} - addGoQACommand(cmd) - assert.True(t, cmd.HasSubCommands()) - sub := cmd.Commands()[0] - assert.Equal(t, "qa", sub.Name()) -} - -func TestDetermineChecks(t *testing.T) { - // Default checks - qaOnly = "" - qaSkip = "" - qaRace = false - qaBench = false - checks := determineChecks() - assert.Contains(t, checks, "fmt") - assert.Contains(t, checks, "test") - - // Only - qaOnly = "fmt,lint" - checks = determineChecks() - assert.Equal(t, []string{"fmt", "lint"}, checks) - - // Skip - qaOnly = "" - qaSkip = "fmt,lint" - checks = determineChecks() - assert.NotContains(t, checks, "fmt") - assert.NotContains(t, checks, "lint") - assert.Contains(t, checks, "test") - - // Race - qaSkip = "" - qaRace = true - checks = determineChecks() - assert.Contains(t, checks, "race") - assert.NotContains(t, checks, "test") - - // Reset - qaRace = false -} - -func TestBuildCheck(t *testing.T) { - qaFix = false - c := buildCheck("fmt") - assert.Equal(t, "format", c.Name) - assert.Equal(t, []string{"-l", "."}, c.Args) - - qaFix = true - c = buildCheck("fmt") - assert.Equal(t, []string{"-w", "."}, c.Args) - - c = buildCheck("vet") - assert.Equal(t, "vet", c.Name) - - c = buildCheck("lint") - assert.Equal(t, "lint", c.Name) - - c = buildCheck("test") - assert.Equal(t, "test", c.Name) - - c = buildCheck("race") - assert.Equal(t, "race", c.Name) - - c = buildCheck("bench") - assert.Equal(t, "bench", c.Name) - - c = buildCheck("vuln") - assert.Equal(t, "vuln", c.Name) - - c = buildCheck("sec") - assert.Equal(t, "sec", c.Name) - - c = buildCheck("fuzz") - assert.Equal(t, "fuzz", c.Name) - - c = buildCheck("docblock") - assert.Equal(t, "docblock", c.Name) - - c = buildCheck("unknown") - assert.Equal(t, "", c.Name) -} - -func TestBuildChecks(t *testing.T) { - checks := buildChecks([]string{"fmt", "vet", "unknown"}) - assert.Equal(t, 2, len(checks)) - assert.Equal(t, "format", checks[0].Name) - assert.Equal(t, "vet", checks[1].Name) -} - -func TestFixHintFor(t *testing.T) { - assert.Contains(t, fixHintFor("format", ""), "core go qa fmt --fix") - assert.Contains(t, fixHintFor("vet", ""), "go vet") - assert.Contains(t, fixHintFor("lint", ""), "core go qa lint --fix") - assert.Contains(t, fixHintFor("test", "--- FAIL: TestFoo"), "TestFoo") - assert.Contains(t, fixHintFor("race", ""), "Data race") - assert.Contains(t, fixHintFor("bench", ""), "Benchmark regression") - assert.Contains(t, fixHintFor("vuln", ""), "govulncheck") - assert.Contains(t, fixHintFor("sec", ""), "gosec") - assert.Contains(t, fixHintFor("fuzz", ""), "crashing input") - assert.Contains(t, fixHintFor("docblock", ""), "doc comments") - assert.Equal(t, "", fixHintFor("unknown", "")) -} - -func TestRunGoQA_NoGoMod(t *testing.T) { - // runGoQA should fail if go.mod is not present in CWD - // We run it in a temp dir without go.mod - tmpDir, _ := os.MkdirTemp("", "test-qa-*") - defer os.RemoveAll(tmpDir) - cwd, _ := os.Getwd() - os.Chdir(tmpDir) - defer os.Chdir(cwd) - - cmd := &cli.Command{Use: "qa"} - err := runGoQA(cmd, []string{}) - assert.Error(t, err) - assert.Contains(t, err.Error(), "no go.mod found") -} diff --git a/go.mod b/go.mod index 3cb553f..e7d2a4e 100644 --- a/go.mod +++ b/go.mod @@ -3,65 +3,13 @@ module forge.lthn.ai/core/go go 1.26.0 require ( - forge.lthn.ai/core/cli v0.1.1 - forge.lthn.ai/core/go-crypt v0.1.2 - forge.lthn.ai/core/go-devops v0.1.2 - forge.lthn.ai/core/go-i18n v0.1.1 - forge.lthn.ai/core/go-io v0.0.4 + forge.lthn.ai/core/go-io v0.0.5 forge.lthn.ai/core/go-log v0.0.1 - github.com/gorilla/websocket v1.5.3 - github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.46.1 ) require ( - forge.lthn.ai/core/go-inference v0.1.1 // indirect - github.com/ProtonMail/go-crypto v1.4.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v1.3.10 // indirect - github.com/charmbracelet/colorprofile v0.4.2 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect - modernc.org/libc v1.68.0 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9013624..57f3230 100644 --- a/go.sum +++ b/go.sum @@ -1,172 +1,21 @@ -forge.lthn.ai/core/cli v0.1.1 h1:AEefSo0ydYV1bZAbUgxsg1mi/llnC3+jqkjR/PyGdj4= -forge.lthn.ai/core/cli v0.1.1/go.mod h1:gST3hY7vyrnlpLYtkRAQU2FyPxJBxLD1xa4+/KPOhn8= -forge.lthn.ai/core/go-crypt v0.1.2 h1:MpVOX9wu0pBTw2+qsExZy2J5n6lo1LjgwrOMQmHTKgc= -forge.lthn.ai/core/go-crypt v0.1.2/go.mod h1:1nD3bQ2NyK5iM2aCd+mi/+TTWwHEp+P/qf9tXLAUPuw= -forge.lthn.ai/core/go-devops v0.1.2 h1:H3MgGxnfoydZVFZU2ZxvkIbmPMiKmAfUuGOohkbyQBc= -forge.lthn.ai/core/go-devops v0.1.2/go.mod h1:48QM3Qv94NbcGF0Y16k7Z8o/wCQXxKwNTrU3F3qUMlQ= -forge.lthn.ai/core/go-i18n v0.1.0 h1:F7JVSoVkZtzx9JfhpntM9z3iQm1vnuMUi/Zklhz8PCI= -forge.lthn.ai/core/go-i18n v0.1.0/go.mod h1:Q4xsrxuNCl/6NfMv1daria7t1RSiyy8ml+6jiPtUcBs= -forge.lthn.ai/core/go-i18n v0.1.1 h1:wxKLPAdITSqcdOqzgwb3yzUgMLdOFi3E5LdV9OBi7eg= -forge.lthn.ai/core/go-i18n v0.1.1/go.mod h1:AGdDRA+Bo67FsU2XGpZxHIGEo6sfos41k0zHoCJ6j4c= -forge.lthn.ai/core/go-inference v0.0.2 h1:aHjBkYyLKxLr9tbO4AvzzV/lsZueGq/jeo33SLh113k= -forge.lthn.ai/core/go-inference v0.0.2/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-inference v0.1.1 h1:uM3dtWitE4vvSCwA6CNPA2l0BRAjUNelENh7z58aecU= -forge.lthn.ai/core/go-inference v0.1.1/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-io v0.0.3 h1:TlhYpGTyjPgAlbEHyYrVSeUChZPhJXcLZ7D/8IbFqfI= -forge.lthn.ai/core/go-io v0.0.3/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0= -forge.lthn.ai/core/go-io v0.0.4 h1:vXs3JTWquZKKG48Tik54DlzqP0WRJD9rnpn/D0GlRDk= -forge.lthn.ai/core/go-io v0.0.4/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0= +forge.lthn.ai/core/go-io v0.0.5 h1:oSyngKTkB1gR5fEWYKXftTg9FxwnpddSiCq2dlwfImE= +forge.lthn.ai/core/go-io v0.0.5/go.mod h1:ZlU9OQpsvNFNmTJoaHbFIkisZyc0eCq0p8znVWQLRf0= forge.lthn.ai/core/go-log v0.0.1 h1:x/E6EfF9vixzqiLHQOl2KT25HyBcMc9qiBkomqVlpPg= forge.lthn.ai/core/go-log v0.0.1/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= -github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= -github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= -github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= -github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so= -modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= -modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=