feat(i18n): add translation keys to all CLI commands

Replace hardcoded strings with i18n.T() calls across all cmd/* packages:
- ai, build, ci, dev, docs, doctor, go, php, pkg, sdk, setup, test, vm

Adds 500+ translation keys to en.json for command descriptions,
flag descriptions, labels, messages, and error strings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 02:37:57 +00:00
parent 0c3bccfceb
commit e8e48127c2
61 changed files with 2549 additions and 1733 deletions

View file

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -32,30 +33,19 @@ var (
var taskCommitCmd = &cobra.Command{ var taskCommitCmd = &cobra.Command{
Use: "task:commit [task-id]", Use: "task:commit [task-id]",
Short: "Auto-commit changes with task reference", Short: i18n.T("cmd.ai.task_commit.short"),
Long: `Creates a git commit with a task reference and co-author attribution. Long: i18n.T("cmd.ai.task_commit.long"),
Args: cobra.ExactArgs(1),
Commit message format:
feat(scope): description
Task: #123
Co-Authored-By: Claude <noreply@anthropic.com>
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 { RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0] taskID := args[0]
if taskCommitMessage == "" { if taskCommitMessage == "" {
return fmt.Errorf("commit message required (--message or -m)") return fmt.Errorf(i18n.T("cmd.ai.task_commit.message_required"))
} }
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -66,7 +56,7 @@ Examples:
// Get task details // Get task details
task, err := client.GetTask(ctx, taskID) task, err := client.GetTask(ctx, taskID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
} }
// Build commit message with optional scope // Build commit message with optional scope
@ -81,35 +71,35 @@ Examples:
// Get current directory // Get current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.working_dir"), err)
} }
// Check for uncommitted changes // Check for uncommitted changes
hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd) hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd)
if err != nil { if err != nil {
return fmt.Errorf("failed to check git status: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.git_status"), err)
} }
if !hasChanges { if !hasChanges {
fmt.Println("No uncommitted changes to commit.") fmt.Println(i18n.T("cmd.ai.task_commit.no_changes"))
return nil return nil
} }
// Create commit // Create commit
fmt.Printf("%s Creating commit for task %s...\n", dimStyle.Render(">>"), taskID) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_commit.creating", map[string]interface{}{"ID": taskID}))
if err := agentic.AutoCommit(ctx, task, cwd, fullMessage); err != nil { if err := agentic.AutoCommit(ctx, task, cwd, fullMessage); err != nil {
return fmt.Errorf("failed to commit: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.commit"), err)
} }
fmt.Printf("%s Committed: %s\n", successStyle.Render(">>"), fullMessage) fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_commit.committed"), fullMessage)
// Push if requested // Push if requested
if taskCommitPush { if taskCommitPush {
fmt.Printf("%s Pushing changes...\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_commit.pushing"))
if err := agentic.PushChanges(ctx, cwd); err != nil { if err := agentic.PushChanges(ctx, cwd); err != nil {
return fmt.Errorf("failed to push: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.push"), err)
} }
fmt.Printf("%s Changes pushed successfully\n", successStyle.Render(">>")) fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_commit.pushed"))
} }
return nil return nil
@ -118,23 +108,15 @@ Examples:
var taskPRCmd = &cobra.Command{ var taskPRCmd = &cobra.Command{
Use: "task:pr [task-id]", Use: "task:pr [task-id]",
Short: "Create a pull request for a task", Short: i18n.T("cmd.ai.task_pr.short"),
Long: `Creates a GitHub pull request linked to a task. Long: i18n.T("cmd.ai.task_pr.long"),
Args: cobra.ExactArgs(1),
Requires the GitHub CLI (gh) to be installed and authenticated.
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 { RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0] taskID := args[0]
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -145,31 +127,31 @@ Examples:
// Get task details // Get task details
task, err := client.GetTask(ctx, taskID) task, err := client.GetTask(ctx, taskID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
} }
// Get current directory // Get current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.working_dir"), err)
} }
// Check current branch // Check current branch
branch, err := agentic.GetCurrentBranch(ctx, cwd) branch, err := agentic.GetCurrentBranch(ctx, cwd)
if err != nil { if err != nil {
return fmt.Errorf("failed to get current branch: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_branch"), err)
} }
if branch == "main" || branch == "master" { if branch == "main" || branch == "master" {
return fmt.Errorf("cannot create PR from %s branch; create a feature branch first", branch) return fmt.Errorf(i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch}))
} }
// Push current branch // Push current branch
fmt.Printf("%s Pushing branch %s...\n", dimStyle.Render(">>"), branch) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_pr.pushing_branch", map[string]interface{}{"Branch": branch}))
if err := agentic.PushChanges(ctx, cwd); err != nil { if err := agentic.PushChanges(ctx, cwd); err != nil {
// Try setting upstream // Try setting upstream
if _, err := runGitCommand(cwd, "push", "-u", "origin", branch); err != nil { if _, err := runGitCommand(cwd, "push", "-u", "origin", branch); err != nil {
return fmt.Errorf("failed to push branch: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.push_branch"), err)
} }
} }
@ -185,14 +167,14 @@ Examples:
} }
// Create PR // Create PR
fmt.Printf("%s Creating pull request...\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_pr.creating"))
prURL, err := agentic.CreatePR(ctx, task, cwd, opts) prURL, err := agentic.CreatePR(ctx, task, cwd, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to create PR: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.create_pr"), err)
} }
fmt.Printf("%s Pull request created!\n", successStyle.Render(">>")) fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_pr.created"))
fmt.Printf(" URL: %s\n", prURL) fmt.Printf(" %s %s\n", i18n.T("cmd.ai.label.url"), prURL)
return nil return nil
}, },
@ -200,15 +182,15 @@ Examples:
func init() { func init() {
// task:commit command flags // task:commit command flags
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", "Commit message (without task reference)") taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message"))
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", "Scope for the commit type (e.g., auth, api, ui)") taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope"))
taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, "Push changes after committing") taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, i18n.T("cmd.ai.task_commit.flag.push"))
// task:pr command flags // task:pr command flags
taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", "PR title (defaults to task title)") taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", i18n.T("cmd.ai.task_pr.flag.title"))
taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, "Create as draft PR") taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, i18n.T("cmd.ai.task_pr.flag.draft"))
taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", "Labels to add (comma-separated)") taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", i18n.T("cmd.ai.task_pr.flag.labels"))
taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", "Base branch (defaults to main)") taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", i18n.T("cmd.ai.task_pr.flag.base"))
} }
func addTaskCommitCommand(parent *cobra.Command) { func addTaskCommitCommand(parent *cobra.Command) {

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -32,18 +33,8 @@ var (
var tasksCmd = &cobra.Command{ var tasksCmd = &cobra.Command{
Use: "tasks", Use: "tasks",
Short: "List available tasks from core-agentic", Short: i18n.T("cmd.ai.tasks.short"),
Long: `Lists tasks from the core-agentic service. Long: i18n.T("cmd.ai.tasks.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
limit := tasksLimit limit := tasksLimit
if limit == 0 { if limit == 0 {
@ -52,7 +43,7 @@ Examples:
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -77,11 +68,11 @@ Examples:
tasks, err := client.ListTasks(ctx, opts) tasks, err := client.ListTasks(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to list tasks: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.list_tasks"), err)
} }
if len(tasks) == 0 { if len(tasks) == 0 {
fmt.Println("No tasks found.") fmt.Println(i18n.T("cmd.ai.tasks.none_found"))
return nil return nil
} }
@ -92,18 +83,12 @@ Examples:
var taskCmd = &cobra.Command{ var taskCmd = &cobra.Command{
Use: "task [task-id]", Use: "task [task-id]",
Short: "Show task details or auto-select a task", Short: i18n.T("cmd.ai.task.short"),
Long: `Shows details of a specific task or auto-selects the highest priority task. Long: i18n.T("cmd.ai.task.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -126,11 +111,11 @@ Examples:
Limit: 50, Limit: 50,
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to list tasks: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.list_tasks"), err)
} }
if len(tasks) == 0 { if len(tasks) == 0 {
fmt.Println("No pending tasks available.") fmt.Println(i18n.T("cmd.ai.task.no_pending"))
return nil return nil
} }
@ -150,12 +135,12 @@ Examples:
taskClaim = true // Auto-select implies claiming taskClaim = true // Auto-select implies claiming
} else { } else {
if taskID == "" { if taskID == "" {
return fmt.Errorf("task ID required (or use --auto)") return fmt.Errorf(i18n.T("cmd.ai.task.id_required"))
} }
task, err = client.GetTask(ctx, taskID) task, err = client.GetTask(ctx, taskID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
} }
} }
@ -164,7 +149,7 @@ Examples:
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
taskCtx, err := agentic.BuildTaskContext(task, cwd) taskCtx, err := agentic.BuildTaskContext(task, cwd)
if err != nil { if err != nil {
fmt.Printf("%s Failed to build context: %s\n", errorStyle.Render(">>"), err) fmt.Printf("%s %s: %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task.context_failed"), err)
} else { } else {
fmt.Println(taskCtx.FormatContext()) fmt.Println(taskCtx.FormatContext())
} }
@ -174,15 +159,15 @@ Examples:
if taskClaim && task.Status == agentic.StatusPending { if taskClaim && task.Status == agentic.StatusPending {
fmt.Println() fmt.Println()
fmt.Printf("%s Claiming task...\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming"))
claimedTask, err := client.ClaimTask(ctx, task.ID) claimedTask, err := client.ClaimTask(ctx, task.ID)
if err != nil { if err != nil {
return fmt.Errorf("failed to claim task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.claim_task"), err)
} }
fmt.Printf("%s Task claimed successfully!\n", successStyle.Render(">>")) fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task.claimed"))
fmt.Printf(" Status: %s\n", formatTaskStatus(claimedTask.Status)) fmt.Printf(" %s %s\n", i18n.T("cmd.ai.label.status"), formatTaskStatus(claimedTask.Status))
} }
return nil return nil
@ -191,16 +176,16 @@ Examples:
func init() { func init() {
// tasks command flags // tasks command flags
tasksCmd.Flags().StringVar(&tasksStatus, "status", "", "Filter by status (pending, in_progress, completed, blocked)") tasksCmd.Flags().StringVar(&tasksStatus, "status", "", i18n.T("cmd.ai.tasks.flag.status"))
tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", "Filter by priority (critical, high, medium, low)") tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", i18n.T("cmd.ai.tasks.flag.priority"))
tasksCmd.Flags().StringVar(&tasksLabels, "labels", "", "Filter by labels (comma-separated)") tasksCmd.Flags().StringVar(&tasksLabels, "labels", "", i18n.T("cmd.ai.tasks.flag.labels"))
tasksCmd.Flags().IntVar(&tasksLimit, "limit", 20, "Max number of tasks to return") tasksCmd.Flags().IntVar(&tasksLimit, "limit", 20, i18n.T("cmd.ai.tasks.flag.limit"))
tasksCmd.Flags().StringVar(&tasksProject, "project", "", "Filter by project") tasksCmd.Flags().StringVar(&tasksProject, "project", "", i18n.T("cmd.ai.tasks.flag.project"))
// task command flags // task command flags
taskCmd.Flags().BoolVar(&taskAutoSelect, "auto", false, "Auto-select highest priority pending task") taskCmd.Flags().BoolVar(&taskAutoSelect, "auto", false, i18n.T("cmd.ai.task.flag.auto"))
taskCmd.Flags().BoolVar(&taskClaim, "claim", false, "Claim the task after showing details") taskCmd.Flags().BoolVar(&taskClaim, "claim", false, i18n.T("cmd.ai.task.flag.claim"))
taskCmd.Flags().BoolVar(&taskShowContext, "context", false, "Show gathered context for AI collaboration") taskCmd.Flags().BoolVar(&taskShowContext, "context", false, i18n.T("cmd.ai.task.flag.context"))
} }
func addTasksCommand(parent *cobra.Command) { func addTasksCommand(parent *cobra.Command) {
@ -212,7 +197,7 @@ func addTaskCommand(parent *cobra.Command) {
} }
func printTaskList(tasks []agentic.Task) { func printTaskList(tasks []agentic.Task) {
fmt.Printf("\n%d task(s) found:\n\n", len(tasks)) fmt.Printf("\n%s\n\n", i18n.T("cmd.ai.tasks.found", map[string]interface{}{"Count": len(tasks)}))
for _, task := range tasks { for _, task := range tasks {
id := taskIDStyle.Render(task.ID) id := taskIDStyle.Render(task.ID)
@ -231,37 +216,37 @@ func printTaskList(tasks []agentic.Task) {
} }
fmt.Println() fmt.Println()
fmt.Printf("%s\n", dimStyle.Render("Use 'core ai task <id>' to view details")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint")))
} }
func printTaskDetails(task *agentic.Task) { func printTaskDetails(task *agentic.Task) {
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", dimStyle.Render("ID:"), taskIDStyle.Render(task.ID)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID))
fmt.Printf("%s %s\n", dimStyle.Render("Title:"), taskTitleStyle.Render(task.Title)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title))
fmt.Printf("%s %s\n", dimStyle.Render("Priority:"), formatTaskPriority(task.Priority)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority))
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), formatTaskStatus(task.Status)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.status")), formatTaskStatus(task.Status))
if task.Project != "" { if task.Project != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Project:"), task.Project) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.project")), task.Project)
} }
if len(task.Labels) > 0 { if len(task.Labels) > 0 {
fmt.Printf("%s %s\n", dimStyle.Render("Labels:"), taskLabelStyle.Render(strings.Join(task.Labels, ", "))) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.labels")), taskLabelStyle.Render(strings.Join(task.Labels, ", ")))
} }
if task.ClaimedBy != "" { if task.ClaimedBy != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Claimed by:"), task.ClaimedBy) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.claimed_by")), task.ClaimedBy)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Created:"), formatAge(task.CreatedAt)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt))
fmt.Println() fmt.Println()
fmt.Printf("%s\n", dimStyle.Render("Description:")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description")))
fmt.Println(task.Description) fmt.Println(task.Description)
if len(task.Files) > 0 { if len(task.Files) > 0 {
fmt.Println() fmt.Println()
fmt.Printf("%s\n", dimStyle.Render("Related files:")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files")))
for _, f := range task.Files { for _, f := range task.Files {
fmt.Printf(" - %s\n", f) fmt.Printf(" - %s\n", f)
} }
@ -269,20 +254,20 @@ func printTaskDetails(task *agentic.Task) {
if len(task.Dependencies) > 0 { if len(task.Dependencies) > 0 {
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", dimStyle.Render("Blocked by:"), strings.Join(task.Dependencies, ", ")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", "))
} }
} }
func formatTaskPriority(p agentic.TaskPriority) string { func formatTaskPriority(p agentic.TaskPriority) string {
switch p { switch p {
case agentic.PriorityCritical: case agentic.PriorityCritical:
return taskPriorityHighStyle.Render("[CRITICAL]") return taskPriorityHighStyle.Render("[" + i18n.T("cmd.ai.priority.critical") + "]")
case agentic.PriorityHigh: case agentic.PriorityHigh:
return taskPriorityHighStyle.Render("[HIGH]") return taskPriorityHighStyle.Render("[" + i18n.T("cmd.ai.priority.high") + "]")
case agentic.PriorityMedium: case agentic.PriorityMedium:
return taskPriorityMediumStyle.Render("[MEDIUM]") return taskPriorityMediumStyle.Render("[" + i18n.T("cmd.ai.priority.medium") + "]")
case agentic.PriorityLow: case agentic.PriorityLow:
return taskPriorityLowStyle.Render("[LOW]") return taskPriorityLowStyle.Render("[" + i18n.T("cmd.ai.priority.low") + "]")
default: default:
return dimStyle.Render("[" + string(p) + "]") return dimStyle.Render("[" + string(p) + "]")
} }
@ -291,13 +276,13 @@ func formatTaskPriority(p agentic.TaskPriority) string {
func formatTaskStatus(s agentic.TaskStatus) string { func formatTaskStatus(s agentic.TaskStatus) string {
switch s { switch s {
case agentic.StatusPending: case agentic.StatusPending:
return taskStatusPendingStyle.Render("pending") return taskStatusPendingStyle.Render(i18n.T("cmd.ai.status.pending"))
case agentic.StatusInProgress: case agentic.StatusInProgress:
return taskStatusInProgressStyle.Render("in_progress") return taskStatusInProgressStyle.Render(i18n.T("cmd.ai.status.in_progress"))
case agentic.StatusCompleted: case agentic.StatusCompleted:
return taskStatusCompletedStyle.Render("completed") return taskStatusCompletedStyle.Render(i18n.T("cmd.ai.status.completed"))
case agentic.StatusBlocked: case agentic.StatusBlocked:
return taskStatusBlockedStyle.Render("blocked") return taskStatusBlockedStyle.Render(i18n.T("cmd.ai.status.blocked"))
default: default:
return dimStyle.Render(string(s)) return dimStyle.Render(string(s))
} }

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/agentic" "github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,23 +28,19 @@ var (
var taskUpdateCmd = &cobra.Command{ var taskUpdateCmd = &cobra.Command{
Use: "task:update [task-id]", Use: "task:update [task-id]",
Short: "Update task status or progress", Short: i18n.T("cmd.ai.task_update.short"),
Long: `Updates a task's status, progress, or adds notes. Long: i18n.T("cmd.ai.task_update.long"),
Args: cobra.ExactArgs(1),
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 { RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0] taskID := args[0]
if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" { if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
return fmt.Errorf("at least one of --status, --progress, or --notes required") return fmt.Errorf(i18n.T("cmd.ai.task_update.flag_required"))
} }
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -60,29 +57,25 @@ Examples:
} }
if err := client.UpdateTask(ctx, taskID, update); err != nil { if err := client.UpdateTask(ctx, taskID, update); err != nil {
return fmt.Errorf("failed to update task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.update_task"), err)
} }
fmt.Printf("%s Task %s updated successfully\n", successStyle.Render(">>"), taskID) fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_update.success", map[string]interface{}{"ID": taskID}))
return nil return nil
}, },
} }
var taskCompleteCmd = &cobra.Command{ var taskCompleteCmd = &cobra.Command{
Use: "task:complete [task-id]", Use: "task:complete [task-id]",
Short: "Mark a task as completed", Short: i18n.T("cmd.ai.task_complete.short"),
Long: `Marks a task as completed with optional output and artifacts. Long: i18n.T("cmd.ai.task_complete.long"),
Args: cobra.ExactArgs(1),
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 { RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0] taskID := args[0]
cfg, err := agentic.LoadConfig("") cfg, err := agentic.LoadConfig("")
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
} }
client := agentic.NewClientFromConfig(cfg) client := agentic.NewClientFromConfig(cfg)
@ -97,13 +90,13 @@ Examples:
} }
if err := client.CompleteTask(ctx, taskID, result); err != nil { if err := client.CompleteTask(ctx, taskID, result); err != nil {
return fmt.Errorf("failed to complete task: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.complete_task"), err)
} }
if taskCompleteFailed { if taskCompleteFailed {
fmt.Printf("%s Task %s marked as failed\n", errorStyle.Render(">>"), taskID) fmt.Printf("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID}))
} else { } else {
fmt.Printf("%s Task %s completed successfully\n", successStyle.Render(">>"), taskID) fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_complete.success", map[string]interface{}{"ID": taskID}))
} }
return nil return nil
}, },
@ -111,14 +104,14 @@ Examples:
func init() { func init() {
// task:update command flags // task:update command flags
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", "New status (pending, in_progress, completed, blocked)") taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status"))
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, "Progress percentage (0-100)") taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress"))
taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", "Notes about the update") taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", i18n.T("cmd.ai.task_update.flag.notes"))
// task:complete command flags // task:complete command flags
taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", "Summary of the completed work") taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", i18n.T("cmd.ai.task_complete.flag.output"))
taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, "Mark the task as failed") taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, i18n.T("cmd.ai.task_complete.flag.failed"))
taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", "Error message if failed") taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", i18n.T("cmd.ai.task_complete.flag.error"))
} }
func addTaskUpdateCommand(parent *cobra.Command) { func addTaskUpdateCommand(parent *cobra.Command) {

View file

@ -10,42 +10,26 @@
// - claude: Claude Code CLI integration (planned) // - claude: Claude Code CLI integration (planned)
package ai package ai
import "github.com/spf13/cobra" import (
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
var aiCmd = &cobra.Command{ var aiCmd = &cobra.Command{
Use: "ai", Use: "ai",
Short: "AI agent task management", Short: i18n.T("cmd.ai.short"),
Long: `Manage tasks from the core-agentic service for AI-assisted development. Long: i18n.T("cmd.ai.long"),
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{ var claudeCmd = &cobra.Command{
Use: "claude", Use: "claude",
Short: "Claude Code integration", Short: i18n.T("cmd.ai.claude.short"),
Long: `Tools for working with Claude Code. Long: i18n.T("cmd.ai.claude.long"),
Commands:
run Run Claude in the current directory
config Manage Claude configuration`,
} }
var claudeRunCmd = &cobra.Command{ var claudeRunCmd = &cobra.Command{
Use: "run", Use: "run",
Short: "Run Claude Code in the current directory", Short: i18n.T("cmd.ai.claude.run.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runClaudeCode() return runClaudeCode()
}, },
@ -53,7 +37,7 @@ var claudeRunCmd = &cobra.Command{
var claudeConfigCmd = &cobra.Command{ var claudeConfigCmd = &cobra.Command{
Use: "config", Use: "config",
Short: "Manage Claude configuration", Short: i18n.T("cmd.ai.claude.config.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return showClaudeConfig() return showClaudeConfig()
}, },

View file

@ -5,6 +5,7 @@ import (
"embed" "embed"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -54,16 +55,8 @@ var (
var buildCmd = &cobra.Command{ var buildCmd = &cobra.Command{
Use: "build", Use: "build",
Short: "Build projects with auto-detection and cross-compilation", Short: i18n.T("cmd.build.short"),
Long: `Builds the current project with automatic type detection. Long: i18n.T("cmd.build.long"),
Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.
Configuration can be provided via .core/build.yaml or command-line flags.
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize) return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize)
}, },
@ -71,7 +64,7 @@ Examples:
var fromPathCmd = &cobra.Command{ var fromPathCmd = &cobra.Command{
Use: "from-path", Use: "from-path",
Short: "Build from a local directory.", Short: i18n.T("cmd.build.from_path.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if fromPath == "" { if fromPath == "" {
return errPathRequired return errPathRequired
@ -82,7 +75,7 @@ var fromPathCmd = &cobra.Command{
var pwaCmd = &cobra.Command{ var pwaCmd = &cobra.Command{
Use: "pwa", Use: "pwa",
Short: "Build from a live PWA URL.", Short: i18n.T("cmd.build.pwa.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if pwaURL == "" { if pwaURL == "" {
return errURLRequired return errURLRequired
@ -93,14 +86,8 @@ var pwaCmd = &cobra.Command{
var sdkBuildCmd = &cobra.Command{ var sdkBuildCmd = &cobra.Command{
Use: "sdk", Use: "sdk",
Short: "Generate API SDKs from OpenAPI spec", Short: i18n.T("cmd.build.sdk.short"),
Long: `Generates typed API clients from OpenAPI specifications. Long: i18n.T("cmd.build.sdk.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun) return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
}, },
@ -108,34 +95,34 @@ Examples:
func init() { func init() {
// Main build command flags // Main build command flags
buildCmd.Flags().StringVar(&buildType, "type", "", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified") buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.build.flag.type"))
buildCmd.Flags().BoolVar(&ciMode, "ci", false, "CI mode - minimal output with JSON artifact list at the end") buildCmd.Flags().BoolVar(&ciMode, "ci", false, i18n.T("cmd.build.flag.ci"))
buildCmd.Flags().StringVar(&targets, "targets", "", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)") buildCmd.Flags().StringVar(&targets, "targets", "", i18n.T("cmd.build.flag.targets"))
buildCmd.Flags().StringVar(&outputDir, "output", "", "Output directory for artifacts (default: dist)") buildCmd.Flags().StringVar(&outputDir, "output", "", i18n.T("cmd.build.flag.output"))
buildCmd.Flags().BoolVar(&doArchive, "archive", true, "Create archives (tar.gz for linux/darwin, zip for windows)") buildCmd.Flags().BoolVar(&doArchive, "archive", true, i18n.T("cmd.build.flag.archive"))
buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, "Generate SHA256 checksums and CHECKSUMS.txt") buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, i18n.T("cmd.build.flag.checksum"))
// Docker/LinuxKit specific // Docker/LinuxKit specific
buildCmd.Flags().StringVar(&configPath, "config", "", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)") buildCmd.Flags().StringVar(&configPath, "config", "", i18n.T("cmd.build.flag.config"))
buildCmd.Flags().StringVar(&format, "format", "", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)") buildCmd.Flags().StringVar(&format, "format", "", i18n.T("cmd.build.flag.format"))
buildCmd.Flags().BoolVar(&push, "push", false, "Push Docker image after build") buildCmd.Flags().BoolVar(&push, "push", false, i18n.T("cmd.build.flag.push"))
buildCmd.Flags().StringVar(&imageName, "image", "", "Docker image name (e.g., host-uk/core-devops)") buildCmd.Flags().StringVar(&imageName, "image", "", i18n.T("cmd.build.flag.image"))
// Signing flags // Signing flags
buildCmd.Flags().BoolVar(&noSign, "no-sign", false, "Skip all code signing") buildCmd.Flags().BoolVar(&noSign, "no-sign", false, i18n.T("cmd.build.flag.no_sign"))
buildCmd.Flags().BoolVar(&notarize, "notarize", false, "Enable macOS notarization (requires Apple credentials)") buildCmd.Flags().BoolVar(&notarize, "notarize", false, i18n.T("cmd.build.flag.notarize"))
// from-path subcommand flags // from-path subcommand flags
fromPathCmd.Flags().StringVar(&fromPath, "path", "", "The path to the static web application files.") fromPathCmd.Flags().StringVar(&fromPath, "path", "", i18n.T("cmd.build.from_path.flag.path"))
// pwa subcommand flags // pwa subcommand flags
pwaCmd.Flags().StringVar(&pwaURL, "url", "", "The URL of the PWA to build.") pwaCmd.Flags().StringVar(&pwaURL, "url", "", i18n.T("cmd.build.pwa.flag.url"))
// sdk subcommand flags // sdk subcommand flags
sdkBuildCmd.Flags().StringVar(&sdkSpec, "spec", "", "Path to OpenAPI spec file") sdkBuildCmd.Flags().StringVar(&sdkSpec, "spec", "", i18n.T("cmd.build.sdk.flag.spec"))
sdkBuildCmd.Flags().StringVar(&sdkLang, "lang", "", "Generate only this language (typescript, python, go, php)") sdkBuildCmd.Flags().StringVar(&sdkLang, "lang", "", i18n.T("cmd.build.sdk.flag.lang"))
sdkBuildCmd.Flags().StringVar(&sdkVersion, "version", "", "Version to embed in generated SDKs") sdkBuildCmd.Flags().StringVar(&sdkVersion, "version", "", i18n.T("cmd.build.sdk.flag.version"))
sdkBuildCmd.Flags().BoolVar(&sdkDryRun, "dry-run", false, "Show what would be generated without writing files") sdkBuildCmd.Flags().BoolVar(&sdkDryRun, "dry-run", false, i18n.T("cmd.build.sdk.flag.dry_run"))
// Add subcommands // Add subcommands
buildCmd.AddCommand(fromPathCmd) buildCmd.AddCommand(fromPathCmd)

View file

@ -17,6 +17,7 @@ import (
buildpkg "github.com/host-uk/core/pkg/build" buildpkg "github.com/host-uk/core/pkg/build"
"github.com/host-uk/core/pkg/build/builders" "github.com/host-uk/core/pkg/build/builders"
"github.com/host-uk/core/pkg/build/signing" "github.com/host-uk/core/pkg/build/signing"
"github.com/host-uk/core/pkg/i18n"
) )
// runProjectBuild handles the main `core build` command with auto-detection. // runProjectBuild handles the main `core build` command with auto-detection.
@ -24,13 +25,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// Get current working directory as project root // Get current working directory as project root
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.working_dir"), err)
} }
// Load configuration from .core/build.yaml (or defaults) // Load configuration from .core/build.yaml (or defaults)
buildCfg, err := buildpkg.LoadConfig(projectDir) buildCfg, err := buildpkg.LoadConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.load_config"), err)
} }
// Detect project type if not specified // Detect project type if not specified
@ -40,11 +41,10 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} else { } else {
projectType, err = buildpkg.PrimaryType(projectDir) projectType, err = buildpkg.PrimaryType(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to detect project type: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.detect_type"), err)
} }
if projectType == "" { if projectType == "" {
return fmt.Errorf("no supported project type detected in %s\n"+ return fmt.Errorf("%s", i18n.T("cmd.build.error.no_project_type", map[string]interface{}{"Dir": projectDir}))
"Supported types: go (go.mod), wails (wails.json), node (package.json), php (composer.json)", projectDir)
} }
} }
@ -82,11 +82,11 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// Print build info (unless CI mode) // Print build info (unless CI mode)
if !ciMode { if !ciMode {
fmt.Printf("%s Building project\n", buildHeaderStyle.Render("Build:")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.build")), i18n.T("cmd.build.building_project"))
fmt.Printf(" Type: %s\n", buildTargetStyle.Render(string(projectType))) fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.type"), buildTargetStyle.Render(string(projectType)))
fmt.Printf(" Output: %s\n", buildTargetStyle.Render(outputDir)) fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.output"), buildTargetStyle.Render(outputDir))
fmt.Printf(" Binary: %s\n", buildTargetStyle.Render(binaryName)) fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.binary"), buildTargetStyle.Render(binaryName))
fmt.Printf(" Targets: %s\n", buildTargetStyle.Render(formatTargets(buildTargets))) fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.targets"), buildTargetStyle.Render(formatTargets(buildTargets)))
fmt.Println() fmt.Println()
} }
@ -120,13 +120,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
artifacts, err := builder.Build(ctx, cfg, buildTargets) artifacts, err := builder.Build(ctx, cfg, buildTargets)
if err != nil { if err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Build failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.build_failed"), err)
} }
return err return err
} }
if !ciMode { if !ciMode {
fmt.Printf("%s Built %d artifact(s)\n", buildSuccessStyle.Render("Success:"), len(artifacts)) fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.success")), i18n.T("cmd.build.built_artifacts", map[string]interface{}{"Count": len(artifacts)}))
fmt.Println() fmt.Println()
for _, artifact := range artifacts { for _, artifact := range artifacts {
relPath, err := filepath.Rel(projectDir, artifact.Path) relPath, err := filepath.Rel(projectDir, artifact.Path)
@ -153,7 +153,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if signCfg.Enabled && runtime.GOOS == "darwin" { if signCfg.Enabled && runtime.GOOS == "darwin" {
if !ciMode { if !ciMode {
fmt.Println() fmt.Println()
fmt.Printf("%s Signing binaries...\n", buildHeaderStyle.Render("Sign:")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
} }
// Convert buildpkg.Artifact to signing.Artifact // Convert buildpkg.Artifact to signing.Artifact
@ -164,7 +164,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil { if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Signing failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.signing_failed"), err)
} }
return err return err
} }
@ -172,7 +172,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if signCfg.MacOS.Notarize { if signCfg.MacOS.Notarize {
if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil { if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Notarization failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
} }
return err return err
} }
@ -184,13 +184,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if doArchive && len(artifacts) > 0 { if doArchive && len(artifacts) > 0 {
if !ciMode { if !ciMode {
fmt.Println() fmt.Println()
fmt.Printf("%s Creating archives...\n", buildHeaderStyle.Render("Archive:")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
} }
archivedArtifacts, err = buildpkg.ArchiveAll(artifacts) archivedArtifacts, err = buildpkg.ArchiveAll(artifacts)
if err != nil { if err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Archive failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
} }
return err return err
} }
@ -240,7 +240,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// JSON output for CI // JSON output for CI
output, err := json.MarshalIndent(outputArtifacts, "", " ") output, err := json.MarshalIndent(outputArtifacts, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal artifacts: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.marshal_artifacts"), err)
} }
fmt.Println(string(output)) fmt.Println(string(output))
} }
@ -252,13 +252,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) { func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) {
if !ciMode { if !ciMode {
fmt.Println() fmt.Println()
fmt.Printf("%s Computing checksums...\n", buildHeaderStyle.Render("Checksum:")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
} }
checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts) checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts)
if err != nil { if err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Checksum failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
} }
return nil, err return nil, err
} }
@ -267,7 +267,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt") checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil { if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s Failed to write CHECKSUMS.txt: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.write_checksums"), err)
} }
return nil, err return nil, err
} }
@ -276,7 +276,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
if signCfg.Enabled { if signCfg.Enabled {
if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil { if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil {
if !ciMode { if !ciMode {
fmt.Printf("%s GPG signing failed: %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.gpg_signing_failed"), err)
} }
return nil, err return nil, err
} }
@ -321,7 +321,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
osArch := strings.Split(part, "/") osArch := strings.Split(part, "/")
if len(osArch) != 2 { if len(osArch) != 2 {
return nil, fmt.Errorf("invalid target format %q, expected OS/arch (e.g., linux/amd64)", part) return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.invalid_target", map[string]interface{}{"Target": part}))
} }
targets = append(targets, buildpkg.Target{ targets = append(targets, buildpkg.Target{
@ -331,7 +331,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
} }
if len(targets) == 0 { if len(targets) == 0 {
return nil, fmt.Errorf("no valid targets specified") return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.no_targets"))
} }
return targets, nil return targets, nil
@ -360,10 +360,10 @@ func getBuilder(projectType buildpkg.ProjectType) (buildpkg.Builder, error) {
case buildpkg.ProjectTypeTaskfile: case buildpkg.ProjectTypeTaskfile:
return builders.NewTaskfileBuilder(), nil return builders.NewTaskfileBuilder(), nil
case buildpkg.ProjectTypeNode: case buildpkg.ProjectTypeNode:
return nil, fmt.Errorf("Node.js builder not yet implemented") return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.node_not_implemented"))
case buildpkg.ProjectTypePHP: case buildpkg.ProjectTypePHP:
return nil, fmt.Errorf("PHP builder not yet implemented") return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented"))
default: default:
return nil, fmt.Errorf("unsupported project type: %s", projectType) return nil, fmt.Errorf("%s: %s", i18n.T("cmd.build.error.unsupported_type"), projectType)
} }
} }

View file

@ -18,6 +18,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/leaanthony/debme" "github.com/leaanthony/debme"
"github.com/leaanthony/gosod" "github.com/leaanthony/gosod"
"golang.org/x/net/html" "golang.org/x/net/html"
@ -26,22 +27,22 @@ import (
// Error sentinels for build commands // Error sentinels for build commands
var ( var (
errPathRequired = errors.New("the --path flag is required") errPathRequired = errors.New("the --path flag is required")
errURLRequired = errors.New("a URL argument is required") errURLRequired = errors.New("the --url flag is required")
) )
// runPwaBuild downloads a PWA from URL and builds it. // runPwaBuild downloads a PWA from URL and builds it.
func runPwaBuild(pwaURL string) error { func runPwaBuild(pwaURL string) error {
fmt.Printf("Starting PWA build from URL: %s\n", pwaURL) fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.starting"), pwaURL)
tempDir, err := os.MkdirTemp("", "core-pwa-build-*") tempDir, err := os.MkdirTemp("", "core-pwa-build-*")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.create_temp_dir"), err)
} }
// defer os.RemoveAll(tempDir) // Keep temp dir for debugging // defer os.RemoveAll(tempDir) // Keep temp dir for debugging
fmt.Printf("Downloading PWA to temporary directory: %s\n", tempDir) fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.downloading_to"), tempDir)
if err := downloadPWA(pwaURL, tempDir); err != nil { if err := downloadPWA(pwaURL, tempDir); err != nil {
return fmt.Errorf("failed to download PWA: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.download_failed"), err)
} }
return runBuild(tempDir) return runBuild(tempDir)
@ -52,48 +53,48 @@ func downloadPWA(baseURL, destDir string) error {
// Fetch the main HTML page // Fetch the main HTML page
resp, err := http.Get(baseURL) resp, err := http.Get(baseURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch URL %s: %w", baseURL, err) return fmt.Errorf("%s %s: %w", i18n.T("cmd.build.pwa.error.fetch_url"), baseURL, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read response body: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.read_response"), err)
} }
// Find the manifest URL from the HTML // Find the manifest URL from the HTML
manifestURL, err := findManifestURL(string(body), baseURL) manifestURL, err := findManifestURL(string(body), baseURL)
if err != nil { if err != nil {
// If no manifest, it's not a PWA, but we can still try to package it as a simple site. // If no manifest, it's not a PWA, but we can still try to package it as a simple site.
fmt.Println("Warning: no manifest file found. Proceeding with basic site download.") fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.warning"), i18n.T("cmd.build.pwa.no_manifest"))
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil { if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
return fmt.Errorf("failed to write index.html: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.write_index"), err)
} }
return nil return nil
} }
fmt.Printf("Found manifest: %s\n", manifestURL) fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.found_manifest"), manifestURL)
// Fetch and parse the manifest // Fetch and parse the manifest
manifest, err := fetchManifest(manifestURL) manifest, err := fetchManifest(manifestURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch or parse manifest: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.fetch_manifest"), err)
} }
// Download all assets listed in the manifest // Download all assets listed in the manifest
assets := collectAssets(manifest, manifestURL) assets := collectAssets(manifest, manifestURL)
for _, assetURL := range assets { for _, assetURL := range assets {
if err := downloadAsset(assetURL, destDir); err != nil { if err := downloadAsset(assetURL, destDir); err != nil {
fmt.Printf("Warning: failed to download asset %s: %v\n", assetURL, err) fmt.Printf("%s %s %s: %v\n", i18n.T("cmd.build.pwa.warning"), i18n.T("cmd.build.pwa.asset_download_failed"), assetURL, err)
} }
} }
// Also save the root index.html // Also save the root index.html
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil { if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
return fmt.Errorf("failed to write index.html: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.write_index"), err)
} }
fmt.Println("PWA download complete.") fmt.Println(i18n.T("cmd.build.pwa.download_complete"))
return nil return nil
} }
@ -129,7 +130,7 @@ func findManifestURL(htmlContent, baseURL string) (string, error) {
f(doc) f(doc)
if manifestPath == "" { if manifestPath == "" {
return "", fmt.Errorf("no <link rel=\"manifest\"> tag found") return "", fmt.Errorf("%s", i18n.T("cmd.build.pwa.error.no_manifest_tag"))
} }
base, err := url.Parse(baseURL) base, err := url.Parse(baseURL)
@ -218,14 +219,14 @@ func downloadAsset(assetURL, destDir string) error {
// runBuild builds a desktop application from a local directory. // runBuild builds a desktop application from a local directory.
func runBuild(fromPath string) error { func runBuild(fromPath string) error {
fmt.Printf("Starting build from path: %s\n", fromPath) fmt.Printf("%s %s\n", i18n.T("cmd.build.from_path.starting"), fromPath)
info, err := os.Stat(fromPath) info, err := os.Stat(fromPath)
if err != nil { if err != nil {
return fmt.Errorf("invalid path specified: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.invalid_path"), err)
} }
if !info.IsDir() { if !info.IsDir() {
return fmt.Errorf("path specified must be a directory") return fmt.Errorf("%s", i18n.T("cmd.build.from_path.error.must_be_directory"))
} }
buildDir := ".core/build/app" buildDir := ".core/build/app"
@ -237,33 +238,33 @@ func runBuild(fromPath string) error {
outputExe := appName outputExe := appName
if err := os.RemoveAll(buildDir); err != nil { if err := os.RemoveAll(buildDir); err != nil {
return fmt.Errorf("failed to clean build directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.clean_build_dir"), err)
} }
// 1. Generate the project from the embedded template // 1. Generate the project from the embedded template
fmt.Println("Generating application from template...") fmt.Println(i18n.T("cmd.build.from_path.generating_template"))
templateFS, err := debme.FS(guiTemplate, "tmpl/gui") templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
if err != nil { if err != nil {
return fmt.Errorf("failed to anchor template filesystem: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.anchor_template"), err)
} }
sod := gosod.New(templateFS) sod := gosod.New(templateFS)
if sod == nil { if sod == nil {
return fmt.Errorf("failed to create new sod instance") return fmt.Errorf("%s", i18n.T("cmd.build.from_path.error.create_sod"))
} }
templateData := map[string]string{"AppName": appName} templateData := map[string]string{"AppName": appName}
if err := sod.Extract(buildDir, templateData); err != nil { if err := sod.Extract(buildDir, templateData); err != nil {
return fmt.Errorf("failed to extract template: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.extract_template"), err)
} }
// 2. Copy the user's web app files // 2. Copy the user's web app files
fmt.Println("Copying application files...") fmt.Println(i18n.T("cmd.build.from_path.copying_files"))
if err := copyDir(fromPath, htmlDir); err != nil { if err := copyDir(fromPath, htmlDir); err != nil {
return fmt.Errorf("failed to copy application files: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.copy_files"), err)
} }
// 3. Compile the application // 3. Compile the application
fmt.Println("Compiling application...") fmt.Println(i18n.T("cmd.build.from_path.compiling"))
// Run go mod tidy // Run go mod tidy
cmd := exec.Command("go", "mod", "tidy") cmd := exec.Command("go", "mod", "tidy")
@ -271,7 +272,7 @@ func runBuild(fromPath string) error {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("go mod tidy failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.go_mod_tidy"), err)
} }
// Run go build // Run go build
@ -280,10 +281,10 @@ func runBuild(fromPath string) error {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("go build failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.go_build"), err)
} }
fmt.Printf("\nBuild successful! Executable created at: %s/%s\n", buildDir, outputExe) fmt.Printf("\n%s %s/%s\n", i18n.T("cmd.build.from_path.success"), buildDir, outputExe)
return nil return nil
} }

View file

@ -11,6 +11,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/sdk" "github.com/host-uk/core/pkg/sdk"
) )
@ -20,7 +21,7 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.working_dir"), err)
} }
// Load config // Load config
@ -34,48 +35,48 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
s.SetVersion(version) s.SetVersion(version)
} }
fmt.Printf("%s Generating SDKs\n", buildHeaderStyle.Render("Build SDK:")) fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.sdk.label")), i18n.T("cmd.build.sdk.generating"))
if dryRun { if dryRun {
fmt.Printf(" %s\n", buildDimStyle.Render("(dry-run mode)")) fmt.Printf(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.sdk.dry_run_mode")))
} }
fmt.Println() fmt.Println()
// Detect spec // Detect spec
detectedSpec, err := s.DetectSpec() detectedSpec, err := s.DetectSpec()
if err != nil { if err != nil {
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
return err return err
} }
fmt.Printf(" Spec: %s\n", buildTargetStyle.Render(detectedSpec)) fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.spec_label"), buildTargetStyle.Render(detectedSpec))
if dryRun { if dryRun {
if lang != "" { if lang != "" {
fmt.Printf(" Language: %s\n", buildTargetStyle.Render(lang)) fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.language_label"), buildTargetStyle.Render(lang))
} else { } else {
fmt.Printf(" Languages: %s\n", buildTargetStyle.Render(strings.Join(config.Languages, ", "))) fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.languages_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
} }
fmt.Println() fmt.Println()
fmt.Printf("%s Would generate SDKs (dry-run)\n", buildSuccessStyle.Render("OK:")) fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.ok")), i18n.T("cmd.build.sdk.would_generate"))
return nil return nil
} }
if lang != "" { if lang != "" {
// Generate single language // Generate single language
if err := s.GenerateLanguage(ctx, lang); err != nil { if err := s.GenerateLanguage(ctx, lang); err != nil {
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
return err return err
} }
fmt.Printf(" Generated: %s\n", buildTargetStyle.Render(lang)) fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(lang))
} else { } else {
// Generate all // Generate all
if err := s.Generate(ctx); err != nil { if err := s.Generate(ctx); err != nil {
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
return err return err
} }
fmt.Printf(" Generated: %s\n", buildTargetStyle.Render(strings.Join(config.Languages, ", "))) fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
} }
fmt.Println() fmt.Println()
fmt.Printf("%s SDK generation complete\n", buildSuccessStyle.Render("Success:")) fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.success")), i18n.T("cmd.build.sdk.complete"))
return nil return nil
} }

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
@ -11,19 +12,19 @@ import (
func runChangelog(fromRef, toRef string) error { func runChangelog(fromRef, toRef string) error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
} }
// Load config for changelog settings // Load config for changelog settings
cfg, err := release.LoadConfig(projectDir) cfg, err := release.LoadConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.load_config"), err)
} }
// Generate changelog // Generate changelog
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog) changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog)
if err != nil { if err != nil {
return fmt.Errorf("failed to generate changelog: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.generate_changelog"), err)
} }
fmt.Println(changelog) fmt.Println(changelog)

View file

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
@ -14,33 +15,34 @@ import (
func runCIReleaseInit() error { func runCIReleaseInit() error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
} }
// Check if config already exists // Check if config already exists
if release.ConfigExists(projectDir) { if release.ConfigExists(projectDir) {
fmt.Printf("%s Configuration already exists at %s\n", fmt.Printf("%s %s %s\n",
releaseDimStyle.Render("Note:"), releaseDimStyle.Render(i18n.T("cmd.ci.label.note")),
i18n.T("cmd.ci.init.config_exists"),
release.ConfigPath(projectDir)) release.ConfigPath(projectDir))
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print("Overwrite? [y/N]: ") fmt.Print(i18n.T("cmd.ci.init.overwrite_prompt"))
response, _ := reader.ReadString('\n') response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response)) response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" { if response != "y" && response != "yes" {
fmt.Println("Aborted.") fmt.Println(i18n.T("cli.confirm.abort"))
return nil return nil
} }
} }
fmt.Printf("%s Creating release configuration\n", releaseHeaderStyle.Render("Init:")) fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.init")), i18n.T("cmd.ci.init.creating"))
fmt.Println() fmt.Println()
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
// Project name // Project name
defaultName := filepath.Base(projectDir) defaultName := filepath.Base(projectDir)
fmt.Printf("Project name [%s]: ", defaultName) fmt.Printf("%s [%s]: ", i18n.T("cmd.ci.init.project_name"), defaultName)
name, _ := reader.ReadString('\n') name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
if name == "" { if name == "" {
@ -48,7 +50,7 @@ func runCIReleaseInit() error {
} }
// Repository // Repository
fmt.Print("GitHub repository (owner/repo): ") fmt.Printf("%s ", i18n.T("cmd.ci.init.github_repo"))
repo, _ := reader.ReadString('\n') repo, _ := reader.ReadString('\n')
repo = strings.TrimSpace(repo) repo = strings.TrimSpace(repo)
@ -59,12 +61,13 @@ func runCIReleaseInit() error {
// Write config // Write config
if err := release.WriteConfig(cfg, projectDir); err != nil { if err := release.WriteConfig(cfg, projectDir); err != nil {
return fmt.Errorf("failed to write config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.write_config"), err)
} }
fmt.Println() fmt.Println()
fmt.Printf("%s Configuration written to %s\n", fmt.Printf("%s %s %s\n",
releaseSuccessStyle.Render("Success:"), releaseSuccessStyle.Render(i18n.T("cmd.ci.label.success")),
i18n.T("cmd.ci.init.config_written"),
release.ConfigPath(projectDir)) release.ConfigPath(projectDir))
return nil return nil

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
@ -16,13 +17,13 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
// Get current directory // Get current directory
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
} }
// Load configuration // Load configuration
cfg, err := release.LoadConfig(projectDir) cfg, err := release.LoadConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to load config: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.load_config"), err)
} }
// Apply CLI overrides // Apply CLI overrides
@ -43,35 +44,35 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
} }
// Print header // Print header
fmt.Printf("%s Publishing release\n", releaseHeaderStyle.Render("CI:")) fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing"))
if dryRun { if dryRun {
fmt.Printf(" %s\n", releaseDimStyle.Render("(dry-run) use --we-are-go-for-launch to publish")) fmt.Printf(" %s\n", releaseDimStyle.Render(i18n.T("cmd.ci.dry_run_hint")))
} else { } else {
fmt.Printf(" %s\n", releaseSuccessStyle.Render("GO FOR LAUNCH")) fmt.Printf(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch")))
} }
fmt.Println() fmt.Println()
// Check for publishers // Check for publishers
if len(cfg.Publishers) == 0 { if len(cfg.Publishers) == 0 {
return fmt.Errorf("no publishers configured in .core/release.yaml") return fmt.Errorf(i18n.T("cmd.ci.error.no_publishers"))
} }
// Publish pre-built artifacts // Publish pre-built artifacts
rel, err := release.Publish(ctx, cfg, dryRun) rel, err := release.Publish(ctx, cfg, dryRun)
if err != nil { if err != nil {
fmt.Printf("%s %v\n", releaseErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", releaseErrorStyle.Render(i18n.T("cmd.ci.label.error")), err)
return err return err
} }
// Print summary // Print summary
fmt.Println() fmt.Println()
fmt.Printf("%s Publish completed!\n", releaseSuccessStyle.Render("Success:")) fmt.Printf("%s %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.label.success")), i18n.T("cmd.ci.publish_completed"))
fmt.Printf(" Version: %s\n", releaseValueStyle.Render(rel.Version)) fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.version"), releaseValueStyle.Render(rel.Version))
fmt.Printf(" Artifacts: %d\n", len(rel.Artifacts)) fmt.Printf(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts))
if !dryRun { if !dryRun {
for _, pub := range cfg.Publishers { for _, pub := range cfg.Publishers {
fmt.Printf(" Published: %s\n", releaseValueStyle.Render(pub.Type)) fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.published"), releaseValueStyle.Render(pub.Type))
} }
} }

View file

@ -3,6 +3,7 @@ package ci
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -31,13 +32,8 @@ var (
var ciCmd = &cobra.Command{ var ciCmd = &cobra.Command{
Use: "ci", Use: "ci",
Short: "Publish releases (dry-run by default)", Short: i18n.T("cmd.ci.short"),
Long: `Publishes pre-built artifacts from dist/ to configured targets. Long: i18n.T("cmd.ci.long"),
Run 'core build' first to create artifacts.
SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.
Configuration: .core/release.yaml`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
dryRun := !ciGoForLaunch dryRun := !ciGoForLaunch
return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease) return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
@ -46,8 +42,8 @@ Configuration: .core/release.yaml`,
var ciInitCmd = &cobra.Command{ var ciInitCmd = &cobra.Command{
Use: "init", Use: "init",
Short: "Initialize release configuration", Short: i18n.T("cmd.ci.init.short"),
Long: "Creates a .core/release.yaml configuration file interactively.", Long: i18n.T("cmd.ci.init.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCIReleaseInit() return runCIReleaseInit()
}, },
@ -55,8 +51,8 @@ var ciInitCmd = &cobra.Command{
var ciChangelogCmd = &cobra.Command{ var ciChangelogCmd = &cobra.Command{
Use: "changelog", Use: "changelog",
Short: "Generate changelog", Short: i18n.T("cmd.ci.changelog.short"),
Long: "Generates a changelog from conventional commits.", Long: i18n.T("cmd.ci.changelog.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runChangelog(changelogFromRef, changelogToRef) return runChangelog(changelogFromRef, changelogToRef)
}, },
@ -64,8 +60,8 @@ var ciChangelogCmd = &cobra.Command{
var ciVersionCmd = &cobra.Command{ var ciVersionCmd = &cobra.Command{
Use: "version", Use: "version",
Short: "Show or set version", Short: i18n.T("cmd.ci.version.short"),
Long: "Shows the determined version or validates a version string.", Long: i18n.T("cmd.ci.version.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCIReleaseVersion() return runCIReleaseVersion()
}, },
@ -73,14 +69,14 @@ var ciVersionCmd = &cobra.Command{
func init() { func init() {
// Main ci command flags // Main ci command flags
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, "Actually publish (default is dry-run for safety)") ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.ci.flag.go_for_launch"))
ciCmd.Flags().StringVar(&ciVersion, "version", "", "Version to release (e.g., v1.2.3)") ciCmd.Flags().StringVar(&ciVersion, "version", "", i18n.T("cmd.ci.flag.version"))
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, "Create release as a draft") ciCmd.Flags().BoolVar(&ciDraft, "draft", false, i18n.T("cmd.ci.flag.draft"))
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, "Mark release as a prerelease") ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, i18n.T("cmd.ci.flag.prerelease"))
// Changelog subcommand flags // Changelog subcommand flags
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", "Starting ref (default: previous tag)") ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", i18n.T("cmd.ci.changelog.flag.from"))
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", "Ending ref (default: HEAD)") ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", i18n.T("cmd.ci.changelog.flag.to"))
// Add subcommands // Add subcommands
ciCmd.AddCommand(ciInitCmd) ciCmd.AddCommand(ciInitCmd)

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release" "github.com/host-uk/core/pkg/release"
) )
@ -11,14 +12,14 @@ import (
func runCIReleaseVersion() error { func runCIReleaseVersion() error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
} }
version, err := release.DetermineVersion(projectDir) version, err := release.DetermineVersion(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to determine version: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.determine_version"), err)
} }
fmt.Printf("Version: %s\n", releaseValueStyle.Render(version)) fmt.Printf("%s %s\n", i18n.T("cmd.ci.label.version"), releaseValueStyle.Render(version))
return nil return nil
} }

View file

@ -30,6 +30,7 @@ package dev
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -55,31 +56,8 @@ var (
func AddCommands(root *cobra.Command) { func AddCommands(root *cobra.Command) {
devCmd := &cobra.Command{ devCmd := &cobra.Command{
Use: "dev", Use: "dev",
Short: "Multi-repo development workflow", Short: i18n.T("cmd.dev.short"),
Long: `Manage multiple git repositories and GitHub integration. Long: i18n.T("cmd.dev.long"),
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) root.AddCommand(devCmd)

View file

@ -1,6 +1,7 @@
package dev package dev
import ( import (
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -9,7 +10,7 @@ func addAPICommands(parent *cobra.Command) {
// Create the 'api' command // Create the 'api' command
apiCmd := &cobra.Command{ apiCmd := &cobra.Command{
Use: "api", Use: "api",
Short: "Tools for managing service APIs", Short: i18n.T("cmd.dev.api.short"),
} }
parent.AddCommand(apiCmd) parent.AddCommand(apiCmd)

View file

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -46,10 +47,8 @@ var (
func addCICommand(parent *cobra.Command) { func addCICommand(parent *cobra.Command) {
ciCmd := &cobra.Command{ ciCmd := &cobra.Command{
Use: "ci", Use: "ci",
Short: "Check CI status across all repos", Short: i18n.T("cmd.dev.ci.short"),
Long: `Fetches GitHub Actions workflow status for all repos. Long: i18n.T("cmd.dev.ci.long"),
Shows latest run status for each repo.
Requires the 'gh' CLI to be installed and authenticated.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
branch := ciBranch branch := ciBranch
if branch == "" { if branch == "" {
@ -59,9 +58,9 @@ Requires the 'gh' CLI to be installed and authenticated.`,
}, },
} }
ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", i18n.T("cmd.dev.ci.flag.registry"))
ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", "Filter by branch") ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", i18n.T("cmd.dev.ci.flag.branch"))
ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, "Show only failed runs") ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, i18n.T("cmd.dev.ci.flag.failed"))
parent.AddCommand(ciCmd) parent.AddCommand(ciCmd)
} }
@ -69,7 +68,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
func runCI(registryPath string, branch string, failedOnly bool) error { func runCI(registryPath string, branch string, failedOnly bool) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/") return fmt.Errorf(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry // Find or use provided registry
@ -105,7 +104,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
repoList := reg.List() repoList := reg.List()
for i, repo := range repoList { for i, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Checking"), i+1, len(repoList), repo.Name) fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.checking")), i+1, len(repoList), repo.Name)
runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch) runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch)
if err != nil { if err != nil {
@ -147,18 +146,18 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
// Print summary // Print summary
fmt.Println() fmt.Println()
fmt.Printf("%d repos checked", len(repoList)) fmt.Printf("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)}))
if success > 0 { if success > 0 {
fmt.Printf(" * %s", ciSuccessStyle.Render(fmt.Sprintf("%d passing", success))) fmt.Printf(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success})))
} }
if failed > 0 { if failed > 0 {
fmt.Printf(" * %s", ciFailureStyle.Render(fmt.Sprintf("%d failing", failed))) fmt.Printf(" * %s", ciFailureStyle.Render(i18n.T("cmd.dev.ci.failing", map[string]interface{}{"Count": failed})))
} }
if pending > 0 { if pending > 0 {
fmt.Printf(" * %s", ciPendingStyle.Render(fmt.Sprintf("%d pending", pending))) fmt.Printf(" * %s", ciPendingStyle.Render(i18n.T("cmd.dev.ci.pending", map[string]interface{}{"Count": pending})))
} }
if len(noCI) > 0 { if len(noCI) > 0 {
fmt.Printf(" * %s", ciSkippedStyle.Render(fmt.Sprintf("%d no CI", len(noCI)))) fmt.Printf(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)})))
} }
fmt.Println() fmt.Println()
fmt.Println() fmt.Println()
@ -183,7 +182,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
fmt.Println() fmt.Println()
for _, err := range fetchErrors { for _, err := range fetchErrors {
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.ci.error_label")), err)
} }
} }

View file

@ -7,6 +7,7 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -21,16 +22,15 @@ var (
func addCommitCommand(parent *cobra.Command) { func addCommitCommand(parent *cobra.Command) {
commitCmd := &cobra.Command{ commitCmd := &cobra.Command{
Use: "commit", Use: "commit",
Short: "Claude-assisted commits across repos", Short: i18n.T("cmd.dev.commit.short"),
Long: `Uses Claude to create commits for dirty repos. Long: i18n.T("cmd.dev.commit.long"),
Shows uncommitted changes and invokes Claude to generate commit messages.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCommit(commitRegistryPath, commitAll) return runCommit(commitRegistryPath, commitAll)
}, },
} }
commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", i18n.T("cmd.dev.commit.flag.registry"))
commitCmd.Flags().BoolVar(&commitAll, "all", false, "Commit all dirty repos without prompting") commitCmd.Flags().BoolVar(&commitAll, "all", false, i18n.T("cmd.dev.commit.flag.all"))
parent.AddCommand(commitCmd) parent.AddCommand(commitCmd)
} }
@ -47,7 +47,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
registryPath, err = repos.FindRegistry() registryPath, err = repos.FindRegistry()
if err == nil { if err == nil {
@ -55,7 +55,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
// Fallback: scan current directory // Fallback: scan current directory
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to scan directory: %w", err) return fmt.Errorf("failed to scan directory: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
registryPath = cwd registryPath = cwd
} }
} }
@ -80,7 +80,7 @@ func runCommit(registryPath string, all bool) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
fmt.Println("No git repositories found.") fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil return nil
} }
@ -99,22 +99,22 @@ func runCommit(registryPath string, all bool) error {
} }
if len(dirtyRepos) == 0 { if len(dirtyRepos) == 0 {
fmt.Println("No uncommitted changes found.") fmt.Println(i18n.T("cmd.dev.no_changes"))
return nil return nil
} }
// Show dirty repos // Show dirty repos
fmt.Printf("\n%d repo(s) with uncommitted changes:\n\n", len(dirtyRepos)) fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.repos_with_changes", map[string]interface{}{"Count": len(dirtyRepos)}))
for _, s := range dirtyRepos { for _, s := range dirtyRepos {
fmt.Printf(" %s: ", repoNameStyle.Render(s.Name)) fmt.Printf(" %s: ", repoNameStyle.Render(s.Name))
if s.Modified > 0 { if s.Modified > 0 {
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d modified", s.Modified))) fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified})))
} }
if s.Untracked > 0 { if s.Untracked > 0 {
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d untracked", s.Untracked))) fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked})))
} }
if s.Staged > 0 { if s.Staged > 0 {
fmt.Printf("%s ", aheadStyle.Render(fmt.Sprintf("%d staged", s.Staged))) fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged})))
} }
fmt.Println() fmt.Println()
} }
@ -122,8 +122,8 @@ func runCommit(registryPath string, all bool) error {
// Confirm unless --all // Confirm unless --all
if !all { if !all {
fmt.Println() fmt.Println()
if !shared.Confirm("Have Claude commit these repos?") { if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
fmt.Println("Aborted.") fmt.Println(i18n.T("cli.aborted"))
return nil return nil
} }
} }
@ -133,22 +133,22 @@ func runCommit(registryPath string, all bool) error {
// Commit each dirty repo // Commit each dirty repo
var succeeded, failed int var succeeded, failed int
for _, s := range dirtyRepos { for _, s := range dirtyRepos {
fmt.Printf("%s %s\n", dimStyle.Render("Committing"), s.Name) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.committing")), s.Name)
if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil { if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil {
fmt.Printf(" %s %s\n", errorStyle.Render("x"), err) fmt.Printf(" %s %s\n", errorStyle.Render("x"), err)
failed++ failed++
} else { } else {
fmt.Printf(" %s committed\n", successStyle.Render("v")) fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed"))
succeeded++ succeeded++
} }
fmt.Println() fmt.Println()
} }
// Summary // Summary
fmt.Printf("%s %d succeeded", successStyle.Render("Done:"), succeeded) fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.done_succeeded", map[string]interface{}{"Count": succeeded})))
if failed > 0 { if failed > 0 {
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
} }
fmt.Println() fmt.Println()

View file

@ -8,6 +8,7 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -22,16 +23,15 @@ var (
func addHealthCommand(parent *cobra.Command) { func addHealthCommand(parent *cobra.Command) {
healthCmd := &cobra.Command{ healthCmd := &cobra.Command{
Use: "health", Use: "health",
Short: "Quick health check across all repos", Short: i18n.T("cmd.dev.health.short"),
Long: `Shows a summary of repository health: Long: i18n.T("cmd.dev.health.long"),
total repos, dirty repos, unpushed commits, etc.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runHealth(healthRegistryPath, healthVerbose) return runHealth(healthRegistryPath, healthVerbose)
}, },
} }
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", i18n.T("cmd.dev.health.flag.registry"))
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown") healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, i18n.T("cmd.dev.health.flag.verbose"))
parent.AddCommand(healthCmd) parent.AddCommand(healthCmd)
} }
@ -77,7 +77,7 @@ func runHealth(registryPath string, verbose bool) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
fmt.Println("No git repositories found.") fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil return nil
} }
@ -125,16 +125,16 @@ func runHealth(registryPath string, verbose bool) error {
// Verbose output // Verbose output
if verbose { if verbose {
if len(dirtyRepos) > 0 { if len(dirtyRepos) > 0 {
fmt.Printf("%s %s\n", warningStyle.Render("Dirty:"), formatRepoList(dirtyRepos)) fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.dirty_label")), formatRepoList(dirtyRepos))
} }
if len(aheadRepos) > 0 { if len(aheadRepos) > 0 {
fmt.Printf("%s %s\n", successStyle.Render("Ahead:"), formatRepoList(aheadRepos)) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.dev.health.ahead_label")), formatRepoList(aheadRepos))
} }
if len(behindRepos) > 0 { if len(behindRepos) > 0 {
fmt.Printf("%s %s\n", warningStyle.Render("Behind:"), formatRepoList(behindRepos)) fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.behind_label")), formatRepoList(behindRepos))
} }
if len(errorRepos) > 0 { if len(errorRepos) > 0 {
fmt.Printf("%s %s\n", errorStyle.Render("Errors:"), formatRepoList(errorRepos)) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos))
} }
fmt.Println() fmt.Println()
} }
@ -144,33 +144,33 @@ func runHealth(registryPath string, verbose bool) error {
func printHealthSummary(total int, dirty, ahead, behind, errors []string) { func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
parts := []string{ parts := []string{
shared.StatusPart(total, "repos", shared.ValueStyle), shared.StatusPart(total, i18n.T("cmd.dev.health.repos"), shared.ValueStyle),
} }
// Dirty status // Dirty status
if len(dirty) > 0 { if len(dirty) > 0 {
parts = append(parts, shared.StatusPart(len(dirty), "dirty", shared.WarningStyle)) parts = append(parts, shared.StatusPart(len(dirty), i18n.T("cmd.dev.health.dirty"), shared.WarningStyle))
} else { } else {
parts = append(parts, shared.StatusText("clean", shared.SuccessStyle)) parts = append(parts, shared.StatusText(i18n.T("cmd.dev.status.clean"), shared.SuccessStyle))
} }
// Push status // Push status
if len(ahead) > 0 { if len(ahead) > 0 {
parts = append(parts, shared.StatusPart(len(ahead), "to push", shared.ValueStyle)) parts = append(parts, shared.StatusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), shared.ValueStyle))
} else { } else {
parts = append(parts, shared.StatusText("synced", shared.SuccessStyle)) parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.synced"), shared.SuccessStyle))
} }
// Pull status // Pull status
if len(behind) > 0 { if len(behind) > 0 {
parts = append(parts, shared.StatusPart(len(behind), "to pull", shared.WarningStyle)) parts = append(parts, shared.StatusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), shared.WarningStyle))
} else { } else {
parts = append(parts, shared.StatusText("up to date", shared.SuccessStyle)) parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.up_to_date"), shared.SuccessStyle))
} }
// Errors (only if any) // Errors (only if any)
if len(errors) > 0 { if len(errors) > 0 {
parts = append(parts, shared.StatusPart(len(errors), "errors", shared.ErrorStyle)) parts = append(parts, shared.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), shared.ErrorStyle))
} }
fmt.Println(shared.StatusLine(parts...)) fmt.Println(shared.StatusLine(parts...))
@ -180,7 +180,7 @@ func formatRepoList(reposList []string) string {
if len(reposList) <= 5 { if len(reposList) <= 5 {
return joinRepos(reposList) return joinRepos(reposList)
} }
return joinRepos(reposList[:5]) + fmt.Sprintf(" +%d more", len(reposList)-5) return joinRepos(reposList[:5]) + " " + i18n.T("cmd.dev.health.more", map[string]interface{}{"Count": len(reposList) - 5})
} }
func joinRepos(reposList []string) string { func joinRepos(reposList []string) string {

View file

@ -5,6 +5,7 @@ import (
"sort" "sort"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,16 +24,15 @@ var impactRegistryPath string
func addImpactCommand(parent *cobra.Command) { func addImpactCommand(parent *cobra.Command) {
impactCmd := &cobra.Command{ impactCmd := &cobra.Command{
Use: "impact <repo-name>", Use: "impact <repo-name>",
Short: "Show impact of changing a repo", Short: i18n.T("cmd.dev.impact.short"),
Long: `Analyzes the dependency graph to show which repos Long: i18n.T("cmd.dev.impact.long"),
would be affected by changes to the specified repo.`, Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runImpact(impactRegistryPath, args[0]) return runImpact(impactRegistryPath, args[0])
}, },
} }
impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", i18n.T("cmd.dev.impact.flag.registry"))
parent.AddCommand(impactCmd) parent.AddCommand(impactCmd)
} }
@ -55,14 +55,14 @@ func runImpact(registryPath string, repoName string) error {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
} else { } else {
return fmt.Errorf("impact analysis requires repos.yaml with dependency information") return fmt.Errorf(i18n.T("cmd.dev.impact.requires_registry"))
} }
} }
// Check repo exists // Check repo exists
repo, exists := reg.Get(repoName) repo, exists := reg.Get(repoName)
if !exists { if !exists {
return fmt.Errorf("repo '%s' not found in registry", repoName) return fmt.Errorf(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName}))
} }
// Build reverse dependency graph // Build reverse dependency graph
@ -91,22 +91,22 @@ func runImpact(registryPath string, repoName string) error {
// Print results // Print results
fmt.Println() fmt.Println()
fmt.Printf("%s %s\n", dimStyle.Render("Impact analysis for"), repoNameStyle.Render(repoName)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName))
if repo.Description != "" { if repo.Description != "" {
fmt.Printf("%s\n", dimStyle.Render(repo.Description)) fmt.Printf("%s\n", dimStyle.Render(repo.Description))
} }
fmt.Println() fmt.Println()
if len(allAffected) == 0 { if len(allAffected) == 0 {
fmt.Printf("%s No repos depend on %s\n", impactSafeStyle.Render("v"), repoName) fmt.Printf("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName}))
return nil return nil
} }
// Direct dependents // Direct dependents
if len(direct) > 0 { if len(direct) > 0 {
fmt.Printf("%s %d direct dependent(s):\n", fmt.Printf("%s %s\n",
impactDirectStyle.Render("*"), impactDirectStyle.Render("*"),
len(direct), i18n.T("cmd.dev.impact.direct_dependents", map[string]interface{}{"Count": len(direct)}),
) )
for _, d := range direct { for _, d := range direct {
r, _ := reg.Get(d) r, _ := reg.Get(d)
@ -121,9 +121,9 @@ func runImpact(registryPath string, repoName string) error {
// Indirect dependents // Indirect dependents
if len(indirect) > 0 { if len(indirect) > 0 {
fmt.Printf("%s %d transitive dependent(s):\n", fmt.Printf("%s %s\n",
impactIndirectStyle.Render("o"), impactIndirectStyle.Render("o"),
len(indirect), i18n.T("cmd.dev.impact.transitive_dependents", map[string]interface{}{"Count": len(indirect)}),
) )
for _, d := range indirect { for _, d := range indirect {
r, _ := reg.Get(d) r, _ := reg.Get(d)
@ -137,10 +137,13 @@ func runImpact(registryPath string, repoName string) error {
} }
// Summary // Summary
fmt.Printf("%s Changes to %s affect %s\n", fmt.Printf("%s %s\n",
dimStyle.Render("Summary:"), dimStyle.Render(i18n.T("cmd.dev.impact.summary")),
repoNameStyle.Render(repoName), i18n.T("cmd.dev.impact.changes_affect", map[string]interface{}{
impactDirectStyle.Render(fmt.Sprintf("%d/%d repos", len(allAffected), len(reg.Repos)-1)), "Repo": repoNameStyle.Render(repoName),
"Affected": len(allAffected),
"Total": len(reg.Repos) - 1,
}),
) )
return nil return nil

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -60,9 +61,8 @@ var (
func addIssuesCommand(parent *cobra.Command) { func addIssuesCommand(parent *cobra.Command) {
issuesCmd := &cobra.Command{ issuesCmd := &cobra.Command{
Use: "issues", Use: "issues",
Short: "List open issues across all repos", Short: i18n.T("cmd.dev.issues.short"),
Long: `Fetches open issues from GitHub for all repos in the registry. Long: i18n.T("cmd.dev.issues.long"),
Requires the 'gh' CLI to be installed and authenticated.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
limit := issuesLimit limit := issuesLimit
if limit == 0 { if limit == 0 {
@ -72,9 +72,9 @@ Requires the 'gh' CLI to be installed and authenticated.`,
}, },
} }
issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", i18n.T("cmd.dev.issues.flag.registry"))
issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, "Max issues per repo") issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, i18n.T("cmd.dev.issues.flag.limit"))
issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", "Filter by assignee (use @me for yourself)") issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", i18n.T("cmd.dev.issues.flag.assignee"))
parent.AddCommand(issuesCmd) parent.AddCommand(issuesCmd)
} }
@ -82,7 +82,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
func runIssues(registryPath string, limit int, assignee string) error { func runIssues(registryPath string, limit int, assignee string) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/") return fmt.Errorf(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry, fall back to directory scan // Find or use provided registry, fall back to directory scan
@ -118,7 +118,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
repoList := reg.List() repoList := reg.List()
for i, repo := range repoList { for i, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name) fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.fetching")), i+1, len(repoList), repo.Name)
issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee) issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee)
if err != nil { if err != nil {
@ -136,11 +136,11 @@ func runIssues(registryPath string, limit int, assignee string) error {
// Print issues // Print issues
if len(allIssues) == 0 { if len(allIssues) == 0 {
fmt.Println("No open issues found.") fmt.Println(i18n.T("cmd.dev.issues.no_issues"))
return nil return nil
} }
fmt.Printf("\n%d open issue(s):\n\n", len(allIssues)) fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.issues.open_issues", map[string]interface{}{"Count": len(allIssues)}))
for _, issue := range allIssues { for _, issue := range allIssues {
printIssue(issue) printIssue(issue)
@ -150,7 +150,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
fmt.Println() fmt.Println()
for _, err := range fetchErrors { for _, err := range fetchErrors {
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.issues.error_label")), err)
} }
} }

View file

@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -21,16 +22,15 @@ var (
func addPullCommand(parent *cobra.Command) { func addPullCommand(parent *cobra.Command) {
pullCmd := &cobra.Command{ pullCmd := &cobra.Command{
Use: "pull", Use: "pull",
Short: "Pull updates across all repos", Short: i18n.T("cmd.dev.pull.short"),
Long: `Pulls updates for all repos. Long: i18n.T("cmd.dev.pull.long"),
By default only pulls repos that are behind. Use --all to pull all repos.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPull(pullRegistryPath, pullAll) return runPull(pullRegistryPath, pullAll)
}, },
} }
pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", i18n.T("cmd.dev.pull.flag.registry"))
pullCmd.Flags().BoolVar(&pullAll, "all", false, "Pull all repos, not just those behind") pullCmd.Flags().BoolVar(&pullAll, "all", false, i18n.T("cmd.dev.pull.flag.all"))
parent.AddCommand(pullCmd) parent.AddCommand(pullCmd)
} }
@ -47,7 +47,7 @@ func runPull(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
registryPath, err = repos.FindRegistry() registryPath, err = repos.FindRegistry()
if err == nil { if err == nil {
@ -55,7 +55,7 @@ func runPull(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
// Fallback: scan current directory // Fallback: scan current directory
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runPull(registryPath string, all bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to scan directory: %w", err) return fmt.Errorf("failed to scan directory: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
} }
} }
@ -79,7 +79,7 @@ func runPull(registryPath string, all bool) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
fmt.Println("No git repositories found.") fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil return nil
} }
@ -101,19 +101,19 @@ func runPull(registryPath string, all bool) error {
} }
if len(toPull) == 0 { if len(toPull) == 0 {
fmt.Println("All repos up to date. Nothing to pull.") fmt.Println(i18n.T("cmd.dev.pull.all_up_to_date"))
return nil return nil
} }
// Show what we're pulling // Show what we're pulling
if all { if all {
fmt.Printf("\nPulling %d repo(s):\n\n", len(toPull)) fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.pulling_repos", map[string]interface{}{"Count": len(toPull)}))
} else { } else {
fmt.Printf("\n%d repo(s) behind upstream:\n\n", len(toPull)) fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.repos_behind", map[string]interface{}{"Count": len(toPull)}))
for _, s := range toPull { for _, s := range toPull {
fmt.Printf(" %s: %s\n", fmt.Printf(" %s: %s\n",
repoNameStyle.Render(s.Name), repoNameStyle.Render(s.Name),
dimStyle.Render(fmt.Sprintf("%d commit(s) behind", s.Behind)), dimStyle.Render(i18n.T("cmd.dev.pull.commits_behind", map[string]interface{}{"Count": s.Behind})),
) )
} }
fmt.Println() fmt.Println()
@ -122,7 +122,7 @@ func runPull(registryPath string, all bool) error {
// Pull each repo // Pull each repo
var succeeded, failed int var succeeded, failed int
for _, s := range toPull { for _, s := range toPull {
fmt.Printf(" %s %s... ", dimStyle.Render("Pulling"), s.Name) fmt.Printf(" %s %s... ", dimStyle.Render(i18n.T("cmd.dev.pull.pulling")), s.Name)
err := gitPull(ctx, s.Path) err := gitPull(ctx, s.Path)
if err != nil { if err != nil {
@ -136,9 +136,9 @@ func runPull(registryPath string, all bool) error {
// Summary // Summary
fmt.Println() fmt.Println()
fmt.Printf("%s %d pulled", successStyle.Render("Done:"), succeeded) fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded})))
if failed > 0 { if failed > 0 {
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
} }
fmt.Println() fmt.Println()

View file

@ -7,6 +7,7 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -21,16 +22,15 @@ var (
func addPushCommand(parent *cobra.Command) { func addPushCommand(parent *cobra.Command) {
pushCmd := &cobra.Command{ pushCmd := &cobra.Command{
Use: "push", Use: "push",
Short: "Push commits across all repos", Short: i18n.T("cmd.dev.push.short"),
Long: `Pushes unpushed commits for all repos. Long: i18n.T("cmd.dev.push.long"),
Shows repos with commits to push and confirms before pushing.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPush(pushRegistryPath, pushForce) return runPush(pushRegistryPath, pushForce)
}, },
} }
pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", i18n.T("cmd.dev.push.flag.registry"))
pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, "Skip confirmation prompt") pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, i18n.T("cmd.dev.push.flag.force"))
parent.AddCommand(pushCmd) parent.AddCommand(pushCmd)
} }
@ -47,7 +47,7 @@ func runPush(registryPath string, force bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
registryPath, err = repos.FindRegistry() registryPath, err = repos.FindRegistry()
if err == nil { if err == nil {
@ -55,7 +55,7 @@ func runPush(registryPath string, force bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
// Fallback: scan current directory // Fallback: scan current directory
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runPush(registryPath string, force bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to scan directory: %w", err) return fmt.Errorf("failed to scan directory: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
} }
} }
@ -79,7 +79,7 @@ func runPush(registryPath string, force bool) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
fmt.Println("No git repositories found.") fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil return nil
} }
@ -98,17 +98,17 @@ func runPush(registryPath string, force bool) error {
} }
if len(aheadRepos) == 0 { if len(aheadRepos) == 0 {
fmt.Println("All repos up to date. Nothing to push.") fmt.Println(i18n.T("cmd.dev.push.all_up_to_date"))
return nil return nil
} }
// Show repos to push // Show repos to push
fmt.Printf("\n%d repo(s) with unpushed commits:\n\n", len(aheadRepos)) fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.push.repos_with_unpushed", map[string]interface{}{"Count": len(aheadRepos)}))
totalCommits := 0 totalCommits := 0
for _, s := range aheadRepos { for _, s := range aheadRepos {
fmt.Printf(" %s: %s\n", fmt.Printf(" %s: %s\n",
repoNameStyle.Render(s.Name), repoNameStyle.Render(s.Name),
aheadStyle.Render(fmt.Sprintf("%d commit(s)", s.Ahead)), aheadStyle.Render(i18n.T("cmd.dev.push.commits_count", map[string]interface{}{"Count": s.Ahead})),
) )
totalCommits += s.Ahead totalCommits += s.Ahead
} }
@ -116,8 +116,8 @@ func runPush(registryPath string, force bool) error {
// Confirm unless --force // Confirm unless --force
if !force { if !force {
fmt.Println() fmt.Println()
if !shared.Confirm(fmt.Sprintf("Push %d commit(s) to %d repo(s)?", totalCommits, len(aheadRepos))) { if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
fmt.Println("Aborted.") fmt.Println(i18n.T("cli.aborted"))
return nil return nil
} }
} }
@ -145,9 +145,9 @@ func runPush(registryPath string, force bool) error {
// Summary // Summary
fmt.Println() fmt.Println()
fmt.Printf("%s %d pushed", successStyle.Render("Done:"), succeeded) fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded})))
if failed > 0 { if failed > 0 {
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
} }
fmt.Println() fmt.Println()

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -61,18 +62,16 @@ var (
func addReviewsCommand(parent *cobra.Command) { func addReviewsCommand(parent *cobra.Command) {
reviewsCmd := &cobra.Command{ reviewsCmd := &cobra.Command{
Use: "reviews", Use: "reviews",
Short: "List PRs needing review across all repos", Short: i18n.T("cmd.dev.reviews.short"),
Long: `Fetches open PRs from GitHub for all repos in the registry. Long: i18n.T("cmd.dev.reviews.long"),
Shows review status (approved, changes requested, pending).
Requires the 'gh' CLI to be installed and authenticated.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll) return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll)
}, },
} }
reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", i18n.T("cmd.dev.reviews.flag.registry"))
reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", "Filter by PR author") reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", i18n.T("cmd.dev.reviews.flag.author"))
reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, "Show all PRs including drafts") reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, i18n.T("cmd.dev.reviews.flag.all"))
parent.AddCommand(reviewsCmd) parent.AddCommand(reviewsCmd)
} }
@ -80,7 +79,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
func runReviews(registryPath string, author string, showAll bool) error { func runReviews(registryPath string, author string, showAll bool) error {
// Check gh is available // Check gh is available
if _, err := exec.LookPath("gh"); err != nil { if _, err := exec.LookPath("gh"); err != nil {
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/") return fmt.Errorf(i18n.T("error.gh_not_found"))
} }
// Find or use provided registry, fall back to directory scan // Find or use provided registry, fall back to directory scan
@ -116,7 +115,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
repoList := reg.List() repoList := reg.List()
for i, repo := range repoList { for i, repo := range repoList {
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name) repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name) fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.fetching")), i+1, len(repoList), repo.Name)
prs, err := fetchPRs(repoFullName, repo.Name, author) prs, err := fetchPRs(repoFullName, repo.Name, author)
if err != nil { if err != nil {
@ -147,7 +146,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
// Print PRs // Print PRs
if len(allPRs) == 0 { if len(allPRs) == 0 {
fmt.Println("No open PRs found.") fmt.Println(i18n.T("cmd.dev.reviews.no_prs"))
return nil return nil
} }
@ -165,15 +164,15 @@ func runReviews(registryPath string, author string, showAll bool) error {
} }
fmt.Println() fmt.Println()
fmt.Printf("%d open PR(s)", len(allPRs)) fmt.Printf("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)}))
if pending > 0 { if pending > 0 {
fmt.Printf(" * %s", prPendingStyle.Render(fmt.Sprintf("%d pending", pending))) fmt.Printf(" * %s", prPendingStyle.Render(i18n.T("cmd.dev.reviews.pending", map[string]interface{}{"Count": pending})))
} }
if approved > 0 { if approved > 0 {
fmt.Printf(" * %s", prApprovedStyle.Render(fmt.Sprintf("%d approved", approved))) fmt.Printf(" * %s", prApprovedStyle.Render(i18n.T("cmd.dev.reviews.approved", map[string]interface{}{"Count": approved})))
} }
if changesRequested > 0 { if changesRequested > 0 {
fmt.Printf(" * %s", prChangesStyle.Render(fmt.Sprintf("%d changes requested", changesRequested))) fmt.Printf(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested})))
} }
fmt.Println() fmt.Println()
fmt.Println() fmt.Println()
@ -186,7 +185,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
if len(fetchErrors) > 0 { if len(fetchErrors) > 0 {
fmt.Println() fmt.Println()
for _, err := range fetchErrors { for _, err := range fetchErrors {
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.issues.error_label")), err)
} }
} }
@ -242,17 +241,17 @@ func printPR(pr GitHubPR) {
var status string var status string
switch pr.ReviewDecision { switch pr.ReviewDecision {
case "APPROVED": case "APPROVED":
status = prApprovedStyle.Render("v approved") status = prApprovedStyle.Render(i18n.T("cmd.dev.reviews.status_approved"))
case "CHANGES_REQUESTED": case "CHANGES_REQUESTED":
status = prChangesStyle.Render("* changes requested") status = prChangesStyle.Render(i18n.T("cmd.dev.reviews.status_changes"))
default: default:
status = prPendingStyle.Render("o pending review") status = prPendingStyle.Render(i18n.T("cmd.dev.reviews.status_pending"))
} }
// Draft indicator // Draft indicator
draft := "" draft := ""
if pr.IsDraft { if pr.IsDraft {
draft = prDraftStyle.Render(" [draft]") draft = prDraftStyle.Render(" " + i18n.T("cmd.dev.reviews.draft"))
} }
age := shared.FormatAge(pr.CreatedAt) age := shared.FormatAge(pr.CreatedAt)

View file

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"text/template" "text/template"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -19,15 +20,13 @@ import (
func addSyncCommand(parent *cobra.Command) { func addSyncCommand(parent *cobra.Command) {
syncCmd := &cobra.Command{ syncCmd := &cobra.Command{
Use: "sync", Use: "sync",
Short: "Synchronizes the public service APIs with their internal implementations.", Short: i18n.T("cmd.dev.sync.short"),
Long: `This command scans the 'pkg' directory for services and ensures that the Long: i18n.T("cmd.dev.sync.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
if err := runSync(); err != nil { if err := runSync(); err != nil {
return fmt.Errorf("Error: %w", err) return fmt.Errorf("%s %w", i18n.T("cmd.dev.sync.error_prefix"), err)
} }
fmt.Println("Public APIs synchronized successfully.") fmt.Println(i18n.T("cmd.dev.sync.success"))
return nil return nil
}, },
} }

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/devops" "github.com/host-uk/core/pkg/devops"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,14 +29,8 @@ func addVMCommands(parent *cobra.Command) {
func addVMInstallCommand(parent *cobra.Command) { func addVMInstallCommand(parent *cobra.Command) {
installCmd := &cobra.Command{ installCmd := &cobra.Command{
Use: "install", Use: "install",
Short: "Download and install the dev environment image", Short: i18n.T("cmd.dev.vm.install.short"),
Long: `Downloads the platform-specific dev environment image. Long: i18n.T("cmd.dev.vm.install.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runVMInstall() return runVMInstall()
}, },
@ -51,15 +46,15 @@ func runVMInstall() error {
} }
if d.IsInstalled() { if d.IsInstalled() {
fmt.Println(successStyle.Render("Dev environment already installed")) fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.already_installed")))
fmt.Println() fmt.Println()
fmt.Printf("Use %s to check for updates\n", dimStyle.Render("core dev update")) fmt.Println(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")}))
return nil return nil
} }
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), devops.ImageName()) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.image_label")), devops.ImageName())
fmt.Println() fmt.Println()
fmt.Println("Downloading dev environment...") fmt.Println(i18n.T("cmd.dev.vm.downloading"))
fmt.Println() fmt.Println()
ctx := context.Background() ctx := context.Background()
@ -70,7 +65,7 @@ func runVMInstall() error {
if total > 0 { if total > 0 {
pct := int(float64(downloaded) / float64(total) * 100) pct := int(float64(downloaded) / float64(total) * 100)
if pct != int(float64(lastProgress)/float64(total)*100) { if pct != int(float64(lastProgress)/float64(total)*100) {
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct) fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct)
lastProgress = downloaded lastProgress = downloaded
} }
} }
@ -84,9 +79,9 @@ func runVMInstall() error {
elapsed := time.Since(start).Round(time.Second) elapsed := time.Since(start).Round(time.Second)
fmt.Println() fmt.Println()
fmt.Printf("%s in %s\n", successStyle.Render("Installed"), elapsed) fmt.Println(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed}))
fmt.Println() fmt.Println()
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot")) fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
return nil return nil
} }
@ -102,21 +97,16 @@ var (
func addVMBootCommand(parent *cobra.Command) { func addVMBootCommand(parent *cobra.Command) {
bootCmd := &cobra.Command{ bootCmd := &cobra.Command{
Use: "boot", Use: "boot",
Short: "Start the dev environment", Short: i18n.T("cmd.dev.vm.boot.short"),
Long: `Boots the dev environment VM. Long: i18n.T("cmd.dev.vm.boot.long"),
Examples:
core dev boot
core dev boot --memory 8192 --cpus 4
core dev boot --fresh`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh) return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh)
}, },
} }
bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, "Memory in MB (default: 4096)") bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, i18n.T("cmd.dev.vm.boot.flag.memory"))
bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, "Number of CPUs (default: 2)") bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, i18n.T("cmd.dev.vm.boot.flag.cpus"))
bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, "Stop existing and start fresh") bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, i18n.T("cmd.dev.vm.boot.flag.fresh"))
parent.AddCommand(bootCmd) parent.AddCommand(bootCmd)
} }
@ -128,7 +118,7 @@ func runVMBoot(memory, cpus int, fresh bool) error {
} }
if !d.IsInstalled() { if !d.IsInstalled() {
return fmt.Errorf("dev environment not installed (run 'core dev install' first)") return fmt.Errorf(i18n.T("cmd.dev.vm.not_installed"))
} }
opts := devops.DefaultBootOptions() opts := devops.DefaultBootOptions()
@ -140,9 +130,9 @@ func runVMBoot(memory, cpus int, fresh bool) error {
} }
opts.Fresh = fresh opts.Fresh = fresh
fmt.Printf("%s %dMB, %d CPUs\n", dimStyle.Render("Config:"), opts.Memory, opts.CPUs) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs}))
fmt.Println() fmt.Println()
fmt.Println("Booting dev environment...") fmt.Println(i18n.T("cmd.dev.vm.booting"))
ctx := context.Background() ctx := context.Background()
if err := d.Boot(ctx, opts); err != nil { if err := d.Boot(ctx, opts); err != nil {
@ -150,10 +140,10 @@ func runVMBoot(memory, cpus int, fresh bool) error {
} }
fmt.Println() fmt.Println()
fmt.Println(successStyle.Render("Dev environment running")) fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.running")))
fmt.Println() fmt.Println()
fmt.Printf("Connect with: %s\n", dimStyle.Render("core dev shell")) fmt.Println(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")}))
fmt.Printf("SSH port: %s\n", dimStyle.Render("2222")) fmt.Printf("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222"))
return nil return nil
} }
@ -162,11 +152,8 @@ func runVMBoot(memory, cpus int, fresh bool) error {
func addVMStopCommand(parent *cobra.Command) { func addVMStopCommand(parent *cobra.Command) {
stopCmd := &cobra.Command{ stopCmd := &cobra.Command{
Use: "stop", Use: "stop",
Short: "Stop the dev environment", Short: i18n.T("cmd.dev.vm.stop.short"),
Long: `Stops the running dev environment VM. Long: i18n.T("cmd.dev.vm.stop.long"),
Examples:
core dev stop`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMStop() return runVMStop()
}, },
@ -188,17 +175,17 @@ func runVMStop() error {
} }
if !running { if !running {
fmt.Println(dimStyle.Render("Dev environment is not running")) fmt.Println(dimStyle.Render(i18n.T("cmd.dev.vm.not_running")))
return nil return nil
} }
fmt.Println("Stopping dev environment...") fmt.Println(i18n.T("cmd.dev.vm.stopping"))
if err := d.Stop(ctx); err != nil { if err := d.Stop(ctx); err != nil {
return err return err
} }
fmt.Println(successStyle.Render("Stopped")) fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.stopped")))
return nil return nil
} }
@ -206,11 +193,8 @@ func runVMStop() error {
func addVMStatusCommand(parent *cobra.Command) { func addVMStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{ statusCmd := &cobra.Command{
Use: "vm-status", Use: "vm-status",
Short: "Show dev environment status", Short: i18n.T("cmd.dev.vm.status.short"),
Long: `Shows the current status of the dev environment. Long: i18n.T("cmd.dev.vm.status.long"),
Examples:
core dev vm-status`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMStatus() return runVMStatus()
}, },
@ -231,19 +215,19 @@ func runVMStatus() error {
return err return err
} }
fmt.Println(headerStyle.Render("Dev Environment Status")) fmt.Println(headerStyle.Render(i18n.T("cmd.dev.vm.status_title")))
fmt.Println() fmt.Println()
// Installation status // Installation status
if status.Installed { if status.Installed {
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), successStyle.Render("Yes")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), successStyle.Render(i18n.T("cmd.dev.vm.installed_yes")))
if status.ImageVersion != "" { if status.ImageVersion != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Version:"), status.ImageVersion) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.version_label")), status.ImageVersion)
} }
} else { } else {
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), errorStyle.Render("No")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no")))
fmt.Println() fmt.Println()
fmt.Printf("Install with: %s\n", dimStyle.Render("core dev install")) fmt.Println(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")}))
return nil return nil
} }
@ -251,16 +235,16 @@ func runVMStatus() error {
// Running status // Running status
if status.Running { if status.Running {
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), successStyle.Render("Running")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.status_label")), successStyle.Render(i18n.T("cmd.dev.vm.status_running")))
fmt.Printf("%s %s\n", dimStyle.Render("Container:"), status.ContainerID[:8]) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8])
fmt.Printf("%s %dMB\n", dimStyle.Render("Memory:"), status.Memory) fmt.Printf("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory)
fmt.Printf("%s %d\n", dimStyle.Render("CPUs:"), status.CPUs) fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs)
fmt.Printf("%s %d\n", dimStyle.Render("SSH Port:"), status.SSHPort) fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort)
fmt.Printf("%s %s\n", dimStyle.Render("Uptime:"), formatVMUptime(status.Uptime)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime))
} else { } else {
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), dimStyle.Render("Stopped")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.status_label")), dimStyle.Render(i18n.T("cmd.dev.vm.status_stopped")))
fmt.Println() fmt.Println()
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot")) fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
} }
return nil return nil
@ -286,21 +270,14 @@ var vmShellConsole bool
func addVMShellCommand(parent *cobra.Command) { func addVMShellCommand(parent *cobra.Command) {
shellCmd := &cobra.Command{ shellCmd := &cobra.Command{
Use: "shell [-- command...]", Use: "shell [-- command...]",
Short: "Connect to the dev environment", Short: i18n.T("cmd.dev.vm.shell.short"),
Long: `Opens an interactive shell in the dev environment. Long: i18n.T("cmd.dev.vm.shell.long"),
Uses SSH by default, or serial console with --console.
Examples:
core dev shell
core dev shell --console
core dev shell -- ls -la`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMShell(vmShellConsole, args) return runVMShell(vmShellConsole, args)
}, },
} }
shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, "Use serial console instead of SSH") shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, i18n.T("cmd.dev.vm.shell.flag.console"))
parent.AddCommand(shellCmd) parent.AddCommand(shellCmd)
} }
@ -330,22 +307,15 @@ var (
func addVMServeCommand(parent *cobra.Command) { func addVMServeCommand(parent *cobra.Command) {
serveCmd := &cobra.Command{ serveCmd := &cobra.Command{
Use: "serve", Use: "serve",
Short: "Mount project and start dev server", Short: i18n.T("cmd.dev.vm.serve.short"),
Long: `Mounts the current project into the dev environment and starts a dev server. Long: i18n.T("cmd.dev.vm.serve.long"),
Auto-detects the appropriate serve command based on project files.
Examples:
core dev serve
core dev serve --port 3000
core dev serve --path public`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMServe(vmServePort, vmServePath) return runVMServe(vmServePort, vmServePath)
}, },
} }
serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, "Port to serve on (default: 8000)") serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, i18n.T("cmd.dev.vm.serve.flag.port"))
serveCmd.Flags().StringVar(&vmServePath, "path", "", "Subdirectory to serve") serveCmd.Flags().StringVar(&vmServePath, "path", "", i18n.T("cmd.dev.vm.serve.flag.path"))
parent.AddCommand(serveCmd) parent.AddCommand(serveCmd)
} }
@ -377,21 +347,14 @@ var vmTestName string
func addVMTestCommand(parent *cobra.Command) { func addVMTestCommand(parent *cobra.Command) {
testCmd := &cobra.Command{ testCmd := &cobra.Command{
Use: "test [-- command...]", Use: "test [-- command...]",
Short: "Run tests in the dev environment", Short: i18n.T("cmd.dev.vm.test.short"),
Long: `Runs tests in the dev environment. Long: i18n.T("cmd.dev.vm.test.long"),
Auto-detects the test command based on project files, or uses .core/test.yaml.
Examples:
core dev test
core dev test --name integration
core dev test -- go test -v ./...`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMTest(vmTestName, args) return runVMTest(vmTestName, args)
}, },
} }
testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", "Run named test command from .core/test.yaml") testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", i18n.T("cmd.dev.vm.test.flag.name"))
parent.AddCommand(testCmd) parent.AddCommand(testCmd)
} }
@ -427,31 +390,16 @@ var (
func addVMClaudeCommand(parent *cobra.Command) { func addVMClaudeCommand(parent *cobra.Command) {
claudeCmd := &cobra.Command{ claudeCmd := &cobra.Command{
Use: "claude", Use: "claude",
Short: "Start sandboxed Claude session", Short: i18n.T("cmd.dev.vm.claude.short"),
Long: `Starts a Claude Code session inside the dev environment sandbox. Long: i18n.T("cmd.dev.vm.claude.long"),
Provides isolation while forwarding selected credentials.
Auto-boots the dev environment if not running.
Auth options (default: all):
gh - GitHub CLI auth
anthropic - Anthropic API key
ssh - SSH agent forwarding
git - Git config (name, email)
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags) return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags)
}, },
} }
claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, "Don't forward any auth credentials") claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, i18n.T("cmd.dev.vm.claude.flag.no_auth"))
claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", "Model to use (opus, sonnet)") claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", i18n.T("cmd.dev.vm.claude.flag.model"))
claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, "Selective auth forwarding (gh,anthropic,ssh,git)") claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, i18n.T("cmd.dev.vm.claude.flag.auth"))
parent.AddCommand(claudeCmd) parent.AddCommand(claudeCmd)
} }
@ -484,18 +432,14 @@ var vmUpdateApply bool
func addVMUpdateCommand(parent *cobra.Command) { func addVMUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{ updateCmd := &cobra.Command{
Use: "update", Use: "update",
Short: "Check for and apply updates", Short: i18n.T("cmd.dev.vm.update.short"),
Long: `Checks for dev environment updates and optionally applies them. Long: i18n.T("cmd.dev.vm.update.long"),
Examples:
core dev update
core dev update --apply`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runVMUpdate(vmUpdateApply) return runVMUpdate(vmUpdateApply)
}, },
} }
updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, "Download and apply the update") updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, i18n.T("cmd.dev.vm.update.flag.apply"))
parent.AddCommand(updateCmd) parent.AddCommand(updateCmd)
} }
@ -508,7 +452,7 @@ func runVMUpdate(apply bool) error {
ctx := context.Background() ctx := context.Background()
fmt.Println("Checking for updates...") fmt.Println(i18n.T("cmd.dev.vm.checking_updates"))
fmt.Println() fmt.Println()
current, latest, hasUpdate, err := d.CheckUpdate(ctx) current, latest, hasUpdate, err := d.CheckUpdate(ctx)
@ -516,38 +460,38 @@ func runVMUpdate(apply bool) error {
return fmt.Errorf("failed to check for updates: %w", err) return fmt.Errorf("failed to check for updates: %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Current:"), valueStyle.Render(current)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.current_label")), valueStyle.Render(current))
fmt.Printf("%s %s\n", dimStyle.Render("Latest:"), valueStyle.Render(latest)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest))
fmt.Println() fmt.Println()
if !hasUpdate { if !hasUpdate {
fmt.Println(successStyle.Render("Already up to date")) fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date")))
return nil return nil
} }
fmt.Println(warningStyle.Render("Update available")) fmt.Println(warningStyle.Render(i18n.T("cmd.dev.vm.update_available")))
fmt.Println() fmt.Println()
if !apply { if !apply {
fmt.Printf("Run %s to update\n", dimStyle.Render("core dev update --apply")) fmt.Println(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")}))
return nil return nil
} }
// Stop if running // Stop if running
running, _ := d.IsRunning(ctx) running, _ := d.IsRunning(ctx)
if running { if running {
fmt.Println("Stopping current instance...") fmt.Println(i18n.T("cmd.dev.vm.stopping_current"))
_ = d.Stop(ctx) _ = d.Stop(ctx)
} }
fmt.Println("Downloading update...") fmt.Println(i18n.T("cmd.dev.vm.downloading_update"))
fmt.Println() fmt.Println()
start := time.Now() start := time.Now()
err = d.Install(ctx, func(downloaded, total int64) { err = d.Install(ctx, func(downloaded, total int64) {
if total > 0 { if total > 0 {
pct := int(float64(downloaded) / float64(total) * 100) pct := int(float64(downloaded) / float64(total) * 100)
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct) fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct)
} }
}) })
@ -559,7 +503,7 @@ func runVMUpdate(apply bool) error {
elapsed := time.Since(start).Round(time.Second) elapsed := time.Since(start).Round(time.Second)
fmt.Println() fmt.Println()
fmt.Printf("%s in %s\n", successStyle.Render("Updated"), elapsed) fmt.Println(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed}))
return nil return nil
} }

View file

@ -11,6 +11,7 @@ import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -26,19 +27,16 @@ var (
func addWorkCommand(parent *cobra.Command) { func addWorkCommand(parent *cobra.Command) {
workCmd := &cobra.Command{ workCmd := &cobra.Command{
Use: "work", Use: "work",
Short: "Multi-repo git operations", Short: i18n.T("cmd.dev.work.short"),
Long: `Manage git status, commits, and pushes across multiple repositories. Long: i18n.T("cmd.dev.work.long"),
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runWork(workRegistryPath, workStatusOnly, workAutoCommit) return runWork(workRegistryPath, workStatusOnly, workAutoCommit)
}, },
} }
workCmd.Flags().BoolVar(&workStatusOnly, "status", false, "Show status only, don't push") workCmd.Flags().BoolVar(&workStatusOnly, "status", false, i18n.T("cmd.dev.work.flag.status"))
workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, "Use Claude to commit dirty repos before pushing") workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, i18n.T("cmd.dev.work.flag.commit"))
workCmd.Flags().StringVar(&workRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") workCmd.Flags().StringVar(&workRegistryPath, "registry", "", i18n.T("cmd.dev.work.flag.registry"))
parent.AddCommand(workCmd) parent.AddCommand(workCmd)
} }
@ -55,7 +53,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
registryPath, err = repos.FindRegistry() registryPath, err = repos.FindRegistry()
if err == nil { if err == nil {
@ -63,7 +61,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
} else { } else {
// Fallback: scan current directory // Fallback: scan current directory
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@ -71,7 +69,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to scan directory: %w", err) return fmt.Errorf("failed to scan directory: %w", err)
} }
fmt.Printf("%s %s\n\n", dimStyle.Render("Scanning:"), cwd) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
} }
} }
@ -87,7 +85,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
} }
if len(paths) == 0 { if len(paths) == 0 {
fmt.Println("No git repositories found.") fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil return nil
} }
@ -124,7 +122,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Auto-commit dirty repos if requested // Auto-commit dirty repos if requested
if autoCommit && len(dirtyRepos) > 0 { if autoCommit && len(dirtyRepos) > 0 {
fmt.Println() fmt.Println()
fmt.Printf("%s\n", shared.TitleStyle.Render("Committing dirty repos with Claude...")) fmt.Printf("%s\n", shared.TitleStyle.Render(i18n.T("cmd.dev.commit.committing")))
fmt.Println() fmt.Println()
for _, s := range dirtyRepos { for _, s := range dirtyRepos {
@ -154,7 +152,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
if statusOnly { if statusOnly {
if len(dirtyRepos) > 0 && !autoCommit { if len(dirtyRepos) > 0 && !autoCommit {
fmt.Println() fmt.Println()
fmt.Printf("%s\n", dimStyle.Render("Use --commit to have Claude create commits")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag")))
} }
return nil return nil
} }
@ -162,19 +160,19 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
// Push repos with unpushed commits // Push repos with unpushed commits
if len(aheadRepos) == 0 { if len(aheadRepos) == 0 {
fmt.Println() fmt.Println()
fmt.Println("All repos up to date.") fmt.Println(i18n.T("cmd.dev.work.all_up_to_date"))
return nil return nil
} }
fmt.Println() fmt.Println()
fmt.Printf("%d repo(s) with unpushed commits:\n", len(aheadRepos)) fmt.Printf("%s\n", i18n.T("cmd.dev.work.repos_with_unpushed", map[string]interface{}{"Count": len(aheadRepos)}))
for _, s := range aheadRepos { for _, s := range aheadRepos {
fmt.Printf(" %s: %d commit(s)\n", s.Name, s.Ahead) fmt.Printf(" %s: %s\n", s.Name, i18n.T("cmd.dev.work.commits_count", map[string]interface{}{"Count": s.Ahead}))
} }
fmt.Println() fmt.Println()
if !shared.Confirm("Push all?") { if !shared.Confirm(i18n.T("cmd.dev.push.confirm")) {
fmt.Println("Aborted.") fmt.Println(i18n.T("cli.aborted"))
return nil return nil
} }
@ -211,11 +209,11 @@ func printStatusTable(statuses []git.RepoStatus) {
// Print header with fixed-width formatting // Print header with fixed-width formatting
fmt.Printf("%-*s %8s %9s %6s %5s\n", fmt.Printf("%-*s %8s %9s %6s %5s\n",
nameWidth, nameWidth,
shared.TitleStyle.Render("Repo"), shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_repo")),
shared.TitleStyle.Render("Modified"), shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_modified")),
shared.TitleStyle.Render("Untracked"), shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_untracked")),
shared.TitleStyle.Render("Staged"), shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_staged")),
shared.TitleStyle.Render("Ahead"), shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_ahead")),
) )
// Print separator // Print separator
@ -227,7 +225,7 @@ func printStatusTable(statuses []git.RepoStatus) {
paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name) paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name)
fmt.Printf("%s %s\n", fmt.Printf("%s %s\n",
repoNameStyle.Render(paddedName), repoNameStyle.Render(paddedName),
errorStyle.Render("error: "+s.Error.Error()), errorStyle.Render(i18n.T("cmd.dev.work.error_prefix")+" "+s.Error.Error()),
) )
continue continue
} }

View file

@ -3,6 +3,7 @@ package docs
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -21,9 +22,8 @@ var (
var docsCmd = &cobra.Command{ var docsCmd = &cobra.Command{
Use: "docs", Use: "docs",
Short: "Documentation management", Short: i18n.T("cmd.docs.short"),
Long: `Manage documentation across all repos. Long: i18n.T("cmd.docs.long"),
Scan for docs, check coverage, and sync to core-php/docs/packages/.`,
} }
func init() { func init() {

View file

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -13,14 +14,15 @@ var docsListRegistryPath string
var docsListCmd = &cobra.Command{ var docsListCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List documentation across repos", Short: i18n.T("cmd.docs.list.short"),
Long: i18n.T("cmd.docs.list.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDocsList(docsListRegistryPath) return runDocsList(docsListRegistryPath)
}, },
} }
func init() { func init() {
docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", "Path to repos.yaml") docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", i18n.T("cmd.docs.list.flag.registry"))
} }
func runDocsList(registryPath string) error { func runDocsList(registryPath string) error {
@ -30,11 +32,11 @@ func runDocsList(registryPath string) error {
} }
fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n", fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n",
headerStyle.Render("Repo"), headerStyle.Render(i18n.T("cmd.docs.list.header.repo")),
headerStyle.Render("README"), headerStyle.Render(i18n.T("cmd.docs.list.header.readme")),
headerStyle.Render("CLAUDE"), headerStyle.Render(i18n.T("cmd.docs.list.header.claude")),
headerStyle.Render("CHANGELOG"), headerStyle.Render(i18n.T("cmd.docs.list.header.changelog")),
headerStyle.Render("docs/"), headerStyle.Render(i18n.T("cmd.docs.list.header.docs")),
) )
fmt.Println(strings.Repeat("─", 70)) fmt.Println(strings.Repeat("─", 70))
@ -48,7 +50,7 @@ func runDocsList(registryPath string) error {
docsDir := shared.CheckMark(false) docsDir := shared.CheckMark(false)
if len(info.DocsFiles) > 0 { if len(info.DocsFiles) > 0 {
docsDir = docsFoundStyle.Render(fmt.Sprintf("%d files", len(info.DocsFiles))) docsDir = docsFoundStyle.Render(i18n.T("cmd.docs.list.files_count", map[string]interface{}{"Count": len(info.DocsFiles)}))
} }
fmt.Printf("%-20s %-8s %-8s %-10s %s\n", fmt.Printf("%-20s %-8s %-8s %-10s %s\n",
@ -67,10 +69,9 @@ func runDocsList(registryPath string) error {
} }
fmt.Println() fmt.Println()
fmt.Printf("%s %d with docs, %d without\n", fmt.Printf("%s %s\n",
shared.Label("Coverage"), shared.Label(i18n.T("cmd.docs.list.coverage_label")),
withDocs, i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}),
withoutDocs,
) )
return nil return nil

View file

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
) )
@ -29,7 +30,7 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
if registryPath != "" { if registryPath != "" {
reg, err = repos.LoadRegistry(registryPath) reg, err = repos.LoadRegistry(registryPath)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to load registry: %w", err) return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.load_registry"), err)
} }
basePath = filepath.Dir(registryPath) basePath = filepath.Dir(registryPath)
} else { } else {
@ -37,14 +38,14 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
if err == nil { if err == nil {
reg, err = repos.LoadRegistry(registryPath) reg, err = repos.LoadRegistry(registryPath)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to load registry: %w", err) return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.load_registry"), err)
} }
basePath = filepath.Dir(registryPath) basePath = filepath.Dir(registryPath)
} else { } else {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
reg, err = repos.ScanDirectory(cwd) reg, err = repos.ScanDirectory(cwd)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to scan directory: %w", err) return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.scan_directory"), err)
} }
basePath = cwd basePath = cwd
} }

View file

@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -18,16 +19,17 @@ var (
var docsSyncCmd = &cobra.Command{ var docsSyncCmd = &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync documentation to core-php/docs/packages/", Short: i18n.T("cmd.docs.sync.short"),
Long: i18n.T("cmd.docs.sync.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun) return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
}, },
} }
func init() { func init() {
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", "Path to repos.yaml") docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("cmd.docs.sync.flag.registry"))
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, "Show what would be synced without copying") docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run"))
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", "Output directory (default: core-php/docs/packages)") docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output"))
} }
// packageOutputName maps repo name to output folder name // packageOutputName maps repo name to output folder name
@ -81,11 +83,11 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
} }
if len(docsInfo) == 0 { if len(docsInfo) == 0 {
fmt.Println("No documentation found in any repos.") fmt.Println(i18n.T("cmd.docs.sync.no_docs_found"))
return nil return nil
} }
fmt.Printf("\n%s %d repo(s) with docs/ directories\n\n", dimStyle.Render("Found"), len(docsInfo)) fmt.Printf("\n%s %s\n\n", dimStyle.Render(i18n.T("cmd.docs.sync.found_label")), i18n.T("cmd.docs.sync.repos_with_docs", map[string]interface{}{"Count": len(docsInfo)}))
// Show what will be synced // Show what will be synced
var totalFiles int var totalFiles int
@ -95,25 +97,26 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
fmt.Printf(" %s → %s %s\n", fmt.Printf(" %s → %s %s\n",
repoNameStyle.Render(info.Name), repoNameStyle.Render(info.Name),
docsFileStyle.Render("packages/"+outName+"/"), docsFileStyle.Render("packages/"+outName+"/"),
dimStyle.Render(fmt.Sprintf("(%d files)", len(info.DocsFiles)))) dimStyle.Render(i18n.T("cmd.docs.sync.files_count", map[string]interface{}{"Count": len(info.DocsFiles)})))
for _, f := range info.DocsFiles { for _, f := range info.DocsFiles {
fmt.Printf(" %s\n", dimStyle.Render(f)) fmt.Printf(" %s\n", dimStyle.Render(f))
} }
} }
fmt.Printf("\n%s %d files from %d repos → %s\n", fmt.Printf("\n%s %s\n",
dimStyle.Render("Total:"), totalFiles, len(docsInfo), outputDir) dimStyle.Render(i18n.T("cmd.docs.sync.total_label")),
i18n.T("cmd.docs.sync.total_summary", map[string]interface{}{"Files": totalFiles, "Repos": len(docsInfo), "Output": outputDir}))
if dryRun { if dryRun {
fmt.Printf("\n%s\n", dimStyle.Render("Dry run - no files copied")) fmt.Printf("\n%s\n", dimStyle.Render(i18n.T("cmd.docs.sync.dry_run_notice")))
return nil return nil
} }
// Confirm // Confirm
fmt.Println() fmt.Println()
if !confirm("Sync?") { if !confirm(i18n.T("cmd.docs.sync.confirm")) {
fmt.Println("Aborted.") fmt.Println(i18n.T("cli.confirm.abort"))
return nil return nil
} }
@ -147,7 +150,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
synced++ synced++
} }
fmt.Printf("\n%s Synced %d packages\n", successStyle.Render("Done:"), synced) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.docs.sync.done_label")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced}))
return nil return nil
} }

View file

@ -3,6 +3,8 @@ package doctor
import ( import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
) )
// check represents a tool check configuration // check represents a tool check configuration
@ -14,68 +16,72 @@ type check struct {
versionFlag string versionFlag string
} }
// requiredChecks are tools that must be installed // requiredChecks returns tools that must be installed
var requiredChecks = []check{ func requiredChecks() []check {
{ return []check{
name: "Git", {
description: "Version control", name: i18n.T("cmd.doctor.check.git.name"),
command: "git", description: i18n.T("cmd.doctor.check.git.description"),
args: []string{"--version"}, command: "git",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
{ },
name: "GitHub CLI", {
description: "GitHub integration (issues, PRs, CI)", name: i18n.T("cmd.doctor.check.gh.name"),
command: "gh", description: i18n.T("cmd.doctor.check.gh.description"),
args: []string{"--version"}, command: "gh",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
{ },
name: "PHP", {
description: "Laravel packages", name: i18n.T("cmd.doctor.check.php.name"),
command: "php", description: i18n.T("cmd.doctor.check.php.description"),
args: []string{"-v"}, command: "php",
versionFlag: "-v", args: []string{"-v"},
}, versionFlag: "-v",
{ },
name: "Composer", {
description: "PHP dependencies", name: i18n.T("cmd.doctor.check.composer.name"),
command: "composer", description: i18n.T("cmd.doctor.check.composer.description"),
args: []string{"--version"}, command: "composer",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
{ },
name: "Node.js", {
description: "Frontend builds", name: i18n.T("cmd.doctor.check.node.name"),
command: "node", description: i18n.T("cmd.doctor.check.node.description"),
args: []string{"--version"}, command: "node",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
},
}
} }
// optionalChecks are tools that are nice to have // optionalChecks returns tools that are nice to have
var optionalChecks = []check{ func optionalChecks() []check {
{ return []check{
name: "pnpm", {
description: "Fast package manager", name: i18n.T("cmd.doctor.check.pnpm.name"),
command: "pnpm", description: i18n.T("cmd.doctor.check.pnpm.description"),
args: []string{"--version"}, command: "pnpm",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
{ },
name: "Claude Code", {
description: "AI-assisted development", name: i18n.T("cmd.doctor.check.claude.name"),
command: "claude", description: i18n.T("cmd.doctor.check.claude.description"),
args: []string{"--version"}, command: "claude",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
{ },
name: "Docker", {
description: "Container runtime", name: i18n.T("cmd.doctor.check.docker.name"),
command: "docker", description: i18n.T("cmd.doctor.check.docker.description"),
args: []string{"--version"}, command: "docker",
versionFlag: "--version", args: []string{"--version"},
}, versionFlag: "--version",
},
}
} }
// runCheck executes a tool check and returns success status and version info // runCheck executes a tool check and returns success status and version info

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -20,27 +21,26 @@ var doctorVerbose bool
var doctorCmd = &cobra.Command{ var doctorCmd = &cobra.Command{
Use: "doctor", Use: "doctor",
Short: "Check development environment", Short: i18n.T("cmd.doctor.short"),
Long: `Checks that all required tools are installed and configured. Long: i18n.T("cmd.doctor.long"),
Run this before 'core setup' to ensure your environment is ready.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runDoctor(doctorVerbose) return runDoctor(doctorVerbose)
}, },
} }
func init() { func init() {
doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, "Show detailed version information") doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, i18n.T("cmd.doctor.verbose_flag"))
} }
func runDoctor(verbose bool) error { func runDoctor(verbose bool) error {
fmt.Println("Checking development environment...") fmt.Println(i18n.T("cmd.doctor.checking"))
fmt.Println() fmt.Println()
var passed, failed, optional int var passed, failed, optional int
// Check required tools // Check required tools
fmt.Println("Required:") fmt.Println(i18n.T("cmd.doctor.required"))
for _, c := range requiredChecks { for _, c := range requiredChecks() {
ok, version := runCheck(c) ok, version := runCheck(c)
if ok { if ok {
if verbose { if verbose {
@ -56,8 +56,8 @@ func runDoctor(verbose bool) error {
} }
// Check optional tools // Check optional tools
fmt.Println("\nOptional:") fmt.Printf("\n%s\n", i18n.T("cmd.doctor.optional"))
for _, c := range optionalChecks { for _, c := range optionalChecks() {
ok, version := runCheck(c) ok, version := runCheck(c)
if ok { if ok {
if verbose { if verbose {
@ -73,34 +73,34 @@ func runDoctor(verbose bool) error {
} }
// Check GitHub access // Check GitHub access
fmt.Println("\nGitHub Access:") fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github"))
if checkGitHubSSH() { if checkGitHubSSH() {
fmt.Println(shared.CheckResult(true, "SSH key found", "")) fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
} else { } else {
fmt.Printf(" %s SSH key missing - run: ssh-keygen && gh ssh-key add\n", errorStyle.Render(shared.SymbolCross)) fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.ssh_missing"))
failed++ failed++
} }
if checkGitHubCLI() { if checkGitHubCLI() {
fmt.Println(shared.CheckResult(true, "CLI authenticated", "")) fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
} else { } else {
fmt.Printf(" %s CLI authentication - run: gh auth login\n", errorStyle.Render(shared.SymbolCross)) fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.cli_auth_missing"))
failed++ failed++
} }
// Check workspace // Check workspace
fmt.Println("\nWorkspace:") fmt.Printf("\n%s\n", i18n.T("cmd.doctor.workspace"))
checkWorkspace() checkWorkspace()
// Summary // Summary
fmt.Println() fmt.Println()
if failed > 0 { if failed > 0 {
fmt.Println(shared.Error(fmt.Sprintf("Doctor: %d issues found", failed))) fmt.Println(shared.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed})))
fmt.Println("\nInstall missing tools:") fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing"))
printInstallInstructions() printInstallInstructions()
return fmt.Errorf("%d required tools missing", failed) return fmt.Errorf("%s", i18n.T("cmd.doctor.issues_error", map[string]interface{}{"Count": failed}))
} }
fmt.Println(shared.Success("Doctor: Environment ready")) fmt.Println(shared.Success(i18n.T("cmd.doctor.ready")))
return nil return nil
} }

View file

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
) )
@ -44,7 +45,7 @@ func checkGitHubCLI() bool {
func checkWorkspace() { func checkWorkspace() {
registryPath, err := repos.FindRegistry() registryPath, err := repos.FindRegistry()
if err == nil { if err == nil {
fmt.Printf(" %s Found repos.yaml at %s\n", successStyle.Render("✓"), registryPath) fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_yaml_found", map[string]interface{}{"Path": registryPath}))
reg, err := repos.LoadRegistry(registryPath) reg, err := repos.LoadRegistry(registryPath)
if err == nil { if err == nil {
@ -69,9 +70,9 @@ func checkWorkspace() {
cloned++ cloned++
} }
} }
fmt.Printf(" %s %d/%d repos cloned\n", successStyle.Render("✓"), cloned, len(allRepos)) fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_cloned", map[string]interface{}{"Cloned": cloned, "Total": len(allRepos)}))
} }
} else { } else {
fmt.Printf(" %s No repos.yaml found (run from workspace directory)\n", dimStyle.Render("○")) fmt.Printf(" %s %s\n", dimStyle.Render("○"), i18n.T("cmd.doctor.no_repos_yaml"))
} }
} }

View file

@ -3,22 +3,24 @@ package doctor
import ( import (
"fmt" "fmt"
"runtime" "runtime"
"github.com/host-uk/core/pkg/i18n"
) )
// printInstallInstructions prints OS-specific installation instructions // printInstallInstructions prints OS-specific installation instructions
func printInstallInstructions() { func printInstallInstructions() {
switch runtime.GOOS { switch runtime.GOOS {
case "darwin": case "darwin":
fmt.Println(" brew install git gh php composer node pnpm docker") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos"))
fmt.Println(" brew install --cask claude") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos_cask"))
case "linux": case "linux":
fmt.Println(" # Install via your package manager or:") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_header"))
fmt.Println(" # Git: apt install git") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_git"))
fmt.Println(" # GitHub CLI: https://cli.github.com/") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_gh"))
fmt.Println(" # PHP: apt install php8.3-cli") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_php"))
fmt.Println(" # Node: https://nodejs.org/") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_node"))
fmt.Println(" # pnpm: npm install -g pnpm") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_pnpm"))
default: default:
fmt.Println(" See documentation for your OS") fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_other"))
} }
} }

View file

@ -5,6 +5,7 @@ package gocmd
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -19,16 +20,8 @@ var (
func AddGoCommands(root *cobra.Command) { func AddGoCommands(root *cobra.Command) {
goCmd := &cobra.Command{ goCmd := &cobra.Command{
Use: "go", Use: "go",
Short: "Go development tools", Short: i18n.T("cmd.go.short"),
Long: "Go development tools with enhanced output and environment setup.\n\n" + Long: i18n.T("cmd.go.long"),
"Commands:\n" +
" test Run tests\n" +
" cov Run tests with coverage report\n" +
" fmt Format Go code\n" +
" lint Run golangci-lint\n" +
" install Install Go binary\n" +
" mod Module management (tidy, download, verify)\n" +
" work Workspace management",
} }
root.AddCommand(goCmd) root.AddCommand(goCmd)

View file

@ -4,6 +4,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -16,12 +17,8 @@ var (
func addGoFmtCommand(parent *cobra.Command) { func addGoFmtCommand(parent *cobra.Command) {
fmtCmd := &cobra.Command{ fmtCmd := &cobra.Command{
Use: "fmt", Use: "fmt",
Short: "Format Go code", Short: i18n.T("cmd.go.fmt.short"),
Long: "Format Go code using gofmt or goimports.\n\n" + Long: i18n.T("cmd.go.fmt.long"),
"Examples:\n" +
" core go fmt # Check formatting\n" +
" core go fmt --fix # Fix formatting\n" +
" core go fmt --diff # Show diff",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
fmtArgs := []string{} fmtArgs := []string{}
if fmtFix { if fmtFix {
@ -49,9 +46,9 @@ func addGoFmtCommand(parent *cobra.Command) {
}, },
} }
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Fix formatting in place") fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.go.fmt.flag.fix"))
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes") fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("cmd.go.fmt.flag.diff"))
fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, "Check only, exit 1 if not formatted") fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, i18n.T("cmd.go.fmt.flag.check"))
parent.AddCommand(fmtCmd) parent.AddCommand(fmtCmd)
} }
@ -61,11 +58,8 @@ var lintFix bool
func addGoLintCommand(parent *cobra.Command) { func addGoLintCommand(parent *cobra.Command) {
lintCmd := &cobra.Command{ lintCmd := &cobra.Command{
Use: "lint", Use: "lint",
Short: "Run golangci-lint", Short: i18n.T("cmd.go.lint.short"),
Long: "Run golangci-lint on the codebase.\n\n" + Long: i18n.T("cmd.go.lint.long"),
"Examples:\n" +
" core go lint\n" +
" core go lint --fix",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
lintArgs := []string{"run"} lintArgs := []string{"run"}
if lintFix { if lintFix {
@ -79,7 +73,7 @@ func addGoLintCommand(parent *cobra.Command) {
}, },
} }
lintCmd.Flags().BoolVar(&lintFix, "fix", false, "Fix issues automatically") lintCmd.Flags().BoolVar(&lintFix, "fix", false, i18n.T("cmd.go.lint.flag.fix"))
parent.AddCommand(lintCmd) parent.AddCommand(lintCmd)
} }

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -25,27 +26,20 @@ var (
func addGoTestCommand(parent *cobra.Command) { func addGoTestCommand(parent *cobra.Command) {
testCmd := &cobra.Command{ testCmd := &cobra.Command{
Use: "test", Use: "test",
Short: "Run tests with coverage", Short: i18n.T("cmd.go.test.short"),
Long: "Run Go tests with coverage reporting.\n\n" + Long: i18n.T("cmd.go.test.long"),
"Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" +
"Filters noisy output and provides colour-coded coverage.\n\n" +
"Examples:\n" +
" core go test\n" +
" core go test --coverage\n" +
" core go test --pkg ./pkg/crypt\n" +
" core go test --run TestHash",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose) return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose)
}, },
} }
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage") testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.go.test.flag.coverage"))
testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package to test (default: ./...)") testCmd.Flags().StringVar(&testPkg, "pkg", "", i18n.T("cmd.go.test.flag.pkg"))
testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching regexp") testCmd.Flags().StringVar(&testRun, "run", "", i18n.T("cmd.go.test.flag.run"))
testCmd.Flags().BoolVar(&testShort, "short", false, "Run only short tests") testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.go.test.flag.short"))
testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector") testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.go.test.flag.race"))
testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON results") testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.go.test.flag.json"))
testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, "Verbose output") testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, i18n.T("cmd.go.test.flag.verbose"))
parent.AddCommand(testCmd) parent.AddCommand(testCmd)
} }
@ -79,8 +73,8 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
args = append(args, pkg) args = append(args, pkg)
if !jsonOut { if !jsonOut {
fmt.Printf("%s Running tests\n", dimStyle.Render("Test:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.test.label")), i18n.T("cmd.go.test.running"))
fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), pkg) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.test.package_label")), pkg)
fmt.Println() fmt.Println()
} }
@ -119,19 +113,19 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
// Summary // Summary
if err == nil { if err == nil {
fmt.Printf(" %s %d passed\n", successStyle.Render("✓"), passed) fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.go.test.passed", map[string]interface{}{"Count": passed}))
} else { } else {
fmt.Printf(" %s %d passed, %d failed\n", errorStyle.Render("✗"), passed, failed) fmt.Printf(" %s %s\n", errorStyle.Render("✗"), i18n.T("cmd.go.test.passed_failed", map[string]interface{}{"Passed": passed, "Failed": failed}))
} }
if cov > 0 { if cov > 0 {
fmt.Printf("\n %s %s\n", shared.ProgressLabel("Coverage"), shared.FormatCoverage(cov)) fmt.Printf("\n %s %s\n", shared.ProgressLabel(i18n.T("cmd.go.test.coverage")), shared.FormatCoverage(cov))
} }
if err == nil { if err == nil {
fmt.Printf("\n%s\n", successStyle.Render("PASS All tests passed")) fmt.Printf("\n%s\n", successStyle.Render(i18n.T("cmd.go.test.all_passed")))
} else { } else {
fmt.Printf("\n%s\n", errorStyle.Render("FAIL Some tests failed")) fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.test.some_failed")))
} }
return err return err
@ -174,23 +168,18 @@ var (
func addGoCovCommand(parent *cobra.Command) { func addGoCovCommand(parent *cobra.Command) {
covCmd := &cobra.Command{ covCmd := &cobra.Command{
Use: "cov", Use: "cov",
Short: "Run tests with coverage report", Short: i18n.T("cmd.go.cov.short"),
Long: "Run tests and generate coverage report.\n\n" + Long: i18n.T("cmd.go.cov.long"),
"Examples:\n" +
" core go cov # Run with coverage summary\n" +
" core go cov --html # Generate HTML report\n" +
" core go cov --open # Generate and open HTML report\n" +
" core go cov --threshold 80 # Fail if coverage < 80%",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
pkg := covPkg pkg := covPkg
if pkg == "" { if pkg == "" {
// Auto-discover packages with tests // Auto-discover packages with tests
pkgs, err := findTestPackages(".") pkgs, err := findTestPackages(".")
if err != nil { if err != nil {
return fmt.Errorf("failed to discover test packages: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.discover"), err)
} }
if len(pkgs) == 0 { if len(pkgs) == 0 {
return fmt.Errorf("no test packages found") return fmt.Errorf(i18n.T("cmd.go.cov.error.no_packages"))
} }
pkg = strings.Join(pkgs, " ") pkg = strings.Join(pkgs, " ")
} }
@ -198,19 +187,19 @@ func addGoCovCommand(parent *cobra.Command) {
// Create temp file for coverage data // Create temp file for coverage data
covFile, err := os.CreateTemp("", "coverage-*.out") covFile, err := os.CreateTemp("", "coverage-*.out")
if err != nil { if err != nil {
return fmt.Errorf("failed to create coverage file: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.create_file"), err)
} }
covPath := covFile.Name() covPath := covFile.Name()
covFile.Close() covFile.Close()
defer os.Remove(covPath) defer os.Remove(covPath)
fmt.Printf("%s Running tests with coverage\n", dimStyle.Render("Coverage:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.cov.label")), i18n.T("cmd.go.cov.running"))
// Truncate package list if too long for display // Truncate package list if too long for display
displayPkg := pkg displayPkg := pkg
if len(displayPkg) > 60 { if len(displayPkg) > 60 {
displayPkg = displayPkg[:57] + "..." displayPkg = displayPkg[:57] + "..."
} }
fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), displayPkg) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.test.package_label")), displayPkg)
fmt.Println() fmt.Println()
// Run tests with coverage // Run tests with coverage
@ -232,7 +221,7 @@ func addGoCovCommand(parent *cobra.Command) {
if testErr != nil { if testErr != nil {
return testErr return testErr
} }
return fmt.Errorf("failed to get coverage: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.get_coverage"), err)
} }
// Parse total coverage from last line // Parse total coverage from last line
@ -252,16 +241,16 @@ func addGoCovCommand(parent *cobra.Command) {
// Print coverage summary // Print coverage summary
fmt.Println() fmt.Println()
fmt.Printf(" %s %s\n", shared.ProgressLabel("Total"), shared.FormatCoverage(totalCov)) fmt.Printf(" %s %s\n", shared.ProgressLabel(i18n.T("label.total")), shared.FormatCoverage(totalCov))
// Generate HTML if requested // Generate HTML if requested
if covHTML || covOpen { if covHTML || covOpen {
htmlPath := "coverage.html" htmlPath := "coverage.html"
htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath) htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath)
if err := htmlCmd.Run(); err != nil { if err := htmlCmd.Run(); err != nil {
return fmt.Errorf("failed to generate HTML: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.generate_html"), err)
} }
fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.cov.html_label")), htmlPath)
if covOpen { if covOpen {
// Open in browser // Open in browser
@ -272,7 +261,7 @@ func addGoCovCommand(parent *cobra.Command) {
case exec.Command("which", "xdg-open").Run() == nil: case exec.Command("which", "xdg-open").Run() == nil:
openCmd = exec.Command("xdg-open", htmlPath) openCmd = exec.Command("xdg-open", htmlPath)
default: default:
fmt.Printf(" %s\n", dimStyle.Render("(open manually)")) fmt.Printf(" %s\n", dimStyle.Render(i18n.T("cmd.go.cov.open_manually")))
} }
if openCmd != nil { if openCmd != nil {
openCmd.Run() openCmd.Run()
@ -282,24 +271,26 @@ func addGoCovCommand(parent *cobra.Command) {
// Check threshold // Check threshold
if covThreshold > 0 && totalCov < covThreshold { if covThreshold > 0 && totalCov < covThreshold {
fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n", fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.cov.below_threshold", map[string]interface{}{
errorStyle.Render("FAIL"), totalCov, covThreshold) "Actual": fmt.Sprintf("%.1f", totalCov),
return fmt.Errorf("coverage below threshold") "Threshold": fmt.Sprintf("%.1f", covThreshold),
})))
return fmt.Errorf(i18n.T("cmd.go.cov.error.below_threshold"))
} }
if testErr != nil { if testErr != nil {
return testErr return testErr
} }
fmt.Printf("\n%s\n", successStyle.Render("OK")) fmt.Printf("\n%s\n", successStyle.Render(i18n.T("cli.ok")))
return nil return nil
}, },
} }
covCmd.Flags().StringVar(&covPkg, "pkg", "", "Package to test (default: ./...)") covCmd.Flags().StringVar(&covPkg, "pkg", "", i18n.T("cmd.go.cov.flag.pkg"))
covCmd.Flags().BoolVar(&covHTML, "html", false, "Generate HTML coverage report") covCmd.Flags().BoolVar(&covHTML, "html", false, i18n.T("cmd.go.cov.flag.html"))
covCmd.Flags().BoolVar(&covOpen, "open", false, "Generate and open HTML report in browser") covCmd.Flags().BoolVar(&covOpen, "open", false, i18n.T("cmd.go.cov.flag.open"))
covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, "Minimum coverage percentage (exit 1 if below)") covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, i18n.T("cmd.go.cov.flag.threshold"))
parent.AddCommand(covCmd) parent.AddCommand(covCmd)
} }

View file

@ -6,6 +6,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,13 +18,8 @@ var (
func addGoInstallCommand(parent *cobra.Command) { func addGoInstallCommand(parent *cobra.Command) {
installCmd := &cobra.Command{ installCmd := &cobra.Command{
Use: "install [path]", Use: "install [path]",
Short: "Install Go binary", Short: i18n.T("cmd.go.install.short"),
Long: "Install Go binary to $GOPATH/bin.\n\n" + Long: i18n.T("cmd.go.install.long"),
"Examples:\n" +
" core go install # Install current module\n" +
" core go install ./cmd/core # Install specific path\n" +
" core go install --no-cgo # Pure Go (no C dependencies)\n" +
" core go install -v # Verbose output",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
// Get install path from args or default to current dir // Get install path from args or default to current dir
installPath := "./..." installPath := "./..."
@ -42,10 +38,10 @@ func addGoInstallCommand(parent *cobra.Command) {
} }
} }
fmt.Printf("%s Installing\n", dimStyle.Render("Install:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.install.label")), i18n.T("cmd.go.install.installing"))
fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.install.path_label")), installPath)
if installNoCgo { if installNoCgo {
fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled") fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.install.cgo_label")), i18n.T("cmd.go.install.cgo_disabled"))
} }
cmdArgs := []string{"install"} cmdArgs := []string{"install"}
@ -62,7 +58,7 @@ func addGoInstallCommand(parent *cobra.Command) {
execCmd.Stderr = os.Stderr execCmd.Stderr = os.Stderr
if err := execCmd.Run(); err != nil { if err := execCmd.Run(); err != nil {
fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed")) fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.install.failed")))
return err return err
} }
@ -74,13 +70,13 @@ func addGoInstallCommand(parent *cobra.Command) {
} }
binDir := filepath.Join(gopath, "bin") binDir := filepath.Join(gopath, "bin")
fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.go.install.success")), i18n.T("cmd.go.install.installed_to", map[string]interface{}{"Path": binDir}))
return nil return nil
}, },
} }
installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, "Verbose output") installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, i18n.T("cmd.go.install.flag.verbose"))
installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, "Disable CGO (CGO_ENABLED=0)") installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, i18n.T("cmd.go.install.flag.no_cgo"))
parent.AddCommand(installCmd) parent.AddCommand(installCmd)
} }
@ -88,19 +84,14 @@ func addGoInstallCommand(parent *cobra.Command) {
func addGoModCommand(parent *cobra.Command) { func addGoModCommand(parent *cobra.Command) {
modCmd := &cobra.Command{ modCmd := &cobra.Command{
Use: "mod", Use: "mod",
Short: "Module management", Short: i18n.T("cmd.go.mod.short"),
Long: "Go module management commands.\n\n" + Long: i18n.T("cmd.go.mod.long"),
"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 // tidy
tidyCmd := &cobra.Command{ tidyCmd := &cobra.Command{
Use: "tidy", Use: "tidy",
Short: "Tidy go.mod", Short: i18n.T("cmd.go.mod.tidy.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "tidy") execCmd := exec.Command("go", "mod", "tidy")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -112,7 +103,7 @@ func addGoModCommand(parent *cobra.Command) {
// download // download
downloadCmd := &cobra.Command{ downloadCmd := &cobra.Command{
Use: "download", Use: "download",
Short: "Download modules", Short: i18n.T("cmd.go.mod.download.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "download") execCmd := exec.Command("go", "mod", "download")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -124,7 +115,7 @@ func addGoModCommand(parent *cobra.Command) {
// verify // verify
verifyCmd := &cobra.Command{ verifyCmd := &cobra.Command{
Use: "verify", Use: "verify",
Short: "Verify dependencies", Short: i18n.T("cmd.go.mod.verify.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "verify") execCmd := exec.Command("go", "mod", "verify")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -136,7 +127,7 @@ func addGoModCommand(parent *cobra.Command) {
// graph // graph
graphCmd := &cobra.Command{ graphCmd := &cobra.Command{
Use: "graph", Use: "graph",
Short: "Print dependency graph", Short: i18n.T("cmd.go.mod.graph.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "mod", "graph") execCmd := exec.Command("go", "mod", "graph")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -155,18 +146,14 @@ func addGoModCommand(parent *cobra.Command) {
func addGoWorkCommand(parent *cobra.Command) { func addGoWorkCommand(parent *cobra.Command) {
workCmd := &cobra.Command{ workCmd := &cobra.Command{
Use: "work", Use: "work",
Short: "Workspace management", Short: i18n.T("cmd.go.work.short"),
Long: "Go workspace management commands.\n\n" + Long: i18n.T("cmd.go.work.long"),
"Commands:\n" +
" sync Sync go.work with modules\n" +
" init Initialize go.work\n" +
" use Add module to workspace",
} }
// sync // sync
syncCmd := &cobra.Command{ syncCmd := &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync workspace", Short: i18n.T("cmd.go.work.sync.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "work", "sync") execCmd := exec.Command("go", "work", "sync")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -178,7 +165,7 @@ func addGoWorkCommand(parent *cobra.Command) {
// init // init
initCmd := &cobra.Command{ initCmd := &cobra.Command{
Use: "init", Use: "init",
Short: "Initialize workspace", Short: i18n.T("cmd.go.work.init.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
execCmd := exec.Command("go", "work", "init") execCmd := exec.Command("go", "work", "init")
execCmd.Stdout = os.Stdout execCmd.Stdout = os.Stdout
@ -200,13 +187,13 @@ func addGoWorkCommand(parent *cobra.Command) {
// use // use
useCmd := &cobra.Command{ useCmd := &cobra.Command{
Use: "use [modules...]", Use: "use [modules...]",
Short: "Add module to workspace", Short: i18n.T("cmd.go.work.use.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
// Auto-detect modules // Auto-detect modules
modules := findGoModules(".") modules := findGoModules(".")
if len(modules) == 0 { if len(modules) == 0 {
return fmt.Errorf("no go.mod files found") return fmt.Errorf(i18n.T("cmd.go.work.error.no_modules"))
} }
for _, mod := range modules { for _, mod := range modules {
execCmd := exec.Command("go", "work", "use", mod) execCmd := exec.Command("go", "work", "use", mod)
@ -215,7 +202,7 @@ func addGoWorkCommand(parent *cobra.Command) {
if err := execCmd.Run(); err != nil { if err := execCmd.Run(); err != nil {
return err return err
} }
fmt.Printf("Added %s\n", mod) fmt.Println(i18n.T("cmd.go.work.added", map[string]interface{}{"Module": mod}))
} }
return nil return nil
} }

View file

@ -4,6 +4,7 @@ package php
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -51,14 +52,8 @@ var (
func AddPHPCommands(root *cobra.Command) { func AddPHPCommands(root *cobra.Command) {
phpCmd := &cobra.Command{ phpCmd := &cobra.Command{
Use: "php", Use: "php",
Short: "Laravel/PHP development tools", Short: i18n.T("cmd.php.short"),
Long: "Manage Laravel development environment with FrankenPHP.\n\n" + Long: i18n.T("cmd.php.long"),
"Services orchestrated:\n" +
" - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" +
" - Vite dev server (port 5173)\n" +
" - Laravel Horizon (queue workers)\n" +
" - Laravel Reverb (WebSocket, port 8080)\n" +
" - Redis (port 6379)",
} }
root.AddCommand(phpCmd) root.AddCommand(phpCmd)

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -25,19 +26,12 @@ var (
func addPHPBuildCommand(parent *cobra.Command) { func addPHPBuildCommand(parent *cobra.Command) {
buildCmd := &cobra.Command{ buildCmd := &cobra.Command{
Use: "build", Use: "build",
Short: "Build Docker or LinuxKit image", Short: i18n.T("cmd.php.build.short"),
Long: "Build a production-ready container image for the PHP project.\n\n" + Long: i18n.T("cmd.php.build.long"),
"By default, builds a Docker image using FrankenPHP.\n" +
"Use --type linuxkit to build a LinuxKit VM image instead.\n\n" +
"Examples:\n" +
" core php build # Build Docker image\n" +
" core php build --name myapp --tag v1.0 # Build with custom name/tag\n" +
" core php build --type linuxkit # Build LinuxKit image\n" +
" core php build --type linuxkit --format iso # Build ISO image",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
ctx := context.Background() ctx := context.Background()
@ -61,15 +55,15 @@ func addPHPBuildCommand(parent *cobra.Command) {
}, },
} }
buildCmd.Flags().StringVar(&buildType, "type", "", "Build type: docker (default) or linuxkit") buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.php.build.flag.type"))
buildCmd.Flags().StringVar(&buildImageName, "name", "", "Image name (default: project directory name)") buildCmd.Flags().StringVar(&buildImageName, "name", "", i18n.T("cmd.php.build.flag.name"))
buildCmd.Flags().StringVar(&buildTag, "tag", "", "Image tag (default: latest)") buildCmd.Flags().StringVar(&buildTag, "tag", "", i18n.T("cmd.php.build.flag.tag"))
buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64, linux/arm64)") buildCmd.Flags().StringVar(&buildPlatform, "platform", "", i18n.T("cmd.php.build.flag.platform"))
buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", "Path to custom Dockerfile") buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", i18n.T("cmd.php.build.flag.dockerfile"))
buildCmd.Flags().StringVar(&buildOutputPath, "output", "", "Output path for LinuxKit image") buildCmd.Flags().StringVar(&buildOutputPath, "output", "", i18n.T("cmd.php.build.flag.output"))
buildCmd.Flags().StringVar(&buildFormat, "format", "", "LinuxKit output format: qcow2 (default), iso, raw, vmdk") buildCmd.Flags().StringVar(&buildFormat, "format", "", i18n.T("cmd.php.build.flag.format"))
buildCmd.Flags().StringVar(&buildTemplate, "template", "", "LinuxKit template name (default: server-php)") buildCmd.Flags().StringVar(&buildTemplate, "template", "", i18n.T("cmd.php.build.flag.template"))
buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, "Build without cache") buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, i18n.T("cmd.php.build.flag.no_cache"))
parent.AddCommand(buildCmd) parent.AddCommand(buildCmd)
} }
@ -90,23 +84,23 @@ type linuxKitBuildOptions struct {
func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error { func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error {
if !phppkg.IsPHPProject(projectDir) { if !phppkg.IsPHPProject(projectDir) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
fmt.Printf("%s Building Docker image...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker"))
// Show detected configuration // Show detected configuration
config, err := phppkg.DetectDockerfileConfig(projectDir) config, err := phppkg.DetectDockerfileConfig(projectDir)
if err != nil { if err != nil {
return fmt.Errorf("failed to detect project configuration: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.detect_config"), err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("PHP Version:"), config.PHPVersion) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.php_version")), config.PHPVersion)
fmt.Printf("%s %v\n", dimStyle.Render("Laravel:"), config.IsLaravel) fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.laravel")), config.IsLaravel)
fmt.Printf("%s %v\n", dimStyle.Render("Octane:"), config.HasOctane) fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.octane")), config.HasOctane)
fmt.Printf("%s %v\n", dimStyle.Render("Frontend:"), config.HasAssets) fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.frontend")), config.HasAssets)
if len(config.PHPExtensions) > 0 { if len(config.PHPExtensions) > 0 {
fmt.Printf("%s %s\n", dimStyle.Render("Extensions:"), strings.Join(config.PHPExtensions, ", ")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.extensions")), strings.Join(config.PHPExtensions, ", "))
} }
fmt.Println() fmt.Println()
@ -134,19 +128,19 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
buildOpts.Tag = "latest" buildOpts.Tag = "latest"
} }
fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), buildOpts.ImageName, buildOpts.Tag) fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.T("cmd.php.build.image")), buildOpts.ImageName, buildOpts.Tag)
if opts.Platform != "" { if opts.Platform != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Platform:"), opts.Platform) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.platform")), opts.Platform)
} }
fmt.Println() fmt.Println()
if err := phppkg.BuildDocker(ctx, buildOpts); err != nil { if err := phppkg.BuildDocker(ctx, buildOpts); err != nil {
return fmt.Errorf("build failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.build_failed"), err)
} }
fmt.Printf("\n%s Docker image built successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.build.docker_success"))
fmt.Printf("%s docker run -p 80:80 -p 443:443 %s:%s\n", fmt.Printf("%s docker run -p 80:80 -p 443:443 %s:%s\n",
dimStyle.Render("Run with:"), dimStyle.Render(i18n.T("cmd.php.build.docker_run_with")),
buildOpts.ImageName, buildOpts.Tag) buildOpts.ImageName, buildOpts.Tag)
return nil return nil
@ -154,10 +148,10 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error { func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error {
if !phppkg.IsPHPProject(projectDir) { if !phppkg.IsPHPProject(projectDir) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
fmt.Printf("%s Building LinuxKit image...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit"))
buildOpts := phppkg.LinuxKitBuildOptions{ buildOpts := phppkg.LinuxKitBuildOptions{
ProjectDir: projectDir, ProjectDir: projectDir,
@ -174,15 +168,15 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu
buildOpts.Template = "server-php" buildOpts.Template = "server-php"
} }
fmt.Printf("%s %s\n", dimStyle.Render("Template:"), buildOpts.Template) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.template")), buildOpts.Template)
fmt.Printf("%s %s\n", dimStyle.Render("Format:"), buildOpts.Format) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format)
fmt.Println() fmt.Println()
if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil { if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil {
return fmt.Errorf("build failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.build_failed"), err)
} }
fmt.Printf("\n%s LinuxKit image built successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.build.linuxkit_success"))
return nil return nil
} }
@ -199,13 +193,8 @@ var (
func addPHPServeCommand(parent *cobra.Command) { func addPHPServeCommand(parent *cobra.Command) {
serveCmd := &cobra.Command{ serveCmd := &cobra.Command{
Use: "serve", Use: "serve",
Short: "Run production container", Short: i18n.T("cmd.php.serve.short"),
Long: "Run a production PHP container.\n\n" + Long: i18n.T("cmd.php.serve.long"),
"This starts the built Docker image in production mode.\n\n" +
"Examples:\n" +
" core php serve --name myapp # Run container\n" +
" core php serve --name myapp -d # Run detached\n" +
" core php serve --name myapp --port 8080 # Custom port",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
imageName := serveImageName imageName := serveImageName
if imageName == "" { if imageName == "" {
@ -218,7 +207,7 @@ func addPHPServeCommand(parent *cobra.Command) {
} }
} }
if imageName == "" { if imageName == "" {
return fmt.Errorf("--name is required: specify the Docker image name") return fmt.Errorf(i18n.T("cmd.php.serve.name_required"))
} }
} }
@ -235,8 +224,8 @@ func addPHPServeCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
} }
fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.running"))
fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string { fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.T("cmd.php.build.image")), imageName, func() string {
if serveTag == "" { if serveTag == "" {
return "latest" return "latest"
} }
@ -257,24 +246,24 @@ func addPHPServeCommand(parent *cobra.Command) {
fmt.Println() fmt.Println()
if err := phppkg.ServeProduction(ctx, opts); err != nil { if err := phppkg.ServeProduction(ctx, opts); err != nil {
return fmt.Errorf("failed to start container: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.start_container"), err)
} }
if !serveDetach { if !serveDetach {
fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:")) fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.stopped"))
} }
return nil return nil
}, },
} }
serveCmd.Flags().StringVar(&serveImageName, "name", "", "Docker image name (required)") serveCmd.Flags().StringVar(&serveImageName, "name", "", i18n.T("cmd.php.serve.flag.name"))
serveCmd.Flags().StringVar(&serveTag, "tag", "", "Image tag (default: latest)") serveCmd.Flags().StringVar(&serveTag, "tag", "", i18n.T("cmd.php.serve.flag.tag"))
serveCmd.Flags().StringVar(&serveContainerName, "container", "", "Container name") serveCmd.Flags().StringVar(&serveContainerName, "container", "", i18n.T("cmd.php.serve.flag.container"))
serveCmd.Flags().IntVar(&servePort, "port", 0, "HTTP port (default: 80)") serveCmd.Flags().IntVar(&servePort, "port", 0, i18n.T("cmd.php.serve.flag.port"))
serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, "HTTPS port (default: 443)") serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, i18n.T("cmd.php.serve.flag.https_port"))
serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, "Run in detached mode") serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, i18n.T("cmd.php.serve.flag.detach"))
serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", "Path to environment file") serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", i18n.T("cmd.php.serve.flag.env_file"))
parent.AddCommand(serveCmd) parent.AddCommand(serveCmd)
} }
@ -282,19 +271,16 @@ func addPHPServeCommand(parent *cobra.Command) {
func addPHPShellCommand(parent *cobra.Command) { func addPHPShellCommand(parent *cobra.Command) {
shellCmd := &cobra.Command{ shellCmd := &cobra.Command{
Use: "shell [container]", Use: "shell [container]",
Short: "Open shell in running container", Short: i18n.T("cmd.php.shell.short"),
Long: "Open an interactive shell in a running PHP container.\n\n" + Long: i18n.T("cmd.php.shell.long"),
"Examples:\n" + Args: cobra.ExactArgs(1),
" core php shell abc123 # Shell into container by ID\n" +
" core php shell myapp # Shell into container by name",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background() ctx := context.Background()
fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0]) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]}))
if err := phppkg.Shell(ctx, args[0]); err != nil { if err := phppkg.Shell(ctx, args[0]); err != nil {
return fmt.Errorf("failed to open shell: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.open_shell"), err)
} }
return nil return nil

View file

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -41,22 +42,12 @@ var (
func addPHPDeployCommand(parent *cobra.Command) { func addPHPDeployCommand(parent *cobra.Command) {
deployCmd := &cobra.Command{ deployCmd := &cobra.Command{
Use: "deploy", Use: "deploy",
Short: "Deploy to Coolify", Short: i18n.T("cmd.php.deploy.short"),
Long: "Deploy the PHP application to Coolify.\n\n" + Long: i18n.T("cmd.php.deploy.long"),
"Requires configuration in .env:\n" +
" COOLIFY_URL=https://coolify.example.com\n" +
" COOLIFY_TOKEN=your-api-token\n" +
" COOLIFY_APP_ID=production-app-id\n" +
" COOLIFY_STAGING_APP_ID=staging-app-id (optional)\n\n" +
"Examples:\n" +
" core php deploy # Deploy to production\n" +
" core php deploy --staging # Deploy to staging\n" +
" core php deploy --force # Force deployment\n" +
" core php deploy --wait # Wait for deployment to complete",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
@ -64,7 +55,7 @@ func addPHPDeployCommand(parent *cobra.Command) {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
fmt.Printf("%s Deploying to %s...\n\n", dimStyle.Render("Deploy:"), env) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env}))
ctx := context.Background() ctx := context.Background()
@ -77,28 +68,28 @@ func addPHPDeployCommand(parent *cobra.Command) {
status, err := phppkg.Deploy(ctx, opts) status, err := phppkg.Deploy(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("deployment failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err)
} }
printDeploymentStatus(status) printDeploymentStatus(status)
if deployWait { if deployWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if phppkg.IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy.success"))
} else { } else {
fmt.Printf("\n%s Deployment ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status}))
} }
} else { } else {
fmt.Printf("\n%s Deployment triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy.triggered"))
} }
return nil return nil
}, },
} }
deployCmd.Flags().BoolVar(&deployStaging, "staging", false, "Deploy to staging environment") deployCmd.Flags().BoolVar(&deployStaging, "staging", false, i18n.T("cmd.php.deploy.flag.staging"))
deployCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if no changes detected") deployCmd.Flags().BoolVar(&deployForce, "force", false, i18n.T("cmd.php.deploy.flag.force"))
deployCmd.Flags().BoolVar(&deployWait, "wait", false, "Wait for deployment to complete") deployCmd.Flags().BoolVar(&deployWait, "wait", false, i18n.T("cmd.php.deploy.flag.wait"))
parent.AddCommand(deployCmd) parent.AddCommand(deployCmd)
} }
@ -111,16 +102,12 @@ var (
func addPHPDeployStatusCommand(parent *cobra.Command) { func addPHPDeployStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{ statusCmd := &cobra.Command{
Use: "deploy:status", Use: "deploy:status",
Short: "Show deployment status", Short: i18n.T("cmd.php.deploy_status.short"),
Long: "Show the status of a deployment.\n\n" + Long: i18n.T("cmd.php.deploy_status.long"),
"Examples:\n" +
" core php deploy:status # Latest production deployment\n" +
" core php deploy:status --staging # Latest staging deployment\n" +
" core php deploy:status --id abc123 # Specific deployment",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
@ -128,7 +115,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
fmt.Printf("%s Checking %s deployment status...\n\n", dimStyle.Render("Deploy:"), env) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_status.checking", map[string]interface{}{"Environment": env}))
ctx := context.Background() ctx := context.Background()
@ -140,7 +127,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
status, err := phppkg.DeployStatus(ctx, opts) status, err := phppkg.DeployStatus(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to get status: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.status_failed"), err)
} }
printDeploymentStatus(status) printDeploymentStatus(status)
@ -149,8 +136,8 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
}, },
} }
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, "Check staging environment") statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, i18n.T("cmd.php.deploy_status.flag.staging"))
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", "Specific deployment ID") statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", i18n.T("cmd.php.deploy_status.flag.id"))
parent.AddCommand(statusCmd) parent.AddCommand(statusCmd)
} }
@ -164,18 +151,12 @@ var (
func addPHPDeployRollbackCommand(parent *cobra.Command) { func addPHPDeployRollbackCommand(parent *cobra.Command) {
rollbackCmd := &cobra.Command{ rollbackCmd := &cobra.Command{
Use: "deploy:rollback", Use: "deploy:rollback",
Short: "Rollback to previous deployment", Short: i18n.T("cmd.php.deploy_rollback.short"),
Long: "Rollback to a previous deployment.\n\n" + Long: i18n.T("cmd.php.deploy_rollback.long"),
"If no deployment ID is specified, rolls back to the most recent\n" +
"successful deployment.\n\n" +
"Examples:\n" +
" core php deploy:rollback # Rollback to previous\n" +
" core php deploy:rollback --staging # Rollback staging\n" +
" core php deploy:rollback --id abc123 # Rollback to specific deployment",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
@ -183,7 +164,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
env = phppkg.EnvStaging env = phppkg.EnvStaging
} }
fmt.Printf("%s Rolling back %s...\n\n", dimStyle.Render("Deploy:"), env) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env}))
ctx := context.Background() ctx := context.Background()
@ -196,28 +177,28 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
status, err := phppkg.Rollback(ctx, opts) status, err := phppkg.Rollback(ctx, opts)
if err != nil { if err != nil {
return fmt.Errorf("rollback failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err)
} }
printDeploymentStatus(status) printDeploymentStatus(status)
if rollbackWait { if rollbackWait {
if phppkg.IsDeploymentSuccessful(status.Status) { if phppkg.IsDeploymentSuccessful(status.Status) {
fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy_rollback.success"))
} else { } else {
fmt.Printf("\n%s Rollback ended with status: %s\n", errorStyle.Render("Warning:"), status.Status) fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status}))
} }
} else { } else {
fmt.Printf("\n%s Rollback triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy_rollback.triggered"))
} }
return nil return nil
}, },
} }
rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, "Rollback staging environment") rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, i18n.T("cmd.php.deploy_rollback.flag.staging"))
rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", "Specific deployment ID to rollback to") rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", i18n.T("cmd.php.deploy_rollback.flag.id"))
rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, "Wait for rollback to complete") rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, i18n.T("cmd.php.deploy_rollback.flag.wait"))
parent.AddCommand(rollbackCmd) parent.AddCommand(rollbackCmd)
} }
@ -230,16 +211,12 @@ var (
func addPHPDeployListCommand(parent *cobra.Command) { func addPHPDeployListCommand(parent *cobra.Command) {
listCmd := &cobra.Command{ listCmd := &cobra.Command{
Use: "deploy:list", Use: "deploy:list",
Short: "List recent deployments", Short: i18n.T("cmd.php.deploy_list.short"),
Long: "List recent deployments.\n\n" + Long: i18n.T("cmd.php.deploy_list.long"),
"Examples:\n" +
" core php deploy:list # List production deployments\n" +
" core php deploy:list --staging # List staging deployments\n" +
" core php deploy:list --limit 20 # List more deployments",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
env := phppkg.EnvProduction env := phppkg.EnvProduction
@ -252,17 +229,17 @@ func addPHPDeployListCommand(parent *cobra.Command) {
limit = 10 limit = 10
} }
fmt.Printf("%s Recent %s deployments:\n\n", dimStyle.Render("Deploy:"), env) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_list.recent", map[string]interface{}{"Environment": env}))
ctx := context.Background() ctx := context.Background()
deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit) deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit)
if err != nil { if err != nil {
return fmt.Errorf("failed to list deployments: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.list_deployments"), err)
} }
if len(deployments) == 0 { if len(deployments) == 0 {
fmt.Printf("%s No deployments found\n", dimStyle.Render("Info:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.deploy_list.none_found"))
return nil return nil
} }
@ -274,8 +251,8 @@ func addPHPDeployListCommand(parent *cobra.Command) {
}, },
} }
listCmd.Flags().BoolVar(&deployListStaging, "staging", false, "List staging deployments") listCmd.Flags().BoolVar(&deployListStaging, "staging", false, i18n.T("cmd.php.deploy_list.flag.staging"))
listCmd.Flags().IntVar(&deployListLimit, "limit", 0, "Number of deployments to list (default: 10)") listCmd.Flags().IntVar(&deployListLimit, "limit", 0, i18n.T("cmd.php.deploy_list.flag.limit"))
parent.AddCommand(listCmd) parent.AddCommand(listCmd)
} }
@ -290,18 +267,18 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
statusStyle = phpDeployFailedStyle statusStyle = phpDeployFailedStyle
} }
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), statusStyle.Render(status.Status)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.status")), statusStyle.Render(status.Status))
if status.ID != "" { if status.ID != "" {
fmt.Printf("%s %s\n", dimStyle.Render("ID:"), status.ID) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.id")), status.ID)
} }
if status.URL != "" { if status.URL != "" {
fmt.Printf("%s %s\n", dimStyle.Render("URL:"), linkStyle.Render(status.URL)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.url")), linkStyle.Render(status.URL))
} }
if status.Branch != "" { if status.Branch != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Branch:"), status.Branch) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.branch")), status.Branch)
} }
if status.Commit != "" { if status.Commit != "" {
@ -309,26 +286,26 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
if len(commit) > 7 { if len(commit) > 7 {
commit = commit[:7] commit = commit[:7]
} }
fmt.Printf("%s %s\n", dimStyle.Render("Commit:"), commit) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.commit")), commit)
if status.CommitMessage != "" { if status.CommitMessage != "" {
// Truncate long messages // Truncate long messages
msg := status.CommitMessage msg := status.CommitMessage
if len(msg) > 60 { if len(msg) > 60 {
msg = msg[:57] + "..." msg = msg[:57] + "..."
} }
fmt.Printf("%s %s\n", dimStyle.Render("Message:"), msg) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.message")), msg)
} }
} }
if !status.StartedAt.IsZero() { if !status.StartedAt.IsZero() {
fmt.Printf("%s %s\n", dimStyle.Render("Started:"), status.StartedAt.Format(time.RFC3339)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.started")), status.StartedAt.Format(time.RFC3339))
} }
if !status.CompletedAt.IsZero() { if !status.CompletedAt.IsZero() {
fmt.Printf("%s %s\n", dimStyle.Render("Completed:"), status.CompletedAt.Format(time.RFC3339)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.completed")), status.CompletedAt.Format(time.RFC3339))
if !status.StartedAt.IsZero() { if !status.StartedAt.IsZero() {
duration := status.CompletedAt.Sub(status.StartedAt) duration := status.CompletedAt.Sub(status.StartedAt)
fmt.Printf("%s %s\n", dimStyle.Render("Duration:"), duration.Round(time.Second)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.duration")), duration.Round(time.Second))
} }
} }
} }
@ -390,24 +367,24 @@ func formatTimeAgo(t time.Time) string {
switch { switch {
case duration < time.Minute: case duration < time.Minute:
return "just now" return i18n.T("cli.time.just_now")
case duration < time.Hour: case duration < time.Hour:
mins := int(duration.Minutes()) mins := int(duration.Minutes())
if mins == 1 { if mins == 1 {
return "1 minute ago" return i18n.T("cli.time.minute_ago")
} }
return fmt.Sprintf("%d minutes ago", mins) return i18n.T("cli.time.minutes_ago", map[string]interface{}{"Count": mins})
case duration < 24*time.Hour: case duration < 24*time.Hour:
hours := int(duration.Hours()) hours := int(duration.Hours())
if hours == 1 { if hours == 1 {
return "1 hour ago" return i18n.T("cli.time.hour_ago")
} }
return fmt.Sprintf("%d hours ago", hours) return i18n.T("cli.time.hours_ago", map[string]interface{}{"Count": hours})
default: default:
days := int(duration.Hours() / 24) days := int(duration.Hours() / 24)
if days == 1 { if days == 1 {
return "1 day ago" return i18n.T("cli.time.day_ago")
} }
return fmt.Sprintf("%d days ago", days) return i18n.T("cli.time.days_ago", map[string]interface{}{"Count": days})
} }
} }

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,13 +29,8 @@ var (
func addPHPDevCommand(parent *cobra.Command) { func addPHPDevCommand(parent *cobra.Command) {
devCmd := &cobra.Command{ devCmd := &cobra.Command{
Use: "dev", Use: "dev",
Short: "Start Laravel development environment", Short: i18n.T("cmd.php.dev.short"),
Long: "Starts all detected Laravel services.\n\n" + Long: i18n.T("cmd.php.dev.long"),
"Auto-detects:\n" +
" - Vite (vite.config.js/ts)\n" +
" - Horizon (config/horizon.php)\n" +
" - Reverb (config/reverb.php)\n" +
" - Redis (from .env)",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPHPDev(phpDevOptions{ return runPHPDev(phpDevOptions{
NoVite: devNoVite, NoVite: devNoVite,
@ -48,13 +44,13 @@ func addPHPDevCommand(parent *cobra.Command) {
}, },
} }
devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, "Skip Vite dev server") devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, i18n.T("cmd.php.dev.flag.no_vite"))
devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, "Skip Laravel Horizon") devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, i18n.T("cmd.php.dev.flag.no_horizon"))
devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, "Skip Laravel Reverb") devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, i18n.T("cmd.php.dev.flag.no_reverb"))
devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, "Skip Redis server") devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, i18n.T("cmd.php.dev.flag.no_redis"))
devCmd.Flags().BoolVar(&devHTTPS, "https", false, "Enable HTTPS with mkcert") devCmd.Flags().BoolVar(&devHTTPS, "https", false, i18n.T("cmd.php.dev.flag.https"))
devCmd.Flags().StringVar(&devDomain, "domain", "", "Domain for SSL certificate (default: from APP_URL or localhost)") devCmd.Flags().StringVar(&devDomain, "domain", "", i18n.T("cmd.php.dev.flag.domain"))
devCmd.Flags().IntVar(&devPort, "port", 0, "FrankenPHP port (default: 8000)") devCmd.Flags().IntVar(&devPort, "port", 0, i18n.T("cmd.php.dev.flag.port"))
parent.AddCommand(devCmd) parent.AddCommand(devCmd)
} }
@ -77,7 +73,7 @@ func runPHPDev(opts phpDevOptions) error {
// Check if this is a Laravel project // Check if this is a Laravel project
if !phppkg.IsLaravelProject(cwd) { if !phppkg.IsLaravelProject(cwd) {
return fmt.Errorf("not a Laravel project (missing artisan or laravel/framework)") return fmt.Errorf(i18n.T("cmd.php.error.not_laravel"))
} }
// Get app name for display // Get app name for display
@ -86,11 +82,11 @@ func runPHPDev(opts phpDevOptions) error {
appName = "Laravel" appName = "Laravel"
} }
fmt.Printf("%s Starting %s development environment\n\n", dimStyle.Render("PHP:"), appName) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName}))
// Detect services // Detect services
services := phppkg.DetectServices(cwd) services := phppkg.DetectServices(cwd)
fmt.Printf("%s Detected services:\n", dimStyle.Render("Services:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services"))
for _, svc := range services { for _, svc := range services {
fmt.Printf(" %s %s\n", successStyle.Render("*"), svc) fmt.Printf(" %s %s\n", successStyle.Render("*"), svc)
} }
@ -125,16 +121,16 @@ func runPHPDev(opts phpDevOptions) error {
go func() { go func() {
<-sigCh <-sigCh
fmt.Printf("\n%s Shutting down...\n", dimStyle.Render("PHP:")) fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.shutting_down"))
cancel() cancel()
}() }()
if err := server.Start(ctx, devOpts); err != nil { if err := server.Start(ctx, devOpts); err != nil {
return fmt.Errorf("failed to start services: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.start_services"), err)
} }
// Print status // Print status
fmt.Printf("%s Services started:\n", successStyle.Render("Running:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.running")), i18n.T("cmd.php.dev.services_started"))
printServiceStatuses(server.Status()) printServiceStatuses(server.Status())
fmt.Println() fmt.Println()
@ -147,19 +143,19 @@ func runPHPDev(opts phpDevOptions) error {
appURL = fmt.Sprintf("http://localhost:%d", port) appURL = fmt.Sprintf("http://localhost:%d", port)
} }
} }
fmt.Printf("%s %s\n", dimStyle.Render("App URL:"), linkStyle.Render(appURL)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL))
// Check for Vite // Check for Vite
if !opts.NoVite && containsService(services, phppkg.ServiceVite) { if !opts.NoVite && containsService(services, phppkg.ServiceVite) {
fmt.Printf("%s %s\n", dimStyle.Render("Vite:"), linkStyle.Render("http://localhost:5173")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173"))
} }
fmt.Printf("\n%s\n\n", dimStyle.Render("Press Ctrl+C to stop all services")) fmt.Printf("\n%s\n\n", dimStyle.Render(i18n.T("cmd.php.dev.press_ctrl_c")))
// Stream unified logs // Stream unified logs
logsReader, err := server.Logs("", true) logsReader, err := server.Logs("", true)
if err != nil { if err != nil {
fmt.Printf("%s Failed to get logs: %v\n", errorStyle.Render("Warning:"), err) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.dev.logs_failed", map[string]interface{}{"Error": err}))
} else { } else {
defer logsReader.Close() defer logsReader.Close()
@ -178,10 +174,10 @@ func runPHPDev(opts phpDevOptions) error {
shutdown: shutdown:
// Stop services // Stop services
if err := server.Stop(); err != nil { if err := server.Stop(); err != nil {
fmt.Printf("%s Error stopping services: %v\n", errorStyle.Render("Error:"), err) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.dev.stop_error", map[string]interface{}{"Error": err}))
} }
fmt.Printf("%s All services stopped\n", successStyle.Render("Done:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.dev.all_stopped"))
return nil return nil
} }
@ -193,16 +189,15 @@ var (
func addPHPLogsCommand(parent *cobra.Command) { func addPHPLogsCommand(parent *cobra.Command) {
logsCmd := &cobra.Command{ logsCmd := &cobra.Command{
Use: "logs", Use: "logs",
Short: "View service logs", Short: i18n.T("cmd.php.logs.short"),
Long: "Stream logs from Laravel services.\n\n" + Long: i18n.T("cmd.php.logs.long"),
"Services: frankenphp, vite, horizon, reverb, redis",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPHPLogs(logsService, logsFollow) return runPHPLogs(logsService, logsFollow)
}, },
} }
logsCmd.Flags().BoolVar(&logsFollow, "follow", false, "Follow log output") logsCmd.Flags().BoolVar(&logsFollow, "follow", false, i18n.T("cmd.php.logs.flag.follow"))
logsCmd.Flags().StringVar(&logsService, "service", "", "Specific service (default: all)") logsCmd.Flags().StringVar(&logsService, "service", "", i18n.T("cmd.php.logs.flag.service"))
parent.AddCommand(logsCmd) parent.AddCommand(logsCmd)
} }
@ -214,7 +209,7 @@ func runPHPLogs(service string, follow bool) error {
} }
if !phppkg.IsLaravelProject(cwd) { if !phppkg.IsLaravelProject(cwd) {
return fmt.Errorf("not a Laravel project") return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short"))
} }
// Create a minimal server just to access logs // Create a minimal server just to access logs
@ -222,7 +217,7 @@ func runPHPLogs(service string, follow bool) error {
logsReader, err := server.Logs(service, follow) logsReader, err := server.Logs(service, follow)
if err != nil { if err != nil {
return fmt.Errorf("failed to get logs: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.get_logs"), err)
} }
defer logsReader.Close() defer logsReader.Close()
@ -254,7 +249,7 @@ func runPHPLogs(service string, follow bool) error {
func addPHPStopCommand(parent *cobra.Command) { func addPHPStopCommand(parent *cobra.Command) {
stopCmd := &cobra.Command{ stopCmd := &cobra.Command{
Use: "stop", Use: "stop",
Short: "Stop all Laravel services", Short: i18n.T("cmd.php.stop.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStop() return runPHPStop()
}, },
@ -269,23 +264,23 @@ func runPHPStop() error {
return err return err
} }
fmt.Printf("%s Stopping services...\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.stop.stopping"))
// We need to find running processes // We need to find running processes
// This is a simplified version - in practice you'd want to track PIDs // This is a simplified version - in practice you'd want to track PIDs
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd}) server := phppkg.NewDevServer(phppkg.Options{Dir: cwd})
if err := server.Stop(); err != nil { if err := server.Stop(); err != nil {
return fmt.Errorf("failed to stop services: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.stop_services"), err)
} }
fmt.Printf("%s All services stopped\n", successStyle.Render("Done:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.dev.all_stopped"))
return nil return nil
} }
func addPHPStatusCommand(parent *cobra.Command) { func addPHPStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{ statusCmd := &cobra.Command{
Use: "status", Use: "status",
Short: "Show service status", Short: i18n.T("cmd.php.status.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStatus() return runPHPStatus()
}, },
@ -301,7 +296,7 @@ func runPHPStatus() error {
} }
if !phppkg.IsLaravelProject(cwd) { if !phppkg.IsLaravelProject(cwd) {
return fmt.Errorf("not a Laravel project") return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short"))
} }
appName := phppkg.GetLaravelAppName(cwd) appName := phppkg.GetLaravelAppName(cwd)
@ -309,11 +304,11 @@ func runPHPStatus() error {
appName = "Laravel" appName = "Laravel"
} }
fmt.Printf("%s %s\n\n", dimStyle.Render("Project:"), appName) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.status.project")), appName)
// Detect available services // Detect available services
services := phppkg.DetectServices(cwd) services := phppkg.DetectServices(cwd)
fmt.Printf("%s\n", dimStyle.Render("Detected services:")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services")))
for _, svc := range services { for _, svc := range services {
style := getServiceStyle(string(svc)) style := getServiceStyle(string(svc))
fmt.Printf(" %s %s\n", style.Render("*"), svc) fmt.Printf(" %s %s\n", style.Render("*"), svc)
@ -322,11 +317,11 @@ func runPHPStatus() error {
// Package manager // Package manager
pm := phppkg.DetectPackageManager(cwd) pm := phppkg.DetectPackageManager(cwd)
fmt.Printf("%s %s\n", dimStyle.Render("Package manager:"), pm) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm)
// FrankenPHP status // FrankenPHP status
if phppkg.IsFrankenPHPProject(cwd) { if phppkg.IsFrankenPHPProject(cwd) {
fmt.Printf("%s %s\n", dimStyle.Render("Octane server:"), "FrankenPHP") fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP")
} }
// SSL status // SSL status
@ -334,9 +329,9 @@ func runPHPStatus() error {
if appURL != "" { if appURL != "" {
domain := phppkg.ExtractDomainFromURL(appURL) domain := phppkg.ExtractDomainFromURL(appURL)
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
fmt.Printf("%s %s\n", dimStyle.Render("SSL certificates:"), successStyle.Render("installed")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed")))
} else { } else {
fmt.Printf("%s %s\n", dimStyle.Render("SSL certificates:"), dimStyle.Render("not setup")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup")))
} }
} }
@ -348,13 +343,13 @@ var sslDomain string
func addPHPSSLCommand(parent *cobra.Command) { func addPHPSSLCommand(parent *cobra.Command) {
sslCmd := &cobra.Command{ sslCmd := &cobra.Command{
Use: "ssl", Use: "ssl",
Short: "Setup SSL certificates with mkcert", Short: i18n.T("cmd.php.ssl.short"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPHPSSL(sslDomain) return runPHPSSL(sslDomain)
}, },
} }
sslCmd.Flags().StringVar(&sslDomain, "domain", "", "Domain for certificate (default: from APP_URL)") sslCmd.Flags().StringVar(&sslDomain, "domain", "", i18n.T("cmd.php.ssl.flag.domain"))
parent.AddCommand(sslCmd) parent.AddCommand(sslCmd)
} }
@ -378,35 +373,35 @@ func runPHPSSL(domain string) error {
// Check if mkcert is installed // Check if mkcert is installed
if !phppkg.IsMkcertInstalled() { if !phppkg.IsMkcertInstalled() {
fmt.Printf("%s mkcert is not installed\n", errorStyle.Render("Error:")) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed"))
fmt.Println("\nInstall with:") fmt.Printf("\n%s\n", i18n.T("cmd.php.ssl.install_with"))
fmt.Println(" macOS: brew install mkcert") fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_macos"))
fmt.Println(" Linux: see https://github.com/FiloSottile/mkcert") fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux"))
return fmt.Errorf("mkcert not installed") return fmt.Errorf(i18n.T("cmd.php.error.mkcert_not_installed"))
} }
fmt.Printf("%s Setting up SSL for %s\n", dimStyle.Render("SSL:"), domain) fmt.Printf("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain}))
// Check if certs already exist // Check if certs already exist
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) { if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
fmt.Printf("%s Certificates already exist\n", dimStyle.Render("Skip:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.skip")), i18n.T("cmd.php.ssl.certs_exist"))
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile)
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
return nil return nil
} }
// Setup SSL // Setup SSL
if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil { if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil {
return fmt.Errorf("failed to setup SSL: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.ssl_setup"), err)
} }
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{}) certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
fmt.Printf("%s SSL certificates created\n", successStyle.Render("Done:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.ssl.certs_created"))
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile)
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
return nil return nil
} }
@ -419,17 +414,17 @@ func printServiceStatuses(statuses []phppkg.ServiceStatus) {
var statusText string var statusText string
if s.Error != nil { if s.Error != nil {
statusText = phpStatusError.Render(fmt.Sprintf("error: %v", s.Error)) statusText = phpStatusError.Render(i18n.T("cmd.php.status.error", map[string]interface{}{"Error": s.Error}))
} else if s.Running { } else if s.Running {
statusText = phpStatusRunning.Render("running") statusText = phpStatusRunning.Render(i18n.T("cmd.php.status.running"))
if s.Port > 0 { if s.Port > 0 {
statusText += dimStyle.Render(fmt.Sprintf(" (port %d)", s.Port)) statusText += dimStyle.Render(fmt.Sprintf(" (%s)", i18n.T("cmd.php.status.port", map[string]interface{}{"Port": s.Port})))
} }
if s.PID > 0 { if s.PID > 0 {
statusText += dimStyle.Render(fmt.Sprintf(" [pid %d]", s.PID)) statusText += dimStyle.Render(fmt.Sprintf(" [%s]", i18n.T("cmd.php.status.pid", map[string]interface{}{"PID": s.PID})))
} }
} else { } else {
statusText = phpStatusStopped.Render("stopped") statusText = phpStatusStopped.Render(i18n.T("cmd.php.status.stopped"))
} }
fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText) fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText)

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -11,15 +12,8 @@ import (
func addPHPPackagesCommands(parent *cobra.Command) { func addPHPPackagesCommands(parent *cobra.Command) {
packagesCmd := &cobra.Command{ packagesCmd := &cobra.Command{
Use: "packages", Use: "packages",
Short: "Manage local PHP packages", Short: i18n.T("cmd.php.packages.short"),
Long: "Link and manage local PHP packages for development.\n\n" + Long: i18n.T("cmd.php.packages.long"),
"Similar to npm link, this adds path repositories to composer.json\n" +
"for developing packages alongside your project.\n\n" +
"Commands:\n" +
" link - Link local packages by path\n" +
" unlink - Unlink packages by name\n" +
" update - Update linked packages\n" +
" list - List linked packages",
} }
parent.AddCommand(packagesCmd) parent.AddCommand(packagesCmd)
@ -32,27 +26,22 @@ func addPHPPackagesCommands(parent *cobra.Command) {
func addPHPPackagesLinkCommand(parent *cobra.Command) { func addPHPPackagesLinkCommand(parent *cobra.Command) {
linkCmd := &cobra.Command{ linkCmd := &cobra.Command{
Use: "link [paths...]", Use: "link [paths...]",
Short: "Link local packages", Short: i18n.T("cmd.php.packages.link.short"),
Long: "Link local PHP packages for development.\n\n" + Long: i18n.T("cmd.php.packages.link.long"),
"Adds path repositories to composer.json with symlink enabled.\n" + Args: cobra.MinimumNArgs(1),
"The package name is auto-detected from each path's composer.json.\n\n" +
"Examples:\n" +
" core php packages link ../my-package\n" +
" core php packages link ../pkg-a ../pkg-b",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
fmt.Printf("%s Linking packages...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking"))
if err := phppkg.LinkPackages(cwd, args); err != nil { if err := phppkg.LinkPackages(cwd, args); err != nil {
return fmt.Errorf("failed to link packages: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.link_packages"), err)
} }
fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.link.done"))
return nil return nil
}, },
} }
@ -63,26 +52,22 @@ func addPHPPackagesLinkCommand(parent *cobra.Command) {
func addPHPPackagesUnlinkCommand(parent *cobra.Command) { func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
unlinkCmd := &cobra.Command{ unlinkCmd := &cobra.Command{
Use: "unlink [packages...]", Use: "unlink [packages...]",
Short: "Unlink packages", Short: i18n.T("cmd.php.packages.unlink.short"),
Long: "Remove linked packages from composer.json.\n\n" + Long: i18n.T("cmd.php.packages.unlink.long"),
"Removes path repositories by package name.\n\n" + Args: cobra.MinimumNArgs(1),
"Examples:\n" +
" core php packages unlink vendor/my-package\n" +
" core php packages unlink vendor/pkg-a vendor/pkg-b",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
fmt.Printf("%s Unlinking packages...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking"))
if err := phppkg.UnlinkPackages(cwd, args); err != nil { if err := phppkg.UnlinkPackages(cwd, args); err != nil {
return fmt.Errorf("failed to unlink packages: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.unlink_packages"), err)
} }
fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.unlink.done"))
return nil return nil
}, },
} }
@ -93,25 +78,21 @@ func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
func addPHPPackagesUpdateCommand(parent *cobra.Command) { func addPHPPackagesUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{ updateCmd := &cobra.Command{
Use: "update [packages...]", Use: "update [packages...]",
Short: "Update linked packages", Short: i18n.T("cmd.php.packages.update.short"),
Long: "Run composer update for linked packages.\n\n" + Long: i18n.T("cmd.php.packages.update.long"),
"If no packages specified, updates all packages.\n\n" +
"Examples:\n" +
" core php packages update\n" +
" core php packages update vendor/my-package",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating"))
if err := phppkg.UpdatePackages(cwd, args); err != nil { if err := phppkg.UpdatePackages(cwd, args); err != nil {
return fmt.Errorf("composer update failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err)
} }
fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.update.done"))
return nil return nil
}, },
} }
@ -122,31 +103,30 @@ func addPHPPackagesUpdateCommand(parent *cobra.Command) {
func addPHPPackagesListCommand(parent *cobra.Command) { func addPHPPackagesListCommand(parent *cobra.Command) {
listCmd := &cobra.Command{ listCmd := &cobra.Command{
Use: "list", Use: "list",
Short: "List linked packages", Short: i18n.T("cmd.php.packages.list.short"),
Long: "List all locally linked packages.\n\n" + Long: i18n.T("cmd.php.packages.list.long"),
"Shows package name, path, and version for each linked package.",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
packages, err := phppkg.ListLinkedPackages(cwd) packages, err := phppkg.ListLinkedPackages(cwd)
if err != nil { if err != nil {
return fmt.Errorf("failed to list packages: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.list_packages"), err)
} }
if len(packages) == 0 { if len(packages) == 0 {
fmt.Printf("%s No linked packages found\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.none_found"))
return nil return nil
} }
fmt.Printf("%s Linked packages:\n\n", dimStyle.Render("PHP:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.linked"))
for _, pkg := range packages { for _, pkg := range packages {
name := pkg.Name name := pkg.Name
if name == "" { if name == "" {
name = "(unknown)" name = i18n.T("cmd.php.packages.list.unknown")
} }
version := pkg.Version version := pkg.Version
if version == "" { if version == "" {
@ -154,8 +134,8 @@ func addPHPPackagesListCommand(parent *cobra.Command) {
} }
fmt.Printf(" %s %s\n", successStyle.Render("*"), name) fmt.Printf(" %s %s\n", successStyle.Render("*"), name)
fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), pkg.Path) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.packages.list.path")), pkg.Path)
fmt.Printf(" %s %s\n", dimStyle.Render("Version:"), version) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.packages.list.version")), version)
fmt.Println() fmt.Println()
} }

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php" phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -24,27 +25,21 @@ var (
func addPHPTestCommand(parent *cobra.Command) { func addPHPTestCommand(parent *cobra.Command) {
testCmd := &cobra.Command{ testCmd := &cobra.Command{
Use: "test", Use: "test",
Short: "Run PHP tests (PHPUnit/Pest)", Short: i18n.T("cmd.php.test.short"),
Long: "Run PHP tests using PHPUnit or Pest.\n\n" + Long: i18n.T("cmd.php.test.long"),
"Auto-detects Pest if tests/Pest.php exists, otherwise uses PHPUnit.\n\n" +
"Examples:\n" +
" core php test # Run all tests\n" +
" core php test --parallel # Run tests in parallel\n" +
" core php test --coverage # Run with coverage\n" +
" core php test --filter UserTest # Filter by test name",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Detect test runner // Detect test runner
runner := phppkg.DetectTestRunner(cwd) runner := phppkg.DetectTestRunner(cwd)
fmt.Printf("%s Running tests with %s\n\n", dimStyle.Render("PHP:"), runner) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.test.running", map[string]interface{}{"Runner": runner}))
ctx := context.Background() ctx := context.Background()
@ -61,17 +56,17 @@ func addPHPTestCommand(parent *cobra.Command) {
} }
if err := phppkg.RunTests(ctx, opts); err != nil { if err := phppkg.RunTests(ctx, opts); err != nil {
return fmt.Errorf("tests failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.tests_failed"), err)
} }
return nil return nil
}, },
} }
testCmd.Flags().BoolVar(&testParallel, "parallel", false, "Run tests in parallel") testCmd.Flags().BoolVar(&testParallel, "parallel", false, i18n.T("cmd.php.test.flag.parallel"))
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Generate code coverage") testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.php.test.flag.coverage"))
testCmd.Flags().StringVar(&testFilter, "filter", "", "Filter tests by name pattern") testCmd.Flags().StringVar(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter"))
testCmd.Flags().StringVar(&testGroup, "group", "", "Run only tests in specified group") testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group"))
parent.AddCommand(testCmd) parent.AddCommand(testCmd)
} }
@ -84,33 +79,31 @@ var (
func addPHPFmtCommand(parent *cobra.Command) { func addPHPFmtCommand(parent *cobra.Command) {
fmtCmd := &cobra.Command{ fmtCmd := &cobra.Command{
Use: "fmt [paths...]", Use: "fmt [paths...]",
Short: "Format PHP code with Laravel Pint", Short: i18n.T("cmd.php.fmt.short"),
Long: "Format PHP code using Laravel Pint.\n\n" + Long: i18n.T("cmd.php.fmt.long"),
"Examples:\n" +
" core php fmt # Check formatting (dry-run)\n" +
" core php fmt --fix # Auto-fix formatting issues\n" +
" core php fmt --diff # Show diff of changes",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Detect formatter // Detect formatter
formatter, found := phppkg.DetectFormatter(cwd) formatter, found := phppkg.DetectFormatter(cwd)
if !found { if !found {
return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)") return fmt.Errorf(i18n.T("cmd.php.fmt.no_formatter"))
} }
action := "Checking" var msg string
if fmtFix { if fmtFix {
action = "Formatting" msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter})
} else {
msg = i18n.T("cmd.php.fmt.checking", map[string]interface{}{"Formatter": formatter})
} }
fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg)
ctx := context.Background() ctx := context.Background()
@ -128,23 +121,23 @@ func addPHPFmtCommand(parent *cobra.Command) {
if err := phppkg.Format(ctx, opts); err != nil { if err := phppkg.Format(ctx, opts); err != nil {
if fmtFix { if fmtFix {
return fmt.Errorf("formatting failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err)
} }
return fmt.Errorf("formatting issues found: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err)
} }
if fmtFix { if fmtFix {
fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.fmt.success"))
} else { } else {
fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.fmt.no_issues"))
} }
return nil return nil
}, },
} }
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Auto-fix formatting issues") fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix"))
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes") fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("cmd.php.fmt.flag.diff"))
parent.AddCommand(fmtCmd) parent.AddCommand(fmtCmd)
} }
@ -157,30 +150,25 @@ var (
func addPHPAnalyseCommand(parent *cobra.Command) { func addPHPAnalyseCommand(parent *cobra.Command) {
analyseCmd := &cobra.Command{ analyseCmd := &cobra.Command{
Use: "analyse [paths...]", Use: "analyse [paths...]",
Short: "Run PHPStan static analysis", Short: i18n.T("cmd.php.analyse.short"),
Long: "Run PHPStan or Larastan static analysis.\n\n" + Long: i18n.T("cmd.php.analyse.long"),
"Auto-detects Larastan if installed, otherwise uses PHPStan.\n\n" +
"Examples:\n" +
" core php analyse # Run analysis\n" +
" core php analyse --level 9 # Run at max strictness\n" +
" core php analyse --memory 2G # Increase memory limit",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Detect analyser // Detect analyser
analyser, found := phppkg.DetectAnalyser(cwd) analyser, found := phppkg.DetectAnalyser(cwd)
if !found { if !found {
return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)") return fmt.Errorf(i18n.T("cmd.php.analyse.no_analyser"))
} }
fmt.Printf("%s Running static analysis with %s\n\n", dimStyle.Render("PHP:"), analyser) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.analyse.running", map[string]interface{}{"Analyser": analyser}))
ctx := context.Background() ctx := context.Background()
@ -197,16 +185,16 @@ func addPHPAnalyseCommand(parent *cobra.Command) {
} }
if err := phppkg.Analyse(ctx, opts); err != nil { if err := phppkg.Analyse(ctx, opts); err != nil {
return fmt.Errorf("analysis found issues: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err)
} }
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.analyse.no_issues"))
return nil return nil
}, },
} }
analyseCmd.Flags().IntVar(&analyseLevel, "level", 0, "PHPStan analysis level (0-9)") analyseCmd.Flags().IntVar(&analyseLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level"))
analyseCmd.Flags().StringVar(&analyseMemory, "memory", "", "Memory limit (e.g., 2G)") analyseCmd.Flags().StringVar(&analyseMemory, "memory", "", i18n.T("cmd.php.analyse.flag.memory"))
parent.AddCommand(analyseCmd) parent.AddCommand(analyseCmd)
} }
@ -225,39 +213,34 @@ var (
func addPHPPsalmCommand(parent *cobra.Command) { func addPHPPsalmCommand(parent *cobra.Command) {
psalmCmd := &cobra.Command{ psalmCmd := &cobra.Command{
Use: "psalm", Use: "psalm",
Short: "Run Psalm static analysis", Short: i18n.T("cmd.php.psalm.short"),
Long: "Run Psalm deep static analysis with Laravel plugin support.\n\n" + Long: i18n.T("cmd.php.psalm.long"),
"Psalm provides deeper type inference than PHPStan and catches\n" +
"different classes of bugs. Both should be run for best coverage.\n\n" +
"Examples:\n" +
" core php psalm # Run analysis\n" +
" core php psalm --fix # Auto-fix issues where possible\n" +
" core php psalm --level 3 # Run at specific level (1-8)\n" +
" core php psalm --baseline # Generate baseline file",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Check if Psalm is available // Check if Psalm is available
_, found := phppkg.DetectPsalm(cwd) _, found := phppkg.DetectPsalm(cwd)
if !found { if !found {
fmt.Printf("%s Psalm not found\n\n", errorStyle.Render("Error:")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.psalm.not_found"))
fmt.Printf("%s composer require --dev vimeo/psalm\n", dimStyle.Render("Install:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.psalm.install"))
fmt.Printf("%s ./vendor/bin/psalm --init\n", dimStyle.Render("Setup:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup"))
return fmt.Errorf("psalm not installed") return fmt.Errorf(i18n.T("cmd.php.error.psalm_not_installed"))
} }
action := "Analysing" var msg string
if psalmFix { if psalmFix {
action = "Analysing and fixing" msg = i18n.T("cmd.php.psalm.analysing_fixing")
} else {
msg = i18n.T("cmd.php.psalm.analysing")
} }
fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg)
ctx := context.Background() ctx := context.Background()
@ -271,18 +254,18 @@ func addPHPPsalmCommand(parent *cobra.Command) {
} }
if err := phppkg.RunPsalm(ctx, opts); err != nil { if err := phppkg.RunPsalm(ctx, opts); err != nil {
return fmt.Errorf("psalm found issues: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err)
} }
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.psalm.no_issues"))
return nil return nil
}, },
} }
psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, "Error level (1=strictest, 8=most lenient)") psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, i18n.T("cmd.php.psalm.flag.level"))
psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, "Auto-fix issues where possible") psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, i18n.T("cmd.php.psalm.flag.fix"))
psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, "Generate/update baseline file") psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline"))
psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, "Show info-level issues") psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, i18n.T("cmd.php.psalm.flag.show_info"))
parent.AddCommand(psalmCmd) parent.AddCommand(psalmCmd)
} }
@ -295,24 +278,19 @@ var (
func addPHPAuditCommand(parent *cobra.Command) { func addPHPAuditCommand(parent *cobra.Command) {
auditCmd := &cobra.Command{ auditCmd := &cobra.Command{
Use: "audit", Use: "audit",
Short: "Security audit for dependencies", Short: i18n.T("cmd.php.audit.short"),
Long: "Check PHP and JavaScript dependencies for known vulnerabilities.\n\n" + Long: i18n.T("cmd.php.audit.long"),
"Runs composer audit and npm audit (if package.json exists).\n\n" +
"Examples:\n" +
" core php audit # Check all dependencies\n" +
" core php audit --json # Output as JSON\n" +
" core php audit --fix # Auto-fix where possible (npm only)",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
fmt.Printf("%s Scanning dependencies for vulnerabilities\n\n", dimStyle.Render("Audit:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning"))
ctx := context.Background() ctx := context.Background()
@ -323,7 +301,7 @@ func addPHPAuditCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
}) })
if err != nil { if err != nil {
return fmt.Errorf("audit failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.audit_failed"), err)
} }
// Print results // Print results
@ -332,15 +310,15 @@ func addPHPAuditCommand(parent *cobra.Command) {
for _, result := range results { for _, result := range results {
icon := successStyle.Render("✓") icon := successStyle.Render("✓")
status := successStyle.Render("secure") status := successStyle.Render(i18n.T("cmd.php.audit.secure"))
if result.Error != nil { if result.Error != nil {
icon = errorStyle.Render("✗") icon = errorStyle.Render("✗")
status = errorStyle.Render("error") status = errorStyle.Render(i18n.T("cmd.php.audit.error"))
hasErrors = true hasErrors = true
} else if result.Vulnerabilities > 0 { } else if result.Vulnerabilities > 0 {
icon = errorStyle.Render("✗") icon = errorStyle.Render("✗")
status = errorStyle.Render(fmt.Sprintf("%d vulnerabilities", result.Vulnerabilities)) status = errorStyle.Render(i18n.T("cmd.php.audit.vulnerabilities", map[string]interface{}{"Count": result.Vulnerabilities}))
totalVulns += result.Vulnerabilities totalVulns += result.Vulnerabilities
} }
@ -363,22 +341,22 @@ func addPHPAuditCommand(parent *cobra.Command) {
fmt.Println() fmt.Println()
if totalVulns > 0 { if totalVulns > 0 {
fmt.Printf("%s Found %d vulnerabilities across dependencies\n", errorStyle.Render("Warning:"), totalVulns) fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns}))
fmt.Printf("%s composer update && npm update\n", dimStyle.Render("Fix:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.fix")), i18n.T("cmd.php.audit.fix_hint"))
return fmt.Errorf("vulnerabilities found") return fmt.Errorf(i18n.T("cmd.php.error.vulns_found"))
} }
if hasErrors { if hasErrors {
return fmt.Errorf("audit completed with errors") return fmt.Errorf(i18n.T("cmd.php.audit.completed_errors"))
} }
fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.audit.all_secure"))
return nil return nil
}, },
} }
auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, "Output in JSON format") auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, i18n.T("cmd.php.audit.flag.json"))
auditCmd.Flags().BoolVar(&auditFix, "fix", false, "Auto-fix vulnerabilities (npm only)") auditCmd.Flags().BoolVar(&auditFix, "fix", false, i18n.T("cmd.php.audit.flag.fix"))
parent.AddCommand(auditCmd) parent.AddCommand(auditCmd)
} }
@ -393,25 +371,19 @@ var (
func addPHPSecurityCommand(parent *cobra.Command) { func addPHPSecurityCommand(parent *cobra.Command) {
securityCmd := &cobra.Command{ securityCmd := &cobra.Command{
Use: "security", Use: "security",
Short: "Security vulnerability scanning", Short: i18n.T("cmd.php.security.short"),
Long: "Scan for security vulnerabilities in configuration and code.\n\n" + Long: i18n.T("cmd.php.security.long"),
"Checks environment config, file permissions, code patterns,\n" +
"and runs security-focused static analysis.\n\n" +
"Examples:\n" +
" core php security # Run all checks\n" +
" core php security --severity=high # Only high+ severity\n" +
" core php security --json # JSON output",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
fmt.Printf("%s Running security checks\n\n", dimStyle.Render("Security:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.T("cmd.php.security.running"))
ctx := context.Background() ctx := context.Background()
@ -424,7 +396,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
Output: os.Stdout, Output: os.Stdout,
}) })
if err != nil { if err != nil {
return fmt.Errorf("security check failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.security_failed"), err)
} }
// Print results by category // Print results by category
@ -436,7 +408,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
fmt.Println() fmt.Println()
} }
currentCategory = category currentCategory = category
fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+" CHECKS:")) fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix")))
} }
icon := successStyle.Render("✓") icon := successStyle.Render("✓")
@ -448,7 +420,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
if !check.Passed && check.Message != "" { if !check.Passed && check.Message != "" {
fmt.Printf(" %s\n", dimStyle.Render(check.Message)) fmt.Printf(" %s\n", dimStyle.Render(check.Message))
if check.Fix != "" { if check.Fix != "" {
fmt.Printf(" %s %s\n", dimStyle.Render("Fix:"), check.Fix) fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.security.fix_label")), check.Fix)
} }
} }
} }
@ -456,34 +428,34 @@ func addPHPSecurityCommand(parent *cobra.Command) {
fmt.Println() fmt.Println()
// Print summary // Print summary
fmt.Printf("%s Security scan complete\n", dimStyle.Render("Summary:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.summary")), i18n.T("cmd.php.security.summary"))
fmt.Printf(" %s %d/%d\n", dimStyle.Render("Passed:"), result.Summary.Passed, result.Summary.Total) fmt.Printf(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total)
if result.Summary.Critical > 0 { if result.Summary.Critical > 0 {
fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render("Critical:"), result.Summary.Critical) fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical)
} }
if result.Summary.High > 0 { if result.Summary.High > 0 {
fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render("High:"), result.Summary.High) fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High)
} }
if result.Summary.Medium > 0 { if result.Summary.Medium > 0 {
fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render("Medium:"), result.Summary.Medium) fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium)
} }
if result.Summary.Low > 0 { if result.Summary.Low > 0 {
fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render("Low:"), result.Summary.Low) fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low)
} }
if result.Summary.Critical > 0 || result.Summary.High > 0 { if result.Summary.Critical > 0 || result.Summary.High > 0 {
return fmt.Errorf("critical or high severity issues found") return fmt.Errorf(i18n.T("cmd.php.error.critical_high_issues"))
} }
return nil return nil
}, },
} }
securityCmd.Flags().StringVar(&securitySeverity, "severity", "", "Minimum severity (critical, high, medium, low)") securityCmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.php.security.flag.severity"))
securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, "Output in JSON format") securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, i18n.T("cmd.php.security.flag.json"))
securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, "Output in SARIF format (for GitHub Security)") securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, i18n.T("cmd.php.security.flag.sarif"))
securityCmd.Flags().StringVar(&securityURL, "url", "", "URL to check HTTP headers (optional)") securityCmd.Flags().StringVar(&securityURL, "url", "", i18n.T("cmd.php.security.flag.url"))
parent.AddCommand(securityCmd) parent.AddCommand(securityCmd)
} }
@ -497,25 +469,16 @@ var (
func addPHPQACommand(parent *cobra.Command) { func addPHPQACommand(parent *cobra.Command) {
qaCmd := &cobra.Command{ qaCmd := &cobra.Command{
Use: "qa", Use: "qa",
Short: "Run full QA pipeline", Short: i18n.T("cmd.php.qa.short"),
Long: "Run the complete quality assurance pipeline.\n\n" + Long: i18n.T("cmd.php.qa.long"),
"Stages:\n" +
" quick: Security audit, code style, PHPStan\n" +
" standard: Psalm, tests\n" +
" full: Rector dry-run, mutation testing (slow)\n\n" +
"Examples:\n" +
" core php qa # Run quick + standard stages\n" +
" core php qa --quick # Only quick checks\n" +
" core php qa --full # All stages including slow ones\n" +
" core php qa --fix # Auto-fix where possible",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Determine stages // Determine stages
@ -532,18 +495,18 @@ func addPHPQACommand(parent *cobra.Command) {
for i, s := range stages { for i, s := range stages {
stageNames[i] = string(s) stageNames[i] = string(s)
} }
fmt.Printf("%s Running QA pipeline (%s)\n\n", dimStyle.Render("QA:"), strings.Join(stageNames, " → ")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.qa")), i18n.T("cmd.php.qa.running", map[string]interface{}{"Stages": strings.Join(stageNames, " -> ")}))
ctx := context.Background() ctx := context.Background()
var allPassed = true var allPassed = true
var results []phppkg.QACheckResult var results []phppkg.QACheckResult
for _, stage := range stages { for _, stage := range stages {
fmt.Printf("%s\n", phpQAStageStyle.Render("═══ "+strings.ToUpper(string(stage))+" STAGE ═══")) fmt.Printf("%s\n", phpQAStageStyle.Render(i18n.T("cmd.php.qa.stage_prefix")+strings.ToUpper(string(stage))+i18n.T("cmd.php.qa.stage_suffix")))
checks := phppkg.GetQAChecks(cwd, stage) checks := phppkg.GetQAChecks(cwd, stage)
if len(checks) == 0 { if len(checks) == 0 {
fmt.Printf(" %s\n\n", dimStyle.Render("No checks available")) fmt.Printf(" %s\n\n", dimStyle.Render(i18n.T("cmd.php.qa.no_checks")))
continue continue
} }
@ -553,10 +516,10 @@ func addPHPQACommand(parent *cobra.Command) {
results = append(results, result) results = append(results, result)
icon := phpQAPassedStyle.Render("✓") icon := phpQAPassedStyle.Render("✓")
status := phpQAPassedStyle.Render("passed") status := phpQAPassedStyle.Render(i18n.T("cmd.php.qa.passed"))
if !result.Passed { if !result.Passed {
icon = phpQAFailedStyle.Render("✗") icon = phpQAFailedStyle.Render("✗")
status = phpQAFailedStyle.Render("failed") status = phpQAFailedStyle.Render(i18n.T("cmd.php.qa.failed"))
allPassed = false allPassed = false
} }
@ -577,33 +540,33 @@ func addPHPQACommand(parent *cobra.Command) {
} }
if allPassed { if allPassed {
fmt.Printf("%s All checks passed (%d/%d)\n", phpQAPassedStyle.Render("QA PASSED:"), passedCount, len(results)) fmt.Printf("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("cmd.php.qa.all_passed", map[string]interface{}{"Passed": passedCount, "Total": len(results)}))
return nil return nil
} }
fmt.Printf("%s Some checks failed (%d/%d passed)\n\n", phpQAFailedStyle.Render("QA FAILED:"), passedCount, len(results)) fmt.Printf("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("cmd.php.qa.some_failed", map[string]interface{}{"Passed": passedCount, "Total": len(results)}))
// Show what needs fixing // Show what needs fixing
fmt.Printf("%s\n", dimStyle.Render("To fix:")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.qa.to_fix")))
for _, check := range failedChecks { for _, check := range failedChecks {
fixCmd := getQAFixCommand(check.Name, qaFix) fixCmd := getQAFixCommand(check.Name, qaFix)
issue := check.Output issue := check.Output
if issue == "" { if issue == "" {
issue = "issues found" issue = "issues found"
} }
fmt.Printf(" %s %s\n", phpQAFailedStyle.Render(""), check.Name+": "+issue) fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("*"), check.Name+": "+issue)
if fixCmd != "" { if fixCmd != "" {
fmt.Printf(" %s %s\n", dimStyle.Render(""), fixCmd) fmt.Printf(" %s %s\n", dimStyle.Render("->"), fixCmd)
} }
} }
return fmt.Errorf("QA pipeline failed") return fmt.Errorf(i18n.T("cmd.php.qa.pipeline_failed"))
}, },
} }
qaCmd.Flags().BoolVar(&qaQuick, "quick", false, "Only run quick checks") qaCmd.Flags().BoolVar(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick"))
qaCmd.Flags().BoolVar(&qaFull, "full", false, "Run all stages including slow checks") qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full"))
qaCmd.Flags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible") qaCmd.Flags().BoolVar(&qaFix, "fix", false, i18n.T("cmd.php.qa.flag.fix"))
parent.AddCommand(qaCmd) parent.AddCommand(qaCmd)
} }
@ -611,25 +574,25 @@ func addPHPQACommand(parent *cobra.Command) {
func getQAFixCommand(checkName string, fixEnabled bool) string { func getQAFixCommand(checkName string, fixEnabled bool) string {
switch checkName { switch checkName {
case "audit": case "audit":
return "composer update && npm update" return i18n.T("cmd.php.qa.fix_audit")
case "fmt": case "fmt":
if fixEnabled { if fixEnabled {
return "" return ""
} }
return "core php fmt --fix" return "core php fmt --fix"
case "analyse": case "analyse":
return "Fix PHPStan errors shown above" return i18n.T("cmd.php.qa.fix_phpstan")
case "psalm": case "psalm":
return "Fix Psalm errors shown above" return i18n.T("cmd.php.qa.fix_psalm")
case "test": case "test":
return "Fix failing tests shown above" return i18n.T("cmd.php.qa.fix_tests")
case "rector": case "rector":
if fixEnabled { if fixEnabled {
return "" return ""
} }
return "core php rector --fix" return "core php rector --fix"
case "infection": case "infection":
return "Improve test coverage for mutated code" return i18n.T("cmd.php.qa.fix_infection")
} }
return "" return ""
} }
@ -662,42 +625,42 @@ func runQACheck(ctx context.Context, dir string, checkName string, fix bool) php
err := phppkg.Format(ctx, phppkg.FormatOptions{Dir: dir, Fix: fix, Output: io.Discard}) err := phppkg.Format(ctx, phppkg.FormatOptions{Dir: dir, Fix: fix, Output: io.Discard})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Code style issues found" result.Output = i18n.T("cmd.php.qa.issue_style")
} }
case "analyse": case "analyse":
err := phppkg.Analyse(ctx, phppkg.AnalyseOptions{Dir: dir, Output: &buf}) err := phppkg.Analyse(ctx, phppkg.AnalyseOptions{Dir: dir, Output: &buf})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Static analysis errors" result.Output = i18n.T("cmd.php.qa.issue_analysis")
} }
case "psalm": case "psalm":
err := phppkg.RunPsalm(ctx, phppkg.PsalmOptions{Dir: dir, Fix: fix, Output: io.Discard}) err := phppkg.RunPsalm(ctx, phppkg.PsalmOptions{Dir: dir, Fix: fix, Output: io.Discard})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Type errors found" result.Output = i18n.T("cmd.php.qa.issue_types")
} }
case "test": case "test":
err := phppkg.RunTests(ctx, phppkg.TestOptions{Dir: dir, Output: io.Discard}) err := phppkg.RunTests(ctx, phppkg.TestOptions{Dir: dir, Output: io.Discard})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Test failures" result.Output = i18n.T("cmd.php.qa.issue_tests")
} }
case "rector": case "rector":
err := phppkg.RunRector(ctx, phppkg.RectorOptions{Dir: dir, Fix: fix, Output: io.Discard}) err := phppkg.RunRector(ctx, phppkg.RectorOptions{Dir: dir, Fix: fix, Output: io.Discard})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Code improvements available" result.Output = i18n.T("cmd.php.qa.issue_rector")
} }
case "infection": case "infection":
err := phppkg.RunInfection(ctx, phppkg.InfectionOptions{Dir: dir, Output: io.Discard}) err := phppkg.RunInfection(ctx, phppkg.InfectionOptions{Dir: dir, Output: io.Discard})
result.Passed = err == nil result.Passed = err == nil
if err != nil { if err != nil {
result.Output = "Mutation score below threshold" result.Output = i18n.T("cmd.php.qa.issue_mutation")
} }
} }
@ -714,37 +677,33 @@ var (
func addPHPRectorCommand(parent *cobra.Command) { func addPHPRectorCommand(parent *cobra.Command) {
rectorCmd := &cobra.Command{ rectorCmd := &cobra.Command{
Use: "rector", Use: "rector",
Short: "Automated code refactoring", Short: i18n.T("cmd.php.rector.short"),
Long: "Run Rector for automated code improvements and PHP upgrades.\n\n" + Long: i18n.T("cmd.php.rector.long"),
"Rector can automatically upgrade PHP syntax, improve code quality,\n" +
"and apply framework-specific refactorings.\n\n" +
"Examples:\n" +
" core php rector # Dry-run (show changes)\n" +
" core php rector --fix # Apply changes\n" +
" core php rector --diff # Show detailed diff",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Check if Rector is available // Check if Rector is available
if !phppkg.DetectRector(cwd) { if !phppkg.DetectRector(cwd) {
fmt.Printf("%s Rector not found\n\n", errorStyle.Render("Error:")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.rector.not_found"))
fmt.Printf("%s composer require --dev rector/rector\n", dimStyle.Render("Install:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.rector.install"))
fmt.Printf("%s ./vendor/bin/rector init\n", dimStyle.Render("Setup:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup"))
return fmt.Errorf("rector not installed") return fmt.Errorf(i18n.T("cmd.php.error.rector_not_installed"))
} }
action := "Analysing" var msg string
if rectorFix { if rectorFix {
action = "Refactoring" msg = i18n.T("cmd.php.rector.refactoring")
} else {
msg = i18n.T("cmd.php.rector.analysing")
} }
fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg)
ctx := context.Background() ctx := context.Background()
@ -758,25 +717,25 @@ func addPHPRectorCommand(parent *cobra.Command) {
if err := phppkg.RunRector(ctx, opts); err != nil { if err := phppkg.RunRector(ctx, opts); err != nil {
if rectorFix { if rectorFix {
return fmt.Errorf("rector failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err)
} }
// Dry-run returns non-zero if changes would be made // Dry-run returns non-zero if changes would be made
fmt.Printf("\n%s Changes suggested (use --fix to apply)\n", phpQAWarningStyle.Render("Info:")) fmt.Printf("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested"))
return nil return nil
} }
if rectorFix { if rectorFix {
fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.rector.refactored"))
} else { } else {
fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.rector.no_changes"))
} }
return nil return nil
}, },
} }
rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, "Apply changes (default is dry-run)") rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, i18n.T("cmd.php.rector.flag.fix"))
rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, "Show detailed diff of changes") rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, i18n.T("cmd.php.rector.flag.diff"))
rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, "Clear Rector cache before running") rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, i18n.T("cmd.php.rector.flag.clear_cache"))
parent.AddCommand(rectorCmd) parent.AddCommand(rectorCmd)
} }
@ -792,34 +751,27 @@ var (
func addPHPInfectionCommand(parent *cobra.Command) { func addPHPInfectionCommand(parent *cobra.Command) {
infectionCmd := &cobra.Command{ infectionCmd := &cobra.Command{
Use: "infection", Use: "infection",
Short: "Mutation testing for test quality", Short: i18n.T("cmd.php.infection.short"),
Long: "Run Infection mutation testing to measure test suite quality.\n\n" + Long: i18n.T("cmd.php.infection.long"),
"Mutation testing modifies your code and checks if tests catch\n" +
"the changes. High mutation score = high quality tests.\n\n" +
"Warning: This can be slow on large codebases.\n\n" +
"Examples:\n" +
" core php infection # Run mutation testing\n" +
" core php infection --min-msi=70 # Require 70% mutation score\n" +
" core php infection --filter=User # Only test User* files",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
} }
if !phppkg.IsPHPProject(cwd) { if !phppkg.IsPHPProject(cwd) {
return fmt.Errorf("not a PHP project (missing composer.json)") return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
} }
// Check if Infection is available // Check if Infection is available
if !phppkg.DetectInfection(cwd) { if !phppkg.DetectInfection(cwd) {
fmt.Printf("%s Infection not found\n\n", errorStyle.Render("Error:")) fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.infection.not_found"))
fmt.Printf("%s composer require --dev infection/infection\n", dimStyle.Render("Install:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.infection.install"))
return fmt.Errorf("infection not installed") return fmt.Errorf(i18n.T("cmd.php.error.infection_not_installed"))
} }
fmt.Printf("%s Running mutation testing\n", dimStyle.Render("Infection:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.T("cmd.php.infection.running"))
fmt.Printf("%s This may take a while...\n\n", dimStyle.Render("Note:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note"))
ctx := context.Background() ctx := context.Background()
@ -834,19 +786,19 @@ func addPHPInfectionCommand(parent *cobra.Command) {
} }
if err := phppkg.RunInfection(ctx, opts); err != nil { if err := phppkg.RunInfection(ctx, opts); err != nil {
return fmt.Errorf("mutation testing failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err)
} }
fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:")) fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.infection.complete"))
return nil return nil
}, },
} }
infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, "Minimum mutation score indicator (0-100, default: 50)") infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, i18n.T("cmd.php.infection.flag.min_msi"))
infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, "Minimum covered mutation score (0-100, default: 70)") infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, i18n.T("cmd.php.infection.flag.min_covered_msi"))
infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, "Number of parallel threads (default: 4)") infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, i18n.T("cmd.php.infection.flag.threads"))
infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", "Filter files by pattern") infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", i18n.T("cmd.php.infection.flag.filter"))
infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, "Only mutate covered code") infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, i18n.T("cmd.php.infection.flag.only_covered"))
parent.AddCommand(infectionCmd) parent.AddCommand(infectionCmd)
} }

View file

@ -3,6 +3,7 @@ package pkg
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -20,14 +21,8 @@ var (
func AddPkgCommands(root *cobra.Command) { func AddPkgCommands(root *cobra.Command) {
pkgCmd := &cobra.Command{ pkgCmd := &cobra.Command{
Use: "pkg", Use: "pkg",
Short: "Package management for core-* repos", Short: i18n.T("cmd.pkg.short"),
Long: "Manage host-uk/core-* packages and repositories.\n\n" + Long: i18n.T("cmd.pkg.long"),
"Commands:\n" +
" search Search GitHub for packages\n" +
" install Clone a package from GitHub\n" +
" list List installed packages\n" +
" update Update installed packages\n" +
" outdated Check for outdated packages",
} }
root.AddCommand(pkgCmd) root.AddCommand(pkgCmd)

View file

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -20,22 +21,18 @@ var (
func addPkgInstallCommand(parent *cobra.Command) { func addPkgInstallCommand(parent *cobra.Command) {
installCmd := &cobra.Command{ installCmd := &cobra.Command{
Use: "install <org/repo>", Use: "install <org/repo>",
Short: "Clone a package from GitHub", Short: i18n.T("cmd.pkg.install.short"),
Long: "Clones a repository from GitHub.\n\n" + Long: i18n.T("cmd.pkg.install.long"),
"Examples:\n" +
" core pkg install host-uk/core-php\n" +
" core pkg install host-uk/core-tenant --dir ./packages\n" +
" core pkg install host-uk/core-admin --add",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)") return fmt.Errorf(i18n.T("cmd.pkg.error.repo_required"))
} }
return runPkgInstall(args[0], installTargetDir, installAddToReg) return runPkgInstall(args[0], installTargetDir, installAddToReg)
}, },
} }
installCmd.Flags().StringVar(&installTargetDir, "dir", "", "Target directory (default: ./packages or current dir)") installCmd.Flags().StringVar(&installTargetDir, "dir", "", i18n.T("cmd.pkg.install.flag.dir"))
installCmd.Flags().BoolVar(&installAddToReg, "add", false, "Add to repos.yaml registry") installCmd.Flags().BoolVar(&installAddToReg, "add", false, i18n.T("cmd.pkg.install.flag.add"))
parent.AddCommand(installCmd) parent.AddCommand(installCmd)
} }
@ -46,7 +43,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
// Parse org/repo // Parse org/repo
parts := strings.Split(repoArg, "/") parts := strings.Split(repoArg, "/")
if len(parts) != 2 { if len(parts) != 2 {
return fmt.Errorf("invalid repo format: use org/repo (e.g., host-uk/core-php)") return fmt.Errorf(i18n.T("cmd.pkg.error.invalid_repo_format"))
} }
org, repoName := parts[0], parts[1] org, repoName := parts[0], parts[1]
@ -76,19 +73,19 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
repoPath := filepath.Join(targetDir, repoName) repoPath := filepath.Join(targetDir, repoName)
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil { if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
fmt.Printf("%s %s already exists at %s\n", dimStyle.Render("Skip:"), repoName, repoPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.install.skip_label")), i18n.T("cmd.pkg.install.already_exists", map[string]string{"Name": repoName, "Path": repoPath}))
return nil return nil
} }
if err := os.MkdirAll(targetDir, 0755); err != nil { if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err) return fmt.Errorf(i18n.T("cmd.pkg.error.create_directory"), err)
} }
fmt.Printf("%s %s/%s\n", dimStyle.Render("Installing:"), org, repoName) fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
fmt.Printf("%s %s\n", dimStyle.Render("Target:"), repoPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.install.target_label")), repoPath)
fmt.Println() fmt.Println()
fmt.Printf(" %s... ", dimStyle.Render("Cloning")) fmt.Printf(" %s... ", dimStyle.Render(i18n.T("cmd.pkg.install.cloning")))
err := gitClone(ctx, org, repoName, repoPath) err := gitClone(ctx, org, repoName, repoPath)
if err != nil { if err != nil {
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error())) fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
@ -98,14 +95,14 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
if addToRegistry { if addToRegistry {
if err := addToRegistryFile(org, repoName); err != nil { if err := addToRegistryFile(org, repoName); err != nil {
fmt.Printf(" %s add to registry: %s\n", errorStyle.Render("✗"), err) fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), i18n.T("cmd.pkg.install.add_to_registry"), err)
} else { } else {
fmt.Printf(" %s added to repos.yaml\n", successStyle.Render("✓")) fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.pkg.install.added_to_registry"))
} }
} }
fmt.Println() fmt.Println()
fmt.Printf("%s Installed %s\n", successStyle.Render("Done:"), repoName) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.pkg.install.done_label")), i18n.T("cmd.pkg.install.installed", map[string]string{"Name": repoName}))
return nil return nil
} }
@ -113,7 +110,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
func addToRegistryFile(org, repoName string) error { func addToRegistryFile(org, repoName string) error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf("no repos.yaml found") return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)

View file

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,11 +16,8 @@ import (
func addPkgListCommand(parent *cobra.Command) { func addPkgListCommand(parent *cobra.Command) {
listCmd := &cobra.Command{ listCmd := &cobra.Command{
Use: "list", Use: "list",
Short: "List installed packages", Short: i18n.T("cmd.pkg.list.short"),
Long: "Lists all packages in the current workspace.\n\n" + Long: i18n.T("cmd.pkg.list.long"),
"Reads from repos.yaml or scans for git repositories.\n\n" +
"Examples:\n" +
" core pkg list",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPkgList() return runPkgList()
}, },
@ -31,12 +29,12 @@ func addPkgListCommand(parent *cobra.Command) {
func runPkgList() error { func runPkgList() error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf("no repos.yaml found - run from workspace directory") return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml_workspace"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
} }
basePath := reg.BasePath basePath := reg.BasePath
@ -49,11 +47,11 @@ func runPkgList() error {
allRepos := reg.List() allRepos := reg.List()
if len(allRepos) == 0 { if len(allRepos) == 0 {
fmt.Println("No packages in registry.") fmt.Println(i18n.T("cmd.pkg.list.no_packages"))
return nil return nil
} }
fmt.Printf("%s\n\n", repoNameStyle.Render("Installed Packages")) fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title")))
var installed, missing int var installed, missing int
for _, r := range allRepos { for _, r := range allRepos {
@ -76,7 +74,7 @@ func runPkgList() error {
desc = desc[:37] + "..." desc = desc[:37] + "..."
} }
if desc == "" { if desc == "" {
desc = dimStyle.Render("(no description)") desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
} }
fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name)) fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name))
@ -84,10 +82,10 @@ func runPkgList() error {
} }
fmt.Println() fmt.Println()
fmt.Printf("%s %d installed, %d missing\n", dimStyle.Render("Total:"), installed, missing) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.list.total_label")), i18n.T("cmd.pkg.list.summary", map[string]int{"Installed": installed, "Missing": missing}))
if missing > 0 { if missing > 0 {
fmt.Printf("\nInstall missing: %s\n", dimStyle.Render("core setup")) fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.list.install_missing"), dimStyle.Render("core setup"))
} }
return nil return nil
@ -99,20 +97,17 @@ var updateAll bool
func addPkgUpdateCommand(parent *cobra.Command) { func addPkgUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{ updateCmd := &cobra.Command{
Use: "update [packages...]", Use: "update [packages...]",
Short: "Update installed packages", Short: i18n.T("cmd.pkg.update.short"),
Long: "Pulls latest changes for installed packages.\n\n" + Long: i18n.T("cmd.pkg.update.long"),
"Examples:\n" +
" core pkg update core-php # Update specific package\n" +
" core pkg update --all # Update all packages",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if !updateAll && len(args) == 0 { if !updateAll && len(args) == 0 {
return fmt.Errorf("specify package name or use --all") return fmt.Errorf(i18n.T("cmd.pkg.error.specify_package"))
} }
return runPkgUpdate(args, updateAll) return runPkgUpdate(args, updateAll)
}, },
} }
updateCmd.Flags().BoolVar(&updateAll, "all", false, "Update all packages") updateCmd.Flags().BoolVar(&updateAll, "all", false, i18n.T("cmd.pkg.update.flag.all"))
parent.AddCommand(updateCmd) parent.AddCommand(updateCmd)
} }
@ -120,12 +115,12 @@ func addPkgUpdateCommand(parent *cobra.Command) {
func runPkgUpdate(packages []string, all bool) error { func runPkgUpdate(packages []string, all bool) error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf("no repos.yaml found") return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
} }
basePath := reg.BasePath basePath := reg.BasePath
@ -145,14 +140,14 @@ func runPkgUpdate(packages []string, all bool) error {
toUpdate = packages toUpdate = packages
} }
fmt.Printf("%s Updating %d package(s)\n\n", dimStyle.Render("Update:"), len(toUpdate)) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)}))
var updated, skipped, failed int var updated, skipped, failed int
for _, name := range toUpdate { for _, name := range toUpdate {
repoPath := filepath.Join(basePath, name) repoPath := filepath.Join(basePath, name)
if _, err := os.Stat(filepath.Join(repoPath, ".git")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(repoPath, ".git")); os.IsNotExist(err) {
fmt.Printf(" %s %s (not installed)\n", dimStyle.Render("○"), name) fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
skipped++ skipped++
continue continue
} }
@ -169,7 +164,7 @@ func runPkgUpdate(packages []string, all bool) error {
} }
if strings.Contains(string(output), "Already up to date") { if strings.Contains(string(output), "Already up to date") {
fmt.Printf("%s\n", dimStyle.Render("up to date")) fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.pkg.update.up_to_date")))
} else { } else {
fmt.Printf("%s\n", successStyle.Render("✓")) fmt.Printf("%s\n", successStyle.Render("✓"))
} }
@ -177,8 +172,8 @@ func runPkgUpdate(packages []string, all bool) error {
} }
fmt.Println() fmt.Println()
fmt.Printf("%s %d updated, %d skipped, %d failed\n", fmt.Printf("%s %s\n",
dimStyle.Render("Done:"), updated, skipped, failed) dimStyle.Render(i18n.T("cmd.pkg.update.done_label")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated, "Skipped": skipped, "Failed": failed}))
return nil return nil
} }
@ -187,10 +182,8 @@ func runPkgUpdate(packages []string, all bool) error {
func addPkgOutdatedCommand(parent *cobra.Command) { func addPkgOutdatedCommand(parent *cobra.Command) {
outdatedCmd := &cobra.Command{ outdatedCmd := &cobra.Command{
Use: "outdated", Use: "outdated",
Short: "Check for outdated packages", Short: i18n.T("cmd.pkg.outdated.short"),
Long: "Checks which packages have unpulled commits.\n\n" + Long: i18n.T("cmd.pkg.outdated.long"),
"Examples:\n" +
" core pkg outdated",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPkgOutdated() return runPkgOutdated()
}, },
@ -202,12 +195,12 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
func runPkgOutdated() error { func runPkgOutdated() error {
regPath, err := repos.FindRegistry() regPath, err := repos.FindRegistry()
if err != nil { if err != nil {
return fmt.Errorf("no repos.yaml found") return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
} }
reg, err := repos.LoadRegistry(regPath) reg, err := repos.LoadRegistry(regPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
} }
basePath := reg.BasePath basePath := reg.BasePath
@ -218,7 +211,7 @@ func runPkgOutdated() error {
basePath = filepath.Join(filepath.Dir(regPath), basePath) basePath = filepath.Join(filepath.Dir(regPath), basePath)
} }
fmt.Printf("%s Checking for updates...\n\n", dimStyle.Render("Outdated:")) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("cmd.pkg.outdated.checking"))
var outdated, upToDate, notInstalled int var outdated, upToDate, notInstalled int
@ -242,8 +235,8 @@ func runPkgOutdated() error {
count := strings.TrimSpace(string(output)) count := strings.TrimSpace(string(output))
if count != "0" { if count != "0" {
fmt.Printf(" %s %s (%s commits behind)\n", fmt.Printf(" %s %s (%s)\n",
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), count) errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
outdated++ outdated++
} else { } else {
upToDate++ upToDate++
@ -252,11 +245,11 @@ func runPkgOutdated() error {
fmt.Println() fmt.Println()
if outdated == 0 { if outdated == 0 {
fmt.Printf("%s All packages up to date\n", successStyle.Render("Done:")) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.pkg.outdated.done_label")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
} else { } else {
fmt.Printf("%s %d outdated, %d up to date\n", fmt.Printf("%s %s\n",
dimStyle.Render("Summary:"), outdated, upToDate) dimStyle.Render(i18n.T("cmd.pkg.outdated.summary_label")), i18n.T("cmd.pkg.outdated.summary", map[string]int{"Outdated": outdated, "UpToDate": upToDate}))
fmt.Printf("\nUpdate with: %s\n", dimStyle.Render("core pkg update --all")) fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.outdated.update_with"), dimStyle.Render("core pkg update --all"))
} }
return nil return nil

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/cache" "github.com/host-uk/core/pkg/cache"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,14 +28,8 @@ var (
func addPkgSearchCommand(parent *cobra.Command) { func addPkgSearchCommand(parent *cobra.Command) {
searchCmd := &cobra.Command{ searchCmd := &cobra.Command{
Use: "search", Use: "search",
Short: "Search GitHub for packages", Short: i18n.T("cmd.pkg.search.short"),
Long: "Searches GitHub for repositories matching a pattern.\n" + Long: i18n.T("cmd.pkg.search.long"),
"Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" +
"Examples:\n" +
" core pkg search # List all host-uk repos\n" +
" core pkg search --pattern 'core-*' # Search for core-* repos\n" +
" core pkg search --org mycompany # Search different org\n" +
" core pkg search --refresh # Bypass cache",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
org := searchOrg org := searchOrg
pattern := searchPattern pattern := searchPattern
@ -52,11 +47,11 @@ func addPkgSearchCommand(parent *cobra.Command) {
}, },
} }
searchCmd.Flags().StringVar(&searchOrg, "org", "", "GitHub organization (default: host-uk)") searchCmd.Flags().StringVar(&searchOrg, "org", "", i18n.T("cmd.pkg.search.flag.org"))
searchCmd.Flags().StringVar(&searchPattern, "pattern", "", "Repo name pattern (* for wildcard)") searchCmd.Flags().StringVar(&searchPattern, "pattern", "", i18n.T("cmd.pkg.search.flag.pattern"))
searchCmd.Flags().StringVar(&searchType, "type", "", "Filter by type in name (mod, services, plug, website)") searchCmd.Flags().StringVar(&searchType, "type", "", i18n.T("cmd.pkg.search.flag.type"))
searchCmd.Flags().IntVar(&searchLimit, "limit", 0, "Max results (default 50)") searchCmd.Flags().IntVar(&searchLimit, "limit", 0, i18n.T("cmd.pkg.search.flag.limit"))
searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, "Bypass cache and fetch fresh data") searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, i18n.T("cmd.pkg.search.flag.refresh"))
parent.AddCommand(searchCmd) parent.AddCommand(searchCmd)
} }
@ -91,22 +86,22 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
if found, err := c.Get(cacheKey, &ghRepos); found && err == nil { if found, err := c.Get(cacheKey, &ghRepos); found && err == nil {
fromCache = true fromCache = true
age := c.Age(cacheKey) age := c.Age(cacheKey)
fmt.Printf("%s %s %s\n", dimStyle.Render("Cache:"), org, dimStyle.Render(fmt.Sprintf("(%s ago)", age.Round(time.Second)))) fmt.Printf("%s %s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.cache_label")), org, dimStyle.Render(fmt.Sprintf("(%s ago)", age.Round(time.Second))))
} }
} }
// Fetch from GitHub if not cached // Fetch from GitHub if not cached
if !fromCache { if !fromCache {
if !ghAuthenticated() { if !ghAuthenticated() {
return fmt.Errorf("gh CLI not authenticated. Run: gh auth login") return fmt.Errorf(i18n.T("cmd.pkg.error.gh_not_authenticated"))
} }
if os.Getenv("GH_TOKEN") != "" { if os.Getenv("GH_TOKEN") != "" {
fmt.Printf("%s GH_TOKEN env var is set - this may cause auth issues\n", dimStyle.Render("Note:")) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.note_label")), i18n.T("cmd.pkg.search.gh_token_warning"))
fmt.Printf("%s Unset it with: unset GH_TOKEN\n\n", dimStyle.Render("")) fmt.Printf("%s %s\n\n", dimStyle.Render(""), i18n.T("cmd.pkg.search.gh_token_unset"))
} }
fmt.Printf("%s %s... ", dimStyle.Render("Fetching:"), org) fmt.Printf("%s %s... ", dimStyle.Render(i18n.T("cmd.pkg.search.fetching_label")), org)
cmd := exec.Command("gh", "repo", "list", org, cmd := exec.Command("gh", "repo", "list", org,
"--json", "name,description,visibility,updatedAt,primaryLanguage", "--json", "name,description,visibility,updatedAt,primaryLanguage",
@ -117,13 +112,13 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
fmt.Println() fmt.Println()
errStr := strings.TrimSpace(string(output)) errStr := strings.TrimSpace(string(output))
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") { if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
return fmt.Errorf("authentication failed - try: unset GH_TOKEN && gh auth login") return fmt.Errorf(i18n.T("cmd.pkg.error.auth_failed"))
} }
return fmt.Errorf("search failed: %s", errStr) return fmt.Errorf(i18n.T("cmd.pkg.error.search_failed"), errStr)
} }
if err := json.Unmarshal(output, &ghRepos); err != nil { if err := json.Unmarshal(output, &ghRepos); err != nil {
return fmt.Errorf("failed to parse results: %w", err) return fmt.Errorf(i18n.T("cmd.pkg.error.parse_results"), err)
} }
if c != nil { if c != nil {
@ -146,7 +141,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
} }
if len(filtered) == 0 { if len(filtered) == 0 {
fmt.Println("No repositories found matching pattern.") fmt.Println(i18n.T("cmd.pkg.search.no_repos_found"))
return nil return nil
} }
@ -154,12 +149,12 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
return filtered[i].Name < filtered[j].Name return filtered[i].Name < filtered[j].Name
}) })
fmt.Printf("Found %d repositories:\n\n", len(filtered)) fmt.Printf(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n")
for _, r := range filtered { for _, r := range filtered {
visibility := "" visibility := ""
if r.Visibility == "private" { if r.Visibility == "private" {
visibility = dimStyle.Render(" [private]") visibility = dimStyle.Render(" " + i18n.T("cmd.pkg.search.private_label"))
} }
desc := r.Description desc := r.Description
@ -167,7 +162,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
desc = desc[:47] + "..." desc = desc[:47] + "..."
} }
if desc == "" { if desc == "" {
desc = dimStyle.Render("(no description)") desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
} }
fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility) fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility)
@ -175,7 +170,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
} }
fmt.Println() fmt.Println()
fmt.Printf("Install with: %s\n", dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org))) fmt.Printf("%s %s\n", i18n.T("cmd.pkg.search.install_with"), dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org)))
return nil return nil
} }

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
sdkpkg "github.com/host-uk/core/pkg/sdk" sdkpkg "github.com/host-uk/core/pkg/sdk"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -20,13 +21,8 @@ var (
var sdkCmd = &cobra.Command{ var sdkCmd = &cobra.Command{
Use: "sdk", Use: "sdk",
Short: "SDK validation and API compatibility tools", Short: i18n.T("cmd.sdk.short"),
Long: `Tools for validating OpenAPI specs and checking API compatibility. Long: i18n.T("cmd.sdk.long"),
To generate SDKs, use: core build sdk
Commands:
diff Check for breaking API changes
validate Validate OpenAPI spec syntax`,
} }
var diffBasePath string var diffBasePath string
@ -34,7 +30,8 @@ var diffSpecPath string
var sdkDiffCmd = &cobra.Command{ var sdkDiffCmd = &cobra.Command{
Use: "diff", Use: "diff",
Short: "Check for breaking API changes", Short: i18n.T("cmd.sdk.diff.short"),
Long: i18n.T("cmd.sdk.diff.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runSDKDiff(diffBasePath, diffSpecPath) return runSDKDiff(diffBasePath, diffSpecPath)
}, },
@ -44,7 +41,8 @@ var validateSpecPath string
var sdkValidateCmd = &cobra.Command{ var sdkValidateCmd = &cobra.Command{
Use: "validate", Use: "validate",
Short: "Validate OpenAPI spec", Short: i18n.T("cmd.sdk.validate.short"),
Long: i18n.T("cmd.sdk.validate.long"),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runSDKValidate(validateSpecPath) return runSDKValidate(validateSpecPath)
}, },
@ -52,11 +50,11 @@ var sdkValidateCmd = &cobra.Command{
func init() { func init() {
// sdk diff flags // sdk diff flags
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", "Base spec (version tag or file)") sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base"))
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", "Current spec file") sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec"))
// sdk validate flags // sdk validate flags
sdkValidateCmd.Flags().StringVar(&validateSpecPath, "spec", "", "Path to OpenAPI spec file") sdkValidateCmd.Flags().StringVar(&validateSpecPath, "spec", "", i18n.T("cmd.sdk.validate.flag.spec"))
// Add subcommands // Add subcommands
sdkCmd.AddCommand(sdkDiffCmd) sdkCmd.AddCommand(sdkDiffCmd)
@ -66,7 +64,7 @@ func init() {
func runSDKDiff(basePath, specPath string) error { func runSDKDiff(basePath, specPath string) error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.sdk.error.working_dir"), err)
} }
// Detect current spec if not provided // Detect current spec if not provided
@ -79,49 +77,49 @@ func runSDKDiff(basePath, specPath string) error {
} }
if basePath == "" { if basePath == "" {
return fmt.Errorf("--base is required (version tag or file path)") return fmt.Errorf(i18n.T("cmd.sdk.diff.error.base_required"))
} }
fmt.Printf("%s Checking for breaking changes\n", sdkHeaderStyle.Render("SDK Diff:")) fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("cmd.sdk.diff.checking"))
fmt.Printf(" Base: %s\n", sdkDimStyle.Render(basePath)) fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.base_label"), sdkDimStyle.Render(basePath))
fmt.Printf(" Current: %s\n", sdkDimStyle.Render(specPath)) fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.current_label"), sdkDimStyle.Render(specPath))
fmt.Println() fmt.Println()
result, err := sdkpkg.Diff(basePath, specPath) result, err := sdkpkg.Diff(basePath, specPath)
if err != nil { if err != nil {
fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.label.error")), err)
os.Exit(2) os.Exit(2)
} }
if result.Breaking { if result.Breaking {
fmt.Printf("%s %s\n", sdkErrorStyle.Render("Breaking:"), result.Summary) fmt.Printf("%s %s\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.diff.breaking")), result.Summary)
for _, change := range result.Changes { for _, change := range result.Changes {
fmt.Printf(" - %s\n", change) fmt.Printf(" - %s\n", change)
} }
os.Exit(1) os.Exit(1)
} }
fmt.Printf("%s %s\n", sdkSuccessStyle.Render("OK:"), result.Summary) fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), result.Summary)
return nil return nil
} }
func runSDKValidate(specPath string) error { func runSDKValidate(specPath string) error {
projectDir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.sdk.error.working_dir"), err)
} }
s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath}) s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath})
fmt.Printf("%s Validating OpenAPI spec\n", sdkHeaderStyle.Render("SDK:")) fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.label.sdk")), i18n.T("cmd.sdk.validate.validating"))
detectedPath, err := s.DetectSpec() detectedPath, err := s.DetectSpec()
if err != nil { if err != nil {
fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err) fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.label.error")), err)
return err return err
} }
fmt.Printf(" Spec: %s\n", sdkDimStyle.Render(detectedPath)) fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.validate.spec_label"), sdkDimStyle.Render(detectedPath))
fmt.Printf("%s Spec is valid\n", sdkSuccessStyle.Render("OK:")) fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), i18n.T("cmd.sdk.validate.valid"))
return nil return nil
} }

View file

@ -3,6 +3,7 @@ package setup
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -33,31 +34,20 @@ var (
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Bootstrap workspace or clone packages from registry", Short: i18n.T("cmd.setup.short"),
Long: `Sets up a development workspace. Long: i18n.T("cmd.setup.long"),
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.
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runSetupOrchestrator(registryPath, only, dryRun, all, name, build) return runSetupOrchestrator(registryPath, only, dryRun, all, name, build)
}, },
} }
func init() { func init() {
setupCmd.Flags().StringVar(&registryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)") setupCmd.Flags().StringVar(&registryPath, "registry", "", i18n.T("cmd.setup.flag.registry"))
setupCmd.Flags().StringVar(&only, "only", "", "Only clone repos of these types (comma-separated: foundation,module,product)") setupCmd.Flags().StringVar(&only, "only", "", i18n.T("cmd.setup.flag.only"))
setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be cloned without cloning") setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, i18n.T("cmd.setup.flag.dry_run"))
setupCmd.Flags().BoolVar(&all, "all", false, "Skip wizard, clone all packages (non-interactive)") setupCmd.Flags().BoolVar(&all, "all", false, i18n.T("cmd.setup.flag.all"))
setupCmd.Flags().StringVar(&name, "name", "", "Project directory name for bootstrap mode") setupCmd.Flags().StringVar(&name, "name", "", i18n.T("cmd.setup.flag.name"))
setupCmd.Flags().BoolVar(&build, "build", false, "Run build after cloning") setupCmd.Flags().BoolVar(&build, "build", false, i18n.T("cmd.setup.flag.build"))
} }
// AddSetupCommand adds the 'setup' command to the given parent command. // AddSetupCommand adds the 'setup' command to the given parent command.

View file

@ -12,6 +12,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
) )
@ -45,7 +46,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
return fmt.Errorf("failed to get working directory: %w", err) return fmt.Errorf("failed to get working directory: %w", err)
} }
fmt.Printf("%s Bootstrap mode (no repos.yaml found)\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.bootstrap_mode"))
var targetDir string var targetDir string
@ -58,7 +59,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
if empty { if empty {
// Clone into current directory // Clone into current directory
targetDir = cwd targetDir = cwd
fmt.Printf("%s Cloning into current directory\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.cloning_current_dir"))
} else { } else {
// Directory has content - check if it's a git repo root // Directory has content - check if it's a git repo root
isRepo := isGitRepoRoot(cwd) isRepo := isGitRepoRoot(cwd)
@ -90,7 +91,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
} }
targetDir = filepath.Join(cwd, projectName) targetDir = filepath.Join(cwd, projectName)
fmt.Printf("%s Creating project directory: %s\n", dimStyle.Render(">>"), projectName) fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.creating_project_dir"), projectName)
if !dryRun { if !dryRun {
if err := os.MkdirAll(targetDir, 0755); err != nil { if err := os.MkdirAll(targetDir, 0755); err != nil {
@ -102,25 +103,25 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
// Clone core-devops first // Clone core-devops first
devopsPath := filepath.Join(targetDir, devopsRepo) devopsPath := filepath.Join(targetDir, devopsRepo)
if _, err := os.Stat(filepath.Join(devopsPath, ".git")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(devopsPath, ".git")); os.IsNotExist(err) {
fmt.Printf("%s Cloning %s...\n", dimStyle.Render(">>"), devopsRepo) fmt.Printf("%s %s %s...\n", dimStyle.Render(">>"), i18n.T("cmd.setup.cloning"), devopsRepo)
if !dryRun { if !dryRun {
if err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil { if err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil {
return fmt.Errorf("failed to clone %s: %w", devopsRepo, err) return fmt.Errorf("failed to clone %s: %w", devopsRepo, err)
} }
fmt.Printf("%s %s cloned\n", successStyle.Render(">>"), devopsRepo) fmt.Printf("%s %s %s\n", successStyle.Render(">>"), devopsRepo, i18n.T("cmd.setup.cloned"))
} else { } else {
fmt.Printf(" Would clone %s/%s to %s\n", defaultOrg, devopsRepo, devopsPath) fmt.Printf(" %s %s/%s to %s\n", i18n.T("cmd.setup.would_clone"), defaultOrg, devopsRepo, devopsPath)
} }
} else { } else {
fmt.Printf("%s %s already exists\n", dimStyle.Render(">>"), devopsRepo) fmt.Printf("%s %s %s\n", dimStyle.Render(">>"), devopsRepo, i18n.T("cmd.setup.already_exists"))
} }
// Load the repos.yaml from core-devops // Load the repos.yaml from core-devops
registryPath := filepath.Join(devopsPath, devopsReposYaml) registryPath := filepath.Join(devopsPath, devopsReposYaml)
if dryRun { if dryRun {
fmt.Printf("\n%s Would load registry from %s and present package wizard\n", dimStyle.Render(">>"), registryPath) fmt.Printf("\n%s %s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.would_load_registry"), registryPath)
return nil return nil
} }

View file

@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
) )
@ -29,8 +30,8 @@ func runRegistrySetup(ctx context.Context, registryPath, only string, dryRun, al
// runRegistrySetupWithReg runs setup with an already-loaded registry. // runRegistrySetupWithReg runs setup with an already-loaded registry.
func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryPath, only string, dryRun, all, runBuild bool) error { func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryPath, only string, dryRun, all, runBuild bool) error {
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.registry_label")), registryPath)
fmt.Printf("%s %s\n", dimStyle.Render("Org:"), reg.Org) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.org_label")), reg.Org)
// Determine base path for cloning // Determine base path for cloning
basePath := reg.BasePath basePath := reg.BasePath
@ -47,7 +48,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
basePath = filepath.Join(home, basePath[2:]) basePath = filepath.Join(home, basePath[2:])
} }
fmt.Printf("%s %s\n", dimStyle.Render("Target:"), basePath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.target_label")), basePath)
// Parse type filter // Parse type filter
var typeFilter []string var typeFilter []string
@ -55,7 +56,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
for _, t := range strings.Split(only, ",") { for _, t := range strings.Split(only, ",") {
typeFilter = append(typeFilter, strings.TrimSpace(t)) typeFilter = append(typeFilter, strings.TrimSpace(t))
} }
fmt.Printf("%s %s\n", dimStyle.Render("Filter:"), only) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.filter_label")), only)
} }
// Ensure base path exists // Ensure base path exists
@ -136,15 +137,18 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
// Summary // Summary
fmt.Println() fmt.Println()
fmt.Printf("%d to clone, %d exist, %d skipped\n", len(toClone), exists, skipped) fmt.Printf("%s, %s, %s\n",
i18n.T("cmd.setup.to_clone", map[string]interface{}{"Count": len(toClone)}),
i18n.T("cmd.setup.exist", map[string]interface{}{"Count": exists}),
i18n.T("cmd.setup.skipped", map[string]interface{}{"Count": skipped}))
if len(toClone) == 0 { if len(toClone) == 0 {
fmt.Println("\nNothing to clone.") fmt.Printf("\n%s\n", i18n.T("cmd.setup.nothing_to_clone"))
return nil return nil
} }
if dryRun { if dryRun {
fmt.Println("\nWould clone:") fmt.Printf("\n%s\n", i18n.T("cmd.setup.would_clone_list"))
for _, repo := range toClone { for _, repo := range toClone {
fmt.Printf(" %s (%s)\n", repoNameStyle.Render(repo.Name), repo.Type) fmt.Printf(" %s (%s)\n", repoNameStyle.Render(repo.Name), repo.Type)
} }
@ -158,7 +162,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
return err return err
} }
if !confirmed { if !confirmed {
fmt.Println("Cancelled.") fmt.Println(i18n.T("cmd.setup.cancelled"))
return nil return nil
} }
} }
@ -168,7 +172,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
var succeeded, failed int var succeeded, failed int
for _, repo := range toClone { for _, repo := range toClone {
fmt.Printf(" %s %s... ", dimStyle.Render("Cloning"), repo.Name) fmt.Printf(" %s %s... ", dimStyle.Render(i18n.T("cmd.setup.cloning")), repo.Name)
repoPath := filepath.Join(basePath, repo.Name) repoPath := filepath.Join(basePath, repo.Name)
@ -177,32 +181,32 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
fmt.Printf("%s\n", errorStyle.Render("x "+err.Error())) fmt.Printf("%s\n", errorStyle.Render("x "+err.Error()))
failed++ failed++
} else { } else {
fmt.Printf("%s\n", successStyle.Render("done")) fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.setup.done")))
succeeded++ succeeded++
} }
} }
// Summary // Summary
fmt.Println() fmt.Println()
fmt.Printf("%s %d cloned", successStyle.Render("Done:"), succeeded) fmt.Printf("%s %s", successStyle.Render(i18n.T("cmd.setup.done_label")), i18n.T("cmd.setup.cloned_count", map[string]interface{}{"Count": succeeded}))
if failed > 0 { if failed > 0 {
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed))) fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.setup.failed_count", map[string]interface{}{"Count": failed})))
} }
if exists > 0 { if exists > 0 {
fmt.Printf(", %d already exist", exists) fmt.Printf(", %s", i18n.T("cmd.setup.already_exist_count", map[string]interface{}{"Count": exists}))
} }
fmt.Println() fmt.Println()
// Run build if requested // Run build if requested
if runBuild && succeeded > 0 { if runBuild && succeeded > 0 {
fmt.Println() fmt.Println()
fmt.Printf("%s Running build...\n", dimStyle.Render(">>")) fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.running_build"))
buildCmd := exec.Command("core", "build") buildCmd := exec.Command("core", "build")
buildCmd.Dir = basePath buildCmd.Dir = basePath
buildCmd.Stdout = os.Stdout buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil { if err := buildCmd.Run(); err != nil {
return fmt.Errorf("build failed: %w", err) return fmt.Errorf("%s: %w", i18n.T("cmd.setup.error.build_failed"), err)
} }
} }

View file

@ -12,15 +12,17 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
) )
// runRepoSetup sets up the current repository with .core/ configuration. // runRepoSetup sets up the current repository with .core/ configuration.
func runRepoSetup(repoPath string, dryRun bool) error { func runRepoSetup(repoPath string, dryRun bool) error {
fmt.Printf("%s Setting up repository: %s\n", dimStyle.Render(">>"), repoPath) fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.setting_up"), repoPath)
// Detect project type // Detect project type
projectType := detectProjectType(repoPath) projectType := detectProjectType(repoPath)
fmt.Printf("%s Detected project type: %s\n", dimStyle.Render(">>"), projectType) fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.detected_type"), projectType)
// Create .core directory // Create .core directory
coreDir := filepath.Join(repoPath, ".core") coreDir := filepath.Join(repoPath, ".core")
@ -39,7 +41,7 @@ func runRepoSetup(repoPath string, dryRun bool) error {
} }
if dryRun { if dryRun {
fmt.Printf("\n%s Would create:\n", dimStyle.Render(">>")) fmt.Printf("\n%s %s:\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.would_create"))
for filename, content := range configs { for filename, content := range configs {
fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename)) fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename))
// Indent content for display // Indent content for display
@ -55,7 +57,7 @@ func runRepoSetup(repoPath string, dryRun bool) error {
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", filename, err) return fmt.Errorf("failed to write %s: %w", filename, err)
} }
fmt.Printf("%s Created %s\n", successStyle.Render(">>"), configPath) fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("cmd.setup.repo.created"), configPath)
} }
return nil return nil

View file

@ -13,6 +13,7 @@ import (
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"golang.org/x/term" "golang.org/x/term"
) )
@ -35,11 +36,11 @@ func promptSetupChoice() (string, error) {
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewSelect[string](). huh.NewSelect[string]().
Title("This directory is a git repository"). Title(i18n.T("cmd.setup.wizard.git_repo_title")).
Description("What would you like to do?"). Description(i18n.T("cmd.setup.wizard.what_to_do")).
Options( Options(
huh.NewOption("Setup Working Directory", "setup").Selected(true), huh.NewOption(i18n.T("cmd.setup.wizard.option_setup"), "setup").Selected(true),
huh.NewOption("Create Package (clone repos into subdirectory)", "package"), huh.NewOption(i18n.T("cmd.setup.wizard.option_package"), "package"),
). ).
Value(&choice), Value(&choice),
), ),
@ -59,8 +60,8 @@ func promptProjectName(defaultName string) (string, error) {
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewInput(). huh.NewInput().
Title("Project directory name"). Title(i18n.T("cmd.setup.wizard.project_name_title")).
Description("Enter the name for your new workspace directory"). Description(i18n.T("cmd.setup.wizard.project_name_desc")).
Placeholder(defaultName). Placeholder(defaultName).
Value(&name), Value(&name),
), ),
@ -158,14 +159,14 @@ func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string,
// Header styling // Header styling
headerStyle := shared.TitleStyle.MarginBottom(1) headerStyle := shared.TitleStyle.MarginBottom(1)
fmt.Println(headerStyle.Render("Package Selection")) fmt.Println(headerStyle.Render(i18n.T("cmd.setup.wizard.package_selection")))
fmt.Println("Use space to select/deselect, enter to confirm") fmt.Println(i18n.T("cmd.setup.wizard.selection_hint"))
fmt.Println() fmt.Println()
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewMultiSelect[string](). huh.NewMultiSelect[string]().
Title("Select packages to clone"). Title(i18n.T("cmd.setup.wizard.select_packages")).
Options(options...). Options(options...).
Value(&selected). Value(&selected).
Filterable(true). Filterable(true).
@ -195,9 +196,9 @@ func confirmClone(count int, target string) (bool, error) {
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewConfirm(). huh.NewConfirm().
Title(fmt.Sprintf("Clone %d packages to %s?", count, target)). Title(i18n.T("cmd.setup.wizard.confirm_clone", map[string]interface{}{"Count": count, "Target": target})).
Affirmative("Yes, clone"). Affirmative(i18n.T("cmd.setup.wizard.confirm_yes")).
Negative("Cancel"). Negative(i18n.T("cmd.setup.wizard.confirm_cancel")).
Value(&confirmed), Value(&confirmed),
), ),
).WithTheme(wizardTheme()) ).WithTheme(wizardTheme())

View file

@ -5,6 +5,7 @@ package testcmd
import ( import (
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -33,31 +34,19 @@ var (
var testCmd = &cobra.Command{ var testCmd = &cobra.Command{
Use: "test", Use: "test",
Short: "Run tests with coverage", Short: i18n.T("cmd.test.short"),
Long: `Runs Go tests with coverage reporting. Long: i18n.T("cmd.test.long"),
Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.
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 { RunE: func(cmd *cobra.Command, args []string) error {
return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON) return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON)
}, },
} }
func init() { func init() {
testCmd.Flags().BoolVar(&testVerbose, "verbose", false, "Show test output as it runs (-v)") testCmd.Flags().BoolVar(&testVerbose, "verbose", false, i18n.T("cmd.test.flag.verbose"))
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage") testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.test.flag.coverage"))
testCmd.Flags().BoolVar(&testShort, "short", false, "Skip long-running tests (-short)") testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.test.flag.short"))
testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package pattern to test (default: ./...)") testCmd.Flags().StringVar(&testPkg, "pkg", "", i18n.T("cmd.test.flag.pkg"))
testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching this regex (-run)") testCmd.Flags().StringVar(&testRun, "run", "", i18n.T("cmd.test.flag.run"))
testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector (-race)") testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.test.flag.race"))
testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON for CI/agents") testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.test.flag.json"))
} }

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
) )
type packageCoverage struct { type packageCoverage struct {
@ -84,19 +85,19 @@ func printTestSummary(results testResults, showCoverage bool) {
// Print pass/fail summary // Print pass/fail summary
total := results.passed + results.failed total := results.passed + results.failed
if total > 0 { if total > 0 {
fmt.Printf(" %s %d passed", testPassStyle.Render("✓"), results.passed) fmt.Printf(" %s %s", testPassStyle.Render("✓"), i18n.T("cmd.test.passed", map[string]interface{}{"Count": results.passed}))
if results.failed > 0 { if results.failed > 0 {
fmt.Printf(" %s %d failed", testFailStyle.Render("✗"), results.failed) fmt.Printf(" %s %s", testFailStyle.Render("✗"), i18n.T("cmd.test.failed", map[string]interface{}{"Count": results.failed}))
} }
if results.skipped > 0 { if results.skipped > 0 {
fmt.Printf(" %s %d skipped", testSkipStyle.Render("○"), results.skipped) fmt.Printf(" %s %s", testSkipStyle.Render("○"), i18n.T("cmd.test.skipped", map[string]interface{}{"Count": results.skipped}))
} }
fmt.Println() fmt.Println()
} }
// Print failed packages // Print failed packages
if len(results.failedPkgs) > 0 { if len(results.failedPkgs) > 0 {
fmt.Printf("\n Failed packages:\n") fmt.Printf("\n %s\n", i18n.T("cmd.test.failed_packages"))
for _, pkg := range results.failedPkgs { for _, pkg := range results.failedPkgs {
fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg) fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg)
} }
@ -107,7 +108,7 @@ func printTestSummary(results testResults, showCoverage bool) {
printCoverageSummary(results) printCoverageSummary(results)
} else if results.covCount > 0 { } else if results.covCount > 0 {
avgCov := results.totalCov / float64(results.covCount) avgCov := results.totalCov / float64(results.covCount)
fmt.Printf("\n Coverage: %s\n", formatCoverage(avgCov)) fmt.Printf("\n %s %s\n", i18n.T("cmd.test.label.coverage"), formatCoverage(avgCov))
} }
} }
@ -116,7 +117,7 @@ func printCoverageSummary(results testResults) {
return return
} }
fmt.Printf("\n %s\n", testHeaderStyle.Render("Coverage by package:")) fmt.Printf("\n %s\n", testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package")))
// Sort packages by name // Sort packages by name
sort.Slice(results.packages, func(i, j int) bool { sort.Slice(results.packages, func(i, j int) bool {
@ -145,8 +146,9 @@ func printCoverageSummary(results testResults) {
// Print average // Print average
if results.covCount > 0 { if results.covCount > 0 {
avgCov := results.totalCov / float64(results.covCount) avgCov := results.totalCov / float64(results.covCount)
padding := strings.Repeat(" ", maxLen-7+2) avgLabel := i18n.T("cmd.test.label.average")
fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render("Average"), padding, formatCoverage(avgCov)) padding := strings.Repeat(" ", maxLen-len(avgLabel)+2)
fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov))
} }
} }

View file

@ -8,12 +8,14 @@ import (
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"github.com/host-uk/core/pkg/i18n"
) )
func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error { func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error {
// Detect if we're in a Go project // Detect if we're in a Go project
if _, err := os.Stat("go.mod"); os.IsNotExist(err) { if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
return fmt.Errorf("no go.mod found - run from a Go project directory") return fmt.Errorf(i18n.T("cmd.test.error.no_go_mod"))
} }
// Build command arguments // Build command arguments
@ -52,10 +54,10 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
cmd.Env = append(os.Environ(), getMacOSDeploymentTarget()) cmd.Env = append(os.Environ(), getMacOSDeploymentTarget())
if !jsonOutput { if !jsonOutput {
fmt.Printf("%s Running tests\n", testHeaderStyle.Render("Test:")) fmt.Printf("%s %s\n", testHeaderStyle.Render(i18n.T("cmd.test.label.test")), i18n.T("cmd.test.running"))
fmt.Printf(" Package: %s\n", testDimStyle.Render(pkg)) fmt.Printf(" %s %s\n", i18n.T("cmd.test.label.package"), testDimStyle.Render(pkg))
if run != "" { if run != "" {
fmt.Printf(" Filter: %s\n", testDimStyle.Render(run)) fmt.Printf(" %s %s\n", i18n.T("cmd.test.label.filter"), testDimStyle.Render(run))
} }
fmt.Println() fmt.Println()
} }
@ -91,7 +93,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
// JSON output for CI/agents // JSON output for CI/agents
printJSONResults(results, exitCode) printJSONResults(results, exitCode)
if exitCode != 0 { if exitCode != 0 {
return fmt.Errorf("tests failed") return fmt.Errorf(i18n.T("cmd.test.error.tests_failed"))
} }
return nil return nil
} }
@ -106,11 +108,11 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
} }
if exitCode != 0 { if exitCode != 0 {
fmt.Printf("\n%s Tests failed\n", testFailStyle.Render("FAIL")) fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed"))
return fmt.Errorf("tests failed") return fmt.Errorf(i18n.T("cmd.test.error.tests_failed"))
} }
fmt.Printf("\n%s All tests passed\n", testPassStyle.Render("PASS")) fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("cmd.test.all_passed"))
return nil return nil
} }

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/host-uk/core/pkg/container" "github.com/host-uk/core/pkg/container"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,17 +28,8 @@ var (
func addVMRunCommand(parent *cobra.Command) { func addVMRunCommand(parent *cobra.Command) {
runCmd := &cobra.Command{ runCmd := &cobra.Command{
Use: "run [image]", Use: "run [image]",
Short: "Run a LinuxKit image or template", Short: i18n.T("cmd.vm.run.short"),
Long: "Runs a LinuxKit image as a VM using the available hypervisor.\n\n" + Long: i18n.T("cmd.vm.run.long"),
"Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" +
"You can also run from a template using --template, which will build and run\n" +
"the image automatically. Use --var to set template variables.\n\n" +
"Examples:\n" +
" core vm run image.iso\n" +
" core vm run -d image.qcow2\n" +
" core vm run --name myvm --memory 2048 --cpus 4 image.iso\n" +
" core vm run --template core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" +
" core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts := container.RunOptions{ opts := container.RunOptions{
Name: runName, Name: runName,
@ -55,7 +47,7 @@ func addVMRunCommand(parent *cobra.Command) {
// Otherwise, require an image path // Otherwise, require an image path
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("image path is required (or use --template)") return fmt.Errorf(i18n.T("cmd.vm.run.error.image_required"))
} }
image := args[0] image := args[0]
@ -63,13 +55,13 @@ func addVMRunCommand(parent *cobra.Command) {
}, },
} }
runCmd.Flags().StringVar(&runName, "name", "", "Name for the container") runCmd.Flags().StringVar(&runName, "name", "", i18n.T("cmd.vm.run.flag.name"))
runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, "Run in detached mode (background)") runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, i18n.T("cmd.vm.run.flag.detach"))
runCmd.Flags().IntVar(&runMemory, "memory", 0, "Memory in MB (default: 1024)") runCmd.Flags().IntVar(&runMemory, "memory", 0, i18n.T("cmd.vm.run.flag.memory"))
runCmd.Flags().IntVar(&runCPUs, "cpus", 0, "Number of CPUs (default: 1)") runCmd.Flags().IntVar(&runCPUs, "cpus", 0, i18n.T("cmd.vm.run.flag.cpus"))
runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, "SSH port for exec commands (default: 2222)") runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, i18n.T("cmd.vm.run.flag.ssh_port"))
runCmd.Flags().StringVar(&runTemplateName, "template", "", "Run from a LinuxKit template (build + run)") runCmd.Flags().StringVar(&runTemplateName, "template", "", i18n.T("cmd.vm.run.flag.template"))
runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, "Template variable in KEY=VALUE format (can be repeated)") runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, i18n.T("cmd.vm.run.flag.var"))
parent.AddCommand(runCmd) parent.AddCommand(runCmd)
} }
@ -77,7 +69,7 @@ func addVMRunCommand(parent *cobra.Command) {
func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error { func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error {
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
opts := container.RunOptions{ opts := container.RunOptions{
@ -88,27 +80,27 @@ func runContainer(image, name string, detach bool, memory, cpus, sshPort int) er
SSHPort: sshPort, SSHPort: sshPort,
} }
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), image) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.image")), image)
if name != "" { if name != "" {
fmt.Printf("%s %s\n", dimStyle.Render("Name:"), name) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.name")), name)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Hypervisor:"), manager.Hypervisor().Name()) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
fmt.Println() fmt.Println()
ctx := context.Background() ctx := context.Background()
c, err := manager.Run(ctx, image, opts) c, err := manager.Run(ctx, image, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to run container: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.run_container")+": %w", err)
} }
if detach { if detach {
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.vm.label.started")), c.ID)
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID) fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
fmt.Println() fmt.Println()
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8]) fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]interface{}{"ID": c.ID[:8]}))
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8]) fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]interface{}{"ID": c.ID[:8]}))
} else { } else {
fmt.Printf("\n%s %s\n", dimStyle.Render("Container stopped:"), c.ID) fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
} }
return nil return nil
@ -120,17 +112,14 @@ var psAll bool
func addVMPsCommand(parent *cobra.Command) { func addVMPsCommand(parent *cobra.Command) {
psCmd := &cobra.Command{ psCmd := &cobra.Command{
Use: "ps", Use: "ps",
Short: "List running VMs", Short: i18n.T("cmd.vm.ps.short"),
Long: "Lists all VMs. By default, only shows running VMs.\n\n" + Long: i18n.T("cmd.vm.ps.long"),
"Examples:\n" +
" core vm ps\n" +
" core vm ps -a",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return listContainers(psAll) return listContainers(psAll)
}, },
} }
psCmd.Flags().BoolVarP(&psAll, "all", "a", false, "Show all containers (including stopped)") psCmd.Flags().BoolVarP(&psAll, "all", "a", false, i18n.T("cmd.vm.ps.flag.all"))
parent.AddCommand(psCmd) parent.AddCommand(psCmd)
} }
@ -138,13 +127,13 @@ func addVMPsCommand(parent *cobra.Command) {
func listContainers(all bool) error { func listContainers(all bool) error {
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
ctx := context.Background() ctx := context.Background()
containers, err := manager.List(ctx) containers, err := manager.List(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to list containers: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.list_containers")+": %w", err)
} }
// Filter if not showing all // Filter if not showing all
@ -160,15 +149,15 @@ func listContainers(all bool) error {
if len(containers) == 0 { if len(containers) == 0 {
if all { if all {
fmt.Println("No containers") fmt.Println(i18n.T("cmd.vm.ps.no_containers"))
} else { } else {
fmt.Println("No running containers") fmt.Println(i18n.T("cmd.vm.ps.no_running"))
} }
return nil return nil
} }
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tIMAGE\tSTATUS\tSTARTED\tPID") fmt.Fprintln(w, i18n.T("cmd.vm.ps.header"))
fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---") fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---")
for _, c := range containers { for _, c := range containers {
@ -217,14 +206,11 @@ func formatDuration(d time.Duration) string {
func addVMStopCommand(parent *cobra.Command) { func addVMStopCommand(parent *cobra.Command) {
stopCmd := &cobra.Command{ stopCmd := &cobra.Command{
Use: "stop <container-id>", Use: "stop <container-id>",
Short: "Stop a running VM", Short: i18n.T("cmd.vm.stop.short"),
Long: "Stops a running VM by ID.\n\n" + Long: i18n.T("cmd.vm.stop.long"),
"Examples:\n" +
" core vm stop abc12345\n" +
" core vm stop abc1",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("container ID is required") return fmt.Errorf(i18n.T("cmd.vm.error.id_required"))
} }
return stopContainer(args[0]) return stopContainer(args[0])
}, },
@ -236,7 +222,7 @@ func addVMStopCommand(parent *cobra.Command) {
func stopContainer(id string) error { func stopContainer(id string) error {
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
// Support partial ID matching // Support partial ID matching
@ -245,14 +231,14 @@ func stopContainer(id string) error {
return err return err
} }
fmt.Printf("%s %s\n", dimStyle.Render("Stopping:"), fullID[:8]) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8])
ctx := context.Background() ctx := context.Background()
if err := manager.Stop(ctx, fullID); err != nil { if err := manager.Stop(ctx, fullID); err != nil {
return fmt.Errorf("failed to stop container: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.stop_container")+": %w", err)
} }
fmt.Printf("%s\n", successStyle.Render("Stopped")) fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.stop.stopped")))
return nil return nil
} }
@ -273,11 +259,11 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s
switch len(matches) { switch len(matches) {
case 0: case 0:
return "", fmt.Errorf("no container found matching: %s", partialID) return "", fmt.Errorf(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID}))
case 1: case 1:
return matches[0].ID, nil return matches[0].ID, nil
default: default:
return "", fmt.Errorf("multiple containers match '%s', be more specific", partialID) return "", fmt.Errorf(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID}))
} }
} }
@ -287,20 +273,17 @@ var logsFollow bool
func addVMLogsCommand(parent *cobra.Command) { func addVMLogsCommand(parent *cobra.Command) {
logsCmd := &cobra.Command{ logsCmd := &cobra.Command{
Use: "logs <container-id>", Use: "logs <container-id>",
Short: "View VM logs", Short: i18n.T("cmd.vm.logs.short"),
Long: "View logs from a VM.\n\n" + Long: i18n.T("cmd.vm.logs.long"),
"Examples:\n" +
" core vm logs abc12345\n" +
" core vm logs -f abc1",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("container ID is required") return fmt.Errorf(i18n.T("cmd.vm.error.id_required"))
} }
return viewLogs(args[0], logsFollow) return viewLogs(args[0], logsFollow)
}, },
} }
logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, "Follow log output") logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, i18n.T("cmd.vm.logs.flag.follow"))
parent.AddCommand(logsCmd) parent.AddCommand(logsCmd)
} }
@ -308,7 +291,7 @@ func addVMLogsCommand(parent *cobra.Command) {
func viewLogs(id string, follow bool) error { func viewLogs(id string, follow bool) error {
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
fullID, err := resolveContainerID(manager, id) fullID, err := resolveContainerID(manager, id)
@ -319,7 +302,7 @@ func viewLogs(id string, follow bool) error {
ctx := context.Background() ctx := context.Background()
reader, err := manager.Logs(ctx, fullID, follow) reader, err := manager.Logs(ctx, fullID, follow)
if err != nil { if err != nil {
return fmt.Errorf("failed to get logs: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.get_logs")+": %w", err)
} }
defer reader.Close() defer reader.Close()
@ -331,14 +314,11 @@ func viewLogs(id string, follow bool) error {
func addVMExecCommand(parent *cobra.Command) { func addVMExecCommand(parent *cobra.Command) {
execCmd := &cobra.Command{ execCmd := &cobra.Command{
Use: "exec <container-id> <command> [args...]", Use: "exec <container-id> <command> [args...]",
Short: "Execute a command in a VM", Short: i18n.T("cmd.vm.exec.short"),
Long: "Execute a command inside a running VM via SSH.\n\n" + Long: i18n.T("cmd.vm.exec.long"),
"Examples:\n" +
" core vm exec abc12345 ls -la\n" +
" core vm exec abc1 /bin/sh",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 { if len(args) < 2 {
return fmt.Errorf("container ID and command are required") return fmt.Errorf(i18n.T("cmd.vm.error.id_and_cmd_required"))
} }
return execInContainer(args[0], args[1:]) return execInContainer(args[0], args[1:])
}, },
@ -350,7 +330,7 @@ func addVMExecCommand(parent *cobra.Command) {
func execInContainer(id string, cmd []string) error { func execInContainer(id string, cmd []string) error {
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
fullID, err := resolveContainerID(manager, id) fullID, err := resolveContainerID(manager, id)

View file

@ -10,6 +10,7 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/host-uk/core/pkg/container" "github.com/host-uk/core/pkg/container"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -17,14 +18,8 @@ import (
func addVMTemplatesCommand(parent *cobra.Command) { func addVMTemplatesCommand(parent *cobra.Command) {
templatesCmd := &cobra.Command{ templatesCmd := &cobra.Command{
Use: "templates", Use: "templates",
Short: "Manage LinuxKit templates", Short: i18n.T("cmd.vm.templates.short"),
Long: "Manage LinuxKit YAML templates for building VMs.\n\n" + Long: i18n.T("cmd.vm.templates.long"),
"Templates provide pre-configured LinuxKit configurations for common use cases.\n" +
"They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" +
"Examples:\n" +
" core vm templates # List available templates\n" +
" core vm templates show core-dev # Show template content\n" +
" core vm templates vars server-php # Show template variables",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return listTemplates() return listTemplates()
}, },
@ -41,14 +36,11 @@ func addVMTemplatesCommand(parent *cobra.Command) {
func addTemplatesShowCommand(parent *cobra.Command) { func addTemplatesShowCommand(parent *cobra.Command) {
showCmd := &cobra.Command{ showCmd := &cobra.Command{
Use: "show <template-name>", Use: "show <template-name>",
Short: "Display template content", Short: i18n.T("cmd.vm.templates.show.short"),
Long: "Display the content of a LinuxKit template.\n\n" + Long: i18n.T("cmd.vm.templates.show.long"),
"Examples:\n" +
" core templates show core-dev\n" +
" core templates show server-php",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("template name is required") return fmt.Errorf(i18n.T("cmd.vm.error.template_required"))
} }
return showTemplate(args[0]) return showTemplate(args[0])
}, },
@ -61,15 +53,11 @@ func addTemplatesShowCommand(parent *cobra.Command) {
func addTemplatesVarsCommand(parent *cobra.Command) { func addTemplatesVarsCommand(parent *cobra.Command) {
varsCmd := &cobra.Command{ varsCmd := &cobra.Command{
Use: "vars <template-name>", Use: "vars <template-name>",
Short: "Show template variables", Short: i18n.T("cmd.vm.templates.vars.short"),
Long: "Display all variables used in a template.\n\n" + Long: i18n.T("cmd.vm.templates.vars.long"),
"Shows required variables (no default) and optional variables (with defaults).\n\n" +
"Examples:\n" +
" core templates vars core-dev\n" +
" core templates vars server-php",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("template name is required") return fmt.Errorf(i18n.T("cmd.vm.error.template_required"))
} }
return showTemplateVars(args[0]) return showTemplateVars(args[0])
}, },
@ -82,14 +70,14 @@ func listTemplates() error {
templates := container.ListTemplates() templates := container.ListTemplates()
if len(templates) == 0 { if len(templates) == 0 {
fmt.Println("No templates available.") fmt.Println(i18n.T("cmd.vm.templates.no_templates"))
return nil return nil
} }
fmt.Printf("%s\n\n", repoNameStyle.Render("Available LinuxKit Templates")) fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.vm.templates.title")))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tDESCRIPTION") fmt.Fprintln(w, i18n.T("cmd.vm.templates.header"))
fmt.Fprintln(w, "----\t-----------") fmt.Fprintln(w, "----\t-----------")
for _, tmpl := range templates { for _, tmpl := range templates {
@ -102,9 +90,9 @@ func listTemplates() error {
w.Flush() w.Flush()
fmt.Println() fmt.Println()
fmt.Printf("Show template: %s\n", dimStyle.Render("core vm templates show <name>")) fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.show"), dimStyle.Render("core vm templates show <name>"))
fmt.Printf("Show variables: %s\n", dimStyle.Render("core vm templates vars <name>")) fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars <name>"))
fmt.Printf("Run from template: %s\n", dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\"")) fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
return nil return nil
} }
@ -115,7 +103,7 @@ func showTemplate(name string) error {
return err return err
} }
fmt.Printf("%s %s\n\n", dimStyle.Render("Template:"), repoNameStyle.Render(name)) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(name))
fmt.Println(content) fmt.Println(content)
return nil return nil
@ -129,10 +117,10 @@ func showTemplateVars(name string) error {
required, optional := container.ExtractVariables(content) required, optional := container.ExtractVariables(content)
fmt.Printf("%s %s\n\n", dimStyle.Render("Template:"), repoNameStyle.Render(name)) fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(name))
if len(required) > 0 { if len(required) > 0 {
fmt.Printf("%s\n", errorStyle.Render("Required Variables (no default):")) fmt.Printf("%s\n", errorStyle.Render(i18n.T("cmd.vm.templates.vars.required")))
for _, v := range required { for _, v := range required {
fmt.Printf(" %s\n", varStyle.Render("${"+v+"}")) fmt.Printf(" %s\n", varStyle.Render("${"+v+"}"))
} }
@ -140,7 +128,7 @@ func showTemplateVars(name string) error {
} }
if len(optional) > 0 { if len(optional) > 0 {
fmt.Printf("%s\n", successStyle.Render("Optional Variables (with defaults):")) fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.templates.vars.optional")))
for v, def := range optional { for v, def := range optional {
fmt.Printf(" %s = %s\n", fmt.Printf(" %s = %s\n",
varStyle.Render("${"+v+"}"), varStyle.Render("${"+v+"}"),
@ -150,7 +138,7 @@ func showTemplateVars(name string) error {
} }
if len(required) == 0 && len(optional) == 0 { if len(required) == 0 && len(optional) == 0 {
fmt.Println("No variables in this template.") fmt.Println(i18n.T("cmd.vm.templates.vars.none"))
} }
return nil return nil
@ -161,63 +149,63 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
// Apply template with variables // Apply template with variables
content, err := container.ApplyTemplate(templateName, vars) content, err := container.ApplyTemplate(templateName, vars)
if err != nil { if err != nil {
return fmt.Errorf("failed to apply template: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.apply_template")+": %w", err)
} }
// Create a temporary directory for the build // Create a temporary directory for the build
tmpDir, err := os.MkdirTemp("", "core-linuxkit-*") tmpDir, err := os.MkdirTemp("", "core-linuxkit-*")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.create_temp")+": %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
// Write the YAML file // Write the YAML file
yamlPath := filepath.Join(tmpDir, templateName+".yml") yamlPath := filepath.Join(tmpDir, templateName+".yml")
if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil { if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write template: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.write_template")+": %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Template:"), repoNameStyle.Render(templateName)) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(templateName))
fmt.Printf("%s %s\n", dimStyle.Render("Building:"), yamlPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath)
// Build the image using linuxkit // Build the image using linuxkit
outputPath := filepath.Join(tmpDir, templateName) outputPath := filepath.Join(tmpDir, templateName)
if err := buildLinuxKitImage(yamlPath, outputPath); err != nil { if err := buildLinuxKitImage(yamlPath, outputPath); err != nil {
return fmt.Errorf("failed to build image: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.build_image")+": %w", err)
} }
// Find the built image (linuxkit creates .iso or other format) // Find the built image (linuxkit creates .iso or other format)
imagePath := findBuiltImage(outputPath) imagePath := findBuiltImage(outputPath)
if imagePath == "" { if imagePath == "" {
return fmt.Errorf("no image found after build") return fmt.Errorf(i18n.T("cmd.vm.error.no_image_found"))
} }
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), imagePath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.image")), imagePath)
fmt.Println() fmt.Println()
// Run the image // Run the image
manager, err := container.NewLinuxKitManager() manager, err := container.NewLinuxKitManager()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize container manager: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
} }
fmt.Printf("%s %s\n", dimStyle.Render("Hypervisor:"), manager.Hypervisor().Name()) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
fmt.Println() fmt.Println()
ctx := context.Background() ctx := context.Background()
c, err := manager.Run(ctx, imagePath, runOpts) c, err := manager.Run(ctx, imagePath, runOpts)
if err != nil { if err != nil {
return fmt.Errorf("failed to run container: %w", err) return fmt.Errorf(i18n.T("cmd.vm.error.run_container")+": %w", err)
} }
if runOpts.Detach { if runOpts.Detach {
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID) fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.vm.label.started")), c.ID)
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID) fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
fmt.Println() fmt.Println()
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8]) fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]interface{}{"ID": c.ID[:8]}))
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8]) fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]interface{}{"ID": c.ID[:8]}))
} else { } else {
fmt.Printf("\n%s %s\n", dimStyle.Render("Container stopped:"), c.ID) fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
} }
return nil return nil
@ -298,7 +286,7 @@ func lookupLinuxKit() (string, error) {
} }
} }
return "", fmt.Errorf("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit") return "", fmt.Errorf(i18n.T("cmd.vm.error.linuxkit_not_found"))
} }
// ParseVarFlags parses --var flags into a map. // ParseVarFlags parses --var flags into a map.

View file

@ -4,6 +4,7 @@ package vm
import ( import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/cmd/shared" "github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -25,17 +26,8 @@ var (
func AddVMCommands(root *cobra.Command) { func AddVMCommands(root *cobra.Command) {
vmCmd := &cobra.Command{ vmCmd := &cobra.Command{
Use: "vm", Use: "vm",
Short: "LinuxKit VM management", Short: i18n.T("cmd.vm.short"),
Long: "Manage LinuxKit virtual machines.\n\n" + Long: i18n.T("cmd.vm.long"),
"LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" +
"They run using qemu or hyperkit depending on your system.\n\n" +
"Commands:\n" +
" run Run a VM from image or template\n" +
" ps List running VMs\n" +
" stop Stop a running VM\n" +
" logs View VM logs\n" +
" exec Execute command in VM\n" +
" templates Manage LinuxKit templates",
} }
root.AddCommand(vmCmd) root.AddCommand(vmCmd)

File diff suppressed because it is too large Load diff