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"
"github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -32,30 +33,19 @@ var (
var taskCommitCmd = &cobra.Command{
Use: "task:commit [task-id]",
Short: "Auto-commit changes with task reference",
Long: `Creates a git commit with a task reference and co-author attribution.
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),
Short: i18n.T("cmd.ai.task_commit.short"),
Long: i18n.T("cmd.ai.task_commit.long"),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
if taskCommitMessage == "" {
return fmt.Errorf("commit message required (--message or -m)")
return fmt.Errorf(i18n.T("cmd.ai.task_commit.message_required"))
}
cfg, err := agentic.LoadConfig("")
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)
@ -66,7 +56,7 @@ Examples:
// Get task details
task, err := client.GetTask(ctx, taskID)
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
@ -81,35 +71,35 @@ Examples:
// Get current directory
cwd, err := os.Getwd()
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
hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd)
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 {
fmt.Println("No uncommitted changes to commit.")
fmt.Println(i18n.T("cmd.ai.task_commit.no_changes"))
return nil
}
// 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 {
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
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 {
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
@ -118,23 +108,15 @@ Examples:
var taskPRCmd = &cobra.Command{
Use: "task:pr [task-id]",
Short: "Create a pull request for a task",
Long: `Creates a GitHub pull request linked to a task.
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),
Short: i18n.T("cmd.ai.task_pr.short"),
Long: i18n.T("cmd.ai.task_pr.long"),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
cfg, err := agentic.LoadConfig("")
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)
@ -145,31 +127,31 @@ Examples:
// Get task details
task, err := client.GetTask(ctx, taskID)
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
cwd, err := os.Getwd()
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
branch, err := agentic.GetCurrentBranch(ctx, cwd)
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" {
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
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 {
// Try setting upstream
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
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)
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(" URL: %s\n", prURL)
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_pr.created"))
fmt.Printf(" %s %s\n", i18n.T("cmd.ai.label.url"), prURL)
return nil
},
@ -200,15 +182,15 @@ Examples:
func init() {
// task:commit command flags
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", "Commit message (without task reference)")
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", "Scope for the commit type (e.g., auth, api, ui)")
taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, "Push changes after committing")
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message"))
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope"))
taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, i18n.T("cmd.ai.task_commit.flag.push"))
// task:pr command flags
taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", "PR title (defaults to task title)")
taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, "Create as draft PR")
taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", "Labels to add (comma-separated)")
taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", "Base branch (defaults to main)")
taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", i18n.T("cmd.ai.task_pr.flag.title"))
taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, i18n.T("cmd.ai.task_pr.flag.draft"))
taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", i18n.T("cmd.ai.task_pr.flag.labels"))
taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", i18n.T("cmd.ai.task_pr.flag.base"))
}
func addTaskCommitCommand(parent *cobra.Command) {

View file

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

View file

@ -8,6 +8,7 @@ import (
"time"
"github.com/host-uk/core/pkg/agentic"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -27,23 +28,19 @@ var (
var taskUpdateCmd = &cobra.Command{
Use: "task:update [task-id]",
Short: "Update task status or progress",
Long: `Updates a task's status, progress, or adds notes.
Examples:
core ai task:update abc123 --status in_progress
core ai task:update abc123 --progress 50 --notes 'Halfway done'`,
Args: cobra.ExactArgs(1),
Short: i18n.T("cmd.ai.task_update.short"),
Long: i18n.T("cmd.ai.task_update.long"),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
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("")
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)
@ -60,29 +57,25 @@ Examples:
}
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
},
}
var taskCompleteCmd = &cobra.Command{
Use: "task:complete [task-id]",
Short: "Mark a task as completed",
Long: `Marks a task as completed with optional output and artifacts.
Examples:
core ai task:complete abc123 --output 'Feature implemented'
core ai task:complete abc123 --failed --error 'Build failed'`,
Args: cobra.ExactArgs(1),
Short: i18n.T("cmd.ai.task_complete.short"),
Long: i18n.T("cmd.ai.task_complete.long"),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
taskID := args[0]
cfg, err := agentic.LoadConfig("")
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)
@ -97,13 +90,13 @@ Examples:
}
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 {
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 {
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
},
@ -111,14 +104,14 @@ Examples:
func init() {
// task:update command flags
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", "New status (pending, in_progress, completed, blocked)")
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, "Progress percentage (0-100)")
taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", "Notes about the update")
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status"))
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress"))
taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", i18n.T("cmd.ai.task_update.flag.notes"))
// task:complete command flags
taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", "Summary of the completed work")
taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, "Mark the task as failed")
taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", "Error message if failed")
taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", i18n.T("cmd.ai.task_complete.flag.output"))
taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, i18n.T("cmd.ai.task_complete.flag.failed"))
taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", i18n.T("cmd.ai.task_complete.flag.error"))
}
func addTaskUpdateCommand(parent *cobra.Command) {

View file

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

View file

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

View file

@ -17,6 +17,7 @@ import (
buildpkg "github.com/host-uk/core/pkg/build"
"github.com/host-uk/core/pkg/build/builders"
"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.
@ -24,13 +25,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// Get current working directory as project root
projectDir, err := os.Getwd()
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)
buildCfg, err := buildpkg.LoadConfig(projectDir)
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
@ -40,11 +41,10 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
} else {
projectType, err = buildpkg.PrimaryType(projectDir)
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 == "" {
return fmt.Errorf("no supported project type detected in %s\n"+
"Supported types: go (go.mod), wails (wails.json), node (package.json), php (composer.json)", projectDir)
return fmt.Errorf("%s", i18n.T("cmd.build.error.no_project_type", map[string]interface{}{"Dir": projectDir}))
}
}
@ -82,11 +82,11 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// Print build info (unless CI mode)
if !ciMode {
fmt.Printf("%s Building project\n", buildHeaderStyle.Render("Build:"))
fmt.Printf(" Type: %s\n", buildTargetStyle.Render(string(projectType)))
fmt.Printf(" Output: %s\n", buildTargetStyle.Render(outputDir))
fmt.Printf(" Binary: %s\n", buildTargetStyle.Render(binaryName))
fmt.Printf(" Targets: %s\n", buildTargetStyle.Render(formatTargets(buildTargets)))
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.build")), i18n.T("cmd.build.building_project"))
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.type"), buildTargetStyle.Render(string(projectType)))
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.output"), buildTargetStyle.Render(outputDir))
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.binary"), buildTargetStyle.Render(binaryName))
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.targets"), buildTargetStyle.Render(formatTargets(buildTargets)))
fmt.Println()
}
@ -120,13 +120,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
artifacts, err := builder.Build(ctx, cfg, buildTargets)
if err != nil {
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
}
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()
for _, artifact := range artifacts {
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 !ciMode {
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
@ -164,7 +164,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil {
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
}
@ -172,7 +172,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if signCfg.MacOS.Notarize {
if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil {
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
}
@ -184,13 +184,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
if doArchive && len(artifacts) > 0 {
if !ciMode {
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)
if err != nil {
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
}
@ -240,7 +240,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
// JSON output for CI
output, err := json.MarshalIndent(outputArtifacts, "", " ")
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))
}
@ -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) {
if !ciMode {
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)
if err != nil {
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
}
@ -267,7 +267,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
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
}
@ -276,7 +276,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
if signCfg.Enabled {
if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil {
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
}
@ -321,7 +321,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
osArch := strings.Split(part, "/")
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{
@ -331,7 +331,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
}
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
@ -360,10 +360,10 @@ func getBuilder(projectType buildpkg.ProjectType) (buildpkg.Builder, error) {
case buildpkg.ProjectTypeTaskfile:
return builders.NewTaskfileBuilder(), nil
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:
return nil, fmt.Errorf("PHP builder not yet implemented")
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented"))
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"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/leaanthony/debme"
"github.com/leaanthony/gosod"
"golang.org/x/net/html"
@ -26,22 +27,22 @@ import (
// Error sentinels for build commands
var (
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.
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-*")
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
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 {
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)
@ -52,48 +53,48 @@ func downloadPWA(baseURL, destDir string) error {
// Fetch the main HTML page
resp, err := http.Get(baseURL)
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()
body, err := io.ReadAll(resp.Body)
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
manifestURL, err := findManifestURL(string(body), baseURL)
if err != nil {
// 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 {
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
}
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
manifest, err := fetchManifest(manifestURL)
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
assets := collectAssets(manifest, manifestURL)
for _, assetURL := range assets {
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
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
}
@ -129,7 +130,7 @@ func findManifestURL(htmlContent, baseURL string) (string, error) {
f(doc)
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)
@ -218,14 +219,14 @@ func downloadAsset(assetURL, destDir string) error {
// runBuild builds a desktop application from a local directory.
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)
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() {
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"
@ -237,33 +238,33 @@ func runBuild(fromPath string) error {
outputExe := appName
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
fmt.Println("Generating application from template...")
fmt.Println(i18n.T("cmd.build.from_path.generating_template"))
templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
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)
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}
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
fmt.Println("Copying application files...")
fmt.Println(i18n.T("cmd.build.from_path.copying_files"))
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
fmt.Println("Compiling application...")
fmt.Println(i18n.T("cmd.build.from_path.compiling"))
// Run go mod tidy
cmd := exec.Command("go", "mod", "tidy")
@ -271,7 +272,7 @@ func runBuild(fromPath string) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
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
@ -280,10 +281,10 @@ func runBuild(fromPath string) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
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
}

View file

@ -11,6 +11,7 @@ import (
"os"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/sdk"
)
@ -20,7 +21,7 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
projectDir, err := os.Getwd()
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
@ -34,48 +35,48 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
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 {
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()
// Detect spec
detectedSpec, err := s.DetectSpec()
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
}
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 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 {
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.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
}
if lang != "" {
// Generate single language
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
}
fmt.Printf(" Generated: %s\n", buildTargetStyle.Render(lang))
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(lang))
} else {
// Generate all
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
}
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.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
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release"
)
@ -11,19 +12,19 @@ import (
func runChangelog(fromRef, toRef string) error {
projectDir, err := os.Getwd()
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
cfg, err := release.LoadConfig(projectDir)
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
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog)
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)

View file

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

View file

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release"
)
@ -16,13 +17,13 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
// Get current directory
projectDir, err := os.Getwd()
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
cfg, err := release.LoadConfig(projectDir)
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
@ -43,35 +44,35 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
}
// 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 {
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 {
fmt.Printf(" %s\n", releaseSuccessStyle.Render("GO FOR LAUNCH"))
fmt.Printf(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch")))
}
fmt.Println()
// Check for publishers
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
rel, err := release.Publish(ctx, cfg, dryRun)
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
}
// Print summary
fmt.Println()
fmt.Printf("%s Publish completed!\n", releaseSuccessStyle.Render("Success:"))
fmt.Printf(" Version: %s\n", releaseValueStyle.Render(rel.Version))
fmt.Printf(" Artifacts: %d\n", len(rel.Artifacts))
fmt.Printf("%s %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.label.success")), i18n.T("cmd.ci.publish_completed"))
fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.version"), releaseValueStyle.Render(rel.Version))
fmt.Printf(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts))
if !dryRun {
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 (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -31,13 +32,8 @@ var (
var ciCmd = &cobra.Command{
Use: "ci",
Short: "Publish releases (dry-run by default)",
Long: `Publishes pre-built artifacts from dist/ to configured targets.
Run 'core build' first to create artifacts.
SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.
Configuration: .core/release.yaml`,
Short: i18n.T("cmd.ci.short"),
Long: i18n.T("cmd.ci.long"),
RunE: func(cmd *cobra.Command, args []string) error {
dryRun := !ciGoForLaunch
return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
@ -46,8 +42,8 @@ Configuration: .core/release.yaml`,
var ciInitCmd = &cobra.Command{
Use: "init",
Short: "Initialize release configuration",
Long: "Creates a .core/release.yaml configuration file interactively.",
Short: i18n.T("cmd.ci.init.short"),
Long: i18n.T("cmd.ci.init.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCIReleaseInit()
},
@ -55,8 +51,8 @@ var ciInitCmd = &cobra.Command{
var ciChangelogCmd = &cobra.Command{
Use: "changelog",
Short: "Generate changelog",
Long: "Generates a changelog from conventional commits.",
Short: i18n.T("cmd.ci.changelog.short"),
Long: i18n.T("cmd.ci.changelog.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runChangelog(changelogFromRef, changelogToRef)
},
@ -64,8 +60,8 @@ var ciChangelogCmd = &cobra.Command{
var ciVersionCmd = &cobra.Command{
Use: "version",
Short: "Show or set version",
Long: "Shows the determined version or validates a version string.",
Short: i18n.T("cmd.ci.version.short"),
Long: i18n.T("cmd.ci.version.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCIReleaseVersion()
},
@ -73,14 +69,14 @@ var ciVersionCmd = &cobra.Command{
func init() {
// Main ci command flags
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, "Actually publish (default is dry-run for safety)")
ciCmd.Flags().StringVar(&ciVersion, "version", "", "Version to release (e.g., v1.2.3)")
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, "Create release as a draft")
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, "Mark release as a prerelease")
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.ci.flag.go_for_launch"))
ciCmd.Flags().StringVar(&ciVersion, "version", "", i18n.T("cmd.ci.flag.version"))
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, i18n.T("cmd.ci.flag.draft"))
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, i18n.T("cmd.ci.flag.prerelease"))
// Changelog subcommand flags
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", "Starting ref (default: previous tag)")
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", "Ending ref (default: HEAD)")
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", i18n.T("cmd.ci.changelog.flag.from"))
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", i18n.T("cmd.ci.changelog.flag.to"))
// Add subcommands
ciCmd.AddCommand(ciInitCmd)

View file

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/release"
)
@ -11,14 +12,14 @@ import (
func runCIReleaseVersion() error {
projectDir, err := os.Getwd()
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)
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
}

View file

@ -30,6 +30,7 @@ package dev
import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -55,31 +56,8 @@ var (
func AddCommands(root *cobra.Command) {
devCmd := &cobra.Command{
Use: "dev",
Short: "Multi-repo development workflow",
Long: `Manage multiple git repositories and GitHub integration.
Uses repos.yaml to discover repositories. Falls back to scanning
the current directory if no registry is found.
Git Operations:
work Combined status -> commit -> push workflow
health Quick repo health summary
commit Claude-assisted commit messages
push Push repos with unpushed commits
pull Pull repos behind remote
GitHub Integration (requires gh CLI):
issues List open issues across repos
reviews List PRs awaiting review
ci Check GitHub Actions status
impact Analyse dependency impact
Dev Environment:
install Download dev environment image
boot Start dev environment VM
stop Stop dev environment VM
shell Open shell in dev VM
status Check dev VM status`,
Short: i18n.T("cmd.dev.short"),
Long: i18n.T("cmd.dev.long"),
}
root.AddCommand(devCmd)

View file

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

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -46,10 +47,8 @@ var (
func addCICommand(parent *cobra.Command) {
ciCmd := &cobra.Command{
Use: "ci",
Short: "Check CI status across all repos",
Long: `Fetches GitHub Actions workflow status for all repos.
Shows latest run status for each repo.
Requires the 'gh' CLI to be installed and authenticated.`,
Short: i18n.T("cmd.dev.ci.short"),
Long: i18n.T("cmd.dev.ci.long"),
RunE: func(cmd *cobra.Command, args []string) error {
branch := ciBranch
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().StringVarP(&ciBranch, "branch", "b", "main", "Filter by branch")
ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, "Show only failed runs")
ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", i18n.T("cmd.dev.ci.flag.registry"))
ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", i18n.T("cmd.dev.ci.flag.branch"))
ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, i18n.T("cmd.dev.ci.flag.failed"))
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 {
// Check gh is available
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
@ -105,7 +104,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
repoList := reg.List()
for i, repo := range repoList {
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)
if err != nil {
@ -147,18 +146,18 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
// Print summary
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 {
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 {
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 {
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 {
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()
@ -183,7 +182,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
if len(fetchErrors) > 0 {
fmt.Println()
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/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -21,16 +22,15 @@ var (
func addCommitCommand(parent *cobra.Command) {
commitCmd := &cobra.Command{
Use: "commit",
Short: "Claude-assisted commits across repos",
Long: `Uses Claude to create commits for dirty repos.
Shows uncommitted changes and invokes Claude to generate commit messages.`,
Short: i18n.T("cmd.dev.commit.short"),
Long: i18n.T("cmd.dev.commit.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runCommit(commitRegistryPath, commitAll)
},
}
commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
commitCmd.Flags().BoolVar(&commitAll, "all", false, "Commit all dirty repos without prompting")
commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", i18n.T("cmd.dev.commit.flag.registry"))
commitCmd.Flags().BoolVar(&commitAll, "all", false, i18n.T("cmd.dev.commit.flag.all"))
parent.AddCommand(commitCmd)
}
@ -47,7 +47,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil {
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 {
registryPath, err = repos.FindRegistry()
if err == nil {
@ -55,7 +55,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil {
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 {
// Fallback: scan current directory
cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runCommit(registryPath string, all bool) error {
if err != nil {
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
}
}
@ -80,7 +80,7 @@ func runCommit(registryPath string, all bool) error {
}
if len(paths) == 0 {
fmt.Println("No git repositories found.")
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil
}
@ -99,22 +99,22 @@ func runCommit(registryPath string, all bool) error {
}
if len(dirtyRepos) == 0 {
fmt.Println("No uncommitted changes found.")
fmt.Println(i18n.T("cmd.dev.no_changes"))
return nil
}
// 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 {
fmt.Printf(" %s: ", repoNameStyle.Render(s.Name))
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 {
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 {
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()
}
@ -122,8 +122,8 @@ func runCommit(registryPath string, all bool) error {
// Confirm unless --all
if !all {
fmt.Println()
if !shared.Confirm("Have Claude commit these repos?") {
fmt.Println("Aborted.")
if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
}
@ -133,22 +133,22 @@ func runCommit(registryPath string, all bool) error {
// Commit each dirty repo
var succeeded, failed int
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 {
fmt.Printf(" %s %s\n", errorStyle.Render("x"), err)
failed++
} else {
fmt.Printf(" %s committed\n", successStyle.Render("v"))
fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed"))
succeeded++
}
fmt.Println()
}
// 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 {
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()

View file

@ -8,6 +8,7 @@ import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -22,16 +23,15 @@ var (
func addHealthCommand(parent *cobra.Command) {
healthCmd := &cobra.Command{
Use: "health",
Short: "Quick health check across all repos",
Long: `Shows a summary of repository health:
total repos, dirty repos, unpushed commits, etc.`,
Short: i18n.T("cmd.dev.health.short"),
Long: i18n.T("cmd.dev.health.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runHealth(healthRegistryPath, healthVerbose)
},
}
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown")
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", i18n.T("cmd.dev.health.flag.registry"))
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, i18n.T("cmd.dev.health.flag.verbose"))
parent.AddCommand(healthCmd)
}
@ -77,7 +77,7 @@ func runHealth(registryPath string, verbose bool) error {
}
if len(paths) == 0 {
fmt.Println("No git repositories found.")
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil
}
@ -125,16 +125,16 @@ func runHealth(registryPath string, verbose bool) error {
// Verbose output
if verbose {
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 {
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 {
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 {
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()
}
@ -144,33 +144,33 @@ func runHealth(registryPath string, verbose bool) error {
func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
parts := []string{
shared.StatusPart(total, "repos", shared.ValueStyle),
shared.StatusPart(total, i18n.T("cmd.dev.health.repos"), shared.ValueStyle),
}
// Dirty status
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 {
parts = append(parts, shared.StatusText("clean", shared.SuccessStyle))
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.status.clean"), shared.SuccessStyle))
}
// Push status
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 {
parts = append(parts, shared.StatusText("synced", shared.SuccessStyle))
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.synced"), shared.SuccessStyle))
}
// Pull status
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 {
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)
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...))
@ -180,7 +180,7 @@ func formatRepoList(reposList []string) string {
if len(reposList) <= 5 {
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 {

View file

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

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -60,9 +61,8 @@ var (
func addIssuesCommand(parent *cobra.Command) {
issuesCmd := &cobra.Command{
Use: "issues",
Short: "List open issues across all repos",
Long: `Fetches open issues from GitHub for all repos in the registry.
Requires the 'gh' CLI to be installed and authenticated.`,
Short: i18n.T("cmd.dev.issues.short"),
Long: i18n.T("cmd.dev.issues.long"),
RunE: func(cmd *cobra.Command, args []string) error {
limit := issuesLimit
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().IntVarP(&issuesLimit, "limit", "l", 10, "Max issues per repo")
issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", "Filter by assignee (use @me for yourself)")
issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", i18n.T("cmd.dev.issues.flag.registry"))
issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, i18n.T("cmd.dev.issues.flag.limit"))
issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", i18n.T("cmd.dev.issues.flag.assignee"))
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 {
// Check gh is available
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
@ -118,7 +118,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
repoList := reg.List()
for i, repo := range repoList {
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)
if err != nil {
@ -136,11 +136,11 @@ func runIssues(registryPath string, limit int, assignee string) error {
// Print issues
if len(allIssues) == 0 {
fmt.Println("No open issues found.")
fmt.Println(i18n.T("cmd.dev.issues.no_issues"))
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 {
printIssue(issue)
@ -150,7 +150,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
if len(fetchErrors) > 0 {
fmt.Println()
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"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -21,16 +22,15 @@ var (
func addPullCommand(parent *cobra.Command) {
pullCmd := &cobra.Command{
Use: "pull",
Short: "Pull updates across all repos",
Long: `Pulls updates for all repos.
By default only pulls repos that are behind. Use --all to pull all repos.`,
Short: i18n.T("cmd.dev.pull.short"),
Long: i18n.T("cmd.dev.pull.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPull(pullRegistryPath, pullAll)
},
}
pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
pullCmd.Flags().BoolVar(&pullAll, "all", false, "Pull all repos, not just those behind")
pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", i18n.T("cmd.dev.pull.flag.registry"))
pullCmd.Flags().BoolVar(&pullAll, "all", false, i18n.T("cmd.dev.pull.flag.all"))
parent.AddCommand(pullCmd)
}
@ -47,7 +47,7 @@ func runPull(registryPath string, all bool) error {
if err != nil {
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 {
registryPath, err = repos.FindRegistry()
if err == nil {
@ -55,7 +55,7 @@ func runPull(registryPath string, all bool) error {
if err != nil {
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 {
// Fallback: scan current directory
cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runPull(registryPath string, all bool) error {
if err != nil {
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 {
fmt.Println("No git repositories found.")
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil
}
@ -101,19 +101,19 @@ func runPull(registryPath string, all bool) error {
}
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
}
// Show what we're pulling
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 {
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 {
fmt.Printf(" %s: %s\n",
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()
@ -122,7 +122,7 @@ func runPull(registryPath string, all bool) error {
// Pull each repo
var succeeded, failed int
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)
if err != nil {
@ -136,9 +136,9 @@ func runPull(registryPath string, all bool) error {
// Summary
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 {
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()

View file

@ -7,6 +7,7 @@ import (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/git"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -21,16 +22,15 @@ var (
func addPushCommand(parent *cobra.Command) {
pushCmd := &cobra.Command{
Use: "push",
Short: "Push commits across all repos",
Long: `Pushes unpushed commits for all repos.
Shows repos with commits to push and confirms before pushing.`,
Short: i18n.T("cmd.dev.push.short"),
Long: i18n.T("cmd.dev.push.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPush(pushRegistryPath, pushForce)
},
}
pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, "Skip confirmation prompt")
pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", i18n.T("cmd.dev.push.flag.registry"))
pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, i18n.T("cmd.dev.push.flag.force"))
parent.AddCommand(pushCmd)
}
@ -47,7 +47,7 @@ func runPush(registryPath string, force bool) error {
if err != nil {
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 {
registryPath, err = repos.FindRegistry()
if err == nil {
@ -55,7 +55,7 @@ func runPush(registryPath string, force bool) error {
if err != nil {
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 {
// Fallback: scan current directory
cwd, _ := os.Getwd()
@ -63,7 +63,7 @@ func runPush(registryPath string, force bool) error {
if err != nil {
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 {
fmt.Println("No git repositories found.")
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
return nil
}
@ -98,17 +98,17 @@ func runPush(registryPath string, force bool) error {
}
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
}
// 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
for _, s := range aheadRepos {
fmt.Printf(" %s: %s\n",
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
}
@ -116,8 +116,8 @@ func runPush(registryPath string, force bool) error {
// Confirm unless --force
if !force {
fmt.Println()
if !shared.Confirm(fmt.Sprintf("Push %d commit(s) to %d repo(s)?", totalCommits, len(aheadRepos))) {
fmt.Println("Aborted.")
if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
fmt.Println(i18n.T("cli.aborted"))
return nil
}
}
@ -145,9 +145,9 @@ func runPush(registryPath string, force bool) error {
// Summary
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 {
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()

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -61,18 +62,16 @@ var (
func addReviewsCommand(parent *cobra.Command) {
reviewsCmd := &cobra.Command{
Use: "reviews",
Short: "List PRs needing review across all repos",
Long: `Fetches open PRs from GitHub for all repos in the registry.
Shows review status (approved, changes requested, pending).
Requires the 'gh' CLI to be installed and authenticated.`,
Short: i18n.T("cmd.dev.reviews.short"),
Long: i18n.T("cmd.dev.reviews.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll)
},
}
reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", "Filter by PR author")
reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, "Show all PRs including drafts")
reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", i18n.T("cmd.dev.reviews.flag.registry"))
reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", i18n.T("cmd.dev.reviews.flag.author"))
reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, i18n.T("cmd.dev.reviews.flag.all"))
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 {
// Check gh is available
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
@ -116,7 +115,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
repoList := reg.List()
for i, repo := range repoList {
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)
if err != nil {
@ -147,7 +146,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
// Print PRs
if len(allPRs) == 0 {
fmt.Println("No open PRs found.")
fmt.Println(i18n.T("cmd.dev.reviews.no_prs"))
return nil
}
@ -165,15 +164,15 @@ func runReviews(registryPath string, author string, showAll bool) error {
}
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 {
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 {
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 {
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()
@ -186,7 +185,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
if len(fetchErrors) > 0 {
fmt.Println()
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
switch pr.ReviewDecision {
case "APPROVED":
status = prApprovedStyle.Render("v approved")
status = prApprovedStyle.Render(i18n.T("cmd.dev.reviews.status_approved"))
case "CHANGES_REQUESTED":
status = prChangesStyle.Render("* changes requested")
status = prChangesStyle.Render(i18n.T("cmd.dev.reviews.status_changes"))
default:
status = prPendingStyle.Render("o pending review")
status = prPendingStyle.Render(i18n.T("cmd.dev.reviews.status_pending"))
}
// Draft indicator
draft := ""
if pr.IsDraft {
draft = prDraftStyle.Render(" [draft]")
draft = prDraftStyle.Render(" " + i18n.T("cmd.dev.reviews.draft"))
}
age := shared.FormatAge(pr.CreatedAt)

View file

@ -10,6 +10,7 @@ import (
"path/filepath"
"text/template"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@ -19,15 +20,13 @@ import (
func addSyncCommand(parent *cobra.Command) {
syncCmd := &cobra.Command{
Use: "sync",
Short: "Synchronizes the public service APIs with their internal implementations.",
Long: `This command scans the 'pkg' directory for services and ensures that the
top-level public API for each service is in sync with its internal implementation.
It automatically generates the necessary Go files with type aliases.`,
Short: i18n.T("cmd.dev.sync.short"),
Long: i18n.T("cmd.dev.sync.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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
},
}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/host-uk/core/pkg/devops"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -28,14 +29,8 @@ func addVMCommands(parent *cobra.Command) {
func addVMInstallCommand(parent *cobra.Command) {
installCmd := &cobra.Command{
Use: "install",
Short: "Download and install the dev environment image",
Long: `Downloads the platform-specific dev environment image.
The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI.
Downloads are cached at ~/.core/images/
Examples:
core dev install`,
Short: i18n.T("cmd.dev.vm.install.short"),
Long: i18n.T("cmd.dev.vm.install.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMInstall()
},
@ -51,15 +46,15 @@ func runVMInstall() error {
}
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.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
}
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("Downloading dev environment...")
fmt.Println(i18n.T("cmd.dev.vm.downloading"))
fmt.Println()
ctx := context.Background()
@ -70,7 +65,7 @@ func runVMInstall() error {
if total > 0 {
pct := int(float64(downloaded) / 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
}
}
@ -84,9 +79,9 @@ func runVMInstall() error {
elapsed := time.Since(start).Round(time.Second)
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.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
}
@ -102,21 +97,16 @@ var (
func addVMBootCommand(parent *cobra.Command) {
bootCmd := &cobra.Command{
Use: "boot",
Short: "Start the dev environment",
Long: `Boots the dev environment VM.
Examples:
core dev boot
core dev boot --memory 8192 --cpus 4
core dev boot --fresh`,
Short: i18n.T("cmd.dev.vm.boot.short"),
Long: i18n.T("cmd.dev.vm.boot.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh)
},
}
bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, "Memory in MB (default: 4096)")
bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, "Number of CPUs (default: 2)")
bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, "Stop existing and start fresh")
bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, i18n.T("cmd.dev.vm.boot.flag.memory"))
bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, i18n.T("cmd.dev.vm.boot.flag.cpus"))
bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, i18n.T("cmd.dev.vm.boot.flag.fresh"))
parent.AddCommand(bootCmd)
}
@ -128,7 +118,7 @@ func runVMBoot(memory, cpus int, fresh bool) error {
}
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()
@ -140,9 +130,9 @@ func runVMBoot(memory, cpus int, fresh bool) error {
}
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("Booting dev environment...")
fmt.Println(i18n.T("cmd.dev.vm.booting"))
ctx := context.Background()
if err := d.Boot(ctx, opts); err != nil {
@ -150,10 +140,10 @@ func runVMBoot(memory, cpus int, fresh bool) error {
}
fmt.Println()
fmt.Println(successStyle.Render("Dev environment running"))
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.running")))
fmt.Println()
fmt.Printf("Connect with: %s\n", dimStyle.Render("core dev shell"))
fmt.Printf("SSH port: %s\n", dimStyle.Render("2222"))
fmt.Println(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")}))
fmt.Printf("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222"))
return nil
}
@ -162,11 +152,8 @@ func runVMBoot(memory, cpus int, fresh bool) error {
func addVMStopCommand(parent *cobra.Command) {
stopCmd := &cobra.Command{
Use: "stop",
Short: "Stop the dev environment",
Long: `Stops the running dev environment VM.
Examples:
core dev stop`,
Short: i18n.T("cmd.dev.vm.stop.short"),
Long: i18n.T("cmd.dev.vm.stop.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMStop()
},
@ -188,17 +175,17 @@ func runVMStop() error {
}
if !running {
fmt.Println(dimStyle.Render("Dev environment is not running"))
fmt.Println(dimStyle.Render(i18n.T("cmd.dev.vm.not_running")))
return nil
}
fmt.Println("Stopping dev environment...")
fmt.Println(i18n.T("cmd.dev.vm.stopping"))
if err := d.Stop(ctx); err != nil {
return err
}
fmt.Println(successStyle.Render("Stopped"))
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.stopped")))
return nil
}
@ -206,11 +193,8 @@ func runVMStop() error {
func addVMStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{
Use: "vm-status",
Short: "Show dev environment status",
Long: `Shows the current status of the dev environment.
Examples:
core dev vm-status`,
Short: i18n.T("cmd.dev.vm.status.short"),
Long: i18n.T("cmd.dev.vm.status.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMStatus()
},
@ -231,19 +215,19 @@ func runVMStatus() error {
return err
}
fmt.Println(headerStyle.Render("Dev Environment Status"))
fmt.Println(headerStyle.Render(i18n.T("cmd.dev.vm.status_title")))
fmt.Println()
// Installation status
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 != "" {
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 {
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.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
}
@ -251,16 +235,16 @@ func runVMStatus() error {
// Running status
if status.Running {
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), successStyle.Render("Running"))
fmt.Printf("%s %s\n", dimStyle.Render("Container:"), status.ContainerID[:8])
fmt.Printf("%s %dMB\n", dimStyle.Render("Memory:"), status.Memory)
fmt.Printf("%s %d\n", dimStyle.Render("CPUs:"), status.CPUs)
fmt.Printf("%s %d\n", dimStyle.Render("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.status_label")), successStyle.Render(i18n.T("cmd.dev.vm.status_running")))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8])
fmt.Printf("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory)
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs)
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime))
} 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.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
@ -286,21 +270,14 @@ var vmShellConsole bool
func addVMShellCommand(parent *cobra.Command) {
shellCmd := &cobra.Command{
Use: "shell [-- command...]",
Short: "Connect to the dev environment",
Long: `Opens an interactive shell in the dev environment.
Uses SSH by default, or serial console with --console.
Examples:
core dev shell
core dev shell --console
core dev shell -- ls -la`,
Short: i18n.T("cmd.dev.vm.shell.short"),
Long: i18n.T("cmd.dev.vm.shell.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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)
}
@ -330,22 +307,15 @@ var (
func addVMServeCommand(parent *cobra.Command) {
serveCmd := &cobra.Command{
Use: "serve",
Short: "Mount project and start dev server",
Long: `Mounts the current project into the dev environment and starts a dev server.
Auto-detects the appropriate serve command based on project files.
Examples:
core dev serve
core dev serve --port 3000
core dev serve --path public`,
Short: i18n.T("cmd.dev.vm.serve.short"),
Long: i18n.T("cmd.dev.vm.serve.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMServe(vmServePort, vmServePath)
},
}
serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, "Port to serve on (default: 8000)")
serveCmd.Flags().StringVar(&vmServePath, "path", "", "Subdirectory to serve")
serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, i18n.T("cmd.dev.vm.serve.flag.port"))
serveCmd.Flags().StringVar(&vmServePath, "path", "", i18n.T("cmd.dev.vm.serve.flag.path"))
parent.AddCommand(serveCmd)
}
@ -377,21 +347,14 @@ var vmTestName string
func addVMTestCommand(parent *cobra.Command) {
testCmd := &cobra.Command{
Use: "test [-- command...]",
Short: "Run tests in the dev environment",
Long: `Runs tests in the dev environment.
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 ./...`,
Short: i18n.T("cmd.dev.vm.test.short"),
Long: i18n.T("cmd.dev.vm.test.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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)
}
@ -427,31 +390,16 @@ var (
func addVMClaudeCommand(parent *cobra.Command) {
claudeCmd := &cobra.Command{
Use: "claude",
Short: "Start sandboxed Claude session",
Long: `Starts a Claude Code session inside the dev environment sandbox.
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`,
Short: i18n.T("cmd.dev.vm.claude.short"),
Long: i18n.T("cmd.dev.vm.claude.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags)
},
}
claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, "Don't forward any auth credentials")
claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", "Model to use (opus, sonnet)")
claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, "Selective auth forwarding (gh,anthropic,ssh,git)")
claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, i18n.T("cmd.dev.vm.claude.flag.no_auth"))
claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", i18n.T("cmd.dev.vm.claude.flag.model"))
claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, i18n.T("cmd.dev.vm.claude.flag.auth"))
parent.AddCommand(claudeCmd)
}
@ -484,18 +432,14 @@ var vmUpdateApply bool
func addVMUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{
Use: "update",
Short: "Check for and apply updates",
Long: `Checks for dev environment updates and optionally applies them.
Examples:
core dev update
core dev update --apply`,
Short: i18n.T("cmd.dev.vm.update.short"),
Long: i18n.T("cmd.dev.vm.update.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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)
}
@ -508,7 +452,7 @@ func runVMUpdate(apply bool) error {
ctx := context.Background()
fmt.Println("Checking for updates...")
fmt.Println(i18n.T("cmd.dev.vm.checking_updates"))
fmt.Println()
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)
}
fmt.Printf("%s %s\n", dimStyle.Render("Current:"), 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.current_label")), valueStyle.Render(current))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest))
fmt.Println()
if !hasUpdate {
fmt.Println(successStyle.Render("Already up to date"))
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date")))
return nil
}
fmt.Println(warningStyle.Render("Update available"))
fmt.Println(warningStyle.Render(i18n.T("cmd.dev.vm.update_available")))
fmt.Println()
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
}
// Stop if running
running, _ := d.IsRunning(ctx)
if running {
fmt.Println("Stopping current instance...")
fmt.Println(i18n.T("cmd.dev.vm.stopping_current"))
_ = d.Stop(ctx)
}
fmt.Println("Downloading update...")
fmt.Println(i18n.T("cmd.dev.vm.downloading_update"))
fmt.Println()
start := time.Now()
err = d.Install(ctx, func(downloaded, total int64) {
if total > 0 {
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)
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
}

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import (
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -13,14 +14,15 @@ var docsListRegistryPath string
var docsListCmd = &cobra.Command{
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 {
return runDocsList(docsListRegistryPath)
},
}
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 {
@ -30,11 +32,11 @@ func runDocsList(registryPath string) error {
}
fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n",
headerStyle.Render("Repo"),
headerStyle.Render("README"),
headerStyle.Render("CLAUDE"),
headerStyle.Render("CHANGELOG"),
headerStyle.Render("docs/"),
headerStyle.Render(i18n.T("cmd.docs.list.header.repo")),
headerStyle.Render(i18n.T("cmd.docs.list.header.readme")),
headerStyle.Render(i18n.T("cmd.docs.list.header.claude")),
headerStyle.Render(i18n.T("cmd.docs.list.header.changelog")),
headerStyle.Render(i18n.T("cmd.docs.list.header.docs")),
)
fmt.Println(strings.Repeat("─", 70))
@ -48,7 +50,7 @@ func runDocsList(registryPath string) error {
docsDir := shared.CheckMark(false)
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",
@ -67,10 +69,9 @@ func runDocsList(registryPath string) error {
}
fmt.Println()
fmt.Printf("%s %d with docs, %d without\n",
shared.Label("Coverage"),
withDocs,
withoutDocs,
fmt.Printf("%s %s\n",
shared.Label(i18n.T("cmd.docs.list.coverage_label")),
i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}),
)
return nil

View file

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
)
@ -29,7 +30,7 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
if registryPath != "" {
reg, err = repos.LoadRegistry(registryPath)
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)
} else {
@ -37,14 +38,14 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
if err == nil {
reg, err = repos.LoadRegistry(registryPath)
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)
} else {
cwd, _ := os.Getwd()
reg, err = repos.ScanDirectory(cwd)
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
}

View file

@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -18,16 +19,17 @@ var (
var docsSyncCmd = &cobra.Command{
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 {
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
},
}
func init() {
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", "Path to repos.yaml")
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, "Show what would be synced without copying")
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", "Output directory (default: core-php/docs/packages)")
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("cmd.docs.sync.flag.registry"))
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run"))
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output"))
}
// 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 {
fmt.Println("No documentation found in any repos.")
fmt.Println(i18n.T("cmd.docs.sync.no_docs_found"))
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
var totalFiles int
@ -95,25 +97,26 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
fmt.Printf(" %s → %s %s\n",
repoNameStyle.Render(info.Name),
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 {
fmt.Printf(" %s\n", dimStyle.Render(f))
}
}
fmt.Printf("\n%s %d files from %d repos → %s\n",
dimStyle.Render("Total:"), totalFiles, len(docsInfo), outputDir)
fmt.Printf("\n%s %s\n",
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 {
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
}
// Confirm
fmt.Println()
if !confirm("Sync?") {
fmt.Println("Aborted.")
if !confirm(i18n.T("cmd.docs.sync.confirm")) {
fmt.Println(i18n.T("cli.confirm.abort"))
return nil
}
@ -147,7 +150,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
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
}

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
)
@ -44,7 +45,7 @@ func checkGitHubCLI() bool {
func checkWorkspace() {
registryPath, err := repos.FindRegistry()
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)
if err == nil {
@ -69,9 +70,9 @@ func checkWorkspace() {
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 {
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 (
"fmt"
"runtime"
"github.com/host-uk/core/pkg/i18n"
)
// printInstallInstructions prints OS-specific installation instructions
func printInstallInstructions() {
switch runtime.GOOS {
case "darwin":
fmt.Println(" brew install git gh php composer node pnpm docker")
fmt.Println(" brew install --cask claude")
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos_cask"))
case "linux":
fmt.Println(" # Install via your package manager or:")
fmt.Println(" # Git: apt install git")
fmt.Println(" # GitHub CLI: https://cli.github.com/")
fmt.Println(" # PHP: apt install php8.3-cli")
fmt.Println(" # Node: https://nodejs.org/")
fmt.Println(" # pnpm: npm install -g pnpm")
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_header"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_git"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_gh"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_php"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_node"))
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_pnpm"))
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 (
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -19,16 +20,8 @@ var (
func AddGoCommands(root *cobra.Command) {
goCmd := &cobra.Command{
Use: "go",
Short: "Go development tools",
Long: "Go development tools with enhanced output and environment setup.\n\n" +
"Commands:\n" +
" test Run tests\n" +
" cov Run tests with coverage report\n" +
" fmt Format Go code\n" +
" lint Run golangci-lint\n" +
" install Install Go binary\n" +
" mod Module management (tidy, download, verify)\n" +
" work Workspace management",
Short: i18n.T("cmd.go.short"),
Long: i18n.T("cmd.go.long"),
}
root.AddCommand(goCmd)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra"
)
@ -41,22 +42,12 @@ var (
func addPHPDeployCommand(parent *cobra.Command) {
deployCmd := &cobra.Command{
Use: "deploy",
Short: "Deploy to Coolify",
Long: "Deploy the PHP application to Coolify.\n\n" +
"Requires configuration in .env:\n" +
" COOLIFY_URL=https://coolify.example.com\n" +
" COOLIFY_TOKEN=your-api-token\n" +
" COOLIFY_APP_ID=production-app-id\n" +
" COOLIFY_STAGING_APP_ID=staging-app-id (optional)\n\n" +
"Examples:\n" +
" core php deploy # Deploy to production\n" +
" core php deploy --staging # Deploy to staging\n" +
" core php deploy --force # Force deployment\n" +
" core php deploy --wait # Wait for deployment to complete",
Short: i18n.T("cmd.php.deploy.short"),
Long: i18n.T("cmd.php.deploy.long"),
RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
}
env := phppkg.EnvProduction
@ -64,7 +55,7 @@ func addPHPDeployCommand(parent *cobra.Command) {
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()
@ -77,28 +68,28 @@ func addPHPDeployCommand(parent *cobra.Command) {
status, err := phppkg.Deploy(ctx, opts)
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)
if deployWait {
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 {
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 {
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
},
}
deployCmd.Flags().BoolVar(&deployStaging, "staging", false, "Deploy to staging environment")
deployCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if no changes detected")
deployCmd.Flags().BoolVar(&deployWait, "wait", false, "Wait for deployment to complete")
deployCmd.Flags().BoolVar(&deployStaging, "staging", false, i18n.T("cmd.php.deploy.flag.staging"))
deployCmd.Flags().BoolVar(&deployForce, "force", false, i18n.T("cmd.php.deploy.flag.force"))
deployCmd.Flags().BoolVar(&deployWait, "wait", false, i18n.T("cmd.php.deploy.flag.wait"))
parent.AddCommand(deployCmd)
}
@ -111,16 +102,12 @@ var (
func addPHPDeployStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{
Use: "deploy:status",
Short: "Show deployment status",
Long: "Show the status of a deployment.\n\n" +
"Examples:\n" +
" core php deploy:status # Latest production deployment\n" +
" core php deploy:status --staging # Latest staging deployment\n" +
" core php deploy:status --id abc123 # Specific deployment",
Short: i18n.T("cmd.php.deploy_status.short"),
Long: i18n.T("cmd.php.deploy_status.long"),
RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
}
env := phppkg.EnvProduction
@ -128,7 +115,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
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()
@ -140,7 +127,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
status, err := phppkg.DeployStatus(ctx, opts)
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)
@ -149,8 +136,8 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
},
}
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, "Check staging environment")
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", "Specific deployment ID")
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, i18n.T("cmd.php.deploy_status.flag.staging"))
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", i18n.T("cmd.php.deploy_status.flag.id"))
parent.AddCommand(statusCmd)
}
@ -164,18 +151,12 @@ var (
func addPHPDeployRollbackCommand(parent *cobra.Command) {
rollbackCmd := &cobra.Command{
Use: "deploy:rollback",
Short: "Rollback to previous deployment",
Long: "Rollback to a previous deployment.\n\n" +
"If no deployment ID is specified, rolls back to the most recent\n" +
"successful deployment.\n\n" +
"Examples:\n" +
" core php deploy:rollback # Rollback to previous\n" +
" core php deploy:rollback --staging # Rollback staging\n" +
" core php deploy:rollback --id abc123 # Rollback to specific deployment",
Short: i18n.T("cmd.php.deploy_rollback.short"),
Long: i18n.T("cmd.php.deploy_rollback.long"),
RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
}
env := phppkg.EnvProduction
@ -183,7 +164,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
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()
@ -196,28 +177,28 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
status, err := phppkg.Rollback(ctx, opts)
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)
if rollbackWait {
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 {
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 {
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
},
}
rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, "Rollback staging environment")
rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", "Specific deployment ID to rollback to")
rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, "Wait for rollback to complete")
rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, i18n.T("cmd.php.deploy_rollback.flag.staging"))
rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", i18n.T("cmd.php.deploy_rollback.flag.id"))
rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, i18n.T("cmd.php.deploy_rollback.flag.wait"))
parent.AddCommand(rollbackCmd)
}
@ -230,16 +211,12 @@ var (
func addPHPDeployListCommand(parent *cobra.Command) {
listCmd := &cobra.Command{
Use: "deploy:list",
Short: "List recent deployments",
Long: "List recent deployments.\n\n" +
"Examples:\n" +
" core php deploy:list # List production deployments\n" +
" core php deploy:list --staging # List staging deployments\n" +
" core php deploy:list --limit 20 # List more deployments",
Short: i18n.T("cmd.php.deploy_list.short"),
Long: i18n.T("cmd.php.deploy_list.long"),
RunE: func(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
}
env := phppkg.EnvProduction
@ -252,17 +229,17 @@ func addPHPDeployListCommand(parent *cobra.Command) {
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()
deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit)
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 {
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
}
@ -274,8 +251,8 @@ func addPHPDeployListCommand(parent *cobra.Command) {
},
}
listCmd.Flags().BoolVar(&deployListStaging, "staging", false, "List staging deployments")
listCmd.Flags().IntVar(&deployListLimit, "limit", 0, "Number of deployments to list (default: 10)")
listCmd.Flags().BoolVar(&deployListStaging, "staging", false, i18n.T("cmd.php.deploy_list.flag.staging"))
listCmd.Flags().IntVar(&deployListLimit, "limit", 0, i18n.T("cmd.php.deploy_list.flag.limit"))
parent.AddCommand(listCmd)
}
@ -290,18 +267,18 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
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 != "" {
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 != "" {
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 != "" {
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 != "" {
@ -309,26 +286,26 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
if len(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 != "" {
// Truncate long messages
msg := status.CommitMessage
if len(msg) > 60 {
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() {
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() {
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() {
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 {
case duration < time.Minute:
return "just now"
return i18n.T("cli.time.just_now")
case duration < time.Hour:
mins := int(duration.Minutes())
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:
hours := int(duration.Hours())
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:
days := int(duration.Hours() / 24)
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"
"github.com/charmbracelet/lipgloss"
"github.com/host-uk/core/pkg/i18n"
phppkg "github.com/host-uk/core/pkg/php"
"github.com/spf13/cobra"
)
@ -28,13 +29,8 @@ var (
func addPHPDevCommand(parent *cobra.Command) {
devCmd := &cobra.Command{
Use: "dev",
Short: "Start Laravel development environment",
Long: "Starts all detected Laravel services.\n\n" +
"Auto-detects:\n" +
" - Vite (vite.config.js/ts)\n" +
" - Horizon (config/horizon.php)\n" +
" - Reverb (config/reverb.php)\n" +
" - Redis (from .env)",
Short: i18n.T("cmd.php.dev.short"),
Long: i18n.T("cmd.php.dev.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPDev(phpDevOptions{
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(&devNoHorizon, "no-horizon", false, "Skip Laravel Horizon")
devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, "Skip Laravel Reverb")
devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, "Skip Redis server")
devCmd.Flags().BoolVar(&devHTTPS, "https", false, "Enable HTTPS with mkcert")
devCmd.Flags().StringVar(&devDomain, "domain", "", "Domain for SSL certificate (default: from APP_URL or localhost)")
devCmd.Flags().IntVar(&devPort, "port", 0, "FrankenPHP port (default: 8000)")
devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, i18n.T("cmd.php.dev.flag.no_vite"))
devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, i18n.T("cmd.php.dev.flag.no_horizon"))
devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, i18n.T("cmd.php.dev.flag.no_reverb"))
devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, i18n.T("cmd.php.dev.flag.no_redis"))
devCmd.Flags().BoolVar(&devHTTPS, "https", false, i18n.T("cmd.php.dev.flag.https"))
devCmd.Flags().StringVar(&devDomain, "domain", "", i18n.T("cmd.php.dev.flag.domain"))
devCmd.Flags().IntVar(&devPort, "port", 0, i18n.T("cmd.php.dev.flag.port"))
parent.AddCommand(devCmd)
}
@ -77,7 +73,7 @@ func runPHPDev(opts phpDevOptions) error {
// Check if this is a Laravel project
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
@ -86,11 +82,11 @@ func runPHPDev(opts phpDevOptions) error {
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
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 {
fmt.Printf(" %s %s\n", successStyle.Render("*"), svc)
}
@ -125,16 +121,16 @@ func runPHPDev(opts phpDevOptions) error {
go func() {
<-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()
}()
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
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())
fmt.Println()
@ -147,19 +143,19 @@ func runPHPDev(opts phpDevOptions) error {
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
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
logsReader, err := server.Logs("", true)
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 {
defer logsReader.Close()
@ -178,10 +174,10 @@ func runPHPDev(opts phpDevOptions) error {
shutdown:
// Stop services
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
}
@ -193,16 +189,15 @@ var (
func addPHPLogsCommand(parent *cobra.Command) {
logsCmd := &cobra.Command{
Use: "logs",
Short: "View service logs",
Long: "Stream logs from Laravel services.\n\n" +
"Services: frankenphp, vite, horizon, reverb, redis",
Short: i18n.T("cmd.php.logs.short"),
Long: i18n.T("cmd.php.logs.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPLogs(logsService, logsFollow)
},
}
logsCmd.Flags().BoolVar(&logsFollow, "follow", false, "Follow log output")
logsCmd.Flags().StringVar(&logsService, "service", "", "Specific service (default: all)")
logsCmd.Flags().BoolVar(&logsFollow, "follow", false, i18n.T("cmd.php.logs.flag.follow"))
logsCmd.Flags().StringVar(&logsService, "service", "", i18n.T("cmd.php.logs.flag.service"))
parent.AddCommand(logsCmd)
}
@ -214,7 +209,7 @@ func runPHPLogs(service string, follow bool) error {
}
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
@ -222,7 +217,7 @@ func runPHPLogs(service string, follow bool) error {
logsReader, err := server.Logs(service, follow)
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()
@ -254,7 +249,7 @@ func runPHPLogs(service string, follow bool) error {
func addPHPStopCommand(parent *cobra.Command) {
stopCmd := &cobra.Command{
Use: "stop",
Short: "Stop all Laravel services",
Short: i18n.T("cmd.php.stop.short"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStop()
},
@ -269,23 +264,23 @@ func runPHPStop() error {
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
// This is a simplified version - in practice you'd want to track PIDs
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd})
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
}
func addPHPStatusCommand(parent *cobra.Command) {
statusCmd := &cobra.Command{
Use: "status",
Short: "Show service status",
Short: i18n.T("cmd.php.status.short"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPHPStatus()
},
@ -301,7 +296,7 @@ func runPHPStatus() error {
}
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)
@ -309,11 +304,11 @@ func runPHPStatus() error {
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
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 {
style := getServiceStyle(string(svc))
fmt.Printf(" %s %s\n", style.Render("*"), svc)
@ -322,11 +317,11 @@ func runPHPStatus() error {
// Package manager
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
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
@ -334,9 +329,9 @@ func runPHPStatus() error {
if appURL != "" {
domain := phppkg.ExtractDomainFromURL(appURL)
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 {
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) {
sslCmd := &cobra.Command{
Use: "ssl",
Short: "Setup SSL certificates with mkcert",
Short: i18n.T("cmd.php.ssl.short"),
RunE: func(cmd *cobra.Command, args []string) error {
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)
}
@ -378,35 +373,35 @@ func runPHPSSL(domain string) error {
// Check if mkcert is installed
if !phppkg.IsMkcertInstalled() {
fmt.Printf("%s mkcert is not installed\n", errorStyle.Render("Error:"))
fmt.Println("\nInstall with:")
fmt.Println(" macOS: brew install mkcert")
fmt.Println(" Linux: see https://github.com/FiloSottile/mkcert")
return fmt.Errorf("mkcert not installed")
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed"))
fmt.Printf("\n%s\n", i18n.T("cmd.php.ssl.install_with"))
fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_macos"))
fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux"))
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
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{})
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile)
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
return nil
}
// Setup SSL
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{})
fmt.Printf("%s SSL certificates created\n", successStyle.Render("Done:"))
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile)
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile)
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(i18n.T("cmd.php.ssl.cert_label")), certFile)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
return nil
}
@ -419,17 +414,17 @@ func printServiceStatuses(statuses []phppkg.ServiceStatus) {
var statusText string
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 {
statusText = phpStatusRunning.Render("running")
statusText = phpStatusRunning.Render(i18n.T("cmd.php.status.running"))
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 {
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 {
statusText = phpStatusStopped.Render("stopped")
statusText = phpStatusStopped.Render(i18n.T("cmd.php.status.stopped"))
}
fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText)

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -20,22 +21,18 @@ var (
func addPkgInstallCommand(parent *cobra.Command) {
installCmd := &cobra.Command{
Use: "install <org/repo>",
Short: "Clone a package from GitHub",
Long: "Clones a repository from GitHub.\n\n" +
"Examples:\n" +
" core pkg install host-uk/core-php\n" +
" core pkg install host-uk/core-tenant --dir ./packages\n" +
" core pkg install host-uk/core-admin --add",
Short: i18n.T("cmd.pkg.install.short"),
Long: i18n.T("cmd.pkg.install.long"),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)")
return fmt.Errorf(i18n.T("cmd.pkg.error.repo_required"))
}
return runPkgInstall(args[0], installTargetDir, installAddToReg)
},
}
installCmd.Flags().StringVar(&installTargetDir, "dir", "", "Target directory (default: ./packages or current dir)")
installCmd.Flags().BoolVar(&installAddToReg, "add", false, "Add to repos.yaml registry")
installCmd.Flags().StringVar(&installTargetDir, "dir", "", i18n.T("cmd.pkg.install.flag.dir"))
installCmd.Flags().BoolVar(&installAddToReg, "add", false, i18n.T("cmd.pkg.install.flag.add"))
parent.AddCommand(installCmd)
}
@ -46,7 +43,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
// Parse org/repo
parts := strings.Split(repoArg, "/")
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]
@ -76,19 +73,19 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
repoPath := filepath.Join(targetDir, repoName)
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
}
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\n", dimStyle.Render("Target:"), repoPath)
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.install.target_label")), repoPath)
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)
if err != nil {
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
@ -98,14 +95,14 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
if addToRegistry {
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 {
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.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
}
@ -113,7 +110,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
func addToRegistryFile(org, repoName string) error {
regPath, err := repos.FindRegistry()
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)

View file

@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -15,11 +16,8 @@ import (
func addPkgListCommand(parent *cobra.Command) {
listCmd := &cobra.Command{
Use: "list",
Short: "List installed packages",
Long: "Lists all packages in the current workspace.\n\n" +
"Reads from repos.yaml or scans for git repositories.\n\n" +
"Examples:\n" +
" core pkg list",
Short: i18n.T("cmd.pkg.list.short"),
Long: i18n.T("cmd.pkg.list.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPkgList()
},
@ -31,12 +29,12 @@ func addPkgListCommand(parent *cobra.Command) {
func runPkgList() error {
regPath, err := repos.FindRegistry()
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)
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
@ -49,11 +47,11 @@ func runPkgList() error {
allRepos := reg.List()
if len(allRepos) == 0 {
fmt.Println("No packages in registry.")
fmt.Println(i18n.T("cmd.pkg.list.no_packages"))
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
for _, r := range allRepos {
@ -76,7 +74,7 @@ func runPkgList() error {
desc = desc[:37] + "..."
}
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))
@ -84,10 +82,10 @@ func runPkgList() error {
}
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 {
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
@ -99,20 +97,17 @@ var updateAll bool
func addPkgUpdateCommand(parent *cobra.Command) {
updateCmd := &cobra.Command{
Use: "update [packages...]",
Short: "Update installed packages",
Long: "Pulls latest changes for installed packages.\n\n" +
"Examples:\n" +
" core pkg update core-php # Update specific package\n" +
" core pkg update --all # Update all packages",
Short: i18n.T("cmd.pkg.update.short"),
Long: i18n.T("cmd.pkg.update.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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)
},
}
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)
}
@ -120,12 +115,12 @@ func addPkgUpdateCommand(parent *cobra.Command) {
func runPkgUpdate(packages []string, all bool) error {
regPath, err := repos.FindRegistry()
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)
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
@ -145,14 +140,14 @@ func runPkgUpdate(packages []string, all bool) error {
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
for _, name := range toUpdate {
repoPath := filepath.Join(basePath, name)
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++
continue
}
@ -169,7 +164,7 @@ func runPkgUpdate(packages []string, all bool) error {
}
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 {
fmt.Printf("%s\n", successStyle.Render("✓"))
}
@ -177,8 +172,8 @@ func runPkgUpdate(packages []string, all bool) error {
}
fmt.Println()
fmt.Printf("%s %d updated, %d skipped, %d failed\n",
dimStyle.Render("Done:"), updated, skipped, failed)
fmt.Printf("%s %s\n",
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
}
@ -187,10 +182,8 @@ func runPkgUpdate(packages []string, all bool) error {
func addPkgOutdatedCommand(parent *cobra.Command) {
outdatedCmd := &cobra.Command{
Use: "outdated",
Short: "Check for outdated packages",
Long: "Checks which packages have unpulled commits.\n\n" +
"Examples:\n" +
" core pkg outdated",
Short: i18n.T("cmd.pkg.outdated.short"),
Long: i18n.T("cmd.pkg.outdated.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return runPkgOutdated()
},
@ -202,12 +195,12 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
func runPkgOutdated() error {
regPath, err := repos.FindRegistry()
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)
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
@ -218,7 +211,7 @@ func runPkgOutdated() error {
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
@ -242,8 +235,8 @@ func runPkgOutdated() error {
count := strings.TrimSpace(string(output))
if count != "0" {
fmt.Printf(" %s %s (%s commits behind)\n",
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), count)
fmt.Printf(" %s %s (%s)\n",
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
outdated++
} else {
upToDate++
@ -252,11 +245,11 @@ func runPkgOutdated() error {
fmt.Println()
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 {
fmt.Printf("%s %d outdated, %d up to date\n",
dimStyle.Render("Summary:"), outdated, upToDate)
fmt.Printf("\nUpdate with: %s\n", dimStyle.Render("core pkg update --all"))
fmt.Printf("%s %s\n",
dimStyle.Render(i18n.T("cmd.pkg.outdated.summary_label")), i18n.T("cmd.pkg.outdated.summary", map[string]int{"Outdated": outdated, "UpToDate": upToDate}))
fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.outdated.update_with"), dimStyle.Render("core pkg update --all"))
}
return nil

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/host-uk/core/pkg/cache"
"github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos"
"github.com/spf13/cobra"
)
@ -27,14 +28,8 @@ var (
func addPkgSearchCommand(parent *cobra.Command) {
searchCmd := &cobra.Command{
Use: "search",
Short: "Search GitHub for packages",
Long: "Searches GitHub for repositories matching a pattern.\n" +
"Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" +
"Examples:\n" +
" core pkg search # List all host-uk repos\n" +
" core pkg search --pattern 'core-*' # Search for core-* repos\n" +
" core pkg search --org mycompany # Search different org\n" +
" core pkg search --refresh # Bypass cache",
Short: i18n.T("cmd.pkg.search.short"),
Long: i18n.T("cmd.pkg.search.long"),
RunE: func(cmd *cobra.Command, args []string) error {
org := searchOrg
pattern := searchPattern
@ -52,11 +47,11 @@ func addPkgSearchCommand(parent *cobra.Command) {
},
}
searchCmd.Flags().StringVar(&searchOrg, "org", "", "GitHub organization (default: host-uk)")
searchCmd.Flags().StringVar(&searchPattern, "pattern", "", "Repo name pattern (* for wildcard)")
searchCmd.Flags().StringVar(&searchType, "type", "", "Filter by type in name (mod, services, plug, website)")
searchCmd.Flags().IntVar(&searchLimit, "limit", 0, "Max results (default 50)")
searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, "Bypass cache and fetch fresh data")
searchCmd.Flags().StringVar(&searchOrg, "org", "", i18n.T("cmd.pkg.search.flag.org"))
searchCmd.Flags().StringVar(&searchPattern, "pattern", "", i18n.T("cmd.pkg.search.flag.pattern"))
searchCmd.Flags().StringVar(&searchType, "type", "", i18n.T("cmd.pkg.search.flag.type"))
searchCmd.Flags().IntVar(&searchLimit, "limit", 0, i18n.T("cmd.pkg.search.flag.limit"))
searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, i18n.T("cmd.pkg.search.flag.refresh"))
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 {
fromCache = true
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
if !fromCache {
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") != "" {
fmt.Printf("%s GH_TOKEN env var is set - this may cause auth issues\n", dimStyle.Render("Note:"))
fmt.Printf("%s Unset it with: unset GH_TOKEN\n\n", dimStyle.Render(""))
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 %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,
"--json", "name,description,visibility,updatedAt,primaryLanguage",
@ -117,13 +112,13 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
fmt.Println()
errStr := strings.TrimSpace(string(output))
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 {
return fmt.Errorf("failed to parse results: %w", err)
return fmt.Errorf(i18n.T("cmd.pkg.error.parse_results"), err)
}
if c != nil {
@ -146,7 +141,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
}
if len(filtered) == 0 {
fmt.Println("No repositories found matching pattern.")
fmt.Println(i18n.T("cmd.pkg.search.no_repos_found"))
return nil
}
@ -154,12 +149,12 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
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 {
visibility := ""
if r.Visibility == "private" {
visibility = dimStyle.Render(" [private]")
visibility = dimStyle.Render(" " + i18n.T("cmd.pkg.search.private_label"))
}
desc := r.Description
@ -167,7 +162,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
desc = desc[:47] + "..."
}
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)
@ -175,7 +170,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
}
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
}

View file

@ -6,6 +6,7 @@ import (
"os"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
sdkpkg "github.com/host-uk/core/pkg/sdk"
"github.com/spf13/cobra"
)
@ -20,13 +21,8 @@ var (
var sdkCmd = &cobra.Command{
Use: "sdk",
Short: "SDK validation and API compatibility tools",
Long: `Tools for validating OpenAPI specs and checking API compatibility.
To generate SDKs, use: core build sdk
Commands:
diff Check for breaking API changes
validate Validate OpenAPI spec syntax`,
Short: i18n.T("cmd.sdk.short"),
Long: i18n.T("cmd.sdk.long"),
}
var diffBasePath string
@ -34,7 +30,8 @@ var diffSpecPath string
var sdkDiffCmd = &cobra.Command{
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 {
return runSDKDiff(diffBasePath, diffSpecPath)
},
@ -44,7 +41,8 @@ var validateSpecPath string
var sdkValidateCmd = &cobra.Command{
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 {
return runSDKValidate(validateSpecPath)
},
@ -52,11 +50,11 @@ var sdkValidateCmd = &cobra.Command{
func init() {
// sdk diff flags
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", "Base spec (version tag or file)")
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", "Current spec file")
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base"))
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec"))
// 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
sdkCmd.AddCommand(sdkDiffCmd)
@ -66,7 +64,7 @@ func init() {
func runSDKDiff(basePath, specPath string) error {
projectDir, err := os.Getwd()
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
@ -79,49 +77,49 @@ func runSDKDiff(basePath, specPath string) error {
}
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(" Base: %s\n", sdkDimStyle.Render(basePath))
fmt.Printf(" Current: %s\n", sdkDimStyle.Render(specPath))
fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("cmd.sdk.diff.checking"))
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.base_label"), sdkDimStyle.Render(basePath))
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.current_label"), sdkDimStyle.Render(specPath))
fmt.Println()
result, err := sdkpkg.Diff(basePath, specPath)
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)
}
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 {
fmt.Printf(" - %s\n", change)
}
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
}
func runSDKValidate(specPath string) error {
projectDir, err := os.Getwd()
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})
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()
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
}
fmt.Printf(" Spec: %s\n", sdkDimStyle.Render(detectedPath))
fmt.Printf("%s Spec is valid\n", sdkSuccessStyle.Render("OK:"))
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.validate.spec_label"), sdkDimStyle.Render(detectedPath))
fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), i18n.T("cmd.sdk.validate.valid"))
return nil
}

View file

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

View file

@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"github.com/host-uk/core/pkg/i18n"
"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)
}
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
@ -58,7 +59,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
if empty {
// Clone into current directory
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 {
// Directory has content - check if it's a git repo root
isRepo := isGitRepoRoot(cwd)
@ -90,7 +91,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
}
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 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
devopsPath := filepath.Join(targetDir, devopsRepo)
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 err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil {
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 {
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 {
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
registryPath := filepath.Join(devopsPath, devopsReposYaml)
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
}

View file

@ -14,6 +14,7 @@ import (
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
"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.
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("Org:"), reg.Org)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.registry_label")), registryPath)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.org_label")), reg.Org)
// Determine base path for cloning
basePath := reg.BasePath
@ -47,7 +48,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
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
var typeFilter []string
@ -55,7 +56,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
for _, t := range strings.Split(only, ",") {
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
@ -136,15 +137,18 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
// Summary
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 {
fmt.Println("\nNothing to clone.")
fmt.Printf("\n%s\n", i18n.T("cmd.setup.nothing_to_clone"))
return nil
}
if dryRun {
fmt.Println("\nWould clone:")
fmt.Printf("\n%s\n", i18n.T("cmd.setup.would_clone_list"))
for _, repo := range toClone {
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
}
if !confirmed {
fmt.Println("Cancelled.")
fmt.Println(i18n.T("cmd.setup.cancelled"))
return nil
}
}
@ -168,7 +172,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
var succeeded, failed int
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)
@ -177,32 +181,32 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
fmt.Printf("%s\n", errorStyle.Render("x "+err.Error()))
failed++
} else {
fmt.Printf("%s\n", successStyle.Render("done"))
fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.setup.done")))
succeeded++
}
}
// Summary
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 {
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 {
fmt.Printf(", %d already exist", exists)
fmt.Printf(", %s", i18n.T("cmd.setup.already_exist_count", map[string]interface{}{"Count": exists}))
}
fmt.Println()
// Run build if requested
if runBuild && succeeded > 0 {
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.Dir = basePath
buildCmd.Stdout = os.Stdout
buildCmd.Stderr = os.Stderr
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"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n"
)
// runRepoSetup sets up the current repository with .core/ configuration.
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
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
coreDir := filepath.Join(repoPath, ".core")
@ -39,7 +41,7 @@ func runRepoSetup(repoPath string, dryRun bool) error {
}
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 {
fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename))
// 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 {
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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import (
"strings"
"github.com/host-uk/core/cmd/shared"
"github.com/host-uk/core/pkg/i18n"
)
type packageCoverage struct {
@ -84,19 +85,19 @@ func printTestSummary(results testResults, showCoverage bool) {
// Print pass/fail summary
total := results.passed + results.failed
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 {
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 {
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()
}
// Print failed packages
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 {
fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg)
}
@ -107,7 +108,7 @@ func printTestSummary(results testResults, showCoverage bool) {
printCoverageSummary(results)
} else if results.covCount > 0 {
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
}
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.Slice(results.packages, func(i, j int) bool {
@ -145,8 +146,9 @@ func printCoverageSummary(results testResults) {
// Print average
if results.covCount > 0 {
avgCov := results.totalCov / float64(results.covCount)
padding := strings.Repeat(" ", maxLen-7+2)
fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render("Average"), padding, formatCoverage(avgCov))
avgLabel := i18n.T("cmd.test.label.average")
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"
"runtime"
"strings"
"github.com/host-uk/core/pkg/i18n"
)
func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error {
// Detect if we're in a Go project
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
@ -52,10 +54,10 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
cmd.Env = append(os.Environ(), getMacOSDeploymentTarget())
if !jsonOutput {
fmt.Printf("%s Running tests\n", testHeaderStyle.Render("Test:"))
fmt.Printf(" Package: %s\n", testDimStyle.Render(pkg))
fmt.Printf("%s %s\n", testHeaderStyle.Render(i18n.T("cmd.test.label.test")), i18n.T("cmd.test.running"))
fmt.Printf(" %s %s\n", i18n.T("cmd.test.label.package"), testDimStyle.Render(pkg))
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()
}
@ -91,7 +93,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
// JSON output for CI/agents
printJSONResults(results, exitCode)
if exitCode != 0 {
return fmt.Errorf("tests failed")
return fmt.Errorf(i18n.T("cmd.test.error.tests_failed"))
}
return nil
}
@ -106,11 +108,11 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
}
if exitCode != 0 {
fmt.Printf("\n%s Tests failed\n", testFailStyle.Render("FAIL"))
return fmt.Errorf("tests failed")
fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.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
}

View file

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

View file

@ -10,6 +10,7 @@ import (
"text/tabwriter"
"github.com/host-uk/core/pkg/container"
"github.com/host-uk/core/pkg/i18n"
"github.com/spf13/cobra"
)
@ -17,14 +18,8 @@ import (
func addVMTemplatesCommand(parent *cobra.Command) {
templatesCmd := &cobra.Command{
Use: "templates",
Short: "Manage LinuxKit templates",
Long: "Manage LinuxKit YAML templates for building VMs.\n\n" +
"Templates provide pre-configured LinuxKit configurations for common use cases.\n" +
"They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" +
"Examples:\n" +
" core vm templates # List available templates\n" +
" core vm templates show core-dev # Show template content\n" +
" core vm templates vars server-php # Show template variables",
Short: i18n.T("cmd.vm.templates.short"),
Long: i18n.T("cmd.vm.templates.long"),
RunE: func(cmd *cobra.Command, args []string) error {
return listTemplates()
},
@ -41,14 +36,11 @@ func addVMTemplatesCommand(parent *cobra.Command) {
func addTemplatesShowCommand(parent *cobra.Command) {
showCmd := &cobra.Command{
Use: "show <template-name>",
Short: "Display template content",
Long: "Display the content of a LinuxKit template.\n\n" +
"Examples:\n" +
" core templates show core-dev\n" +
" core templates show server-php",
Short: i18n.T("cmd.vm.templates.show.short"),
Long: i18n.T("cmd.vm.templates.show.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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])
},
@ -61,15 +53,11 @@ func addTemplatesShowCommand(parent *cobra.Command) {
func addTemplatesVarsCommand(parent *cobra.Command) {
varsCmd := &cobra.Command{
Use: "vars <template-name>",
Short: "Show template variables",
Long: "Display all variables used in a template.\n\n" +
"Shows required variables (no default) and optional variables (with defaults).\n\n" +
"Examples:\n" +
" core templates vars core-dev\n" +
" core templates vars server-php",
Short: i18n.T("cmd.vm.templates.vars.short"),
Long: i18n.T("cmd.vm.templates.vars.long"),
RunE: func(cmd *cobra.Command, args []string) error {
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])
},
@ -82,14 +70,14 @@ func listTemplates() error {
templates := container.ListTemplates()
if len(templates) == 0 {
fmt.Println("No templates available.")
fmt.Println(i18n.T("cmd.vm.templates.no_templates"))
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)
fmt.Fprintln(w, "NAME\tDESCRIPTION")
fmt.Fprintln(w, i18n.T("cmd.vm.templates.header"))
fmt.Fprintln(w, "----\t-----------")
for _, tmpl := range templates {
@ -102,9 +90,9 @@ func listTemplates() error {
w.Flush()
fmt.Println()
fmt.Printf("Show template: %s\n", dimStyle.Render("core vm templates show <name>"))
fmt.Printf("Show variables: %s\n", 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.show"), dimStyle.Render("core vm templates show <name>"))
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars <name>"))
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
return nil
}
@ -115,7 +103,7 @@ func showTemplate(name string) error {
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)
return nil
@ -129,10 +117,10 @@ func showTemplateVars(name string) error {
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 {
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 {
fmt.Printf(" %s\n", varStyle.Render("${"+v+"}"))
}
@ -140,7 +128,7 @@ func showTemplateVars(name string) error {
}
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 {
fmt.Printf(" %s = %s\n",
varStyle.Render("${"+v+"}"),
@ -150,7 +138,7 @@ func showTemplateVars(name string) error {
}
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
@ -161,63 +149,63 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
// Apply template with variables
content, err := container.ApplyTemplate(templateName, vars)
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
tmpDir, err := os.MkdirTemp("", "core-linuxkit-*")
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)
// Write the YAML file
yamlPath := filepath.Join(tmpDir, templateName+".yml")
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("Building:"), yamlPath)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(templateName))
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath)
// Build the image using linuxkit
outputPath := filepath.Join(tmpDir, templateName)
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)
imagePath := findBuiltImage(outputPath)
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()
// Run the image
manager, err := container.NewLinuxKitManager()
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()
ctx := context.Background()
c, err := manager.Run(ctx, imagePath, runOpts)
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 {
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID)
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID)
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.vm.label.started")), c.ID)
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
fmt.Println()
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8])
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8])
fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]interface{}{"ID": c.ID[:8]}))
fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]interface{}{"ID": c.ID[:8]}))
} 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
@ -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.

View file

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

File diff suppressed because it is too large Load diff