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

View file

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

View file

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

View file

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

View file

@ -10,52 +10,70 @@
// - claude: Claude Code CLI integration (planned)
package ai
import "github.com/leaanthony/clir"
import "github.com/spf13/cobra"
// AddCommands registers the 'ai' command and all subcommands.
func AddCommands(app *clir.Cli) {
aiCmd := app.NewSubCommand("ai", "AI agent task management")
aiCmd.LongDescription("Manage tasks from the core-agentic service for AI-assisted development.\n\n" +
"Commands:\n" +
" tasks List tasks (filterable by status, priority, labels)\n" +
" task View task details or auto-select highest priority\n" +
" task:update Update task status or progress\n" +
" task:complete Mark task as completed or failed\n" +
" task:commit Create git commit with task reference\n" +
" task:pr Create GitHub PR linked to task\n" +
" claude Claude Code integration\n\n" +
"Workflow:\n" +
" core ai tasks # List pending tasks\n" +
" core ai task --auto --claim # Auto-select and claim a task\n" +
" core ai task:commit <id> -m 'msg' # Commit with task reference\n" +
" core ai task:complete <id> # Mark task done")
var aiCmd = &cobra.Command{
Use: "ai",
Short: "AI agent task management",
Long: `Manage tasks from the core-agentic service for AI-assisted development.
// Add Claude command
addClaudeCommand(aiCmd)
Commands:
tasks List tasks (filterable by status, priority, labels)
task View task details or auto-select highest priority
task:update Update task status or progress
task:complete Mark task as completed or failed
task:commit Create git commit with task reference
task:pr Create GitHub PR linked to task
claude Claude Code integration
Workflow:
core ai tasks # List pending tasks
core ai task --auto --claim # Auto-select and claim a task
core ai task:commit <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
AddAgenticCommands(aiCmd)
}
// addClaudeCommand adds the 'claude' subcommand for Claude Code integration.
func addClaudeCommand(parent *clir.Command) {
claudeCmd := parent.NewSubCommand("claude", "Claude Code integration")
claudeCmd.LongDescription("Tools for working with Claude Code.\n\n" +
"Commands:\n" +
" run Run Claude in the current directory\n" +
" config Manage Claude configuration")
// core ai claude run
runCmd := claudeCmd.NewSubCommand("run", "Run Claude Code in the current directory")
runCmd.Action(func() error {
return runClaudeCode()
})
// core ai claude config
configCmd := claudeCmd.NewSubCommand("config", "Manage Claude configuration")
configCmd.Action(func() error {
return showClaudeConfig()
})
// AddCommands registers the 'ai' command and all subcommands.
func AddCommands(root *cobra.Command) {
root.AddCommand(aiCmd)
}
func runClaudeCode() error {

View file

@ -5,7 +5,7 @@ import (
"embed"
"github.com/charmbracelet/lipgloss"
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
)
// Build command styles
@ -32,100 +32,130 @@ var (
//go:embed all:tmpl/gui
var guiTemplate embed.FS
// AddBuildCommand adds the new build command and its subcommands to the clir app.
func AddBuildCommand(app *clir.Cli) {
buildCmd := app.NewSubCommand("build", "Build projects with auto-detection and cross-compilation")
buildCmd.LongDescription("Builds the current project with automatic type detection.\n" +
"Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.\n" +
"Configuration can be provided via .core/build.yaml or command-line flags.\n\n" +
"Examples:\n" +
" core build # Auto-detect and build\n" +
" core build --type docker # Build Docker image\n" +
" core build --type linuxkit # Build LinuxKit image\n" +
" core build --type linuxkit --config linuxkit.yml --format qcow2-bios")
// Flags for the main build command
var buildType string
var ciMode bool
var targets string
var outputDir string
var doArchive bool
var doChecksum bool
// Flags for the main build command
var (
buildType string
ciMode bool
targets string
outputDir string
doArchive bool
doChecksum bool
// Docker/LinuxKit specific flags
var configPath string
var format string
var push bool
var imageName string
configPath string
format string
push bool
imageName string
// Signing flags
var noSign bool
var notarize bool
noSign bool
notarize bool
buildCmd.StringFlag("type", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified", &buildType)
buildCmd.BoolFlag("ci", "CI mode - minimal output with JSON artifact list at the end", &ciMode)
buildCmd.StringFlag("targets", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)", &targets)
buildCmd.StringFlag("output", "Output directory for artifacts (default: dist)", &outputDir)
buildCmd.BoolFlag("archive", "Create archives (tar.gz for linux/darwin, zip for windows) - default: true", &doArchive)
buildCmd.BoolFlag("checksum", "Generate SHA256 checksums and CHECKSUMS.txt - default: true", &doChecksum)
// from-path subcommand
fromPath string
// Docker/LinuxKit specific
buildCmd.StringFlag("config", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)", &configPath)
buildCmd.StringFlag("format", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)", &format)
buildCmd.BoolFlag("push", "Push Docker image after build (default: false)", &push)
buildCmd.StringFlag("image", "Docker image name (e.g., host-uk/core-devops)", &imageName)
// pwa subcommand
pwaURL string
// Signing flags
buildCmd.BoolFlag("no-sign", "Skip all code signing", &noSign)
buildCmd.BoolFlag("notarize", "Enable macOS notarization (requires Apple credentials)", &notarize)
// sdk subcommand
sdkSpec string
sdkLang string
sdkVersion string
sdkDryRun bool
)
// Set defaults for archive and checksum (true by default)
doArchive = true
doChecksum = true
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build projects with auto-detection and cross-compilation",
Long: `Builds the current project with automatic type detection.
Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.
Configuration can be provided via .core/build.yaml or command-line flags.
// Default action for `core build` (no subcommand)
buildCmd.Action(func() error {
Examples:
core build # Auto-detect and build
core build --type docker # Build Docker image
core build --type linuxkit # Build LinuxKit image
core build --type linuxkit --config linuxkit.yml --format qcow2-bios`,
RunE: func(cmd *cobra.Command, args []string) error {
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize)
})
},
}
// --- `build from-path` command (legacy PWA/GUI build) ---
fromPathCmd := buildCmd.NewSubCommand("from-path", "Build from a local directory.")
var fromPath string
fromPathCmd.StringFlag("path", "The path to the static web application files.", &fromPath)
fromPathCmd.Action(func() error {
var fromPathCmd = &cobra.Command{
Use: "from-path",
Short: "Build from a local directory.",
RunE: func(cmd *cobra.Command, args []string) error {
if fromPath == "" {
return errPathRequired
}
return runBuild(fromPath)
})
},
}
// --- `build pwa` command (legacy PWA build) ---
pwaCmd := buildCmd.NewSubCommand("pwa", "Build from a live PWA URL.")
var pwaURL string
pwaCmd.StringFlag("url", "The URL of the PWA to build.", &pwaURL)
pwaCmd.Action(func() error {
var pwaCmd = &cobra.Command{
Use: "pwa",
Short: "Build from a live PWA URL.",
RunE: func(cmd *cobra.Command, args []string) error {
if pwaURL == "" {
return errURLRequired
}
return runPwaBuild(pwaURL)
})
// --- `build sdk` command ---
sdkBuildCmd := buildCmd.NewSubCommand("sdk", "Generate API SDKs from OpenAPI spec")
sdkBuildCmd.LongDescription("Generates typed API clients from OpenAPI specifications.\n" +
"Supports TypeScript, Python, Go, and PHP.\n\n" +
"Examples:\n" +
" core build sdk # Generate all configured SDKs\n" +
" core build sdk --lang typescript # Generate only TypeScript SDK\n" +
" core build sdk --spec api.yaml # Use specific OpenAPI spec")
var sdkSpec, sdkLang, sdkVersion string
var sdkDryRun bool
sdkBuildCmd.StringFlag("spec", "Path to OpenAPI spec file", &sdkSpec)
sdkBuildCmd.StringFlag("lang", "Generate only this language (typescript, python, go, php)", &sdkLang)
sdkBuildCmd.StringFlag("version", "Version to embed in generated SDKs", &sdkVersion)
sdkBuildCmd.BoolFlag("dry-run", "Show what would be generated without writing files", &sdkDryRun)
sdkBuildCmd.Action(func() error {
return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
})
},
}
var sdkBuildCmd = &cobra.Command{
Use: "sdk",
Short: "Generate API SDKs from OpenAPI spec",
Long: `Generates typed API clients from OpenAPI specifications.
Supports TypeScript, Python, Go, and PHP.
Examples:
core build sdk # Generate all configured SDKs
core build sdk --lang typescript # Generate only TypeScript SDK
core build sdk --spec api.yaml # Use specific OpenAPI spec`,
RunE: func(cmd *cobra.Command, args []string) error {
return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
},
}
func init() {
// Main build command flags
buildCmd.Flags().StringVar(&buildType, "type", "", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified")
buildCmd.Flags().BoolVar(&ciMode, "ci", false, "CI mode - minimal output with JSON artifact list at the end")
buildCmd.Flags().StringVar(&targets, "targets", "", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)")
buildCmd.Flags().StringVar(&outputDir, "output", "", "Output directory for artifacts (default: dist)")
buildCmd.Flags().BoolVar(&doArchive, "archive", true, "Create archives (tar.gz for linux/darwin, zip for windows)")
buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, "Generate SHA256 checksums and CHECKSUMS.txt")
// Docker/LinuxKit specific
buildCmd.Flags().StringVar(&configPath, "config", "", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)")
buildCmd.Flags().StringVar(&format, "format", "", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)")
buildCmd.Flags().BoolVar(&push, "push", false, "Push Docker image after build")
buildCmd.Flags().StringVar(&imageName, "image", "", "Docker image name (e.g., host-uk/core-devops)")
// Signing flags
buildCmd.Flags().BoolVar(&noSign, "no-sign", false, "Skip all code signing")
buildCmd.Flags().BoolVar(&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
package build
import "github.com/leaanthony/clir"
import "github.com/spf13/cobra"
// AddCommands registers the 'build' command and all subcommands.
func AddCommands(app *clir.Cli) {
AddBuildCommand(app)
func AddCommands(root *cobra.Command) {
AddBuildCommand(root)
}

View file

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

View file

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

View file

@ -17,32 +17,92 @@
package cmd
import (
"github.com/charmbracelet/lipgloss"
"github.com/leaanthony/clir"
"os"
"github.com/host-uk/core/cmd/shared"
"github.com/spf13/cobra"
)
// Terminal styles using Tailwind color palette.
// Terminal styles using Tailwind colour palette (from shared package).
var (
// coreStyle is used for primary headings and the CLI name.
coreStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#3b82f6")). // blue-500
Bold(true)
// subPkgStyle is used for subcommand names and secondary headings.
subPkgStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#e2e8f0")). // gray-200
Bold(true)
coreStyle = shared.RepoNameStyle
// linkStyle is used for URLs and clickable references.
linkStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#3b82f6")). // blue-500
Underline(true)
linkStyle = shared.LinkStyle
)
// rootCmd is the base command for the CLI.
var rootCmd = &cobra.Command{
Use: "core",
Short: "CLI tool for development and production",
Version: "0.1.0",
}
// Execute initialises and runs the CLI application.
// Commands are registered based on build tags (see core_ci.go and core_dev.go).
func Execute() error {
app := clir.NewCli("core", "CLI tool for development and production", "0.1.0")
registerCommands(app)
return app.Run()
return rootCmd.Execute()
}
func init() {
// Add shell completion command
rootCmd.AddCommand(completionCmd)
}
// completionCmd generates shell completion scripts.
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `Generate shell completion script for the specified shell.
To load completions:
Bash:
$ source <(core completion bash)
# To load completions for each session, execute once:
# Linux:
$ core completion bash > /etc/bash_completion.d/core
# macOS:
$ core completion bash > $(brew --prefix)/etc/bash_completion.d/core
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ core completion zsh > "${fpath[1]}/_core"
# You will need to start a new shell for this setup to take effect.
Fish:
$ core completion fish | source
# To load completions for each session, execute once:
$ core completion fish > ~/.config/fish/completions/core.fish
PowerShell:
PS> core completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> core completion powershell > core.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
_ = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
_ = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
_ = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,17 @@
package dev
import (
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
)
// addAPICommands adds the 'api' command and its subcommands to the given parent command.
func addAPICommands(parent *clir.Command) {
func addAPICommands(parent *cobra.Command) {
// Create the 'api' command
apiCmd := parent.NewSubCommand("api", "Tools for managing service APIs")
apiCmd := &cobra.Command{
Use: "api",
Short: "Tools for managing service APIs",
}
parent.AddCommand(apiCmd)
// Add the 'sync' command to 'api'
addSyncCommand(apiCmd)

View file

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

View file

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

View file

@ -8,24 +8,31 @@ import (
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/repos"
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
)
// Health command flags
var (
healthRegistryPath string
healthVerbose bool
)
// addHealthCommand adds the 'health' command to the given parent command.
func addHealthCommand(parent *clir.Command) {
var registryPath string
var verbose bool
func addHealthCommand(parent *cobra.Command) {
healthCmd := &cobra.Command{
Use: "health",
Short: "Quick health check across all repos",
Long: `Shows a summary of repository health:
total repos, dirty repos, unpushed commits, etc.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runHealth(healthRegistryPath, healthVerbose)
},
}
healthCmd := parent.NewSubCommand("health", "Quick health check across all repos")
healthCmd.LongDescription("Shows a summary of repository health:\n" +
"total repos, dirty repos, unpushed commits, etc.")
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown")
healthCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath)
healthCmd.BoolFlag("verbose", "Show detailed breakdown", &verbose)
healthCmd.Action(func() error {
return runHealth(registryPath, verbose)
})
parent.AddCommand(healthCmd)
}
func runHealth(registryPath string, verbose bool) error {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,22 +10,29 @@ import (
"path/filepath"
"text/template"
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// addSyncCommand adds the 'sync' command to the given parent command.
func addSyncCommand(parent *clir.Command) {
syncCmd := parent.NewSubCommand("sync", "Synchronizes the public service APIs with their internal implementations.")
syncCmd.LongDescription("This command scans the 'pkg' directory for services and ensures that the\ntop-level public API for each service is in sync with its internal implementation.\nIt automatically generates the necessary Go files with type aliases.")
syncCmd.Action(func() error {
func addSyncCommand(parent *cobra.Command) {
syncCmd := &cobra.Command{
Use: "sync",
Short: "Synchronizes the public service APIs with their internal implementations.",
Long: `This command scans the 'pkg' directory for services and ensures that the
top-level public API for each service is in sync with its internal implementation.
It automatically generates the necessary Go files with type aliases.`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := runSync(); err != nil {
return fmt.Errorf("Error: %w", err)
}
fmt.Println("Public APIs synchronized successfully.")
return nil
})
},
}
parent.AddCommand(syncCmd)
}
type symbolInfo struct {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package setup
import (
"github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
)
// Style aliases from shared package
@ -21,34 +21,46 @@ const (
devopsReposYaml = "repos.yaml"
)
// AddSetupCommand adds the 'setup' command to the given parent command.
func AddSetupCommand(parent *clir.Cli) {
var registryPath string
var only string
var dryRun bool
var all bool
var name string
var build bool
// Setup command flags
var (
registryPath string
only string
dryRun bool
all bool
name string
build bool
)
setupCmd := parent.NewSubCommand("setup", "Bootstrap workspace or clone packages from registry")
setupCmd.LongDescription("Sets up a development workspace.\n\n" +
"REGISTRY MODE (repos.yaml exists):\n" +
" Clones all repositories defined in repos.yaml into packages/.\n" +
" Skips repos that already exist. Use --only to filter by type.\n\n" +
"BOOTSTRAP MODE (no repos.yaml):\n" +
" 1. Clones core-devops to set up the workspace\n" +
" 2. Presents an interactive wizard to select packages\n" +
" 3. Clones selected packages\n\n" +
"Use --all to skip the wizard and clone everything.")
var setupCmd = &cobra.Command{
Use: "setup",
Short: "Bootstrap workspace or clone packages from registry",
Long: `Sets up a development workspace.
setupCmd.StringFlag("registry", "Path to repos.yaml (auto-detected if not specified)", &registryPath)
setupCmd.StringFlag("only", "Only clone repos of these types (comma-separated: foundation,module,product)", &only)
setupCmd.BoolFlag("dry-run", "Show what would be cloned without cloning", &dryRun)
setupCmd.BoolFlag("all", "Skip wizard, clone all packages (non-interactive)", &all)
setupCmd.StringFlag("name", "Project directory name for bootstrap mode", &name)
setupCmd.BoolFlag("build", "Run build after cloning", &build)
REGISTRY MODE (repos.yaml exists):
Clones all repositories defined in repos.yaml into packages/.
Skips repos that already exist. Use --only to filter by type.
setupCmd.Action(func() error {
BOOTSTRAP MODE (no repos.yaml):
1. Clones core-devops to set up the workspace
2. Presents an interactive wizard to select packages
3. Clones selected packages
Use --all to skip the wizard and clone everything.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSetupOrchestrator(registryPath, only, dryRun, all, name, build)
})
},
}
func init() {
setupCmd.Flags().StringVar(&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
package testcmd
import "github.com/leaanthony/clir"
import "github.com/spf13/cobra"
// AddCommands registers the 'test' command and all subcommands.
func AddCommands(app *clir.Cli) {
AddTestCommand(app)
func AddCommands(root *cobra.Command) {
root.AddCommand(testCmd)
}

View file

@ -6,7 +6,7 @@ package testcmd
import (
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared"
"github.com/leaanthony/clir"
"github.com/spf13/cobra"
)
// Style aliases from shared
@ -30,38 +30,44 @@ var (
Foreground(lipgloss.Color("#ef4444")) // red-500
)
// AddTestCommand adds the 'test' command to the given parent command.
func AddTestCommand(parent *clir.Cli) {
var verbose bool
var coverage bool
var short bool
var pkg string
var run string
var race bool
var json bool
// Flag variables for test command
var (
testVerbose bool
testCoverage bool
testShort bool
testPkg string
testRun string
testRace bool
testJSON bool
)
testCmd := parent.NewSubCommand("test", "Run tests with coverage")
testCmd.LongDescription("Runs Go tests with coverage reporting.\n\n" +
"Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.\n\n" +
"Examples:\n" +
" core test # Run all tests with coverage summary\n" +
" core test --verbose # Show test output as it runs\n" +
" core test --coverage # Show detailed per-package coverage\n" +
" core test --pkg ./pkg/... # Test specific packages\n" +
" core test --run TestName # Run specific test by name\n" +
" core test --short # Skip long-running tests\n" +
" core test --race # Enable race detector\n" +
" core test --json # Output JSON for CI/agents")
var testCmd = &cobra.Command{
Use: "test",
Short: "Run tests with coverage",
Long: `Runs Go tests with coverage reporting.
testCmd.BoolFlag("verbose", "Show test output as it runs (-v)", &verbose)
testCmd.BoolFlag("coverage", "Show detailed per-package coverage", &coverage)
testCmd.BoolFlag("short", "Skip long-running tests (-short)", &short)
testCmd.StringFlag("pkg", "Package pattern to test (default: ./...)", &pkg)
testCmd.StringFlag("run", "Run only tests matching this regex (-run)", &run)
testCmd.BoolFlag("race", "Enable race detector (-race)", &race)
testCmd.BoolFlag("json", "Output JSON for CI/agents", &json)
Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.
testCmd.Action(func() error {
return runTest(verbose, coverage, short, pkg, run, race, json)
})
Examples:
core test # Run all tests with coverage summary
core test --verbose # Show test output as it runs
core test --coverage # Show detailed per-package coverage
core test --pkg ./pkg/... # Test specific packages
core test --run TestName # Run specific test by name
core test --short # Skip long-running tests
core test --race # Enable race detector
core test --json # Output JSON for CI/agents`,
RunE: func(cmd *cobra.Command, args []string) error {
return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON)
},
}
func init() {
testCmd.Flags().BoolVar(&testVerbose, "verbose", false, "Show test output as it runs (-v)")
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage")
testCmd.Flags().BoolVar(&testShort, "short", false, "Skip long-running tests (-short)")
testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package pattern to test (default: ./...)")
testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching this regex (-run)")
testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector (-race)")
testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON for CI/agents")
}

View file

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

View file

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

View file

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

View file

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