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 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 00:47:54 +00:00
parent 6d8edeb89c
commit a2bad1c0aa
58 changed files with 3258 additions and 2719 deletions

View file

@ -5,7 +5,7 @@ package ai
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared package // Style aliases from shared package
@ -53,7 +53,7 @@ var (
) )
// AddAgenticCommands adds the agentic task management commands to the ai command. // 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 // Task listing and viewing
addTasksCommand(parent) addTasksCommand(parent)
addTaskCommand(parent) addTaskCommand(parent)

View file

@ -12,47 +12,44 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addTaskCommitCommand(parent *clir.Command) { // task:commit command flags
var message string var (
var scope string taskCommitMessage string
var push bool taskCommitScope string
taskCommitPush bool
)
cmd := parent.NewSubCommand("task:commit", "Auto-commit changes with task reference") // task:pr command flags
cmd.LongDescription("Creates a git commit with a task reference and co-author attribution.\n\n" + var (
"Commit message format:\n" + taskPRTitle string
" feat(scope): description\n" + taskPRDraft bool
"\n" + taskPRLabels string
" Task: #123\n" + taskPRBase string
" Co-Authored-By: Claude <noreply@anthropic.com>\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")
cmd.StringFlag("message", "Commit message (without task reference)", &message) var taskCommitCmd = &cobra.Command{
cmd.StringFlag("m", "Commit message (short form)", &message) Use: "task:commit [task-id]",
cmd.StringFlag("scope", "Scope for the commit type (e.g., auth, api, ui)", &scope) Short: "Auto-commit changes with task reference",
cmd.BoolFlag("push", "Push changes after committing", &push) Long: `Creates a git commit with a task reference and co-author attribution.
cmd.Action(func() error { Commit message format:
// Find task ID from args feat(scope): description
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
}
}
if taskID == "" { Task: #123
return fmt.Errorf("task ID required") Co-Authored-By: Claude <noreply@anthropic.com>
}
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)") return fmt.Errorf("commit message required (--message or -m)")
} }
@ -75,10 +72,10 @@ func addTaskCommitCommand(parent *clir.Command) {
// Build commit message with optional scope // Build commit message with optional scope
commitType := inferCommitType(task.Labels) commitType := inferCommitType(task.Labels)
var fullMessage string var fullMessage string
if scope != "" { if taskCommitScope != "" {
fullMessage = fmt.Sprintf("%s(%s): %s", commitType, scope, message) fullMessage = fmt.Sprintf("%s(%s): %s", commitType, taskCommitScope, taskCommitMessage)
} else { } else {
fullMessage = fmt.Sprintf("%s: %s", commitType, message) fullMessage = fmt.Sprintf("%s: %s", commitType, taskCommitMessage)
} }
// Get current directory // Get current directory
@ -107,7 +104,7 @@ func addTaskCommitCommand(parent *clir.Command) {
fmt.Printf("%s Committed: %s\n", successStyle.Render(">>"), fullMessage) fmt.Printf("%s Committed: %s\n", successStyle.Render(">>"), fullMessage)
// Push if requested // Push if requested
if push { if taskCommitPush {
fmt.Printf("%s Pushing changes...\n", dimStyle.Render(">>")) fmt.Printf("%s Pushing changes...\n", dimStyle.Render(">>"))
if err := agentic.PushChanges(ctx, cwd); err != nil { if err := agentic.PushChanges(ctx, cwd); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("failed to push: %w", err)
@ -116,43 +113,24 @@ func addTaskCommitCommand(parent *clir.Command) {
} }
return nil return nil
}) },
} }
func addTaskPRCommand(parent *clir.Command) { var taskPRCmd = &cobra.Command{
var title string Use: "task:pr [task-id]",
var draft bool Short: "Create a pull request for a task",
var labels string Long: `Creates a GitHub pull request linked to a task.
var base string
cmd := parent.NewSubCommand("task:pr", "Create a pull request for a task") Requires the GitHub CLI (gh) to be installed and authenticated.
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")
cmd.StringFlag("title", "PR title (defaults to task title)", &title) Examples:
cmd.BoolFlag("draft", "Create as draft PR", &draft) core ai task:pr abc123
cmd.StringFlag("labels", "Labels to add (comma-separated)", &labels) core ai task:pr abc123 --title 'Add authentication feature'
cmd.StringFlag("base", "Base branch (defaults to main)", &base) core ai task:pr abc123 --draft --labels 'enhancement,needs-review'
core ai task:pr abc123 --base develop`,
cmd.Action(func() error { Args: cobra.ExactArgs(1),
// Find task ID from args RunE: func(cmd *cobra.Command, args []string) error {
args := os.Args taskID := args[0]
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")
}
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
@ -197,13 +175,13 @@ func addTaskPRCommand(parent *clir.Command) {
// Build PR options // Build PR options
opts := agentic.PROptions{ opts := agentic.PROptions{
Title: title, Title: taskPRTitle,
Draft: draft, Draft: taskPRDraft,
Base: base, Base: taskPRBase,
} }
if labels != "" { if taskPRLabels != "" {
opts.Labels = strings.Split(labels, ",") opts.Labels = strings.Split(taskPRLabels, ",")
} }
// Create PR // Create PR
@ -217,7 +195,28 @@ func addTaskPRCommand(parent *clir.Command) {
fmt.Printf(" URL: %s\n", prURL) fmt.Printf(" URL: %s\n", prURL)
return nil 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. // inferCommitType infers the commit type from task labels.

View file

@ -11,34 +11,41 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addTasksCommand(parent *clir.Command) { // tasks command flags
var status string var (
var priority string tasksStatus string
var labels string tasksPriority string
var limit int tasksLabels string
var project string tasksLimit int
tasksProject string
)
cmd := parent.NewSubCommand("tasks", "List available tasks from core-agentic") // task command flags
cmd.LongDescription("Lists tasks from the core-agentic service.\n\n" + var (
"Configuration is loaded from:\n" + taskAutoSelect bool
" 1. Environment variables (AGENTIC_TOKEN, AGENTIC_BASE_URL)\n" + taskClaim bool
" 2. .env file in current directory\n" + taskShowContext bool
" 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")
cmd.StringFlag("status", "Filter by status (pending, in_progress, completed, blocked)", &status) var tasksCmd = &cobra.Command{
cmd.StringFlag("priority", "Filter by priority (critical, high, medium, low)", &priority) Use: "tasks",
cmd.StringFlag("labels", "Filter by labels (comma-separated)", &labels) Short: "List available tasks from core-agentic",
cmd.IntFlag("limit", "Max number of tasks to return (default 20)", &limit) Long: `Lists tasks from the core-agentic service.
cmd.StringFlag("project", "Filter by project", &project)
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 { if limit == 0 {
limit = 20 limit = 20
} }
@ -52,17 +59,17 @@ func addTasksCommand(parent *clir.Command) {
opts := agentic.ListOptions{ opts := agentic.ListOptions{
Limit: limit, Limit: limit,
Project: project, Project: tasksProject,
} }
if status != "" { if tasksStatus != "" {
opts.Status = agentic.TaskStatus(status) opts.Status = agentic.TaskStatus(tasksStatus)
} }
if priority != "" { if tasksPriority != "" {
opts.Priority = agentic.TaskPriority(priority) opts.Priority = agentic.TaskPriority(tasksPriority)
} }
if labels != "" { if tasksLabels != "" {
opts.Labels = strings.Split(labels, ",") opts.Labels = strings.Split(tasksLabels, ",")
} }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -80,27 +87,20 @@ func addTasksCommand(parent *clir.Command) {
printTaskList(tasks) printTaskList(tasks)
return nil return nil
}) },
} }
func addTaskCommand(parent *clir.Command) { var taskCmd = &cobra.Command{
var autoSelect bool Use: "task [task-id]",
var claim bool Short: "Show task details or auto-select a task",
var showContext bool 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") Examples:
cmd.LongDescription("Shows details of a specific task or auto-selects the highest priority task.\n\n" + core ai task abc123 # Show task details
"Examples:\n" + core ai task abc123 --claim # Show and claim the task
" core ai task abc123 # Show task details\n" + core ai task abc123 --context # Show task with gathered context
" core ai task abc123 --claim # Show and claim the task\n" + core ai task --auto # Auto-select highest priority pending task`,
" core ai task abc123 --context # Show task with gathered context\n" + RunE: func(cmd *cobra.Command, args []string) error {
" 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 {
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("failed to load config: %w", err)
@ -113,19 +113,13 @@ func addTaskCommand(parent *clir.Command) {
var task *agentic.Task var task *agentic.Task
// Get the task ID from remaining args // Get the task ID from args
args := os.Args
var taskID string var taskID string
if len(args) > 0 {
// Find the task ID in args (after "task" subcommand) taskID = args[0]
for i, arg := range args {
if arg == "task" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
taskID = args[i+1]
break
}
} }
if autoSelect { if taskAutoSelect {
// Auto-select: find highest priority pending task // Auto-select: find highest priority pending task
tasks, err := client.ListTasks(ctx, agentic.ListOptions{ tasks, err := client.ListTasks(ctx, agentic.ListOptions{
Status: agentic.StatusPending, Status: agentic.StatusPending,
@ -153,7 +147,7 @@ func addTaskCommand(parent *clir.Command) {
}) })
task = &tasks[0] task = &tasks[0]
claim = true // Auto-select implies claiming taskClaim = true // Auto-select implies claiming
} else { } else {
if taskID == "" { if taskID == "" {
return fmt.Errorf("task ID required (or use --auto)") return fmt.Errorf("task ID required (or use --auto)")
@ -166,7 +160,7 @@ func addTaskCommand(parent *clir.Command) {
} }
// Show context if requested // Show context if requested
if showContext { if taskShowContext {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
taskCtx, err := agentic.BuildTaskContext(task, cwd) taskCtx, err := agentic.BuildTaskContext(task, cwd)
if err != nil { if err != nil {
@ -178,7 +172,7 @@ func addTaskCommand(parent *clir.Command) {
printTaskDetails(task) printTaskDetails(task)
} }
if claim && task.Status == agentic.StatusPending { if taskClaim && task.Status == agentic.StatusPending {
fmt.Println() fmt.Println()
fmt.Printf("%s Claiming task...\n", dimStyle.Render(">>")) fmt.Printf("%s Claiming task...\n", dimStyle.Render(">>"))
@ -192,7 +186,29 @@ func addTaskCommand(parent *clir.Command) {
} }
return nil 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) { func printTaskList(tasks []agentic.Task) {

View file

@ -5,45 +5,39 @@ package ai
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strings"
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addTaskUpdateCommand(parent *clir.Command) { // task:update command flags
var status string var (
var progress int taskUpdateStatus string
var notes string taskUpdateProgress int
taskUpdateNotes string
)
cmd := parent.NewSubCommand("task:update", "Update task status or progress") // task:complete command flags
cmd.LongDescription("Updates a task's status, progress, or adds notes.\n\n" + var (
"Examples:\n" + taskCompleteOutput string
" core ai task:update abc123 --status in_progress\n" + taskCompleteFailed bool
" core ai task:update abc123 --progress 50 --notes 'Halfway done'") taskCompleteErrorMsg string
)
cmd.StringFlag("status", "New status (pending, in_progress, completed, blocked)", &status) var taskUpdateCmd = &cobra.Command{
cmd.IntFlag("progress", "Progress percentage (0-100)", &progress) Use: "task:update [task-id]",
cmd.StringFlag("notes", "Notes about the update", &notes) Short: "Update task status or progress",
Long: `Updates a task's status, progress, or adds notes.
cmd.Action(func() error { Examples:
// Find task ID from args core ai task:update abc123 --status in_progress
args := os.Args core ai task:update abc123 --progress 50 --notes 'Halfway done'`,
var taskID string Args: cobra.ExactArgs(1),
for i, arg := range args { RunE: func(cmd *cobra.Command, args []string) error {
if arg == "task:update" && i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { taskID := args[0]
taskID = args[i+1]
break
}
}
if taskID == "" { if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
return fmt.Errorf("task ID required")
}
if status == "" && progress == 0 && notes == "" {
return fmt.Errorf("at least one of --status, --progress, or --notes required") return fmt.Errorf("at least one of --status, --progress, or --notes required")
} }
@ -58,11 +52,11 @@ func addTaskUpdateCommand(parent *clir.Command) {
defer cancel() defer cancel()
update := agentic.TaskUpdate{ update := agentic.TaskUpdate{
Progress: progress, Progress: taskUpdateProgress,
Notes: notes, Notes: taskUpdateNotes,
} }
if status != "" { if taskUpdateStatus != "" {
update.Status = agentic.TaskStatus(status) update.Status = agentic.TaskStatus(taskUpdateStatus)
} }
if err := client.UpdateTask(ctx, taskID, update); err != nil { 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) fmt.Printf("%s Task %s updated successfully\n", successStyle.Render(">>"), taskID)
return nil return nil
}) },
} }
func addTaskCompleteCommand(parent *clir.Command) { var taskCompleteCmd = &cobra.Command{
var output string Use: "task:complete [task-id]",
var failed bool Short: "Mark a task as completed",
var errorMsg string Long: `Marks a task as completed with optional output and artifacts.
cmd := parent.NewSubCommand("task:complete", "Mark a task as completed") Examples:
cmd.LongDescription("Marks a task as completed with optional output and artifacts.\n\n" + core ai task:complete abc123 --output 'Feature implemented'
"Examples:\n" + core ai task:complete abc123 --failed --error 'Build failed'`,
" core ai task:complete abc123 --output 'Feature implemented'\n" + Args: cobra.ExactArgs(1),
" core ai task:complete abc123 --failed --error 'Build failed'") RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
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")
}
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
@ -115,20 +91,40 @@ func addTaskCompleteCommand(parent *clir.Command) {
defer cancel() defer cancel()
result := agentic.TaskResult{ result := agentic.TaskResult{
Success: !failed, Success: !taskCompleteFailed,
Output: output, Output: taskCompleteOutput,
ErrorMessage: errorMsg, ErrorMessage: taskCompleteErrorMsg,
} }
if err := client.CompleteTask(ctx, taskID, result); err != nil { if err := client.CompleteTask(ctx, taskID, result); err != nil {
return fmt.Errorf("failed to complete task: %w", err) 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) fmt.Printf("%s Task %s marked as failed\n", errorStyle.Render(">>"), taskID)
} else { } else {
fmt.Printf("%s Task %s completed successfully\n", successStyle.Render(">>"), taskID) fmt.Printf("%s Task %s completed successfully\n", successStyle.Render(">>"), taskID)
} }
return nil 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)
} }

View file

@ -10,52 +10,70 @@
// - claude: Claude Code CLI integration (planned) // - claude: Claude Code CLI integration (planned)
package ai package ai
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'ai' command and all subcommands. var aiCmd = &cobra.Command{
func AddCommands(app *clir.Cli) { Use: "ai",
aiCmd := app.NewSubCommand("ai", "AI agent task management") Short: "AI agent task management",
aiCmd.LongDescription("Manage tasks from the core-agentic service for AI-assisted development.\n\n" + Long: `Manage tasks from the core-agentic service for AI-assisted development.
"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 <id> -m 'msg' # Commit with task reference\n" +
" core ai task:complete <id> # Mark task done")
// Add Claude command Commands:
addClaudeCommand(aiCmd) 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 <id> -m 'msg' # Commit with task reference
core ai task:complete <id> # 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 // Add agentic task commands
AddAgenticCommands(aiCmd) AddAgenticCommands(aiCmd)
} }
// addClaudeCommand adds the 'claude' subcommand for Claude Code integration. // AddCommands registers the 'ai' command and all subcommands.
func addClaudeCommand(parent *clir.Command) { func AddCommands(root *cobra.Command) {
claudeCmd := parent.NewSubCommand("claude", "Claude Code integration") root.AddCommand(aiCmd)
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()
})
} }
func runClaudeCode() error { func runClaudeCode() error {

View file

@ -5,7 +5,7 @@ import (
"embed" "embed"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Build command styles // Build command styles
@ -32,100 +32,130 @@ var (
//go:embed all:tmpl/gui //go:embed all:tmpl/gui
var guiTemplate embed.FS 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 // Flags for the main build command
var buildType string var (
var ciMode bool buildType string
var targets string ciMode bool
var outputDir string targets string
var doArchive bool outputDir string
var doChecksum bool doArchive bool
doChecksum bool
// Docker/LinuxKit specific flags // Docker/LinuxKit specific flags
var configPath string configPath string
var format string format string
var push bool push bool
var imageName string imageName string
// Signing flags // Signing flags
var noSign bool noSign bool
var notarize bool notarize bool
buildCmd.StringFlag("type", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified", &buildType) // from-path subcommand
buildCmd.BoolFlag("ci", "CI mode - minimal output with JSON artifact list at the end", &ciMode) fromPath string
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)
// Docker/LinuxKit specific // pwa subcommand
buildCmd.StringFlag("config", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)", &configPath) pwaURL string
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)
// Signing flags // sdk subcommand
buildCmd.BoolFlag("no-sign", "Skip all code signing", &noSign) sdkSpec string
buildCmd.BoolFlag("notarize", "Enable macOS notarization (requires Apple credentials)", &notarize) sdkLang string
sdkVersion string
sdkDryRun bool
)
// Set defaults for archive and checksum (true by default) var buildCmd = &cobra.Command{
doArchive = true Use: "build",
doChecksum = true 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) Examples:
buildCmd.Action(func() error { 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) return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize)
}) },
}
// --- `build from-path` command (legacy PWA/GUI build) --- var fromPathCmd = &cobra.Command{
fromPathCmd := buildCmd.NewSubCommand("from-path", "Build from a local directory.") Use: "from-path",
var fromPath string Short: "Build from a local directory.",
fromPathCmd.StringFlag("path", "The path to the static web application files.", &fromPath) RunE: func(cmd *cobra.Command, args []string) error {
fromPathCmd.Action(func() error {
if fromPath == "" { if fromPath == "" {
return errPathRequired return errPathRequired
} }
return runBuild(fromPath) return runBuild(fromPath)
}) },
}
// --- `build pwa` command (legacy PWA build) --- var pwaCmd = &cobra.Command{
pwaCmd := buildCmd.NewSubCommand("pwa", "Build from a live PWA URL.") Use: "pwa",
var pwaURL string Short: "Build from a live PWA URL.",
pwaCmd.StringFlag("url", "The URL of the PWA to build.", &pwaURL) RunE: func(cmd *cobra.Command, args []string) error {
pwaCmd.Action(func() error {
if pwaURL == "" { if pwaURL == "" {
return errURLRequired return errURLRequired
} }
return runPwaBuild(pwaURL) return runPwaBuild(pwaURL)
}) },
}
// --- `build sdk` command ---
sdkBuildCmd := buildCmd.NewSubCommand("sdk", "Generate API SDKs from OpenAPI spec") var sdkBuildCmd = &cobra.Command{
sdkBuildCmd.LongDescription("Generates typed API clients from OpenAPI specifications.\n" + Use: "sdk",
"Supports TypeScript, Python, Go, and PHP.\n\n" + Short: "Generate API SDKs from OpenAPI spec",
"Examples:\n" + Long: `Generates typed API clients from OpenAPI specifications.
" core build sdk # Generate all configured SDKs\n" + Supports TypeScript, Python, Go, and PHP.
" core build sdk --lang typescript # Generate only TypeScript SDK\n" +
" core build sdk --spec api.yaml # Use specific OpenAPI spec") Examples:
core build sdk # Generate all configured SDKs
var sdkSpec, sdkLang, sdkVersion string core build sdk --lang typescript # Generate only TypeScript SDK
var sdkDryRun bool core build sdk --spec api.yaml # Use specific OpenAPI spec`,
sdkBuildCmd.StringFlag("spec", "Path to OpenAPI spec file", &sdkSpec) RunE: func(cmd *cobra.Command, args []string) error {
sdkBuildCmd.StringFlag("lang", "Generate only this language (typescript, python, go, php)", &sdkLang) return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
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) 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(&notarize, "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)
} }

View file

@ -16,9 +16,9 @@
// - build sdk: Generate API SDKs from OpenAPI spec // - build sdk: Generate API SDKs from OpenAPI spec
package build package build
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'build' command and all subcommands. // AddCommands registers the 'build' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddBuildCommand(app) AddBuildCommand(root)
} }

View file

@ -3,7 +3,7 @@ package ci
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared // Style aliases from shared
@ -15,52 +15,75 @@ var (
releaseValueStyle = shared.ValueStyle releaseValueStyle = shared.ValueStyle
) )
// AddCIReleaseCommand adds the release command and its subcommands. // Flag variables for ci command
func AddCIReleaseCommand(app *clir.Cli) { var (
releaseCmd := app.NewSubCommand("ci", "Publish releases (dry-run by default)") ciGoForLaunch bool
releaseCmd.LongDescription("Publishes pre-built artifacts from dist/ to configured targets.\n" + ciVersion string
"Run 'core build' first to create artifacts.\n\n" + ciDraft bool
"SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.\n\n" + ciPrerelease bool
"Configuration: .core/release.yaml") )
// Flags for the main release command // Flag variables for changelog subcommand
var goForLaunch bool var (
var version string changelogFromRef string
var draft bool changelogToRef string
var prerelease bool )
releaseCmd.BoolFlag("we-are-go-for-launch", "Actually publish (default is dry-run for safety)", &goForLaunch) var ciCmd = &cobra.Command{
releaseCmd.StringFlag("version", "Version to release (e.g., v1.2.3)", &version) Use: "ci",
releaseCmd.BoolFlag("draft", "Create release as a draft", &draft) Short: "Publish releases (dry-run by default)",
releaseCmd.BoolFlag("prerelease", "Mark release as a prerelease", &prerelease) 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 SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.
releaseCmd.Action(func() error {
dryRun := !goForLaunch
return runCIPublish(dryRun, version, draft, prerelease)
})
// `release init` subcommand Configuration: .core/release.yaml`,
initCmd := releaseCmd.NewSubCommand("init", "Initialize release configuration") RunE: func(cmd *cobra.Command, args []string) error {
initCmd.LongDescription("Creates a .core/release.yaml configuration file interactively.") dryRun := !ciGoForLaunch
initCmd.Action(func() error { return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
return runCIReleaseInit() },
}) }
// `release changelog` subcommand var ciInitCmd = &cobra.Command{
changelogCmd := releaseCmd.NewSubCommand("changelog", "Generate changelog") Use: "init",
changelogCmd.LongDescription("Generates a changelog from conventional commits.") Short: "Initialize release configuration",
var fromRef, toRef string Long: "Creates a .core/release.yaml configuration file interactively.",
changelogCmd.StringFlag("from", "Starting ref (default: previous tag)", &fromRef) RunE: func(cmd *cobra.Command, args []string) error {
changelogCmd.StringFlag("to", "Ending ref (default: HEAD)", &toRef) return runCIReleaseInit()
changelogCmd.Action(func() error { },
return runChangelog(fromRef, toRef) }
})
var ciChangelogCmd = &cobra.Command{
// `release version` subcommand Use: "changelog",
versionCmd := releaseCmd.NewSubCommand("version", "Show or set version") Short: "Generate changelog",
versionCmd.LongDescription("Shows the determined version or validates a version string.") Long: "Generates a changelog from conventional commits.",
versionCmd.Action(func() error { RunE: func(cmd *cobra.Command, args []string) error {
return runCIReleaseVersion() 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)
} }

View file

@ -9,9 +9,9 @@
// Configuration via .core/release.yaml. // Configuration via .core/release.yaml.
package ci package ci
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'ci' command and all subcommands. // AddCommands registers the 'ci' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddCIReleaseCommand(app) root.AddCommand(ciCmd)
} }

View file

@ -17,32 +17,92 @@
package cmd package cmd
import ( import (
"github.com/charmbracelet/lipgloss" "os"
"github.com/leaanthony/clir"
"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 ( var (
// coreStyle is used for primary headings and the CLI name. // coreStyle is used for primary headings and the CLI name.
coreStyle = lipgloss.NewStyle(). coreStyle = shared.RepoNameStyle
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)
// linkStyle is used for URLs and clickable references. // linkStyle is used for URLs and clickable references.
linkStyle = lipgloss.NewStyle(). linkStyle = shared.LinkStyle
Foreground(lipgloss.Color("#3b82f6")). // blue-500
Underline(true)
) )
// 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. // Execute initialises and runs the CLI application.
// Commands are registered based on build tags (see core_ci.go and core_dev.go). // Commands are registered based on build tags (see core_ci.go and core_dev.go).
func Execute() error { func Execute() error {
app := clir.NewCli("core", "CLI tool for development and production", "0.1.0") return rootCmd.Execute()
registerCommands(app) }
return app.Run()
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)
}
},
} }

View file

@ -19,13 +19,11 @@ import (
"github.com/host-uk/core/cmd/ci" "github.com/host-uk/core/cmd/ci"
"github.com/host-uk/core/cmd/doctor" "github.com/host-uk/core/cmd/doctor"
"github.com/host-uk/core/cmd/sdk" "github.com/host-uk/core/cmd/sdk"
"github.com/leaanthony/clir"
) )
// registerCommands adds CI/release commands only. func init() {
func registerCommands(app *clir.Cli) { build.AddCommands(rootCmd)
build.AddCommands(app) ci.AddCommands(rootCmd)
ci.AddCommands(app) sdk.AddCommands(rootCmd)
sdk.AddCommands(app) doctor.AddCommands(rootCmd)
doctor.AddCommands(app)
} }

View file

@ -35,31 +35,29 @@ import (
"github.com/host-uk/core/cmd/setup" "github.com/host-uk/core/cmd/setup"
testcmd "github.com/host-uk/core/cmd/test" testcmd "github.com/host-uk/core/cmd/test"
"github.com/host-uk/core/cmd/vm" "github.com/host-uk/core/cmd/vm"
"github.com/leaanthony/clir"
) )
// registerCommands adds all development commands. func init() {
func registerCommands(app *clir.Cli) {
// Multi-repo workflow // Multi-repo workflow
dev.AddCommands(app) dev.AddCommands(rootCmd)
// AI agent tools // AI agent tools
ai.AddCommands(app) ai.AddCommands(rootCmd)
// Language tooling // Language tooling
gocmd.AddCommands(app) gocmd.AddCommands(rootCmd)
php.AddCommands(app) php.AddCommands(rootCmd)
// Build and release // Build and release
build.AddCommands(app) build.AddCommands(rootCmd)
ci.AddCommands(app) ci.AddCommands(rootCmd)
sdk.AddCommands(app) sdk.AddCommands(rootCmd)
// Environment management // Environment management
pkg.AddCommands(app) pkg.AddCommands(rootCmd)
vm.AddCommands(app) vm.AddCommands(rootCmd)
docs.AddCommands(app) docs.AddCommands(rootCmd)
setup.AddCommands(app) setup.AddCommands(rootCmd)
doctor.AddCommands(app) doctor.AddCommands(rootCmd)
testcmd.AddCommands(app) testcmd.AddCommands(rootCmd)
} }

View file

@ -31,7 +31,7 @@ package dev
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared package // Style aliases from shared package
@ -64,28 +64,36 @@ var (
) )
// AddCommands registers the 'dev' command and all subcommands. // AddCommands registers the 'dev' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
devCmd := app.NewSubCommand("dev", "Multi-repo development workflow") devCmd := &cobra.Command{
devCmd.LongDescription("Manage multiple git repositories and GitHub integration.\n\n" + Use: "dev",
"Uses repos.yaml to discover repositories. Falls back to scanning\n" + Short: "Multi-repo development workflow",
"the current directory if no registry is found.\n\n" + Long: `Manage multiple git repositories and GitHub integration.
"Git Operations:\n" +
" work Combined status -> commit -> push workflow\n" + Uses repos.yaml to discover repositories. Falls back to scanning
" health Quick repo health summary\n" + the current directory if no registry is found.
" commit Claude-assisted commit messages\n" +
" push Push repos with unpushed commits\n" + Git Operations:
" pull Pull repos behind remote\n\n" + work Combined status -> commit -> push workflow
"GitHub Integration (requires gh CLI):\n" + health Quick repo health summary
" issues List open issues across repos\n" + commit Claude-assisted commit messages
" reviews List PRs awaiting review\n" + push Push repos with unpushed commits
" ci Check GitHub Actions status\n" + pull Pull repos behind remote
" impact Analyse dependency impact\n\n" +
"Dev Environment:\n" + GitHub Integration (requires gh CLI):
" install Download dev environment image\n" + issues List open issues across repos
" boot Start dev environment VM\n" + reviews List PRs awaiting review
" stop Stop dev environment VM\n" + ci Check GitHub Actions status
" shell Open shell in dev VM\n" + impact Analyse dependency impact
" status Check dev VM status")
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 // Git operations
addWorkCommand(devCmd) addWorkCommand(devCmd)

View file

@ -1,13 +1,17 @@
package dev package dev
import ( import (
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// addAPICommands adds the 'api' command and its subcommands to the given parent command. // 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 // 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' // Add the 'sync' command to 'api'
addSyncCommand(apiCmd) addSyncCommand(apiCmd)

View file

@ -11,7 +11,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// CI-specific styles // CI-specific styles
@ -45,27 +45,35 @@ type WorkflowRun struct {
RepoName string `json:"-"` RepoName string `json:"-"`
} }
// CI command flags
var (
ciRegistryPath string
ciBranch string
ciFailedOnly bool
)
// addCICommand adds the 'ci' command to the given parent command. // addCICommand adds the 'ci' command to the given parent command.
func addCICommand(parent *clir.Command) { func addCICommand(parent *cobra.Command) {
var registryPath string ciCmd := &cobra.Command{
var branch string Use: "ci",
var failedOnly bool Short: "Check CI status across all repos",
Long: `Fetches GitHub Actions workflow status for all repos.
ciCmd := parent.NewSubCommand("ci", "Check CI status across all repos") Shows latest run status for each repo.
ciCmd.LongDescription("Fetches GitHub Actions workflow status for all repos.\n" + Requires the 'gh' CLI to be installed and authenticated.`,
"Shows latest run status for each repo.\n" + RunE: func(cmd *cobra.Command, args []string) error {
"Requires the 'gh' CLI to be installed and authenticated.") branch := ciBranch
ciCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath)
ciCmd.StringFlag("branch", "Filter by branch (default: main)", &branch)
ciCmd.BoolFlag("failed", "Show only failed runs", &failedOnly)
ciCmd.Action(func() error {
if branch == "" { if branch == "" {
branch = "main" branch = "main"
} }
return runCI(registryPath, branch, failedOnly) return runCI(ciRegistryPath, branch, ciFailedOnly)
}) },
}
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")
parent.AddCommand(ciCmd)
} }
func runCI(registryPath string, branch string, failedOnly bool) error { func runCI(registryPath string, branch string, failedOnly bool) error {

View file

@ -8,24 +8,31 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos" "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. // addCommitCommand adds the 'commit' command to the given parent command.
func addCommitCommand(parent *clir.Command) { func addCommitCommand(parent *cobra.Command) {
var registryPath string commitCmd := &cobra.Command{
var all bool 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.Flags().StringVar(&commitRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
commitCmd.LongDescription("Uses Claude to create commits for dirty repos.\n" + commitCmd.Flags().BoolVar(&commitAll, "all", false, "Commit all dirty repos without prompting")
"Shows uncommitted changes and invokes Claude to generate commit messages.")
commitCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) parent.AddCommand(commitCmd)
commitCmd.BoolFlag("all", "Commit all dirty repos without prompting", &all)
commitCmd.Action(func() error {
return runCommit(registryPath, all)
})
} }
func runCommit(registryPath string, all bool) error { func runCommit(registryPath string, all bool) error {

View file

@ -8,24 +8,31 @@ import (
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos" "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. // addHealthCommand adds the 'health' command to the given parent command.
func addHealthCommand(parent *clir.Command) { func addHealthCommand(parent *cobra.Command) {
var registryPath string healthCmd := &cobra.Command{
var verbose bool 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.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
healthCmd.LongDescription("Shows a summary of repository health:\n" + healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown")
"total repos, dirty repos, unpushed commits, etc.")
healthCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) parent.AddCommand(healthCmd)
healthCmd.BoolFlag("verbose", "Show detailed breakdown", &verbose)
healthCmd.Action(func() error {
return runHealth(registryPath, verbose)
})
} }
func runHealth(registryPath string, verbose bool) error { func runHealth(registryPath string, verbose bool) error {

View file

@ -2,13 +2,12 @@ package dev
import ( import (
"fmt" "fmt"
"os"
"sort" "sort"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Impact-specific styles // Impact-specific styles
@ -24,31 +23,25 @@ var (
Foreground(lipgloss.Color("#22c55e")) // green-500 Foreground(lipgloss.Color("#22c55e")) // green-500
) )
// Impact command flags
var impactRegistryPath string
// addImpactCommand adds the 'impact' command to the given parent command. // addImpactCommand adds the 'impact' command to the given parent command.
func addImpactCommand(parent *clir.Command) { func addImpactCommand(parent *cobra.Command) {
var registryPath string impactCmd := &cobra.Command{
Use: "impact <repo-name>",
impactCmd := parent.NewSubCommand("impact", "Show impact of changing a repo") Short: "Show impact of changing a repo",
impactCmd.LongDescription("Analyzes the dependency graph to show which repos\n" + Long: `Analyzes the dependency graph to show which repos
"would be affected by changes to the specified repo.") would be affected by changes to the specified repo.`,
Args: cobra.ExactArgs(1),
impactCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) RunE: func(cmd *cobra.Command, args []string) error {
return runImpact(impactRegistryPath, args[0])
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 == "" { impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
return fmt.Errorf("usage: core impact <repo-name>")
} parent.AddCommand(impactCmd)
return runImpact(registryPath, repoName)
})
} }
func runImpact(registryPath string, repoName string) error { func runImpact(registryPath string, repoName string) error {

View file

@ -12,7 +12,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Issue-specific styles // Issue-specific styles
@ -62,26 +62,34 @@ type GitHubIssue struct {
RepoName string `json:"-"` RepoName string `json:"-"`
} }
// Issues command flags
var (
issuesRegistryPath string
issuesLimit int
issuesAssignee string
)
// addIssuesCommand adds the 'issues' command to the given parent command. // addIssuesCommand adds the 'issues' command to the given parent command.
func addIssuesCommand(parent *clir.Command) { func addIssuesCommand(parent *cobra.Command) {
var registryPath string issuesCmd := &cobra.Command{
var limit int Use: "issues",
var assignee string Short: "List open issues across all repos",
Long: `Fetches open issues from GitHub for all repos in the registry.
issuesCmd := parent.NewSubCommand("issues", "List open issues across all repos") Requires the 'gh' CLI to be installed and authenticated.`,
issuesCmd.LongDescription("Fetches open issues from GitHub for all repos in the registry.\n" + RunE: func(cmd *cobra.Command, args []string) error {
"Requires the 'gh' CLI to be installed and authenticated.") limit := issuesLimit
issuesCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath)
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 { if limit == 0 {
limit = 10 limit = 10
} }
return runIssues(registryPath, limit, assignee) return runIssues(issuesRegistryPath, limit, issuesAssignee)
}) },
}
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)")
parent.AddCommand(issuesCmd)
} }
func runIssues(registryPath string, limit int, assignee string) error { func runIssues(registryPath string, limit int, assignee string) error {

View file

@ -8,24 +8,31 @@ import (
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos" "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. // addPullCommand adds the 'pull' command to the given parent command.
func addPullCommand(parent *clir.Command) { func addPullCommand(parent *cobra.Command) {
var registryPath string pullCmd := &cobra.Command{
var all bool 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.Flags().StringVar(&pullRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
pullCmd.LongDescription("Pulls updates for all repos.\n" + pullCmd.Flags().BoolVar(&pullAll, "all", false, "Pull all repos, not just those behind")
"By default only pulls repos that are behind. Use --all to pull all repos.")
pullCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) parent.AddCommand(pullCmd)
pullCmd.BoolFlag("all", "Pull all repos, not just those behind", &all)
pullCmd.Action(func() error {
return runPull(registryPath, all)
})
} }
func runPull(registryPath string, all bool) error { func runPull(registryPath string, all bool) error {

View file

@ -8,24 +8,31 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos" "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. // addPushCommand adds the 'push' command to the given parent command.
func addPushCommand(parent *clir.Command) { func addPushCommand(parent *cobra.Command) {
var registryPath string pushCmd := &cobra.Command{
var force bool 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.Flags().StringVar(&pushRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
pushCmd.LongDescription("Pushes unpushed commits for all repos.\n" + pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, "Skip confirmation prompt")
"Shows repos with commits to push and confirms before pushing.")
pushCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) parent.AddCommand(pushCmd)
pushCmd.BoolFlag("force", "Skip confirmation prompt", &force)
pushCmd.Action(func() error {
return runPush(registryPath, force)
})
} }
func runPush(registryPath string, force bool) error { func runPush(registryPath string, force bool) error {

View file

@ -12,7 +12,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// PR-specific styles // PR-specific styles
@ -67,24 +67,31 @@ type GitHubPR struct {
RepoName string `json:"-"` RepoName string `json:"-"`
} }
// Reviews command flags
var (
reviewsRegistryPath string
reviewsAuthor string
reviewsShowAll bool
)
// addReviewsCommand adds the 'reviews' command to the given parent command. // addReviewsCommand adds the 'reviews' command to the given parent command.
func addReviewsCommand(parent *clir.Command) { func addReviewsCommand(parent *cobra.Command) {
var registryPath string reviewsCmd := &cobra.Command{
var author string Use: "reviews",
var showAll bool 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.Flags().StringVar(&reviewsRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
reviewsCmd.LongDescription("Fetches open PRs from GitHub for all repos in the registry.\n" + reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", "Filter by PR author")
"Shows review status (approved, changes requested, pending).\n" + reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, "Show all PRs including drafts")
"Requires the 'gh' CLI to be installed and authenticated.")
reviewsCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) parent.AddCommand(reviewsCmd)
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)
})
} }
func runReviews(registryPath string, author string, showAll bool) error { func runReviews(registryPath string, author string, showAll bool) error {

View file

@ -10,22 +10,29 @@ import (
"path/filepath" "path/filepath"
"text/template" "text/template"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
// addSyncCommand adds the 'sync' command to the given parent command. // addSyncCommand adds the 'sync' command to the given parent command.
func addSyncCommand(parent *clir.Command) { func addSyncCommand(parent *cobra.Command) {
syncCmd := parent.NewSubCommand("sync", "Synchronizes the public service APIs with their internal implementations.") syncCmd := &cobra.Command{
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.") Use: "sync",
syncCmd.Action(func() error { 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 { if err := runSync(); err != nil {
return fmt.Errorf("Error: %w", err) return fmt.Errorf("Error: %w", err)
} }
fmt.Println("Public APIs synchronized successfully.") fmt.Println("Public APIs synchronized successfully.")
return nil return nil
}) },
}
parent.AddCommand(syncCmd)
} }
type symbolInfo struct { type symbolInfo struct {

View file

@ -7,12 +7,12 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/devops" "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. // 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. // 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) addVMInstallCommand(parent)
addVMBootCommand(parent) addVMBootCommand(parent)
addVMStopCommand(parent) addVMStopCommand(parent)
@ -25,17 +25,23 @@ func addVMCommands(parent *clir.Command) {
} }
// addVMInstallCommand adds the 'dev install' command. // addVMInstallCommand adds the 'dev install' command.
func addVMInstallCommand(parent *clir.Command) { func addVMInstallCommand(parent *cobra.Command) {
installCmd := parent.NewSubCommand("install", "Download and install the dev environment image") installCmd := &cobra.Command{
installCmd.LongDescription("Downloads the platform-specific dev environment image.\n\n" + Use: "install",
"The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI.\n" + Short: "Download and install the dev environment image",
"Downloads are cached at ~/.core/images/\n\n" + Long: `Downloads the platform-specific dev environment image.
"Examples:\n" +
" core dev install")
installCmd.Action(func() error { 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() return runVMInstall()
}) },
}
parent.AddCommand(installCmd)
} }
func runVMInstall() error { func runVMInstall() error {
@ -85,26 +91,34 @@ func runVMInstall() error {
return nil return nil
} }
// VM boot command flags
var (
vmBootMemory int
vmBootCPUs int
vmBootFresh bool
)
// addVMBootCommand adds the 'devops boot' command. // addVMBootCommand adds the 'devops boot' command.
func addVMBootCommand(parent *clir.Command) { func addVMBootCommand(parent *cobra.Command) {
var memory int bootCmd := &cobra.Command{
var cpus int Use: "boot",
var fresh bool Short: "Start the dev environment",
Long: `Boots the dev environment VM.
bootCmd := parent.NewSubCommand("boot", "Start the dev environment") Examples:
bootCmd.LongDescription("Boots the dev environment VM.\n\n" + core dev boot
"Examples:\n" + core dev boot --memory 8192 --cpus 4
" core dev boot\n" + core dev boot --fresh`,
" core dev boot --memory 8192 --cpus 4\n" + RunE: func(cmd *cobra.Command, args []string) error {
" core dev boot --fresh") return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh)
},
}
bootCmd.IntFlag("memory", "Memory in MB (default: 4096)", &memory) bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, "Memory in MB (default: 4096)")
bootCmd.IntFlag("cpus", "Number of CPUs (default: 2)", &cpus) bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, "Number of CPUs (default: 2)")
bootCmd.BoolFlag("fresh", "Stop existing and start fresh", &fresh) bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, "Stop existing and start fresh")
bootCmd.Action(func() error { parent.AddCommand(bootCmd)
return runVMBoot(memory, cpus, fresh)
})
} }
func runVMBoot(memory, cpus int, fresh bool) error { 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. // addVMStopCommand adds the 'devops stop' command.
func addVMStopCommand(parent *clir.Command) { func addVMStopCommand(parent *cobra.Command) {
stopCmd := parent.NewSubCommand("stop", "Stop the dev environment") stopCmd := &cobra.Command{
stopCmd.LongDescription("Stops the running dev environment VM.\n\n" + Use: "stop",
"Examples:\n" + Short: "Stop the dev environment",
" core dev stop") Long: `Stops the running dev environment VM.
stopCmd.Action(func() error { Examples:
core dev stop`,
RunE: func(cmd *cobra.Command, args []string) error {
return runVMStop() return runVMStop()
}) },
}
parent.AddCommand(stopCmd)
} }
func runVMStop() error { func runVMStop() error {
@ -184,15 +203,20 @@ func runVMStop() error {
} }
// addVMStatusCommand adds the 'devops status' command. // addVMStatusCommand adds the 'devops status' command.
func addVMStatusCommand(parent *clir.Command) { func addVMStatusCommand(parent *cobra.Command) {
statusCmd := parent.NewSubCommand("vm-status", "Show dev environment status") statusCmd := &cobra.Command{
statusCmd.LongDescription("Shows the current status of the dev environment.\n\n" + Use: "vm-status",
"Examples:\n" + Short: "Show dev environment status",
" core dev vm-status") Long: `Shows the current status of the dev environment.
statusCmd.Action(func() error { Examples:
core dev vm-status`,
RunE: func(cmd *cobra.Command, args []string) error {
return runVMStatus() return runVMStatus()
}) },
}
parent.AddCommand(statusCmd)
} }
func runVMStatus() error { 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) 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. // addVMShellCommand adds the 'devops shell' command.
func addVMShellCommand(parent *clir.Command) { func addVMShellCommand(parent *cobra.Command) {
var console bool 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") Uses SSH by default, or serial console with --console.
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")
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 { shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, "Use serial console instead of SSH")
args := shellCmd.OtherArgs()
return runVMShell(console, args) parent.AddCommand(shellCmd)
})
} }
func runVMShell(console bool, command []string) error { func runVMShell(console bool, command []string) error {
@ -290,25 +320,34 @@ func runVMShell(console bool, command []string) error {
return d.Shell(ctx, opts) return d.Shell(ctx, opts)
} }
// VM serve command flags
var (
vmServePort int
vmServePath string
)
// addVMServeCommand adds the 'devops serve' command. // addVMServeCommand adds the 'devops serve' command.
func addVMServeCommand(parent *clir.Command) { func addVMServeCommand(parent *cobra.Command) {
var port int serveCmd := &cobra.Command{
var path string 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") Auto-detects the appropriate serve command based on project files.
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")
serveCmd.IntFlag("port", "Port to serve on (default: 8000)", &port) Examples:
serveCmd.StringFlag("path", "Subdirectory to serve", &path) 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 { serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, "Port to serve on (default: 8000)")
return runVMServe(port, path) serveCmd.Flags().StringVar(&vmServePath, "path", "", "Subdirectory to serve")
})
parent.AddCommand(serveCmd)
} }
func runVMServe(port int, path string) error { func runVMServe(port int, path string) error {
@ -331,24 +370,30 @@ func runVMServe(port int, path string) error {
return d.Serve(ctx, projectDir, opts) return d.Serve(ctx, projectDir, opts)
} }
// VM test command flags
var vmTestName string
// addVMTestCommand adds the 'devops test' command. // addVMTestCommand adds the 'devops test' command.
func addVMTestCommand(parent *clir.Command) { func addVMTestCommand(parent *cobra.Command) {
var name string 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") Auto-detects the test command based on project files, or uses .core/test.yaml.
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 ./...")
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 { testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", "Run named test command from .core/test.yaml")
args := testCmd.OtherArgs()
return runVMTest(name, args) parent.AddCommand(testCmd)
})
} }
func runVMTest(name string, command []string) error { func runVMTest(name string, command []string) error {
@ -371,34 +416,44 @@ func runVMTest(name string, command []string) error {
return d.Test(ctx, projectDir, opts) return d.Test(ctx, projectDir, opts)
} }
// VM claude command flags
var (
vmClaudeNoAuth bool
vmClaudeModel string
vmClaudeAuthFlags []string
)
// addVMClaudeCommand adds the 'devops claude' command. // addVMClaudeCommand adds the 'devops claude' command.
func addVMClaudeCommand(parent *clir.Command) { func addVMClaudeCommand(parent *cobra.Command) {
var noAuth bool claudeCmd := &cobra.Command{
var model string Use: "claude",
var authFlags []string Short: "Start sandboxed Claude session",
Long: `Starts a Claude Code session inside the dev environment sandbox.
claudeCmd := parent.NewSubCommand("claude", "Start sandboxed Claude session") Provides isolation while forwarding selected credentials.
claudeCmd.LongDescription("Starts a Claude Code session inside the dev environment sandbox.\n\n" + Auto-boots the dev environment if not running.
"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")
claudeCmd.BoolFlag("no-auth", "Don't forward any auth credentials", &noAuth) Auth options (default: all):
claudeCmd.StringFlag("model", "Model to use (opus, sonnet)", &model) gh - GitHub CLI auth
claudeCmd.StringsFlag("auth", "Selective auth forwarding (gh,anthropic,ssh,git)", &authFlags) anthropic - Anthropic API key
ssh - SSH agent forwarding
git - Git config (name, email)
claudeCmd.Action(func() error { Examples:
return runVMClaude(noAuth, model, authFlags) 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 { 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) return d.Claude(ctx, projectDir, opts)
} }
// VM update command flags
var vmUpdateApply bool
// addVMUpdateCommand adds the 'devops update' command. // addVMUpdateCommand adds the 'devops update' command.
func addVMUpdateCommand(parent *clir.Command) { func addVMUpdateCommand(parent *cobra.Command) {
var apply bool 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") Examples:
updateCmd.LongDescription("Checks for dev environment updates and optionally applies them.\n\n" + core dev update
"Examples:\n" + core dev update --apply`,
" core dev update\n" + RunE: func(cmd *cobra.Command, args []string) error {
" core dev update --apply") 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 { parent.AddCommand(updateCmd)
return runVMUpdate(apply)
})
} }
func runVMUpdate(apply bool) error { func runVMUpdate(apply bool) error {

View file

@ -13,27 +13,35 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos" "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. // addWorkCommand adds the 'work' command to the given parent command.
func addWorkCommand(parent *clir.Command) { func addWorkCommand(parent *cobra.Command) {
var statusOnly bool workCmd := &cobra.Command{
var autoCommit bool Use: "work",
var registryPath string Short: "Multi-repo git operations",
Long: `Manage git status, commits, and pushes across multiple repositories.
workCmd := parent.NewSubCommand("work", "Multi-repo git operations") Reads repos.yaml to discover repositories and their relationships.
workCmd.LongDescription("Manage git status, commits, and pushes across multiple repositories.\n\n" + Shows status, optionally commits with Claude, and pushes changes.`,
"Reads repos.yaml to discover repositories and their relationships.\n" + RunE: func(cmd *cobra.Command, args []string) error {
"Shows status, optionally commits with Claude, and pushes changes.") return runWork(workRegistryPath, workStatusOnly, workAutoCommit)
},
}
workCmd.BoolFlag("status", "Show status only, don't push", &statusOnly) workCmd.Flags().BoolVar(&workStatusOnly, "status", false, "Show status only, don't push")
workCmd.BoolFlag("commit", "Use Claude to commit dirty repos before pushing", &autoCommit) workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, "Use Claude to commit dirty repos before pushing")
workCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) workCmd.Flags().StringVar(&workRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
workCmd.Action(func() error { parent.AddCommand(workCmd)
return runWork(registryPath, statusOnly, autoCommit)
})
} }
func runWork(registryPath string, statusOnly, autoCommit bool) error { func runWork(registryPath string, statusOnly, autoCommit bool) error {

View file

@ -8,9 +8,9 @@
// to a central location for unified documentation builds. // to a central location for unified documentation builds.
package docs package docs
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'docs' command and all subcommands. // AddCommands registers the 'docs' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddDocsCommand(app) root.AddCommand(docsCmd)
} }

View file

@ -4,7 +4,7 @@ package docs
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style and utility aliases from shared // Style and utility aliases from shared
@ -29,13 +29,14 @@ var (
Foreground(lipgloss.Color("#3b82f6")) // blue-500 Foreground(lipgloss.Color("#3b82f6")) // blue-500
) )
// AddDocsCommand adds the 'docs' command to the given parent command. var docsCmd = &cobra.Command{
func AddDocsCommand(parent *clir.Cli) { Use: "docs",
docsCmd := parent.NewSubCommand("docs", "Documentation management") Short: "Documentation management",
docsCmd.LongDescription("Manage documentation across all repos.\n" + Long: `Manage documentation across all repos.
"Scan for docs, check coverage, and sync to core-php/docs/packages/.") Scan for docs, check coverage, and sync to core-php/docs/packages/.`,
}
// Add subcommands
addDocsSyncCommand(docsCmd) func init() {
addDocsListCommand(docsCmd) docsCmd.AddCommand(docsSyncCmd)
docsCmd.AddCommand(docsListCmd)
} }

View file

@ -4,18 +4,22 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addDocsListCommand(parent *clir.Command) { // Flag variable for list command
var registryPath string var docsListRegistryPath string
listCmd := parent.NewSubCommand("list", "List documentation across repos") var docsListCmd = &cobra.Command{
listCmd.StringFlag("registry", "Path to repos.yaml", &registryPath) Use: "list",
Short: "List documentation across repos",
RunE: func(cmd *cobra.Command, args []string) error {
return runDocsList(docsListRegistryPath)
},
}
listCmd.Action(func() error { func init() {
return runDocsList(registryPath) docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", "Path to repos.yaml")
})
} }
func runDocsList(registryPath string) error { func runDocsList(registryPath string) error {

View file

@ -6,22 +6,28 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addDocsSyncCommand(parent *clir.Command) { // Flag variables for sync command
var registryPath string var (
var dryRun bool docsSyncRegistryPath string
var outputDir string docsSyncDryRun bool
docsSyncOutputDir string
)
syncCmd := parent.NewSubCommand("sync", "Sync documentation to core-php/docs/packages/") var docsSyncCmd = &cobra.Command{
syncCmd.StringFlag("registry", "Path to repos.yaml", &registryPath) Use: "sync",
syncCmd.BoolFlag("dry-run", "Show what would be synced without copying", &dryRun) Short: "Sync documentation to core-php/docs/packages/",
syncCmd.StringFlag("output", "Output directory (default: core-php/docs/packages)", &outputDir) RunE: func(cmd *cobra.Command, args []string) error {
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
},
}
syncCmd.Action(func() error { func init() {
return runDocsSync(registryPath, outputDir, dryRun) 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 // packageOutputName maps repo name to output folder name

View file

@ -10,9 +10,9 @@
// Provides platform-specific installation instructions for missing tools. // Provides platform-specific installation instructions for missing tools.
package doctor package doctor
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'doctor' command and all subcommands. // AddCommands registers the 'doctor' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddDoctorCommand(app) root.AddCommand(doctorCmd)
} }

View file

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared // Style aliases from shared
@ -15,19 +15,21 @@ var (
dimStyle = shared.DimStyle dimStyle = shared.DimStyle
) )
// AddDoctorCommand adds the 'doctor' command to the given parent command. // Flag variable for doctor command
func AddDoctorCommand(parent *clir.Cli) { var doctorVerbose bool
var verbose bool
doctorCmd := parent.NewSubCommand("doctor", "Check development environment") var doctorCmd = &cobra.Command{
doctorCmd.LongDescription("Checks that all required tools are installed and configured.\n" + Use: "doctor",
"Run this before `core setup` to ensure your environment is ready.") 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) func init() {
doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, "Show detailed version information")
doctorCmd.Action(func() error {
return runDoctor(verbose)
})
} }
func runDoctor(verbose bool) error { func runDoctor(verbose bool) error {

View file

@ -14,9 +14,9 @@
// Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS. // Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS.
package gocmd package gocmd
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'go' command and all subcommands. // AddCommands registers the 'go' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddGoCommands(app) AddGoCommands(root)
} }

View file

@ -5,7 +5,7 @@ package gocmd
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases for shared styles // Style aliases for shared styles
@ -16,9 +16,11 @@ var (
) )
// AddGoCommands adds Go development commands. // AddGoCommands adds Go development commands.
func AddGoCommands(parent *clir.Cli) { func AddGoCommands(root *cobra.Command) {
goCmd := parent.NewSubCommand("go", "Go development tools") goCmd := &cobra.Command{
goCmd.LongDescription("Go development tools with enhanced output and environment setup.\n\n" + Use: "go",
Short: "Go development tools",
Long: "Go development tools with enhanced output and environment setup.\n\n" +
"Commands:\n" + "Commands:\n" +
" test Run tests\n" + " test Run tests\n" +
" cov Run tests with coverage report\n" + " cov Run tests with coverage report\n" +
@ -26,8 +28,10 @@ func AddGoCommands(parent *clir.Cli) {
" lint Run golangci-lint\n" + " lint Run golangci-lint\n" +
" install Install Go binary\n" + " install Install Go binary\n" +
" mod Module management (tidy, download, verify)\n" + " mod Module management (tidy, download, verify)\n" +
" work Workspace management") " work Workspace management",
}
root.AddCommand(goCmd)
addGoTestCommand(goCmd) addGoTestCommand(goCmd)
addGoCovCommand(goCmd) addGoCovCommand(goCmd)
addGoFmtCommand(goCmd) addGoFmtCommand(goCmd)

View file

@ -4,74 +4,82 @@ import (
"os" "os"
"os/exec" "os/exec"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addGoFmtCommand(parent *clir.Command) {
var ( var (
fix bool fmtFix bool
diff bool fmtDiff bool
check bool fmtCheck bool
) )
fmtCmd := parent.NewSubCommand("fmt", "Format Go code") func addGoFmtCommand(parent *cobra.Command) {
fmtCmd.LongDescription("Format Go code using gofmt or goimports.\n\n" + fmtCmd := &cobra.Command{
Use: "fmt",
Short: "Format Go code",
Long: "Format Go code using gofmt or goimports.\n\n" +
"Examples:\n" + "Examples:\n" +
" core go fmt # Check formatting\n" + " core go fmt # Check formatting\n" +
" core go fmt --fix # Fix formatting\n" + " core go fmt --fix # Fix formatting\n" +
" core go fmt --diff # Show diff") " core go fmt --diff # Show diff",
RunE: func(cmd *cobra.Command, args []string) error {
fmtCmd.BoolFlag("fix", "Fix formatting in place", &fix) fmtArgs := []string{}
fmtCmd.BoolFlag("diff", "Show diff of changes", &diff) if fmtFix {
fmtCmd.BoolFlag("check", "Check only, exit 1 if not formatted", &check) fmtArgs = append(fmtArgs, "-w")
fmtCmd.Action(func() error {
args := []string{}
if fix {
args = append(args, "-w")
} }
if diff { if fmtDiff {
args = append(args, "-d") fmtArgs = append(fmtArgs, "-d")
} }
if !fix && !diff { if !fmtFix && !fmtDiff {
args = append(args, "-l") fmtArgs = append(fmtArgs, "-l")
} }
args = append(args, ".") fmtArgs = append(fmtArgs, ".")
// Try goimports first, fall back to gofmt // Try goimports first, fall back to gofmt
var cmd *exec.Cmd var execCmd *exec.Cmd
if _, err := exec.LookPath("goimports"); err == nil { if _, err := exec.LookPath("goimports"); err == nil {
cmd = exec.Command("goimports", args...) execCmd = exec.Command("goimports", fmtArgs...)
} else { } else {
cmd = exec.Command("gofmt", args...) execCmd = exec.Command("gofmt", fmtArgs...)
} }
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
return cmd.Run() return execCmd.Run()
}) },
} }
func addGoLintCommand(parent *clir.Command) { fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Fix formatting in place")
var fix bool fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes")
fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, "Check only, exit 1 if not formatted")
lintCmd := parent.NewSubCommand("lint", "Run golangci-lint") parent.AddCommand(fmtCmd)
lintCmd.LongDescription("Run golangci-lint on the codebase.\n\n" + }
var lintFix bool
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" + "Examples:\n" +
" core go lint\n" + " core go lint\n" +
" core go lint --fix") " core go lint --fix",
RunE: func(cmd *cobra.Command, args []string) error {
lintCmd.BoolFlag("fix", "Fix issues automatically", &fix) lintArgs := []string{"run"}
if lintFix {
lintCmd.Action(func() error { lintArgs = append(lintArgs, "--fix")
args := []string{"run"}
if fix {
args = append(args, "--fix")
} }
cmd := exec.Command("golangci-lint", args...) execCmd := exec.Command("golangci-lint", lintArgs...)
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
return cmd.Run() return execCmd.Run()
}) },
}
lintCmd.Flags().BoolVar(&lintFix, "fix", false, "Fix issues automatically")
parent.AddCommand(lintCmd)
} }

View file

@ -9,41 +9,45 @@ import (
"strings" "strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addGoTestCommand(parent *clir.Command) {
var ( var (
coverage bool testCoverage bool
pkg string testPkg string
run string testRun string
short bool testShort bool
race bool testRace bool
json bool testJSON bool
verbose bool testVerbose bool
) )
testCmd := parent.NewSubCommand("test", "Run tests with coverage") func addGoTestCommand(parent *cobra.Command) {
testCmd.LongDescription("Run Go tests with coverage reporting.\n\n" + 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" + "Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" +
"Filters noisy output and provides colour-coded coverage.\n\n" + "Filters noisy output and provides colour-coded coverage.\n\n" +
"Examples:\n" + "Examples:\n" +
" core go test\n" + " core go test\n" +
" core go test --coverage\n" + " core go test --coverage\n" +
" core go test --pkg ./pkg/crypt\n" + " core go test --pkg ./pkg/crypt\n" +
" core go test --run TestHash") " 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.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage")
testCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package to test (default: ./...)")
testCmd.StringFlag("run", "Run only tests matching regexp", &run) testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching regexp")
testCmd.BoolFlag("short", "Run only short tests", &short) testCmd.Flags().BoolVar(&testShort, "short", false, "Run only short tests")
testCmd.BoolFlag("race", "Enable race detector", &race) testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector")
testCmd.BoolFlag("json", "Output JSON results", &json) testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON results")
testCmd.BoolFlag("v", "Verbose output", &verbose) testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, "Verbose output")
testCmd.Action(func() error { parent.AddCommand(testCmd)
return runGoTest(coverage, pkg, run, short, race, json, verbose)
})
} }
func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose bool) error { func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose bool) error {
@ -166,28 +170,25 @@ func parseOverallCoverage(output string) float64 {
return total / float64(len(matches)) return total / float64(len(matches))
} }
func addGoCovCommand(parent *clir.Command) {
var ( var (
pkg string covPkg string
html bool covHTML bool
open bool covOpen bool
threshold float64 covThreshold float64
) )
covCmd := parent.NewSubCommand("cov", "Run tests with coverage report") func addGoCovCommand(parent *cobra.Command) {
covCmd.LongDescription("Run tests and generate coverage report.\n\n" + covCmd := &cobra.Command{
Use: "cov",
Short: "Run tests with coverage report",
Long: "Run tests and generate coverage report.\n\n" +
"Examples:\n" + "Examples:\n" +
" core go cov # Run with coverage summary\n" + " core go cov # Run with coverage summary\n" +
" core go cov --html # Generate HTML report\n" + " core go cov --html # Generate HTML report\n" +
" core go cov --open # Generate and open HTML report\n" + " core go cov --open # Generate and open HTML report\n" +
" core go cov --threshold 80 # Fail if coverage < 80%") " core go cov --threshold 80 # Fail if coverage < 80%",
RunE: func(cmd *cobra.Command, args []string) error {
covCmd.StringFlag("pkg", "Package to test (default: ./...)", &pkg) pkg := covPkg
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 == "" { if pkg == "" {
// Auto-discover packages with tests // Auto-discover packages with tests
pkgs, err := findTestPackages(".") pkgs, err := findTestPackages(".")
@ -221,18 +222,18 @@ func addGoCovCommand(parent *clir.Command) {
// Run tests with coverage // Run tests with coverage
// We need to split pkg into individual arguments if it contains spaces // We need to split pkg into individual arguments if it contains spaces
pkgArgs := strings.Fields(pkg) pkgArgs := strings.Fields(pkg)
args := append([]string{"test", "-coverprofile=" + covPath, "-covermode=atomic"}, pkgArgs...) cmdArgs := append([]string{"test", "-coverprofile=" + covPath, "-covermode=atomic"}, pkgArgs...)
cmd := exec.Command("go", args...) goCmd := exec.Command("go", cmdArgs...)
cmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0") goCmd.Env = append(os.Environ(), "MACOSX_DEPLOYMENT_TARGET=26.0")
cmd.Stdout = os.Stdout goCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr goCmd.Stderr = os.Stderr
testErr := cmd.Run() testErr := goCmd.Run()
// Get coverage percentage // Get coverage percentage
covCmd := exec.Command("go", "tool", "cover", "-func="+covPath) coverCmd := exec.Command("go", "tool", "cover", "-func="+covPath)
covOutput, err := covCmd.Output() covOutput, err := coverCmd.Output()
if err != nil { if err != nil {
if testErr != nil { if testErr != nil {
return testErr return testErr
@ -266,7 +267,7 @@ func addGoCovCommand(parent *clir.Command) {
fmt.Printf(" %s %s\n", dimStyle.Render("Total:"), covStyle.Render(fmt.Sprintf("%.1f%%", totalCov))) fmt.Printf(" %s %s\n", dimStyle.Render("Total:"), covStyle.Render(fmt.Sprintf("%.1f%%", totalCov)))
// Generate HTML if requested // Generate HTML if requested
if html || open { if covHTML || covOpen {
htmlPath := "coverage.html" htmlPath := "coverage.html"
htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath)
if err := htmlCmd.Run(); err != nil { if err := htmlCmd.Run(); err != nil {
@ -274,7 +275,7 @@ func addGoCovCommand(parent *clir.Command) {
} }
fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath) fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath)
if open { if covOpen {
// Open in browser // Open in browser
var openCmd *exec.Cmd var openCmd *exec.Cmd
switch { switch {
@ -292,9 +293,9 @@ func addGoCovCommand(parent *clir.Command) {
} }
// Check threshold // Check threshold
if threshold > 0 && totalCov < threshold { if covThreshold > 0 && totalCov < covThreshold {
fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n", fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n",
errorStyle.Render("FAIL"), totalCov, threshold) errorStyle.Render("FAIL"), totalCov, covThreshold)
return fmt.Errorf("coverage below threshold") return fmt.Errorf("coverage below threshold")
} }
@ -304,7 +305,15 @@ func addGoCovCommand(parent *clir.Command) {
fmt.Printf("\n%s\n", successStyle.Render("OK")) fmt.Printf("\n%s\n", successStyle.Render("OK"))
return nil return nil
}) },
}
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)")
parent.AddCommand(covCmd)
} }
func findTestPackages(root string) ([]string, error) { func findTestPackages(root string) ([]string, error) {

View file

@ -6,27 +6,26 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addGoInstallCommand(parent *clir.Command) { var (
var verbose bool installVerbose bool
var noCgo bool installNoCgo bool
)
installCmd := parent.NewSubCommand("install", "Install Go binary") func addGoInstallCommand(parent *cobra.Command) {
installCmd.LongDescription("Install Go binary to $GOPATH/bin.\n\n" + installCmd := &cobra.Command{
Use: "install [path]",
Short: "Install Go binary",
Long: "Install Go binary to $GOPATH/bin.\n\n" +
"Examples:\n" + "Examples:\n" +
" core go install # Install current module\n" + " core go install # Install current module\n" +
" core go install ./cmd/core # Install specific path\n" + " core go install ./cmd/core # Install specific path\n" +
" core go install --no-cgo # Pure Go (no C dependencies)\n" + " core go install --no-cgo # Pure Go (no C dependencies)\n" +
" core go install -v # Verbose output") " core go install -v # Verbose output",
RunE: func(cmd *cobra.Command, args []string) error {
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 // Get install path from args or default to current dir
args := installCmd.OtherArgs()
installPath := "./..." installPath := "./..."
if len(args) > 0 { if len(args) > 0 {
installPath = args[0] installPath = args[0]
@ -45,24 +44,24 @@ func addGoInstallCommand(parent *clir.Command) {
fmt.Printf("%s Installing\n", dimStyle.Render("Install:")) fmt.Printf("%s Installing\n", dimStyle.Render("Install:"))
fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath) fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath)
if noCgo { if installNoCgo {
fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled") fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled")
} }
cmdArgs := []string{"install"} cmdArgs := []string{"install"}
if verbose { if installVerbose {
cmdArgs = append(cmdArgs, "-v") cmdArgs = append(cmdArgs, "-v")
} }
cmdArgs = append(cmdArgs, installPath) cmdArgs = append(cmdArgs, installPath)
cmd := exec.Command("go", cmdArgs...) execCmd := exec.Command("go", cmdArgs...)
if noCgo { if installNoCgo {
cmd.Env = append(os.Environ(), "CGO_ENABLED=0") execCmd.Env = append(os.Environ(), "CGO_ENABLED=0")
} }
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := execCmd.Run(); err != nil {
fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed")) fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed"))
return err return err
} }
@ -77,95 +76,132 @@ func addGoInstallCommand(parent *clir.Command) {
fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir) fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir)
return nil return nil
}) },
} }
func addGoModCommand(parent *clir.Command) { installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, "Verbose output")
modCmd := parent.NewSubCommand("mod", "Module management") installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, "Disable CGO (CGO_ENABLED=0)")
modCmd.LongDescription("Go module management commands.\n\n" +
parent.AddCommand(installCmd)
}
func addGoModCommand(parent *cobra.Command) {
modCmd := &cobra.Command{
Use: "mod",
Short: "Module management",
Long: "Go module management commands.\n\n" +
"Commands:\n" + "Commands:\n" +
" tidy Add missing and remove unused modules\n" + " tidy Add missing and remove unused modules\n" +
" download Download modules to local cache\n" + " download Download modules to local cache\n" +
" verify Verify dependencies\n" + " verify Verify dependencies\n" +
" graph Print module dependency graph") " graph Print module dependency graph",
// tidy
tidyCmd := modCmd.NewSubCommand("tidy", "Tidy go.mod")
tidyCmd.Action(func() error {
cmd := exec.Command("go", "mod", "tidy")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// download
downloadCmd := modCmd.NewSubCommand("download", "Download modules")
downloadCmd.Action(func() error {
cmd := exec.Command("go", "mod", "download")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// verify
verifyCmd := modCmd.NewSubCommand("verify", "Verify dependencies")
verifyCmd.Action(func() error {
cmd := exec.Command("go", "mod", "verify")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// graph
graphCmd := modCmd.NewSubCommand("graph", "Print dependency graph")
graphCmd.Action(func() error {
cmd := exec.Command("go", "mod", "graph")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
} }
func addGoWorkCommand(parent *clir.Command) { // tidy
workCmd := parent.NewSubCommand("work", "Workspace management") tidyCmd := &cobra.Command{
workCmd.LongDescription("Go workspace management commands.\n\n" + 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 := &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 := &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 := &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 *cobra.Command) {
workCmd := &cobra.Command{
Use: "work",
Short: "Workspace management",
Long: "Go workspace management commands.\n\n" +
"Commands:\n" + "Commands:\n" +
" sync Sync go.work with modules\n" + " sync Sync go.work with modules\n" +
" init Initialize go.work\n" + " init Initialize go.work\n" +
" use Add module to workspace") " use Add module to workspace",
}
// sync // sync
syncCmd := workCmd.NewSubCommand("sync", "Sync workspace") syncCmd := &cobra.Command{
syncCmd.Action(func() error { Use: "sync",
cmd := exec.Command("go", "work", "sync") Short: "Sync workspace",
cmd.Stdout = os.Stdout RunE: func(cmd *cobra.Command, args []string) error {
cmd.Stderr = os.Stderr execCmd := exec.Command("go", "work", "sync")
return cmd.Run() execCmd.Stdout = os.Stdout
}) execCmd.Stderr = os.Stderr
return execCmd.Run()
},
}
// init // init
initCmd := workCmd.NewSubCommand("init", "Initialize workspace") initCmd := &cobra.Command{
initCmd.Action(func() error { Use: "init",
cmd := exec.Command("go", "work", "init") Short: "Initialize workspace",
cmd.Stdout = os.Stdout RunE: func(cmd *cobra.Command, args []string) error {
cmd.Stderr = os.Stderr execCmd := exec.Command("go", "work", "init")
if err := cmd.Run(); err != nil { execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
if err := execCmd.Run(); err != nil {
return err return err
} }
// Auto-add current module if go.mod exists // Auto-add current module if go.mod exists
if _, err := os.Stat("go.mod"); err == nil { if _, err := os.Stat("go.mod"); err == nil {
cmd = exec.Command("go", "work", "use", ".") execCmd = exec.Command("go", "work", "use", ".")
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
return cmd.Run() return execCmd.Run()
} }
return nil return nil
}) },
}
// use // use
useCmd := workCmd.NewSubCommand("use", "Add module to workspace") useCmd := &cobra.Command{
useCmd.Action(func() error { Use: "use [modules...]",
args := useCmd.OtherArgs() Short: "Add module to workspace",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
// Auto-detect modules // Auto-detect modules
modules := findGoModules(".") modules := findGoModules(".")
@ -173,10 +209,10 @@ func addGoWorkCommand(parent *clir.Command) {
return fmt.Errorf("no go.mod files found") return fmt.Errorf("no go.mod files found")
} }
for _, mod := range modules { for _, mod := range modules {
cmd := exec.Command("go", "work", "use", mod) execCmd := exec.Command("go", "work", "use", mod)
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := execCmd.Run(); err != nil {
return err return err
} }
fmt.Printf("Added %s\n", mod) fmt.Printf("Added %s\n", mod)
@ -185,11 +221,17 @@ func addGoWorkCommand(parent *clir.Command) {
} }
cmdArgs := append([]string{"work", "use"}, args...) cmdArgs := append([]string{"work", "use"}, args...)
cmd := exec.Command("go", cmdArgs...) execCmd := exec.Command("go", cmdArgs...)
cmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
return cmd.Run() return execCmd.Run()
}) },
}
workCmd.AddCommand(syncCmd)
workCmd.AddCommand(initCmd)
workCmd.AddCommand(useCmd)
parent.AddCommand(workCmd)
} }
func findGoModules(root string) []string { func findGoModules(root string) []string {

View file

@ -33,9 +33,9 @@
// - deploy:list: List recent deployments // - deploy:list: List recent deployments
package php package php
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'php' command and all subcommands. // AddCommands registers the 'php' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddPHPCommands(app) AddPHPCommands(root)
} }

View file

@ -4,7 +4,7 @@ package php
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared // Style aliases from shared
@ -78,15 +78,19 @@ var (
) )
// AddPHPCommands adds PHP/Laravel development commands. // AddPHPCommands adds PHP/Laravel development commands.
func AddPHPCommands(parent *clir.Cli) { func AddPHPCommands(root *cobra.Command) {
phpCmd := parent.NewSubCommand("php", "Laravel/PHP development tools") phpCmd := &cobra.Command{
phpCmd.LongDescription("Manage Laravel development environment with FrankenPHP.\n\n" + Use: "php",
Short: "Laravel/PHP development tools",
Long: "Manage Laravel development environment with FrankenPHP.\n\n" +
"Services orchestrated:\n" + "Services orchestrated:\n" +
" - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" + " - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" +
" - Vite dev server (port 5173)\n" + " - Vite dev server (port 5173)\n" +
" - Laravel Horizon (queue workers)\n" + " - Laravel Horizon (queue workers)\n" +
" - Laravel Reverb (WebSocket, port 8080)\n" + " - Laravel Reverb (WebSocket, port 8080)\n" +
" - Redis (port 6379)") " - Redis (port 6379)",
}
root.AddCommand(phpCmd)
// Development // Development
addPHPDevCommand(phpCmd) addPHPDevCommand(phpCmd)

View file

@ -7,43 +7,34 @@ import (
"strings" "strings"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addPHPBuildCommand(parent *clir.Command) {
var ( var (
buildType string buildType string
imageName string buildImageName string
tag string buildTag string
platform string buildPlatform string
dockerfile string buildDockerfile string
outputPath string buildOutputPath string
format string buildFormat string
template string buildTemplate string
noCache bool buildNoCache bool
) )
buildCmd := parent.NewSubCommand("build", "Build Docker or LinuxKit image") func addPHPBuildCommand(parent *cobra.Command) {
buildCmd.LongDescription("Build a production-ready container image for the PHP project.\n\n" + 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" + "By default, builds a Docker image using FrankenPHP.\n" +
"Use --type linuxkit to build a LinuxKit VM image instead.\n\n" + "Use --type linuxkit to build a LinuxKit VM image instead.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php build # Build Docker image\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 --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 # Build LinuxKit image\n" +
" core php build --type linuxkit --format iso # Build ISO image") " core php build --type linuxkit --format iso # Build ISO image",
RunE: func(cmd *cobra.Command, args []string) error {
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)
buildCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -54,20 +45,33 @@ func addPHPBuildCommand(parent *clir.Command) {
switch strings.ToLower(buildType) { switch strings.ToLower(buildType) {
case "linuxkit": case "linuxkit":
return runPHPBuildLinuxKit(ctx, cwd, linuxKitBuildOptions{ return runPHPBuildLinuxKit(ctx, cwd, linuxKitBuildOptions{
OutputPath: outputPath, OutputPath: buildOutputPath,
Format: format, Format: buildFormat,
Template: template, Template: buildTemplate,
}) })
default: default:
return runPHPBuildDocker(ctx, cwd, dockerBuildOptions{ return runPHPBuildDocker(ctx, cwd, dockerBuildOptions{
ImageName: imageName, ImageName: buildImageName,
Tag: tag, Tag: buildTag,
Platform: platform, Platform: buildPlatform,
Dockerfile: dockerfile, Dockerfile: buildDockerfile,
NoCache: noCache, NoCache: buildNoCache,
}) })
} }
}) },
}
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")
parent.AddCommand(buildCmd)
} }
type dockerBuildOptions struct { type dockerBuildOptions struct {
@ -182,34 +186,28 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu
return nil return nil
} }
func addPHPServeCommand(parent *clir.Command) {
var ( var (
imageName string serveImageName string
tag string serveTag string
containerName string serveContainerName string
port int servePort int
httpsPort int serveHTTPSPort int
detach bool serveDetach bool
envFile string serveEnvFile string
) )
serveCmd := parent.NewSubCommand("serve", "Run production container") func addPHPServeCommand(parent *cobra.Command) {
serveCmd.LongDescription("Run a production PHP container.\n\n" + 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" + "This starts the built Docker image in production mode.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php serve --name myapp # Run container\n" + " core php serve --name myapp # Run container\n" +
" core php serve --name myapp -d # Run detached\n" + " core php serve --name myapp -d # Run detached\n" +
" core php serve --name myapp --port 8080 # Custom port") " core php serve --name myapp --port 8080 # Custom port",
RunE: func(cmd *cobra.Command, args []string) error {
serveCmd.StringFlag("name", "Docker image name (required)", &imageName) imageName := serveImageName
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 == "" { if imageName == "" {
// Try to detect from current directory // Try to detect from current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
@ -228,28 +226,28 @@ func addPHPServeCommand(parent *clir.Command) {
opts := phppkg.ServeOptions{ opts := phppkg.ServeOptions{
ImageName: imageName, ImageName: imageName,
Tag: tag, Tag: serveTag,
ContainerName: containerName, ContainerName: serveContainerName,
Port: port, Port: servePort,
HTTPSPort: httpsPort, HTTPSPort: serveHTTPSPort,
Detach: detach, Detach: serveDetach,
EnvFile: envFile, EnvFile: serveEnvFile,
Output: os.Stdout, Output: os.Stdout,
} }
fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:"))
fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string { fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string {
if tag == "" { if serveTag == "" {
return "latest" return "latest"
} }
return tag return serveTag
}()) }())
effectivePort := port effectivePort := servePort
if effectivePort == 0 { if effectivePort == 0 {
effectivePort = 80 effectivePort = 80
} }
effectiveHTTPSPort := httpsPort effectiveHTTPSPort := serveHTTPSPort
if effectiveHTTPSPort == 0 { if effectiveHTTPSPort == 0 {
effectiveHTTPSPort = 443 effectiveHTTPSPort = 443
} }
@ -262,27 +260,35 @@ func addPHPServeCommand(parent *clir.Command) {
return fmt.Errorf("failed to start container: %w", err) return fmt.Errorf("failed to start container: %w", err)
} }
if !detach { if !serveDetach {
fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:")) fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:"))
} }
return nil return nil
}) },
} }
func addPHPShellCommand(parent *clir.Command) { serveCmd.Flags().StringVar(&serveImageName, "name", "", "Docker image name (required)")
shellCmd := parent.NewSubCommand("shell", "Open shell in running container") serveCmd.Flags().StringVar(&serveTag, "tag", "", "Image tag (default: latest)")
shellCmd.LongDescription("Open an interactive shell in a running PHP container.\n\n" + 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 *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" + "Examples:\n" +
" core php shell abc123 # Shell into container by ID\n" + " core php shell abc123 # Shell into container by ID\n" +
" core php shell myapp # Shell into container by name") " core php shell myapp # Shell into container by name",
Args: cobra.ExactArgs(1),
shellCmd.Action(func() error { RunE: func(cmd *cobra.Command, args []string) error {
args := shellCmd.OtherArgs()
if len(args) == 0 {
return fmt.Errorf("container ID or name is required")
}
ctx := context.Background() ctx := context.Background()
fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0]) fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0])
@ -292,5 +298,8 @@ func addPHPShellCommand(parent *clir.Command) {
} }
return nil return nil
}) },
}
parent.AddCommand(shellCmd)
} }

View file

@ -8,7 +8,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Deploy command styles // Deploy command styles
@ -23,7 +23,7 @@ var (
Foreground(lipgloss.Color("#ef4444")) // red-500 Foreground(lipgloss.Color("#ef4444")) // red-500
) )
func addPHPDeployCommands(parent *clir.Command) { func addPHPDeployCommands(parent *cobra.Command) {
// Main deploy command // Main deploy command
addPHPDeployCommand(parent) addPHPDeployCommand(parent)
@ -37,15 +37,17 @@ func addPHPDeployCommands(parent *clir.Command) {
addPHPDeployListCommand(parent) addPHPDeployListCommand(parent)
} }
func addPHPDeployCommand(parent *clir.Command) {
var ( var (
staging bool deployStaging bool
force bool deployForce bool
wait bool deployWait bool
) )
deployCmd := parent.NewSubCommand("deploy", "Deploy to Coolify") func addPHPDeployCommand(parent *cobra.Command) {
deployCmd.LongDescription("Deploy the PHP application to Coolify.\n\n" + deployCmd := &cobra.Command{
Use: "deploy",
Short: "Deploy to Coolify",
Long: "Deploy the PHP application to Coolify.\n\n" +
"Requires configuration in .env:\n" + "Requires configuration in .env:\n" +
" COOLIFY_URL=https://coolify.example.com\n" + " COOLIFY_URL=https://coolify.example.com\n" +
" COOLIFY_TOKEN=your-api-token\n" + " COOLIFY_TOKEN=your-api-token\n" +
@ -55,20 +57,15 @@ func addPHPDeployCommand(parent *clir.Command) {
" core php deploy # Deploy to production\n" + " core php deploy # Deploy to production\n" +
" core php deploy --staging # Deploy to staging\n" + " core php deploy --staging # Deploy to staging\n" +
" core php deploy --force # Force deployment\n" + " core php deploy --force # Force deployment\n" +
" core php deploy --wait # Wait for deployment to complete") " core php deploy --wait # Wait for deployment to complete",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
if staging { if deployStaging {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
@ -79,8 +76,8 @@ func addPHPDeployCommand(parent *clir.Command) {
opts := phppkg.DeployOptions{ opts := phppkg.DeployOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
Force: force, Force: deployForce,
Wait: wait, Wait: deployWait,
} }
status, err := phppkg.Deploy(ctx, opts) status, err := phppkg.Deploy(ctx, opts)
@ -90,7 +87,7 @@ func addPHPDeployCommand(parent *clir.Command) {
printDeploymentStatus(status) printDeploymentStatus(status)
if wait { if deployWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if phppkg.IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:"))
} else { } else {
@ -101,33 +98,38 @@ func addPHPDeployCommand(parent *clir.Command) {
} }
return nil return nil
}) },
}
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")
parent.AddCommand(deployCmd)
} }
func addPHPDeployStatusCommand(parent *clir.Command) {
var ( var (
staging bool deployStatusStaging bool
deploymentID string deployStatusDeploymentID string
) )
statusCmd := parent.NewSubCommand("deploy:status", "Show deployment status") func addPHPDeployStatusCommand(parent *cobra.Command) {
statusCmd.LongDescription("Show the status of a deployment.\n\n" + statusCmd := &cobra.Command{
Use: "deploy:status",
Short: "Show deployment status",
Long: "Show the status of a deployment.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php deploy:status # Latest production deployment\n" + " core php deploy:status # Latest production deployment\n" +
" core php deploy:status --staging # Latest staging deployment\n" + " core php deploy:status --staging # Latest staging deployment\n" +
" core php deploy:status --id abc123 # Specific deployment") " core php deploy:status --id abc123 # Specific deployment",
RunE: func(cmd *cobra.Command, args []string) error {
statusCmd.BoolFlag("staging", "Check staging environment", &staging)
statusCmd.StringFlag("id", "Specific deployment ID", &deploymentID)
statusCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
if staging { if deployStatusStaging {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
@ -138,7 +140,7 @@ func addPHPDeployStatusCommand(parent *clir.Command) {
opts := phppkg.StatusOptions{ opts := phppkg.StatusOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
DeploymentID: deploymentID, DeploymentID: deployStatusDeploymentID,
} }
status, err := phppkg.DeployStatus(ctx, opts) status, err := phppkg.DeployStatus(ctx, opts)
@ -149,37 +151,40 @@ func addPHPDeployStatusCommand(parent *clir.Command) {
printDeploymentStatus(status) printDeploymentStatus(status)
return nil return nil
}) },
}
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, "Check staging environment")
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", "Specific deployment ID")
parent.AddCommand(statusCmd)
} }
func addPHPDeployRollbackCommand(parent *clir.Command) {
var ( var (
staging bool rollbackStaging bool
deploymentID string rollbackDeploymentID string
wait bool rollbackWait bool
) )
rollbackCmd := parent.NewSubCommand("deploy:rollback", "Rollback to previous deployment") func addPHPDeployRollbackCommand(parent *cobra.Command) {
rollbackCmd.LongDescription("Rollback to a previous deployment.\n\n" + 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" + "If no deployment ID is specified, rolls back to the most recent\n" +
"successful deployment.\n\n" + "successful deployment.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php deploy:rollback # Rollback to previous\n" + " core php deploy:rollback # Rollback to previous\n" +
" core php deploy:rollback --staging # Rollback staging\n" + " core php deploy:rollback --staging # Rollback staging\n" +
" core php deploy:rollback --id abc123 # Rollback to specific deployment") " core php deploy:rollback --id abc123 # Rollback to specific deployment",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
if staging { if rollbackStaging {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
@ -190,8 +195,8 @@ func addPHPDeployRollbackCommand(parent *clir.Command) {
opts := phppkg.RollbackOptions{ opts := phppkg.RollbackOptions{
Dir: cwd, Dir: cwd,
Environment: env, Environment: env,
DeploymentID: deploymentID, DeploymentID: rollbackDeploymentID,
Wait: wait, Wait: rollbackWait,
} }
status, err := phppkg.Rollback(ctx, opts) status, err := phppkg.Rollback(ctx, opts)
@ -201,7 +206,7 @@ func addPHPDeployRollbackCommand(parent *clir.Command) {
printDeploymentStatus(status) printDeploymentStatus(status)
if wait { if rollbackWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if phppkg.IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:"))
} else { } else {
@ -212,36 +217,42 @@ func addPHPDeployRollbackCommand(parent *clir.Command) {
} }
return nil 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)
} }
func addPHPDeployListCommand(parent *clir.Command) {
var ( var (
staging bool deployListStaging bool
limit int deployListLimit int
) )
listCmd := parent.NewSubCommand("deploy:list", "List recent deployments") func addPHPDeployListCommand(parent *cobra.Command) {
listCmd.LongDescription("List recent deployments.\n\n" + listCmd := &cobra.Command{
Use: "deploy:list",
Short: "List recent deployments",
Long: "List recent deployments.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php deploy:list # List production deployments\n" + " core php deploy:list # List production deployments\n" +
" core php deploy:list --staging # List staging deployments\n" + " core php deploy:list --staging # List staging deployments\n" +
" core php deploy:list --limit 20 # List more deployments") " core php deploy:list --limit 20 # List more deployments",
RunE: func(cmd *cobra.Command, args []string) error {
listCmd.BoolFlag("staging", "List staging deployments", &staging)
listCmd.IntFlag("limit", "Number of deployments to list (default: 10)", &limit)
listCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
if staging { if deployListStaging {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
limit := deployListLimit
if limit == 0 { if limit == 0 {
limit = 10 limit = 10
} }
@ -265,7 +276,13 @@ func addPHPDeployListCommand(parent *clir.Command) {
} }
return nil 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) { func printDeploymentStatus(status *phppkg.DeploymentStatus) {

View file

@ -12,47 +12,51 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addPHPDevCommand(parent *clir.Command) {
var ( var (
noVite bool devNoVite bool
noHorizon bool devNoHorizon bool
noReverb bool devNoReverb bool
noRedis bool devNoRedis bool
https bool devHTTPS bool
domain string devDomain string
port int devPort int
) )
devCmd := parent.NewSubCommand("dev", "Start Laravel development environment") func addPHPDevCommand(parent *cobra.Command) {
devCmd.LongDescription("Starts all detected Laravel services.\n\n" + devCmd := &cobra.Command{
Use: "dev",
Short: "Start Laravel development environment",
Long: "Starts all detected Laravel services.\n\n" +
"Auto-detects:\n" + "Auto-detects:\n" +
" - Vite (vite.config.js/ts)\n" + " - Vite (vite.config.js/ts)\n" +
" - Horizon (config/horizon.php)\n" + " - Horizon (config/horizon.php)\n" +
" - Reverb (config/reverb.php)\n" + " - Reverb (config/reverb.php)\n" +
" - Redis (from .env)") " - Redis (from .env)",
RunE: func(cmd *cobra.Command, args []string) error {
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.Action(func() error {
return runPHPDev(phpDevOptions{ return runPHPDev(phpDevOptions{
NoVite: noVite, NoVite: devNoVite,
NoHorizon: noHorizon, NoHorizon: devNoHorizon,
NoReverb: noReverb, NoReverb: devNoReverb,
NoRedis: noRedis, NoRedis: devNoRedis,
HTTPS: https, HTTPS: devHTTPS,
Domain: domain, Domain: devDomain,
Port: port, Port: devPort,
})
}) })
},
}
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)")
parent.AddCommand(devCmd)
} }
type phpDevOptions struct { type phpDevOptions struct {
@ -181,20 +185,26 @@ shutdown:
return nil return nil
} }
func addPHPLogsCommand(parent *clir.Command) { var (
var follow bool logsFollow bool
var service string logsService string
)
logsCmd := parent.NewSubCommand("logs", "View service logs") func addPHPLogsCommand(parent *cobra.Command) {
logsCmd.LongDescription("Stream logs from Laravel services.\n\n" + logsCmd := &cobra.Command{
"Services: frankenphp, vite, horizon, reverb, redis") 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.Flags().BoolVar(&logsFollow, "follow", false, "Follow log output")
logsCmd.StringFlag("service", "Specific service (default: all)", &service) logsCmd.Flags().StringVar(&logsService, "service", "", "Specific service (default: all)")
logsCmd.Action(func() error { parent.AddCommand(logsCmd)
return runPHPLogs(service, follow)
})
} }
func runPHPLogs(service string, follow bool) error { func runPHPLogs(service string, follow bool) error {
@ -241,12 +251,16 @@ func runPHPLogs(service string, follow bool) error {
return scanner.Err() return scanner.Err()
} }
func addPHPStopCommand(parent *clir.Command) { func addPHPStopCommand(parent *cobra.Command) {
stopCmd := parent.NewSubCommand("stop", "Stop all Laravel services") stopCmd := &cobra.Command{
Use: "stop",
stopCmd.Action(func() error { Short: "Stop all Laravel services",
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStop() return runPHPStop()
}) },
}
parent.AddCommand(stopCmd)
} }
func runPHPStop() error { func runPHPStop() error {
@ -268,12 +282,16 @@ func runPHPStop() error {
return nil return nil
} }
func addPHPStatusCommand(parent *clir.Command) { func addPHPStatusCommand(parent *cobra.Command) {
statusCmd := parent.NewSubCommand("status", "Show service status") statusCmd := &cobra.Command{
Use: "status",
statusCmd.Action(func() error { Short: "Show service status",
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStatus() return runPHPStatus()
}) },
}
parent.AddCommand(statusCmd)
} }
func runPHPStatus() error { func runPHPStatus() error {
@ -325,16 +343,20 @@ func runPHPStatus() error {
return nil return nil
} }
func addPHPSSLCommand(parent *clir.Command) { var sslDomain string
var domain 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 { parent.AddCommand(sslCmd)
return runPHPSSL(domain)
})
} }
func runPHPSSL(domain string) error { func runPHPSSL(domain string) error {

View file

@ -5,19 +5,23 @@ import (
"os" "os"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addPHPPackagesCommands(parent *clir.Command) { func addPHPPackagesCommands(parent *cobra.Command) {
packagesCmd := parent.NewSubCommand("packages", "Manage local PHP packages") packagesCmd := &cobra.Command{
packagesCmd.LongDescription("Link and manage local PHP packages for development.\n\n" + 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" + "Similar to npm link, this adds path repositories to composer.json\n" +
"for developing packages alongside your project.\n\n" + "for developing packages alongside your project.\n\n" +
"Commands:\n" + "Commands:\n" +
" link - Link local packages by path\n" + " link - Link local packages by path\n" +
" unlink - Unlink packages by name\n" + " unlink - Unlink packages by name\n" +
" update - Update linked packages\n" + " update - Update linked packages\n" +
" list - List linked packages") " list - List linked packages",
}
parent.AddCommand(packagesCmd)
addPHPPackagesLinkCommand(packagesCmd) addPHPPackagesLinkCommand(packagesCmd)
addPHPPackagesUnlinkCommand(packagesCmd) addPHPPackagesUnlinkCommand(packagesCmd)
@ -25,21 +29,18 @@ func addPHPPackagesCommands(parent *clir.Command) {
addPHPPackagesListCommand(packagesCmd) addPHPPackagesListCommand(packagesCmd)
} }
func addPHPPackagesLinkCommand(parent *clir.Command) { func addPHPPackagesLinkCommand(parent *cobra.Command) {
linkCmd := parent.NewSubCommand("link", "Link local packages") linkCmd := &cobra.Command{
linkCmd.LongDescription("Link local PHP packages for development.\n\n" + 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" + "Adds path repositories to composer.json with symlink enabled.\n" +
"The package name is auto-detected from each path's composer.json.\n\n" + "The package name is auto-detected from each path's composer.json.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php packages link ../my-package\n" + " core php packages link ../my-package\n" +
" core php packages link ../pkg-a ../pkg-b") " core php packages link ../pkg-a ../pkg-b",
Args: cobra.MinimumNArgs(1),
linkCmd.Action(func() error { RunE: func(cmd *cobra.Command, args []string) error {
args := linkCmd.OtherArgs()
if len(args) == 0 {
return fmt.Errorf("at least one package path is required")
}
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -53,23 +54,23 @@ func addPHPPackagesLinkCommand(parent *clir.Command) {
fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:")) fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:"))
return nil return nil
}) },
} }
func addPHPPackagesUnlinkCommand(parent *clir.Command) { parent.AddCommand(linkCmd)
unlinkCmd := parent.NewSubCommand("unlink", "Unlink packages") }
unlinkCmd.LongDescription("Remove linked packages from composer.json.\n\n" +
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" + "Removes path repositories by package name.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php packages unlink vendor/my-package\n" + " core php packages unlink vendor/my-package\n" +
" core php packages unlink vendor/pkg-a vendor/pkg-b") " core php packages unlink vendor/pkg-a vendor/pkg-b",
Args: cobra.MinimumNArgs(1),
unlinkCmd.Action(func() error { RunE: func(cmd *cobra.Command, args []string) error {
args := unlinkCmd.OtherArgs()
if len(args) == 0 {
return fmt.Errorf("at least one package name is required")
}
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -83,25 +84,27 @@ func addPHPPackagesUnlinkCommand(parent *clir.Command) {
fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:")) fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:"))
return nil return nil
}) },
} }
func addPHPPackagesUpdateCommand(parent *clir.Command) { parent.AddCommand(unlinkCmd)
updateCmd := parent.NewSubCommand("update", "Update linked packages") }
updateCmd.LongDescription("Run composer update for linked packages.\n\n" +
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" + "If no packages specified, updates all packages.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php packages update\n" + " core php packages update\n" +
" core php packages update vendor/my-package") " core php packages update vendor/my-package",
RunE: func(cmd *cobra.Command, args []string) error {
updateCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
args := updateCmd.OtherArgs()
fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:"))
if err := phppkg.UpdatePackages(cwd, args); err != nil { if err := phppkg.UpdatePackages(cwd, args); err != nil {
@ -110,15 +113,19 @@ func addPHPPackagesUpdateCommand(parent *clir.Command) {
fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:")) fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:"))
return nil return nil
}) },
} }
func addPHPPackagesListCommand(parent *clir.Command) { parent.AddCommand(updateCmd)
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 { 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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -153,5 +160,8 @@ func addPHPPackagesListCommand(parent *clir.Command) {
} }
return nil return nil
}) },
}
parent.AddCommand(listCmd)
} }

View file

@ -11,32 +11,28 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
func addPHPTestCommand(parent *clir.Command) {
var ( var (
parallel bool testParallel bool
coverage bool testCoverage bool
filter string testFilter string
group string testGroup string
) )
testCmd := parent.NewSubCommand("test", "Run PHP tests (PHPUnit/Pest)") func addPHPTestCommand(parent *cobra.Command) {
testCmd.LongDescription("Run PHP tests using PHPUnit or Pest.\n\n" + 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" + "Auto-detects Pest if tests/Pest.php exists, otherwise uses PHPUnit.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php test # Run all tests\n" + " core php test # Run all tests\n" +
" core php test --parallel # Run tests in parallel\n" + " core php test --parallel # Run tests in parallel\n" +
" core php test --coverage # Run with coverage\n" + " core php test --coverage # Run with coverage\n" +
" core php test --filter UserTest # Filter by test name") " core php test --filter UserTest # Filter by test name",
RunE: func(cmd *cobra.Command, args []string) error {
testCmd.BoolFlag("parallel", "Run tests in parallel", &parallel)
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -54,14 +50,14 @@ func addPHPTestCommand(parent *clir.Command) {
opts := phppkg.TestOptions{ opts := phppkg.TestOptions{
Dir: cwd, Dir: cwd,
Filter: filter, Filter: testFilter,
Parallel: parallel, Parallel: testParallel,
Coverage: coverage, Coverage: testCoverage,
Output: os.Stdout, Output: os.Stdout,
} }
if group != "" { if testGroup != "" {
opts.Groups = []string{group} opts.Groups = []string{testGroup}
} }
if err := phppkg.RunTests(ctx, opts); err != nil { if err := phppkg.RunTests(ctx, opts); err != nil {
@ -69,26 +65,32 @@ func addPHPTestCommand(parent *clir.Command) {
} }
return nil 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 addPHPFmtCommand(parent *clir.Command) {
var ( var (
fix bool fmtFix bool
diff bool fmtDiff bool
) )
fmtCmd := parent.NewSubCommand("fmt", "Format PHP code with Laravel Pint") func addPHPFmtCommand(parent *cobra.Command) {
fmtCmd.LongDescription("Format PHP code using Laravel Pint.\n\n" + fmtCmd := &cobra.Command{
Use: "fmt [paths...]",
Short: "Format PHP code with Laravel Pint",
Long: "Format PHP code using Laravel Pint.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php fmt # Check formatting (dry-run)\n" + " core php fmt # Check formatting (dry-run)\n" +
" core php fmt --fix # Auto-fix formatting issues\n" + " core php fmt --fix # Auto-fix formatting issues\n" +
" core php fmt --diff # Show diff of changes") " core php fmt --diff # Show diff of changes",
RunE: func(cmd *cobra.Command, args []string) error {
fmtCmd.BoolFlag("fix", "Auto-fix formatting issues", &fix)
fmtCmd.BoolFlag("diff", "Show diff of changes", &diff)
fmtCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -105,7 +107,7 @@ func addPHPFmtCommand(parent *clir.Command) {
} }
action := "Checking" action := "Checking"
if fix { if fmtFix {
action = "Formatting" action = "Formatting"
} }
fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter) fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter)
@ -114,51 +116,55 @@ func addPHPFmtCommand(parent *clir.Command) {
opts := phppkg.FormatOptions{ opts := phppkg.FormatOptions{
Dir: cwd, Dir: cwd,
Fix: fix, Fix: fmtFix,
Diff: diff, Diff: fmtDiff,
Output: os.Stdout, Output: os.Stdout,
} }
// Get any additional paths from args // Get any additional paths from args
if args := fmtCmd.OtherArgs(); len(args) > 0 { if len(args) > 0 {
opts.Paths = args opts.Paths = args
} }
if err := phppkg.Format(ctx, opts); err != nil { if err := phppkg.Format(ctx, opts); err != nil {
if fix { if fmtFix {
return fmt.Errorf("formatting failed: %w", err) return fmt.Errorf("formatting failed: %w", err)
} }
return fmt.Errorf("formatting issues found: %w", err) return fmt.Errorf("formatting issues found: %w", err)
} }
if fix { if fmtFix {
fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:"))
} else { } else {
fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:"))
} }
return nil 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)
} }
func addPHPAnalyseCommand(parent *clir.Command) {
var ( var (
level int analyseLevel int
memory string analyseMemory string
) )
analyseCmd := parent.NewSubCommand("analyse", "Run PHPStan static analysis") func addPHPAnalyseCommand(parent *cobra.Command) {
analyseCmd.LongDescription("Run PHPStan or Larastan static analysis.\n\n" + 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" + "Auto-detects Larastan if installed, otherwise uses PHPStan.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php analyse # Run analysis\n" + " core php analyse # Run analysis\n" +
" core php analyse --level 9 # Run at max strictness\n" + " core php analyse --level 9 # Run at max strictness\n" +
" core php analyse --memory 2G # Increase memory limit") " core php analyse --memory 2G # Increase memory limit",
RunE: func(cmd *cobra.Command, args []string) error {
analyseCmd.IntFlag("level", "PHPStan analysis level (0-9)", &level)
analyseCmd.StringFlag("memory", "Memory limit (e.g., 2G)", &memory)
analyseCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -180,13 +186,13 @@ func addPHPAnalyseCommand(parent *clir.Command) {
opts := phppkg.AnalyseOptions{ opts := phppkg.AnalyseOptions{
Dir: cwd, Dir: cwd,
Level: level, Level: analyseLevel,
Memory: memory, Memory: analyseMemory,
Output: os.Stdout, Output: os.Stdout,
} }
// Get any additional paths from args // Get any additional paths from args
if args := analyseCmd.OtherArgs(); len(args) > 0 { if len(args) > 0 {
opts.Paths = args opts.Paths = args
} }
@ -196,37 +202,39 @@ func addPHPAnalyseCommand(parent *clir.Command) {
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:"))
return nil 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 // New QA Commands
// ============================================================================= // =============================================================================
func addPHPPsalmCommand(parent *clir.Command) {
var ( var (
level int psalmLevel int
fix bool psalmFix bool
baseline bool psalmBaseline bool
showInfo bool psalmShowInfo bool
) )
psalmCmd := parent.NewSubCommand("psalm", "Run Psalm static analysis") func addPHPPsalmCommand(parent *cobra.Command) {
psalmCmd.LongDescription("Run Psalm deep static analysis with Laravel plugin support.\n\n" + 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" + "Psalm provides deeper type inference than PHPStan and catches\n" +
"different classes of bugs. Both should be run for best coverage.\n\n" + "different classes of bugs. Both should be run for best coverage.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php psalm # Run analysis\n" + " core php psalm # Run analysis\n" +
" core php psalm --fix # Auto-fix issues where possible\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 --level 3 # Run at specific level (1-8)\n" +
" core php psalm --baseline # Generate baseline file") " core php psalm --baseline # Generate baseline file",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -246,7 +254,7 @@ func addPHPPsalmCommand(parent *clir.Command) {
} }
action := "Analysing" action := "Analysing"
if fix { if psalmFix {
action = "Analysing and fixing" action = "Analysing and fixing"
} }
fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action) fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action)
@ -255,10 +263,10 @@ func addPHPPsalmCommand(parent *clir.Command) {
opts := phppkg.PsalmOptions{ opts := phppkg.PsalmOptions{
Dir: cwd, Dir: cwd,
Level: level, Level: psalmLevel,
Fix: fix, Fix: psalmFix,
Baseline: baseline, Baseline: psalmBaseline,
ShowInfo: showInfo, ShowInfo: psalmShowInfo,
Output: os.Stdout, Output: os.Stdout,
} }
@ -268,27 +276,33 @@ func addPHPPsalmCommand(parent *clir.Command) {
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:"))
return nil return nil
}) },
}
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")
parent.AddCommand(psalmCmd)
} }
func addPHPAuditCommand(parent *clir.Command) {
var ( var (
jsonOutput bool auditJSONOutput bool
fix bool auditFix bool
) )
auditCmd := parent.NewSubCommand("audit", "Security audit for dependencies") func addPHPAuditCommand(parent *cobra.Command) {
auditCmd.LongDescription("Check PHP and JavaScript dependencies for known vulnerabilities.\n\n" + 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" + "Runs composer audit and npm audit (if package.json exists).\n\n" +
"Examples:\n" + "Examples:\n" +
" core php audit # Check all dependencies\n" + " core php audit # Check all dependencies\n" +
" core php audit --json # Output as JSON\n" + " core php audit --json # Output as JSON\n" +
" core php audit --fix # Auto-fix where possible (npm only)") " core php audit --fix # Auto-fix where possible (npm only)",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -304,8 +318,8 @@ func addPHPAuditCommand(parent *clir.Command) {
results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{ results, err := phppkg.RunAudit(ctx, phppkg.AuditOptions{
Dir: cwd, Dir: cwd,
JSON: jsonOutput, JSON: auditJSONOutput,
Fix: fix, Fix: auditFix,
Output: os.Stdout, Output: os.Stdout,
}) })
if err != nil { if err != nil {
@ -360,32 +374,34 @@ func addPHPAuditCommand(parent *clir.Command) {
fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:")) fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:"))
return nil 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)
} }
func addPHPSecurityCommand(parent *clir.Command) {
var ( var (
severity string securitySeverity string
jsonOutput bool securityJSONOutput bool
sarif bool securitySarif bool
url string securityURL string
) )
securityCmd := parent.NewSubCommand("security", "Security vulnerability scanning") func addPHPSecurityCommand(parent *cobra.Command) {
securityCmd.LongDescription("Scan for security vulnerabilities in configuration and code.\n\n" + 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" + "Checks environment config, file permissions, code patterns,\n" +
"and runs security-focused static analysis.\n\n" + "and runs security-focused static analysis.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php security # Run all checks\n" + " core php security # Run all checks\n" +
" core php security --severity=high # Only high+ severity\n" + " core php security --severity=high # Only high+ severity\n" +
" core php security --json # JSON output") " core php security --json # JSON output",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -401,10 +417,10 @@ func addPHPSecurityCommand(parent *clir.Command) {
result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{ result, err := phppkg.RunSecurityChecks(ctx, phppkg.SecurityOptions{
Dir: cwd, Dir: cwd,
Severity: severity, Severity: securitySeverity,
JSON: jsonOutput, JSON: securityJSONOutput,
SARIF: sarif, SARIF: securitySarif,
URL: url, URL: securityURL,
Output: os.Stdout, Output: os.Stdout,
}) })
if err != nil { if err != nil {
@ -461,18 +477,28 @@ func addPHPSecurityCommand(parent *clir.Command) {
} }
return nil 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)
} }
func addPHPQACommand(parent *clir.Command) {
var ( var (
quick bool qaQuick bool
full bool qaFull bool
fix bool qaFix bool
) )
qaCmd := parent.NewSubCommand("qa", "Run full QA pipeline") func addPHPQACommand(parent *cobra.Command) {
qaCmd.LongDescription("Run the complete quality assurance pipeline.\n\n" + qaCmd := &cobra.Command{
Use: "qa",
Short: "Run full QA pipeline",
Long: "Run the complete quality assurance pipeline.\n\n" +
"Stages:\n" + "Stages:\n" +
" quick: Security audit, code style, PHPStan\n" + " quick: Security audit, code style, PHPStan\n" +
" standard: Psalm, tests\n" + " standard: Psalm, tests\n" +
@ -481,13 +507,8 @@ func addPHPQACommand(parent *clir.Command) {
" core php qa # Run quick + standard stages\n" + " core php qa # Run quick + standard stages\n" +
" core php qa --quick # Only quick checks\n" + " core php qa --quick # Only quick checks\n" +
" core php qa --full # All stages including slow ones\n" + " core php qa --full # All stages including slow ones\n" +
" core php qa --fix # Auto-fix where possible") " core php qa --fix # Auto-fix where possible",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -500,9 +521,9 @@ func addPHPQACommand(parent *clir.Command) {
// Determine stages // Determine stages
opts := phppkg.QAOptions{ opts := phppkg.QAOptions{
Dir: cwd, Dir: cwd,
Quick: quick, Quick: qaQuick,
Full: full, Full: qaFull,
Fix: fix, Fix: qaFix,
} }
stages := phppkg.GetQAStages(opts) stages := phppkg.GetQAStages(opts)
@ -527,7 +548,7 @@ func addPHPQACommand(parent *clir.Command) {
} }
for _, checkName := range checks { for _, checkName := range checks {
result := runQACheck(ctx, cwd, checkName, fix) result := runQACheck(ctx, cwd, checkName, qaFix)
result.Stage = stage result.Stage = stage
results = append(results, result) results = append(results, result)
@ -565,7 +586,7 @@ func addPHPQACommand(parent *clir.Command) {
// Show what needs fixing // Show what needs fixing
fmt.Printf("%s\n", dimStyle.Render("To fix:")) fmt.Printf("%s\n", dimStyle.Render("To fix:"))
for _, check := range failedChecks { for _, check := range failedChecks {
fixCmd := getQAFixCommand(check.Name, fix) fixCmd := getQAFixCommand(check.Name, qaFix)
issue := check.Output issue := check.Output
if issue == "" { if issue == "" {
issue = "issues found" issue = "issues found"
@ -577,7 +598,14 @@ func addPHPQACommand(parent *clir.Command) {
} }
return fmt.Errorf("QA pipeline failed") 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 { func getQAFixCommand(checkName string, fixEnabled bool) string {
@ -677,27 +705,24 @@ func runQACheck(ctx context.Context, dir string, checkName string, fix bool) php
return result return result
} }
func addPHPRectorCommand(parent *clir.Command) {
var ( var (
fix bool rectorFix bool
diff bool rectorDiff bool
clearCache bool rectorClearCache bool
) )
rectorCmd := parent.NewSubCommand("rector", "Automated code refactoring") func addPHPRectorCommand(parent *cobra.Command) {
rectorCmd.LongDescription("Run Rector for automated code improvements and PHP upgrades.\n\n" + 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" + "Rector can automatically upgrade PHP syntax, improve code quality,\n" +
"and apply framework-specific refactorings.\n\n" + "and apply framework-specific refactorings.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php rector # Dry-run (show changes)\n" + " core php rector # Dry-run (show changes)\n" +
" core php rector --fix # Apply changes\n" + " core php rector --fix # Apply changes\n" +
" core php rector --diff # Show detailed diff") " core php rector --diff # Show detailed diff",
RunE: func(cmd *cobra.Command, args []string) error {
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() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -716,7 +741,7 @@ func addPHPRectorCommand(parent *clir.Command) {
} }
action := "Analysing" action := "Analysing"
if fix { if rectorFix {
action = "Refactoring" action = "Refactoring"
} }
fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action) fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action)
@ -725,14 +750,14 @@ func addPHPRectorCommand(parent *clir.Command) {
opts := phppkg.RectorOptions{ opts := phppkg.RectorOptions{
Dir: cwd, Dir: cwd,
Fix: fix, Fix: rectorFix,
Diff: diff, Diff: rectorDiff,
ClearCache: clearCache, ClearCache: rectorClearCache,
Output: os.Stdout, Output: os.Stdout,
} }
if err := phppkg.RunRector(ctx, opts); err != nil { if err := phppkg.RunRector(ctx, opts); err != nil {
if fix { if rectorFix {
return fmt.Errorf("rector failed: %w", err) return fmt.Errorf("rector failed: %w", err)
} }
// Dry-run returns non-zero if changes would be made // Dry-run returns non-zero if changes would be made
@ -740,41 +765,43 @@ func addPHPRectorCommand(parent *clir.Command) {
return nil return nil
} }
if fix { if rectorFix {
fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:"))
} else { } else {
fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:")) fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:"))
} }
return nil 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 ( var (
minMSI int infectionMinMSI int
minCoveredMSI int infectionMinCoveredMSI int
threads int infectionThreads int
filter string infectionFilter string
onlyCovered bool infectionOnlyCovered bool
) )
infectionCmd := parent.NewSubCommand("infection", "Mutation testing for test quality") func addPHPInfectionCommand(parent *cobra.Command) {
infectionCmd.LongDescription("Run Infection mutation testing to measure test suite quality.\n\n" + 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" + "Mutation testing modifies your code and checks if tests catch\n" +
"the changes. High mutation score = high quality tests.\n\n" + "the changes. High mutation score = high quality tests.\n\n" +
"Warning: This can be slow on large codebases.\n\n" + "Warning: This can be slow on large codebases.\n\n" +
"Examples:\n" + "Examples:\n" +
" core php infection # Run mutation testing\n" + " core php infection # Run mutation testing\n" +
" core php infection --min-msi=70 # Require 70% mutation score\n" + " core php infection --min-msi=70 # Require 70% mutation score\n" +
" core php infection --filter=User # Only test User* files") " core php infection --filter=User # Only test User* files",
RunE: func(cmd *cobra.Command, args []string) error {
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)
infectionCmd.Action(func() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
@ -798,11 +825,11 @@ func addPHPInfectionCommand(parent *clir.Command) {
opts := phppkg.InfectionOptions{ opts := phppkg.InfectionOptions{
Dir: cwd, Dir: cwd,
MinMSI: minMSI, MinMSI: infectionMinMSI,
MinCoveredMSI: minCoveredMSI, MinCoveredMSI: infectionMinCoveredMSI,
Threads: threads, Threads: infectionThreads,
Filter: filter, Filter: infectionFilter,
OnlyCovered: onlyCovered, OnlyCovered: infectionOnlyCovered,
Output: os.Stdout, Output: os.Stdout,
} }
@ -812,7 +839,16 @@ func addPHPInfectionCommand(parent *clir.Command) {
fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:")) fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:"))
return nil return nil
}) },
}
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")
parent.AddCommand(infectionCmd)
} }
func getSeverityStyle(severity string) lipgloss.Style { func getSeverityStyle(severity string) lipgloss.Style {

View file

@ -11,9 +11,9 @@
// .core/cache/ within the workspace directory. // .core/cache/ within the workspace directory.
package pkg package pkg
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'pkg' command and all subcommands. // AddCommands registers the 'pkg' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddPkgCommands(app) AddPkgCommands(root)
} }

View file

@ -3,7 +3,7 @@ package pkg
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style and utility aliases // Style and utility aliases
@ -17,16 +17,20 @@ var (
) )
// AddPkgCommands adds the 'pkg' command and subcommands for package management. // AddPkgCommands adds the 'pkg' command and subcommands for package management.
func AddPkgCommands(parent *clir.Cli) { func AddPkgCommands(root *cobra.Command) {
pkgCmd := parent.NewSubCommand("pkg", "Package management for core-* repos") pkgCmd := &cobra.Command{
pkgCmd.LongDescription("Manage host-uk/core-* packages and repositories.\n\n" + Use: "pkg",
Short: "Package management for core-* repos",
Long: "Manage host-uk/core-* packages and repositories.\n\n" +
"Commands:\n" + "Commands:\n" +
" search Search GitHub for packages\n" + " search Search GitHub for packages\n" +
" install Clone a package from GitHub\n" + " install Clone a package from GitHub\n" +
" list List installed packages\n" + " list List installed packages\n" +
" update Update installed packages\n" + " update Update installed packages\n" +
" outdated Check for outdated packages") " outdated Check for outdated packages",
}
root.AddCommand(pkgCmd)
addPkgSearchCommand(pkgCmd) addPkgSearchCommand(pkgCmd)
addPkgInstallCommand(pkgCmd) addPkgInstallCommand(pkgCmd)
addPkgListCommand(pkgCmd) addPkgListCommand(pkgCmd)

View file

@ -8,31 +8,36 @@ import (
"strings" "strings"
"github.com/host-uk/core/pkg/repos" "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. // addPkgInstallCommand adds the 'pkg install' command.
func addPkgInstallCommand(parent *clir.Command) { func addPkgInstallCommand(parent *cobra.Command) {
var targetDir string installCmd := &cobra.Command{
var addToRegistry bool Use: "install <org/repo>",
Short: "Clone a package from GitHub",
installCmd := parent.NewSubCommand("install", "Clone a package from GitHub") Long: "Clones a repository from GitHub.\n\n" +
installCmd.LongDescription("Clones a repository from GitHub.\n\n" +
"Examples:\n" + "Examples:\n" +
" core pkg install host-uk/core-php\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-tenant --dir ./packages\n" +
" core pkg install host-uk/core-admin --add") " core pkg install host-uk/core-admin --add",
RunE: func(cmd *cobra.Command, args []string) error {
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 { if len(args) == 0 {
return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)") return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)")
} }
return runPkgInstall(args[0], targetDir, addToRegistry) return runPkgInstall(args[0], installTargetDir, installAddToReg)
}) },
}
installCmd.Flags().StringVar(&installTargetDir, "dir", "", "Target directory (default: ./packages or current dir)")
installCmd.Flags().BoolVar(&installAddToReg, "add", false, "Add to repos.yaml registry")
parent.AddCommand(installCmd)
} }
func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error { func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {

View file

@ -8,20 +8,24 @@ import (
"strings" "strings"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// addPkgListCommand adds the 'pkg list' command. // addPkgListCommand adds the 'pkg list' command.
func addPkgListCommand(parent *clir.Command) { func addPkgListCommand(parent *cobra.Command) {
listCmd := parent.NewSubCommand("list", "List installed packages") listCmd := &cobra.Command{
listCmd.LongDescription("Lists all packages in the current workspace.\n\n" + 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" + "Reads from repos.yaml or scans for git repositories.\n\n" +
"Examples:\n" + "Examples:\n" +
" core pkg list") " core pkg list",
RunE: func(cmd *cobra.Command, args []string) error {
listCmd.Action(func() error {
return runPkgList() return runPkgList()
}) },
}
parent.AddCommand(listCmd)
} }
func runPkgList() error { func runPkgList() error {
@ -89,25 +93,28 @@ func runPkgList() error {
return nil return nil
} }
// addPkgUpdateCommand adds the 'pkg update' command. var updateAll bool
func addPkgUpdateCommand(parent *clir.Command) {
var all bool
updateCmd := parent.NewSubCommand("update", "Update installed packages") // addPkgUpdateCommand adds the 'pkg update' command.
updateCmd.LongDescription("Pulls latest changes for installed packages.\n\n" + 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" + "Examples:\n" +
" core pkg update core-php # Update specific package\n" + " core pkg update core-php # Update specific package\n" +
" core pkg update --all # Update all packages") " core pkg update --all # Update all packages",
RunE: func(cmd *cobra.Command, args []string) error {
updateCmd.BoolFlag("all", "Update all packages", &all) if !updateAll && len(args) == 0 {
updateCmd.Action(func() error {
args := updateCmd.OtherArgs()
if !all && len(args) == 0 {
return fmt.Errorf("specify package name or use --all") return fmt.Errorf("specify package name or use --all")
} }
return runPkgUpdate(args, all) return runPkgUpdate(args, updateAll)
}) },
}
updateCmd.Flags().BoolVar(&updateAll, "all", false, "Update all packages")
parent.AddCommand(updateCmd)
} }
func runPkgUpdate(packages []string, all bool) error { func runPkgUpdate(packages []string, all bool) error {
@ -177,15 +184,19 @@ func runPkgUpdate(packages []string, all bool) error {
} }
// addPkgOutdatedCommand adds the 'pkg outdated' command. // addPkgOutdatedCommand adds the 'pkg outdated' command.
func addPkgOutdatedCommand(parent *clir.Command) { func addPkgOutdatedCommand(parent *cobra.Command) {
outdatedCmd := parent.NewSubCommand("outdated", "Check for outdated packages") outdatedCmd := &cobra.Command{
outdatedCmd.LongDescription("Checks which packages have unpulled commits.\n\n" + Use: "outdated",
Short: "Check for outdated packages",
Long: "Checks which packages have unpulled commits.\n\n" +
"Examples:\n" + "Examples:\n" +
" core pkg outdated") " core pkg outdated",
RunE: func(cmd *cobra.Command, args []string) error {
outdatedCmd.Action(func() error {
return runPkgOutdated() return runPkgOutdated()
}) },
}
parent.AddCommand(outdatedCmd)
} }
func runPkgOutdated() error { func runPkgOutdated() error {

View file

@ -12,33 +12,33 @@ import (
"github.com/host-uk/core/pkg/cache" "github.com/host-uk/core/pkg/cache"
"github.com/host-uk/core/pkg/repos" "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. // addPkgSearchCommand adds the 'pkg search' command.
func addPkgSearchCommand(parent *clir.Command) { func addPkgSearchCommand(parent *cobra.Command) {
var org string searchCmd := &cobra.Command{
var pattern string Use: "search",
var repoType string Short: "Search GitHub for packages",
var limit int Long: "Searches GitHub for repositories matching a pattern.\n" +
var refresh bool
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" + "Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" +
"Examples:\n" + "Examples:\n" +
" core pkg search # List all host-uk repos\n" + " core pkg search # List all host-uk repos\n" +
" core pkg search --pattern 'core-*' # Search for core-* repos\n" + " core pkg search --pattern 'core-*' # Search for core-* repos\n" +
" core pkg search --org mycompany # Search different org\n" + " core pkg search --org mycompany # Search different org\n" +
" core pkg search --refresh # Bypass cache") " core pkg search --refresh # Bypass cache",
RunE: func(cmd *cobra.Command, args []string) error {
searchCmd.StringFlag("org", "GitHub organization (default: host-uk)", &org) org := searchOrg
searchCmd.StringFlag("pattern", "Repo name pattern (* for wildcard)", &pattern) pattern := searchPattern
searchCmd.StringFlag("type", "Filter by type in name (mod, services, plug, website)", &repoType) limit := searchLimit
searchCmd.IntFlag("limit", "Max results (default 50)", &limit)
searchCmd.BoolFlag("refresh", "Bypass cache and fetch fresh data", &refresh)
searchCmd.Action(func() error {
if org == "" { if org == "" {
org = "host-uk" org = "host-uk"
} }
@ -48,8 +48,17 @@ func addPkgSearchCommand(parent *clir.Command) {
if limit == 0 { if limit == 0 {
limit = 50 limit = 50
} }
return runPkgSearch(org, pattern, repoType, limit, refresh) return runPkgSearch(org, pattern, searchType, limit, searchRefresh)
}) },
}
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")
parent.AddCommand(searchCmd)
} }
type ghRepo struct { type ghRepo struct {

View file

@ -7,9 +7,9 @@
// Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk // Configuration via .core/sdk.yaml. For SDK generation, use: core build sdk
package sdk package sdk
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'sdk' command and all subcommands. // AddCommands registers the 'sdk' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddSDKCommand(app) root.AddCommand(sdkCmd)
} }

View file

@ -7,7 +7,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
sdkpkg "github.com/host-uk/core/pkg/sdk" sdkpkg "github.com/host-uk/core/pkg/sdk"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
var ( var (
@ -27,30 +27,49 @@ var (
Foreground(lipgloss.Color("#6b7280")) Foreground(lipgloss.Color("#6b7280"))
) )
// AddSDKCommand adds the sdk command and its subcommands. var sdkCmd = &cobra.Command{
func AddSDKCommand(app *clir.Cli) { Use: "sdk",
sdkCmd := app.NewSubCommand("sdk", "SDK validation and API compatibility tools") Short: "SDK validation and API compatibility tools",
sdkCmd.LongDescription("Tools for validating OpenAPI specs and checking API compatibility.\n" + Long: `Tools for validating OpenAPI specs and checking API compatibility.
"To generate SDKs, use: core build sdk\n\n" + To generate SDKs, use: core build sdk
"Commands:\n" +
" diff Check for breaking API changes\n" +
" validate Validate OpenAPI spec syntax")
// sdk diff Commands:
diffCmd := sdkCmd.NewSubCommand("diff", "Check for breaking API changes") diff Check for breaking API changes
var basePath, specPath string validate Validate OpenAPI spec syntax`,
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)
})
// sdk validate var diffBasePath string
validateCmd := sdkCmd.NewSubCommand("validate", "Validate OpenAPI spec") var diffSpecPath string
validateCmd.StringFlag("spec", "Path to OpenAPI spec file", &specPath)
validateCmd.Action(func() error { var sdkDiffCmd = &cobra.Command{
return runSDKValidate(specPath) 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 { func runSDKDiff(basePath, specPath string) error {

View file

@ -23,9 +23,9 @@
// Uses gh CLI with HTTPS when authenticated, falls back to SSH. // Uses gh CLI with HTTPS when authenticated, falls back to SSH.
package setup package setup
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'setup' command and all subcommands. // AddCommands registers the 'setup' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddSetupCommand(app) AddSetupCommand(root)
} }

View file

@ -3,7 +3,7 @@ package setup
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared package // Style aliases from shared package
@ -21,34 +21,46 @@ const (
devopsReposYaml = "repos.yaml" devopsReposYaml = "repos.yaml"
) )
// AddSetupCommand adds the 'setup' command to the given parent command. // Setup command flags
func AddSetupCommand(parent *clir.Cli) { var (
var registryPath string registryPath string
var only string only string
var dryRun bool dryRun bool
var all bool all bool
var name string name string
var build bool build bool
)
setupCmd := parent.NewSubCommand("setup", "Bootstrap workspace or clone packages from registry") var setupCmd = &cobra.Command{
setupCmd.LongDescription("Sets up a development workspace.\n\n" + Use: "setup",
"REGISTRY MODE (repos.yaml exists):\n" + Short: "Bootstrap workspace or clone packages from registry",
" Clones all repositories defined in repos.yaml into packages/.\n" + Long: `Sets up a development workspace.
" 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.")
setupCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath) REGISTRY MODE (repos.yaml exists):
setupCmd.StringFlag("only", "Only clone repos of these types (comma-separated: foundation,module,product)", &only) Clones all repositories defined in repos.yaml into packages/.
setupCmd.BoolFlag("dry-run", "Show what would be cloned without cloning", &dryRun) Skips repos that already exist. Use --only to filter by type.
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)
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) return runSetupOrchestrator(registryPath, only, dryRun, all, name, build)
}) },
}
func init() {
setupCmd.Flags().StringVar(&registryPath, "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)
} }

View file

@ -11,9 +11,9 @@
// Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json // Flags: --verbose, --coverage, --short, --pkg, --run, --race, --json
package testcmd package testcmd
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'test' command and all subcommands. // AddCommands registers the 'test' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddTestCommand(app) root.AddCommand(testCmd)
} }

View file

@ -6,7 +6,7 @@ package testcmd
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared // Style aliases from shared
@ -30,38 +30,44 @@ var (
Foreground(lipgloss.Color("#ef4444")) // red-500 Foreground(lipgloss.Color("#ef4444")) // red-500
) )
// AddTestCommand adds the 'test' command to the given parent command. // Flag variables for test command
func AddTestCommand(parent *clir.Cli) { var (
var verbose bool testVerbose bool
var coverage bool testCoverage bool
var short bool testShort bool
var pkg string testPkg string
var run string testRun string
var race bool testRace bool
var json bool testJSON bool
)
testCmd := parent.NewSubCommand("test", "Run tests with coverage") var testCmd = &cobra.Command{
testCmd.LongDescription("Runs Go tests with coverage reporting.\n\n" + Use: "test",
"Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.\n\n" + Short: "Run tests with coverage",
"Examples:\n" + Long: `Runs Go tests with coverage reporting.
" 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")
testCmd.BoolFlag("verbose", "Show test output as it runs (-v)", &verbose) Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.
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)
testCmd.Action(func() error { Examples:
return runTest(verbose, coverage, short, pkg, run, race, json) 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")
} }

View file

@ -12,9 +12,9 @@
// Templates are built from YAML definitions and can include variables. // Templates are built from YAML definitions and can include variables.
package vm package vm
import "github.com/leaanthony/clir" import "github.com/spf13/cobra"
// AddCommands registers the 'vm' command and all subcommands. // AddCommands registers the 'vm' command and all subcommands.
func AddCommands(app *clir.Cli) { func AddCommands(root *cobra.Command) {
AddVMCommands(app) AddVMCommands(root)
} }

View file

@ -10,23 +10,25 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/container" "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. // addVMRunCommand adds the 'run' command under vm.
func addVMRunCommand(parent *clir.Command) { func addVMRunCommand(parent *cobra.Command) {
var ( runCmd := &cobra.Command{
name string Use: "run [image]",
detach bool Short: "Run a LinuxKit image or template",
memory int Long: "Runs a LinuxKit image as a VM using the available hypervisor.\n\n" +
cpus int
sshPort int
templateName string
varFlags []string
)
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" + "Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" +
"You can also run from a template using --template, which will build and run\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" + "the image automatically. Use --var to set template variables.\n\n" +
@ -35,40 +37,41 @@ func addVMRunCommand(parent *clir.Command) {
" core vm run -d image.qcow2\n" + " core vm run -d image.qcow2\n" +
" core vm run --name myvm --memory 2048 --cpus 4 image.iso\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 core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" +
" core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com") " core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com",
RunE: func(cmd *cobra.Command, args []string) error {
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)
runCmd.Action(func() error {
opts := container.RunOptions{ opts := container.RunOptions{
Name: name, Name: runName,
Detach: detach, Detach: runDetach,
Memory: memory, Memory: runMemory,
CPUs: cpus, CPUs: runCPUs,
SSHPort: sshPort, SSHPort: runSSHPort,
} }
// If template is specified, build and run from template // If template is specified, build and run from template
if templateName != "" { if runTemplateName != "" {
vars := ParseVarFlags(varFlags) vars := ParseVarFlags(runVarFlags)
return RunFromTemplate(templateName, vars, opts) return RunFromTemplate(runTemplateName, vars, opts)
} }
// Otherwise, require an image path // Otherwise, require an image path
args := runCmd.OtherArgs()
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("image path is required (or use --template)") return fmt.Errorf("image path is required (or use --template)")
} }
image := args[0] image := args[0]
return runContainer(image, name, detach, memory, cpus, sshPort) return runContainer(image, runName, runDetach, runMemory, runCPUs, runSSHPort)
}) },
}
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)")
parent.AddCommand(runCmd)
} }
func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error { 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 return nil
} }
// addVMPsCommand adds the 'ps' command under vm. var psAll bool
func addVMPsCommand(parent *clir.Command) {
var all bool
psCmd := parent.NewSubCommand("ps", "List running VMs") // addVMPsCommand adds the 'ps' command under vm.
psCmd.LongDescription("Lists all VMs. By default, only shows running VMs.\n\n" + 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" + "Examples:\n" +
" core vm ps\n" + " core vm ps\n" +
" core vm ps -a") " core vm ps -a",
RunE: func(cmd *cobra.Command, args []string) error {
return listContainers(psAll)
},
}
psCmd.BoolFlag("a", "Show all containers (including stopped)", &all) psCmd.Flags().BoolVarP(&psAll, "all", "a", false, "Show all containers (including stopped)")
psCmd.Action(func() error { parent.AddCommand(psCmd)
return listContainers(all)
})
} }
func listContainers(all bool) error { func listContainers(all bool) error {
@ -207,20 +214,23 @@ func formatDuration(d time.Duration) string {
} }
// addVMStopCommand adds the 'stop' command under vm. // addVMStopCommand adds the 'stop' command under vm.
func addVMStopCommand(parent *clir.Command) { func addVMStopCommand(parent *cobra.Command) {
stopCmd := parent.NewSubCommand("stop", "Stop a running VM") stopCmd := &cobra.Command{
stopCmd.LongDescription("Stops a running VM by ID.\n\n" + Use: "stop <container-id>",
Short: "Stop a running VM",
Long: "Stops a running VM by ID.\n\n" +
"Examples:\n" + "Examples:\n" +
" core vm stop abc12345\n" + " core vm stop abc12345\n" +
" core vm stop abc1") " core vm stop abc1",
RunE: func(cmd *cobra.Command, args []string) error {
stopCmd.Action(func() error {
args := stopCmd.OtherArgs()
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("container ID is required") return fmt.Errorf("container ID is required")
} }
return stopContainer(args[0]) return stopContainer(args[0])
}) },
}
parent.AddCommand(stopCmd)
} }
func stopContainer(id string) error { func stopContainer(id string) error {
@ -271,25 +281,28 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s
} }
} }
// addVMLogsCommand adds the 'logs' command under vm. var logsFollow bool
func addVMLogsCommand(parent *clir.Command) {
var follow bool
logsCmd := parent.NewSubCommand("logs", "View VM logs") // addVMLogsCommand adds the 'logs' command under vm.
logsCmd.LongDescription("View logs from a VM.\n\n" + func addVMLogsCommand(parent *cobra.Command) {
logsCmd := &cobra.Command{
Use: "logs <container-id>",
Short: "View VM logs",
Long: "View logs from a VM.\n\n" +
"Examples:\n" + "Examples:\n" +
" core vm logs abc12345\n" + " core vm logs abc12345\n" +
" core vm logs -f abc1") " core vm logs -f abc1",
RunE: func(cmd *cobra.Command, args []string) error {
logsCmd.BoolFlag("f", "Follow log output", &follow)
logsCmd.Action(func() error {
args := logsCmd.OtherArgs()
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("container ID is required") return fmt.Errorf("container ID is required")
} }
return viewLogs(args[0], follow) return viewLogs(args[0], logsFollow)
}) },
}
logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, "Follow log output")
parent.AddCommand(logsCmd)
} }
func viewLogs(id string, follow bool) error { 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. // addVMExecCommand adds the 'exec' command under vm.
func addVMExecCommand(parent *clir.Command) { func addVMExecCommand(parent *cobra.Command) {
execCmd := parent.NewSubCommand("exec", "Execute a command in a VM") execCmd := &cobra.Command{
execCmd.LongDescription("Execute a command inside a running VM via SSH.\n\n" + Use: "exec <container-id> <command> [args...]",
Short: "Execute a command in a VM",
Long: "Execute a command inside a running VM via SSH.\n\n" +
"Examples:\n" + "Examples:\n" +
" core vm exec abc12345 ls -la\n" + " core vm exec abc12345 ls -la\n" +
" core vm exec abc1 /bin/sh") " core vm exec abc1 /bin/sh",
RunE: func(cmd *cobra.Command, args []string) error {
execCmd.Action(func() error {
args := execCmd.OtherArgs()
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("container ID and command are required") return fmt.Errorf("container ID and command are required")
} }
return execInContainer(args[0], args[1:]) return execInContainer(args[0], args[1:])
}) },
}
parent.AddCommand(execCmd)
} }
func execInContainer(id string, cmd []string) error { func execInContainer(id string, cmd []string) error {

View file

@ -10,63 +10,72 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/host-uk/core/pkg/container" "github.com/host-uk/core/pkg/container"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// addVMTemplatesCommand adds the 'templates' command under vm. // addVMTemplatesCommand adds the 'templates' command under vm.
func addVMTemplatesCommand(parent *clir.Command) { func addVMTemplatesCommand(parent *cobra.Command) {
templatesCmd := parent.NewSubCommand("templates", "Manage LinuxKit templates") templatesCmd := &cobra.Command{
templatesCmd.LongDescription("Manage LinuxKit YAML templates for building VMs.\n\n" + 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" + "Templates provide pre-configured LinuxKit configurations for common use cases.\n" +
"They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" + "They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" +
"Examples:\n" + "Examples:\n" +
" core vm templates # List available templates\n" + " core vm templates # List available templates\n" +
" core vm templates show core-dev # Show template content\n" + " core vm templates show core-dev # Show template content\n" +
" core vm templates vars server-php # Show template variables") " core vm templates vars server-php # Show template variables",
RunE: func(cmd *cobra.Command, args []string) error {
// Default action: list templates
templatesCmd.Action(func() error {
return listTemplates() return listTemplates()
}) },
}
// Add subcommands // Add subcommands
addTemplatesShowCommand(templatesCmd) addTemplatesShowCommand(templatesCmd)
addTemplatesVarsCommand(templatesCmd) addTemplatesVarsCommand(templatesCmd)
parent.AddCommand(templatesCmd)
} }
// addTemplatesShowCommand adds the 'templates show' subcommand. // addTemplatesShowCommand adds the 'templates show' subcommand.
func addTemplatesShowCommand(parent *clir.Command) { func addTemplatesShowCommand(parent *cobra.Command) {
showCmd := parent.NewSubCommand("show", "Display template content") showCmd := &cobra.Command{
showCmd.LongDescription("Display the content of a LinuxKit template.\n\n" + Use: "show <template-name>",
Short: "Display template content",
Long: "Display the content of a LinuxKit template.\n\n" +
"Examples:\n" + "Examples:\n" +
" core templates show core-dev\n" + " core templates show core-dev\n" +
" core templates show server-php") " core templates show server-php",
RunE: func(cmd *cobra.Command, args []string) error {
showCmd.Action(func() error {
args := showCmd.OtherArgs()
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("template name is required") return fmt.Errorf("template name is required")
} }
return showTemplate(args[0]) return showTemplate(args[0])
}) },
}
parent.AddCommand(showCmd)
} }
// addTemplatesVarsCommand adds the 'templates vars' subcommand. // addTemplatesVarsCommand adds the 'templates vars' subcommand.
func addTemplatesVarsCommand(parent *clir.Command) { func addTemplatesVarsCommand(parent *cobra.Command) {
varsCmd := parent.NewSubCommand("vars", "Show template variables") varsCmd := &cobra.Command{
varsCmd.LongDescription("Display all variables used in a template.\n\n" + Use: "vars <template-name>",
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" + "Shows required variables (no default) and optional variables (with defaults).\n\n" +
"Examples:\n" + "Examples:\n" +
" core templates vars core-dev\n" + " core templates vars core-dev\n" +
" core templates vars server-php") " core templates vars server-php",
RunE: func(cmd *cobra.Command, args []string) error {
varsCmd.Action(func() error {
args := varsCmd.OtherArgs()
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("template name is required") return fmt.Errorf("template name is required")
} }
return showTemplateVars(args[0]) return showTemplateVars(args[0])
}) },
}
parent.AddCommand(varsCmd)
} }
func listTemplates() error { func listTemplates() error {

View file

@ -4,7 +4,7 @@ package vm
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir" "github.com/spf13/cobra"
) )
// Style aliases from shared // Style aliases from shared
@ -22,9 +22,11 @@ var (
) )
// AddVMCommands adds container-related commands under 'vm' to the CLI. // AddVMCommands adds container-related commands under 'vm' to the CLI.
func AddVMCommands(parent *clir.Cli) { func AddVMCommands(root *cobra.Command) {
vmCmd := parent.NewSubCommand("vm", "LinuxKit VM management") vmCmd := &cobra.Command{
vmCmd.LongDescription("Manage LinuxKit virtual machines.\n\n" + Use: "vm",
Short: "LinuxKit VM management",
Long: "Manage LinuxKit virtual machines.\n\n" +
"LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" + "LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" +
"They run using qemu or hyperkit depending on your system.\n\n" + "They run using qemu or hyperkit depending on your system.\n\n" +
"Commands:\n" + "Commands:\n" +
@ -33,8 +35,10 @@ func AddVMCommands(parent *clir.Cli) {
" stop Stop a running VM\n" + " stop Stop a running VM\n" +
" logs View VM logs\n" + " logs View VM logs\n" +
" exec Execute command in VM\n" + " exec Execute command in VM\n" +
" templates Manage LinuxKit templates") " templates Manage LinuxKit templates",
}
root.AddCommand(vmCmd)
addVMRunCommand(vmCmd) addVMRunCommand(vmCmd)
addVMPsCommand(vmCmd) addVMPsCommand(vmCmd)
addVMStopCommand(vmCmd) addVMStopCommand(vmCmd)