From 2d2b63af394f0f8334e04852f2288d11d8ea7291 Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 29 Jan 2026 12:22:01 +0000 Subject: [PATCH] feat(cli): add core go command group Add Go development tools under `core go`: - test: Run tests with coverage (CGO_ENABLED=0) - fmt: Format code with goimports/gofmt - lint: Run golangci-lint - mod: Module management (tidy, download, verify, graph) - work: Workspace management (sync, init, use) Update SKILL.md with Go Development section. Keep `core test` at root for backward compatibility. Co-Authored-By: Claude Opus 4.5 --- .claude/skills/core/SKILL.md | 158 ++++++++++----- cmd/core/cmd/go.go | 380 +++++++++++++++++++++++++++++++++++ cmd/core/cmd/root.go | 1 + 3 files changed, 490 insertions(+), 49 deletions(-) create mode 100644 cmd/core/cmd/go.go diff --git a/.claude/skills/core/SKILL.md b/.claude/skills/core/SKILL.md index de11e87..f7a096c 100644 --- a/.claude/skills/core/SKILL.md +++ b/.claude/skills/core/SKILL.md @@ -13,8 +13,12 @@ The `core` command provides a unified interface for Go/Wails development, multi- | Task | Command | Notes | |------|---------|-------| -| Run Go tests | `core test` | Sets macOS deployment target, filters warnings | -| Run Go tests with coverage | `core test --coverage` | Per-package breakdown | +| Run Go tests | `core go test` | Sets macOS deployment target, filters warnings | +| Run Go tests with coverage | `core go test --coverage` | Per-package breakdown | +| Format Go code | `core go fmt --fix` | Uses goimports/gofmt | +| Lint Go code | `core go lint` | Uses golangci-lint | +| Tidy Go modules | `core go mod tidy` | go mod tidy wrapper | +| Sync Go workspace | `core go work sync` | go work sync wrapper | | Run PHP tests | `core php test` | Auto-detects Pest/PHPUnit | | Start PHP dev server | `core php dev` | FrankenPHP + Vite + Horizon + Reverb | | Format PHP code | `core php fmt --fix` | Laravel Pint | @@ -38,51 +42,6 @@ The `core` command provides a unified interface for Go/Wails development, multi- | Update packages | `core pkg update` | Pull latest for all packages | | Run VM | `core vm run ` | Run LinuxKit VM | -## Testing - -**Always use `core test` instead of `go test`.** - -```bash -# Run all tests with coverage summary -core test - -# Detailed per-package coverage -core test --coverage - -# Test specific packages -core test --pkg ./pkg/crypt - -# Run specific tests -core test --run TestHash -core test --run "Test.*Good" - -# Skip integration tests -core test --short - -# Race detection -core test --race - -# JSON output for CI/parsing -core test --json -``` - -**Why:** Sets `MACOSX_DEPLOYMENT_TARGET=26.0` to suppress linker warnings, filters noise from output, provides colour-coded coverage. - -### JSON Output - -For programmatic use: - -```json -{ - "passed": 14, - "failed": 0, - "skipped": 0, - "coverage": 75.1, - "exit_code": 0, - "failed_packages": [] -} -``` - ## Building **Always use `core build` instead of `go build`.** @@ -218,6 +177,102 @@ core pkg update core-api # Update specific package core pkg outdated ``` +## Go Development + +**Always use `core go` commands instead of raw go commands.** + +### Quick Reference + +| Task | Command | Notes | +|------|---------|-------| +| Run tests | `core go test` | CGO_ENABLED=0, filters warnings | +| Run tests with coverage | `core go test --coverage` | Per-package breakdown | +| Format code | `core go fmt --fix` | Uses goimports if available | +| Lint code | `core go lint` | Uses golangci-lint | +| Tidy modules | `core go mod tidy` | go mod tidy | +| Sync workspace | `core go work sync` | go work sync | + +### Testing + +```bash +# Run all tests +core go test + +# With coverage +core go test --coverage + +# Specific package +core go test --pkg ./pkg/errors + +# Run specific tests +core go test --run TestHash + +# Short tests only +core go test --short + +# Race detection +core go test --race + +# JSON output for CI +core go test --json + +# Verbose +core go test -v +``` + +**Why:** Sets `CGO_ENABLED=0` and `MACOSX_DEPLOYMENT_TARGET=26.0`, filters linker warnings, provides colour-coded coverage. + +### Formatting & Linting + +```bash +# Check formatting +core go fmt + +# Fix formatting +core go fmt --fix + +# Show diff +core go fmt --diff + +# Run linter +core go lint + +# Lint with auto-fix +core go lint --fix +``` + +### Module Management + +```bash +# Tidy go.mod +core go mod tidy + +# Download dependencies +core go mod download + +# Verify dependencies +core go mod verify + +# Show dependency graph +core go mod graph +``` + +### Workspace Management + +```bash +# Sync workspace +core go work sync + +# Initialize workspace +core go work init + +# Add module to workspace +core go work use ./pkg/mymodule + +# Auto-add all modules +core go work use +``` + ## PHP Development **Always use `core php` commands instead of raw artisan/composer/phpunit.** @@ -416,7 +471,10 @@ core vm templates vars # Show template variables ``` Go project? - └── Run tests: core test [--coverage] + └── Run tests: core go test [--coverage] + └── Format: core go fmt --fix + └── Lint: core go lint + └── Tidy modules: core go mod tidy └── Build: core build [--targets ] └── Release: core release @@ -454,7 +512,9 @@ Managing packages? | Wrong | Right | Why | |-------|-------|-----| -| `go test ./...` | `core test` | Missing deployment target, noisy output | +| `go test ./...` | `core go test` | CGO disabled, filters warnings, coverage | +| `go fmt ./...` | `core go fmt --fix` | Uses goimports, consistent | +| `golangci-lint run` | `core go lint` | Consistent interface | | `go build` | `core build` | Missing cross-compile, signing, checksums | | `php artisan serve` | `core php dev` | Missing Vite, Horizon, Reverb, Redis | | `./vendor/bin/pest` | `core php test` | Inconsistent invocation | diff --git a/cmd/core/cmd/go.go b/cmd/core/cmd/go.go new file mode 100644 index 0000000..8f10afa --- /dev/null +++ b/cmd/core/cmd/go.go @@ -0,0 +1,380 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/leaanthony/clir" +) + +// AddGoCommands adds Go development commands. +func AddGoCommands(parent *clir.Cli) { + goCmd := parent.NewSubCommand("go", "Go development tools") + goCmd.LongDescription("Go development tools with enhanced output and environment setup.\n\n" + + "Commands:\n" + + " test Run tests with coverage\n" + + " fmt Format Go code\n" + + " lint Run golangci-lint\n" + + " mod Module management (tidy, download, verify)\n" + + " work Workspace management") + + addGoTestCommand(goCmd) + addGoFmtCommand(goCmd) + addGoLintCommand(goCmd) + addGoModCommand(goCmd) + addGoWorkCommand(goCmd) +} + +func addGoTestCommand(parent *clir.Command) { + var ( + coverage bool + pkg string + run string + short bool + race bool + json bool + verbose bool + ) + + testCmd := parent.NewSubCommand("test", "Run tests with coverage") + testCmd.LongDescription("Run Go tests with coverage reporting.\n\n" + + "Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" + + "Filters noisy output and provides colour-coded coverage.\n\n" + + "Examples:\n" + + " core go test\n" + + " core go test --coverage\n" + + " core go test --pkg ./pkg/crypt\n" + + " core go test --run TestHash") + + testCmd.BoolFlag("coverage", "Show detailed per-package coverage", &coverage) + testCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) + testCmd.StringFlag("run", "Run only tests matching regexp", &run) + testCmd.BoolFlag("short", "Run only short tests", &short) + testCmd.BoolFlag("race", "Enable race detector", &race) + testCmd.BoolFlag("json", "Output JSON results", &json) + testCmd.BoolFlag("v", "Verbose output", &verbose) + + testCmd.Action(func() error { + return runGoTest(coverage, pkg, run, short, race, json, verbose) + }) +} + +func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose bool) error { + if pkg == "" { + pkg = "./..." + } + + args := []string{"test"} + + if coverage { + args = append(args, "-cover") + } else { + args = append(args, "-cover") + } + + 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 { + fmt.Printf("%s Running tests\n", dimStyle.Render("Test:")) + fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), pkg) + fmt.Println() + } + + 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 { + fmt.Printf(`{"passed":%d,"failed":%d,"skipped":%d,"coverage":%.1f,"exit_code":%d}`, + passed, failed, skipped, cov, cmd.ProcessState.ExitCode()) + fmt.Println() + return err + } + + // Print filtered output if verbose or failed + if verbose || err != nil { + fmt.Println(outputStr) + } + + // Summary + if err == nil { + fmt.Printf(" %s %d passed\n", successStyle.Render("✓"), passed) + } else { + fmt.Printf(" %s %d passed, %d failed\n", errorStyle.Render("✗"), passed, failed) + } + + if cov > 0 { + covStyle := successStyle + if cov < 50 { + covStyle = errorStyle + } else if cov < 80 { + covStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")) + } + fmt.Printf("\n %s %s\n", dimStyle.Render("Coverage:"), covStyle.Render(fmt.Sprintf("%.1f%%", cov))) + } + + if err == nil { + fmt.Printf("\n%s\n", successStyle.Render("PASS All tests passed")) + } else { + fmt.Printf("\n%s\n", errorStyle.Render("FAIL Some tests failed")) + } + + 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)) +} + +func addGoFmtCommand(parent *clir.Command) { + var ( + fix bool + diff bool + check bool + ) + + fmtCmd := parent.NewSubCommand("fmt", "Format Go code") + fmtCmd.LongDescription("Format Go code using gofmt or goimports.\n\n" + + "Examples:\n" + + " core go fmt # Check formatting\n" + + " core go fmt --fix # Fix formatting\n" + + " core go fmt --diff # Show diff") + + fmtCmd.BoolFlag("fix", "Fix formatting in place", &fix) + fmtCmd.BoolFlag("diff", "Show diff of changes", &diff) + fmtCmd.BoolFlag("check", "Check only, exit 1 if not formatted", &check) + + fmtCmd.Action(func() error { + args := []string{} + if fix { + args = append(args, "-w") + } + if diff { + args = append(args, "-d") + } + if !fix && !diff { + args = append(args, "-l") + } + args = append(args, ".") + + // Try goimports first, fall back to gofmt + var cmd *exec.Cmd + if _, err := exec.LookPath("goimports"); err == nil { + cmd = exec.Command("goimports", args...) + } else { + cmd = exec.Command("gofmt", args...) + } + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) +} + +func addGoLintCommand(parent *clir.Command) { + var fix bool + + lintCmd := parent.NewSubCommand("lint", "Run golangci-lint") + lintCmd.LongDescription("Run golangci-lint on the codebase.\n\n" + + "Examples:\n" + + " core go lint\n" + + " core go lint --fix") + + lintCmd.BoolFlag("fix", "Fix issues automatically", &fix) + + lintCmd.Action(func() error { + args := []string{"run"} + if fix { + args = append(args, "--fix") + } + + cmd := exec.Command("golangci-lint", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) +} + +func addGoModCommand(parent *clir.Command) { + modCmd := parent.NewSubCommand("mod", "Module management") + modCmd.LongDescription("Go module management commands.\n\n" + + "Commands:\n" + + " tidy Add missing and remove unused modules\n" + + " download Download modules to local cache\n" + + " verify Verify dependencies\n" + + " graph Print module dependency graph") + + // tidy + tidyCmd := modCmd.NewSubCommand("tidy", "Tidy go.mod") + tidyCmd.Action(func() error { + cmd := exec.Command("go", "mod", "tidy") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) + + // download + downloadCmd := modCmd.NewSubCommand("download", "Download modules") + downloadCmd.Action(func() error { + cmd := exec.Command("go", "mod", "download") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) + + // verify + verifyCmd := modCmd.NewSubCommand("verify", "Verify dependencies") + verifyCmd.Action(func() error { + cmd := exec.Command("go", "mod", "verify") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) + + // graph + graphCmd := modCmd.NewSubCommand("graph", "Print dependency graph") + graphCmd.Action(func() error { + cmd := exec.Command("go", "mod", "graph") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) +} + +func addGoWorkCommand(parent *clir.Command) { + workCmd := parent.NewSubCommand("work", "Workspace management") + workCmd.LongDescription("Go workspace management commands.\n\n" + + "Commands:\n" + + " sync Sync go.work with modules\n" + + " init Initialize go.work\n" + + " use Add module to workspace") + + // sync + syncCmd := workCmd.NewSubCommand("sync", "Sync workspace") + syncCmd.Action(func() error { + cmd := exec.Command("go", "work", "sync") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) + + // init + initCmd := workCmd.NewSubCommand("init", "Initialize workspace") + initCmd.Action(func() error { + cmd := exec.Command("go", "work", "init") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + // Auto-add current module if go.mod exists + if _, err := os.Stat("go.mod"); err == nil { + cmd = exec.Command("go", "work", "use", ".") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + return nil + }) + + // use + useCmd := workCmd.NewSubCommand("use", "Add module to workspace") + useCmd.Action(func() error { + args := useCmd.OtherArgs() + if len(args) == 0 { + // Auto-detect modules + modules := findGoModules(".") + if len(modules) == 0 { + return fmt.Errorf("no go.mod files found") + } + for _, mod := range modules { + cmd := exec.Command("go", "work", "use", mod) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + fmt.Printf("Added %s\n", mod) + } + return nil + } + + cmdArgs := append([]string{"work", "use"}, args...) + cmd := exec.Command("go", cmdArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + }) +} + +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/core/cmd/root.go b/cmd/core/cmd/root.go index 4031abf..69f66b1 100644 --- a/cmd/core/cmd/root.go +++ b/cmd/core/cmd/root.go @@ -46,6 +46,7 @@ func Execute() error { AddPkgCommands(app) AddReleaseCommand(app) AddContainerCommands(app) + AddGoCommands(app) AddPHPCommands(app) AddSDKCommand(app) AddTestCommand(app)