From a2bad1c0aa5745d5d0bcd7c4bf0e8c7badc1f07f Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 30 Jan 2026 00:47:54 +0000 Subject: [PATCH] refactor(cmd): migrate CLI from clir to cobra Replace leaanthony/clir with spf13/cobra across all command packages. This provides better subcommand handling, built-in shell completion, and a more widely-used CLI framework. Changes: - Update cmd/core.go with cobra root command and completion support - Convert all subcommand packages to use *cobra.Command - Use init() functions for flag registration instead of inline setup - Maintain all existing functionality and flag behaviors Co-Authored-By: Claude Opus 4.5 --- cmd/ai/ai.go | 4 +- cmd/ai/ai_git.go | 153 +++-- cmd/ai/ai_tasks.go | 140 +++-- cmd/ai/ai_updates.go | 132 ++-- cmd/ai/commands.go | 96 +-- cmd/build/build.go | 184 +++--- cmd/build/commands.go | 6 +- cmd/ci/ci_release.go | 113 ++-- cmd/ci/commands.go | 6 +- cmd/core.go | 94 ++- cmd/core_ci.go | 12 +- cmd/core_dev.go | 30 +- cmd/dev/dev.go | 54 +- cmd/dev/dev_api.go | 10 +- cmd/dev/dev_ci.go | 46 +- cmd/dev/dev_commit.go | 33 +- cmd/dev/dev_health.go | 33 +- cmd/dev/dev_impact.go | 41 +- cmd/dev/dev_issues.go | 44 +- cmd/dev/dev_pull.go | 33 +- cmd/dev/dev_push.go | 33 +- cmd/dev/dev_reviews.go | 39 +- cmd/dev/dev_sync.go | 29 +- cmd/dev/dev_vm.go | 305 +++++---- cmd/dev/dev_work.go | 38 +- cmd/docs/commands.go | 6 +- cmd/docs/docs.go | 21 +- cmd/docs/list.go | 20 +- cmd/docs/sync.go | 30 +- cmd/doctor/commands.go | 6 +- cmd/doctor/doctor.go | 26 +- cmd/go/commands.go | 6 +- cmd/go/go.go | 28 +- cmd/go/go_format.go | 122 ++-- cmd/go/go_test_cmd.go | 313 ++++----- cmd/go/go_tools.go | 350 ++++++----- cmd/php/commands.go | 6 +- cmd/php/php.go | 24 +- cmd/php/php_build.go | 303 ++++----- cmd/php/php_deploy.go | 451 ++++++------- cmd/php/php_dev.go | 150 +++-- cmd/php/php_packages.go | 280 +++++---- cmd/php/php_quality.go | 1324 ++++++++++++++++++++------------------- cmd/pkg/commands.go | 6 +- cmd/pkg/pkg.go | 24 +- cmd/pkg/pkg_install.go | 45 +- cmd/pkg/pkg_manage.go | 79 ++- cmd/pkg/pkg_search.go | 75 ++- cmd/sdk/commands.go | 6 +- cmd/sdk/sdk.go | 65 +- cmd/setup/commands.go | 6 +- cmd/setup/setup.go | 66 +- cmd/test/commands.go | 6 +- cmd/test/test.go | 70 ++- cmd/vm/commands.go | 6 +- cmd/vm/container.go | 226 +++---- cmd/vm/templates.go | 93 +-- cmd/vm/vm.go | 30 +- 58 files changed, 3258 insertions(+), 2719 deletions(-) diff --git a/cmd/ai/ai.go b/cmd/ai/ai.go index cb516e42..0e3dbd10 100644 --- a/cmd/ai/ai.go +++ b/cmd/ai/ai.go @@ -5,7 +5,7 @@ package ai import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared package @@ -53,7 +53,7 @@ var ( ) // AddAgenticCommands adds the agentic task management commands to the ai command. -func AddAgenticCommands(parent *clir.Command) { +func AddAgenticCommands(parent *cobra.Command) { // Task listing and viewing addTasksCommand(parent) addTaskCommand(parent) diff --git a/cmd/ai/ai_git.go b/cmd/ai/ai_git.go index 111efc8a..560812ce 100644 --- a/cmd/ai/ai_git.go +++ b/cmd/ai/ai_git.go @@ -12,47 +12,44 @@ import ( "time" "github.com/host-uk/core/pkg/agentic" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addTaskCommitCommand(parent *clir.Command) { - var message string - var scope string - var push bool +// task:commit command flags +var ( + taskCommitMessage string + taskCommitScope string + taskCommitPush bool +) - cmd := parent.NewSubCommand("task:commit", "Auto-commit changes with task reference") - cmd.LongDescription("Creates a git commit with a task reference and co-author attribution.\n\n" + - "Commit message format:\n" + - " feat(scope): description\n" + - "\n" + - " Task: #123\n" + - " Co-Authored-By: Claude \n\n" + - "Examples:\n" + - " core ai task:commit abc123 --message 'add user authentication'\n" + - " core ai task:commit abc123 -m 'fix login bug' --scope auth\n" + - " core ai task:commit abc123 -m 'update docs' --push") +// task:pr command flags +var ( + taskPRTitle string + taskPRDraft bool + taskPRLabels string + taskPRBase string +) - cmd.StringFlag("message", "Commit message (without task reference)", &message) - cmd.StringFlag("m", "Commit message (short form)", &message) - cmd.StringFlag("scope", "Scope for the commit type (e.g., auth, api, ui)", &scope) - cmd.BoolFlag("push", "Push changes after committing", &push) +var taskCommitCmd = &cobra.Command{ + Use: "task:commit [task-id]", + Short: "Auto-commit changes with task reference", + Long: `Creates a git commit with a task reference and co-author attribution. - cmd.Action(func() error { - // Find task ID from args - args := os.Args - var taskID string - for i, arg := range args { - if arg == "task:commit" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - taskID = args[i+1] - break - } - } +Commit message format: + feat(scope): description - if taskID == "" { - return fmt.Errorf("task ID required") - } + Task: #123 + Co-Authored-By: Claude - if message == "" { +Examples: + core ai task:commit abc123 --message 'add user authentication' + core ai task:commit abc123 -m 'fix login bug' --scope auth + core ai task:commit abc123 -m 'update docs' --push`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + taskID := args[0] + + if taskCommitMessage == "" { return fmt.Errorf("commit message required (--message or -m)") } @@ -75,10 +72,10 @@ func addTaskCommitCommand(parent *clir.Command) { // Build commit message with optional scope commitType := inferCommitType(task.Labels) var fullMessage string - if scope != "" { - fullMessage = fmt.Sprintf("%s(%s): %s", commitType, scope, message) + if taskCommitScope != "" { + fullMessage = fmt.Sprintf("%s(%s): %s", commitType, taskCommitScope, taskCommitMessage) } else { - fullMessage = fmt.Sprintf("%s: %s", commitType, message) + fullMessage = fmt.Sprintf("%s: %s", commitType, taskCommitMessage) } // Get current directory @@ -107,7 +104,7 @@ func addTaskCommitCommand(parent *clir.Command) { fmt.Printf("%s Committed: %s\n", successStyle.Render(">>"), fullMessage) // Push if requested - if push { + if taskCommitPush { fmt.Printf("%s Pushing changes...\n", dimStyle.Render(">>")) if err := agentic.PushChanges(ctx, cwd); err != nil { return fmt.Errorf("failed to push: %w", err) @@ -116,43 +113,24 @@ func addTaskCommitCommand(parent *clir.Command) { } return nil - }) + }, } -func addTaskPRCommand(parent *clir.Command) { - var title string - var draft bool - var labels string - var base string +var taskPRCmd = &cobra.Command{ + Use: "task:pr [task-id]", + Short: "Create a pull request for a task", + Long: `Creates a GitHub pull request linked to a task. - cmd := parent.NewSubCommand("task:pr", "Create a pull request for a task") - cmd.LongDescription("Creates a GitHub pull request linked to a task.\n\n" + - "Requires the GitHub CLI (gh) to be installed and authenticated.\n\n" + - "Examples:\n" + - " core ai task:pr abc123\n" + - " core ai task:pr abc123 --title 'Add authentication feature'\n" + - " core ai task:pr abc123 --draft --labels 'enhancement,needs-review'\n" + - " core ai task:pr abc123 --base develop") +Requires the GitHub CLI (gh) to be installed and authenticated. - cmd.StringFlag("title", "PR title (defaults to task title)", &title) - cmd.BoolFlag("draft", "Create as draft PR", &draft) - cmd.StringFlag("labels", "Labels to add (comma-separated)", &labels) - cmd.StringFlag("base", "Base branch (defaults to main)", &base) - - cmd.Action(func() error { - // Find task ID from args - args := os.Args - var taskID string - for i, arg := range args { - if arg == "task:pr" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - taskID = args[i+1] - break - } - } - - if taskID == "" { - return fmt.Errorf("task ID required") - } +Examples: + core ai task:pr abc123 + core ai task:pr abc123 --title 'Add authentication feature' + core ai task:pr abc123 --draft --labels 'enhancement,needs-review' + core ai task:pr abc123 --base develop`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + taskID := args[0] cfg, err := agentic.LoadConfig("") if err != nil { @@ -197,13 +175,13 @@ func addTaskPRCommand(parent *clir.Command) { // Build PR options opts := agentic.PROptions{ - Title: title, - Draft: draft, - Base: base, + Title: taskPRTitle, + Draft: taskPRDraft, + Base: taskPRBase, } - if labels != "" { - opts.Labels = strings.Split(labels, ",") + if taskPRLabels != "" { + opts.Labels = strings.Split(taskPRLabels, ",") } // Create PR @@ -217,7 +195,28 @@ func addTaskPRCommand(parent *clir.Command) { fmt.Printf(" URL: %s\n", prURL) return nil - }) + }, +} + +func init() { + // task:commit command flags + taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", "Commit message (without task reference)") + taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", "Scope for the commit type (e.g., auth, api, ui)") + taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, "Push changes after committing") + + // task:pr command flags + taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", "PR title (defaults to task title)") + taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, "Create as draft PR") + taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", "Labels to add (comma-separated)") + taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", "Base branch (defaults to main)") +} + +func addTaskCommitCommand(parent *cobra.Command) { + parent.AddCommand(taskCommitCmd) +} + +func addTaskPRCommand(parent *cobra.Command) { + parent.AddCommand(taskPRCmd) } // inferCommitType infers the commit type from task labels. diff --git a/cmd/ai/ai_tasks.go b/cmd/ai/ai_tasks.go index 3cea22bd..e390e9c2 100644 --- a/cmd/ai/ai_tasks.go +++ b/cmd/ai/ai_tasks.go @@ -11,34 +11,41 @@ import ( "time" "github.com/host-uk/core/pkg/agentic" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addTasksCommand(parent *clir.Command) { - var status string - var priority string - var labels string - var limit int - var project string +// tasks command flags +var ( + tasksStatus string + tasksPriority string + tasksLabels string + tasksLimit int + tasksProject string +) - cmd := parent.NewSubCommand("tasks", "List available tasks from core-agentic") - cmd.LongDescription("Lists tasks from the core-agentic service.\n\n" + - "Configuration is loaded from:\n" + - " 1. Environment variables (AGENTIC_TOKEN, AGENTIC_BASE_URL)\n" + - " 2. .env file in current directory\n" + - " 3. ~/.core/agentic.yaml\n\n" + - "Examples:\n" + - " core ai tasks\n" + - " core ai tasks --status pending --priority high\n" + - " core ai tasks --labels bug,urgent") +// task command flags +var ( + taskAutoSelect bool + taskClaim bool + taskShowContext bool +) - cmd.StringFlag("status", "Filter by status (pending, in_progress, completed, blocked)", &status) - cmd.StringFlag("priority", "Filter by priority (critical, high, medium, low)", &priority) - cmd.StringFlag("labels", "Filter by labels (comma-separated)", &labels) - cmd.IntFlag("limit", "Max number of tasks to return (default 20)", &limit) - cmd.StringFlag("project", "Filter by project", &project) +var tasksCmd = &cobra.Command{ + Use: "tasks", + Short: "List available tasks from core-agentic", + Long: `Lists tasks from the core-agentic service. - cmd.Action(func() error { +Configuration is loaded from: + 1. Environment variables (AGENTIC_TOKEN, AGENTIC_BASE_URL) + 2. .env file in current directory + 3. ~/.core/agentic.yaml + +Examples: + core ai tasks + core ai tasks --status pending --priority high + core ai tasks --labels bug,urgent`, + RunE: func(cmd *cobra.Command, args []string) error { + limit := tasksLimit if limit == 0 { limit = 20 } @@ -52,17 +59,17 @@ func addTasksCommand(parent *clir.Command) { opts := agentic.ListOptions{ Limit: limit, - Project: project, + Project: tasksProject, } - if status != "" { - opts.Status = agentic.TaskStatus(status) + if tasksStatus != "" { + opts.Status = agentic.TaskStatus(tasksStatus) } - if priority != "" { - opts.Priority = agentic.TaskPriority(priority) + if tasksPriority != "" { + opts.Priority = agentic.TaskPriority(tasksPriority) } - if labels != "" { - opts.Labels = strings.Split(labels, ",") + if tasksLabels != "" { + opts.Labels = strings.Split(tasksLabels, ",") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -80,27 +87,20 @@ func addTasksCommand(parent *clir.Command) { printTaskList(tasks) return nil - }) + }, } -func addTaskCommand(parent *clir.Command) { - var autoSelect bool - var claim bool - var showContext bool +var taskCmd = &cobra.Command{ + Use: "task [task-id]", + Short: "Show task details or auto-select a task", + Long: `Shows details of a specific task or auto-selects the highest priority task. - cmd := parent.NewSubCommand("task", "Show task details or auto-select a task") - cmd.LongDescription("Shows details of a specific task or auto-selects the highest priority task.\n\n" + - "Examples:\n" + - " core ai task abc123 # Show task details\n" + - " core ai task abc123 --claim # Show and claim the task\n" + - " core ai task abc123 --context # Show task with gathered context\n" + - " core ai task --auto # Auto-select highest priority pending task") - - cmd.BoolFlag("auto", "Auto-select highest priority pending task", &autoSelect) - cmd.BoolFlag("claim", "Claim the task after showing details", &claim) - cmd.BoolFlag("context", "Show gathered context for AI collaboration", &showContext) - - cmd.Action(func() error { +Examples: + core ai task abc123 # Show task details + core ai task abc123 --claim # Show and claim the task + core ai task abc123 --context # Show task with gathered context + core ai task --auto # Auto-select highest priority pending task`, + RunE: func(cmd *cobra.Command, args []string) error { cfg, err := agentic.LoadConfig("") if err != nil { return fmt.Errorf("failed to load config: %w", err) @@ -113,19 +113,13 @@ func addTaskCommand(parent *clir.Command) { var task *agentic.Task - // Get the task ID from remaining args - args := os.Args + // Get the task ID from args var taskID string - - // Find the task ID in args (after "task" subcommand) - for i, arg := range args { - if arg == "task" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - taskID = args[i+1] - break - } + if len(args) > 0 { + taskID = args[0] } - if autoSelect { + if taskAutoSelect { // Auto-select: find highest priority pending task tasks, err := client.ListTasks(ctx, agentic.ListOptions{ Status: agentic.StatusPending, @@ -153,7 +147,7 @@ func addTaskCommand(parent *clir.Command) { }) task = &tasks[0] - claim = true // Auto-select implies claiming + taskClaim = true // Auto-select implies claiming } else { if taskID == "" { return fmt.Errorf("task ID required (or use --auto)") @@ -166,7 +160,7 @@ func addTaskCommand(parent *clir.Command) { } // Show context if requested - if showContext { + if taskShowContext { cwd, _ := os.Getwd() taskCtx, err := agentic.BuildTaskContext(task, cwd) if err != nil { @@ -178,7 +172,7 @@ func addTaskCommand(parent *clir.Command) { printTaskDetails(task) } - if claim && task.Status == agentic.StatusPending { + if taskClaim && task.Status == agentic.StatusPending { fmt.Println() fmt.Printf("%s Claiming task...\n", dimStyle.Render(">>")) @@ -192,7 +186,29 @@ func addTaskCommand(parent *clir.Command) { } return nil - }) + }, +} + +func init() { + // tasks command flags + tasksCmd.Flags().StringVar(&tasksStatus, "status", "", "Filter by status (pending, in_progress, completed, blocked)") + tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", "Filter by priority (critical, high, medium, low)") + tasksCmd.Flags().StringVar(&tasksLabels, "labels", "", "Filter by labels (comma-separated)") + tasksCmd.Flags().IntVar(&tasksLimit, "limit", 20, "Max number of tasks to return") + tasksCmd.Flags().StringVar(&tasksProject, "project", "", "Filter by project") + + // task command flags + taskCmd.Flags().BoolVar(&taskAutoSelect, "auto", false, "Auto-select highest priority pending task") + taskCmd.Flags().BoolVar(&taskClaim, "claim", false, "Claim the task after showing details") + taskCmd.Flags().BoolVar(&taskShowContext, "context", false, "Show gathered context for AI collaboration") +} + +func addTasksCommand(parent *cobra.Command) { + parent.AddCommand(tasksCmd) +} + +func addTaskCommand(parent *cobra.Command) { + parent.AddCommand(taskCmd) } func printTaskList(tasks []agentic.Task) { diff --git a/cmd/ai/ai_updates.go b/cmd/ai/ai_updates.go index 4f1127b7..74fd1e2c 100644 --- a/cmd/ai/ai_updates.go +++ b/cmd/ai/ai_updates.go @@ -5,45 +5,39 @@ package ai import ( "context" "fmt" - "os" - "strings" "time" "github.com/host-uk/core/pkg/agentic" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addTaskUpdateCommand(parent *clir.Command) { - var status string - var progress int - var notes string +// task:update command flags +var ( + taskUpdateStatus string + taskUpdateProgress int + taskUpdateNotes string +) - cmd := parent.NewSubCommand("task:update", "Update task status or progress") - cmd.LongDescription("Updates a task's status, progress, or adds notes.\n\n" + - "Examples:\n" + - " core ai task:update abc123 --status in_progress\n" + - " core ai task:update abc123 --progress 50 --notes 'Halfway done'") +// task:complete command flags +var ( + taskCompleteOutput string + taskCompleteFailed bool + taskCompleteErrorMsg string +) - cmd.StringFlag("status", "New status (pending, in_progress, completed, blocked)", &status) - cmd.IntFlag("progress", "Progress percentage (0-100)", &progress) - cmd.StringFlag("notes", "Notes about the update", ¬es) +var taskUpdateCmd = &cobra.Command{ + Use: "task:update [task-id]", + Short: "Update task status or progress", + Long: `Updates a task's status, progress, or adds notes. - cmd.Action(func() error { - // Find task ID from args - args := os.Args - var taskID string - for i, arg := range args { - if arg == "task:update" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - taskID = args[i+1] - break - } - } +Examples: + core ai task:update abc123 --status in_progress + core ai task:update abc123 --progress 50 --notes 'Halfway done'`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + taskID := args[0] - if taskID == "" { - return fmt.Errorf("task ID required") - } - - if status == "" && progress == 0 && notes == "" { + if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { return fmt.Errorf("at least one of --status, --progress, or --notes required") } @@ -58,11 +52,11 @@ func addTaskUpdateCommand(parent *clir.Command) { defer cancel() update := agentic.TaskUpdate{ - Progress: progress, - Notes: notes, + Progress: taskUpdateProgress, + Notes: taskUpdateNotes, } - if status != "" { - update.Status = agentic.TaskStatus(status) + if taskUpdateStatus != "" { + update.Status = agentic.TaskStatus(taskUpdateStatus) } if err := client.UpdateTask(ctx, taskID, update); err != nil { @@ -71,38 +65,20 @@ func addTaskUpdateCommand(parent *clir.Command) { fmt.Printf("%s Task %s updated successfully\n", successStyle.Render(">>"), taskID) return nil - }) + }, } -func addTaskCompleteCommand(parent *clir.Command) { - var output string - var failed bool - var errorMsg string +var taskCompleteCmd = &cobra.Command{ + Use: "task:complete [task-id]", + Short: "Mark a task as completed", + Long: `Marks a task as completed with optional output and artifacts. - cmd := parent.NewSubCommand("task:complete", "Mark a task as completed") - cmd.LongDescription("Marks a task as completed with optional output and artifacts.\n\n" + - "Examples:\n" + - " core ai task:complete abc123 --output 'Feature implemented'\n" + - " core ai task:complete abc123 --failed --error 'Build failed'") - - cmd.StringFlag("output", "Summary of the completed work", &output) - cmd.BoolFlag("failed", "Mark the task as failed", &failed) - cmd.StringFlag("error", "Error message if failed", &errorMsg) - - cmd.Action(func() error { - // Find task ID from args - args := os.Args - var taskID string - for i, arg := range args { - if arg == "task:complete" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { - taskID = args[i+1] - break - } - } - - if taskID == "" { - return fmt.Errorf("task ID required") - } +Examples: + core ai task:complete abc123 --output 'Feature implemented' + core ai task:complete abc123 --failed --error 'Build failed'`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + taskID := args[0] cfg, err := agentic.LoadConfig("") if err != nil { @@ -115,20 +91,40 @@ func addTaskCompleteCommand(parent *clir.Command) { defer cancel() result := agentic.TaskResult{ - Success: !failed, - Output: output, - ErrorMessage: errorMsg, + Success: !taskCompleteFailed, + Output: taskCompleteOutput, + ErrorMessage: taskCompleteErrorMsg, } if err := client.CompleteTask(ctx, taskID, result); err != nil { return fmt.Errorf("failed to complete task: %w", err) } - if failed { + if taskCompleteFailed { fmt.Printf("%s Task %s marked as failed\n", errorStyle.Render(">>"), taskID) } else { fmt.Printf("%s Task %s completed successfully\n", successStyle.Render(">>"), taskID) } return nil - }) + }, +} + +func init() { + // task:update command flags + taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", "New status (pending, in_progress, completed, blocked)") + taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, "Progress percentage (0-100)") + taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", "Notes about the update") + + // task:complete command flags + taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", "Summary of the completed work") + taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, "Mark the task as failed") + taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", "Error message if failed") +} + +func addTaskUpdateCommand(parent *cobra.Command) { + parent.AddCommand(taskUpdateCmd) +} + +func addTaskCompleteCommand(parent *cobra.Command) { + parent.AddCommand(taskCompleteCmd) } diff --git a/cmd/ai/commands.go b/cmd/ai/commands.go index e06669c3..e9850d28 100644 --- a/cmd/ai/commands.go +++ b/cmd/ai/commands.go @@ -10,52 +10,70 @@ // - claude: Claude Code CLI integration (planned) package ai -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" -// AddCommands registers the 'ai' command and all subcommands. -func AddCommands(app *clir.Cli) { - aiCmd := app.NewSubCommand("ai", "AI agent task management") - aiCmd.LongDescription("Manage tasks from the core-agentic service for AI-assisted development.\n\n" + - "Commands:\n" + - " tasks List tasks (filterable by status, priority, labels)\n" + - " task View task details or auto-select highest priority\n" + - " task:update Update task status or progress\n" + - " task:complete Mark task as completed or failed\n" + - " task:commit Create git commit with task reference\n" + - " task:pr Create GitHub PR linked to task\n" + - " claude Claude Code integration\n\n" + - "Workflow:\n" + - " core ai tasks # List pending tasks\n" + - " core ai task --auto --claim # Auto-select and claim a task\n" + - " core ai task:commit -m 'msg' # Commit with task reference\n" + - " core ai task:complete # Mark task done") +var aiCmd = &cobra.Command{ + Use: "ai", + Short: "AI agent task management", + Long: `Manage tasks from the core-agentic service for AI-assisted development. - // Add Claude command - addClaudeCommand(aiCmd) +Commands: + tasks List tasks (filterable by status, priority, labels) + task View task details or auto-select highest priority + task:update Update task status or progress + task:complete Mark task as completed or failed + task:commit Create git commit with task reference + task:pr Create GitHub PR linked to task + claude Claude Code integration + +Workflow: + core ai tasks # List pending tasks + core ai task --auto --claim # Auto-select and claim a task + core ai task:commit -m 'msg' # Commit with task reference + core ai task:complete # Mark task done`, +} + +var claudeCmd = &cobra.Command{ + Use: "claude", + Short: "Claude Code integration", + Long: `Tools for working with Claude Code. + +Commands: + run Run Claude in the current directory + config Manage Claude configuration`, +} + +var claudeRunCmd = &cobra.Command{ + Use: "run", + Short: "Run Claude Code in the current directory", + RunE: func(cmd *cobra.Command, args []string) error { + return runClaudeCode() + }, +} + +var claudeConfigCmd = &cobra.Command{ + Use: "config", + Short: "Manage Claude configuration", + RunE: func(cmd *cobra.Command, args []string) error { + return showClaudeConfig() + }, +} + +func init() { + // Add Claude subcommands + claudeCmd.AddCommand(claudeRunCmd) + claudeCmd.AddCommand(claudeConfigCmd) + + // Add Claude command to ai + aiCmd.AddCommand(claudeCmd) // Add agentic task commands AddAgenticCommands(aiCmd) } -// addClaudeCommand adds the 'claude' subcommand for Claude Code integration. -func addClaudeCommand(parent *clir.Command) { - claudeCmd := parent.NewSubCommand("claude", "Claude Code integration") - claudeCmd.LongDescription("Tools for working with Claude Code.\n\n" + - "Commands:\n" + - " run Run Claude in the current directory\n" + - " config Manage Claude configuration") - - // core ai claude run - runCmd := claudeCmd.NewSubCommand("run", "Run Claude Code in the current directory") - runCmd.Action(func() error { - return runClaudeCode() - }) - - // core ai claude config - configCmd := claudeCmd.NewSubCommand("config", "Manage Claude configuration") - configCmd.Action(func() error { - return showClaudeConfig() - }) +// AddCommands registers the 'ai' command and all subcommands. +func AddCommands(root *cobra.Command) { + root.AddCommand(aiCmd) } func runClaudeCode() error { diff --git a/cmd/build/build.go b/cmd/build/build.go index dd03c39b..e74e1309 100644 --- a/cmd/build/build.go +++ b/cmd/build/build.go @@ -5,7 +5,7 @@ import ( "embed" "github.com/charmbracelet/lipgloss" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Build command styles @@ -32,100 +32,130 @@ var ( //go:embed all:tmpl/gui var guiTemplate embed.FS -// AddBuildCommand adds the new build command and its subcommands to the clir app. -func AddBuildCommand(app *clir.Cli) { - buildCmd := app.NewSubCommand("build", "Build projects with auto-detection and cross-compilation") - buildCmd.LongDescription("Builds the current project with automatic type detection.\n" + - "Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.\n" + - "Configuration can be provided via .core/build.yaml or command-line flags.\n\n" + - "Examples:\n" + - " core build # Auto-detect and build\n" + - " core build --type docker # Build Docker image\n" + - " core build --type linuxkit # Build LinuxKit image\n" + - " core build --type linuxkit --config linuxkit.yml --format qcow2-bios") - - // Flags for the main build command - var buildType string - var ciMode bool - var targets string - var outputDir string - var doArchive bool - var doChecksum bool +// Flags for the main build command +var ( + buildType string + ciMode bool + targets string + outputDir string + doArchive bool + doChecksum bool // Docker/LinuxKit specific flags - var configPath string - var format string - var push bool - var imageName string + configPath string + format string + push bool + imageName string // Signing flags - var noSign bool - var notarize bool + noSign bool + notarize bool - buildCmd.StringFlag("type", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified", &buildType) - buildCmd.BoolFlag("ci", "CI mode - minimal output with JSON artifact list at the end", &ciMode) - buildCmd.StringFlag("targets", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)", &targets) - buildCmd.StringFlag("output", "Output directory for artifacts (default: dist)", &outputDir) - buildCmd.BoolFlag("archive", "Create archives (tar.gz for linux/darwin, zip for windows) - default: true", &doArchive) - buildCmd.BoolFlag("checksum", "Generate SHA256 checksums and CHECKSUMS.txt - default: true", &doChecksum) + // from-path subcommand + fromPath string - // Docker/LinuxKit specific - buildCmd.StringFlag("config", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)", &configPath) - buildCmd.StringFlag("format", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)", &format) - buildCmd.BoolFlag("push", "Push Docker image after build (default: false)", &push) - buildCmd.StringFlag("image", "Docker image name (e.g., host-uk/core-devops)", &imageName) + // pwa subcommand + pwaURL string - // Signing flags - buildCmd.BoolFlag("no-sign", "Skip all code signing", &noSign) - buildCmd.BoolFlag("notarize", "Enable macOS notarization (requires Apple credentials)", ¬arize) + // sdk subcommand + sdkSpec string + sdkLang string + sdkVersion string + sdkDryRun bool +) - // Set defaults for archive and checksum (true by default) - doArchive = true - doChecksum = true +var buildCmd = &cobra.Command{ + Use: "build", + Short: "Build projects with auto-detection and cross-compilation", + Long: `Builds the current project with automatic type detection. +Supports Go, Wails, Docker, LinuxKit, and Taskfile projects. +Configuration can be provided via .core/build.yaml or command-line flags. - // Default action for `core build` (no subcommand) - buildCmd.Action(func() error { +Examples: + core build # Auto-detect and build + core build --type docker # Build Docker image + core build --type linuxkit # Build LinuxKit image + core build --type linuxkit --config linuxkit.yml --format qcow2-bios`, + RunE: func(cmd *cobra.Command, args []string) error { return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize) - }) + }, +} - // --- `build from-path` command (legacy PWA/GUI build) --- - fromPathCmd := buildCmd.NewSubCommand("from-path", "Build from a local directory.") - var fromPath string - fromPathCmd.StringFlag("path", "The path to the static web application files.", &fromPath) - fromPathCmd.Action(func() error { +var fromPathCmd = &cobra.Command{ + Use: "from-path", + Short: "Build from a local directory.", + RunE: func(cmd *cobra.Command, args []string) error { if fromPath == "" { return errPathRequired } return runBuild(fromPath) - }) + }, +} - // --- `build pwa` command (legacy PWA build) --- - pwaCmd := buildCmd.NewSubCommand("pwa", "Build from a live PWA URL.") - var pwaURL string - pwaCmd.StringFlag("url", "The URL of the PWA to build.", &pwaURL) - pwaCmd.Action(func() error { +var pwaCmd = &cobra.Command{ + Use: "pwa", + Short: "Build from a live PWA URL.", + RunE: func(cmd *cobra.Command, args []string) error { if pwaURL == "" { return errURLRequired } return runPwaBuild(pwaURL) - }) - - // --- `build sdk` command --- - sdkBuildCmd := buildCmd.NewSubCommand("sdk", "Generate API SDKs from OpenAPI spec") - sdkBuildCmd.LongDescription("Generates typed API clients from OpenAPI specifications.\n" + - "Supports TypeScript, Python, Go, and PHP.\n\n" + - "Examples:\n" + - " core build sdk # Generate all configured SDKs\n" + - " core build sdk --lang typescript # Generate only TypeScript SDK\n" + - " core build sdk --spec api.yaml # Use specific OpenAPI spec") - - var sdkSpec, sdkLang, sdkVersion string - var sdkDryRun bool - sdkBuildCmd.StringFlag("spec", "Path to OpenAPI spec file", &sdkSpec) - sdkBuildCmd.StringFlag("lang", "Generate only this language (typescript, python, go, php)", &sdkLang) - sdkBuildCmd.StringFlag("version", "Version to embed in generated SDKs", &sdkVersion) - sdkBuildCmd.BoolFlag("dry-run", "Show what would be generated without writing files", &sdkDryRun) - sdkBuildCmd.Action(func() error { - return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun) - }) + }, +} + +var sdkBuildCmd = &cobra.Command{ + Use: "sdk", + Short: "Generate API SDKs from OpenAPI spec", + Long: `Generates typed API clients from OpenAPI specifications. +Supports TypeScript, Python, Go, and PHP. + +Examples: + core build sdk # Generate all configured SDKs + core build sdk --lang typescript # Generate only TypeScript SDK + core build sdk --spec api.yaml # Use specific OpenAPI spec`, + RunE: func(cmd *cobra.Command, args []string) error { + return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun) + }, +} + +func init() { + // Main build command flags + buildCmd.Flags().StringVar(&buildType, "type", "", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified") + buildCmd.Flags().BoolVar(&ciMode, "ci", false, "CI mode - minimal output with JSON artifact list at the end") + buildCmd.Flags().StringVar(&targets, "targets", "", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)") + buildCmd.Flags().StringVar(&outputDir, "output", "", "Output directory for artifacts (default: dist)") + buildCmd.Flags().BoolVar(&doArchive, "archive", true, "Create archives (tar.gz for linux/darwin, zip for windows)") + buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, "Generate SHA256 checksums and CHECKSUMS.txt") + + // Docker/LinuxKit specific + buildCmd.Flags().StringVar(&configPath, "config", "", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)") + buildCmd.Flags().StringVar(&format, "format", "", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)") + buildCmd.Flags().BoolVar(&push, "push", false, "Push Docker image after build") + buildCmd.Flags().StringVar(&imageName, "image", "", "Docker image name (e.g., host-uk/core-devops)") + + // Signing flags + buildCmd.Flags().BoolVar(&noSign, "no-sign", false, "Skip all code signing") + buildCmd.Flags().BoolVar(¬arize, "notarize", false, "Enable macOS notarization (requires Apple credentials)") + + // from-path subcommand flags + fromPathCmd.Flags().StringVar(&fromPath, "path", "", "The path to the static web application files.") + + // pwa subcommand flags + pwaCmd.Flags().StringVar(&pwaURL, "url", "", "The URL of the PWA to build.") + + // sdk subcommand flags + sdkBuildCmd.Flags().StringVar(&sdkSpec, "spec", "", "Path to OpenAPI spec file") + sdkBuildCmd.Flags().StringVar(&sdkLang, "lang", "", "Generate only this language (typescript, python, go, php)") + sdkBuildCmd.Flags().StringVar(&sdkVersion, "version", "", "Version to embed in generated SDKs") + sdkBuildCmd.Flags().BoolVar(&sdkDryRun, "dry-run", false, "Show what would be generated without writing files") + + // Add subcommands + buildCmd.AddCommand(fromPathCmd) + buildCmd.AddCommand(pwaCmd) + buildCmd.AddCommand(sdkBuildCmd) +} + +// AddBuildCommand adds the new build command and its subcommands to the cobra app. +func AddBuildCommand(root *cobra.Command) { + root.AddCommand(buildCmd) } diff --git a/cmd/build/commands.go b/cmd/build/commands.go index 04f27a17..9415962c 100644 --- a/cmd/build/commands.go +++ b/cmd/build/commands.go @@ -16,9 +16,9 @@ // - build sdk: Generate API SDKs from OpenAPI spec package build -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'build' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddBuildCommand(app) +func AddCommands(root *cobra.Command) { + AddBuildCommand(root) } diff --git a/cmd/ci/ci_release.go b/cmd/ci/ci_release.go index 75ec8093..00f00758 100644 --- a/cmd/ci/ci_release.go +++ b/cmd/ci/ci_release.go @@ -3,7 +3,7 @@ package ci import ( "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared @@ -15,52 +15,75 @@ var ( releaseValueStyle = shared.ValueStyle ) -// AddCIReleaseCommand adds the release command and its subcommands. -func AddCIReleaseCommand(app *clir.Cli) { - releaseCmd := app.NewSubCommand("ci", "Publish releases (dry-run by default)") - releaseCmd.LongDescription("Publishes pre-built artifacts from dist/ to configured targets.\n" + - "Run 'core build' first to create artifacts.\n\n" + - "SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.\n\n" + - "Configuration: .core/release.yaml") +// Flag variables for ci command +var ( + ciGoForLaunch bool + ciVersion string + ciDraft bool + ciPrerelease bool +) - // Flags for the main release command - var goForLaunch bool - var version string - var draft bool - var prerelease bool +// Flag variables for changelog subcommand +var ( + changelogFromRef string + changelogToRef string +) - releaseCmd.BoolFlag("we-are-go-for-launch", "Actually publish (default is dry-run for safety)", &goForLaunch) - releaseCmd.StringFlag("version", "Version to release (e.g., v1.2.3)", &version) - releaseCmd.BoolFlag("draft", "Create release as a draft", &draft) - releaseCmd.BoolFlag("prerelease", "Mark release as a prerelease", &prerelease) +var ciCmd = &cobra.Command{ + Use: "ci", + Short: "Publish releases (dry-run by default)", + Long: `Publishes pre-built artifacts from dist/ to configured targets. +Run 'core build' first to create artifacts. - // Default action for `core ci` - dry-run by default for safety - releaseCmd.Action(func() error { - dryRun := !goForLaunch - return runCIPublish(dryRun, version, draft, prerelease) - }) +SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified. - // `release init` subcommand - initCmd := releaseCmd.NewSubCommand("init", "Initialize release configuration") - initCmd.LongDescription("Creates a .core/release.yaml configuration file interactively.") - initCmd.Action(func() error { - return runCIReleaseInit() - }) - - // `release changelog` subcommand - changelogCmd := releaseCmd.NewSubCommand("changelog", "Generate changelog") - changelogCmd.LongDescription("Generates a changelog from conventional commits.") - var fromRef, toRef string - changelogCmd.StringFlag("from", "Starting ref (default: previous tag)", &fromRef) - changelogCmd.StringFlag("to", "Ending ref (default: HEAD)", &toRef) - changelogCmd.Action(func() error { - return runChangelog(fromRef, toRef) - }) - - // `release version` subcommand - versionCmd := releaseCmd.NewSubCommand("version", "Show or set version") - versionCmd.LongDescription("Shows the determined version or validates a version string.") - versionCmd.Action(func() error { - return runCIReleaseVersion() - }) +Configuration: .core/release.yaml`, + RunE: func(cmd *cobra.Command, args []string) error { + dryRun := !ciGoForLaunch + return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease) + }, +} + +var ciInitCmd = &cobra.Command{ + Use: "init", + Short: "Initialize release configuration", + Long: "Creates a .core/release.yaml configuration file interactively.", + RunE: func(cmd *cobra.Command, args []string) error { + return runCIReleaseInit() + }, +} + +var ciChangelogCmd = &cobra.Command{ + Use: "changelog", + Short: "Generate changelog", + Long: "Generates a changelog from conventional commits.", + RunE: func(cmd *cobra.Command, args []string) error { + return runChangelog(changelogFromRef, changelogToRef) + }, +} + +var ciVersionCmd = &cobra.Command{ + Use: "version", + Short: "Show or set version", + Long: "Shows the determined version or validates a version string.", + RunE: func(cmd *cobra.Command, args []string) error { + return runCIReleaseVersion() + }, +} + +func init() { + // Main ci command flags + ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, "Actually publish (default is dry-run for safety)") + ciCmd.Flags().StringVar(&ciVersion, "version", "", "Version to release (e.g., v1.2.3)") + ciCmd.Flags().BoolVar(&ciDraft, "draft", false, "Create release as a draft") + ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, "Mark release as a prerelease") + + // Changelog subcommand flags + ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", "Starting ref (default: previous tag)") + ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", "Ending ref (default: HEAD)") + + // Add subcommands + ciCmd.AddCommand(ciInitCmd) + ciCmd.AddCommand(ciChangelogCmd) + ciCmd.AddCommand(ciVersionCmd) } diff --git a/cmd/ci/commands.go b/cmd/ci/commands.go index 819df247..253e7b1c 100644 --- a/cmd/ci/commands.go +++ b/cmd/ci/commands.go @@ -9,9 +9,9 @@ // Configuration via .core/release.yaml. package ci -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'ci' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddCIReleaseCommand(app) +func AddCommands(root *cobra.Command) { + root.AddCommand(ciCmd) } diff --git a/cmd/core.go b/cmd/core.go index c0c46c6f..8173773d 100644 --- a/cmd/core.go +++ b/cmd/core.go @@ -17,32 +17,92 @@ package cmd import ( - "github.com/charmbracelet/lipgloss" - "github.com/leaanthony/clir" + "os" + + "github.com/host-uk/core/cmd/shared" + "github.com/spf13/cobra" ) -// Terminal styles using Tailwind color palette. +// Terminal styles using Tailwind colour palette (from shared package). var ( // coreStyle is used for primary headings and the CLI name. - coreStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#3b82f6")). // blue-500 - Bold(true) - - // subPkgStyle is used for subcommand names and secondary headings. - subPkgStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#e2e8f0")). // gray-200 - Bold(true) + coreStyle = shared.RepoNameStyle // linkStyle is used for URLs and clickable references. - linkStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#3b82f6")). // blue-500 - Underline(true) + linkStyle = shared.LinkStyle ) +// rootCmd is the base command for the CLI. +var rootCmd = &cobra.Command{ + Use: "core", + Short: "CLI tool for development and production", + Version: "0.1.0", +} + // Execute initialises and runs the CLI application. // Commands are registered based on build tags (see core_ci.go and core_dev.go). func Execute() error { - app := clir.NewCli("core", "CLI tool for development and production", "0.1.0") - registerCommands(app) - return app.Run() + return rootCmd.Execute() +} + +func init() { + // Add shell completion command + rootCmd.AddCommand(completionCmd) +} + +// completionCmd generates shell completion scripts. +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion script", + Long: `Generate shell completion script for the specified shell. + +To load completions: + +Bash: + $ source <(core completion bash) + + # To load completions for each session, execute once: + # Linux: + $ core completion bash > /etc/bash_completion.d/core + # macOS: + $ core completion bash > $(brew --prefix)/etc/bash_completion.d/core + +Zsh: + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ core completion zsh > "${fpath[1]}/_core" + + # You will need to start a new shell for this setup to take effect. + +Fish: + $ core completion fish | source + + # To load completions for each session, execute once: + $ core completion fish > ~/.config/fish/completions/core.fish + +PowerShell: + PS> core completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> core completion powershell > core.ps1 + # and source this file from your PowerShell profile. +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + _ = cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + _ = cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + _ = cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + }, } diff --git a/cmd/core_ci.go b/cmd/core_ci.go index 3260c1d3..5bae7037 100644 --- a/cmd/core_ci.go +++ b/cmd/core_ci.go @@ -19,13 +19,11 @@ import ( "github.com/host-uk/core/cmd/ci" "github.com/host-uk/core/cmd/doctor" "github.com/host-uk/core/cmd/sdk" - "github.com/leaanthony/clir" ) -// registerCommands adds CI/release commands only. -func registerCommands(app *clir.Cli) { - build.AddCommands(app) - ci.AddCommands(app) - sdk.AddCommands(app) - doctor.AddCommands(app) +func init() { + build.AddCommands(rootCmd) + ci.AddCommands(rootCmd) + sdk.AddCommands(rootCmd) + doctor.AddCommands(rootCmd) } diff --git a/cmd/core_dev.go b/cmd/core_dev.go index d5a13252..bde76bb9 100644 --- a/cmd/core_dev.go +++ b/cmd/core_dev.go @@ -35,31 +35,29 @@ import ( "github.com/host-uk/core/cmd/setup" testcmd "github.com/host-uk/core/cmd/test" "github.com/host-uk/core/cmd/vm" - "github.com/leaanthony/clir" ) -// registerCommands adds all development commands. -func registerCommands(app *clir.Cli) { +func init() { // Multi-repo workflow - dev.AddCommands(app) + dev.AddCommands(rootCmd) // AI agent tools - ai.AddCommands(app) + ai.AddCommands(rootCmd) // Language tooling - gocmd.AddCommands(app) - php.AddCommands(app) + gocmd.AddCommands(rootCmd) + php.AddCommands(rootCmd) // Build and release - build.AddCommands(app) - ci.AddCommands(app) - sdk.AddCommands(app) + build.AddCommands(rootCmd) + ci.AddCommands(rootCmd) + sdk.AddCommands(rootCmd) // Environment management - pkg.AddCommands(app) - vm.AddCommands(app) - docs.AddCommands(app) - setup.AddCommands(app) - doctor.AddCommands(app) - testcmd.AddCommands(app) + pkg.AddCommands(rootCmd) + vm.AddCommands(rootCmd) + docs.AddCommands(rootCmd) + setup.AddCommands(rootCmd) + doctor.AddCommands(rootCmd) + testcmd.AddCommands(rootCmd) } diff --git a/cmd/dev/dev.go b/cmd/dev/dev.go index 278d9a43..86a6b02f 100644 --- a/cmd/dev/dev.go +++ b/cmd/dev/dev.go @@ -31,7 +31,7 @@ package dev import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared package @@ -64,28 +64,36 @@ var ( ) // AddCommands registers the 'dev' command and all subcommands. -func AddCommands(app *clir.Cli) { - devCmd := app.NewSubCommand("dev", "Multi-repo development workflow") - devCmd.LongDescription("Manage multiple git repositories and GitHub integration.\n\n" + - "Uses repos.yaml to discover repositories. Falls back to scanning\n" + - "the current directory if no registry is found.\n\n" + - "Git Operations:\n" + - " work Combined status -> commit -> push workflow\n" + - " health Quick repo health summary\n" + - " commit Claude-assisted commit messages\n" + - " push Push repos with unpushed commits\n" + - " pull Pull repos behind remote\n\n" + - "GitHub Integration (requires gh CLI):\n" + - " issues List open issues across repos\n" + - " reviews List PRs awaiting review\n" + - " ci Check GitHub Actions status\n" + - " impact Analyse dependency impact\n\n" + - "Dev Environment:\n" + - " install Download dev environment image\n" + - " boot Start dev environment VM\n" + - " stop Stop dev environment VM\n" + - " shell Open shell in dev VM\n" + - " status Check dev VM status") +func AddCommands(root *cobra.Command) { + devCmd := &cobra.Command{ + Use: "dev", + Short: "Multi-repo development workflow", + Long: `Manage multiple git repositories and GitHub integration. + +Uses repos.yaml to discover repositories. Falls back to scanning +the current directory if no registry is found. + +Git Operations: + work Combined status -> commit -> push workflow + health Quick repo health summary + commit Claude-assisted commit messages + push Push repos with unpushed commits + pull Pull repos behind remote + +GitHub Integration (requires gh CLI): + issues List open issues across repos + reviews List PRs awaiting review + ci Check GitHub Actions status + impact Analyse dependency impact + +Dev Environment: + install Download dev environment image + boot Start dev environment VM + stop Stop dev environment VM + shell Open shell in dev VM + status Check dev VM status`, + } + root.AddCommand(devCmd) // Git operations addWorkCommand(devCmd) diff --git a/cmd/dev/dev_api.go b/cmd/dev/dev_api.go index 1060fb1c..7f0fbdeb 100644 --- a/cmd/dev/dev_api.go +++ b/cmd/dev/dev_api.go @@ -1,13 +1,17 @@ package dev import ( - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // addAPICommands adds the 'api' command and its subcommands to the given parent command. -func addAPICommands(parent *clir.Command) { +func addAPICommands(parent *cobra.Command) { // Create the 'api' command - apiCmd := parent.NewSubCommand("api", "Tools for managing service APIs") + apiCmd := &cobra.Command{ + Use: "api", + Short: "Tools for managing service APIs", + } + parent.AddCommand(apiCmd) // Add the 'sync' command to 'api' addSyncCommand(apiCmd) diff --git a/cmd/dev/dev_ci.go b/cmd/dev/dev_ci.go index de8a3171..d2c3cd14 100644 --- a/cmd/dev/dev_ci.go +++ b/cmd/dev/dev_ci.go @@ -11,7 +11,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // CI-specific styles @@ -45,27 +45,35 @@ type WorkflowRun struct { RepoName string `json:"-"` } +// CI command flags +var ( + ciRegistryPath string + ciBranch string + ciFailedOnly bool +) + // addCICommand adds the 'ci' command to the given parent command. -func addCICommand(parent *clir.Command) { - var registryPath string - var branch string - var failedOnly bool +func addCICommand(parent *cobra.Command) { + ciCmd := &cobra.Command{ + Use: "ci", + Short: "Check CI status across all repos", + Long: `Fetches GitHub Actions workflow status for all repos. +Shows latest run status for each repo. +Requires the 'gh' CLI to be installed and authenticated.`, + RunE: func(cmd *cobra.Command, args []string) error { + branch := ciBranch + if branch == "" { + branch = "main" + } + return runCI(ciRegistryPath, branch, ciFailedOnly) + }, + } - ciCmd := parent.NewSubCommand("ci", "Check CI status across all repos") - ciCmd.LongDescription("Fetches GitHub Actions workflow status for all repos.\n" + - "Shows latest run status for each repo.\n" + - "Requires the 'gh' CLI to be installed and authenticated.") + ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", "Filter by branch") + ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, "Show only failed runs") - ciCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - ciCmd.StringFlag("branch", "Filter by branch (default: main)", &branch) - ciCmd.BoolFlag("failed", "Show only failed runs", &failedOnly) - - ciCmd.Action(func() error { - if branch == "" { - branch = "main" - } - return runCI(registryPath, branch, failedOnly) - }) + parent.AddCommand(ciCmd) } func runCI(registryPath string, branch string, failedOnly bool) error { diff --git a/cmd/dev/dev_commit.go b/cmd/dev/dev_commit.go index 383836f7..05a3c5c6 100644 --- a/cmd/dev/dev_commit.go +++ b/cmd/dev/dev_commit.go @@ -8,24 +8,31 @@ import ( "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +// Commit command flags +var ( + commitRegistryPath string + commitAll bool ) // addCommitCommand adds the 'commit' command to the given parent command. -func addCommitCommand(parent *clir.Command) { - var registryPath string - var all bool +func addCommitCommand(parent *cobra.Command) { + commitCmd := &cobra.Command{ + Use: "commit", + Short: "Claude-assisted commits across repos", + Long: `Uses Claude to create commits for dirty repos. +Shows uncommitted changes and invokes Claude to generate commit messages.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runCommit(commitRegistryPath, commitAll) + }, + } - commitCmd := parent.NewSubCommand("commit", "Claude-assisted commits across repos") - commitCmd.LongDescription("Uses Claude to create commits for dirty repos.\n" + - "Shows uncommitted changes and invokes Claude to generate commit messages.") + commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + commitCmd.Flags().BoolVar(&commitAll, "all", false, "Commit all dirty repos without prompting") - commitCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - commitCmd.BoolFlag("all", "Commit all dirty repos without prompting", &all) - - commitCmd.Action(func() error { - return runCommit(registryPath, all) - }) + parent.AddCommand(commitCmd) } func runCommit(registryPath string, all bool) error { diff --git a/cmd/dev/dev_health.go b/cmd/dev/dev_health.go index 420da226..b35b6a40 100644 --- a/cmd/dev/dev_health.go +++ b/cmd/dev/dev_health.go @@ -8,24 +8,31 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +// Health command flags +var ( + healthRegistryPath string + healthVerbose bool ) // addHealthCommand adds the 'health' command to the given parent command. -func addHealthCommand(parent *clir.Command) { - var registryPath string - var verbose bool +func addHealthCommand(parent *cobra.Command) { + healthCmd := &cobra.Command{ + Use: "health", + Short: "Quick health check across all repos", + Long: `Shows a summary of repository health: +total repos, dirty repos, unpushed commits, etc.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runHealth(healthRegistryPath, healthVerbose) + }, + } - healthCmd := parent.NewSubCommand("health", "Quick health check across all repos") - healthCmd.LongDescription("Shows a summary of repository health:\n" + - "total repos, dirty repos, unpushed commits, etc.") + healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown") - healthCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - healthCmd.BoolFlag("verbose", "Show detailed breakdown", &verbose) - - healthCmd.Action(func() error { - return runHealth(registryPath, verbose) - }) + parent.AddCommand(healthCmd) } func runHealth(registryPath string, verbose bool) error { diff --git a/cmd/dev/dev_impact.go b/cmd/dev/dev_impact.go index d0187b7f..6e52ca6b 100644 --- a/cmd/dev/dev_impact.go +++ b/cmd/dev/dev_impact.go @@ -2,13 +2,12 @@ package dev import ( "fmt" - "os" "sort" "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Impact-specific styles @@ -24,31 +23,25 @@ var ( Foreground(lipgloss.Color("#22c55e")) // green-500 ) +// Impact command flags +var impactRegistryPath string + // addImpactCommand adds the 'impact' command to the given parent command. -func addImpactCommand(parent *clir.Command) { - var registryPath string +func addImpactCommand(parent *cobra.Command) { + impactCmd := &cobra.Command{ + Use: "impact ", + Short: "Show impact of changing a repo", + Long: `Analyzes the dependency graph to show which repos +would be affected by changes to the specified repo.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runImpact(impactRegistryPath, args[0]) + }, + } - impactCmd := parent.NewSubCommand("impact", "Show impact of changing a repo") - impactCmd.LongDescription("Analyzes the dependency graph to show which repos\n" + - "would be affected by changes to the specified repo.") + impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") - impactCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - - impactCmd.Action(func() error { - args := os.Args[2:] // Skip "core" and "impact" - // Filter out flags - var repoName string - for _, arg := range args { - if arg[0] != '-' { - repoName = arg - break - } - } - if repoName == "" { - return fmt.Errorf("usage: core impact ") - } - return runImpact(registryPath, repoName) - }) + parent.AddCommand(impactCmd) } func runImpact(registryPath string, repoName string) error { diff --git a/cmd/dev/dev_issues.go b/cmd/dev/dev_issues.go index 67db407d..b9c5a412 100644 --- a/cmd/dev/dev_issues.go +++ b/cmd/dev/dev_issues.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Issue-specific styles @@ -62,26 +62,34 @@ type GitHubIssue struct { RepoName string `json:"-"` } +// Issues command flags +var ( + issuesRegistryPath string + issuesLimit int + issuesAssignee string +) + // addIssuesCommand adds the 'issues' command to the given parent command. -func addIssuesCommand(parent *clir.Command) { - var registryPath string - var limit int - var assignee string +func addIssuesCommand(parent *cobra.Command) { + issuesCmd := &cobra.Command{ + Use: "issues", + Short: "List open issues across all repos", + Long: `Fetches open issues from GitHub for all repos in the registry. +Requires the 'gh' CLI to be installed and authenticated.`, + RunE: func(cmd *cobra.Command, args []string) error { + limit := issuesLimit + if limit == 0 { + limit = 10 + } + return runIssues(issuesRegistryPath, limit, issuesAssignee) + }, + } - issuesCmd := parent.NewSubCommand("issues", "List open issues across all repos") - issuesCmd.LongDescription("Fetches open issues from GitHub for all repos in the registry.\n" + - "Requires the 'gh' CLI to be installed and authenticated.") + issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, "Max issues per repo") + issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", "Filter by assignee (use @me for yourself)") - issuesCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - issuesCmd.IntFlag("limit", "Max issues per repo (default 10)", &limit) - issuesCmd.StringFlag("assignee", "Filter by assignee (use @me for yourself)", &assignee) - - issuesCmd.Action(func() error { - if limit == 0 { - limit = 10 - } - return runIssues(registryPath, limit, assignee) - }) + parent.AddCommand(issuesCmd) } func runIssues(registryPath string, limit int, assignee string) error { diff --git a/cmd/dev/dev_pull.go b/cmd/dev/dev_pull.go index fd010b8b..03b09603 100644 --- a/cmd/dev/dev_pull.go +++ b/cmd/dev/dev_pull.go @@ -8,24 +8,31 @@ import ( "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +// Pull command flags +var ( + pullRegistryPath string + pullAll bool ) // addPullCommand adds the 'pull' command to the given parent command. -func addPullCommand(parent *clir.Command) { - var registryPath string - var all bool +func addPullCommand(parent *cobra.Command) { + pullCmd := &cobra.Command{ + Use: "pull", + Short: "Pull updates across all repos", + Long: `Pulls updates for all repos. +By default only pulls repos that are behind. Use --all to pull all repos.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runPull(pullRegistryPath, pullAll) + }, + } - pullCmd := parent.NewSubCommand("pull", "Pull updates across all repos") - pullCmd.LongDescription("Pulls updates for all repos.\n" + - "By default only pulls repos that are behind. Use --all to pull all repos.") + pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + pullCmd.Flags().BoolVar(&pullAll, "all", false, "Pull all repos, not just those behind") - pullCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - pullCmd.BoolFlag("all", "Pull all repos, not just those behind", &all) - - pullCmd.Action(func() error { - return runPull(registryPath, all) - }) + parent.AddCommand(pullCmd) } func runPull(registryPath string, all bool) error { diff --git a/cmd/dev/dev_push.go b/cmd/dev/dev_push.go index e3757385..81e9a5c4 100644 --- a/cmd/dev/dev_push.go +++ b/cmd/dev/dev_push.go @@ -8,24 +8,31 @@ import ( "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +// Push command flags +var ( + pushRegistryPath string + pushForce bool ) // addPushCommand adds the 'push' command to the given parent command. -func addPushCommand(parent *clir.Command) { - var registryPath string - var force bool +func addPushCommand(parent *cobra.Command) { + pushCmd := &cobra.Command{ + Use: "push", + Short: "Push commits across all repos", + Long: `Pushes unpushed commits for all repos. +Shows repos with commits to push and confirms before pushing.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runPush(pushRegistryPath, pushForce) + }, + } - pushCmd := parent.NewSubCommand("push", "Push commits across all repos") - pushCmd.LongDescription("Pushes unpushed commits for all repos.\n" + - "Shows repos with commits to push and confirms before pushing.") + pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, "Skip confirmation prompt") - pushCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - pushCmd.BoolFlag("force", "Skip confirmation prompt", &force) - - pushCmd.Action(func() error { - return runPush(registryPath, force) - }) + parent.AddCommand(pushCmd) } func runPush(registryPath string, force bool) error { diff --git a/cmd/dev/dev_reviews.go b/cmd/dev/dev_reviews.go index ce63ed37..9bebd98b 100644 --- a/cmd/dev/dev_reviews.go +++ b/cmd/dev/dev_reviews.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // PR-specific styles @@ -67,24 +67,31 @@ type GitHubPR struct { RepoName string `json:"-"` } +// Reviews command flags +var ( + reviewsRegistryPath string + reviewsAuthor string + reviewsShowAll bool +) + // addReviewsCommand adds the 'reviews' command to the given parent command. -func addReviewsCommand(parent *clir.Command) { - var registryPath string - var author string - var showAll bool +func addReviewsCommand(parent *cobra.Command) { + reviewsCmd := &cobra.Command{ + Use: "reviews", + Short: "List PRs needing review across all repos", + Long: `Fetches open PRs from GitHub for all repos in the registry. +Shows review status (approved, changes requested, pending). +Requires the 'gh' CLI to be installed and authenticated.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll) + }, + } - reviewsCmd := parent.NewSubCommand("reviews", "List PRs needing review across all repos") - reviewsCmd.LongDescription("Fetches open PRs from GitHub for all repos in the registry.\n" + - "Shows review status (approved, changes requested, pending).\n" + - "Requires the 'gh' CLI to be installed and authenticated.") + reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", "Filter by PR author") + reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, "Show all PRs including drafts") - reviewsCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - reviewsCmd.StringFlag("author", "Filter by PR author", &author) - reviewsCmd.BoolFlag("all", "Show all PRs including drafts", &showAll) - - reviewsCmd.Action(func() error { - return runReviews(registryPath, author, showAll) - }) + parent.AddCommand(reviewsCmd) } func runReviews(registryPath string, author string, showAll bool) error { diff --git a/cmd/dev/dev_sync.go b/cmd/dev/dev_sync.go index 580b12fc..ec37a683 100644 --- a/cmd/dev/dev_sync.go +++ b/cmd/dev/dev_sync.go @@ -10,22 +10,29 @@ import ( "path/filepath" "text/template" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" ) // addSyncCommand adds the 'sync' command to the given parent command. -func addSyncCommand(parent *clir.Command) { - syncCmd := parent.NewSubCommand("sync", "Synchronizes the public service APIs with their internal implementations.") - syncCmd.LongDescription("This command scans the 'pkg' directory for services and ensures that the\ntop-level public API for each service is in sync with its internal implementation.\nIt automatically generates the necessary Go files with type aliases.") - syncCmd.Action(func() error { - if err := runSync(); err != nil { - return fmt.Errorf("Error: %w", err) - } - fmt.Println("Public APIs synchronized successfully.") - return nil - }) +func addSyncCommand(parent *cobra.Command) { + syncCmd := &cobra.Command{ + Use: "sync", + Short: "Synchronizes the public service APIs with their internal implementations.", + Long: `This command scans the 'pkg' directory for services and ensures that the +top-level public API for each service is in sync with its internal implementation. +It automatically generates the necessary Go files with type aliases.`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := runSync(); err != nil { + return fmt.Errorf("Error: %w", err) + } + fmt.Println("Public APIs synchronized successfully.") + return nil + }, + } + + parent.AddCommand(syncCmd) } type symbolInfo struct { diff --git a/cmd/dev/dev_vm.go b/cmd/dev/dev_vm.go index 66a14444..1f195606 100644 --- a/cmd/dev/dev_vm.go +++ b/cmd/dev/dev_vm.go @@ -7,12 +7,12 @@ import ( "time" "github.com/host-uk/core/pkg/devops" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // addVMCommands adds the dev environment VM commands to the dev parent command. // These are added as direct subcommands: core dev install, core dev boot, etc. -func addVMCommands(parent *clir.Command) { +func addVMCommands(parent *cobra.Command) { addVMInstallCommand(parent) addVMBootCommand(parent) addVMStopCommand(parent) @@ -25,17 +25,23 @@ func addVMCommands(parent *clir.Command) { } // addVMInstallCommand adds the 'dev install' command. -func addVMInstallCommand(parent *clir.Command) { - installCmd := parent.NewSubCommand("install", "Download and install the dev environment image") - installCmd.LongDescription("Downloads the platform-specific dev environment image.\n\n" + - "The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI.\n" + - "Downloads are cached at ~/.core/images/\n\n" + - "Examples:\n" + - " core dev install") +func addVMInstallCommand(parent *cobra.Command) { + installCmd := &cobra.Command{ + Use: "install", + Short: "Download and install the dev environment image", + Long: `Downloads the platform-specific dev environment image. - installCmd.Action(func() error { - return runVMInstall() - }) +The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI. +Downloads are cached at ~/.core/images/ + +Examples: + core dev install`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMInstall() + }, + } + + parent.AddCommand(installCmd) } func runVMInstall() error { @@ -85,26 +91,34 @@ func runVMInstall() error { return nil } +// VM boot command flags +var ( + vmBootMemory int + vmBootCPUs int + vmBootFresh bool +) + // addVMBootCommand adds the 'devops boot' command. -func addVMBootCommand(parent *clir.Command) { - var memory int - var cpus int - var fresh bool +func addVMBootCommand(parent *cobra.Command) { + bootCmd := &cobra.Command{ + Use: "boot", + Short: "Start the dev environment", + Long: `Boots the dev environment VM. - bootCmd := parent.NewSubCommand("boot", "Start the dev environment") - bootCmd.LongDescription("Boots the dev environment VM.\n\n" + - "Examples:\n" + - " core dev boot\n" + - " core dev boot --memory 8192 --cpus 4\n" + - " core dev boot --fresh") +Examples: + core dev boot + core dev boot --memory 8192 --cpus 4 + core dev boot --fresh`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh) + }, + } - bootCmd.IntFlag("memory", "Memory in MB (default: 4096)", &memory) - bootCmd.IntFlag("cpus", "Number of CPUs (default: 2)", &cpus) - bootCmd.BoolFlag("fresh", "Stop existing and start fresh", &fresh) + bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, "Memory in MB (default: 4096)") + bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, "Number of CPUs (default: 2)") + bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, "Stop existing and start fresh") - bootCmd.Action(func() error { - return runVMBoot(memory, cpus, fresh) - }) + parent.AddCommand(bootCmd) } func runVMBoot(memory, cpus int, fresh bool) error { @@ -145,15 +159,20 @@ func runVMBoot(memory, cpus int, fresh bool) error { } // addVMStopCommand adds the 'devops stop' command. -func addVMStopCommand(parent *clir.Command) { - stopCmd := parent.NewSubCommand("stop", "Stop the dev environment") - stopCmd.LongDescription("Stops the running dev environment VM.\n\n" + - "Examples:\n" + - " core dev stop") +func addVMStopCommand(parent *cobra.Command) { + stopCmd := &cobra.Command{ + Use: "stop", + Short: "Stop the dev environment", + Long: `Stops the running dev environment VM. - stopCmd.Action(func() error { - return runVMStop() - }) +Examples: + core dev stop`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMStop() + }, + } + + parent.AddCommand(stopCmd) } func runVMStop() error { @@ -184,15 +203,20 @@ func runVMStop() error { } // addVMStatusCommand adds the 'devops status' command. -func addVMStatusCommand(parent *clir.Command) { - statusCmd := parent.NewSubCommand("vm-status", "Show dev environment status") - statusCmd.LongDescription("Shows the current status of the dev environment.\n\n" + - "Examples:\n" + - " core dev vm-status") +func addVMStatusCommand(parent *cobra.Command) { + statusCmd := &cobra.Command{ + Use: "vm-status", + Short: "Show dev environment status", + Long: `Shows the current status of the dev environment. - statusCmd.Action(func() error { - return runVMStatus() - }) +Examples: + core dev vm-status`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMStatus() + }, + } + + parent.AddCommand(statusCmd) } func runVMStatus() error { @@ -255,24 +279,30 @@ func formatVMUptime(d time.Duration) string { return fmt.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24) } +// VM shell command flags +var vmShellConsole bool + // addVMShellCommand adds the 'devops shell' command. -func addVMShellCommand(parent *clir.Command) { - var console bool +func addVMShellCommand(parent *cobra.Command) { + shellCmd := &cobra.Command{ + Use: "shell [-- command...]", + Short: "Connect to the dev environment", + Long: `Opens an interactive shell in the dev environment. - shellCmd := parent.NewSubCommand("shell", "Connect to the dev environment") - shellCmd.LongDescription("Opens an interactive shell in the dev environment.\n\n" + - "Uses SSH by default, or serial console with --console.\n\n" + - "Examples:\n" + - " core dev shell\n" + - " core dev shell --console\n" + - " core dev shell -- ls -la") +Uses SSH by default, or serial console with --console. - shellCmd.BoolFlag("console", "Use serial console instead of SSH", &console) +Examples: + core dev shell + core dev shell --console + core dev shell -- ls -la`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMShell(vmShellConsole, args) + }, + } - shellCmd.Action(func() error { - args := shellCmd.OtherArgs() - return runVMShell(console, args) - }) + shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, "Use serial console instead of SSH") + + parent.AddCommand(shellCmd) } func runVMShell(console bool, command []string) error { @@ -290,25 +320,34 @@ func runVMShell(console bool, command []string) error { return d.Shell(ctx, opts) } +// VM serve command flags +var ( + vmServePort int + vmServePath string +) + // addVMServeCommand adds the 'devops serve' command. -func addVMServeCommand(parent *clir.Command) { - var port int - var path string +func addVMServeCommand(parent *cobra.Command) { + serveCmd := &cobra.Command{ + Use: "serve", + Short: "Mount project and start dev server", + Long: `Mounts the current project into the dev environment and starts a dev server. - serveCmd := parent.NewSubCommand("serve", "Mount project and start dev server") - serveCmd.LongDescription("Mounts the current project into the dev environment and starts a dev server.\n\n" + - "Auto-detects the appropriate serve command based on project files.\n\n" + - "Examples:\n" + - " core dev serve\n" + - " core dev serve --port 3000\n" + - " core dev serve --path public") +Auto-detects the appropriate serve command based on project files. - serveCmd.IntFlag("port", "Port to serve on (default: 8000)", &port) - serveCmd.StringFlag("path", "Subdirectory to serve", &path) +Examples: + core dev serve + core dev serve --port 3000 + core dev serve --path public`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMServe(vmServePort, vmServePath) + }, + } - serveCmd.Action(func() error { - return runVMServe(port, path) - }) + serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, "Port to serve on (default: 8000)") + serveCmd.Flags().StringVar(&vmServePath, "path", "", "Subdirectory to serve") + + parent.AddCommand(serveCmd) } func runVMServe(port int, path string) error { @@ -331,24 +370,30 @@ func runVMServe(port int, path string) error { return d.Serve(ctx, projectDir, opts) } +// VM test command flags +var vmTestName string + // addVMTestCommand adds the 'devops test' command. -func addVMTestCommand(parent *clir.Command) { - var name string +func addVMTestCommand(parent *cobra.Command) { + testCmd := &cobra.Command{ + Use: "test [-- command...]", + Short: "Run tests in the dev environment", + Long: `Runs tests in the dev environment. - testCmd := parent.NewSubCommand("test", "Run tests in the dev environment") - testCmd.LongDescription("Runs tests in the dev environment.\n\n" + - "Auto-detects the test command based on project files, or uses .core/test.yaml.\n\n" + - "Examples:\n" + - " core dev test\n" + - " core dev test --name integration\n" + - " core dev test -- go test -v ./...") +Auto-detects the test command based on project files, or uses .core/test.yaml. - testCmd.StringFlag("name", "Run named test command from .core/test.yaml", &name) +Examples: + core dev test + core dev test --name integration + core dev test -- go test -v ./...`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMTest(vmTestName, args) + }, + } - testCmd.Action(func() error { - args := testCmd.OtherArgs() - return runVMTest(name, args) - }) + testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", "Run named test command from .core/test.yaml") + + parent.AddCommand(testCmd) } func runVMTest(name string, command []string) error { @@ -371,34 +416,44 @@ func runVMTest(name string, command []string) error { return d.Test(ctx, projectDir, opts) } +// VM claude command flags +var ( + vmClaudeNoAuth bool + vmClaudeModel string + vmClaudeAuthFlags []string +) + // addVMClaudeCommand adds the 'devops claude' command. -func addVMClaudeCommand(parent *clir.Command) { - var noAuth bool - var model string - var authFlags []string +func addVMClaudeCommand(parent *cobra.Command) { + claudeCmd := &cobra.Command{ + Use: "claude", + Short: "Start sandboxed Claude session", + Long: `Starts a Claude Code session inside the dev environment sandbox. - claudeCmd := parent.NewSubCommand("claude", "Start sandboxed Claude session") - claudeCmd.LongDescription("Starts a Claude Code session inside the dev environment sandbox.\n\n" + - "Provides isolation while forwarding selected credentials.\n" + - "Auto-boots the dev environment if not running.\n\n" + - "Auth options (default: all):\n" + - " gh - GitHub CLI auth\n" + - " anthropic - Anthropic API key\n" + - " ssh - SSH agent forwarding\n" + - " git - Git config (name, email)\n\n" + - "Examples:\n" + - " core dev claude\n" + - " core dev claude --model opus\n" + - " core dev claude --auth gh,anthropic\n" + - " core dev claude --no-auth") +Provides isolation while forwarding selected credentials. +Auto-boots the dev environment if not running. - claudeCmd.BoolFlag("no-auth", "Don't forward any auth credentials", &noAuth) - claudeCmd.StringFlag("model", "Model to use (opus, sonnet)", &model) - claudeCmd.StringsFlag("auth", "Selective auth forwarding (gh,anthropic,ssh,git)", &authFlags) +Auth options (default: all): + gh - GitHub CLI auth + anthropic - Anthropic API key + ssh - SSH agent forwarding + git - Git config (name, email) - claudeCmd.Action(func() error { - return runVMClaude(noAuth, model, authFlags) - }) +Examples: + core dev claude + core dev claude --model opus + core dev claude --auth gh,anthropic + core dev claude --no-auth`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags) + }, + } + + claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, "Don't forward any auth credentials") + claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", "Model to use (opus, sonnet)") + claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, "Selective auth forwarding (gh,anthropic,ssh,git)") + + parent.AddCommand(claudeCmd) } func runVMClaude(noAuth bool, model string, authFlags []string) error { @@ -422,21 +477,27 @@ func runVMClaude(noAuth bool, model string, authFlags []string) error { return d.Claude(ctx, projectDir, opts) } +// VM update command flags +var vmUpdateApply bool + // addVMUpdateCommand adds the 'devops update' command. -func addVMUpdateCommand(parent *clir.Command) { - var apply bool +func addVMUpdateCommand(parent *cobra.Command) { + updateCmd := &cobra.Command{ + Use: "update", + Short: "Check for and apply updates", + Long: `Checks for dev environment updates and optionally applies them. - updateCmd := parent.NewSubCommand("update", "Check for and apply updates") - updateCmd.LongDescription("Checks for dev environment updates and optionally applies them.\n\n" + - "Examples:\n" + - " core dev update\n" + - " core dev update --apply") +Examples: + core dev update + core dev update --apply`, + RunE: func(cmd *cobra.Command, args []string) error { + return runVMUpdate(vmUpdateApply) + }, + } - updateCmd.BoolFlag("apply", "Download and apply the update", &apply) + updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, "Download and apply the update") - updateCmd.Action(func() error { - return runVMUpdate(apply) - }) + parent.AddCommand(updateCmd) } func runVMUpdate(apply bool) error { diff --git a/cmd/dev/dev_work.go b/cmd/dev/dev_work.go index e40af5ec..50cfaa66 100644 --- a/cmd/dev/dev_work.go +++ b/cmd/dev/dev_work.go @@ -13,27 +13,35 @@ import ( "github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +// Work command flags +var ( + workStatusOnly bool + workAutoCommit bool + workRegistryPath string ) // addWorkCommand adds the 'work' command to the given parent command. -func addWorkCommand(parent *clir.Command) { - var statusOnly bool - var autoCommit bool - var registryPath string +func addWorkCommand(parent *cobra.Command) { + workCmd := &cobra.Command{ + Use: "work", + Short: "Multi-repo git operations", + Long: `Manage git status, commits, and pushes across multiple repositories. - workCmd := parent.NewSubCommand("work", "Multi-repo git operations") - workCmd.LongDescription("Manage git status, commits, and pushes across multiple repositories.\n\n" + - "Reads repos.yaml to discover repositories and their relationships.\n" + - "Shows status, optionally commits with Claude, and pushes changes.") +Reads repos.yaml to discover repositories and their relationships. +Shows status, optionally commits with Claude, and pushes changes.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runWork(workRegistryPath, workStatusOnly, workAutoCommit) + }, + } - workCmd.BoolFlag("status", "Show status only, don't push", &statusOnly) - workCmd.BoolFlag("commit", "Use Claude to commit dirty repos before pushing", &autoCommit) - workCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) + workCmd.Flags().BoolVar(&workStatusOnly, "status", false, "Show status only, don't push") + workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, "Use Claude to commit dirty repos before pushing") + workCmd.Flags().StringVar(&workRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") - workCmd.Action(func() error { - return runWork(registryPath, statusOnly, autoCommit) - }) + parent.AddCommand(workCmd) } func runWork(registryPath string, statusOnly, autoCommit bool) error { diff --git a/cmd/docs/commands.go b/cmd/docs/commands.go index ba61ea03..1a3b48e5 100644 --- a/cmd/docs/commands.go +++ b/cmd/docs/commands.go @@ -8,9 +8,9 @@ // to a central location for unified documentation builds. package docs -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'docs' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddDocsCommand(app) +func AddCommands(root *cobra.Command) { + root.AddCommand(docsCmd) } diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 0592a41e..d5d8823d 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -4,7 +4,7 @@ package docs import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style and utility aliases from shared @@ -29,13 +29,14 @@ var ( Foreground(lipgloss.Color("#3b82f6")) // blue-500 ) -// AddDocsCommand adds the 'docs' command to the given parent command. -func AddDocsCommand(parent *clir.Cli) { - docsCmd := parent.NewSubCommand("docs", "Documentation management") - docsCmd.LongDescription("Manage documentation across all repos.\n" + - "Scan for docs, check coverage, and sync to core-php/docs/packages/.") - - // Add subcommands - addDocsSyncCommand(docsCmd) - addDocsListCommand(docsCmd) +var docsCmd = &cobra.Command{ + Use: "docs", + Short: "Documentation management", + Long: `Manage documentation across all repos. +Scan for docs, check coverage, and sync to core-php/docs/packages/.`, +} + +func init() { + docsCmd.AddCommand(docsSyncCmd) + docsCmd.AddCommand(docsListCmd) } diff --git a/cmd/docs/list.go b/cmd/docs/list.go index b371e5b6..b35fe777 100644 --- a/cmd/docs/list.go +++ b/cmd/docs/list.go @@ -4,18 +4,22 @@ import ( "fmt" "strings" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addDocsListCommand(parent *clir.Command) { - var registryPath string +// Flag variable for list command +var docsListRegistryPath string - listCmd := parent.NewSubCommand("list", "List documentation across repos") - listCmd.StringFlag("registry", "Path to repos.yaml", ®istryPath) +var docsListCmd = &cobra.Command{ + Use: "list", + Short: "List documentation across repos", + RunE: func(cmd *cobra.Command, args []string) error { + return runDocsList(docsListRegistryPath) + }, +} - listCmd.Action(func() error { - return runDocsList(registryPath) - }) +func init() { + docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", "Path to repos.yaml") } func runDocsList(registryPath string) error { diff --git a/cmd/docs/sync.go b/cmd/docs/sync.go index 7c47f804..66db0524 100644 --- a/cmd/docs/sync.go +++ b/cmd/docs/sync.go @@ -6,22 +6,28 @@ import ( "path/filepath" "strings" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addDocsSyncCommand(parent *clir.Command) { - var registryPath string - var dryRun bool - var outputDir string +// Flag variables for sync command +var ( + docsSyncRegistryPath string + docsSyncDryRun bool + docsSyncOutputDir string +) - syncCmd := parent.NewSubCommand("sync", "Sync documentation to core-php/docs/packages/") - syncCmd.StringFlag("registry", "Path to repos.yaml", ®istryPath) - syncCmd.BoolFlag("dry-run", "Show what would be synced without copying", &dryRun) - syncCmd.StringFlag("output", "Output directory (default: core-php/docs/packages)", &outputDir) +var docsSyncCmd = &cobra.Command{ + Use: "sync", + Short: "Sync documentation to core-php/docs/packages/", + RunE: func(cmd *cobra.Command, args []string) error { + return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun) + }, +} - syncCmd.Action(func() error { - return runDocsSync(registryPath, outputDir, dryRun) - }) +func init() { + docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", "Path to repos.yaml") + docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, "Show what would be synced without copying") + docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", "Output directory (default: core-php/docs/packages)") } // packageOutputName maps repo name to output folder name diff --git a/cmd/doctor/commands.go b/cmd/doctor/commands.go index 00219ed7..6b91129a 100644 --- a/cmd/doctor/commands.go +++ b/cmd/doctor/commands.go @@ -10,9 +10,9 @@ // Provides platform-specific installation instructions for missing tools. package doctor -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'doctor' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddDoctorCommand(app) +func AddCommands(root *cobra.Command) { + root.AddCommand(doctorCmd) } diff --git a/cmd/doctor/doctor.go b/cmd/doctor/doctor.go index fbf3a20e..b2ce7c11 100644 --- a/cmd/doctor/doctor.go +++ b/cmd/doctor/doctor.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared @@ -15,19 +15,21 @@ var ( dimStyle = shared.DimStyle ) -// AddDoctorCommand adds the 'doctor' command to the given parent command. -func AddDoctorCommand(parent *clir.Cli) { - var verbose bool +// Flag variable for doctor command +var doctorVerbose bool - doctorCmd := parent.NewSubCommand("doctor", "Check development environment") - doctorCmd.LongDescription("Checks that all required tools are installed and configured.\n" + - "Run this before `core setup` to ensure your environment is ready.") +var doctorCmd = &cobra.Command{ + Use: "doctor", + Short: "Check development environment", + Long: `Checks that all required tools are installed and configured. +Run this before 'core setup' to ensure your environment is ready.`, + RunE: func(cmd *cobra.Command, args []string) error { + return runDoctor(doctorVerbose) + }, +} - doctorCmd.BoolFlag("verbose", "Show detailed version information", &verbose) - - doctorCmd.Action(func() error { - return runDoctor(verbose) - }) +func init() { + doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, "Show detailed version information") } func runDoctor(verbose bool) error { diff --git a/cmd/go/commands.go b/cmd/go/commands.go index 86f78427..05d96aaf 100644 --- a/cmd/go/commands.go +++ b/cmd/go/commands.go @@ -14,9 +14,9 @@ // Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS. package gocmd -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'go' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddGoCommands(app) +func AddCommands(root *cobra.Command) { + AddGoCommands(root) } diff --git a/cmd/go/go.go b/cmd/go/go.go index 7b1ca163..3ad1a9a1 100644 --- a/cmd/go/go.go +++ b/cmd/go/go.go @@ -5,7 +5,7 @@ package gocmd import ( "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases for shared styles @@ -16,18 +16,22 @@ var ( ) // AddGoCommands adds Go development commands. -func AddGoCommands(parent *clir.Cli) { - goCmd := parent.NewSubCommand("go", "Go development tools") - goCmd.LongDescription("Go development tools with enhanced output and environment setup.\n\n" + - "Commands:\n" + - " test Run tests\n" + - " cov Run tests with coverage report\n" + - " fmt Format Go code\n" + - " lint Run golangci-lint\n" + - " install Install Go binary\n" + - " mod Module management (tidy, download, verify)\n" + - " work Workspace management") +func AddGoCommands(root *cobra.Command) { + goCmd := &cobra.Command{ + Use: "go", + Short: "Go development tools", + Long: "Go development tools with enhanced output and environment setup.\n\n" + + "Commands:\n" + + " test Run tests\n" + + " cov Run tests with coverage report\n" + + " fmt Format Go code\n" + + " lint Run golangci-lint\n" + + " install Install Go binary\n" + + " mod Module management (tidy, download, verify)\n" + + " work Workspace management", + } + root.AddCommand(goCmd) addGoTestCommand(goCmd) addGoCovCommand(goCmd) addGoFmtCommand(goCmd) diff --git a/cmd/go/go_format.go b/cmd/go/go_format.go index 1a89ab23..235198df 100644 --- a/cmd/go/go_format.go +++ b/cmd/go/go_format.go @@ -4,74 +4,82 @@ import ( "os" "os/exec" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addGoFmtCommand(parent *clir.Command) { - var ( - fix bool - diff bool - check bool - ) +var ( + fmtFix bool + fmtDiff bool + fmtCheck bool +) - fmtCmd := parent.NewSubCommand("fmt", "Format Go code") - fmtCmd.LongDescription("Format Go code using gofmt or goimports.\n\n" + - "Examples:\n" + - " core go fmt # Check formatting\n" + - " core go fmt --fix # Fix formatting\n" + - " core go fmt --diff # Show diff") +func addGoFmtCommand(parent *cobra.Command) { + fmtCmd := &cobra.Command{ + Use: "fmt", + Short: "Format Go code", + Long: "Format Go code using gofmt or goimports.\n\n" + + "Examples:\n" + + " core go fmt # Check formatting\n" + + " core go fmt --fix # Fix formatting\n" + + " core go fmt --diff # Show diff", + RunE: func(cmd *cobra.Command, args []string) error { + fmtArgs := []string{} + if fmtFix { + fmtArgs = append(fmtArgs, "-w") + } + if fmtDiff { + fmtArgs = append(fmtArgs, "-d") + } + if !fmtFix && !fmtDiff { + fmtArgs = append(fmtArgs, "-l") + } + fmtArgs = append(fmtArgs, ".") - fmtCmd.BoolFlag("fix", "Fix formatting in place", &fix) - fmtCmd.BoolFlag("diff", "Show diff of changes", &diff) - fmtCmd.BoolFlag("check", "Check only, exit 1 if not formatted", &check) + // Try goimports first, fall back to gofmt + var execCmd *exec.Cmd + if _, err := exec.LookPath("goimports"); err == nil { + execCmd = exec.Command("goimports", fmtArgs...) + } else { + execCmd = exec.Command("gofmt", fmtArgs...) + } - fmtCmd.Action(func() error { - args := []string{} - if fix { - args = append(args, "-w") - } - if diff { - args = append(args, "-d") - } - if !fix && !diff { - args = append(args, "-l") - } - args = append(args, ".") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } - // Try goimports first, fall back to gofmt - var cmd *exec.Cmd - if _, err := exec.LookPath("goimports"); err == nil { - cmd = exec.Command("goimports", args...) - } else { - cmd = exec.Command("gofmt", args...) - } + fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Fix formatting in place") + fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes") + fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, "Check only, exit 1 if not formatted") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + parent.AddCommand(fmtCmd) } -func addGoLintCommand(parent *clir.Command) { - var fix bool +var lintFix bool - lintCmd := parent.NewSubCommand("lint", "Run golangci-lint") - lintCmd.LongDescription("Run golangci-lint on the codebase.\n\n" + - "Examples:\n" + - " core go lint\n" + - " core go lint --fix") +func addGoLintCommand(parent *cobra.Command) { + lintCmd := &cobra.Command{ + Use: "lint", + Short: "Run golangci-lint", + Long: "Run golangci-lint on the codebase.\n\n" + + "Examples:\n" + + " core go lint\n" + + " core go lint --fix", + RunE: func(cmd *cobra.Command, args []string) error { + lintArgs := []string{"run"} + if lintFix { + lintArgs = append(lintArgs, "--fix") + } - lintCmd.BoolFlag("fix", "Fix issues automatically", &fix) + execCmd := exec.Command("golangci-lint", lintArgs...) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } - lintCmd.Action(func() error { - args := []string{"run"} - if fix { - args = append(args, "--fix") - } + lintCmd.Flags().BoolVar(&lintFix, "fix", false, "Fix issues automatically") - cmd := exec.Command("golangci-lint", args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + parent.AddCommand(lintCmd) } diff --git a/cmd/go/go_test_cmd.go b/cmd/go/go_test_cmd.go index 181848e0..792ba83b 100644 --- a/cmd/go/go_test_cmd.go +++ b/cmd/go/go_test_cmd.go @@ -9,41 +9,45 @@ import ( "strings" "github.com/charmbracelet/lipgloss" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addGoTestCommand(parent *clir.Command) { - var ( - coverage bool - pkg string - run string - short bool - race bool - json bool - verbose bool - ) +var ( + testCoverage bool + testPkg string + testRun string + testShort bool + testRace bool + testJSON bool + testVerbose bool +) - testCmd := parent.NewSubCommand("test", "Run tests with coverage") - testCmd.LongDescription("Run Go tests with coverage reporting.\n\n" + - "Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" + - "Filters noisy output and provides colour-coded coverage.\n\n" + - "Examples:\n" + - " core go test\n" + - " core go test --coverage\n" + - " core go test --pkg ./pkg/crypt\n" + - " core go test --run TestHash") +func addGoTestCommand(parent *cobra.Command) { + testCmd := &cobra.Command{ + Use: "test", + Short: "Run tests with coverage", + Long: "Run Go tests with coverage reporting.\n\n" + + "Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" + + "Filters noisy output and provides colour-coded coverage.\n\n" + + "Examples:\n" + + " core go test\n" + + " core go test --coverage\n" + + " core go test --pkg ./pkg/crypt\n" + + " core go test --run TestHash", + RunE: func(cmd *cobra.Command, args []string) error { + return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose) + }, + } - testCmd.BoolFlag("coverage", "Show detailed per-package coverage", &coverage) - testCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) - testCmd.StringFlag("run", "Run only tests matching regexp", &run) - testCmd.BoolFlag("short", "Run only short tests", &short) - testCmd.BoolFlag("race", "Enable race detector", &race) - testCmd.BoolFlag("json", "Output JSON results", &json) - testCmd.BoolFlag("v", "Verbose output", &verbose) + testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage") + testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package to test (default: ./...)") + testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching regexp") + testCmd.Flags().BoolVar(&testShort, "short", false, "Run only short tests") + testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector") + testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON results") + testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, "Verbose output") - testCmd.Action(func() error { - return runGoTest(coverage, pkg, run, short, race, json, verbose) - }) + parent.AddCommand(testCmd) } func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose bool) error { @@ -166,145 +170,150 @@ func parseOverallCoverage(output string) float64 { return total / float64(len(matches)) } -func addGoCovCommand(parent *clir.Command) { - var ( - pkg string - html bool - open bool - threshold float64 - ) +var ( + covPkg string + covHTML bool + covOpen bool + covThreshold float64 +) - covCmd := parent.NewSubCommand("cov", "Run tests with coverage report") - covCmd.LongDescription("Run tests and generate coverage report.\n\n" + - "Examples:\n" + - " core go cov # Run with coverage summary\n" + - " core go cov --html # Generate HTML report\n" + - " core go cov --open # Generate and open HTML report\n" + - " core go cov --threshold 80 # Fail if coverage < 80%") +func addGoCovCommand(parent *cobra.Command) { + covCmd := &cobra.Command{ + Use: "cov", + Short: "Run tests with coverage report", + Long: "Run tests and generate coverage report.\n\n" + + "Examples:\n" + + " core go cov # Run with coverage summary\n" + + " core go cov --html # Generate HTML report\n" + + " core go cov --open # Generate and open HTML report\n" + + " core go cov --threshold 80 # Fail if coverage < 80%", + RunE: func(cmd *cobra.Command, args []string) error { + pkg := covPkg + if pkg == "" { + // Auto-discover packages with tests + pkgs, err := findTestPackages(".") + if err != nil { + return fmt.Errorf("failed to discover test packages: %w", err) + } + if len(pkgs) == 0 { + return fmt.Errorf("no test packages found") + } + pkg = strings.Join(pkgs, " ") + } - covCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) - covCmd.BoolFlag("html", "Generate HTML coverage report", &html) - covCmd.BoolFlag("open", "Generate and open HTML report in browser", &open) - covCmd.Float64Flag("threshold", "Minimum coverage percentage (exit 1 if below)", &threshold) - - covCmd.Action(func() error { - if pkg == "" { - // Auto-discover packages with tests - pkgs, err := findTestPackages(".") + // Create temp file for coverage data + covFile, err := os.CreateTemp("", "coverage-*.out") if err != nil { - return fmt.Errorf("failed to discover test packages: %w", err) + return fmt.Errorf("failed to create coverage file: %w", err) } - if len(pkgs) == 0 { - return fmt.Errorf("no test packages found") + covPath := covFile.Name() + covFile.Close() + defer os.Remove(covPath) + + fmt.Printf("%s Running tests with coverage\n", dimStyle.Render("Coverage:")) + // Truncate package list if too long for display + displayPkg := pkg + if len(displayPkg) > 60 { + displayPkg = displayPkg[:57] + "..." } - pkg = strings.Join(pkgs, " ") - } + fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), displayPkg) + fmt.Println() - // Create temp file for coverage data - covFile, err := os.CreateTemp("", "coverage-*.out") - if err != nil { - return fmt.Errorf("failed to create coverage file: %w", err) - } - covPath := covFile.Name() - covFile.Close() - defer os.Remove(covPath) + // Run tests with coverage + // We need to split pkg into individual arguments if it contains spaces + pkgArgs := strings.Fields(pkg) + cmdArgs := append([]string{"test", "-coverprofile=" + covPath, "-covermode=atomic"}, pkgArgs...) - fmt.Printf("%s Running tests with coverage\n", dimStyle.Render("Coverage:")) - // Truncate package list if too long for display - displayPkg := pkg - if len(displayPkg) > 60 { - displayPkg = displayPkg[:57] + "..." - } - fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), displayPkg) - fmt.Println() + goCmd := exec.Command("go", cmdArgs...) + goCmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0") + goCmd.Stdout = os.Stdout + goCmd.Stderr = os.Stderr - // Run tests with coverage - // We need to split pkg into individual arguments if it contains spaces - pkgArgs := strings.Fields(pkg) - args := append([]string{"test", "-coverprofile=" + covPath, "-covermode=atomic"}, pkgArgs...) + testErr := goCmd.Run() - cmd := exec.Command("go", args...) - cmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + // Get coverage percentage + coverCmd := exec.Command("go", "tool", "cover", "-func="+covPath) + covOutput, err := coverCmd.Output() + if err != nil { + if testErr != nil { + return testErr + } + return fmt.Errorf("failed to get coverage: %w", err) + } - testErr := cmd.Run() + // Parse total coverage from last line + lines := strings.Split(strings.TrimSpace(string(covOutput)), "\n") + var totalCov float64 + if len(lines) > 0 { + lastLine := lines[len(lines)-1] + // Format: "total: (statements) XX.X%" + if strings.Contains(lastLine, "total:") { + parts := strings.Fields(lastLine) + if len(parts) >= 3 { + covStr := strings.TrimSuffix(parts[len(parts)-1], "%") + fmt.Sscanf(covStr, "%f", &totalCov) + } + } + } + + // Print coverage summary + fmt.Println() + covStyle := successStyle + if totalCov < 50 { + covStyle = errorStyle + } else if totalCov < 80 { + covStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")) + } + fmt.Printf(" %s %s\n", dimStyle.Render("Total:"), covStyle.Render(fmt.Sprintf("%.1f%%", totalCov))) + + // Generate HTML if requested + if covHTML || covOpen { + htmlPath := "coverage.html" + htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) + if err := htmlCmd.Run(); err != nil { + return fmt.Errorf("failed to generate HTML: %w", err) + } + fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath) + + if covOpen { + // Open in browser + var openCmd *exec.Cmd + switch { + case exec.Command("which", "open").Run() == nil: + openCmd = exec.Command("open", htmlPath) + case exec.Command("which", "xdg-open").Run() == nil: + openCmd = exec.Command("xdg-open", htmlPath) + default: + fmt.Printf(" %s\n", dimStyle.Render("(open manually)")) + } + if openCmd != nil { + openCmd.Run() + } + } + } + + // Check threshold + if covThreshold > 0 && totalCov < covThreshold { + fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n", + errorStyle.Render("FAIL"), totalCov, covThreshold) + return fmt.Errorf("coverage below threshold") + } - // Get coverage percentage - covCmd := exec.Command("go", "tool", "cover", "-func="+covPath) - covOutput, err := covCmd.Output() - if err != nil { if testErr != nil { return testErr } - return fmt.Errorf("failed to get coverage: %w", err) - } - // Parse total coverage from last line - lines := strings.Split(strings.TrimSpace(string(covOutput)), "\n") - var totalCov float64 - if len(lines) > 0 { - lastLine := lines[len(lines)-1] - // Format: "total: (statements) XX.X%" - if strings.Contains(lastLine, "total:") { - parts := strings.Fields(lastLine) - if len(parts) >= 3 { - covStr := strings.TrimSuffix(parts[len(parts)-1], "%") - fmt.Sscanf(covStr, "%f", &totalCov) - } - } - } + fmt.Printf("\n%s\n", successStyle.Render("OK")) + return nil + }, + } - // Print coverage summary - fmt.Println() - covStyle := successStyle - if totalCov < 50 { - covStyle = errorStyle - } else if totalCov < 80 { - covStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")) - } - fmt.Printf(" %s %s\n", dimStyle.Render("Total:"), covStyle.Render(fmt.Sprintf("%.1f%%", totalCov))) + covCmd.Flags().StringVar(&covPkg, "pkg", "", "Package to test (default: ./...)") + covCmd.Flags().BoolVar(&covHTML, "html", false, "Generate HTML coverage report") + covCmd.Flags().BoolVar(&covOpen, "open", false, "Generate and open HTML report in browser") + covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, "Minimum coverage percentage (exit 1 if below)") - // Generate HTML if requested - if html || open { - htmlPath := "coverage.html" - htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) - if err := htmlCmd.Run(); err != nil { - return fmt.Errorf("failed to generate HTML: %w", err) - } - fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath) - - if open { - // Open in browser - var openCmd *exec.Cmd - switch { - case exec.Command("which", "open").Run() == nil: - openCmd = exec.Command("open", htmlPath) - case exec.Command("which", "xdg-open").Run() == nil: - openCmd = exec.Command("xdg-open", htmlPath) - default: - fmt.Printf(" %s\n", dimStyle.Render("(open manually)")) - } - if openCmd != nil { - openCmd.Run() - } - } - } - - // Check threshold - if threshold > 0 && totalCov < threshold { - fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n", - errorStyle.Render("FAIL"), totalCov, threshold) - return fmt.Errorf("coverage below threshold") - } - - if testErr != nil { - return testErr - } - - fmt.Printf("\n%s\n", successStyle.Render("OK")) - return nil - }) + parent.AddCommand(covCmd) } func findTestPackages(root string) ([]string, error) { diff --git a/cmd/go/go_tools.go b/cmd/go/go_tools.go index dfba18bb..e6852510 100644 --- a/cmd/go/go_tools.go +++ b/cmd/go/go_tools.go @@ -6,190 +6,232 @@ import ( "os/exec" "path/filepath" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addGoInstallCommand(parent *clir.Command) { - var verbose bool - var noCgo bool +var ( + installVerbose bool + installNoCgo bool +) - installCmd := parent.NewSubCommand("install", "Install Go binary") - installCmd.LongDescription("Install Go binary to $GOPATH/bin.\n\n" + - "Examples:\n" + - " core go install # Install current module\n" + - " core go install ./cmd/core # Install specific path\n" + - " core go install --no-cgo # Pure Go (no C dependencies)\n" + - " core go install -v # Verbose output") - - installCmd.BoolFlag("v", "Verbose output", &verbose) - installCmd.BoolFlag("no-cgo", "Disable CGO (CGO_ENABLED=0)", &noCgo) - - installCmd.Action(func() error { - // Get install path from args or default to current dir - args := installCmd.OtherArgs() - installPath := "./..." - if len(args) > 0 { - installPath = args[0] - } - - // Detect if we're in a module with cmd/ subdirectories or a root main.go - if installPath == "./..." { - if _, err := os.Stat("core.go"); err == nil { - installPath = "." - } else if entries, err := os.ReadDir("cmd"); err == nil && len(entries) > 0 { - installPath = "./cmd/..." - } else if _, err := os.Stat("main.go"); err == nil { - installPath = "." +func addGoInstallCommand(parent *cobra.Command) { + installCmd := &cobra.Command{ + Use: "install [path]", + Short: "Install Go binary", + Long: "Install Go binary to $GOPATH/bin.\n\n" + + "Examples:\n" + + " core go install # Install current module\n" + + " core go install ./cmd/core # Install specific path\n" + + " core go install --no-cgo # Pure Go (no C dependencies)\n" + + " core go install -v # Verbose output", + RunE: func(cmd *cobra.Command, args []string) error { + // Get install path from args or default to current dir + installPath := "./..." + if len(args) > 0 { + installPath = args[0] } - } - fmt.Printf("%s Installing\n", dimStyle.Render("Install:")) - fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath) - if noCgo { - fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled") - } + // Detect if we're in a module with cmd/ subdirectories or a root main.go + if installPath == "./..." { + if _, err := os.Stat("core.go"); err == nil { + installPath = "." + } else if entries, err := os.ReadDir("cmd"); err == nil && len(entries) > 0 { + installPath = "./cmd/..." + } else if _, err := os.Stat("main.go"); err == nil { + installPath = "." + } + } - cmdArgs := []string{"install"} - if verbose { - cmdArgs = append(cmdArgs, "-v") - } - cmdArgs = append(cmdArgs, installPath) + fmt.Printf("%s Installing\n", dimStyle.Render("Install:")) + fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath) + if installNoCgo { + fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled") + } - cmd := exec.Command("go", cmdArgs...) - if noCgo { - cmd.Env = append(os.Environ(), "CGO_ENABLED=0") - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmdArgs := []string{"install"} + if installVerbose { + cmdArgs = append(cmdArgs, "-v") + } + cmdArgs = append(cmdArgs, installPath) - if err := cmd.Run(); err != nil { - fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed")) - return err - } + execCmd := exec.Command("go", cmdArgs...) + if installNoCgo { + execCmd.Env = append(os.Environ(), "CGO_ENABLED=0") + } + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr - // Show where it was installed - gopath := os.Getenv("GOPATH") - if gopath == "" { - home, _ := os.UserHomeDir() - gopath = filepath.Join(home, "go") - } - binDir := filepath.Join(gopath, "bin") + if err := execCmd.Run(); err != nil { + fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed")) + return err + } - fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir) - return nil - }) + // Show where it was installed + gopath := os.Getenv("GOPATH") + if gopath == "" { + home, _ := os.UserHomeDir() + gopath = filepath.Join(home, "go") + } + binDir := filepath.Join(gopath, "bin") + + fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir) + return nil + }, + } + + installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, "Verbose output") + installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, "Disable CGO (CGO_ENABLED=0)") + + parent.AddCommand(installCmd) } -func addGoModCommand(parent *clir.Command) { - modCmd := parent.NewSubCommand("mod", "Module management") - modCmd.LongDescription("Go module management commands.\n\n" + - "Commands:\n" + - " tidy Add missing and remove unused modules\n" + - " download Download modules to local cache\n" + - " verify Verify dependencies\n" + - " graph Print module dependency graph") +func addGoModCommand(parent *cobra.Command) { + modCmd := &cobra.Command{ + Use: "mod", + Short: "Module management", + Long: "Go module management commands.\n\n" + + "Commands:\n" + + " tidy Add missing and remove unused modules\n" + + " download Download modules to local cache\n" + + " verify Verify dependencies\n" + + " graph Print module dependency graph", + } // tidy - tidyCmd := modCmd.NewSubCommand("tidy", "Tidy go.mod") - tidyCmd.Action(func() error { - cmd := exec.Command("go", "mod", "tidy") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + tidyCmd := &cobra.Command{ + Use: "tidy", + Short: "Tidy go.mod", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "mod", "tidy") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } // download - downloadCmd := modCmd.NewSubCommand("download", "Download modules") - downloadCmd.Action(func() error { - cmd := exec.Command("go", "mod", "download") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + downloadCmd := &cobra.Command{ + Use: "download", + Short: "Download modules", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "mod", "download") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } // verify - verifyCmd := modCmd.NewSubCommand("verify", "Verify dependencies") - verifyCmd.Action(func() error { - cmd := exec.Command("go", "mod", "verify") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + verifyCmd := &cobra.Command{ + Use: "verify", + Short: "Verify dependencies", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "mod", "verify") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } // graph - graphCmd := modCmd.NewSubCommand("graph", "Print dependency graph") - graphCmd.Action(func() error { - cmd := exec.Command("go", "mod", "graph") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + graphCmd := &cobra.Command{ + Use: "graph", + Short: "Print dependency graph", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "mod", "graph") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } + + modCmd.AddCommand(tidyCmd) + modCmd.AddCommand(downloadCmd) + modCmd.AddCommand(verifyCmd) + modCmd.AddCommand(graphCmd) + parent.AddCommand(modCmd) } -func addGoWorkCommand(parent *clir.Command) { - workCmd := parent.NewSubCommand("work", "Workspace management") - workCmd.LongDescription("Go workspace management commands.\n\n" + - "Commands:\n" + - " sync Sync go.work with modules\n" + - " init Initialize go.work\n" + - " use Add module to workspace") +func addGoWorkCommand(parent *cobra.Command) { + workCmd := &cobra.Command{ + Use: "work", + Short: "Workspace management", + Long: "Go workspace management commands.\n\n" + + "Commands:\n" + + " sync Sync go.work with modules\n" + + " init Initialize go.work\n" + + " use Add module to workspace", + } // sync - syncCmd := workCmd.NewSubCommand("sync", "Sync workspace") - syncCmd.Action(func() error { - cmd := exec.Command("go", "work", "sync") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + syncCmd := &cobra.Command{ + Use: "sync", + Short: "Sync workspace", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "work", "sync") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } // init - initCmd := workCmd.NewSubCommand("init", "Initialize workspace") - initCmd.Action(func() error { - cmd := exec.Command("go", "work", "init") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return err - } - // Auto-add current module if go.mod exists - if _, err := os.Stat("go.mod"); err == nil { - cmd = exec.Command("go", "work", "use", ".") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } - return nil - }) - - // use - useCmd := workCmd.NewSubCommand("use", "Add module to workspace") - useCmd.Action(func() error { - args := useCmd.OtherArgs() - if len(args) == 0 { - // Auto-detect modules - modules := findGoModules(".") - if len(modules) == 0 { - return fmt.Errorf("no go.mod files found") + initCmd := &cobra.Command{ + Use: "init", + Short: "Initialize workspace", + RunE: func(cmd *cobra.Command, args []string) error { + execCmd := exec.Command("go", "work", "init") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + if err := execCmd.Run(); err != nil { + return err } - for _, mod := range modules { - cmd := exec.Command("go", "work", "use", mod) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return err - } - fmt.Printf("Added %s\n", mod) + // Auto-add current module if go.mod exists + if _, err := os.Stat("go.mod"); err == nil { + execCmd = exec.Command("go", "work", "use", ".") + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() } return nil - } + }, + } - cmdArgs := append([]string{"work", "use"}, args...) - cmd := exec.Command("go", cmdArgs...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - }) + // use + useCmd := &cobra.Command{ + Use: "use [modules...]", + Short: "Add module to workspace", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + // Auto-detect modules + modules := findGoModules(".") + if len(modules) == 0 { + return fmt.Errorf("no go.mod files found") + } + for _, mod := range modules { + execCmd := exec.Command("go", "work", "use", mod) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + if err := execCmd.Run(); err != nil { + return err + } + fmt.Printf("Added %s\n", mod) + } + return nil + } + + cmdArgs := append([]string{"work", "use"}, args...) + execCmd := exec.Command("go", cmdArgs...) + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + return execCmd.Run() + }, + } + + workCmd.AddCommand(syncCmd) + workCmd.AddCommand(initCmd) + workCmd.AddCommand(useCmd) + parent.AddCommand(workCmd) } func findGoModules(root string) []string { diff --git a/cmd/php/commands.go b/cmd/php/commands.go index a83555ac..61d4b0a3 100644 --- a/cmd/php/commands.go +++ b/cmd/php/commands.go @@ -33,9 +33,9 @@ // - deploy:list: List recent deployments package php -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'php' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddPHPCommands(app) +func AddCommands(root *cobra.Command) { + AddPHPCommands(root) } diff --git a/cmd/php/php.go b/cmd/php/php.go index c4f97e4c..5f2a66ce 100644 --- a/cmd/php/php.go +++ b/cmd/php/php.go @@ -4,7 +4,7 @@ package php import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared @@ -78,15 +78,19 @@ var ( ) // AddPHPCommands adds PHP/Laravel development commands. -func AddPHPCommands(parent *clir.Cli) { - phpCmd := parent.NewSubCommand("php", "Laravel/PHP development tools") - phpCmd.LongDescription("Manage Laravel development environment with FrankenPHP.\n\n" + - "Services orchestrated:\n" + - " - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" + - " - Vite dev server (port 5173)\n" + - " - Laravel Horizon (queue workers)\n" + - " - Laravel Reverb (WebSocket, port 8080)\n" + - " - Redis (port 6379)") +func AddPHPCommands(root *cobra.Command) { + phpCmd := &cobra.Command{ + Use: "php", + Short: "Laravel/PHP development tools", + Long: "Manage Laravel development environment with FrankenPHP.\n\n" + + "Services orchestrated:\n" + + " - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" + + " - Vite dev server (port 5173)\n" + + " - Laravel Horizon (queue workers)\n" + + " - Laravel Reverb (WebSocket, port 8080)\n" + + " - Redis (port 6379)", + } + root.AddCommand(phpCmd) // Development addPHPDevCommand(phpCmd) diff --git a/cmd/php/php_build.go b/cmd/php/php_build.go index 942ad732..3b215f01 100644 --- a/cmd/php/php_build.go +++ b/cmd/php/php_build.go @@ -7,67 +7,71 @@ import ( "strings" phppkg "github.com/host-uk/core/pkg/php" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addPHPBuildCommand(parent *clir.Command) { - var ( - buildType string - imageName string - tag string - platform string - dockerfile string - outputPath string - format string - template string - noCache bool - ) +var ( + buildType string + buildImageName string + buildTag string + buildPlatform string + buildDockerfile string + buildOutputPath string + buildFormat string + buildTemplate string + buildNoCache bool +) - buildCmd := parent.NewSubCommand("build", "Build Docker or LinuxKit image") - buildCmd.LongDescription("Build a production-ready container image for the PHP project.\n\n" + - "By default, builds a Docker image using FrankenPHP.\n" + - "Use --type linuxkit to build a LinuxKit VM image instead.\n\n" + - "Examples:\n" + - " core php build # Build Docker image\n" + - " core php build --name myapp --tag v1.0 # Build with custom name/tag\n" + - " core php build --type linuxkit # Build LinuxKit image\n" + - " core php build --type linuxkit --format iso # Build ISO image") +func addPHPBuildCommand(parent *cobra.Command) { + buildCmd := &cobra.Command{ + Use: "build", + Short: "Build Docker or LinuxKit image", + Long: "Build a production-ready container image for the PHP project.\n\n" + + "By default, builds a Docker image using FrankenPHP.\n" + + "Use --type linuxkit to build a LinuxKit VM image instead.\n\n" + + "Examples:\n" + + " core php build # Build Docker image\n" + + " core php build --name myapp --tag v1.0 # Build with custom name/tag\n" + + " core php build --type linuxkit # Build LinuxKit image\n" + + " core php build --type linuxkit --format iso # Build ISO image", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } - buildCmd.StringFlag("type", "Build type: docker (default) or linuxkit", &buildType) - buildCmd.StringFlag("name", "Image name (default: project directory name)", &imageName) - buildCmd.StringFlag("tag", "Image tag (default: latest)", &tag) - buildCmd.StringFlag("platform", "Target platform (e.g., linux/amd64, linux/arm64)", &platform) - buildCmd.StringFlag("dockerfile", "Path to custom Dockerfile", &dockerfile) - buildCmd.StringFlag("output", "Output path for LinuxKit image", &outputPath) - buildCmd.StringFlag("format", "LinuxKit output format: qcow2 (default), iso, raw, vmdk", &format) - buildCmd.StringFlag("template", "LinuxKit template name (default: server-php)", &template) - buildCmd.BoolFlag("no-cache", "Build without cache", &noCache) + ctx := context.Background() - buildCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } + switch strings.ToLower(buildType) { + case "linuxkit": + return runPHPBuildLinuxKit(ctx, cwd, linuxKitBuildOptions{ + OutputPath: buildOutputPath, + Format: buildFormat, + Template: buildTemplate, + }) + default: + return runPHPBuildDocker(ctx, cwd, dockerBuildOptions{ + ImageName: buildImageName, + Tag: buildTag, + Platform: buildPlatform, + Dockerfile: buildDockerfile, + NoCache: buildNoCache, + }) + } + }, + } - ctx := context.Background() + buildCmd.Flags().StringVar(&buildType, "type", "", "Build type: docker (default) or linuxkit") + buildCmd.Flags().StringVar(&buildImageName, "name", "", "Image name (default: project directory name)") + buildCmd.Flags().StringVar(&buildTag, "tag", "", "Image tag (default: latest)") + buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64, linux/arm64)") + buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", "Path to custom Dockerfile") + buildCmd.Flags().StringVar(&buildOutputPath, "output", "", "Output path for LinuxKit image") + buildCmd.Flags().StringVar(&buildFormat, "format", "", "LinuxKit output format: qcow2 (default), iso, raw, vmdk") + buildCmd.Flags().StringVar(&buildTemplate, "template", "", "LinuxKit template name (default: server-php)") + buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, "Build without cache") - switch strings.ToLower(buildType) { - case "linuxkit": - return runPHPBuildLinuxKit(ctx, cwd, linuxKitBuildOptions{ - OutputPath: outputPath, - Format: format, - Template: template, - }) - default: - return runPHPBuildDocker(ctx, cwd, dockerBuildOptions{ - ImageName: imageName, - Tag: tag, - Platform: platform, - Dockerfile: dockerfile, - NoCache: noCache, - }) - } - }) + parent.AddCommand(buildCmd) } type dockerBuildOptions struct { @@ -182,115 +186,120 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu return nil } -func addPHPServeCommand(parent *clir.Command) { - var ( - imageName string - tag string - containerName string - port int - httpsPort int - detach bool - envFile string - ) +var ( + serveImageName string + serveTag string + serveContainerName string + servePort int + serveHTTPSPort int + serveDetach bool + serveEnvFile string +) - serveCmd := parent.NewSubCommand("serve", "Run production container") - serveCmd.LongDescription("Run a production PHP container.\n\n" + - "This starts the built Docker image in production mode.\n\n" + - "Examples:\n" + - " core php serve --name myapp # Run container\n" + - " core php serve --name myapp -d # Run detached\n" + - " core php serve --name myapp --port 8080 # Custom port") - - serveCmd.StringFlag("name", "Docker image name (required)", &imageName) - serveCmd.StringFlag("tag", "Image tag (default: latest)", &tag) - serveCmd.StringFlag("container", "Container name", &containerName) - serveCmd.IntFlag("port", "HTTP port (default: 80)", &port) - serveCmd.IntFlag("https-port", "HTTPS port (default: 443)", &httpsPort) - serveCmd.BoolFlag("d", "Run in detached mode", &detach) - serveCmd.StringFlag("env-file", "Path to environment file", &envFile) - - serveCmd.Action(func() error { - if imageName == "" { - // Try to detect from current directory - cwd, err := os.Getwd() - if err == nil { - imageName = phppkg.GetLaravelAppName(cwd) - if imageName != "" { - imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-")) +func addPHPServeCommand(parent *cobra.Command) { + serveCmd := &cobra.Command{ + Use: "serve", + Short: "Run production container", + Long: "Run a production PHP container.\n\n" + + "This starts the built Docker image in production mode.\n\n" + + "Examples:\n" + + " core php serve --name myapp # Run container\n" + + " core php serve --name myapp -d # Run detached\n" + + " core php serve --name myapp --port 8080 # Custom port", + RunE: func(cmd *cobra.Command, args []string) error { + imageName := serveImageName + if imageName == "" { + // Try to detect from current directory + cwd, err := os.Getwd() + if err == nil { + imageName = phppkg.GetLaravelAppName(cwd) + if imageName != "" { + imageName = strings.ToLower(strings.ReplaceAll(imageName, " ", "-")) + } + } + if imageName == "" { + return fmt.Errorf("--name is required: specify the Docker image name") } } - if imageName == "" { - return fmt.Errorf("--name is required: specify the Docker image name") + + ctx := context.Background() + + opts := phppkg.ServeOptions{ + ImageName: imageName, + Tag: serveTag, + ContainerName: serveContainerName, + Port: servePort, + HTTPSPort: serveHTTPSPort, + Detach: serveDetach, + EnvFile: serveEnvFile, + Output: os.Stdout, } - } - ctx := context.Background() + fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:")) + fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string { + if serveTag == "" { + return "latest" + } + return serveTag + }()) - opts := phppkg.ServeOptions{ - ImageName: imageName, - Tag: tag, - ContainerName: containerName, - Port: port, - HTTPSPort: httpsPort, - Detach: detach, - EnvFile: envFile, - Output: os.Stdout, - } - - fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:")) - fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string { - if tag == "" { - return "latest" + effectivePort := servePort + if effectivePort == 0 { + effectivePort = 80 + } + effectiveHTTPSPort := serveHTTPSPort + if effectiveHTTPSPort == 0 { + effectiveHTTPSPort = 443 } - return tag - }()) - effectivePort := port - if effectivePort == 0 { - effectivePort = 80 - } - effectiveHTTPSPort := httpsPort - if effectiveHTTPSPort == 0 { - effectiveHTTPSPort = 443 - } + fmt.Printf("%s http://localhost:%d, https://localhost:%d\n", + dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) + fmt.Println() - fmt.Printf("%s http://localhost:%d, https://localhost:%d\n", - dimStyle.Render("Ports:"), effectivePort, effectiveHTTPSPort) - fmt.Println() + if err := phppkg.ServeProduction(ctx, opts); err != nil { + return fmt.Errorf("failed to start container: %w", err) + } - if err := phppkg.ServeProduction(ctx, opts); err != nil { - return fmt.Errorf("failed to start container: %w", err) - } + if !serveDetach { + fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:")) + } - if !detach { - fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:")) - } + return nil + }, + } - return nil - }) + serveCmd.Flags().StringVar(&serveImageName, "name", "", "Docker image name (required)") + serveCmd.Flags().StringVar(&serveTag, "tag", "", "Image tag (default: latest)") + serveCmd.Flags().StringVar(&serveContainerName, "container", "", "Container name") + serveCmd.Flags().IntVar(&servePort, "port", 0, "HTTP port (default: 80)") + serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, "HTTPS port (default: 443)") + serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, "Run in detached mode") + serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", "Path to environment file") + + parent.AddCommand(serveCmd) } -func addPHPShellCommand(parent *clir.Command) { - shellCmd := parent.NewSubCommand("shell", "Open shell in running container") - shellCmd.LongDescription("Open an interactive shell in a running PHP container.\n\n" + - "Examples:\n" + - " core php shell abc123 # Shell into container by ID\n" + - " core php shell myapp # Shell into container by name") +func addPHPShellCommand(parent *cobra.Command) { + shellCmd := &cobra.Command{ + Use: "shell [container]", + Short: "Open shell in running container", + Long: "Open an interactive shell in a running PHP container.\n\n" + + "Examples:\n" + + " core php shell abc123 # Shell into container by ID\n" + + " core php shell myapp # Shell into container by name", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() - shellCmd.Action(func() error { - args := shellCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("container ID or name is required") - } + fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0]) - ctx := context.Background() + if err := phppkg.Shell(ctx, args[0]); err != nil { + return fmt.Errorf("failed to open shell: %w", err) + } - fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0]) + return nil + }, + } - if err := phppkg.Shell(ctx, args[0]); err != nil { - return fmt.Errorf("failed to open shell: %w", err) - } - - return nil - }) + parent.AddCommand(shellCmd) } diff --git a/cmd/php/php_deploy.go b/cmd/php/php_deploy.go index 4a482fc1..57ea918a 100644 --- a/cmd/php/php_deploy.go +++ b/cmd/php/php_deploy.go @@ -8,7 +8,7 @@ import ( "github.com/charmbracelet/lipgloss" phppkg "github.com/host-uk/core/pkg/php" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Deploy command styles @@ -23,7 +23,7 @@ var ( Foreground(lipgloss.Color("#ef4444")) // red-500 ) -func addPHPDeployCommands(parent *clir.Command) { +func addPHPDeployCommands(parent *cobra.Command) { // Main deploy command addPHPDeployCommand(parent) @@ -37,235 +37,252 @@ func addPHPDeployCommands(parent *clir.Command) { addPHPDeployListCommand(parent) } -func addPHPDeployCommand(parent *clir.Command) { - var ( - staging bool - force bool - wait bool - ) +var ( + deployStaging bool + deployForce bool + deployWait bool +) - deployCmd := parent.NewSubCommand("deploy", "Deploy to Coolify") - deployCmd.LongDescription("Deploy the PHP application to Coolify.\n\n" + - "Requires configuration in .env:\n" + - " COOLIFY_URL=https://coolify.example.com\n" + - " COOLIFY_TOKEN=your-api-token\n" + - " COOLIFY_APP_ID=production-app-id\n" + - " COOLIFY_STAGING_APP_ID=staging-app-id (optional)\n\n" + - "Examples:\n" + - " core php deploy # Deploy to production\n" + - " core php deploy --staging # Deploy to staging\n" + - " core php deploy --force # Force deployment\n" + - " core php deploy --wait # Wait for deployment to complete") - - deployCmd.BoolFlag("staging", "Deploy to staging environment", &staging) - deployCmd.BoolFlag("force", "Force deployment even if no changes detected", &force) - deployCmd.BoolFlag("wait", "Wait for deployment to complete", &wait) - - deployCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - env := phppkg.EnvProduction - if staging { - env = phppkg.EnvStaging - } - - fmt.Printf("%s Deploying to %s...\n\n", dimStyle.Render("Deploy:"), env) - - ctx := context.Background() - - opts := phppkg.DeployOptions{ - Dir: cwd, - Environment: env, - Force: force, - Wait: wait, - } - - status, err := phppkg.Deploy(ctx, opts) - if err != nil { - return fmt.Errorf("deployment failed: %w", err) - } - - printDeploymentStatus(status) - - if wait { - if phppkg.IsDeploymentSuccessful(status.Status) { - fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:")) - } else { - fmt.Printf("\n%s Deployment ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) +func addPHPDeployCommand(parent *cobra.Command) { + deployCmd := &cobra.Command{ + Use: "deploy", + Short: "Deploy to Coolify", + Long: "Deploy the PHP application to Coolify.\n\n" + + "Requires configuration in .env:\n" + + " COOLIFY_URL=https://coolify.example.com\n" + + " COOLIFY_TOKEN=your-api-token\n" + + " COOLIFY_APP_ID=production-app-id\n" + + " COOLIFY_STAGING_APP_ID=staging-app-id (optional)\n\n" + + "Examples:\n" + + " core php deploy # Deploy to production\n" + + " core php deploy --staging # Deploy to staging\n" + + " core php deploy --force # Force deployment\n" + + " core php deploy --wait # Wait for deployment to complete", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } - } else { - fmt.Printf("\n%s Deployment triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) - } - return nil - }) -} - -func addPHPDeployStatusCommand(parent *clir.Command) { - var ( - staging bool - deploymentID string - ) - - statusCmd := parent.NewSubCommand("deploy:status", "Show deployment status") - statusCmd.LongDescription("Show the status of a deployment.\n\n" + - "Examples:\n" + - " core php deploy:status # Latest production deployment\n" + - " core php deploy:status --staging # Latest staging deployment\n" + - " core php deploy:status --id abc123 # Specific deployment") - - statusCmd.BoolFlag("staging", "Check staging environment", &staging) - statusCmd.StringFlag("id", "Specific deployment ID", &deploymentID) - - statusCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - env := phppkg.EnvProduction - if staging { - env = phppkg.EnvStaging - } - - fmt.Printf("%s Checking %s deployment status...\n\n", dimStyle.Render("Deploy:"), env) - - ctx := context.Background() - - opts := phppkg.StatusOptions{ - Dir: cwd, - Environment: env, - DeploymentID: deploymentID, - } - - status, err := phppkg.DeployStatus(ctx, opts) - if err != nil { - return fmt.Errorf("failed to get status: %w", err) - } - - printDeploymentStatus(status) - - return nil - }) -} - -func addPHPDeployRollbackCommand(parent *clir.Command) { - var ( - staging bool - deploymentID string - wait bool - ) - - rollbackCmd := parent.NewSubCommand("deploy:rollback", "Rollback to previous deployment") - rollbackCmd.LongDescription("Rollback to a previous deployment.\n\n" + - "If no deployment ID is specified, rolls back to the most recent\n" + - "successful deployment.\n\n" + - "Examples:\n" + - " core php deploy:rollback # Rollback to previous\n" + - " core php deploy:rollback --staging # Rollback staging\n" + - " core php deploy:rollback --id abc123 # Rollback to specific deployment") - - rollbackCmd.BoolFlag("staging", "Rollback staging environment", &staging) - rollbackCmd.StringFlag("id", "Specific deployment ID to rollback to", &deploymentID) - rollbackCmd.BoolFlag("wait", "Wait for rollback to complete", &wait) - - rollbackCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - env := phppkg.EnvProduction - if staging { - env = phppkg.EnvStaging - } - - fmt.Printf("%s Rolling back %s...\n\n", dimStyle.Render("Deploy:"), env) - - ctx := context.Background() - - opts := phppkg.RollbackOptions{ - Dir: cwd, - Environment: env, - DeploymentID: deploymentID, - Wait: wait, - } - - status, err := phppkg.Rollback(ctx, opts) - if err != nil { - return fmt.Errorf("rollback failed: %w", err) - } - - printDeploymentStatus(status) - - if wait { - if phppkg.IsDeploymentSuccessful(status.Status) { - fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:")) - } else { - fmt.Printf("\n%s Rollback ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) + env := phppkg.EnvProduction + if deployStaging { + env = phppkg.EnvStaging } - } else { - fmt.Printf("\n%s Rollback triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) - } - return nil - }) -} + fmt.Printf("%s Deploying to %s...\n\n", dimStyle.Render("Deploy:"), env) -func addPHPDeployListCommand(parent *clir.Command) { - var ( - staging bool - limit int - ) + ctx := context.Background() - listCmd := parent.NewSubCommand("deploy:list", "List recent deployments") - listCmd.LongDescription("List recent deployments.\n\n" + - "Examples:\n" + - " core php deploy:list # List production deployments\n" + - " core php deploy:list --staging # List staging deployments\n" + - " core php deploy:list --limit 20 # List more deployments") + opts := phppkg.DeployOptions{ + Dir: cwd, + Environment: env, + Force: deployForce, + Wait: deployWait, + } - listCmd.BoolFlag("staging", "List staging deployments", &staging) - listCmd.IntFlag("limit", "Number of deployments to list (default: 10)", &limit) + status, err := phppkg.Deploy(ctx, opts) + if err != nil { + return fmt.Errorf("deployment failed: %w", err) + } - listCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } + printDeploymentStatus(status) - env := phppkg.EnvProduction - if staging { - env = phppkg.EnvStaging - } + if deployWait { + if phppkg.IsDeploymentSuccessful(status.Status) { + fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:")) + } else { + fmt.Printf("\n%s Deployment ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) + } + } else { + fmt.Printf("\n%s Deployment triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) + } - if limit == 0 { - limit = 10 - } - - fmt.Printf("%s Recent %s deployments:\n\n", dimStyle.Render("Deploy:"), env) - - ctx := context.Background() - - deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit) - if err != nil { - return fmt.Errorf("failed to list deployments: %w", err) - } - - if len(deployments) == 0 { - fmt.Printf("%s No deployments found\n", dimStyle.Render("Info:")) return nil - } + }, + } - for i, d := range deployments { - printDeploymentSummary(i+1, &d) - } + deployCmd.Flags().BoolVar(&deployStaging, "staging", false, "Deploy to staging environment") + deployCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if no changes detected") + deployCmd.Flags().BoolVar(&deployWait, "wait", false, "Wait for deployment to complete") - return nil - }) + parent.AddCommand(deployCmd) +} + +var ( + deployStatusStaging bool + deployStatusDeploymentID string +) + +func addPHPDeployStatusCommand(parent *cobra.Command) { + statusCmd := &cobra.Command{ + Use: "deploy:status", + Short: "Show deployment status", + Long: "Show the status of a deployment.\n\n" + + "Examples:\n" + + " core php deploy:status # Latest production deployment\n" + + " core php deploy:status --staging # Latest staging deployment\n" + + " core php deploy:status --id abc123 # Specific deployment", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + env := phppkg.EnvProduction + if deployStatusStaging { + env = phppkg.EnvStaging + } + + fmt.Printf("%s Checking %s deployment status...\n\n", dimStyle.Render("Deploy:"), env) + + ctx := context.Background() + + opts := phppkg.StatusOptions{ + Dir: cwd, + Environment: env, + DeploymentID: deployStatusDeploymentID, + } + + status, err := phppkg.DeployStatus(ctx, opts) + if err != nil { + return fmt.Errorf("failed to get status: %w", err) + } + + printDeploymentStatus(status) + + return nil + }, + } + + statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, "Check staging environment") + statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", "Specific deployment ID") + + parent.AddCommand(statusCmd) +} + +var ( + rollbackStaging bool + rollbackDeploymentID string + rollbackWait bool +) + +func addPHPDeployRollbackCommand(parent *cobra.Command) { + rollbackCmd := &cobra.Command{ + Use: "deploy:rollback", + Short: "Rollback to previous deployment", + Long: "Rollback to a previous deployment.\n\n" + + "If no deployment ID is specified, rolls back to the most recent\n" + + "successful deployment.\n\n" + + "Examples:\n" + + " core php deploy:rollback # Rollback to previous\n" + + " core php deploy:rollback --staging # Rollback staging\n" + + " core php deploy:rollback --id abc123 # Rollback to specific deployment", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + env := phppkg.EnvProduction + if rollbackStaging { + env = phppkg.EnvStaging + } + + fmt.Printf("%s Rolling back %s...\n\n", dimStyle.Render("Deploy:"), env) + + ctx := context.Background() + + opts := phppkg.RollbackOptions{ + Dir: cwd, + Environment: env, + DeploymentID: rollbackDeploymentID, + Wait: rollbackWait, + } + + status, err := phppkg.Rollback(ctx, opts) + if err != nil { + return fmt.Errorf("rollback failed: %w", err) + } + + printDeploymentStatus(status) + + if rollbackWait { + if phppkg.IsDeploymentSuccessful(status.Status) { + fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:")) + } else { + fmt.Printf("\n%s Rollback ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) + } + } else { + fmt.Printf("\n%s Rollback triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) + } + + return nil + }, + } + + rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, "Rollback staging environment") + rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", "Specific deployment ID to rollback to") + rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, "Wait for rollback to complete") + + parent.AddCommand(rollbackCmd) +} + +var ( + deployListStaging bool + deployListLimit int +) + +func addPHPDeployListCommand(parent *cobra.Command) { + listCmd := &cobra.Command{ + Use: "deploy:list", + Short: "List recent deployments", + Long: "List recent deployments.\n\n" + + "Examples:\n" + + " core php deploy:list # List production deployments\n" + + " core php deploy:list --staging # List staging deployments\n" + + " core php deploy:list --limit 20 # List more deployments", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + env := phppkg.EnvProduction + if deployListStaging { + env = phppkg.EnvStaging + } + + limit := deployListLimit + if limit == 0 { + limit = 10 + } + + fmt.Printf("%s Recent %s deployments:\n\n", dimStyle.Render("Deploy:"), env) + + ctx := context.Background() + + deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit) + if err != nil { + return fmt.Errorf("failed to list deployments: %w", err) + } + + if len(deployments) == 0 { + fmt.Printf("%s No deployments found\n", dimStyle.Render("Info:")) + return nil + } + + for i, d := range deployments { + printDeploymentSummary(i+1, &d) + } + + return nil + }, + } + + listCmd.Flags().BoolVar(&deployListStaging, "staging", false, "List staging deployments") + listCmd.Flags().IntVar(&deployListLimit, "limit", 0, "Number of deployments to list (default: 10)") + + parent.AddCommand(listCmd) } func printDeploymentStatus(status *phppkg.DeploymentStatus) { diff --git a/cmd/php/php_dev.go b/cmd/php/php_dev.go index 7f9bd3e9..c326cd6e 100644 --- a/cmd/php/php_dev.go +++ b/cmd/php/php_dev.go @@ -12,47 +12,51 @@ import ( "github.com/charmbracelet/lipgloss" phppkg "github.com/host-uk/core/pkg/php" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addPHPDevCommand(parent *clir.Command) { - var ( - noVite bool - noHorizon bool - noReverb bool - noRedis bool - https bool - domain string - port int - ) +var ( + devNoVite bool + devNoHorizon bool + devNoReverb bool + devNoRedis bool + devHTTPS bool + devDomain string + devPort int +) - devCmd := parent.NewSubCommand("dev", "Start Laravel development environment") - devCmd.LongDescription("Starts all detected Laravel services.\n\n" + - "Auto-detects:\n" + - " - Vite (vite.config.js/ts)\n" + - " - Horizon (config/horizon.php)\n" + - " - Reverb (config/reverb.php)\n" + - " - Redis (from .env)") +func addPHPDevCommand(parent *cobra.Command) { + devCmd := &cobra.Command{ + Use: "dev", + Short: "Start Laravel development environment", + Long: "Starts all detected Laravel services.\n\n" + + "Auto-detects:\n" + + " - Vite (vite.config.js/ts)\n" + + " - Horizon (config/horizon.php)\n" + + " - Reverb (config/reverb.php)\n" + + " - Redis (from .env)", + RunE: func(cmd *cobra.Command, args []string) error { + return runPHPDev(phpDevOptions{ + NoVite: devNoVite, + NoHorizon: devNoHorizon, + NoReverb: devNoReverb, + NoRedis: devNoRedis, + HTTPS: devHTTPS, + Domain: devDomain, + Port: devPort, + }) + }, + } - devCmd.BoolFlag("no-vite", "Skip Vite dev server", &noVite) - devCmd.BoolFlag("no-horizon", "Skip Laravel Horizon", &noHorizon) - devCmd.BoolFlag("no-reverb", "Skip Laravel Reverb", &noReverb) - devCmd.BoolFlag("no-redis", "Skip Redis server", &noRedis) - devCmd.BoolFlag("https", "Enable HTTPS with mkcert", &https) - devCmd.StringFlag("domain", "Domain for SSL certificate (default: from APP_URL or localhost)", &domain) - devCmd.IntFlag("port", "FrankenPHP port (default: 8000)", &port) + devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, "Skip Vite dev server") + devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, "Skip Laravel Horizon") + devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, "Skip Laravel Reverb") + devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, "Skip Redis server") + devCmd.Flags().BoolVar(&devHTTPS, "https", false, "Enable HTTPS with mkcert") + devCmd.Flags().StringVar(&devDomain, "domain", "", "Domain for SSL certificate (default: from APP_URL or localhost)") + devCmd.Flags().IntVar(&devPort, "port", 0, "FrankenPHP port (default: 8000)") - devCmd.Action(func() error { - return runPHPDev(phpDevOptions{ - NoVite: noVite, - NoHorizon: noHorizon, - NoReverb: noReverb, - NoRedis: noRedis, - HTTPS: https, - Domain: domain, - Port: port, - }) - }) + parent.AddCommand(devCmd) } type phpDevOptions struct { @@ -181,20 +185,26 @@ shutdown: return nil } -func addPHPLogsCommand(parent *clir.Command) { - var follow bool - var service string +var ( + logsFollow bool + logsService string +) - logsCmd := parent.NewSubCommand("logs", "View service logs") - logsCmd.LongDescription("Stream logs from Laravel services.\n\n" + - "Services: frankenphp, vite, horizon, reverb, redis") +func addPHPLogsCommand(parent *cobra.Command) { + logsCmd := &cobra.Command{ + Use: "logs", + Short: "View service logs", + Long: "Stream logs from Laravel services.\n\n" + + "Services: frankenphp, vite, horizon, reverb, redis", + RunE: func(cmd *cobra.Command, args []string) error { + return runPHPLogs(logsService, logsFollow) + }, + } - logsCmd.BoolFlag("follow", "Follow log output", &follow) - logsCmd.StringFlag("service", "Specific service (default: all)", &service) + logsCmd.Flags().BoolVar(&logsFollow, "follow", false, "Follow log output") + logsCmd.Flags().StringVar(&logsService, "service", "", "Specific service (default: all)") - logsCmd.Action(func() error { - return runPHPLogs(service, follow) - }) + parent.AddCommand(logsCmd) } func runPHPLogs(service string, follow bool) error { @@ -241,12 +251,16 @@ func runPHPLogs(service string, follow bool) error { return scanner.Err() } -func addPHPStopCommand(parent *clir.Command) { - stopCmd := parent.NewSubCommand("stop", "Stop all Laravel services") +func addPHPStopCommand(parent *cobra.Command) { + stopCmd := &cobra.Command{ + Use: "stop", + Short: "Stop all Laravel services", + RunE: func(cmd *cobra.Command, args []string) error { + return runPHPStop() + }, + } - stopCmd.Action(func() error { - return runPHPStop() - }) + parent.AddCommand(stopCmd) } func runPHPStop() error { @@ -268,12 +282,16 @@ func runPHPStop() error { return nil } -func addPHPStatusCommand(parent *clir.Command) { - statusCmd := parent.NewSubCommand("status", "Show service status") +func addPHPStatusCommand(parent *cobra.Command) { + statusCmd := &cobra.Command{ + Use: "status", + Short: "Show service status", + RunE: func(cmd *cobra.Command, args []string) error { + return runPHPStatus() + }, + } - statusCmd.Action(func() error { - return runPHPStatus() - }) + parent.AddCommand(statusCmd) } func runPHPStatus() error { @@ -325,16 +343,20 @@ func runPHPStatus() error { return nil } -func addPHPSSLCommand(parent *clir.Command) { - var domain string +var sslDomain string - sslCmd := parent.NewSubCommand("ssl", "Setup SSL certificates with mkcert") +func addPHPSSLCommand(parent *cobra.Command) { + sslCmd := &cobra.Command{ + Use: "ssl", + Short: "Setup SSL certificates with mkcert", + RunE: func(cmd *cobra.Command, args []string) error { + return runPHPSSL(sslDomain) + }, + } - sslCmd.StringFlag("domain", "Domain for certificate (default: from APP_URL)", &domain) + sslCmd.Flags().StringVar(&sslDomain, "domain", "", "Domain for certificate (default: from APP_URL)") - sslCmd.Action(func() error { - return runPHPSSL(domain) - }) + parent.AddCommand(sslCmd) } func runPHPSSL(domain string) error { diff --git a/cmd/php/php_packages.go b/cmd/php/php_packages.go index 306a0a54..b80b4462 100644 --- a/cmd/php/php_packages.go +++ b/cmd/php/php_packages.go @@ -5,19 +5,23 @@ import ( "os" phppkg "github.com/host-uk/core/pkg/php" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addPHPPackagesCommands(parent *clir.Command) { - packagesCmd := parent.NewSubCommand("packages", "Manage local PHP packages") - packagesCmd.LongDescription("Link and manage local PHP packages for development.\n\n" + - "Similar to npm link, this adds path repositories to composer.json\n" + - "for developing packages alongside your project.\n\n" + - "Commands:\n" + - " link - Link local packages by path\n" + - " unlink - Unlink packages by name\n" + - " update - Update linked packages\n" + - " list - List linked packages") +func addPHPPackagesCommands(parent *cobra.Command) { + packagesCmd := &cobra.Command{ + Use: "packages", + Short: "Manage local PHP packages", + Long: "Link and manage local PHP packages for development.\n\n" + + "Similar to npm link, this adds path repositories to composer.json\n" + + "for developing packages alongside your project.\n\n" + + "Commands:\n" + + " link - Link local packages by path\n" + + " unlink - Unlink packages by name\n" + + " update - Update linked packages\n" + + " list - List linked packages", + } + parent.AddCommand(packagesCmd) addPHPPackagesLinkCommand(packagesCmd) addPHPPackagesUnlinkCommand(packagesCmd) @@ -25,133 +29,139 @@ func addPHPPackagesCommands(parent *clir.Command) { addPHPPackagesListCommand(packagesCmd) } -func addPHPPackagesLinkCommand(parent *clir.Command) { - linkCmd := parent.NewSubCommand("link", "Link local packages") - linkCmd.LongDescription("Link local PHP packages for development.\n\n" + - "Adds path repositories to composer.json with symlink enabled.\n" + - "The package name is auto-detected from each path's composer.json.\n\n" + - "Examples:\n" + - " core php packages link ../my-package\n" + - " core php packages link ../pkg-a ../pkg-b") +func addPHPPackagesLinkCommand(parent *cobra.Command) { + linkCmd := &cobra.Command{ + Use: "link [paths...]", + Short: "Link local packages", + Long: "Link local PHP packages for development.\n\n" + + "Adds path repositories to composer.json with symlink enabled.\n" + + "The package name is auto-detected from each path's composer.json.\n\n" + + "Examples:\n" + + " core php packages link ../my-package\n" + + " core php packages link ../pkg-a ../pkg-b", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } - linkCmd.Action(func() error { - args := linkCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("at least one package path is required") - } + fmt.Printf("%s Linking packages...\n\n", dimStyle.Render("PHP:")) - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } + if err := phppkg.LinkPackages(cwd, args); err != nil { + return fmt.Errorf("failed to link packages: %w", err) + } - fmt.Printf("%s Linking packages...\n\n", dimStyle.Render("PHP:")) - - if err := phppkg.LinkPackages(cwd, args); err != nil { - return fmt.Errorf("failed to link packages: %w", err) - } - - fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:")) - return nil - }) -} - -func addPHPPackagesUnlinkCommand(parent *clir.Command) { - unlinkCmd := parent.NewSubCommand("unlink", "Unlink packages") - unlinkCmd.LongDescription("Remove linked packages from composer.json.\n\n" + - "Removes path repositories by package name.\n\n" + - "Examples:\n" + - " core php packages unlink vendor/my-package\n" + - " core php packages unlink vendor/pkg-a vendor/pkg-b") - - unlinkCmd.Action(func() error { - args := unlinkCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("at least one package name is required") - } - - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - fmt.Printf("%s Unlinking packages...\n\n", dimStyle.Render("PHP:")) - - if err := phppkg.UnlinkPackages(cwd, args); err != nil { - return fmt.Errorf("failed to unlink packages: %w", err) - } - - fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:")) - return nil - }) -} - -func addPHPPackagesUpdateCommand(parent *clir.Command) { - updateCmd := parent.NewSubCommand("update", "Update linked packages") - updateCmd.LongDescription("Run composer update for linked packages.\n\n" + - "If no packages specified, updates all packages.\n\n" + - "Examples:\n" + - " core php packages update\n" + - " core php packages update vendor/my-package") - - updateCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - args := updateCmd.OtherArgs() - - fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:")) - - if err := phppkg.UpdatePackages(cwd, args); err != nil { - return fmt.Errorf("composer update failed: %w", err) - } - - fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:")) - return nil - }) -} - -func addPHPPackagesListCommand(parent *clir.Command) { - listCmd := parent.NewSubCommand("list", "List linked packages") - listCmd.LongDescription("List all locally linked packages.\n\n" + - "Shows package name, path, and version for each linked package.") - - listCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - packages, err := phppkg.ListLinkedPackages(cwd) - if err != nil { - return fmt.Errorf("failed to list packages: %w", err) - } - - if len(packages) == 0 { - fmt.Printf("%s No linked packages found\n", dimStyle.Render("PHP:")) + fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:")) return nil - } + }, + } - fmt.Printf("%s Linked packages:\n\n", dimStyle.Render("PHP:")) - - for _, pkg := range packages { - name := pkg.Name - if name == "" { - name = "(unknown)" - } - version := pkg.Version - if version == "" { - version = "dev" - } - - fmt.Printf(" %s %s\n", successStyle.Render("*"), name) - fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), pkg.Path) - fmt.Printf(" %s %s\n", dimStyle.Render("Version:"), version) - fmt.Println() - } - - return nil - }) + parent.AddCommand(linkCmd) +} + +func addPHPPackagesUnlinkCommand(parent *cobra.Command) { + unlinkCmd := &cobra.Command{ + Use: "unlink [packages...]", + Short: "Unlink packages", + Long: "Remove linked packages from composer.json.\n\n" + + "Removes path repositories by package name.\n\n" + + "Examples:\n" + + " core php packages unlink vendor/my-package\n" + + " core php packages unlink vendor/pkg-a vendor/pkg-b", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + fmt.Printf("%s Unlinking packages...\n\n", dimStyle.Render("PHP:")) + + if err := phppkg.UnlinkPackages(cwd, args); err != nil { + return fmt.Errorf("failed to unlink packages: %w", err) + } + + fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:")) + return nil + }, + } + + parent.AddCommand(unlinkCmd) +} + +func addPHPPackagesUpdateCommand(parent *cobra.Command) { + updateCmd := &cobra.Command{ + Use: "update [packages...]", + Short: "Update linked packages", + Long: "Run composer update for linked packages.\n\n" + + "If no packages specified, updates all packages.\n\n" + + "Examples:\n" + + " core php packages update\n" + + " core php packages update vendor/my-package", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:")) + + if err := phppkg.UpdatePackages(cwd, args); err != nil { + return fmt.Errorf("composer update failed: %w", err) + } + + fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:")) + return nil + }, + } + + parent.AddCommand(updateCmd) +} + +func addPHPPackagesListCommand(parent *cobra.Command) { + listCmd := &cobra.Command{ + Use: "list", + Short: "List linked packages", + Long: "List all locally linked packages.\n\n" + + "Shows package name, path, and version for each linked package.", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + packages, err := phppkg.ListLinkedPackages(cwd) + if err != nil { + return fmt.Errorf("failed to list packages: %w", err) + } + + if len(packages) == 0 { + fmt.Printf("%s No linked packages found\n", dimStyle.Render("PHP:")) + return nil + } + + fmt.Printf("%s Linked packages:\n\n", dimStyle.Render("PHP:")) + + for _, pkg := range packages { + name := pkg.Name + if name == "" { + name = "(unknown)" + } + version := pkg.Version + if version == "" { + version = "dev" + } + + fmt.Printf(" %s %s\n", successStyle.Render("*"), name) + fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), pkg.Path) + fmt.Printf(" %s %s\n", dimStyle.Render("Version:"), version) + fmt.Println() + } + + return nil + }, + } + + parent.AddCommand(listCmd) } diff --git a/cmd/php/php_quality.go b/cmd/php/php_quality.go index 57150b3d..84c54b21 100644 --- a/cmd/php/php_quality.go +++ b/cmd/php/php_quality.go @@ -11,573 +11,601 @@ import ( "github.com/charmbracelet/lipgloss" phppkg "github.com/host-uk/core/pkg/php" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) -func addPHPTestCommand(parent *clir.Command) { - var ( - parallel bool - coverage bool - filter string - group string - ) +var ( + testParallel bool + testCoverage bool + testFilter string + testGroup string +) - testCmd := parent.NewSubCommand("test", "Run PHP tests (PHPUnit/Pest)") - testCmd.LongDescription("Run PHP tests using PHPUnit or Pest.\n\n" + - "Auto-detects Pest if tests/Pest.php exists, otherwise uses PHPUnit.\n\n" + - "Examples:\n" + - " core php test # Run all tests\n" + - " core php test --parallel # Run tests in parallel\n" + - " core php test --coverage # Run with coverage\n" + - " core php test --filter UserTest # Filter by test name") - - testCmd.BoolFlag("parallel", "Run tests in parallel", ¶llel) - testCmd.BoolFlag("coverage", "Generate code coverage", &coverage) - testCmd.StringFlag("filter", "Filter tests by name pattern", &filter) - testCmd.StringFlag("group", "Run only tests in specified group", &group) - - testCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - // Detect test runner - runner := phppkg.DetectTestRunner(cwd) - fmt.Printf("%s Running tests with %s\n\n", dimStyle.Render("PHP:"), runner) - - ctx := context.Background() - - opts := phppkg.TestOptions{ - Dir: cwd, - Filter: filter, - Parallel: parallel, - Coverage: coverage, - Output: os.Stdout, - } - - if group != "" { - opts.Groups = []string{group} - } - - if err := phppkg.RunTests(ctx, opts); err != nil { - return fmt.Errorf("tests failed: %w", err) - } - - return nil - }) -} - -func addPHPFmtCommand(parent *clir.Command) { - var ( - fix bool - diff bool - ) - - fmtCmd := parent.NewSubCommand("fmt", "Format PHP code with Laravel Pint") - fmtCmd.LongDescription("Format PHP code using Laravel Pint.\n\n" + - "Examples:\n" + - " core php fmt # Check formatting (dry-run)\n" + - " core php fmt --fix # Auto-fix formatting issues\n" + - " core php fmt --diff # Show diff of changes") - - fmtCmd.BoolFlag("fix", "Auto-fix formatting issues", &fix) - fmtCmd.BoolFlag("diff", "Show diff of changes", &diff) - - fmtCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - // Detect formatter - formatter, found := phppkg.DetectFormatter(cwd) - if !found { - return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") - } - - action := "Checking" - if fix { - action = "Formatting" - } - fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter) - - ctx := context.Background() - - opts := phppkg.FormatOptions{ - Dir: cwd, - Fix: fix, - Diff: diff, - Output: os.Stdout, - } - - // Get any additional paths from args - if args := fmtCmd.OtherArgs(); len(args) > 0 { - opts.Paths = args - } - - if err := phppkg.Format(ctx, opts); err != nil { - if fix { - return fmt.Errorf("formatting failed: %w", err) +func addPHPTestCommand(parent *cobra.Command) { + testCmd := &cobra.Command{ + Use: "test", + Short: "Run PHP tests (PHPUnit/Pest)", + Long: "Run PHP tests using PHPUnit or Pest.\n\n" + + "Auto-detects Pest if tests/Pest.php exists, otherwise uses PHPUnit.\n\n" + + "Examples:\n" + + " core php test # Run all tests\n" + + " core php test --parallel # Run tests in parallel\n" + + " core php test --coverage # Run with coverage\n" + + " core php test --filter UserTest # Filter by test name", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } - return fmt.Errorf("formatting issues found: %w", err) - } - if fix { - fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:")) - } else { - fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:")) - } + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } - return nil - }) + // Detect test runner + runner := phppkg.DetectTestRunner(cwd) + fmt.Printf("%s Running tests with %s\n\n", dimStyle.Render("PHP:"), runner) + + ctx := context.Background() + + opts := phppkg.TestOptions{ + Dir: cwd, + Filter: testFilter, + Parallel: testParallel, + Coverage: testCoverage, + Output: os.Stdout, + } + + if testGroup != "" { + opts.Groups = []string{testGroup} + } + + if err := phppkg.RunTests(ctx, opts); err != nil { + return fmt.Errorf("tests failed: %w", err) + } + + return nil + }, + } + + testCmd.Flags().BoolVar(&testParallel, "parallel", false, "Run tests in parallel") + testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Generate code coverage") + testCmd.Flags().StringVar(&testFilter, "filter", "", "Filter tests by name pattern") + testCmd.Flags().StringVar(&testGroup, "group", "", "Run only tests in specified group") + + parent.AddCommand(testCmd) } -func addPHPAnalyseCommand(parent *clir.Command) { - var ( - level int - memory string - ) +var ( + fmtFix bool + fmtDiff bool +) - analyseCmd := parent.NewSubCommand("analyse", "Run PHPStan static analysis") - analyseCmd.LongDescription("Run PHPStan or Larastan static analysis.\n\n" + - "Auto-detects Larastan if installed, otherwise uses PHPStan.\n\n" + - "Examples:\n" + - " core php analyse # Run analysis\n" + - " core php analyse --level 9 # Run at max strictness\n" + - " core php analyse --memory 2G # Increase memory limit") +func addPHPFmtCommand(parent *cobra.Command) { + fmtCmd := &cobra.Command{ + Use: "fmt [paths...]", + Short: "Format PHP code with Laravel Pint", + Long: "Format PHP code using Laravel Pint.\n\n" + + "Examples:\n" + + " core php fmt # Check formatting (dry-run)\n" + + " core php fmt --fix # Auto-fix formatting issues\n" + + " core php fmt --diff # Show diff of changes", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } - analyseCmd.IntFlag("level", "PHPStan analysis level (0-9)", &level) - analyseCmd.StringFlag("memory", "Memory limit (e.g., 2G)", &memory) + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } - analyseCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } + // Detect formatter + formatter, found := phppkg.DetectFormatter(cwd) + if !found { + return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") + } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } + action := "Checking" + if fmtFix { + action = "Formatting" + } + fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter) - // Detect analyser - analyser, found := phppkg.DetectAnalyser(cwd) - if !found { - return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") - } + ctx := context.Background() - fmt.Printf("%s Running static analysis with %s\n\n", dimStyle.Render("PHP:"), analyser) + opts := phppkg.FormatOptions{ + Dir: cwd, + Fix: fmtFix, + Diff: fmtDiff, + Output: os.Stdout, + } - ctx := context.Background() + // Get any additional paths from args + if len(args) > 0 { + opts.Paths = args + } - opts := phppkg.AnalyseOptions{ - Dir: cwd, - Level: level, - Memory: memory, - Output: os.Stdout, - } + if err := phppkg.Format(ctx, opts); err != nil { + if fmtFix { + return fmt.Errorf("formatting failed: %w", err) + } + return fmt.Errorf("formatting issues found: %w", err) + } - // Get any additional paths from args - if args := analyseCmd.OtherArgs(); len(args) > 0 { - opts.Paths = args - } + if fmtFix { + fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:")) + } else { + fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:")) + } - if err := phppkg.Analyse(ctx, opts); err != nil { - return fmt.Errorf("analysis found issues: %w", err) - } + return nil + }, + } - fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) - return nil - }) + fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Auto-fix formatting issues") + fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes") + + parent.AddCommand(fmtCmd) +} + +var ( + analyseLevel int + analyseMemory string +) + +func addPHPAnalyseCommand(parent *cobra.Command) { + analyseCmd := &cobra.Command{ + Use: "analyse [paths...]", + Short: "Run PHPStan static analysis", + Long: "Run PHPStan or Larastan static analysis.\n\n" + + "Auto-detects Larastan if installed, otherwise uses PHPStan.\n\n" + + "Examples:\n" + + " core php analyse # Run analysis\n" + + " core php analyse --level 9 # Run at max strictness\n" + + " core php analyse --memory 2G # Increase memory limit", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } + + // Detect analyser + analyser, found := phppkg.DetectAnalyser(cwd) + if !found { + return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") + } + + fmt.Printf("%s Running static analysis with %s\n\n", dimStyle.Render("PHP:"), analyser) + + ctx := context.Background() + + opts := phppkg.AnalyseOptions{ + Dir: cwd, + Level: analyseLevel, + Memory: analyseMemory, + Output: os.Stdout, + } + + // Get any additional paths from args + if len(args) > 0 { + opts.Paths = args + } + + if err := phppkg.Analyse(ctx, opts); err != nil { + return fmt.Errorf("analysis found issues: %w", err) + } + + fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) + return nil + }, + } + + analyseCmd.Flags().IntVar(&analyseLevel, "level", 0, "PHPStan analysis level (0-9)") + analyseCmd.Flags().StringVar(&analyseMemory, "memory", "", "Memory limit (e.g., 2G)") + + parent.AddCommand(analyseCmd) } // ============================================================================= // New QA Commands // ============================================================================= -func addPHPPsalmCommand(parent *clir.Command) { - var ( - level int - fix bool - baseline bool - showInfo bool - ) +var ( + psalmLevel int + psalmFix bool + psalmBaseline bool + psalmShowInfo bool +) - psalmCmd := parent.NewSubCommand("psalm", "Run Psalm static analysis") - psalmCmd.LongDescription("Run Psalm deep static analysis with Laravel plugin support.\n\n" + - "Psalm provides deeper type inference than PHPStan and catches\n" + - "different classes of bugs. Both should be run for best coverage.\n\n" + - "Examples:\n" + - " core php psalm # Run analysis\n" + - " core php psalm --fix # Auto-fix issues where possible\n" + - " core php psalm --level 3 # Run at specific level (1-8)\n" + - " core php psalm --baseline # Generate baseline file") - - psalmCmd.IntFlag("level", "Error level (1=strictest, 8=most lenient)", &level) - psalmCmd.BoolFlag("fix", "Auto-fix issues where possible", &fix) - psalmCmd.BoolFlag("baseline", "Generate/update baseline file", &baseline) - psalmCmd.BoolFlag("show-info", "Show info-level issues", &showInfo) - - psalmCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - // Check if Psalm is available - _, found := phppkg.DetectPsalm(cwd) - if !found { - fmt.Printf("%s Psalm not found\n\n", errorStyle.Render("Error:")) - fmt.Printf("%s composer require --dev vimeo/psalm\n", dimStyle.Render("Install:")) - fmt.Printf("%s ./vendor/bin/psalm --init\n", dimStyle.Render("Setup:")) - return fmt.Errorf("psalm not installed") - } - - action := "Analysing" - if fix { - action = "Analysing and fixing" - } - fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action) - - ctx := context.Background() - - opts := phppkg.PsalmOptions{ - Dir: cwd, - Level: level, - Fix: fix, - Baseline: baseline, - ShowInfo: showInfo, - Output: os.Stdout, - } - - if err := phppkg.RunPsalm(ctx, opts); err != nil { - return fmt.Errorf("psalm found issues: %w", err) - } - - fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) - return nil - }) -} - -func addPHPAuditCommand(parent *clir.Command) { - var ( - jsonOutput bool - fix bool - ) - - auditCmd := parent.NewSubCommand("audit", "Security audit for dependencies") - auditCmd.LongDescription("Check PHP and JavaScript dependencies for known vulnerabilities.\n\n" + - "Runs composer audit and npm audit (if package.json exists).\n\n" + - "Examples:\n" + - " core php audit # Check all dependencies\n" + - " core php audit --json # Output as JSON\n" + - " core php audit --fix # Auto-fix where possible (npm only)") - - auditCmd.BoolFlag("json", "Output in JSON format", &jsonOutput) - auditCmd.BoolFlag("fix", "Auto-fix vulnerabilities (npm only)", &fix) - - auditCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - fmt.Printf("%s Scanning dependencies for vulnerabilities\n\n", dimStyle.Render("Audit:")) - - ctx := context.Background() - - results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{ - Dir: cwd, - JSON: jsonOutput, - Fix: fix, - Output: os.Stdout, - }) - if err != nil { - return fmt.Errorf("audit failed: %w", err) - } - - // Print results - totalVulns := 0 - hasErrors := false - - for _, result := range results { - icon := successStyle.Render("✓") - status := successStyle.Render("secure") - - if result.Error != nil { - icon = errorStyle.Render("✗") - status = errorStyle.Render("error") - hasErrors = true - } else if result.Vulnerabilities > 0 { - icon = errorStyle.Render("✗") - status = errorStyle.Render(fmt.Sprintf("%d vulnerabilities", result.Vulnerabilities)) - totalVulns += result.Vulnerabilities +func addPHPPsalmCommand(parent *cobra.Command) { + psalmCmd := &cobra.Command{ + Use: "psalm", + Short: "Run Psalm static analysis", + Long: "Run Psalm deep static analysis with Laravel plugin support.\n\n" + + "Psalm provides deeper type inference than PHPStan and catches\n" + + "different classes of bugs. Both should be run for best coverage.\n\n" + + "Examples:\n" + + " core php psalm # Run analysis\n" + + " core php psalm --fix # Auto-fix issues where possible\n" + + " core php psalm --level 3 # Run at specific level (1-8)\n" + + " core php psalm --baseline # Generate baseline file", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } - fmt.Printf(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status) - - // Show advisories - for _, adv := range result.Advisories { - severity := adv.Severity - if severity == "" { - severity = "unknown" - } - sevStyle := getSeverityStyle(severity) - fmt.Printf(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package) - if adv.Title != "" { - fmt.Printf(" %s\n", dimStyle.Render(adv.Title)) - } - } - } - - fmt.Println() - - if totalVulns > 0 { - fmt.Printf("%s Found %d vulnerabilities across dependencies\n", errorStyle.Render("Warning:"), totalVulns) - fmt.Printf("%s composer update && npm update\n", dimStyle.Render("Fix:")) - return fmt.Errorf("vulnerabilities found") - } - - if hasErrors { - return fmt.Errorf("audit completed with errors") - } - - fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:")) - return nil - }) -} - -func addPHPSecurityCommand(parent *clir.Command) { - var ( - severity string - jsonOutput bool - sarif bool - url string - ) - - securityCmd := parent.NewSubCommand("security", "Security vulnerability scanning") - securityCmd.LongDescription("Scan for security vulnerabilities in configuration and code.\n\n" + - "Checks environment config, file permissions, code patterns,\n" + - "and runs security-focused static analysis.\n\n" + - "Examples:\n" + - " core php security # Run all checks\n" + - " core php security --severity=high # Only high+ severity\n" + - " core php security --json # JSON output") - - securityCmd.StringFlag("severity", "Minimum severity (critical, high, medium, low)", &severity) - securityCmd.BoolFlag("json", "Output in JSON format", &jsonOutput) - securityCmd.BoolFlag("sarif", "Output in SARIF format (for GitHub Security)", &sarif) - securityCmd.StringFlag("url", "URL to check HTTP headers (optional)", &url) - - securityCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - fmt.Printf("%s Running security checks\n\n", dimStyle.Render("Security:")) - - ctx := context.Background() - - result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{ - Dir: cwd, - Severity: severity, - JSON: jsonOutput, - SARIF: sarif, - URL: url, - Output: os.Stdout, - }) - if err != nil { - return fmt.Errorf("security check failed: %w", err) - } - - // Print results by category - currentCategory := "" - for _, check := range result.Checks { - category := strings.Split(check.ID, "_")[0] - if category != currentCategory { - if currentCategory != "" { - fmt.Println() - } - currentCategory = category - fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+" CHECKS:")) + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") } - icon := successStyle.Render("✓") - if !check.Passed { - icon = getSeverityStyle(check.Severity).Render("✗") + // Check if Psalm is available + _, found := phppkg.DetectPsalm(cwd) + if !found { + fmt.Printf("%s Psalm not found\n\n", errorStyle.Render("Error:")) + fmt.Printf("%s composer require --dev vimeo/psalm\n", dimStyle.Render("Install:")) + fmt.Printf("%s ./vendor/bin/psalm --init\n", dimStyle.Render("Setup:")) + return fmt.Errorf("psalm not installed") } - fmt.Printf(" %s %s\n", icon, check.Name) - if !check.Passed && check.Message != "" { - fmt.Printf(" %s\n", dimStyle.Render(check.Message)) - if check.Fix != "" { - fmt.Printf(" %s %s\n", dimStyle.Render("Fix:"), check.Fix) - } + action := "Analysing" + if psalmFix { + action = "Analysing and fixing" } - } + fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action) - fmt.Println() + ctx := context.Background() - // Print summary - fmt.Printf("%s Security scan complete\n", dimStyle.Render("Summary:")) - fmt.Printf(" %s %d/%d\n", dimStyle.Render("Passed:"), result.Summary.Passed, result.Summary.Total) - - if result.Summary.Critical > 0 { - fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render("Critical:"), result.Summary.Critical) - } - if result.Summary.High > 0 { - fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render("High:"), result.Summary.High) - } - if result.Summary.Medium > 0 { - fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render("Medium:"), result.Summary.Medium) - } - if result.Summary.Low > 0 { - fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render("Low:"), result.Summary.Low) - } - - if result.Summary.Critical > 0 || result.Summary.High > 0 { - return fmt.Errorf("critical or high severity issues found") - } - - return nil - }) -} - -func addPHPQACommand(parent *clir.Command) { - var ( - quick bool - full bool - fix bool - ) - - qaCmd := parent.NewSubCommand("qa", "Run full QA pipeline") - qaCmd.LongDescription("Run the complete quality assurance pipeline.\n\n" + - "Stages:\n" + - " quick: Security audit, code style, PHPStan\n" + - " standard: Psalm, tests\n" + - " full: Rector dry-run, mutation testing (slow)\n\n" + - "Examples:\n" + - " core php qa # Run quick + standard stages\n" + - " core php qa --quick # Only quick checks\n" + - " core php qa --full # All stages including slow ones\n" + - " core php qa --fix # Auto-fix where possible") - - qaCmd.BoolFlag("quick", "Only run quick checks", &quick) - qaCmd.BoolFlag("full", "Run all stages including slow checks", &full) - qaCmd.BoolFlag("fix", "Auto-fix issues where possible", &fix) - - qaCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - // Determine stages - opts := phppkg.QAOptions{ - Dir: cwd, - Quick: quick, - Full: full, - Fix: fix, - } - stages := phppkg.GetQAStages(opts) - - // Print header - stageNames := make([]string, len(stages)) - for i, s := range stages { - stageNames[i] = string(s) - } - fmt.Printf("%s Running QA pipeline (%s)\n\n", dimStyle.Render("QA:"), strings.Join(stageNames, " → ")) - - ctx := context.Background() - var allPassed = true - var results []phppkg.QACheckResult - - for _, stage := range stages { - fmt.Printf("%s\n", phpQAStageStyle.Render("═══ "+strings.ToUpper(string(stage))+" STAGE ═══")) - - checks := phppkg.GetQAChecks(cwd, stage) - if len(checks) == 0 { - fmt.Printf(" %s\n\n", dimStyle.Render("No checks available")) - continue + opts := phppkg.PsalmOptions{ + Dir: cwd, + Level: psalmLevel, + Fix: psalmFix, + Baseline: psalmBaseline, + ShowInfo: psalmShowInfo, + Output: os.Stdout, } - for _, checkName := range checks { - result := runQACheck(ctx, cwd, checkName, fix) - result.Stage = stage - results = append(results, result) - - icon := phpQAPassedStyle.Render("✓") - status := phpQAPassedStyle.Render("passed") - if !result.Passed { - icon = phpQAFailedStyle.Render("✗") - status = phpQAFailedStyle.Render("failed") - allPassed = false - } - - fmt.Printf(" %s %s %s %s\n", icon, result.Name, status, dimStyle.Render(result.Duration)) + if err := phppkg.RunPsalm(ctx, opts); err != nil { + return fmt.Errorf("psalm found issues: %w", err) } - fmt.Println() - } - // Print summary - passedCount := 0 - var failedChecks []phppkg.QACheckResult - for _, r := range results { - if r.Passed { - passedCount++ - } else { - failedChecks = append(failedChecks, r) - } - } - - if allPassed { - fmt.Printf("%s All checks passed (%d/%d)\n", phpQAPassedStyle.Render("QA PASSED:"), passedCount, len(results)) + fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) return nil - } + }, + } - fmt.Printf("%s Some checks failed (%d/%d passed)\n\n", phpQAFailedStyle.Render("QA FAILED:"), passedCount, len(results)) + psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, "Error level (1=strictest, 8=most lenient)") + psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, "Auto-fix issues where possible") + psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, "Generate/update baseline file") + psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, "Show info-level issues") - // Show what needs fixing - fmt.Printf("%s\n", dimStyle.Render("To fix:")) - for _, check := range failedChecks { - fixCmd := getQAFixCommand(check.Name, fix) - issue := check.Output - if issue == "" { - issue = "issues found" + parent.AddCommand(psalmCmd) +} + +var ( + auditJSONOutput bool + auditFix bool +) + +func addPHPAuditCommand(parent *cobra.Command) { + auditCmd := &cobra.Command{ + Use: "audit", + Short: "Security audit for dependencies", + Long: "Check PHP and JavaScript dependencies for known vulnerabilities.\n\n" + + "Runs composer audit and npm audit (if package.json exists).\n\n" + + "Examples:\n" + + " core php audit # Check all dependencies\n" + + " core php audit --json # Output as JSON\n" + + " core php audit --fix # Auto-fix where possible (npm only)", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } - fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("•"), check.Name+": "+issue) - if fixCmd != "" { - fmt.Printf(" %s %s\n", dimStyle.Render("→"), fixCmd) - } - } - return fmt.Errorf("QA pipeline failed") - }) + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } + + fmt.Printf("%s Scanning dependencies for vulnerabilities\n\n", dimStyle.Render("Audit:")) + + ctx := context.Background() + + results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{ + Dir: cwd, + JSON: auditJSONOutput, + Fix: auditFix, + Output: os.Stdout, + }) + if err != nil { + return fmt.Errorf("audit failed: %w", err) + } + + // Print results + totalVulns := 0 + hasErrors := false + + for _, result := range results { + icon := successStyle.Render("✓") + status := successStyle.Render("secure") + + if result.Error != nil { + icon = errorStyle.Render("✗") + status = errorStyle.Render("error") + hasErrors = true + } else if result.Vulnerabilities > 0 { + icon = errorStyle.Render("✗") + status = errorStyle.Render(fmt.Sprintf("%d vulnerabilities", result.Vulnerabilities)) + totalVulns += result.Vulnerabilities + } + + fmt.Printf(" %s %s %s\n", icon, dimStyle.Render(result.Tool+":"), status) + + // Show advisories + for _, adv := range result.Advisories { + severity := adv.Severity + if severity == "" { + severity = "unknown" + } + sevStyle := getSeverityStyle(severity) + fmt.Printf(" %s %s\n", sevStyle.Render("["+severity+"]"), adv.Package) + if adv.Title != "" { + fmt.Printf(" %s\n", dimStyle.Render(adv.Title)) + } + } + } + + fmt.Println() + + if totalVulns > 0 { + fmt.Printf("%s Found %d vulnerabilities across dependencies\n", errorStyle.Render("Warning:"), totalVulns) + fmt.Printf("%s composer update && npm update\n", dimStyle.Render("Fix:")) + return fmt.Errorf("vulnerabilities found") + } + + if hasErrors { + return fmt.Errorf("audit completed with errors") + } + + fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:")) + return nil + }, + } + + auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, "Output in JSON format") + auditCmd.Flags().BoolVar(&auditFix, "fix", false, "Auto-fix vulnerabilities (npm only)") + + parent.AddCommand(auditCmd) +} + +var ( + securitySeverity string + securityJSONOutput bool + securitySarif bool + securityURL string +) + +func addPHPSecurityCommand(parent *cobra.Command) { + securityCmd := &cobra.Command{ + Use: "security", + Short: "Security vulnerability scanning", + Long: "Scan for security vulnerabilities in configuration and code.\n\n" + + "Checks environment config, file permissions, code patterns,\n" + + "and runs security-focused static analysis.\n\n" + + "Examples:\n" + + " core php security # Run all checks\n" + + " core php security --severity=high # Only high+ severity\n" + + " core php security --json # JSON output", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } + + fmt.Printf("%s Running security checks\n\n", dimStyle.Render("Security:")) + + ctx := context.Background() + + result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{ + Dir: cwd, + Severity: securitySeverity, + JSON: securityJSONOutput, + SARIF: securitySarif, + URL: securityURL, + Output: os.Stdout, + }) + if err != nil { + return fmt.Errorf("security check failed: %w", err) + } + + // Print results by category + currentCategory := "" + for _, check := range result.Checks { + category := strings.Split(check.ID, "_")[0] + if category != currentCategory { + if currentCategory != "" { + fmt.Println() + } + currentCategory = category + fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+" CHECKS:")) + } + + icon := successStyle.Render("✓") + if !check.Passed { + icon = getSeverityStyle(check.Severity).Render("✗") + } + + fmt.Printf(" %s %s\n", icon, check.Name) + if !check.Passed && check.Message != "" { + fmt.Printf(" %s\n", dimStyle.Render(check.Message)) + if check.Fix != "" { + fmt.Printf(" %s %s\n", dimStyle.Render("Fix:"), check.Fix) + } + } + } + + fmt.Println() + + // Print summary + fmt.Printf("%s Security scan complete\n", dimStyle.Render("Summary:")) + fmt.Printf(" %s %d/%d\n", dimStyle.Render("Passed:"), result.Summary.Passed, result.Summary.Total) + + if result.Summary.Critical > 0 { + fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render("Critical:"), result.Summary.Critical) + } + if result.Summary.High > 0 { + fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render("High:"), result.Summary.High) + } + if result.Summary.Medium > 0 { + fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render("Medium:"), result.Summary.Medium) + } + if result.Summary.Low > 0 { + fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render("Low:"), result.Summary.Low) + } + + if result.Summary.Critical > 0 || result.Summary.High > 0 { + return fmt.Errorf("critical or high severity issues found") + } + + return nil + }, + } + + securityCmd.Flags().StringVar(&securitySeverity, "severity", "", "Minimum severity (critical, high, medium, low)") + securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, "Output in JSON format") + securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, "Output in SARIF format (for GitHub Security)") + securityCmd.Flags().StringVar(&securityURL, "url", "", "URL to check HTTP headers (optional)") + + parent.AddCommand(securityCmd) +} + +var ( + qaQuick bool + qaFull bool + qaFix bool +) + +func addPHPQACommand(parent *cobra.Command) { + qaCmd := &cobra.Command{ + Use: "qa", + Short: "Run full QA pipeline", + Long: "Run the complete quality assurance pipeline.\n\n" + + "Stages:\n" + + " quick: Security audit, code style, PHPStan\n" + + " standard: Psalm, tests\n" + + " full: Rector dry-run, mutation testing (slow)\n\n" + + "Examples:\n" + + " core php qa # Run quick + standard stages\n" + + " core php qa --quick # Only quick checks\n" + + " core php qa --full # All stages including slow ones\n" + + " core php qa --fix # Auto-fix where possible", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } + + // Determine stages + opts := phppkg.QAOptions{ + Dir: cwd, + Quick: qaQuick, + Full: qaFull, + Fix: qaFix, + } + stages := phppkg.GetQAStages(opts) + + // Print header + stageNames := make([]string, len(stages)) + for i, s := range stages { + stageNames[i] = string(s) + } + fmt.Printf("%s Running QA pipeline (%s)\n\n", dimStyle.Render("QA:"), strings.Join(stageNames, " → ")) + + ctx := context.Background() + var allPassed = true + var results []phppkg.QACheckResult + + for _, stage := range stages { + fmt.Printf("%s\n", phpQAStageStyle.Render("═══ "+strings.ToUpper(string(stage))+" STAGE ═══")) + + checks := phppkg.GetQAChecks(cwd, stage) + if len(checks) == 0 { + fmt.Printf(" %s\n\n", dimStyle.Render("No checks available")) + continue + } + + for _, checkName := range checks { + result := runQACheck(ctx, cwd, checkName, qaFix) + result.Stage = stage + results = append(results, result) + + icon := phpQAPassedStyle.Render("✓") + status := phpQAPassedStyle.Render("passed") + if !result.Passed { + icon = phpQAFailedStyle.Render("✗") + status = phpQAFailedStyle.Render("failed") + allPassed = false + } + + fmt.Printf(" %s %s %s %s\n", icon, result.Name, status, dimStyle.Render(result.Duration)) + } + fmt.Println() + } + + // Print summary + passedCount := 0 + var failedChecks []phppkg.QACheckResult + for _, r := range results { + if r.Passed { + passedCount++ + } else { + failedChecks = append(failedChecks, r) + } + } + + if allPassed { + fmt.Printf("%s All checks passed (%d/%d)\n", phpQAPassedStyle.Render("QA PASSED:"), passedCount, len(results)) + return nil + } + + fmt.Printf("%s Some checks failed (%d/%d passed)\n\n", phpQAFailedStyle.Render("QA FAILED:"), passedCount, len(results)) + + // Show what needs fixing + fmt.Printf("%s\n", dimStyle.Render("To fix:")) + for _, check := range failedChecks { + fixCmd := getQAFixCommand(check.Name, qaFix) + issue := check.Output + if issue == "" { + issue = "issues found" + } + fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("•"), check.Name+": "+issue) + if fixCmd != "" { + fmt.Printf(" %s %s\n", dimStyle.Render("→"), fixCmd) + } + } + + return fmt.Errorf("QA pipeline failed") + }, + } + + qaCmd.Flags().BoolVar(&qaQuick, "quick", false, "Only run quick checks") + qaCmd.Flags().BoolVar(&qaFull, "full", false, "Run all stages including slow checks") + qaCmd.Flags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible") + + parent.AddCommand(qaCmd) } func getQAFixCommand(checkName string, fixEnabled bool) string { @@ -677,142 +705,150 @@ func runQACheck(ctx context.Context, dir string, checkName string, fix bool) php return result } -func addPHPRectorCommand(parent *clir.Command) { - var ( - fix bool - diff bool - clearCache bool - ) +var ( + rectorFix bool + rectorDiff bool + rectorClearCache bool +) - rectorCmd := parent.NewSubCommand("rector", "Automated code refactoring") - rectorCmd.LongDescription("Run Rector for automated code improvements and PHP upgrades.\n\n" + - "Rector can automatically upgrade PHP syntax, improve code quality,\n" + - "and apply framework-specific refactorings.\n\n" + - "Examples:\n" + - " core php rector # Dry-run (show changes)\n" + - " core php rector --fix # Apply changes\n" + - " core php rector --diff # Show detailed diff") - - rectorCmd.BoolFlag("fix", "Apply changes (default is dry-run)", &fix) - rectorCmd.BoolFlag("diff", "Show detailed diff of changes", &diff) - rectorCmd.BoolFlag("clear-cache", "Clear Rector cache before running", &clearCache) - - rectorCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } - - // Check if Rector is available - if !phppkg.DetectRector(cwd) { - fmt.Printf("%s Rector not found\n\n", errorStyle.Render("Error:")) - fmt.Printf("%s composer require --dev rector/rector\n", dimStyle.Render("Install:")) - fmt.Printf("%s ./vendor/bin/rector init\n", dimStyle.Render("Setup:")) - return fmt.Errorf("rector not installed") - } - - action := "Analysing" - if fix { - action = "Refactoring" - } - fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action) - - ctx := context.Background() - - opts := phppkg.RectorOptions{ - Dir: cwd, - Fix: fix, - Diff: diff, - ClearCache: clearCache, - Output: os.Stdout, - } - - if err := phppkg.RunRector(ctx, opts); err != nil { - if fix { - return fmt.Errorf("rector failed: %w", err) +func addPHPRectorCommand(parent *cobra.Command) { + rectorCmd := &cobra.Command{ + Use: "rector", + Short: "Automated code refactoring", + Long: "Run Rector for automated code improvements and PHP upgrades.\n\n" + + "Rector can automatically upgrade PHP syntax, improve code quality,\n" + + "and apply framework-specific refactorings.\n\n" + + "Examples:\n" + + " core php rector # Dry-run (show changes)\n" + + " core php rector --fix # Apply changes\n" + + " core php rector --diff # Show detailed diff", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) } - // Dry-run returns non-zero if changes would be made - fmt.Printf("\n%s Changes suggested (use --fix to apply)\n", phpQAWarningStyle.Render("Info:")) - return nil - } - if fix { - fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:")) - } else { - fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:")) - } - return nil - }) + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } + + // Check if Rector is available + if !phppkg.DetectRector(cwd) { + fmt.Printf("%s Rector not found\n\n", errorStyle.Render("Error:")) + fmt.Printf("%s composer require --dev rector/rector\n", dimStyle.Render("Install:")) + fmt.Printf("%s ./vendor/bin/rector init\n", dimStyle.Render("Setup:")) + return fmt.Errorf("rector not installed") + } + + action := "Analysing" + if rectorFix { + action = "Refactoring" + } + fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action) + + ctx := context.Background() + + opts := phppkg.RectorOptions{ + Dir: cwd, + Fix: rectorFix, + Diff: rectorDiff, + ClearCache: rectorClearCache, + Output: os.Stdout, + } + + if err := phppkg.RunRector(ctx, opts); err != nil { + if rectorFix { + return fmt.Errorf("rector failed: %w", err) + } + // Dry-run returns non-zero if changes would be made + fmt.Printf("\n%s Changes suggested (use --fix to apply)\n", phpQAWarningStyle.Render("Info:")) + return nil + } + + if rectorFix { + fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:")) + } else { + fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:")) + } + return nil + }, + } + + rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, "Apply changes (default is dry-run)") + rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, "Show detailed diff of changes") + rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, "Clear Rector cache before running") + + parent.AddCommand(rectorCmd) } -func addPHPInfectionCommand(parent *clir.Command) { - var ( - minMSI int - minCoveredMSI int - threads int - filter string - onlyCovered bool - ) +var ( + infectionMinMSI int + infectionMinCoveredMSI int + infectionThreads int + infectionFilter string + infectionOnlyCovered bool +) - infectionCmd := parent.NewSubCommand("infection", "Mutation testing for test quality") - infectionCmd.LongDescription("Run Infection mutation testing to measure test suite quality.\n\n" + - "Mutation testing modifies your code and checks if tests catch\n" + - "the changes. High mutation score = high quality tests.\n\n" + - "Warning: This can be slow on large codebases.\n\n" + - "Examples:\n" + - " core php infection # Run mutation testing\n" + - " core php infection --min-msi=70 # Require 70% mutation score\n" + - " core php infection --filter=User # Only test User* files") +func addPHPInfectionCommand(parent *cobra.Command) { + infectionCmd := &cobra.Command{ + Use: "infection", + Short: "Mutation testing for test quality", + Long: "Run Infection mutation testing to measure test suite quality.\n\n" + + "Mutation testing modifies your code and checks if tests catch\n" + + "the changes. High mutation score = high quality tests.\n\n" + + "Warning: This can be slow on large codebases.\n\n" + + "Examples:\n" + + " core php infection # Run mutation testing\n" + + " core php infection --min-msi=70 # Require 70% mutation score\n" + + " core php infection --filter=User # Only test User* files", + RunE: func(cmd *cobra.Command, args []string) error { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } - infectionCmd.IntFlag("min-msi", "Minimum mutation score indicator (0-100, default: 50)", &minMSI) - infectionCmd.IntFlag("min-covered-msi", "Minimum covered mutation score (0-100, default: 70)", &minCoveredMSI) - infectionCmd.IntFlag("threads", "Number of parallel threads (default: 4)", &threads) - infectionCmd.StringFlag("filter", "Filter files by pattern", &filter) - infectionCmd.BoolFlag("only-covered", "Only mutate covered code", &onlyCovered) + if !phppkg.IsPHPProject(cwd) { + return fmt.Errorf("not a PHP project (missing composer.json)") + } - infectionCmd.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } + // Check if Infection is available + if !phppkg.DetectInfection(cwd) { + fmt.Printf("%s Infection not found\n\n", errorStyle.Render("Error:")) + fmt.Printf("%s composer require --dev infection/infection\n", dimStyle.Render("Install:")) + return fmt.Errorf("infection not installed") + } - if !phppkg.IsPHPProject(cwd) { - return fmt.Errorf("not a PHP project (missing composer.json)") - } + fmt.Printf("%s Running mutation testing\n", dimStyle.Render("Infection:")) + fmt.Printf("%s This may take a while...\n\n", dimStyle.Render("Note:")) - // Check if Infection is available - if !phppkg.DetectInfection(cwd) { - fmt.Printf("%s Infection not found\n\n", errorStyle.Render("Error:")) - fmt.Printf("%s composer require --dev infection/infection\n", dimStyle.Render("Install:")) - return fmt.Errorf("infection not installed") - } + ctx := context.Background() - fmt.Printf("%s Running mutation testing\n", dimStyle.Render("Infection:")) - fmt.Printf("%s This may take a while...\n\n", dimStyle.Render("Note:")) + opts := phppkg.InfectionOptions{ + Dir: cwd, + MinMSI: infectionMinMSI, + MinCoveredMSI: infectionMinCoveredMSI, + Threads: infectionThreads, + Filter: infectionFilter, + OnlyCovered: infectionOnlyCovered, + Output: os.Stdout, + } - ctx := context.Background() + if err := phppkg.RunInfection(ctx, opts); err != nil { + return fmt.Errorf("mutation testing failed: %w", err) + } - opts := phppkg.InfectionOptions{ - Dir: cwd, - MinMSI: minMSI, - MinCoveredMSI: minCoveredMSI, - Threads: threads, - Filter: filter, - OnlyCovered: onlyCovered, - Output: os.Stdout, - } + fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:")) + return nil + }, + } - if err := phppkg.RunInfection(ctx, opts); err != nil { - return fmt.Errorf("mutation testing failed: %w", err) - } + infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, "Minimum mutation score indicator (0-100, default: 50)") + infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, "Minimum covered mutation score (0-100, default: 70)") + infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, "Number of parallel threads (default: 4)") + infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", "Filter files by pattern") + infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, "Only mutate covered code") - fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:")) - return nil - }) + parent.AddCommand(infectionCmd) } func getSeverityStyle(severity string) lipgloss.Style { diff --git a/cmd/pkg/commands.go b/cmd/pkg/commands.go index 70abd6af..3a80338e 100644 --- a/cmd/pkg/commands.go +++ b/cmd/pkg/commands.go @@ -11,9 +11,9 @@ // .core/cache/ within the workspace directory. package pkg -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'pkg' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddPkgCommands(app) +func AddCommands(root *cobra.Command) { + AddPkgCommands(root) } diff --git a/cmd/pkg/pkg.go b/cmd/pkg/pkg.go index 52170cfc..2f576c75 100644 --- a/cmd/pkg/pkg.go +++ b/cmd/pkg/pkg.go @@ -3,7 +3,7 @@ package pkg import ( "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style and utility aliases @@ -17,16 +17,20 @@ var ( ) // AddPkgCommands adds the 'pkg' command and subcommands for package management. -func AddPkgCommands(parent *clir.Cli) { - pkgCmd := parent.NewSubCommand("pkg", "Package management for core-* repos") - pkgCmd.LongDescription("Manage host-uk/core-* packages and repositories.\n\n" + - "Commands:\n" + - " search Search GitHub for packages\n" + - " install Clone a package from GitHub\n" + - " list List installed packages\n" + - " update Update installed packages\n" + - " outdated Check for outdated packages") +func AddPkgCommands(root *cobra.Command) { + pkgCmd := &cobra.Command{ + Use: "pkg", + Short: "Package management for core-* repos", + Long: "Manage host-uk/core-* packages and repositories.\n\n" + + "Commands:\n" + + " search Search GitHub for packages\n" + + " install Clone a package from GitHub\n" + + " list List installed packages\n" + + " update Update installed packages\n" + + " outdated Check for outdated packages", + } + root.AddCommand(pkgCmd) addPkgSearchCommand(pkgCmd) addPkgInstallCommand(pkgCmd) addPkgListCommand(pkgCmd) diff --git a/cmd/pkg/pkg_install.go b/cmd/pkg/pkg_install.go index c4aa47f9..71c99068 100644 --- a/cmd/pkg/pkg_install.go +++ b/cmd/pkg/pkg_install.go @@ -8,31 +8,36 @@ import ( "strings" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +var ( + installTargetDir string + installAddToReg bool ) // addPkgInstallCommand adds the 'pkg install' command. -func addPkgInstallCommand(parent *clir.Command) { - var targetDir string - var addToRegistry bool +func addPkgInstallCommand(parent *cobra.Command) { + installCmd := &cobra.Command{ + Use: "install ", + Short: "Clone a package from GitHub", + Long: "Clones a repository from GitHub.\n\n" + + "Examples:\n" + + " core pkg install host-uk/core-php\n" + + " core pkg install host-uk/core-tenant --dir ./packages\n" + + " core pkg install host-uk/core-admin --add", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)") + } + return runPkgInstall(args[0], installTargetDir, installAddToReg) + }, + } - installCmd := parent.NewSubCommand("install", "Clone a package from GitHub") - installCmd.LongDescription("Clones a repository from GitHub.\n\n" + - "Examples:\n" + - " core pkg install host-uk/core-php\n" + - " core pkg install host-uk/core-tenant --dir ./packages\n" + - " core pkg install host-uk/core-admin --add") + installCmd.Flags().StringVar(&installTargetDir, "dir", "", "Target directory (default: ./packages or current dir)") + installCmd.Flags().BoolVar(&installAddToReg, "add", false, "Add to repos.yaml registry") - installCmd.StringFlag("dir", "Target directory (default: ./packages or current dir)", &targetDir) - installCmd.BoolFlag("add", "Add to repos.yaml registry", &addToRegistry) - - installCmd.Action(func() error { - args := installCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)") - } - return runPkgInstall(args[0], targetDir, addToRegistry) - }) + parent.AddCommand(installCmd) } func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error { diff --git a/cmd/pkg/pkg_manage.go b/cmd/pkg/pkg_manage.go index 96debefa..37e11c9b 100644 --- a/cmd/pkg/pkg_manage.go +++ b/cmd/pkg/pkg_manage.go @@ -8,20 +8,24 @@ import ( "strings" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // addPkgListCommand adds the 'pkg list' command. -func addPkgListCommand(parent *clir.Command) { - listCmd := parent.NewSubCommand("list", "List installed packages") - listCmd.LongDescription("Lists all packages in the current workspace.\n\n" + - "Reads from repos.yaml or scans for git repositories.\n\n" + - "Examples:\n" + - " core pkg list") +func addPkgListCommand(parent *cobra.Command) { + listCmd := &cobra.Command{ + Use: "list", + Short: "List installed packages", + Long: "Lists all packages in the current workspace.\n\n" + + "Reads from repos.yaml or scans for git repositories.\n\n" + + "Examples:\n" + + " core pkg list", + RunE: func(cmd *cobra.Command, args []string) error { + return runPkgList() + }, + } - listCmd.Action(func() error { - return runPkgList() - }) + parent.AddCommand(listCmd) } func runPkgList() error { @@ -89,25 +93,28 @@ func runPkgList() error { return nil } +var updateAll bool + // addPkgUpdateCommand adds the 'pkg update' command. -func addPkgUpdateCommand(parent *clir.Command) { - var all bool +func addPkgUpdateCommand(parent *cobra.Command) { + updateCmd := &cobra.Command{ + Use: "update [packages...]", + Short: "Update installed packages", + Long: "Pulls latest changes for installed packages.\n\n" + + "Examples:\n" + + " core pkg update core-php # Update specific package\n" + + " core pkg update --all # Update all packages", + RunE: func(cmd *cobra.Command, args []string) error { + if !updateAll && len(args) == 0 { + return fmt.Errorf("specify package name or use --all") + } + return runPkgUpdate(args, updateAll) + }, + } - updateCmd := parent.NewSubCommand("update", "Update installed packages") - updateCmd.LongDescription("Pulls latest changes for installed packages.\n\n" + - "Examples:\n" + - " core pkg update core-php # Update specific package\n" + - " core pkg update --all # Update all packages") + updateCmd.Flags().BoolVar(&updateAll, "all", false, "Update all packages") - updateCmd.BoolFlag("all", "Update all packages", &all) - - updateCmd.Action(func() error { - args := updateCmd.OtherArgs() - if !all && len(args) == 0 { - return fmt.Errorf("specify package name or use --all") - } - return runPkgUpdate(args, all) - }) + parent.AddCommand(updateCmd) } func runPkgUpdate(packages []string, all bool) error { @@ -177,15 +184,19 @@ func runPkgUpdate(packages []string, all bool) error { } // addPkgOutdatedCommand adds the 'pkg outdated' command. -func addPkgOutdatedCommand(parent *clir.Command) { - outdatedCmd := parent.NewSubCommand("outdated", "Check for outdated packages") - outdatedCmd.LongDescription("Checks which packages have unpulled commits.\n\n" + - "Examples:\n" + - " core pkg outdated") +func addPkgOutdatedCommand(parent *cobra.Command) { + outdatedCmd := &cobra.Command{ + Use: "outdated", + Short: "Check for outdated packages", + Long: "Checks which packages have unpulled commits.\n\n" + + "Examples:\n" + + " core pkg outdated", + RunE: func(cmd *cobra.Command, args []string) error { + return runPkgOutdated() + }, + } - outdatedCmd.Action(func() error { - return runPkgOutdated() - }) + parent.AddCommand(outdatedCmd) } func runPkgOutdated() error { diff --git a/cmd/pkg/pkg_search.go b/cmd/pkg/pkg_search.go index d20f2ffc..ec9f4e1f 100644 --- a/cmd/pkg/pkg_search.go +++ b/cmd/pkg/pkg_search.go @@ -12,44 +12,53 @@ import ( "github.com/host-uk/core/pkg/cache" "github.com/host-uk/core/pkg/repos" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +var ( + searchOrg string + searchPattern string + searchType string + searchLimit int + searchRefresh bool ) // addPkgSearchCommand adds the 'pkg search' command. -func addPkgSearchCommand(parent *clir.Command) { - var org string - var pattern string - var repoType string - var limit int - var refresh bool +func addPkgSearchCommand(parent *cobra.Command) { + searchCmd := &cobra.Command{ + Use: "search", + Short: "Search GitHub for packages", + Long: "Searches GitHub for repositories matching a pattern.\n" + + "Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" + + "Examples:\n" + + " core pkg search # List all host-uk repos\n" + + " core pkg search --pattern 'core-*' # Search for core-* repos\n" + + " core pkg search --org mycompany # Search different org\n" + + " core pkg search --refresh # Bypass cache", + RunE: func(cmd *cobra.Command, args []string) error { + org := searchOrg + pattern := searchPattern + limit := searchLimit + if org == "" { + org = "host-uk" + } + if pattern == "" { + pattern = "*" + } + if limit == 0 { + limit = 50 + } + return runPkgSearch(org, pattern, searchType, limit, searchRefresh) + }, + } - searchCmd := parent.NewSubCommand("search", "Search GitHub for packages") - searchCmd.LongDescription("Searches GitHub for repositories matching a pattern.\n" + - "Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" + - "Examples:\n" + - " core pkg search # List all host-uk repos\n" + - " core pkg search --pattern 'core-*' # Search for core-* repos\n" + - " core pkg search --org mycompany # Search different org\n" + - " core pkg search --refresh # Bypass cache") + searchCmd.Flags().StringVar(&searchOrg, "org", "", "GitHub organization (default: host-uk)") + searchCmd.Flags().StringVar(&searchPattern, "pattern", "", "Repo name pattern (* for wildcard)") + searchCmd.Flags().StringVar(&searchType, "type", "", "Filter by type in name (mod, services, plug, website)") + searchCmd.Flags().IntVar(&searchLimit, "limit", 0, "Max results (default 50)") + searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, "Bypass cache and fetch fresh data") - searchCmd.StringFlag("org", "GitHub organization (default: host-uk)", &org) - searchCmd.StringFlag("pattern", "Repo name pattern (* for wildcard)", &pattern) - searchCmd.StringFlag("type", "Filter by type in name (mod, services, plug, website)", &repoType) - searchCmd.IntFlag("limit", "Max results (default 50)", &limit) - searchCmd.BoolFlag("refresh", "Bypass cache and fetch fresh data", &refresh) - - searchCmd.Action(func() error { - if org == "" { - org = "host-uk" - } - if pattern == "" { - pattern = "*" - } - if limit == 0 { - limit = 50 - } - return runPkgSearch(org, pattern, repoType, limit, refresh) - }) + parent.AddCommand(searchCmd) } type ghRepo struct { diff --git a/cmd/sdk/commands.go b/cmd/sdk/commands.go index 4fdeae77..49e621d0 100644 --- a/cmd/sdk/commands.go +++ b/cmd/sdk/commands.go @@ -7,9 +7,9 @@ // Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk package sdk -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'sdk' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddSDKCommand(app) +func AddCommands(root *cobra.Command) { + root.AddCommand(sdkCmd) } diff --git a/cmd/sdk/sdk.go b/cmd/sdk/sdk.go index 017945fc..124bf193 100644 --- a/cmd/sdk/sdk.go +++ b/cmd/sdk/sdk.go @@ -7,7 +7,7 @@ import ( "github.com/charmbracelet/lipgloss" sdkpkg "github.com/host-uk/core/pkg/sdk" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) var ( @@ -27,30 +27,49 @@ var ( Foreground(lipgloss.Color("#6b7280")) ) -// AddSDKCommand adds the sdk command and its subcommands. -func AddSDKCommand(app *clir.Cli) { - sdkCmd := app.NewSubCommand("sdk", "SDK validation and API compatibility tools") - sdkCmd.LongDescription("Tools for validating OpenAPI specs and checking API compatibility.\n" + - "To generate SDKs, use: core build sdk\n\n" + - "Commands:\n" + - " diff Check for breaking API changes\n" + - " validate Validate OpenAPI spec syntax") +var sdkCmd = &cobra.Command{ + Use: "sdk", + Short: "SDK validation and API compatibility tools", + Long: `Tools for validating OpenAPI specs and checking API compatibility. +To generate SDKs, use: core build sdk - // sdk diff - diffCmd := sdkCmd.NewSubCommand("diff", "Check for breaking API changes") - var basePath, specPath string - diffCmd.StringFlag("base", "Base spec (version tag or file)", &basePath) - diffCmd.StringFlag("spec", "Current spec file", &specPath) - diffCmd.Action(func() error { - return runSDKDiff(basePath, specPath) - }) +Commands: + diff Check for breaking API changes + validate Validate OpenAPI spec syntax`, +} - // sdk validate - validateCmd := sdkCmd.NewSubCommand("validate", "Validate OpenAPI spec") - validateCmd.StringFlag("spec", "Path to OpenAPI spec file", &specPath) - validateCmd.Action(func() error { - return runSDKValidate(specPath) - }) +var diffBasePath string +var diffSpecPath string + +var sdkDiffCmd = &cobra.Command{ + Use: "diff", + Short: "Check for breaking API changes", + RunE: func(cmd *cobra.Command, args []string) error { + return runSDKDiff(diffBasePath, diffSpecPath) + }, +} + +var validateSpecPath string + +var sdkValidateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate OpenAPI spec", + RunE: func(cmd *cobra.Command, args []string) error { + return runSDKValidate(validateSpecPath) + }, +} + +func init() { + // sdk diff flags + sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", "Base spec (version tag or file)") + sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", "Current spec file") + + // sdk validate flags + sdkValidateCmd.Flags().StringVar(&validateSpecPath, "spec", "", "Path to OpenAPI spec file") + + // Add subcommands + sdkCmd.AddCommand(sdkDiffCmd) + sdkCmd.AddCommand(sdkValidateCmd) } func runSDKDiff(basePath, specPath string) error { diff --git a/cmd/setup/commands.go b/cmd/setup/commands.go index 2548dbbd..da919b04 100644 --- a/cmd/setup/commands.go +++ b/cmd/setup/commands.go @@ -23,9 +23,9 @@ // Uses gh CLI with HTTPS when authenticated, falls back to SSH. package setup -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'setup' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddSetupCommand(app) +func AddCommands(root *cobra.Command) { + AddSetupCommand(root) } diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 51e159a6..69e5f049 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -3,7 +3,7 @@ package setup import ( "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared package @@ -21,34 +21,46 @@ const ( devopsReposYaml = "repos.yaml" ) -// AddSetupCommand adds the 'setup' command to the given parent command. -func AddSetupCommand(parent *clir.Cli) { - var registryPath string - var only string - var dryRun bool - var all bool - var name string - var build bool +// Setup command flags +var ( + registryPath string + only string + dryRun bool + all bool + name string + build bool +) - setupCmd := parent.NewSubCommand("setup", "Bootstrap workspace or clone packages from registry") - setupCmd.LongDescription("Sets up a development workspace.\n\n" + - "REGISTRY MODE (repos.yaml exists):\n" + - " Clones all repositories defined in repos.yaml into packages/.\n" + - " Skips repos that already exist. Use --only to filter by type.\n\n" + - "BOOTSTRAP MODE (no repos.yaml):\n" + - " 1. Clones core-devops to set up the workspace\n" + - " 2. Presents an interactive wizard to select packages\n" + - " 3. Clones selected packages\n\n" + - "Use --all to skip the wizard and clone everything.") +var setupCmd = &cobra.Command{ + Use: "setup", + Short: "Bootstrap workspace or clone packages from registry", + Long: `Sets up a development workspace. - setupCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", ®istryPath) - setupCmd.StringFlag("only", "Only clone repos of these types (comma-separated: foundation,module,product)", &only) - setupCmd.BoolFlag("dry-run", "Show what would be cloned without cloning", &dryRun) - setupCmd.BoolFlag("all", "Skip wizard, clone all packages (non-interactive)", &all) - setupCmd.StringFlag("name", "Project directory name for bootstrap mode", &name) - setupCmd.BoolFlag("build", "Run build after cloning", &build) +REGISTRY MODE (repos.yaml exists): + Clones all repositories defined in repos.yaml into packages/. + Skips repos that already exist. Use --only to filter by type. - setupCmd.Action(func() error { +BOOTSTRAP MODE (no repos.yaml): + 1. Clones core-devops to set up the workspace + 2. Presents an interactive wizard to select packages + 3. Clones selected packages + +Use --all to skip the wizard and clone everything.`, + RunE: func(cmd *cobra.Command, args []string) error { return runSetupOrchestrator(registryPath, only, dryRun, all, name, build) - }) + }, +} + +func init() { + setupCmd.Flags().StringVar(®istryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") + setupCmd.Flags().StringVar(&only, "only", "", "Only clone repos of these types (comma-separated: foundation,module,product)") + setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be cloned without cloning") + setupCmd.Flags().BoolVar(&all, "all", false, "Skip wizard, clone all packages (non-interactive)") + setupCmd.Flags().StringVar(&name, "name", "", "Project directory name for bootstrap mode") + setupCmd.Flags().BoolVar(&build, "build", false, "Run build after cloning") +} + +// AddSetupCommand adds the 'setup' command to the given parent command. +func AddSetupCommand(root *cobra.Command) { + root.AddCommand(setupCmd) } diff --git a/cmd/test/commands.go b/cmd/test/commands.go index cd0c8f05..c52c47c1 100644 --- a/cmd/test/commands.go +++ b/cmd/test/commands.go @@ -11,9 +11,9 @@ // Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json package testcmd -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'test' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddTestCommand(app) +func AddCommands(root *cobra.Command) { + root.AddCommand(testCmd) } diff --git a/cmd/test/test.go b/cmd/test/test.go index 7350103c..89e51055 100644 --- a/cmd/test/test.go +++ b/cmd/test/test.go @@ -6,7 +6,7 @@ package testcmd import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared @@ -30,38 +30,44 @@ var ( Foreground(lipgloss.Color("#ef4444")) // red-500 ) -// AddTestCommand adds the 'test' command to the given parent command. -func AddTestCommand(parent *clir.Cli) { - var verbose bool - var coverage bool - var short bool - var pkg string - var run string - var race bool - var json bool +// Flag variables for test command +var ( + testVerbose bool + testCoverage bool + testShort bool + testPkg string + testRun string + testRace bool + testJSON bool +) - testCmd := parent.NewSubCommand("test", "Run tests with coverage") - testCmd.LongDescription("Runs Go tests with coverage reporting.\n\n" + - "Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.\n\n" + - "Examples:\n" + - " core test # Run all tests with coverage summary\n" + - " core test --verbose # Show test output as it runs\n" + - " core test --coverage # Show detailed per-package coverage\n" + - " core test --pkg ./pkg/... # Test specific packages\n" + - " core test --run TestName # Run specific test by name\n" + - " core test --short # Skip long-running tests\n" + - " core test --race # Enable race detector\n" + - " core test --json # Output JSON for CI/agents") +var testCmd = &cobra.Command{ + Use: "test", + Short: "Run tests with coverage", + Long: `Runs Go tests with coverage reporting. - testCmd.BoolFlag("verbose", "Show test output as it runs (-v)", &verbose) - testCmd.BoolFlag("coverage", "Show detailed per-package coverage", &coverage) - testCmd.BoolFlag("short", "Skip long-running tests (-short)", &short) - testCmd.StringFlag("pkg", "Package pattern to test (default: ./...)", &pkg) - testCmd.StringFlag("run", "Run only tests matching this regex (-run)", &run) - testCmd.BoolFlag("race", "Enable race detector (-race)", &race) - testCmd.BoolFlag("json", "Output JSON for CI/agents", &json) +Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS. - testCmd.Action(func() error { - return runTest(verbose, coverage, short, pkg, run, race, json) - }) +Examples: + core test # Run all tests with coverage summary + core test --verbose # Show test output as it runs + core test --coverage # Show detailed per-package coverage + core test --pkg ./pkg/... # Test specific packages + core test --run TestName # Run specific test by name + core test --short # Skip long-running tests + core test --race # Enable race detector + core test --json # Output JSON for CI/agents`, + RunE: func(cmd *cobra.Command, args []string) error { + return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON) + }, +} + +func init() { + testCmd.Flags().BoolVar(&testVerbose, "verbose", false, "Show test output as it runs (-v)") + testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage") + testCmd.Flags().BoolVar(&testShort, "short", false, "Skip long-running tests (-short)") + testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package pattern to test (default: ./...)") + testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching this regex (-run)") + testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector (-race)") + testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON for CI/agents") } diff --git a/cmd/vm/commands.go b/cmd/vm/commands.go index b18e9ab7..2052bbf2 100644 --- a/cmd/vm/commands.go +++ b/cmd/vm/commands.go @@ -12,9 +12,9 @@ // Templates are built from YAML definitions and can include variables. package vm -import "github.com/leaanthony/clir" +import "github.com/spf13/cobra" // AddCommands registers the 'vm' command and all subcommands. -func AddCommands(app *clir.Cli) { - AddVMCommands(app) +func AddCommands(root *cobra.Command) { + AddVMCommands(root) } diff --git a/cmd/vm/container.go b/cmd/vm/container.go index 314b2534..e716fca0 100644 --- a/cmd/vm/container.go +++ b/cmd/vm/container.go @@ -10,65 +10,68 @@ import ( "time" "github.com/host-uk/core/pkg/container" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" +) + +var ( + runName string + runDetach bool + runMemory int + runCPUs int + runSSHPort int + runTemplateName string + runVarFlags []string ) // addVMRunCommand adds the 'run' command under vm. -func addVMRunCommand(parent *clir.Command) { - var ( - name string - detach bool - memory int - cpus int - sshPort int - templateName string - varFlags []string - ) +func addVMRunCommand(parent *cobra.Command) { + runCmd := &cobra.Command{ + Use: "run [image]", + Short: "Run a LinuxKit image or template", + Long: "Runs a LinuxKit image as a VM using the available hypervisor.\n\n" + + "Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" + + "You can also run from a template using --template, which will build and run\n" + + "the image automatically. Use --var to set template variables.\n\n" + + "Examples:\n" + + " core vm run image.iso\n" + + " core vm run -d image.qcow2\n" + + " core vm run --name myvm --memory 2048 --cpus 4 image.iso\n" + + " core vm run --template core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" + + " core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com", + RunE: func(cmd *cobra.Command, args []string) error { + opts := container.RunOptions{ + Name: runName, + Detach: runDetach, + Memory: runMemory, + CPUs: runCPUs, + SSHPort: runSSHPort, + } - runCmd := parent.NewSubCommand("run", "Run a LinuxKit image or template") - runCmd.LongDescription("Runs a LinuxKit image as a VM using the available hypervisor.\n\n" + - "Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" + - "You can also run from a template using --template, which will build and run\n" + - "the image automatically. Use --var to set template variables.\n\n" + - "Examples:\n" + - " core vm run image.iso\n" + - " core vm run -d image.qcow2\n" + - " core vm run --name myvm --memory 2048 --cpus 4 image.iso\n" + - " core vm run --template core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" + - " core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com") + // If template is specified, build and run from template + if runTemplateName != "" { + vars := ParseVarFlags(runVarFlags) + return RunFromTemplate(runTemplateName, vars, opts) + } - runCmd.StringFlag("name", "Name for the container", &name) - runCmd.BoolFlag("d", "Run in detached mode (background)", &detach) - runCmd.IntFlag("memory", "Memory in MB (default: 1024)", &memory) - runCmd.IntFlag("cpus", "Number of CPUs (default: 1)", &cpus) - runCmd.IntFlag("ssh-port", "SSH port for exec commands (default: 2222)", &sshPort) - runCmd.StringFlag("template", "Run from a LinuxKit template (build + run)", &templateName) - runCmd.StringsFlag("var", "Template variable in KEY=VALUE format (can be repeated)", &varFlags) + // Otherwise, require an image path + if len(args) == 0 { + return fmt.Errorf("image path is required (or use --template)") + } + image := args[0] - runCmd.Action(func() error { - opts := container.RunOptions{ - Name: name, - Detach: detach, - Memory: memory, - CPUs: cpus, - SSHPort: sshPort, - } + return runContainer(image, runName, runDetach, runMemory, runCPUs, runSSHPort) + }, + } - // If template is specified, build and run from template - if templateName != "" { - vars := ParseVarFlags(varFlags) - return RunFromTemplate(templateName, vars, opts) - } + runCmd.Flags().StringVar(&runName, "name", "", "Name for the container") + runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, "Run in detached mode (background)") + runCmd.Flags().IntVar(&runMemory, "memory", 0, "Memory in MB (default: 1024)") + runCmd.Flags().IntVar(&runCPUs, "cpus", 0, "Number of CPUs (default: 1)") + runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, "SSH port for exec commands (default: 2222)") + runCmd.Flags().StringVar(&runTemplateName, "template", "", "Run from a LinuxKit template (build + run)") + runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, "Template variable in KEY=VALUE format (can be repeated)") - // Otherwise, require an image path - args := runCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("image path is required (or use --template)") - } - image := args[0] - - return runContainer(image, name, detach, memory, cpus, sshPort) - }) + parent.AddCommand(runCmd) } func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error { @@ -111,21 +114,25 @@ func runContainer(image, name string, detach bool, memory, cpus, sshPort int) er return nil } +var psAll bool + // addVMPsCommand adds the 'ps' command under vm. -func addVMPsCommand(parent *clir.Command) { - var all bool +func addVMPsCommand(parent *cobra.Command) { + psCmd := &cobra.Command{ + Use: "ps", + Short: "List running VMs", + Long: "Lists all VMs. By default, only shows running VMs.\n\n" + + "Examples:\n" + + " core vm ps\n" + + " core vm ps -a", + RunE: func(cmd *cobra.Command, args []string) error { + return listContainers(psAll) + }, + } - psCmd := parent.NewSubCommand("ps", "List running VMs") - psCmd.LongDescription("Lists all VMs. By default, only shows running VMs.\n\n" + - "Examples:\n" + - " core vm ps\n" + - " core vm ps -a") + psCmd.Flags().BoolVarP(&psAll, "all", "a", false, "Show all containers (including stopped)") - psCmd.BoolFlag("a", "Show all containers (including stopped)", &all) - - psCmd.Action(func() error { - return listContainers(all) - }) + parent.AddCommand(psCmd) } func listContainers(all bool) error { @@ -207,20 +214,23 @@ func formatDuration(d time.Duration) string { } // addVMStopCommand adds the 'stop' command under vm. -func addVMStopCommand(parent *clir.Command) { - stopCmd := parent.NewSubCommand("stop", "Stop a running VM") - stopCmd.LongDescription("Stops a running VM by ID.\n\n" + - "Examples:\n" + - " core vm stop abc12345\n" + - " core vm stop abc1") +func addVMStopCommand(parent *cobra.Command) { + stopCmd := &cobra.Command{ + Use: "stop ", + Short: "Stop a running VM", + Long: "Stops a running VM by ID.\n\n" + + "Examples:\n" + + " core vm stop abc12345\n" + + " core vm stop abc1", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("container ID is required") + } + return stopContainer(args[0]) + }, + } - stopCmd.Action(func() error { - args := stopCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("container ID is required") - } - return stopContainer(args[0]) - }) + parent.AddCommand(stopCmd) } func stopContainer(id string) error { @@ -271,25 +281,28 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s } } +var logsFollow bool + // addVMLogsCommand adds the 'logs' command under vm. -func addVMLogsCommand(parent *clir.Command) { - var follow bool +func addVMLogsCommand(parent *cobra.Command) { + logsCmd := &cobra.Command{ + Use: "logs ", + Short: "View VM logs", + Long: "View logs from a VM.\n\n" + + "Examples:\n" + + " core vm logs abc12345\n" + + " core vm logs -f abc1", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("container ID is required") + } + return viewLogs(args[0], logsFollow) + }, + } - logsCmd := parent.NewSubCommand("logs", "View VM logs") - logsCmd.LongDescription("View logs from a VM.\n\n" + - "Examples:\n" + - " core vm logs abc12345\n" + - " core vm logs -f abc1") + logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, "Follow log output") - logsCmd.BoolFlag("f", "Follow log output", &follow) - - logsCmd.Action(func() error { - args := logsCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("container ID is required") - } - return viewLogs(args[0], follow) - }) + parent.AddCommand(logsCmd) } func viewLogs(id string, follow bool) error { @@ -315,20 +328,23 @@ func viewLogs(id string, follow bool) error { } // addVMExecCommand adds the 'exec' command under vm. -func addVMExecCommand(parent *clir.Command) { - execCmd := parent.NewSubCommand("exec", "Execute a command in a VM") - execCmd.LongDescription("Execute a command inside a running VM via SSH.\n\n" + - "Examples:\n" + - " core vm exec abc12345 ls -la\n" + - " core vm exec abc1 /bin/sh") +func addVMExecCommand(parent *cobra.Command) { + execCmd := &cobra.Command{ + Use: "exec [args...]", + Short: "Execute a command in a VM", + Long: "Execute a command inside a running VM via SSH.\n\n" + + "Examples:\n" + + " core vm exec abc12345 ls -la\n" + + " core vm exec abc1 /bin/sh", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("container ID and command are required") + } + return execInContainer(args[0], args[1:]) + }, + } - execCmd.Action(func() error { - args := execCmd.OtherArgs() - if len(args) < 2 { - return fmt.Errorf("container ID and command are required") - } - return execInContainer(args[0], args[1:]) - }) + parent.AddCommand(execCmd) } func execInContainer(id string, cmd []string) error { diff --git a/cmd/vm/templates.go b/cmd/vm/templates.go index 140dcd96..ed544d3c 100644 --- a/cmd/vm/templates.go +++ b/cmd/vm/templates.go @@ -10,63 +10,72 @@ import ( "text/tabwriter" "github.com/host-uk/core/pkg/container" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // addVMTemplatesCommand adds the 'templates' command under vm. -func addVMTemplatesCommand(parent *clir.Command) { - templatesCmd := parent.NewSubCommand("templates", "Manage LinuxKit templates") - templatesCmd.LongDescription("Manage LinuxKit YAML templates for building VMs.\n\n" + - "Templates provide pre-configured LinuxKit configurations for common use cases.\n" + - "They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" + - "Examples:\n" + - " core vm templates # List available templates\n" + - " core vm templates show core-dev # Show template content\n" + - " core vm templates vars server-php # Show template variables") - - // Default action: list templates - templatesCmd.Action(func() error { - return listTemplates() - }) +func addVMTemplatesCommand(parent *cobra.Command) { + templatesCmd := &cobra.Command{ + Use: "templates", + Short: "Manage LinuxKit templates", + Long: "Manage LinuxKit YAML templates for building VMs.\n\n" + + "Templates provide pre-configured LinuxKit configurations for common use cases.\n" + + "They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" + + "Examples:\n" + + " core vm templates # List available templates\n" + + " core vm templates show core-dev # Show template content\n" + + " core vm templates vars server-php # Show template variables", + RunE: func(cmd *cobra.Command, args []string) error { + return listTemplates() + }, + } // Add subcommands addTemplatesShowCommand(templatesCmd) addTemplatesVarsCommand(templatesCmd) + + parent.AddCommand(templatesCmd) } // addTemplatesShowCommand adds the 'templates show' subcommand. -func addTemplatesShowCommand(parent *clir.Command) { - showCmd := parent.NewSubCommand("show", "Display template content") - showCmd.LongDescription("Display the content of a LinuxKit template.\n\n" + - "Examples:\n" + - " core templates show core-dev\n" + - " core templates show server-php") +func addTemplatesShowCommand(parent *cobra.Command) { + showCmd := &cobra.Command{ + Use: "show ", + Short: "Display template content", + Long: "Display the content of a LinuxKit template.\n\n" + + "Examples:\n" + + " core templates show core-dev\n" + + " core templates show server-php", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("template name is required") + } + return showTemplate(args[0]) + }, + } - showCmd.Action(func() error { - args := showCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("template name is required") - } - return showTemplate(args[0]) - }) + parent.AddCommand(showCmd) } // addTemplatesVarsCommand adds the 'templates vars' subcommand. -func addTemplatesVarsCommand(parent *clir.Command) { - varsCmd := parent.NewSubCommand("vars", "Show template variables") - varsCmd.LongDescription("Display all variables used in a template.\n\n" + - "Shows required variables (no default) and optional variables (with defaults).\n\n" + - "Examples:\n" + - " core templates vars core-dev\n" + - " core templates vars server-php") +func addTemplatesVarsCommand(parent *cobra.Command) { + varsCmd := &cobra.Command{ + Use: "vars ", + Short: "Show template variables", + Long: "Display all variables used in a template.\n\n" + + "Shows required variables (no default) and optional variables (with defaults).\n\n" + + "Examples:\n" + + " core templates vars core-dev\n" + + " core templates vars server-php", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("template name is required") + } + return showTemplateVars(args[0]) + }, + } - varsCmd.Action(func() error { - args := varsCmd.OtherArgs() - if len(args) == 0 { - return fmt.Errorf("template name is required") - } - return showTemplateVars(args[0]) - }) + parent.AddCommand(varsCmd) } func listTemplates() error { diff --git a/cmd/vm/vm.go b/cmd/vm/vm.go index 0a0f39ad..1fba55af 100644 --- a/cmd/vm/vm.go +++ b/cmd/vm/vm.go @@ -4,7 +4,7 @@ package vm import ( "github.com/charmbracelet/lipgloss" "github.com/host-uk/core/cmd/shared" - "github.com/leaanthony/clir" + "github.com/spf13/cobra" ) // Style aliases from shared @@ -22,19 +22,23 @@ var ( ) // AddVMCommands adds container-related commands under 'vm' to the CLI. -func AddVMCommands(parent *clir.Cli) { - vmCmd := parent.NewSubCommand("vm", "LinuxKit VM management") - vmCmd.LongDescription("Manage LinuxKit virtual machines.\n\n" + - "LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" + - "They run using qemu or hyperkit depending on your system.\n\n" + - "Commands:\n" + - " run Run a VM from image or template\n" + - " ps List running VMs\n" + - " stop Stop a running VM\n" + - " logs View VM logs\n" + - " exec Execute command in VM\n" + - " templates Manage LinuxKit templates") +func AddVMCommands(root *cobra.Command) { + vmCmd := &cobra.Command{ + Use: "vm", + Short: "LinuxKit VM management", + Long: "Manage LinuxKit virtual machines.\n\n" + + "LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" + + "They run using qemu or hyperkit depending on your system.\n\n" + + "Commands:\n" + + " run Run a VM from image or template\n" + + " ps List running VMs\n" + + " stop Stop a running VM\n" + + " logs View VM logs\n" + + " exec Execute command in VM\n" + + " templates Manage LinuxKit templates", + } + root.AddCommand(vmCmd) addVMRunCommand(vmCmd) addVMPsCommand(vmCmd) addVMStopCommand(vmCmd)