diff --git a/cmd/core.go b/cmd/core.go index 92360c38..94837f8d 100644 --- a/cmd/core.go +++ b/cmd/core.go @@ -22,6 +22,10 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/framework" "github.com/spf13/cobra" + + // Build variants import commands via self-registration. + // See cmd/variants/ for available variants: full, ci, php, minimal. + _ "github.com/host-uk/core/cmd/variants" ) const ( @@ -29,21 +33,7 @@ const ( appVersion = "0.1.0" ) -// Terminal styles using Tailwind colour palette (from shared package). -var ( - // coreStyle is used for primary headings and the CLI name. - coreStyle = cli.RepoNameStyle - // linkStyle is used for URLs and clickable references. - linkStyle = cli.LinkStyle -) - -// rootCmd is the base command for the CLI. -var rootCmd = &cobra.Command{ - Use: appName, - Short: "CLI tool for development and production", - Version: appVersion, -} // Execute initialises and runs the CLI application. // Commands are registered based on build tags (see core_ci.go and core_dev.go). @@ -63,13 +53,12 @@ func Execute() error { } defer cli.Shutdown() - return rootCmd.Execute() + // Add completion command to the CLI's root + cli.RootCmd().AddCommand(completionCmd) + + return cli.Execute() } -func init() { - // Add shell completion command - rootCmd.AddCommand(completionCmd) -} // completionCmd generates shell completion scripts. var completionCmd = &cobra.Command{ diff --git a/cmd/core_dev.go b/cmd/core_dev.go deleted file mode 100644 index bde76bb9..00000000 --- a/cmd/core_dev.go +++ /dev/null @@ -1,63 +0,0 @@ -//go:build !ci - -// core_dev.go registers commands for the full development binary. -// -// Build with: go build (default) -// -// This is the default build variant with all development tools: -// - dev: Multi-repo git workflows (commit, push, pull, sync) -// - ai: AI agent task management -// - go: Go module and build tools -// - php: Laravel/Composer development tools -// - build: Cross-platform compilation -// - ci: Release publishing -// - sdk: API compatibility checks -// - pkg: Package management -// - vm: LinuxKit VM management -// - docs: Documentation generation -// - setup: Repository cloning and setup -// - doctor: Environment health checks -// - test: Test runner with coverage - -package cmd - -import ( - "github.com/host-uk/core/cmd/ai" - "github.com/host-uk/core/cmd/build" - "github.com/host-uk/core/cmd/ci" - "github.com/host-uk/core/cmd/dev" - "github.com/host-uk/core/cmd/docs" - "github.com/host-uk/core/cmd/doctor" - gocmd "github.com/host-uk/core/cmd/go" - "github.com/host-uk/core/cmd/php" - "github.com/host-uk/core/cmd/pkg" - "github.com/host-uk/core/cmd/sdk" - "github.com/host-uk/core/cmd/setup" - testcmd "github.com/host-uk/core/cmd/test" - "github.com/host-uk/core/cmd/vm" -) - -func init() { - // Multi-repo workflow - dev.AddCommands(rootCmd) - - // AI agent tools - ai.AddCommands(rootCmd) - - // Language tooling - gocmd.AddCommands(rootCmd) - php.AddCommands(rootCmd) - - // Build and release - build.AddCommands(rootCmd) - ci.AddCommands(rootCmd) - sdk.AddCommands(rootCmd) - - // Environment management - pkg.AddCommands(rootCmd) - vm.AddCommands(rootCmd) - docs.AddCommands(rootCmd) - setup.AddCommands(rootCmd) - doctor.AddCommands(rootCmd) - testcmd.AddCommands(rootCmd) -} diff --git a/cmd/sdk/commands.go b/cmd/sdk/commands.go deleted file mode 100644 index 49e621d0..00000000 --- a/cmd/sdk/commands.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package sdk provides SDK validation and API compatibility commands. -// -// Commands: -// - diff: Check for breaking API changes between spec versions -// - validate: Validate OpenAPI spec syntax -// -// Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk -package sdk - -import "github.com/spf13/cobra" - -// AddCommands registers the 'sdk' command and all subcommands. -func AddCommands(root *cobra.Command) { - root.AddCommand(sdkCmd) -} diff --git a/cmd/core_ci.go b/cmd/variants/ci.go similarity index 50% rename from cmd/core_ci.go rename to cmd/variants/ci.go index 5bae7037..313dd474 100644 --- a/cmd/core_ci.go +++ b/cmd/variants/ci.go @@ -1,6 +1,6 @@ //go:build ci -// core_ci.go registers commands for the minimal CI/release binary. +// ci.go imports packages for the minimal CI/release binary. // // Build with: go build -tags ci // @@ -12,18 +12,12 @@ // // Use this build to reduce binary size and attack surface in production. -package cmd +package variants import ( - "github.com/host-uk/core/cmd/build" - "github.com/host-uk/core/cmd/ci" - "github.com/host-uk/core/cmd/doctor" - "github.com/host-uk/core/cmd/sdk" + // Commands via self-registration + _ "github.com/host-uk/core/pkg/build/buildcmd" + _ "github.com/host-uk/core/pkg/ci" + _ "github.com/host-uk/core/pkg/doctor" + _ "github.com/host-uk/core/pkg/sdk" ) - -func init() { - build.AddCommands(rootCmd) - ci.AddCommands(rootCmd) - sdk.AddCommands(rootCmd) - doctor.AddCommands(rootCmd) -} diff --git a/cmd/variants/full.go b/cmd/variants/full.go new file mode 100644 index 00000000..e94a9277 --- /dev/null +++ b/cmd/variants/full.go @@ -0,0 +1,39 @@ +//go:build !ci && !php && !minimal + +// full.go imports all packages for the full development binary. +// +// Build with: go build (default) +// +// This is the default build variant with all development tools: +// - dev: Multi-repo git workflows (commit, push, pull, sync) +// - ai: AI agent task management +// - go: Go module and build tools +// - php: Laravel/Composer development tools +// - build: Cross-platform compilation +// - ci: Release publishing +// - sdk: API compatibility checks +// - pkg: Package management +// - vm: LinuxKit VM management +// - docs: Documentation generation +// - setup: Repository cloning and setup +// - doctor: Environment health checks +// - test: Test runner with coverage + +package variants + +import ( + // Commands via self-registration + _ "github.com/host-uk/core/pkg/ai" + _ "github.com/host-uk/core/pkg/build/buildcmd" + _ "github.com/host-uk/core/pkg/ci" + _ "github.com/host-uk/core/pkg/dev" + _ "github.com/host-uk/core/pkg/docs" + _ "github.com/host-uk/core/pkg/doctor" + _ "github.com/host-uk/core/pkg/go" + _ "github.com/host-uk/core/pkg/php" + _ "github.com/host-uk/core/pkg/pkgcmd" + _ "github.com/host-uk/core/pkg/sdk" + _ "github.com/host-uk/core/pkg/setup" + _ "github.com/host-uk/core/pkg/test" + _ "github.com/host-uk/core/pkg/vm" +) diff --git a/cmd/variants/minimal.go b/cmd/variants/minimal.go new file mode 100644 index 00000000..69f4bffb --- /dev/null +++ b/cmd/variants/minimal.go @@ -0,0 +1,17 @@ +//go:build minimal + +// minimal.go imports only core packages for a minimal binary. +// +// Build with: go build -tags minimal +// +// This variant includes only the absolute essentials: +// - doctor: Environment verification +// +// Use this for the smallest possible binary with just health checks. + +package variants + +import ( + // Commands via self-registration + _ "github.com/host-uk/core/pkg/doctor" +) diff --git a/cmd/variants/php.go b/cmd/variants/php.go new file mode 100644 index 00000000..c7a574d2 --- /dev/null +++ b/cmd/variants/php.go @@ -0,0 +1,19 @@ +//go:build php + +// php.go imports packages for the PHP-only binary. +// +// Build with: go build -tags php +// +// This variant includes only PHP/Laravel development tools: +// - php: Laravel/Composer development tools +// - doctor: Environment verification +// +// Use this for PHP-focused workflows without other tooling. + +package variants + +import ( + // Commands via self-registration + _ "github.com/host-uk/core/pkg/doctor" + _ "github.com/host-uk/core/pkg/php" +) diff --git a/cmd/ai/ai.go b/pkg/ai/cmd_ai.go similarity index 85% rename from cmd/ai/ai.go rename to pkg/ai/cmd_ai.go index b7beabfa..631db0e6 100644 --- a/cmd/ai/ai.go +++ b/pkg/ai/cmd_ai.go @@ -1,4 +1,4 @@ -// ai.go defines styles and the AddAgenticCommands function for AI task management. +// cmd_ai.go defines styles and the AddAgenticCommands function for AI task management. package ai @@ -29,8 +29,8 @@ var ( // Task-specific styles (aliases to shared where possible) var ( - taskIDStyle = cli.TitleStyle // Bold + blue - taskTitleStyle = cli.ValueStyle // Light gray + taskIDStyle = cli.TitleStyle // Bold + blue + taskTitleStyle = cli.ValueStyle // Light gray taskLabelStyle = cli.AccentLabelStyle // Violet for labels ) diff --git a/cmd/ai/commands.go b/pkg/ai/cmd_commands.go similarity index 87% rename from cmd/ai/commands.go rename to pkg/ai/cmd_commands.go index aa073704..ccf18ce0 100644 --- a/cmd/ai/commands.go +++ b/pkg/ai/cmd_commands.go @@ -11,10 +11,15 @@ package ai import ( + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddAICommands) +} + var aiCmd = &cobra.Command{ Use: "ai", Short: i18n.T("cmd.ai.short"), @@ -43,7 +48,7 @@ var claudeConfigCmd = &cobra.Command{ }, } -func init() { +func initCommands() { // Add Claude subcommands claudeCmd.AddCommand(claudeRunCmd) claudeCmd.AddCommand(claudeConfigCmd) @@ -55,8 +60,9 @@ func init() { AddAgenticCommands(aiCmd) } -// AddCommands registers the 'ai' command and all subcommands. -func AddCommands(root *cobra.Command) { +// AddAICommands registers the 'ai' command and all subcommands. +func AddAICommands(root *cobra.Command) { + initCommands() root.AddCommand(aiCmd) } diff --git a/cmd/ai/ai_git.go b/pkg/ai/cmd_git.go similarity index 96% rename from cmd/ai/ai_git.go rename to pkg/ai/cmd_git.go index aeb72c6a..237fe2ec 100644 --- a/cmd/ai/ai_git.go +++ b/pkg/ai/cmd_git.go @@ -1,4 +1,4 @@ -// ai_git.go implements git integration commands for task commits and PRs. +// cmd_git.go implements git integration commands for task commits and PRs. package ai @@ -40,7 +40,7 @@ var taskCommitCmd = &cobra.Command{ taskID := args[0] if taskCommitMessage == "" { - return fmt.Errorf(i18n.T("cmd.ai.task_commit.message_required")) + return fmt.Errorf("%s", i18n.T("cmd.ai.task_commit.message_required")) } cfg, err := agentic.LoadConfig("") @@ -143,7 +143,7 @@ var taskPRCmd = &cobra.Command{ } if branch == "main" || branch == "master" { - return fmt.Errorf(i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch})) + return fmt.Errorf("%s", i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch})) } // Push current branch @@ -180,7 +180,7 @@ var taskPRCmd = &cobra.Command{ }, } -func init() { +func initGitFlags() { // task:commit command flags taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message")) taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope")) @@ -194,6 +194,7 @@ func init() { } func addTaskCommitCommand(parent *cobra.Command) { + initGitFlags() parent.AddCommand(taskCommitCmd) } diff --git a/cmd/ai/ai_tasks.go b/pkg/ai/cmd_tasks.go similarity index 98% rename from cmd/ai/ai_tasks.go rename to pkg/ai/cmd_tasks.go index ad93014f..05edb33e 100644 --- a/cmd/ai/ai_tasks.go +++ b/pkg/ai/cmd_tasks.go @@ -1,4 +1,4 @@ -// ai_tasks.go implements task listing and viewing commands. +// cmd_tasks.go implements task listing and viewing commands. package ai @@ -135,7 +135,7 @@ var taskCmd = &cobra.Command{ taskClaim = true // Auto-select implies claiming } else { if taskID == "" { - return fmt.Errorf(i18n.T("cmd.ai.task.id_required")) + return fmt.Errorf("%s", i18n.T("cmd.ai.task.id_required")) } task, err = client.GetTask(ctx, taskID) @@ -174,7 +174,7 @@ var taskCmd = &cobra.Command{ }, } -func init() { +func initTasksFlags() { // tasks command flags tasksCmd.Flags().StringVar(&tasksStatus, "status", "", i18n.T("cmd.ai.tasks.flag.status")) tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", i18n.T("cmd.ai.tasks.flag.priority")) @@ -189,6 +189,7 @@ func init() { } func addTasksCommand(parent *cobra.Command) { + initTasksFlags() parent.AddCommand(tasksCmd) } diff --git a/cmd/ai/ai_updates.go b/pkg/ai/cmd_updates.go similarity index 95% rename from cmd/ai/ai_updates.go rename to pkg/ai/cmd_updates.go index 52c634dd..b43974b0 100644 --- a/cmd/ai/ai_updates.go +++ b/pkg/ai/cmd_updates.go @@ -1,4 +1,4 @@ -// ai_updates.go implements task update and completion commands. +// cmd_updates.go implements task update and completion commands. package ai @@ -35,7 +35,7 @@ var taskUpdateCmd = &cobra.Command{ taskID := args[0] if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { - return fmt.Errorf(i18n.T("cmd.ai.task_update.flag_required")) + return fmt.Errorf("%s", i18n.T("cmd.ai.task_update.flag_required")) } cfg, err := agentic.LoadConfig("") @@ -102,7 +102,7 @@ var taskCompleteCmd = &cobra.Command{ }, } -func init() { +func initUpdatesFlags() { // task:update command flags taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status")) taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress")) @@ -115,6 +115,7 @@ func init() { } func addTaskUpdateCommand(parent *cobra.Command) { + initUpdatesFlags() parent.AddCommand(taskUpdateCmd) } diff --git a/cmd/build/build.go b/pkg/build/buildcmd/cmd_build.go similarity index 92% rename from cmd/build/build.go rename to pkg/build/buildcmd/cmd_build.go index 888c035c..50f774c0 100644 --- a/cmd/build/build.go +++ b/pkg/build/buildcmd/cmd_build.go @@ -1,5 +1,5 @@ -// Package build provides project build commands with auto-detection. -package build +// Package buildcmd provides project build commands with auto-detection. +package buildcmd import ( "embed" @@ -9,6 +9,10 @@ import ( "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddBuildCommands) +} + // Style aliases from shared package var ( buildHeaderStyle = cli.TitleStyle @@ -93,7 +97,7 @@ var sdkBuildCmd = &cobra.Command{ }, } -func init() { +func initBuildFlags() { // Main build command flags buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.build.flag.type")) buildCmd.Flags().BoolVar(&ciMode, "ci", false, i18n.T("cmd.build.flag.ci")) @@ -130,7 +134,8 @@ func init() { buildCmd.AddCommand(sdkBuildCmd) } -// AddBuildCommand adds the new build command and its subcommands to the cobra app. -func AddBuildCommand(root *cobra.Command) { +// AddBuildCommands registers the 'build' command and all subcommands. +func AddBuildCommands(root *cobra.Command) { + initBuildFlags() root.AddCommand(buildCmd) } diff --git a/cmd/build/commands.go b/pkg/build/buildcmd/cmd_commands.go similarity index 64% rename from cmd/build/commands.go rename to pkg/build/buildcmd/cmd_commands.go index 9415962c..310d5580 100644 --- a/cmd/build/commands.go +++ b/pkg/build/buildcmd/cmd_commands.go @@ -1,4 +1,4 @@ -// Package build provides project build commands with auto-detection. +// Package buildcmd provides project build commands with auto-detection. // // Supports building: // - Go projects (standard and cross-compilation) @@ -14,11 +14,8 @@ // - build from-path: Build from a local static web app directory // - build pwa: Build from a live PWA URL // - build sdk: Generate API SDKs from OpenAPI spec -package build +package buildcmd -import "github.com/spf13/cobra" - -// AddCommands registers the 'build' command and all subcommands. -func AddCommands(root *cobra.Command) { - AddBuildCommand(root) -} +// Note: The AddBuildCommands function is defined in cmd_build.go +// This file exists for documentation purposes and maintains the original +// package documentation from commands.go. diff --git a/cmd/build/build_project.go b/pkg/build/buildcmd/cmd_project.go similarity index 87% rename from cmd/build/build_project.go rename to pkg/build/buildcmd/cmd_project.go index 7e5fb887..1b7109a7 100644 --- a/cmd/build/build_project.go +++ b/pkg/build/buildcmd/cmd_project.go @@ -1,9 +1,9 @@ -// build_project.go implements the main project build logic. +// cmd_project.go implements the main project build logic. // // This handles auto-detection of project types (Go, Wails, Docker, LinuxKit, Taskfile) // and orchestrates the build process including signing, archiving, and checksums. -package build +package buildcmd import ( "context" @@ -14,7 +14,7 @@ import ( "runtime" "strings" - buildpkg "github.com/host-uk/core/pkg/build" + "github.com/host-uk/core/pkg/build" "github.com/host-uk/core/pkg/build/builders" "github.com/host-uk/core/pkg/build/signing" "github.com/host-uk/core/pkg/i18n" @@ -29,17 +29,17 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Load configuration from .core/build.yaml (or defaults) - buildCfg, err := buildpkg.LoadConfig(projectDir) + buildCfg, err := build.LoadConfig(projectDir) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load config"}), err) } // Detect project type if not specified - var projectType buildpkg.ProjectType + var projectType build.ProjectType if buildType != "" { - projectType = buildpkg.ProjectType(buildType) + projectType = build.ProjectType(buildType) } else { - projectType, err = buildpkg.PrimaryType(projectDir) + projectType, err = build.PrimaryType(projectDir) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project type"}), err) } @@ -49,7 +49,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Determine targets - var buildTargets []buildpkg.Target + var buildTargets []build.Target if targetsFlag != "" { // Parse from command line buildTargets, err = parseTargets(targetsFlag) @@ -61,7 +61,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi buildTargets = buildCfg.ToTargets() } else { // Fall back to current OS/arch - buildTargets = []buildpkg.Target{ + buildTargets = []build.Target{ {OS: runtime.GOOS, Arch: runtime.GOARCH}, } } @@ -97,7 +97,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Create build config for the builder - cfg := &buildpkg.Config{ + cfg := &build.Config{ ProjectDir: projectDir, OutputDir: outputDir, Name: binaryName, @@ -156,7 +156,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries")) } - // Convert buildpkg.Artifact to signing.Artifact + // Convert build.Artifact to signing.Artifact signingArtifacts := make([]signing.Artifact, len(artifacts)) for i, a := range artifacts { signingArtifacts[i] = signing.Artifact{Path: a.Path, OS: a.OS, Arch: a.Arch} @@ -180,14 +180,14 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Archive artifacts if enabled - var archivedArtifacts []buildpkg.Artifact + var archivedArtifacts []build.Artifact if doArchive && len(artifacts) > 0 { if !ciMode { fmt.Println() fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives")) } - archivedArtifacts, err = buildpkg.ArchiveAll(artifacts) + archivedArtifacts, err = build.ArchiveAll(artifacts) if err != nil { if !ciMode { fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.archive_failed"), err) @@ -211,7 +211,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // Compute checksums if enabled - var checksummedArtifacts []buildpkg.Artifact + var checksummedArtifacts []build.Artifact if doChecksum && len(archivedArtifacts) > 0 { checksummedArtifacts, err = computeAndWriteChecksums(ctx, projectDir, outputDir, archivedArtifacts, signCfg, ciMode) if err != nil { @@ -228,7 +228,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi // Output results for CI mode if ciMode { // Determine which artifacts to output (prefer checksummed > archived > raw) - var outputArtifacts []buildpkg.Artifact + var outputArtifacts []build.Artifact if len(checksummedArtifacts) > 0 { outputArtifacts = checksummedArtifacts } else if len(archivedArtifacts) > 0 { @@ -249,13 +249,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi } // computeAndWriteChecksums computes checksums for artifacts and writes CHECKSUMS.txt. -func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) { +func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []build.Artifact, signCfg signing.SignConfig, ciMode bool) ([]build.Artifact, error) { if !ciMode { fmt.Println() fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums")) } - checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts) + checksummedArtifacts, err := build.ChecksumAll(artifacts) if err != nil { if !ciMode { fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.checksum_failed"), err) @@ -265,7 +265,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, // Write CHECKSUMS.txt checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt") - if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil { + if err := build.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil { if !ciMode { fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("common.error.failed", map[string]any{"Action": "write CHECKSUMS.txt"}), err) } @@ -309,9 +309,9 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, } // parseTargets parses a comma-separated list of OS/arch pairs. -func parseTargets(targetsFlag string) ([]buildpkg.Target, error) { +func parseTargets(targetsFlag string) ([]build.Target, error) { parts := strings.Split(targetsFlag, ",") - var targets []buildpkg.Target + var targets []build.Target for _, part := range parts { part = strings.TrimSpace(part) @@ -324,7 +324,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) { return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.invalid_target", map[string]interface{}{"Target": part})) } - targets = append(targets, buildpkg.Target{ + targets = append(targets, build.Target{ OS: strings.TrimSpace(osArch[0]), Arch: strings.TrimSpace(osArch[1]), }) @@ -338,7 +338,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) { } // formatTargets returns a human-readable string of targets. -func formatTargets(targets []buildpkg.Target) string { +func formatTargets(targets []build.Target) string { var parts []string for _, t := range targets { parts = append(parts, t.String()) @@ -347,21 +347,21 @@ func formatTargets(targets []buildpkg.Target) string { } // getBuilder returns the appropriate builder for the project type. -func getBuilder(projectType buildpkg.ProjectType) (buildpkg.Builder, error) { +func getBuilder(projectType build.ProjectType) (build.Builder, error) { switch projectType { - case buildpkg.ProjectTypeWails: + case build.ProjectTypeWails: return builders.NewWailsBuilder(), nil - case buildpkg.ProjectTypeGo: + case build.ProjectTypeGo: return builders.NewGoBuilder(), nil - case buildpkg.ProjectTypeDocker: + case build.ProjectTypeDocker: return builders.NewDockerBuilder(), nil - case buildpkg.ProjectTypeLinuxKit: + case build.ProjectTypeLinuxKit: return builders.NewLinuxKitBuilder(), nil - case buildpkg.ProjectTypeTaskfile: + case build.ProjectTypeTaskfile: return builders.NewTaskfileBuilder(), nil - case buildpkg.ProjectTypeNode: + case build.ProjectTypeNode: return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.node_not_implemented")) - case buildpkg.ProjectTypePHP: + case build.ProjectTypePHP: return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented")) default: return nil, fmt.Errorf("%s: %s", i18n.T("cmd.build.error.unsupported_type"), projectType) diff --git a/cmd/build/build_pwa.go b/pkg/build/buildcmd/cmd_pwa.go similarity index 99% rename from cmd/build/build_pwa.go rename to pkg/build/buildcmd/cmd_pwa.go index b3ca3f42..09f3f134 100644 --- a/cmd/build/build_pwa.go +++ b/pkg/build/buildcmd/cmd_pwa.go @@ -1,10 +1,10 @@ -// build_pwa.go implements PWA and legacy GUI build functionality. +// cmd_pwa.go implements PWA and legacy GUI build functionality. // // Supports building desktop applications from: // - Local static web application directories // - Live PWA URLs (downloads and packages) -package build +package buildcmd import ( "encoding/json" diff --git a/cmd/build/build_sdk.go b/pkg/build/buildcmd/cmd_sdk.go similarity index 96% rename from cmd/build/build_sdk.go rename to pkg/build/buildcmd/cmd_sdk.go index d7080363..8102293d 100644 --- a/cmd/build/build_sdk.go +++ b/pkg/build/buildcmd/cmd_sdk.go @@ -1,9 +1,9 @@ -// build_sdk.go implements SDK generation from OpenAPI specifications. +// cmd_sdk.go implements SDK generation from OpenAPI specifications. // // Generates typed API clients for TypeScript, Python, Go, and PHP // from OpenAPI/Swagger specifications. -package build +package buildcmd import ( "context" diff --git a/cmd/build/tmpl/gui/go.mod.tmpl b/pkg/build/buildcmd/tmpl/gui/go.mod.tmpl similarity index 100% rename from cmd/build/tmpl/gui/go.mod.tmpl rename to pkg/build/buildcmd/tmpl/gui/go.mod.tmpl diff --git a/cmd/build/tmpl/gui/html/.gitkeep b/pkg/build/buildcmd/tmpl/gui/html/.gitkeep similarity index 100% rename from cmd/build/tmpl/gui/html/.gitkeep rename to pkg/build/buildcmd/tmpl/gui/html/.gitkeep diff --git a/cmd/build/tmpl/gui/html/.placeholder b/pkg/build/buildcmd/tmpl/gui/html/.placeholder similarity index 100% rename from cmd/build/tmpl/gui/html/.placeholder rename to pkg/build/buildcmd/tmpl/gui/html/.placeholder diff --git a/cmd/build/tmpl/gui/main.go.tmpl b/pkg/build/buildcmd/tmpl/gui/main.go.tmpl similarity index 100% rename from cmd/build/tmpl/gui/main.go.tmpl rename to pkg/build/buildcmd/tmpl/gui/main.go.tmpl diff --git a/cmd/ci/ci_changelog.go b/pkg/ci/cmd_changelog.go similarity index 100% rename from cmd/ci/ci_changelog.go rename to pkg/ci/cmd_changelog.go diff --git a/cmd/ci/ci_release.go b/pkg/ci/cmd_ci.go similarity index 100% rename from cmd/ci/ci_release.go rename to pkg/ci/cmd_ci.go diff --git a/cmd/ci/commands.go b/pkg/ci/cmd_commands.go similarity index 62% rename from cmd/ci/commands.go rename to pkg/ci/cmd_commands.go index 253e7b1c..89436b63 100644 --- a/cmd/ci/commands.go +++ b/pkg/ci/cmd_commands.go @@ -9,9 +9,16 @@ // Configuration via .core/release.yaml. package ci -import "github.com/spf13/cobra" +import ( + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) -// AddCommands registers the 'ci' command and all subcommands. -func AddCommands(root *cobra.Command) { +func init() { + cli.RegisterCommands(AddCICommands) +} + +// AddCICommands registers the 'ci' command and all subcommands. +func AddCICommands(root *cobra.Command) { root.AddCommand(ciCmd) } diff --git a/cmd/ci/ci_init.go b/pkg/ci/cmd_init.go similarity index 100% rename from cmd/ci/ci_init.go rename to pkg/ci/cmd_init.go diff --git a/cmd/ci/ci_publish.go b/pkg/ci/cmd_publish.go similarity index 96% rename from cmd/ci/ci_publish.go rename to pkg/ci/cmd_publish.go index b543d3ec..2f7e62a8 100644 --- a/cmd/ci/ci_publish.go +++ b/pkg/ci/cmd_publish.go @@ -2,6 +2,7 @@ package ci import ( "context" + "errors" "fmt" "os" @@ -54,7 +55,7 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error { // Check for publishers if len(cfg.Publishers) == 0 { - return fmt.Errorf(i18n.T("cmd.ci.error.no_publishers")) + return errors.New(i18n.T("cmd.ci.error.no_publishers")) } // Publish pre-built artifacts diff --git a/cmd/ci/ci_version.go b/pkg/ci/cmd_version.go similarity index 100% rename from cmd/ci/ci_version.go rename to pkg/ci/cmd_version.go diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go new file mode 100644 index 00000000..20ea2da8 --- /dev/null +++ b/pkg/cli/commands.go @@ -0,0 +1,50 @@ +// Package cli provides the CLI runtime and utilities. +package cli + +import ( + "sync" + + "github.com/spf13/cobra" +) + +// CommandRegistration is a function that adds commands to the root. +type CommandRegistration func(root *cobra.Command) + +var ( + registeredCommands []CommandRegistration + registeredCommandsMu sync.Mutex + commandsAttached bool +) + +// RegisterCommands registers a function that adds commands to the CLI. +// Call this in your package's init() to register commands. +// +// func init() { +// cli.RegisterCommands(AddCommands) +// } +// +// func AddCommands(root *cobra.Command) { +// root.AddCommand(myCmd) +// } +func RegisterCommands(fn CommandRegistration) { + registeredCommandsMu.Lock() + defer registeredCommandsMu.Unlock() + registeredCommands = append(registeredCommands, fn) + + // If commands already attached (CLI already running), attach immediately + if commandsAttached && instance != nil && instance.root != nil { + fn(instance.root) + } +} + +// attachRegisteredCommands calls all registered command functions. +// Called by Init() after creating the root command. +func attachRegisteredCommands(root *cobra.Command) { + registeredCommandsMu.Lock() + defer registeredCommandsMu.Unlock() + + for _, fn := range registeredCommands { + fn(root) + } + commandsAttached = true +} diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index eb3fa79b..cd160e6c 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -22,6 +22,7 @@ import ( "syscall" "github.com/host-uk/core/pkg/framework" + "github.com/spf13/cobra" ) var ( @@ -32,6 +33,7 @@ var ( // runtime is the CLI's internal Core runtime. type runtime struct { core *framework.Core + root *cobra.Command ctx context.Context cancel context.CancelFunc } @@ -54,14 +56,24 @@ func Init(opts Options) error { once.Do(func() { ctx, cancel := context.WithCancel(context.Background()) + // Create root command + rootCmd := &cobra.Command{ + Use: opts.AppName, + Version: opts.Version, + } + + // Attach all registered commands + attachRegisteredCommands(rootCmd) + // Build signal service options var signalOpts []SignalOption if opts.OnReload != nil { signalOpts = append(signalOpts, WithReloadHandler(opts.OnReload)) } - // Build options: signal service + any additional services + // Build options: app, signal service + any additional services coreOpts := []framework.Option{ + framework.WithApp(rootCmd), framework.WithName("signal", newSignalService(cancel, signalOpts...)), } coreOpts = append(coreOpts, opts.Services...) @@ -76,6 +88,7 @@ func Init(opts Options) error { instance = &runtime{ core: c, + root: rootCmd, ctx: ctx, cancel: cancel, } @@ -102,6 +115,19 @@ func Core() *framework.Core { return instance.core } +// RootCmd returns the CLI's root cobra command. +func RootCmd() *cobra.Command { + mustInit() + return instance.root +} + +// Execute runs the CLI root command. +// Returns an error if the command fails. +func Execute() error { + mustInit() + return instance.root.Execute() +} + // Context returns the CLI's root context. // Cancelled on SIGINT/SIGTERM. func Context() context.Context { diff --git a/cmd/dev/dev_api.go b/pkg/dev/cmd_api.go similarity index 100% rename from cmd/dev/dev_api.go rename to pkg/dev/cmd_api.go diff --git a/cmd/dev/bundles.go b/pkg/dev/cmd_bundles.go similarity index 92% rename from cmd/dev/bundles.go rename to pkg/dev/cmd_bundles.go index bd9cccf4..e2374e20 100644 --- a/cmd/dev/bundles.go +++ b/pkg/dev/cmd_bundles.go @@ -4,7 +4,6 @@ import ( "context" "github.com/host-uk/core/pkg/agentic" - devpkg "github.com/host-uk/core/pkg/dev" "github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/git" ) @@ -24,7 +23,7 @@ type WorkBundleOptions struct { // Includes: dev (orchestration), git, agentic services. func NewWorkBundle(opts WorkBundleOptions) (*WorkBundle, error) { c, err := framework.New( - framework.WithService(devpkg.NewService(devpkg.ServiceOptions{ + framework.WithService(NewService(ServiceOptions{ RegistryPath: opts.RegistryPath, })), framework.WithService(git.NewService(git.ServiceOptions{})), @@ -64,7 +63,7 @@ type StatusBundleOptions struct { // Includes: dev (orchestration), git services. No agentic - commits not available. func NewStatusBundle(opts StatusBundleOptions) (*StatusBundle, error) { c, err := framework.New( - framework.WithService(devpkg.NewService(devpkg.ServiceOptions{ + framework.WithService(NewService(ServiceOptions{ RegistryPath: opts.RegistryPath, })), framework.WithService(git.NewService(git.ServiceOptions{})), diff --git a/cmd/dev/dev_ci.go b/pkg/dev/cmd_ci.go similarity index 99% rename from cmd/dev/dev_ci.go rename to pkg/dev/cmd_ci.go index 266fa9b5..90d9aacf 100644 --- a/cmd/dev/dev_ci.go +++ b/pkg/dev/cmd_ci.go @@ -2,6 +2,7 @@ package dev import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -68,7 +69,7 @@ func addCICommand(parent *cobra.Command) { func runCI(registryPath string, branch string, failedOnly bool) error { // Check gh is available if _, err := exec.LookPath("gh"); err != nil { - return fmt.Errorf(i18n.T("error.gh_not_found")) + return errors.New(i18n.T("error.gh_not_found")) } // Find or use provided registry diff --git a/cmd/dev/dev_commit.go b/pkg/dev/cmd_commit.go similarity index 100% rename from cmd/dev/dev_commit.go rename to pkg/dev/cmd_commit.go diff --git a/cmd/dev/dev.go b/pkg/dev/cmd_dev.go similarity index 92% rename from cmd/dev/dev.go rename to pkg/dev/cmd_dev.go index da23bcc0..799c5e9b 100644 --- a/cmd/dev/dev.go +++ b/pkg/dev/cmd_dev.go @@ -34,6 +34,10 @@ import ( "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddDevCommands) +} + // Style aliases from shared package var ( successStyle = cli.SuccessStyle @@ -52,8 +56,8 @@ var ( cleanStyle = cli.GitCleanStyle.Padding(0, 1) ) -// AddCommands registers the 'dev' command and all subcommands. -func AddCommands(root *cobra.Command) { +// AddDevCommands registers the 'dev' command and all subcommands. +func AddDevCommands(root *cobra.Command) { devCmd := &cobra.Command{ Use: "dev", Short: i18n.T("cmd.dev.short"), diff --git a/cmd/dev/dev_health.go b/pkg/dev/cmd_health.go similarity index 100% rename from cmd/dev/dev_health.go rename to pkg/dev/cmd_health.go diff --git a/cmd/dev/dev_impact.go b/pkg/dev/cmd_impact.go similarity index 97% rename from cmd/dev/dev_impact.go rename to pkg/dev/cmd_impact.go index 4825d6f7..d2675f75 100644 --- a/cmd/dev/dev_impact.go +++ b/pkg/dev/cmd_impact.go @@ -1,6 +1,7 @@ package dev import ( + "errors" "fmt" "sort" @@ -55,14 +56,14 @@ func runImpact(registryPath string, repoName string) error { return fmt.Errorf("failed to load registry: %w", err) } } else { - return fmt.Errorf(i18n.T("cmd.dev.impact.requires_registry")) + return errors.New(i18n.T("cmd.dev.impact.requires_registry")) } } // Check repo exists repo, exists := reg.Get(repoName) if !exists { - return fmt.Errorf(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName})) + return errors.New(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName})) } // Build reverse dependency graph diff --git a/cmd/dev/dev_issues.go b/pkg/dev/cmd_issues.go similarity index 99% rename from cmd/dev/dev_issues.go rename to pkg/dev/cmd_issues.go index df996bea..ceaaf301 100644 --- a/cmd/dev/dev_issues.go +++ b/pkg/dev/cmd_issues.go @@ -2,6 +2,7 @@ package dev import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -82,7 +83,7 @@ func addIssuesCommand(parent *cobra.Command) { func runIssues(registryPath string, limit int, assignee string) error { // Check gh is available if _, err := exec.LookPath("gh"); err != nil { - return fmt.Errorf(i18n.T("error.gh_not_found")) + return errors.New(i18n.T("error.gh_not_found")) } // Find or use provided registry, fall back to directory scan diff --git a/cmd/dev/dev_pull.go b/pkg/dev/cmd_pull.go similarity index 100% rename from cmd/dev/dev_pull.go rename to pkg/dev/cmd_pull.go diff --git a/cmd/dev/dev_push.go b/pkg/dev/cmd_push.go similarity index 100% rename from cmd/dev/dev_push.go rename to pkg/dev/cmd_push.go diff --git a/cmd/dev/dev_reviews.go b/pkg/dev/cmd_reviews.go similarity index 99% rename from cmd/dev/dev_reviews.go rename to pkg/dev/cmd_reviews.go index 2a938c16..9ff2dff1 100644 --- a/cmd/dev/dev_reviews.go +++ b/pkg/dev/cmd_reviews.go @@ -2,6 +2,7 @@ package dev import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -79,7 +80,7 @@ func addReviewsCommand(parent *cobra.Command) { func runReviews(registryPath string, author string, showAll bool) error { // Check gh is available if _, err := exec.LookPath("gh"); err != nil { - return fmt.Errorf(i18n.T("error.gh_not_found")) + return errors.New(i18n.T("error.gh_not_found")) } // Find or use provided registry, fall back to directory scan diff --git a/cmd/dev/dev_sync.go b/pkg/dev/cmd_sync.go similarity index 100% rename from cmd/dev/dev_sync.go rename to pkg/dev/cmd_sync.go diff --git a/cmd/dev/dev_vm.go b/pkg/dev/cmd_vm.go similarity index 99% rename from cmd/dev/dev_vm.go rename to pkg/dev/cmd_vm.go index 40edbf73..7590e4ff 100644 --- a/cmd/dev/dev_vm.go +++ b/pkg/dev/cmd_vm.go @@ -2,6 +2,7 @@ package dev import ( "context" + "errors" "fmt" "os" "time" @@ -118,7 +119,7 @@ func runVMBoot(memory, cpus int, fresh bool) error { } if !d.IsInstalled() { - return fmt.Errorf(i18n.T("cmd.dev.vm.not_installed")) + return errors.New(i18n.T("cmd.dev.vm.not_installed")) } opts := devops.DefaultBootOptions() diff --git a/cmd/dev/dev_work.go b/pkg/dev/cmd_work.go similarity index 100% rename from cmd/dev/dev_work.go rename to pkg/dev/cmd_work.go index 87d04bd2..fb8f8d04 100644 --- a/cmd/dev/dev_work.go +++ b/pkg/dev/cmd_work.go @@ -8,8 +8,8 @@ import ( "sort" "strings" - "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/agentic" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" diff --git a/cmd/docs/commands.go b/pkg/docs/cmd_commands.go similarity index 63% rename from cmd/docs/commands.go rename to pkg/docs/cmd_commands.go index 1a3b48e5..fa1e4195 100644 --- a/cmd/docs/commands.go +++ b/pkg/docs/cmd_commands.go @@ -8,9 +8,16 @@ // to a central location for unified documentation builds. package docs -import "github.com/spf13/cobra" +import ( + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) -// AddCommands registers the 'docs' command and all subcommands. -func AddCommands(root *cobra.Command) { +func init() { + cli.RegisterCommands(AddDocsCommands) +} + +// AddDocsCommands registers the 'docs' command and all subcommands. +func AddDocsCommands(root *cobra.Command) { root.AddCommand(docsCmd) } diff --git a/cmd/docs/docs.go b/pkg/docs/cmd_docs.go similarity index 100% rename from cmd/docs/docs.go rename to pkg/docs/cmd_docs.go diff --git a/cmd/docs/list.go b/pkg/docs/cmd_list.go similarity index 100% rename from cmd/docs/list.go rename to pkg/docs/cmd_list.go diff --git a/cmd/docs/scan.go b/pkg/docs/cmd_scan.go similarity index 100% rename from cmd/docs/scan.go rename to pkg/docs/cmd_scan.go diff --git a/cmd/docs/sync.go b/pkg/docs/cmd_sync.go similarity index 100% rename from cmd/docs/sync.go rename to pkg/docs/cmd_sync.go diff --git a/cmd/doctor/checks.go b/pkg/doctor/cmd_checks.go similarity index 100% rename from cmd/doctor/checks.go rename to pkg/doctor/cmd_checks.go diff --git a/cmd/doctor/commands.go b/pkg/doctor/cmd_commands.go similarity index 65% rename from cmd/doctor/commands.go rename to pkg/doctor/cmd_commands.go index 6b91129a..91c8efa2 100644 --- a/cmd/doctor/commands.go +++ b/pkg/doctor/cmd_commands.go @@ -10,9 +10,16 @@ // Provides platform-specific installation instructions for missing tools. package doctor -import "github.com/spf13/cobra" +import ( + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) -// AddCommands registers the 'doctor' command and all subcommands. -func AddCommands(root *cobra.Command) { +func init() { + cli.RegisterCommands(AddDoctorCommands) +} + +// AddDoctorCommands registers the 'doctor' command and all subcommands. +func AddDoctorCommands(root *cobra.Command) { root.AddCommand(doctorCmd) } diff --git a/cmd/doctor/doctor.go b/pkg/doctor/cmd_doctor.go similarity index 100% rename from cmd/doctor/doctor.go rename to pkg/doctor/cmd_doctor.go diff --git a/cmd/doctor/environment.go b/pkg/doctor/cmd_environment.go similarity index 100% rename from cmd/doctor/environment.go rename to pkg/doctor/cmd_environment.go diff --git a/cmd/doctor/install.go b/pkg/doctor/cmd_install.go similarity index 100% rename from cmd/doctor/install.go rename to pkg/doctor/cmd_install.go diff --git a/cmd/go/commands.go b/pkg/go/cmd_commands.go similarity index 79% rename from cmd/go/commands.go rename to pkg/go/cmd_commands.go index 05d96aaf..02655577 100644 --- a/cmd/go/commands.go +++ b/pkg/go/cmd_commands.go @@ -14,9 +14,8 @@ // Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS. package gocmd -import "github.com/spf13/cobra" +import "github.com/host-uk/core/pkg/cli" -// AddCommands registers the 'go' command and all subcommands. -func AddCommands(root *cobra.Command) { - AddGoCommands(root) +func init() { + cli.RegisterCommands(AddGoCommands) } diff --git a/cmd/go/go_format.go b/pkg/go/cmd_format.go similarity index 100% rename from cmd/go/go_format.go rename to pkg/go/cmd_format.go diff --git a/cmd/go/go.go b/pkg/go/cmd_go.go similarity index 100% rename from cmd/go/go.go rename to pkg/go/cmd_go.go diff --git a/cmd/go/go_test_cmd.go b/pkg/go/cmd_gotest.go similarity index 98% rename from cmd/go/go_test_cmd.go rename to pkg/go/cmd_gotest.go index 72f0e3e6..2c14c66c 100644 --- a/cmd/go/go_test_cmd.go +++ b/pkg/go/cmd_gotest.go @@ -1,6 +1,7 @@ package gocmd import ( + "errors" "fmt" "os" "os/exec" @@ -179,7 +180,7 @@ func addGoCovCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "discover test packages"}), err) } if len(pkgs) == 0 { - return fmt.Errorf(i18n.T("cmd.go.cov.error.no_packages")) + return errors.New(i18n.T("cmd.go.cov.error.no_packages")) } pkg = strings.Join(pkgs, " ") } @@ -275,7 +276,7 @@ func addGoCovCommand(parent *cobra.Command) { "Actual": fmt.Sprintf("%.1f", totalCov), "Threshold": fmt.Sprintf("%.1f", covThreshold), }))) - return fmt.Errorf(i18n.T("cmd.go.cov.error.below_threshold")) + return errors.New(i18n.T("cmd.go.cov.error.below_threshold")) } if testErr != nil { diff --git a/cmd/go/go_tools.go b/pkg/go/cmd_tools.go similarity index 98% rename from cmd/go/go_tools.go rename to pkg/go/cmd_tools.go index d9a264bf..a58bb675 100644 --- a/cmd/go/go_tools.go +++ b/pkg/go/cmd_tools.go @@ -1,6 +1,7 @@ package gocmd import ( + "errors" "fmt" "os" "os/exec" @@ -193,7 +194,7 @@ func addGoWorkCommand(parent *cobra.Command) { // Auto-detect modules modules := findGoModules(".") if len(modules) == 0 { - return fmt.Errorf(i18n.T("cmd.go.work.error.no_modules")) + return errors.New(i18n.T("cmd.go.work.error.no_modules")) } for _, mod := range modules { execCmd := exec.Command("go", "work", "use", mod) diff --git a/cmd/php/php.go b/pkg/php/cmd.go similarity index 97% rename from cmd/php/php.go rename to pkg/php/cmd.go index efb0fdc0..571ec5a5 100644 --- a/cmd/php/php.go +++ b/pkg/php/cmd.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddPHPCommands) +} + // Style aliases from shared var ( successStyle = cli.SuccessStyle diff --git a/cmd/php/php_build.go b/pkg/php/cmd_build.go similarity index 91% rename from cmd/php/php_build.go rename to pkg/php/cmd_build.go index d46e322c..f086a312 100644 --- a/cmd/php/php_build.go +++ b/pkg/php/cmd_build.go @@ -2,12 +2,12 @@ package php import ( "context" + "errors" "fmt" "os" "strings" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/spf13/cobra" ) @@ -83,14 +83,14 @@ type linuxKitBuildOptions struct { } func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error { - if !phppkg.IsPHPProject(projectDir) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(projectDir) { + return errors.New(i18n.T("cmd.php.error.not_php")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker")) // Show detected configuration - config, err := phppkg.DetectDockerfileConfig(projectDir) + config, err := DetectDockerfileConfig(projectDir) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "detect project configuration"}), err) } @@ -105,7 +105,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO fmt.Println() // Build options - buildOpts := phppkg.DockerBuildOptions{ + buildOpts := DockerBuildOptions{ ProjectDir: projectDir, ImageName: opts.ImageName, Tag: opts.Tag, @@ -116,7 +116,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO } if buildOpts.ImageName == "" { - buildOpts.ImageName = phppkg.GetLaravelAppName(projectDir) + buildOpts.ImageName = GetLaravelAppName(projectDir) if buildOpts.ImageName == "" { buildOpts.ImageName = "php-app" } @@ -134,7 +134,7 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO } fmt.Println() - if err := phppkg.BuildDocker(ctx, buildOpts); err != nil { + if err := BuildDocker(ctx, buildOpts); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err) } @@ -147,13 +147,13 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO } func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error { - if !phppkg.IsPHPProject(projectDir) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(projectDir) { + return errors.New(i18n.T("cmd.php.error.not_php")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit")) - buildOpts := phppkg.LinuxKitBuildOptions{ + buildOpts := LinuxKitBuildOptions{ ProjectDir: projectDir, OutputPath: opts.OutputPath, Format: opts.Format, @@ -172,7 +172,7 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format) fmt.Println() - if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil { + if err := BuildLinuxKit(ctx, buildOpts); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "build"}), err) } @@ -201,19 +201,19 @@ func addPHPServeCommand(parent *cobra.Command) { // Try to detect from current directory cwd, err := os.Getwd() if err == nil { - imageName = phppkg.GetLaravelAppName(cwd) + imageName = GetLaravelAppName(cwd) if imageName != "" { imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-")) } } if imageName == "" { - return fmt.Errorf(i18n.T("cmd.php.serve.name_required")) + return errors.New(i18n.T("cmd.php.serve.name_required")) } } ctx := context.Background() - opts := phppkg.ServeOptions{ + opts := ServeOptions{ ImageName: imageName, Tag: serveTag, ContainerName: serveContainerName, @@ -245,7 +245,7 @@ func addPHPServeCommand(parent *cobra.Command) { dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) fmt.Println() - if err := phppkg.ServeProduction(ctx, opts); err != nil { + if err := ServeProduction(ctx, opts); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "start container"}), err) } @@ -279,7 +279,7 @@ func addPHPShellCommand(parent *cobra.Command) { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]})) - if err := phppkg.Shell(ctx, args[0]); err != nil { + if err := Shell(ctx, args[0]); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "open shell"}), err) } diff --git a/cmd/php/commands.go b/pkg/php/cmd_commands.go similarity index 100% rename from cmd/php/commands.go rename to pkg/php/cmd_commands.go diff --git a/cmd/php/php_deploy.go b/pkg/php/cmd_deploy.go similarity index 92% rename from cmd/php/php_deploy.go rename to pkg/php/cmd_deploy.go index 3a764e40..c48c25b0 100644 --- a/cmd/php/php_deploy.go +++ b/pkg/php/cmd_deploy.go @@ -8,7 +8,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/spf13/cobra" ) @@ -50,23 +49,23 @@ func addPHPDeployCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - env := phppkg.EnvProduction + env := EnvProduction if deployStaging { - env = phppkg.EnvStaging + env = EnvStaging } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env})) ctx := context.Background() - opts := phppkg.DeployOptions{ + opts := DeployOptions{ Dir: cwd, Environment: env, Force: deployForce, Wait: deployWait, } - status, err := phppkg.Deploy(ctx, opts) + status, err := Deploy(ctx, opts) if err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err) } @@ -74,7 +73,7 @@ func addPHPDeployCommand(parent *cobra.Command) { printDeploymentStatus(status) if deployWait { - if phppkg.IsDeploymentSuccessful(status.Status) { + if IsDeploymentSuccessful(status.Status) { fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Deployment completed"})) } else { fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status})) @@ -110,22 +109,22 @@ func addPHPDeployStatusCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - env := phppkg.EnvProduction + env := EnvProduction if deployStatusStaging { - env = phppkg.EnvStaging + env = EnvStaging } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("common.progress.checking", map[string]any{"Item": "deployment status"})) ctx := context.Background() - opts := phppkg.StatusOptions{ + opts := StatusOptions{ Dir: cwd, Environment: env, DeploymentID: deployStatusDeploymentID, } - status, err := phppkg.DeployStatus(ctx, opts) + status, err := DeployStatus(ctx, opts) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get status"}), err) } @@ -159,23 +158,23 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - env := phppkg.EnvProduction + env := EnvProduction if rollbackStaging { - env = phppkg.EnvStaging + env = EnvStaging } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env})) ctx := context.Background() - opts := phppkg.RollbackOptions{ + opts := RollbackOptions{ Dir: cwd, Environment: env, DeploymentID: rollbackDeploymentID, Wait: rollbackWait, } - status, err := phppkg.Rollback(ctx, opts) + status, err := Rollback(ctx, opts) if err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err) } @@ -183,7 +182,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) { printDeploymentStatus(status) if rollbackWait { - if phppkg.IsDeploymentSuccessful(status.Status) { + if IsDeploymentSuccessful(status.Status) { fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("common.success.completed", map[string]any{"Action": "Rollback completed"})) } else { fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status})) @@ -219,9 +218,9 @@ func addPHPDeployListCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - env := phppkg.EnvProduction + env := EnvProduction if deployListStaging { - env = phppkg.EnvStaging + env = EnvStaging } limit := deployListLimit @@ -233,7 +232,7 @@ func addPHPDeployListCommand(parent *cobra.Command) { ctx := context.Background() - deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit) + deployments, err := ListDeployments(ctx, cwd, env, limit) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list deployments"}), err) } @@ -257,7 +256,7 @@ func addPHPDeployListCommand(parent *cobra.Command) { parent.AddCommand(listCmd) } -func printDeploymentStatus(status *phppkg.DeploymentStatus) { +func printDeploymentStatus(status *DeploymentStatus) { // Status with color statusStyle := phpDeployStyle switch status.Status { @@ -310,7 +309,7 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) { } } -func printDeploymentSummary(index int, status *phppkg.DeploymentStatus) { +func printDeploymentSummary(index int, status *DeploymentStatus) { // Status with color statusStyle := phpDeployStyle switch status.Status { diff --git a/cmd/php/php_dev.go b/pkg/php/cmd_dev.go similarity index 88% rename from cmd/php/php_dev.go rename to pkg/php/cmd_dev.go index ca88f98a..60865768 100644 --- a/cmd/php/php_dev.go +++ b/pkg/php/cmd_dev.go @@ -3,6 +3,7 @@ package php import ( "bufio" "context" + "errors" "fmt" "os" "os/signal" @@ -12,7 +13,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/spf13/cobra" ) @@ -72,12 +72,12 @@ func runPHPDev(opts phpDevOptions) error { } // Check if this is a Laravel project - if !phppkg.IsLaravelProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_laravel")) + if !IsLaravelProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_laravel")) } // Get app name for display - appName := phppkg.GetLaravelAppName(cwd) + appName := GetLaravelAppName(cwd) if appName == "" { appName = "Laravel" } @@ -85,7 +85,7 @@ func runPHPDev(opts phpDevOptions) error { fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName})) // Detect services - services := phppkg.DetectServices(cwd) + services := DetectServices(cwd) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services")) for _, svc := range services { fmt.Printf(" %s %s\n", successStyle.Render("*"), svc) @@ -98,7 +98,7 @@ func runPHPDev(opts phpDevOptions) error { port = 8000 } - devOpts := phppkg.Options{ + devOpts := Options{ Dir: cwd, NoVite: opts.NoVite, NoHorizon: opts.NoHorizon, @@ -110,7 +110,7 @@ func runPHPDev(opts phpDevOptions) error { } // Create and start dev server - server := phppkg.NewDevServer(devOpts) + server := NewDevServer(devOpts) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -135,7 +135,7 @@ func runPHPDev(opts phpDevOptions) error { fmt.Println() // Print URLs - appURL := phppkg.GetLaravelAppURL(cwd) + appURL := GetLaravelAppURL(cwd) if appURL == "" { if opts.HTTPS { appURL = fmt.Sprintf("https://localhost:%d", port) @@ -146,7 +146,7 @@ func runPHPDev(opts phpDevOptions) error { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL)) // Check for Vite - if !opts.NoVite && containsService(services, phppkg.ServiceVite) { + if !opts.NoVite && containsService(services, ServiceVite) { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173")) } @@ -208,12 +208,12 @@ func runPHPLogs(service string, follow bool) error { return err } - if !phppkg.IsLaravelProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short")) + if !IsLaravelProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_laravel_short")) } // Create a minimal server just to access logs - server := phppkg.NewDevServer(phppkg.Options{Dir: cwd}) + server := NewDevServer(Options{Dir: cwd}) logsReader, err := server.Logs(service, follow) if err != nil { @@ -268,7 +268,7 @@ func runPHPStop() error { // We need to find running processes // This is a simplified version - in practice you'd want to track PIDs - server := phppkg.NewDevServer(phppkg.Options{Dir: cwd}) + server := NewDevServer(Options{Dir: cwd}) if err := server.Stop(); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "stop services"}), err) } @@ -295,11 +295,11 @@ func runPHPStatus() error { return err } - if !phppkg.IsLaravelProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short")) + if !IsLaravelProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_laravel_short")) } - appName := phppkg.GetLaravelAppName(cwd) + appName := GetLaravelAppName(cwd) if appName == "" { appName = "Laravel" } @@ -307,7 +307,7 @@ func runPHPStatus() error { fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.project")), appName) // Detect available services - services := phppkg.DetectServices(cwd) + services := DetectServices(cwd) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services"))) for _, svc := range services { style := getServiceStyle(string(svc)) @@ -316,19 +316,19 @@ func runPHPStatus() error { fmt.Println() // Package manager - pm := phppkg.DetectPackageManager(cwd) + pm := DetectPackageManager(cwd) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm) // FrankenPHP status - if phppkg.IsFrankenPHPProject(cwd) { + if IsFrankenPHPProject(cwd) { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP") } // SSL status - appURL := phppkg.GetLaravelAppURL(cwd) + appURL := GetLaravelAppURL(cwd) if appURL != "" { - domain := phppkg.ExtractDomainFromURL(appURL) - if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { + domain := ExtractDomainFromURL(appURL) + if CertsExist(domain, SSLOptions{}) { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed"))) } else { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup"))) @@ -362,9 +362,9 @@ func runPHPSSL(domain string) error { // Get domain from APP_URL if not specified if domain == "" { - appURL := phppkg.GetLaravelAppURL(cwd) + appURL := GetLaravelAppURL(cwd) if appURL != "" { - domain = phppkg.ExtractDomainFromURL(appURL) + domain = ExtractDomainFromURL(appURL) } } if domain == "" { @@ -372,32 +372,32 @@ func runPHPSSL(domain string) error { } // Check if mkcert is installed - if !phppkg.IsMkcertInstalled() { + if !IsMkcertInstalled() { fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed")) fmt.Printf("\n%s\n", i18n.T("common.hint.install_with")) fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_macos")) fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux")) - return fmt.Errorf(i18n.T("cmd.php.error.mkcert_not_installed")) + return errors.New(i18n.T("cmd.php.error.mkcert_not_installed")) } fmt.Printf("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain})) // Check if certs already exist - if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { + if CertsExist(domain, SSLOptions{}) { fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.skip")), i18n.T("cmd.php.ssl.certs_exist")) - certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) + certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile) return nil } // Setup SSL - if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil { + if err := SetupSSL(domain, SSLOptions{}); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "setup SSL"}), err) } - certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) + certFile, keyFile, _ := CertPaths(domain, SSLOptions{}) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("cmd.php.ssl.certs_created")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile) @@ -408,7 +408,7 @@ func runPHPSSL(domain string) error { // Helper functions for dev commands -func printServiceStatuses(statuses []phppkg.ServiceStatus) { +func printServiceStatuses(statuses []ServiceStatus) { for _, s := range statuses { style := getServiceStyle(s.Name) var statusText string @@ -488,7 +488,7 @@ func getServiceStyle(name string) lipgloss.Style { } } -func containsService(services []phppkg.DetectedService, target phppkg.DetectedService) bool { +func containsService(services []DetectedService, target DetectedService) bool { for _, s := range services { if s == target { return true diff --git a/cmd/php/php_packages.go b/pkg/php/cmd_packages.go similarity index 94% rename from cmd/php/php_packages.go rename to pkg/php/cmd_packages.go index 32fd60be..b587088e 100644 --- a/cmd/php/php_packages.go +++ b/pkg/php/cmd_packages.go @@ -5,7 +5,6 @@ import ( "os" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/spf13/cobra" ) @@ -37,7 +36,7 @@ func addPHPPackagesLinkCommand(parent *cobra.Command) { fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking")) - if err := phppkg.LinkPackages(cwd, args); err != nil { + if err := LinkPackages(cwd, args); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "link packages"}), err) } @@ -63,7 +62,7 @@ func addPHPPackagesUnlinkCommand(parent *cobra.Command) { fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking")) - if err := phppkg.UnlinkPackages(cwd, args); err != nil { + if err := UnlinkPackages(cwd, args); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "unlink packages"}), err) } @@ -88,7 +87,7 @@ func addPHPPackagesUpdateCommand(parent *cobra.Command) { fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating")) - if err := phppkg.UpdatePackages(cwd, args); err != nil { + if err := UpdatePackages(cwd, args); err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err) } @@ -111,7 +110,7 @@ func addPHPPackagesListCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - packages, err := phppkg.ListLinkedPackages(cwd) + packages, err := ListLinkedPackages(cwd) if err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "list packages"}), err) } diff --git a/cmd/php/qa_runner.go b/pkg/php/cmd_qa_runner.go similarity index 94% rename from cmd/php/qa_runner.go rename to pkg/php/cmd_qa_runner.go index 7658db61..b284a843 100644 --- a/cmd/php/qa_runner.go +++ b/pkg/php/cmd_qa_runner.go @@ -10,7 +10,6 @@ import ( "github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/host-uk/core/pkg/process" ) @@ -78,11 +77,11 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { } case "fmt": - formatter, found := phppkg.DetectFormatter(r.dir) + formatter, found := DetectFormatter(r.dir) if !found { return nil } - if formatter == phppkg.FormatterPint { + if formatter == FormatterPint { vendorBin := filepath.Join(r.dir, "vendor", "bin", "pint") cmd := "pint" if _, err := os.Stat(vendorBin); err == nil { @@ -103,7 +102,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { return nil case "stan": - _, found := phppkg.DetectAnalyser(r.dir) + _, found := DetectAnalyser(r.dir) if !found { return nil } @@ -121,7 +120,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { } case "psalm": - _, found := phppkg.DetectPsalm(r.dir) + _, found := DetectPsalm(r.dir) if !found { return nil } @@ -158,7 +157,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { // Tests depend on stan (or psalm if available) after := []string{"stan"} - if _, found := phppkg.DetectPsalm(r.dir); found { + if _, found := DetectPsalm(r.dir); found { after = []string{"psalm"} } @@ -171,7 +170,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { } case "rector": - if !phppkg.DetectRector(r.dir) { + if !DetectRector(r.dir) { return nil } vendorBin := filepath.Join(r.dir, "vendor", "bin", "rector") @@ -193,7 +192,7 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { } case "infection": - if !phppkg.DetectInfection(r.dir) { + if !DetectInfection(r.dir) { return nil } vendorBin := filepath.Join(r.dir, "vendor", "bin", "infection") @@ -215,11 +214,11 @@ func (r *QARunner) buildSpec(check string) *process.RunSpec { } // Run executes all QA checks and returns the results. -func (r *QARunner) Run(ctx context.Context, stages []phppkg.QAStage) (*QARunResult, error) { +func (r *QARunner) Run(ctx context.Context, stages []QAStage) (*QARunResult, error) { // Collect all checks from all stages var allChecks []string for _, stage := range stages { - checks := phppkg.GetQAChecks(r.dir, stage) + checks := GetQAChecks(r.dir, stage) allChecks = append(allChecks, checks...) } diff --git a/cmd/php/php_quality.go b/pkg/php/cmd_quality.go similarity index 89% rename from cmd/php/php_quality.go rename to pkg/php/cmd_quality.go index e3768b62..b1045d24 100644 --- a/cmd/php/php_quality.go +++ b/pkg/php/cmd_quality.go @@ -2,13 +2,13 @@ package php import ( "context" + "errors" "fmt" "os" "strings" "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/pkg/i18n" - phppkg "github.com/host-uk/core/pkg/php" "github.com/spf13/cobra" ) @@ -30,15 +30,15 @@ func addPHPTestCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("common.progress.running", map[string]any{"Task": "tests"})) ctx := context.Background() - opts := phppkg.TestOptions{ + opts := TestOptions{ Dir: cwd, Filter: testFilter, Parallel: testParallel, @@ -50,7 +50,7 @@ func addPHPTestCommand(parent *cobra.Command) { opts.Groups = []string{testGroup} } - if err := phppkg.RunTests(ctx, opts); err != nil { + if err := RunTests(ctx, opts); err != nil { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "run tests"}), err) } @@ -82,14 +82,14 @@ func addPHPFmtCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Detect formatter - formatter, found := phppkg.DetectFormatter(cwd) + formatter, found := DetectFormatter(cwd) if !found { - return fmt.Errorf(i18n.T("cmd.php.fmt.no_formatter")) + return errors.New(i18n.T("cmd.php.fmt.no_formatter")) } var msg string @@ -102,7 +102,7 @@ func addPHPFmtCommand(parent *cobra.Command) { ctx := context.Background() - opts := phppkg.FormatOptions{ + opts := FormatOptions{ Dir: cwd, Fix: fmtFix, Diff: fmtDiff, @@ -114,7 +114,7 @@ func addPHPFmtCommand(parent *cobra.Command) { opts.Paths = args } - if err := phppkg.Format(ctx, opts); err != nil { + if err := Format(ctx, opts); err != nil { if fmtFix { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err) } @@ -153,21 +153,21 @@ func addPHPStanCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Detect analyser - _, found := phppkg.DetectAnalyser(cwd) + _, found := DetectAnalyser(cwd) if !found { - return fmt.Errorf(i18n.T("cmd.php.analyse.no_analyser")) + return errors.New(i18n.T("cmd.php.analyse.no_analyser")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("common.progress.running", map[string]any{"Task": "static analysis"})) ctx := context.Background() - opts := phppkg.AnalyseOptions{ + opts := AnalyseOptions{ Dir: cwd, Level: stanLevel, Memory: stanMemory, @@ -179,7 +179,7 @@ func addPHPStanCommand(parent *cobra.Command) { opts.Paths = args } - if err := phppkg.Analyse(ctx, opts); err != nil { + if err := Analyse(ctx, opts); err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) } @@ -216,17 +216,17 @@ func addPHPPsalmCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Check if Psalm is available - _, found := phppkg.DetectPsalm(cwd) + _, found := DetectPsalm(cwd) if !found { fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.psalm.not_found")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.psalm.install")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup")) - return fmt.Errorf(i18n.T("cmd.php.error.psalm_not_installed")) + return errors.New(i18n.T("cmd.php.error.psalm_not_installed")) } var msg string @@ -239,7 +239,7 @@ func addPHPPsalmCommand(parent *cobra.Command) { ctx := context.Background() - opts := phppkg.PsalmOptions{ + opts := PsalmOptions{ Dir: cwd, Level: psalmLevel, Fix: psalmFix, @@ -248,7 +248,7 @@ func addPHPPsalmCommand(parent *cobra.Command) { Output: os.Stdout, } - if err := phppkg.RunPsalm(ctx, opts); err != nil { + if err := RunPsalm(ctx, opts); err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) } @@ -281,15 +281,15 @@ func addPHPAuditCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning")) ctx := context.Background() - results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{ + results, err := RunAudit(ctx, AuditOptions{ Dir: cwd, JSON: auditJSONOutput, Fix: auditFix, @@ -338,11 +338,11 @@ func addPHPAuditCommand(parent *cobra.Command) { if totalVulns > 0 { fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("common.label.warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns})) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.fix")), i18n.T("common.hint.fix_deps")) - return fmt.Errorf(i18n.T("cmd.php.error.vulns_found")) + return errors.New(i18n.T("cmd.php.error.vulns_found")) } if hasErrors { - return fmt.Errorf(i18n.T("cmd.php.audit.completed_errors")) + return errors.New(i18n.T("cmd.php.audit.completed_errors")) } fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.done")), i18n.T("cmd.php.audit.all_secure")) @@ -374,15 +374,15 @@ func addPHPSecurityCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.T("common.progress.running", map[string]any{"Task": "security checks"})) ctx := context.Background() - result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{ + result, err := RunSecurityChecks(ctx, SecurityOptions{ Dir: cwd, Severity: securitySeverity, JSON: securityJSONOutput, @@ -440,7 +440,7 @@ func addPHPSecurityCommand(parent *cobra.Command) { } if result.Summary.Critical > 0 || result.Summary.High > 0 { - return fmt.Errorf(i18n.T("cmd.php.error.critical_high_issues")) + return errors.New(i18n.T("cmd.php.error.critical_high_issues")) } return nil @@ -472,18 +472,18 @@ func addPHPQACommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Determine stages - opts := phppkg.QAOptions{ + opts := QAOptions{ Dir: cwd, Quick: qaQuick, Full: qaFull, Fix: qaFix, } - stages := phppkg.GetQAStages(opts) + stages := GetQAStages(opts) // Print header fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) @@ -567,9 +567,9 @@ func addPHPQACommand(parent *cobra.Command) { } // getCheckStage determines which stage a check belongs to. -func getCheckStage(checkName string, stages []phppkg.QAStage, dir string) string { +func getCheckStage(checkName string, stages []QAStage, dir string) string { for _, stage := range stages { - checks := phppkg.GetQAChecks(dir, stage) + checks := GetQAChecks(dir, stage) for _, c := range checks { if c == checkName { return string(stage) @@ -622,16 +622,16 @@ func addPHPRectorCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Check if Rector is available - if !phppkg.DetectRector(cwd) { + if !DetectRector(cwd) { fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.rector.not_found")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.rector.install")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup")) - return fmt.Errorf(i18n.T("cmd.php.error.rector_not_installed")) + return errors.New(i18n.T("cmd.php.error.rector_not_installed")) } var msg string @@ -644,7 +644,7 @@ func addPHPRectorCommand(parent *cobra.Command) { ctx := context.Background() - opts := phppkg.RectorOptions{ + opts := RectorOptions{ Dir: cwd, Fix: rectorFix, Diff: rectorDiff, @@ -652,7 +652,7 @@ func addPHPRectorCommand(parent *cobra.Command) { Output: os.Stdout, } - if err := phppkg.RunRector(ctx, opts); err != nil { + if err := RunRector(ctx, opts); err != nil { if rectorFix { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err) } @@ -696,15 +696,15 @@ func addPHPInfectionCommand(parent *cobra.Command) { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf(i18n.T("cmd.php.error.not_php")) + if !IsPHPProject(cwd) { + return errors.New(i18n.T("cmd.php.error.not_php")) } // Check if Infection is available - if !phppkg.DetectInfection(cwd) { + if !DetectInfection(cwd) { fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.php.infection.not_found")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.install")), i18n.T("cmd.php.infection.install")) - return fmt.Errorf(i18n.T("cmd.php.error.infection_not_installed")) + return errors.New(i18n.T("cmd.php.error.infection_not_installed")) } fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.T("common.progress.running", map[string]any{"Task": "mutation testing"})) @@ -712,7 +712,7 @@ func addPHPInfectionCommand(parent *cobra.Command) { ctx := context.Background() - opts := phppkg.InfectionOptions{ + opts := InfectionOptions{ Dir: cwd, MinMSI: infectionMinMSI, MinCoveredMSI: infectionMinCoveredMSI, @@ -722,7 +722,7 @@ func addPHPInfectionCommand(parent *cobra.Command) { Output: os.Stdout, } - if err := phppkg.RunInfection(ctx, opts); err != nil { + if err := RunInfection(ctx, opts); err != nil { return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err) } diff --git a/cmd/pkg/commands.go b/pkg/pkgcmd/cmd_commands.go similarity index 62% rename from cmd/pkg/commands.go rename to pkg/pkgcmd/cmd_commands.go index 3a80338e..33298f9d 100644 --- a/cmd/pkg/commands.go +++ b/pkg/pkgcmd/cmd_commands.go @@ -1,4 +1,4 @@ -// Package pkg provides GitHub package management for host-uk repositories. +// Package pkgcmd provides GitHub package management for host-uk repositories. // // Commands: // - search: Search GitHub org for repos (cached for 1 hour) @@ -9,11 +9,4 @@ // // Uses gh CLI for authenticated GitHub access. Results are cached in // .core/cache/ within the workspace directory. -package pkg - -import "github.com/spf13/cobra" - -// AddCommands registers the 'pkg' command and all subcommands. -func AddCommands(root *cobra.Command) { - AddPkgCommands(root) -} +package pkgcmd diff --git a/cmd/pkg/pkg_install.go b/pkg/pkgcmd/cmd_install.go similarity index 91% rename from cmd/pkg/pkg_install.go rename to pkg/pkgcmd/cmd_install.go index b8cdceae..f10342c2 100644 --- a/cmd/pkg/pkg_install.go +++ b/pkg/pkgcmd/cmd_install.go @@ -1,7 +1,8 @@ -package pkg +package pkgcmd import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -13,8 +14,8 @@ import ( ) var ( - installTargetDir string - installAddToReg bool + installTargetDir string + installAddToReg bool ) // addPkgInstallCommand adds the 'pkg install' command. @@ -25,7 +26,7 @@ func addPkgInstallCommand(parent *cobra.Command) { Long: i18n.T("cmd.pkg.install.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.pkg.error.repo_required")) + return errors.New(i18n.T("cmd.pkg.error.repo_required")) } return runPkgInstall(args[0], installTargetDir, installAddToReg) }, @@ -43,7 +44,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error { // Parse org/repo parts := strings.Split(repoArg, "/") if len(parts) != 2 { - return fmt.Errorf(i18n.T("cmd.pkg.error.invalid_repo_format")) + return errors.New(i18n.T("cmd.pkg.error.invalid_repo_format")) } org, repoName := parts[0], parts[1] @@ -78,7 +79,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error { } if err := os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "create directory"}), err) + return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "create directory"}), err) } fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName) @@ -110,7 +111,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error { func addToRegistryFile(org, repoName string) error { regPath, err := repos.FindRegistry() if err != nil { - return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) + return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml")) } reg, err := repos.LoadRegistry(regPath) diff --git a/cmd/pkg/pkg_manage.go b/pkg/pkgcmd/cmd_manage.go similarity index 91% rename from cmd/pkg/pkg_manage.go rename to pkg/pkgcmd/cmd_manage.go index d07d9d84..8e4e3e15 100644 --- a/cmd/pkg/pkg_manage.go +++ b/pkg/pkgcmd/cmd_manage.go @@ -1,6 +1,7 @@ -package pkg +package pkgcmd import ( + "errors" "fmt" "os" "os/exec" @@ -29,12 +30,12 @@ func addPkgListCommand(parent *cobra.Command) { func runPkgList() error { regPath, err := repos.FindRegistry() if err != nil { - return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml_workspace")) + return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml_workspace")) } reg, err := repos.LoadRegistry(regPath) if err != nil { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) + return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) } basePath := reg.BasePath @@ -101,7 +102,7 @@ func addPkgUpdateCommand(parent *cobra.Command) { Long: i18n.T("cmd.pkg.update.long"), RunE: func(cmd *cobra.Command, args []string) error { if !updateAll && len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.pkg.error.specify_package")) + return errors.New(i18n.T("cmd.pkg.error.specify_package")) } return runPkgUpdate(args, updateAll) }, @@ -115,12 +116,12 @@ func addPkgUpdateCommand(parent *cobra.Command) { func runPkgUpdate(packages []string, all bool) error { regPath, err := repos.FindRegistry() if err != nil { - return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) + return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml")) } reg, err := repos.LoadRegistry(regPath) if err != nil { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) + return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) } basePath := reg.BasePath @@ -195,12 +196,12 @@ func addPkgOutdatedCommand(parent *cobra.Command) { func runPkgOutdated() error { regPath, err := repos.FindRegistry() if err != nil { - return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml")) + return errors.New(i18n.T("cmd.pkg.error.no_repos_yaml")) } reg, err := repos.LoadRegistry(regPath) if err != nil { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) + return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "load registry"}), err) } basePath := reg.BasePath diff --git a/cmd/pkg/pkg.go b/pkg/pkgcmd/cmd_pkg.go similarity index 84% rename from cmd/pkg/pkg.go rename to pkg/pkgcmd/cmd_pkg.go index 76ca31cd..de463da9 100644 --- a/cmd/pkg/pkg.go +++ b/pkg/pkgcmd/cmd_pkg.go @@ -1,5 +1,5 @@ -// Package pkg provides package management commands for core-* repos. -package pkg +// Package pkgcmd provides package management commands for core-* repos. +package pkgcmd import ( "github.com/host-uk/core/pkg/cli" @@ -7,6 +7,10 @@ import ( "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddPkgCommands) +} + // Style and utility aliases var ( repoNameStyle = cli.RepoNameStyle diff --git a/cmd/pkg/pkg_search.go b/pkg/pkgcmd/cmd_search.go similarity index 92% rename from cmd/pkg/pkg_search.go rename to pkg/pkgcmd/cmd_search.go index cece2667..02e1eae2 100644 --- a/cmd/pkg/pkg_search.go +++ b/pkg/pkgcmd/cmd_search.go @@ -1,7 +1,8 @@ -package pkg +package pkgcmd import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -93,7 +94,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error // Fetch from GitHub if not cached if !fromCache { if !ghAuthenticated() { - return fmt.Errorf(i18n.T("cmd.pkg.error.gh_not_authenticated")) + return errors.New(i18n.T("cmd.pkg.error.gh_not_authenticated")) } if os.Getenv("GH_TOKEN") != "" { @@ -112,13 +113,13 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error fmt.Println() errStr := strings.TrimSpace(string(output)) if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") { - return fmt.Errorf(i18n.T("cmd.pkg.error.auth_failed")) + return errors.New(i18n.T("cmd.pkg.error.auth_failed")) } - return fmt.Errorf(i18n.T("cmd.pkg.error.search_failed"), errStr) + return fmt.Errorf("%s: %s", i18n.T("cmd.pkg.error.search_failed"), errStr) } if err := json.Unmarshal(output, &ghRepos); err != nil { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "parse results"}), err) + return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "parse results"}), err) } if c != nil { @@ -149,7 +150,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error return filtered[i].Name < filtered[j].Name }) - fmt.Printf(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n") + fmt.Print(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n") for _, r := range filtered { visibility := "" diff --git a/pkg/sdk/cmd_commands.go b/pkg/sdk/cmd_commands.go new file mode 100644 index 00000000..d0b5ecc7 --- /dev/null +++ b/pkg/sdk/cmd_commands.go @@ -0,0 +1,8 @@ +// SDK validation and API compatibility commands. +// +// Commands: +// - diff: Check for breaking API changes between spec versions +// - validate: Validate OpenAPI spec syntax +// +// Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk +package sdk diff --git a/cmd/sdk/sdk.go b/pkg/sdk/cmd_sdk.go similarity index 88% rename from cmd/sdk/sdk.go rename to pkg/sdk/cmd_sdk.go index df3d004b..f4926f2d 100644 --- a/cmd/sdk/sdk.go +++ b/pkg/sdk/cmd_sdk.go @@ -1,16 +1,19 @@ -// Package sdk provides SDK validation and API compatibility commands. package sdk import ( + "errors" "fmt" "os" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - sdkpkg "github.com/host-uk/core/pkg/sdk" "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddSDKCommands) +} + // SDK styles (aliases to shared) var ( sdkHeaderStyle = cli.TitleStyle @@ -48,7 +51,7 @@ var sdkValidateCmd = &cobra.Command{ }, } -func init() { +func initSDKCommands() { // sdk diff flags sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base")) sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec")) @@ -61,6 +64,12 @@ func init() { sdkCmd.AddCommand(sdkValidateCmd) } +// AddSDKCommands registers the 'sdk' command and all subcommands. +func AddSDKCommands(root *cobra.Command) { + initSDKCommands() + root.AddCommand(sdkCmd) +} + func runSDKDiff(basePath, specPath string) error { projectDir, err := os.Getwd() if err != nil { @@ -69,7 +78,7 @@ func runSDKDiff(basePath, specPath string) error { // Detect current spec if not provided if specPath == "" { - s := sdkpkg.New(projectDir, nil) + s := New(projectDir, nil) specPath, err = s.DetectSpec() if err != nil { return err @@ -77,7 +86,7 @@ func runSDKDiff(basePath, specPath string) error { } if basePath == "" { - return fmt.Errorf(i18n.T("cmd.sdk.diff.error.base_required")) + return errors.New(i18n.T("cmd.sdk.diff.error.base_required")) } fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("common.progress.checking", map[string]any{"Item": "breaking changes"})) @@ -85,7 +94,7 @@ func runSDKDiff(basePath, specPath string) error { fmt.Printf(" %s %s\n", i18n.T("common.label.current"), sdkDimStyle.Render(specPath)) fmt.Println() - result, err := sdkpkg.Diff(basePath, specPath) + result, err := Diff(basePath, specPath) if err != nil { fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("common.label.error")), err) os.Exit(2) @@ -109,7 +118,7 @@ func runSDKValidate(specPath string) error { return fmt.Errorf("%s: %w", i18n.T("common.error.failed", map[string]any{"Action": "get working directory"}), err) } - s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath}) + s := New(projectDir, &Config{Spec: specPath}) fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.label.sdk")), i18n.T("cmd.sdk.validate.validating")) diff --git a/cmd/setup/setup_bootstrap.go b/pkg/setup/cmd_bootstrap.go similarity index 98% rename from cmd/setup/setup_bootstrap.go rename to pkg/setup/cmd_bootstrap.go index afada064..96de5bbc 100644 --- a/cmd/setup/setup_bootstrap.go +++ b/pkg/setup/cmd_bootstrap.go @@ -1,4 +1,4 @@ -// setup_bootstrap.go implements bootstrap mode for new workspaces. +// cmd_bootstrap.go implements bootstrap mode for new workspaces. // // Bootstrap mode is activated when no repos.yaml exists in the current // directory or any parent. It clones core-devops first, then uses its diff --git a/cmd/setup/commands.go b/pkg/setup/cmd_commands.go similarity index 79% rename from cmd/setup/commands.go rename to pkg/setup/cmd_commands.go index da919b04..4bf46a68 100644 --- a/cmd/setup/commands.go +++ b/pkg/setup/cmd_commands.go @@ -23,9 +23,16 @@ // Uses gh CLI with HTTPS when authenticated, falls back to SSH. package setup -import "github.com/spf13/cobra" +import ( + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) -// AddCommands registers the 'setup' command and all subcommands. -func AddCommands(root *cobra.Command) { +func init() { + cli.RegisterCommands(AddSetupCommands) +} + +// AddSetupCommands registers the 'setup' command and all subcommands. +func AddSetupCommands(root *cobra.Command) { AddSetupCommand(root) } diff --git a/cmd/setup/setup_registry.go b/pkg/setup/cmd_registry.go similarity index 99% rename from cmd/setup/setup_registry.go rename to pkg/setup/cmd_registry.go index cec49510..31a108c8 100644 --- a/cmd/setup/setup_registry.go +++ b/pkg/setup/cmd_registry.go @@ -1,4 +1,4 @@ -// setup_registry.go implements registry mode for cloning packages. +// cmd_registry.go implements registry mode for cloning packages. // // Registry mode is activated when a repos.yaml exists. It reads the registry // and clones all (or selected) packages into the configured packages directory. diff --git a/cmd/setup/setup_repo.go b/pkg/setup/cmd_repo.go similarity index 98% rename from cmd/setup/setup_repo.go rename to pkg/setup/cmd_repo.go index 1b7461a1..330313a0 100644 --- a/cmd/setup/setup_repo.go +++ b/pkg/setup/cmd_repo.go @@ -1,4 +1,4 @@ -// setup_repo.go implements repository setup with .core/ configuration. +// cmd_repo.go implements repository setup with .core/ configuration. // // When running setup in an existing git repository, this generates // build.yaml, release.yaml, and test.yaml configurations based on diff --git a/cmd/setup/setup.go b/pkg/setup/cmd_setup.go similarity index 97% rename from cmd/setup/setup.go rename to pkg/setup/cmd_setup.go index df643e4b..142450fc 100644 --- a/cmd/setup/setup.go +++ b/pkg/setup/cmd_setup.go @@ -41,7 +41,7 @@ var setupCmd = &cobra.Command{ }, } -func init() { +func initSetupFlags() { setupCmd.Flags().StringVar(®istryPath, "registry", "", i18n.T("cmd.setup.flag.registry")) setupCmd.Flags().StringVar(&only, "only", "", i18n.T("cmd.setup.flag.only")) setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, i18n.T("cmd.setup.flag.dry_run")) @@ -52,5 +52,6 @@ func init() { // AddSetupCommand adds the 'setup' command to the given parent command. func AddSetupCommand(root *cobra.Command) { + initSetupFlags() root.AddCommand(setupCmd) } diff --git a/cmd/setup/setup_wizard.go b/pkg/setup/cmd_wizard.go similarity index 98% rename from cmd/setup/setup_wizard.go rename to pkg/setup/cmd_wizard.go index eba09204..c89348cd 100644 --- a/cmd/setup/setup_wizard.go +++ b/pkg/setup/cmd_wizard.go @@ -1,4 +1,4 @@ -// setup_wizard.go implements the interactive package selection wizard. +// cmd_wizard.go implements the interactive package selection wizard. // // Uses charmbracelet/huh for a rich terminal UI with multi-select checkboxes. // Falls back to non-interactive mode when not in a TTY or --all is specified. diff --git a/cmd/test/commands.go b/pkg/test/cmd_commands.go similarity index 73% rename from cmd/test/commands.go rename to pkg/test/cmd_commands.go index c52c47c1..4cebd347 100644 --- a/cmd/test/commands.go +++ b/pkg/test/cmd_commands.go @@ -11,9 +11,8 @@ // Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json package testcmd -import "github.com/spf13/cobra" +import "github.com/host-uk/core/pkg/cli" -// AddCommands registers the 'test' command and all subcommands. -func AddCommands(root *cobra.Command) { - root.AddCommand(testCmd) +func init() { + cli.RegisterCommands(AddTestCommands) } diff --git a/cmd/test/test.go b/pkg/test/cmd_main.go similarity index 89% rename from cmd/test/test.go rename to pkg/test/cmd_main.go index 06d9d566..32f64c6d 100644 --- a/cmd/test/test.go +++ b/pkg/test/cmd_main.go @@ -41,7 +41,7 @@ var testCmd = &cobra.Command{ }, } -func init() { +func initTestFlags() { testCmd.Flags().BoolVar(&testVerbose, "verbose", false, i18n.T("cmd.test.flag.verbose")) testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("common.flag.coverage")) testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.test.flag.short")) @@ -50,3 +50,9 @@ func init() { testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.test.flag.race")) testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.test.flag.json")) } + +// AddTestCommands registers the 'test' command and all subcommands. +func AddTestCommands(root *cobra.Command) { + initTestFlags() + root.AddCommand(testCmd) +} diff --git a/cmd/test/test_output.go b/pkg/test/cmd_output.go similarity index 100% rename from cmd/test/test_output.go rename to pkg/test/cmd_output.go diff --git a/cmd/test/test_runner.go b/pkg/test/cmd_runner.go similarity index 94% rename from cmd/test/test_runner.go rename to pkg/test/cmd_runner.go index 813380ac..a120ffcb 100644 --- a/cmd/test/test_runner.go +++ b/pkg/test/cmd_runner.go @@ -2,6 +2,7 @@ package testcmd import ( "bufio" + "errors" "fmt" "io" "os" @@ -15,7 +16,7 @@ import ( func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error { // Detect if we're in a Go project if _, err := os.Stat("go.mod"); os.IsNotExist(err) { - return fmt.Errorf(i18n.T("cmd.test.error.no_go_mod")) + return errors.New(i18n.T("cmd.test.error.no_go_mod")) } // Build command arguments @@ -93,7 +94,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo // JSON output for CI/agents printJSONResults(results, exitCode) if exitCode != 0 { - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) + return errors.New(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) } return nil } @@ -109,7 +110,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo if exitCode != 0 { fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed")) - return fmt.Errorf(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) + return errors.New(i18n.T("common.error.failed", map[string]any{"Action": "run tests"})) } fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("common.result.all_passed")) diff --git a/cmd/vm/commands.go b/pkg/vm/cmd_commands.go similarity index 75% rename from cmd/vm/commands.go rename to pkg/vm/cmd_commands.go index 2052bbf2..2631e824 100644 --- a/cmd/vm/commands.go +++ b/pkg/vm/cmd_commands.go @@ -11,10 +11,3 @@ // Uses qemu or hyperkit depending on system availability. // Templates are built from YAML definitions and can include variables. package vm - -import "github.com/spf13/cobra" - -// AddCommands registers the 'vm' command and all subcommands. -func AddCommands(root *cobra.Command) { - AddVMCommands(root) -} diff --git a/cmd/vm/container.go b/pkg/vm/cmd_container.go similarity index 96% rename from cmd/vm/container.go rename to pkg/vm/cmd_container.go index 5d0907dd..fd961f9a 100644 --- a/cmd/vm/container.go +++ b/pkg/vm/cmd_container.go @@ -2,6 +2,7 @@ package vm import ( "context" + "errors" "fmt" "io" "os" @@ -47,7 +48,7 @@ func addVMRunCommand(parent *cobra.Command) { // Otherwise, require an image path if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.vm.run.error.image_required")) + return errors.New(i18n.T("cmd.vm.run.error.image_required")) } image := args[0] @@ -210,7 +211,7 @@ func addVMStopCommand(parent *cobra.Command) { Long: i18n.T("cmd.vm.stop.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.vm.error.id_required")) + return errors.New(i18n.T("cmd.vm.error.id_required")) } return stopContainer(args[0]) }, @@ -259,11 +260,11 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s switch len(matches) { case 0: - return "", fmt.Errorf(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID})) + return "", errors.New(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID})) case 1: return matches[0].ID, nil default: - return "", fmt.Errorf(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID})) + return "", errors.New(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID})) } } @@ -277,7 +278,7 @@ func addVMLogsCommand(parent *cobra.Command) { Long: i18n.T("cmd.vm.logs.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.vm.error.id_required")) + return errors.New(i18n.T("cmd.vm.error.id_required")) } return viewLogs(args[0], logsFollow) }, @@ -318,7 +319,7 @@ func addVMExecCommand(parent *cobra.Command) { Long: i18n.T("cmd.vm.exec.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) < 2 { - return fmt.Errorf(i18n.T("cmd.vm.error.id_and_cmd_required")) + return errors.New(i18n.T("cmd.vm.error.id_and_cmd_required")) } return execInContainer(args[0], args[1:]) }, diff --git a/cmd/vm/templates.go b/pkg/vm/cmd_templates.go similarity index 96% rename from cmd/vm/templates.go rename to pkg/vm/cmd_templates.go index 700008d0..fa27e9e1 100644 --- a/cmd/vm/templates.go +++ b/pkg/vm/cmd_templates.go @@ -2,6 +2,7 @@ package vm import ( "context" + "errors" "fmt" "os" "os/exec" @@ -40,7 +41,7 @@ func addTemplatesShowCommand(parent *cobra.Command) { Long: i18n.T("cmd.vm.templates.show.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.vm.error.template_required")) + return errors.New(i18n.T("cmd.vm.error.template_required")) } return showTemplate(args[0]) }, @@ -57,7 +58,7 @@ func addTemplatesVarsCommand(parent *cobra.Command) { Long: i18n.T("cmd.vm.templates.vars.long"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf(i18n.T("cmd.vm.error.template_required")) + return errors.New(i18n.T("cmd.vm.error.template_required")) } return showTemplateVars(args[0]) }, @@ -177,7 +178,7 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai // Find the built image (linuxkit creates .iso or other format) imagePath := findBuiltImage(outputPath) if imagePath == "" { - return fmt.Errorf(i18n.T("cmd.vm.error.no_image_found")) + return errors.New(i18n.T("cmd.vm.error.no_image_found")) } fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.image")), imagePath) @@ -286,7 +287,7 @@ func lookupLinuxKit() (string, error) { } } - return "", fmt.Errorf(i18n.T("cmd.vm.error.linuxkit_not_found")) + return "", errors.New(i18n.T("cmd.vm.error.linuxkit_not_found")) } // ParseVarFlags parses --var flags into a map. diff --git a/cmd/vm/vm.go b/pkg/vm/cmd_vm.go similarity index 94% rename from cmd/vm/vm.go rename to pkg/vm/cmd_vm.go index 9ab83bb2..8efb6c56 100644 --- a/cmd/vm/vm.go +++ b/pkg/vm/cmd_vm.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/cobra" ) +func init() { + cli.RegisterCommands(AddVMCommands) +} + // Style aliases from shared var ( repoNameStyle = cli.RepoNameStyle