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
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
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()
},
}
func addGoLintCommand(parent *clir.Command) {
var fix bool
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")
lintCmd := parent.NewSubCommand("lint", "Run golangci-lint")
lintCmd.LongDescription("Run golangci-lint on the codebase.\n\n" +
parent.AddCommand(fmtCmd)
}
var lintFix bool
func addGoLintCommand(parent *cobra.Command) {
lintCmd := &cobra.Command{
Use: "lint",
Short: "Run golangci-lint",
Long: "Run golangci-lint on the codebase.\n\n" +
"Examples:\n" +
" 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
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
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
})
},
}
func addGoModCommand(parent *clir.Command) {
modCmd := parent.NewSubCommand("mod", "Module management")
modCmd.LongDescription("Go module management commands.\n\n" +
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 *cobra.Command) {
modCmd := &cobra.Command{
Use: "mod",
Short: "Module management",
Long: "Go module management commands.\n\n" +
"Commands:\n" +
" tidy Add missing and remove unused modules\n" +
" download Download modules to local cache\n" +
" verify Verify dependencies\n" +
" graph Print module dependency graph")
// tidy
tidyCmd := modCmd.NewSubCommand("tidy", "Tidy go.mod")
tidyCmd.Action(func() error {
cmd := exec.Command("go", "mod", "tidy")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// download
downloadCmd := modCmd.NewSubCommand("download", "Download modules")
downloadCmd.Action(func() error {
cmd := exec.Command("go", "mod", "download")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// verify
verifyCmd := modCmd.NewSubCommand("verify", "Verify dependencies")
verifyCmd.Action(func() error {
cmd := exec.Command("go", "mod", "verify")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
// graph
graphCmd := modCmd.NewSubCommand("graph", "Print dependency graph")
graphCmd.Action(func() error {
cmd := exec.Command("go", "mod", "graph")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
" graph Print module dependency graph",
}
func addGoWorkCommand(parent *clir.Command) {
workCmd := parent.NewSubCommand("work", "Workspace management")
workCmd.LongDescription("Go workspace management commands.\n\n" +
// tidy
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 := &cobra.Command{
Use: "download",
Short: "Download modules",
RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "download")
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
},
}
// verify
verifyCmd := &cobra.Command{
Use: "verify",
Short: "Verify dependencies",
RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "verify")
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
},
}
// graph
graphCmd := &cobra.Command{
Use: "graph",
Short: "Print dependency graph",
RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "graph")
execCmd.Stdout = os.Stdout
execCmd.Stderr = os.Stderr
return execCmd.Run()
},
}
modCmd.AddCommand(tidyCmd)
modCmd.AddCommand(downloadCmd)
modCmd.AddCommand(verifyCmd)
modCmd.AddCommand(graphCmd)
parent.AddCommand(modCmd)
}
func addGoWorkCommand(parent *cobra.Command) {
workCmd := &cobra.Command{
Use: "work",
Short: "Workspace management",
Long: "Go workspace management commands.\n\n" +
"Commands:\n" +
" 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 (
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
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" +
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")
shellCmd.Action(func() error {
args := shellCmd.OtherArgs()
if len(args) == 0 {
return fmt.Errorf("container ID or name is required")
}
" 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
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
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
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
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
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
})
},
}
func addPHPPackagesUnlinkCommand(parent *clir.Command) {
unlinkCmd := parent.NewSubCommand("unlink", "Unlink packages")
unlinkCmd.LongDescription("Remove linked packages from composer.json.\n\n" +
parent.AddCommand(linkCmd)
}
func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
unlinkCmd := &cobra.Command{
Use: "unlink [packages...]",
Short: "Unlink packages",
Long: "Remove linked packages from composer.json.\n\n" +
"Removes path repositories by package name.\n\n" +
"Examples:\n" +
" core php packages unlink vendor/my-package\n" +
" core php packages unlink vendor/pkg-a vendor/pkg-b")
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
})
},
}
func addPHPPackagesUpdateCommand(parent *clir.Command) {
updateCmd := parent.NewSubCommand("update", "Update linked packages")
updateCmd.LongDescription("Run composer update for linked packages.\n\n" +
parent.AddCommand(unlinkCmd)
}
func addPHPPackagesUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{
Use: "update [packages...]",
Short: "Update linked packages",
Long: "Run composer update for linked packages.\n\n" +
"If no packages specified, updates all packages.\n\n" +
"Examples:\n" +
" core php packages update\n" +
" core php packages update vendor/my-package")
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
})
},
}
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.")
parent.AddCommand(updateCmd)
}
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
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
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
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
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
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
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
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
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
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)