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:
parent
0c3bccfceb
commit
e8e48127c2
61 changed files with 2549 additions and 1733 deletions
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/agentic"
|
"github.com/host-uk/core/pkg/agentic"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,30 +33,19 @@ var (
|
||||||
|
|
||||||
var taskCommitCmd = &cobra.Command{
|
var taskCommitCmd = &cobra.Command{
|
||||||
Use: "task:commit [task-id]",
|
Use: "task:commit [task-id]",
|
||||||
Short: "Auto-commit changes with task reference",
|
Short: i18n.T("cmd.ai.task_commit.short"),
|
||||||
Long: `Creates a git commit with a task reference and co-author attribution.
|
Long: i18n.T("cmd.ai.task_commit.long"),
|
||||||
|
|
||||||
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),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
taskID := args[0]
|
taskID := args[0]
|
||||||
|
|
||||||
if taskCommitMessage == "" {
|
if taskCommitMessage == "" {
|
||||||
return fmt.Errorf("commit message required (--message or -m)")
|
return fmt.Errorf(i18n.T("cmd.ai.task_commit.message_required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -66,7 +56,7 @@ Examples:
|
||||||
// Get task details
|
// Get task details
|
||||||
task, err := client.GetTask(ctx, taskID)
|
task, err := client.GetTask(ctx, taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build commit message with optional scope
|
// Build commit message with optional scope
|
||||||
|
|
@ -81,35 +71,35 @@ Examples:
|
||||||
// Get current directory
|
// Get current directory
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for uncommitted changes
|
// Check for uncommitted changes
|
||||||
hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd)
|
hasChanges, err := agentic.HasUncommittedChanges(ctx, cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check git status: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.git_status"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasChanges {
|
if !hasChanges {
|
||||||
fmt.Println("No uncommitted changes to commit.")
|
fmt.Println(i18n.T("cmd.ai.task_commit.no_changes"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create commit
|
// Create commit
|
||||||
fmt.Printf("%s Creating commit for task %s...\n", dimStyle.Render(">>"), taskID)
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_commit.creating", map[string]interface{}{"ID": taskID}))
|
||||||
if err := agentic.AutoCommit(ctx, task, cwd, fullMessage); err != nil {
|
if err := agentic.AutoCommit(ctx, task, cwd, fullMessage); err != nil {
|
||||||
return fmt.Errorf("failed to commit: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.commit"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Committed: %s\n", successStyle.Render(">>"), fullMessage)
|
fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_commit.committed"), fullMessage)
|
||||||
|
|
||||||
// Push if requested
|
// Push if requested
|
||||||
if taskCommitPush {
|
if taskCommitPush {
|
||||||
fmt.Printf("%s Pushing changes...\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_commit.pushing"))
|
||||||
if err := agentic.PushChanges(ctx, cwd); err != nil {
|
if err := agentic.PushChanges(ctx, cwd); err != nil {
|
||||||
return fmt.Errorf("failed to push: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.push"), err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s Changes pushed successfully\n", successStyle.Render(">>"))
|
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_commit.pushed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -118,23 +108,15 @@ Examples:
|
||||||
|
|
||||||
var taskPRCmd = &cobra.Command{
|
var taskPRCmd = &cobra.Command{
|
||||||
Use: "task:pr [task-id]",
|
Use: "task:pr [task-id]",
|
||||||
Short: "Create a pull request for a task",
|
Short: i18n.T("cmd.ai.task_pr.short"),
|
||||||
Long: `Creates a GitHub pull request linked to a task.
|
Long: i18n.T("cmd.ai.task_pr.long"),
|
||||||
|
|
||||||
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),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
taskID := args[0]
|
taskID := args[0]
|
||||||
|
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -145,31 +127,31 @@ Examples:
|
||||||
// Get task details
|
// Get task details
|
||||||
task, err := client.GetTask(ctx, taskID)
|
task, err := client.GetTask(ctx, taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current directory
|
// Get current directory
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check current branch
|
// Check current branch
|
||||||
branch, err := agentic.GetCurrentBranch(ctx, cwd)
|
branch, err := agentic.GetCurrentBranch(ctx, cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get current branch: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_branch"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if branch == "main" || branch == "master" {
|
if branch == "main" || branch == "master" {
|
||||||
return fmt.Errorf("cannot create PR from %s branch; create a feature branch first", branch)
|
return fmt.Errorf(i18n.T("cmd.ai.task_pr.branch_error", map[string]interface{}{"Branch": branch}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push current branch
|
// Push current branch
|
||||||
fmt.Printf("%s Pushing branch %s...\n", dimStyle.Render(">>"), branch)
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_pr.pushing_branch", map[string]interface{}{"Branch": branch}))
|
||||||
if err := agentic.PushChanges(ctx, cwd); err != nil {
|
if err := agentic.PushChanges(ctx, cwd); err != nil {
|
||||||
// Try setting upstream
|
// Try setting upstream
|
||||||
if _, err := runGitCommand(cwd, "push", "-u", "origin", branch); err != nil {
|
if _, err := runGitCommand(cwd, "push", "-u", "origin", branch); err != nil {
|
||||||
return fmt.Errorf("failed to push branch: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.push_branch"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,14 +167,14 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create PR
|
// Create PR
|
||||||
fmt.Printf("%s Creating pull request...\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task_pr.creating"))
|
||||||
prURL, err := agentic.CreatePR(ctx, task, cwd, opts)
|
prURL, err := agentic.CreatePR(ctx, task, cwd, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create PR: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.create_pr"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Pull request created!\n", successStyle.Render(">>"))
|
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_pr.created"))
|
||||||
fmt.Printf(" URL: %s\n", prURL)
|
fmt.Printf(" %s %s\n", i18n.T("cmd.ai.label.url"), prURL)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
@ -200,15 +182,15 @@ Examples:
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// task:commit command flags
|
// task:commit command flags
|
||||||
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", "Commit message (without task reference)")
|
taskCommitCmd.Flags().StringVarP(&taskCommitMessage, "message", "m", "", i18n.T("cmd.ai.task_commit.flag.message"))
|
||||||
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", "Scope for the commit type (e.g., auth, api, ui)")
|
taskCommitCmd.Flags().StringVar(&taskCommitScope, "scope", "", i18n.T("cmd.ai.task_commit.flag.scope"))
|
||||||
taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, "Push changes after committing")
|
taskCommitCmd.Flags().BoolVar(&taskCommitPush, "push", false, i18n.T("cmd.ai.task_commit.flag.push"))
|
||||||
|
|
||||||
// task:pr command flags
|
// task:pr command flags
|
||||||
taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", "PR title (defaults to task title)")
|
taskPRCmd.Flags().StringVar(&taskPRTitle, "title", "", i18n.T("cmd.ai.task_pr.flag.title"))
|
||||||
taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, "Create as draft PR")
|
taskPRCmd.Flags().BoolVar(&taskPRDraft, "draft", false, i18n.T("cmd.ai.task_pr.flag.draft"))
|
||||||
taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", "Labels to add (comma-separated)")
|
taskPRCmd.Flags().StringVar(&taskPRLabels, "labels", "", i18n.T("cmd.ai.task_pr.flag.labels"))
|
||||||
taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", "Base branch (defaults to main)")
|
taskPRCmd.Flags().StringVar(&taskPRBase, "base", "", i18n.T("cmd.ai.task_pr.flag.base"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTaskCommitCommand(parent *cobra.Command) {
|
func addTaskCommitCommand(parent *cobra.Command) {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/agentic"
|
"github.com/host-uk/core/pkg/agentic"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,18 +33,8 @@ var (
|
||||||
|
|
||||||
var tasksCmd = &cobra.Command{
|
var tasksCmd = &cobra.Command{
|
||||||
Use: "tasks",
|
Use: "tasks",
|
||||||
Short: "List available tasks from core-agentic",
|
Short: i18n.T("cmd.ai.tasks.short"),
|
||||||
Long: `Lists tasks from the core-agentic service.
|
Long: i18n.T("cmd.ai.tasks.long"),
|
||||||
|
|
||||||
Configuration is loaded from:
|
|
||||||
1. Environment variables (AGENTIC_TOKEN, AGENTIC_BASE_URL)
|
|
||||||
2. .env file in current directory
|
|
||||||
3. ~/.core/agentic.yaml
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core ai tasks
|
|
||||||
core ai tasks --status pending --priority high
|
|
||||||
core ai tasks --labels bug,urgent`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
limit := tasksLimit
|
limit := tasksLimit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
|
|
@ -52,7 +43,7 @@ Examples:
|
||||||
|
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -77,11 +68,11 @@ Examples:
|
||||||
|
|
||||||
tasks, err := client.ListTasks(ctx, opts)
|
tasks, err := client.ListTasks(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list tasks: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.list_tasks"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
fmt.Println("No tasks found.")
|
fmt.Println(i18n.T("cmd.ai.tasks.none_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,18 +83,12 @@ Examples:
|
||||||
|
|
||||||
var taskCmd = &cobra.Command{
|
var taskCmd = &cobra.Command{
|
||||||
Use: "task [task-id]",
|
Use: "task [task-id]",
|
||||||
Short: "Show task details or auto-select a task",
|
Short: i18n.T("cmd.ai.task.short"),
|
||||||
Long: `Shows details of a specific task or auto-selects the highest priority task.
|
Long: i18n.T("cmd.ai.task.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core ai task abc123 # Show task details
|
|
||||||
core ai task abc123 --claim # Show and claim the task
|
|
||||||
core ai task abc123 --context # Show task with gathered context
|
|
||||||
core ai task --auto # Auto-select highest priority pending task`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -126,11 +111,11 @@ Examples:
|
||||||
Limit: 50,
|
Limit: 50,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list tasks: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.list_tasks"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
fmt.Println("No pending tasks available.")
|
fmt.Println(i18n.T("cmd.ai.task.no_pending"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,12 +135,12 @@ Examples:
|
||||||
taskClaim = true // Auto-select implies claiming
|
taskClaim = true // Auto-select implies claiming
|
||||||
} else {
|
} else {
|
||||||
if taskID == "" {
|
if taskID == "" {
|
||||||
return fmt.Errorf("task ID required (or use --auto)")
|
return fmt.Errorf(i18n.T("cmd.ai.task.id_required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err = client.GetTask(ctx, taskID)
|
task, err = client.GetTask(ctx, taskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.get_task"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +149,7 @@ Examples:
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
taskCtx, err := agentic.BuildTaskContext(task, cwd)
|
taskCtx, err := agentic.BuildTaskContext(task, cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s Failed to build context: %s\n", errorStyle.Render(">>"), err)
|
fmt.Printf("%s %s: %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task.context_failed"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(taskCtx.FormatContext())
|
fmt.Println(taskCtx.FormatContext())
|
||||||
}
|
}
|
||||||
|
|
@ -174,15 +159,15 @@ Examples:
|
||||||
|
|
||||||
if taskClaim && task.Status == agentic.StatusPending {
|
if taskClaim && task.Status == agentic.StatusPending {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Claiming task...\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.ai.task.claiming"))
|
||||||
|
|
||||||
claimedTask, err := client.ClaimTask(ctx, task.ID)
|
claimedTask, err := client.ClaimTask(ctx, task.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to claim task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.claim_task"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Task claimed successfully!\n", successStyle.Render(">>"))
|
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task.claimed"))
|
||||||
fmt.Printf(" Status: %s\n", formatTaskStatus(claimedTask.Status))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.ai.label.status"), formatTaskStatus(claimedTask.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -191,16 +176,16 @@ Examples:
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// tasks command flags
|
// tasks command flags
|
||||||
tasksCmd.Flags().StringVar(&tasksStatus, "status", "", "Filter by status (pending, in_progress, completed, blocked)")
|
tasksCmd.Flags().StringVar(&tasksStatus, "status", "", i18n.T("cmd.ai.tasks.flag.status"))
|
||||||
tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", "Filter by priority (critical, high, medium, low)")
|
tasksCmd.Flags().StringVar(&tasksPriority, "priority", "", i18n.T("cmd.ai.tasks.flag.priority"))
|
||||||
tasksCmd.Flags().StringVar(&tasksLabels, "labels", "", "Filter by labels (comma-separated)")
|
tasksCmd.Flags().StringVar(&tasksLabels, "labels", "", i18n.T("cmd.ai.tasks.flag.labels"))
|
||||||
tasksCmd.Flags().IntVar(&tasksLimit, "limit", 20, "Max number of tasks to return")
|
tasksCmd.Flags().IntVar(&tasksLimit, "limit", 20, i18n.T("cmd.ai.tasks.flag.limit"))
|
||||||
tasksCmd.Flags().StringVar(&tasksProject, "project", "", "Filter by project")
|
tasksCmd.Flags().StringVar(&tasksProject, "project", "", i18n.T("cmd.ai.tasks.flag.project"))
|
||||||
|
|
||||||
// task command flags
|
// task command flags
|
||||||
taskCmd.Flags().BoolVar(&taskAutoSelect, "auto", false, "Auto-select highest priority pending task")
|
taskCmd.Flags().BoolVar(&taskAutoSelect, "auto", false, i18n.T("cmd.ai.task.flag.auto"))
|
||||||
taskCmd.Flags().BoolVar(&taskClaim, "claim", false, "Claim the task after showing details")
|
taskCmd.Flags().BoolVar(&taskClaim, "claim", false, i18n.T("cmd.ai.task.flag.claim"))
|
||||||
taskCmd.Flags().BoolVar(&taskShowContext, "context", false, "Show gathered context for AI collaboration")
|
taskCmd.Flags().BoolVar(&taskShowContext, "context", false, i18n.T("cmd.ai.task.flag.context"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTasksCommand(parent *cobra.Command) {
|
func addTasksCommand(parent *cobra.Command) {
|
||||||
|
|
@ -212,7 +197,7 @@ func addTaskCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func printTaskList(tasks []agentic.Task) {
|
func printTaskList(tasks []agentic.Task) {
|
||||||
fmt.Printf("\n%d task(s) found:\n\n", len(tasks))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.ai.tasks.found", map[string]interface{}{"Count": len(tasks)}))
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
id := taskIDStyle.Render(task.ID)
|
id := taskIDStyle.Render(task.ID)
|
||||||
|
|
@ -231,37 +216,37 @@ func printTaskList(tasks []agentic.Task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s\n", dimStyle.Render("Use 'core ai task <id>' to view details"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.tasks.hint")))
|
||||||
}
|
}
|
||||||
|
|
||||||
func printTaskDetails(task *agentic.Task) {
|
func printTaskDetails(task *agentic.Task) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("ID:"), taskIDStyle.Render(task.ID))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.id")), taskIDStyle.Render(task.ID))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Title:"), taskTitleStyle.Render(task.Title))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.title")), taskTitleStyle.Render(task.Title))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Priority:"), formatTaskPriority(task.Priority))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.priority")), formatTaskPriority(task.Priority))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), formatTaskStatus(task.Status))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.status")), formatTaskStatus(task.Status))
|
||||||
|
|
||||||
if task.Project != "" {
|
if task.Project != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Project:"), task.Project)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.project")), task.Project)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(task.Labels) > 0 {
|
if len(task.Labels) > 0 {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Labels:"), taskLabelStyle.Render(strings.Join(task.Labels, ", ")))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.labels")), taskLabelStyle.Render(strings.Join(task.Labels, ", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.ClaimedBy != "" {
|
if task.ClaimedBy != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Claimed by:"), task.ClaimedBy)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.claimed_by")), task.ClaimedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Created:"), formatAge(task.CreatedAt))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.created")), formatAge(task.CreatedAt))
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s\n", dimStyle.Render("Description:"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.description")))
|
||||||
fmt.Println(task.Description)
|
fmt.Println(task.Description)
|
||||||
|
|
||||||
if len(task.Files) > 0 {
|
if len(task.Files) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s\n", dimStyle.Render("Related files:"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.ai.label.related_files")))
|
||||||
for _, f := range task.Files {
|
for _, f := range task.Files {
|
||||||
fmt.Printf(" - %s\n", f)
|
fmt.Printf(" - %s\n", f)
|
||||||
}
|
}
|
||||||
|
|
@ -269,20 +254,20 @@ func printTaskDetails(task *agentic.Task) {
|
||||||
|
|
||||||
if len(task.Dependencies) > 0 {
|
if len(task.Dependencies) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Blocked by:"), strings.Join(task.Dependencies, ", "))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.ai.label.blocked_by")), strings.Join(task.Dependencies, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTaskPriority(p agentic.TaskPriority) string {
|
func formatTaskPriority(p agentic.TaskPriority) string {
|
||||||
switch p {
|
switch p {
|
||||||
case agentic.PriorityCritical:
|
case agentic.PriorityCritical:
|
||||||
return taskPriorityHighStyle.Render("[CRITICAL]")
|
return taskPriorityHighStyle.Render("[" + i18n.T("cmd.ai.priority.critical") + "]")
|
||||||
case agentic.PriorityHigh:
|
case agentic.PriorityHigh:
|
||||||
return taskPriorityHighStyle.Render("[HIGH]")
|
return taskPriorityHighStyle.Render("[" + i18n.T("cmd.ai.priority.high") + "]")
|
||||||
case agentic.PriorityMedium:
|
case agentic.PriorityMedium:
|
||||||
return taskPriorityMediumStyle.Render("[MEDIUM]")
|
return taskPriorityMediumStyle.Render("[" + i18n.T("cmd.ai.priority.medium") + "]")
|
||||||
case agentic.PriorityLow:
|
case agentic.PriorityLow:
|
||||||
return taskPriorityLowStyle.Render("[LOW]")
|
return taskPriorityLowStyle.Render("[" + i18n.T("cmd.ai.priority.low") + "]")
|
||||||
default:
|
default:
|
||||||
return dimStyle.Render("[" + string(p) + "]")
|
return dimStyle.Render("[" + string(p) + "]")
|
||||||
}
|
}
|
||||||
|
|
@ -291,13 +276,13 @@ func formatTaskPriority(p agentic.TaskPriority) string {
|
||||||
func formatTaskStatus(s agentic.TaskStatus) string {
|
func formatTaskStatus(s agentic.TaskStatus) string {
|
||||||
switch s {
|
switch s {
|
||||||
case agentic.StatusPending:
|
case agentic.StatusPending:
|
||||||
return taskStatusPendingStyle.Render("pending")
|
return taskStatusPendingStyle.Render(i18n.T("cmd.ai.status.pending"))
|
||||||
case agentic.StatusInProgress:
|
case agentic.StatusInProgress:
|
||||||
return taskStatusInProgressStyle.Render("in_progress")
|
return taskStatusInProgressStyle.Render(i18n.T("cmd.ai.status.in_progress"))
|
||||||
case agentic.StatusCompleted:
|
case agentic.StatusCompleted:
|
||||||
return taskStatusCompletedStyle.Render("completed")
|
return taskStatusCompletedStyle.Render(i18n.T("cmd.ai.status.completed"))
|
||||||
case agentic.StatusBlocked:
|
case agentic.StatusBlocked:
|
||||||
return taskStatusBlockedStyle.Render("blocked")
|
return taskStatusBlockedStyle.Render(i18n.T("cmd.ai.status.blocked"))
|
||||||
default:
|
default:
|
||||||
return dimStyle.Render(string(s))
|
return dimStyle.Render(string(s))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/agentic"
|
"github.com/host-uk/core/pkg/agentic"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,23 +28,19 @@ var (
|
||||||
|
|
||||||
var taskUpdateCmd = &cobra.Command{
|
var taskUpdateCmd = &cobra.Command{
|
||||||
Use: "task:update [task-id]",
|
Use: "task:update [task-id]",
|
||||||
Short: "Update task status or progress",
|
Short: i18n.T("cmd.ai.task_update.short"),
|
||||||
Long: `Updates a task's status, progress, or adds notes.
|
Long: i18n.T("cmd.ai.task_update.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core ai task:update abc123 --status in_progress
|
|
||||||
core ai task:update abc123 --progress 50 --notes 'Halfway done'`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
taskID := args[0]
|
taskID := args[0]
|
||||||
|
|
||||||
if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
|
if taskUpdateStatus == "" && taskUpdateProgress == 0 && taskUpdateNotes == "" {
|
||||||
return fmt.Errorf("at least one of --status, --progress, or --notes required")
|
return fmt.Errorf(i18n.T("cmd.ai.task_update.flag_required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -60,29 +57,25 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.UpdateTask(ctx, taskID, update); err != nil {
|
if err := client.UpdateTask(ctx, taskID, update); err != nil {
|
||||||
return fmt.Errorf("failed to update task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.update_task"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Task %s updated successfully\n", successStyle.Render(">>"), taskID)
|
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_update.success", map[string]interface{}{"ID": taskID}))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskCompleteCmd = &cobra.Command{
|
var taskCompleteCmd = &cobra.Command{
|
||||||
Use: "task:complete [task-id]",
|
Use: "task:complete [task-id]",
|
||||||
Short: "Mark a task as completed",
|
Short: i18n.T("cmd.ai.task_complete.short"),
|
||||||
Long: `Marks a task as completed with optional output and artifacts.
|
Long: i18n.T("cmd.ai.task_complete.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core ai task:complete abc123 --output 'Feature implemented'
|
|
||||||
core ai task:complete abc123 --failed --error 'Build failed'`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
taskID := args[0]
|
taskID := args[0]
|
||||||
|
|
||||||
cfg, err := agentic.LoadConfig("")
|
cfg, err := agentic.LoadConfig("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := agentic.NewClientFromConfig(cfg)
|
client := agentic.NewClientFromConfig(cfg)
|
||||||
|
|
@ -97,13 +90,13 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.CompleteTask(ctx, taskID, result); err != nil {
|
if err := client.CompleteTask(ctx, taskID, result); err != nil {
|
||||||
return fmt.Errorf("failed to complete task: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ai.error.complete_task"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if taskCompleteFailed {
|
if taskCompleteFailed {
|
||||||
fmt.Printf("%s Task %s marked as failed\n", errorStyle.Render(">>"), taskID)
|
fmt.Printf("%s %s\n", errorStyle.Render(">>"), i18n.T("cmd.ai.task_complete.failed", map[string]interface{}{"ID": taskID}))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s Task %s completed successfully\n", successStyle.Render(">>"), taskID)
|
fmt.Printf("%s %s\n", successStyle.Render(">>"), i18n.T("cmd.ai.task_complete.success", map[string]interface{}{"ID": taskID}))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
@ -111,14 +104,14 @@ Examples:
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// task:update command flags
|
// task:update command flags
|
||||||
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", "New status (pending, in_progress, completed, blocked)")
|
taskUpdateCmd.Flags().StringVar(&taskUpdateStatus, "status", "", i18n.T("cmd.ai.task_update.flag.status"))
|
||||||
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, "Progress percentage (0-100)")
|
taskUpdateCmd.Flags().IntVar(&taskUpdateProgress, "progress", 0, i18n.T("cmd.ai.task_update.flag.progress"))
|
||||||
taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", "Notes about the update")
|
taskUpdateCmd.Flags().StringVar(&taskUpdateNotes, "notes", "", i18n.T("cmd.ai.task_update.flag.notes"))
|
||||||
|
|
||||||
// task:complete command flags
|
// task:complete command flags
|
||||||
taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", "Summary of the completed work")
|
taskCompleteCmd.Flags().StringVar(&taskCompleteOutput, "output", "", i18n.T("cmd.ai.task_complete.flag.output"))
|
||||||
taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, "Mark the task as failed")
|
taskCompleteCmd.Flags().BoolVar(&taskCompleteFailed, "failed", false, i18n.T("cmd.ai.task_complete.flag.failed"))
|
||||||
taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", "Error message if failed")
|
taskCompleteCmd.Flags().StringVar(&taskCompleteErrorMsg, "error", "", i18n.T("cmd.ai.task_complete.flag.error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTaskUpdateCommand(parent *cobra.Command) {
|
func addTaskUpdateCommand(parent *cobra.Command) {
|
||||||
|
|
|
||||||
|
|
@ -10,42 +10,26 @@
|
||||||
// - claude: Claude Code CLI integration (planned)
|
// - claude: Claude Code CLI integration (planned)
|
||||||
package ai
|
package ai
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
import (
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
var aiCmd = &cobra.Command{
|
var aiCmd = &cobra.Command{
|
||||||
Use: "ai",
|
Use: "ai",
|
||||||
Short: "AI agent task management",
|
Short: i18n.T("cmd.ai.short"),
|
||||||
Long: `Manage tasks from the core-agentic service for AI-assisted development.
|
Long: i18n.T("cmd.ai.long"),
|
||||||
|
|
||||||
Commands:
|
|
||||||
tasks List tasks (filterable by status, priority, labels)
|
|
||||||
task View task details or auto-select highest priority
|
|
||||||
task:update Update task status or progress
|
|
||||||
task:complete Mark task as completed or failed
|
|
||||||
task:commit Create git commit with task reference
|
|
||||||
task:pr Create GitHub PR linked to task
|
|
||||||
claude Claude Code integration
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
core ai tasks # List pending tasks
|
|
||||||
core ai task --auto --claim # Auto-select and claim a task
|
|
||||||
core ai task:commit <id> -m 'msg' # Commit with task reference
|
|
||||||
core ai task:complete <id> # Mark task done`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var claudeCmd = &cobra.Command{
|
var claudeCmd = &cobra.Command{
|
||||||
Use: "claude",
|
Use: "claude",
|
||||||
Short: "Claude Code integration",
|
Short: i18n.T("cmd.ai.claude.short"),
|
||||||
Long: `Tools for working with Claude Code.
|
Long: i18n.T("cmd.ai.claude.long"),
|
||||||
|
|
||||||
Commands:
|
|
||||||
run Run Claude in the current directory
|
|
||||||
config Manage Claude configuration`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var claudeRunCmd = &cobra.Command{
|
var claudeRunCmd = &cobra.Command{
|
||||||
Use: "run",
|
Use: "run",
|
||||||
Short: "Run Claude Code in the current directory",
|
Short: i18n.T("cmd.ai.claude.run.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runClaudeCode()
|
return runClaudeCode()
|
||||||
},
|
},
|
||||||
|
|
@ -53,7 +37,7 @@ var claudeRunCmd = &cobra.Command{
|
||||||
|
|
||||||
var claudeConfigCmd = &cobra.Command{
|
var claudeConfigCmd = &cobra.Command{
|
||||||
Use: "config",
|
Use: "config",
|
||||||
Short: "Manage Claude configuration",
|
Short: i18n.T("cmd.ai.claude.config.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return showClaudeConfig()
|
return showClaudeConfig()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -54,16 +55,8 @@ var (
|
||||||
|
|
||||||
var buildCmd = &cobra.Command{
|
var buildCmd = &cobra.Command{
|
||||||
Use: "build",
|
Use: "build",
|
||||||
Short: "Build projects with auto-detection and cross-compilation",
|
Short: i18n.T("cmd.build.short"),
|
||||||
Long: `Builds the current project with automatic type detection.
|
Long: i18n.T("cmd.build.long"),
|
||||||
Supports Go, Wails, Docker, LinuxKit, and Taskfile projects.
|
|
||||||
Configuration can be provided via .core/build.yaml or command-line flags.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core build # Auto-detect and build
|
|
||||||
core build --type docker # Build Docker image
|
|
||||||
core build --type linuxkit # Build LinuxKit image
|
|
||||||
core build --type linuxkit --config linuxkit.yml --format qcow2-bios`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize)
|
return runProjectBuild(buildType, ciMode, targets, outputDir, doArchive, doChecksum, configPath, format, push, imageName, noSign, notarize)
|
||||||
},
|
},
|
||||||
|
|
@ -71,7 +64,7 @@ Examples:
|
||||||
|
|
||||||
var fromPathCmd = &cobra.Command{
|
var fromPathCmd = &cobra.Command{
|
||||||
Use: "from-path",
|
Use: "from-path",
|
||||||
Short: "Build from a local directory.",
|
Short: i18n.T("cmd.build.from_path.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if fromPath == "" {
|
if fromPath == "" {
|
||||||
return errPathRequired
|
return errPathRequired
|
||||||
|
|
@ -82,7 +75,7 @@ var fromPathCmd = &cobra.Command{
|
||||||
|
|
||||||
var pwaCmd = &cobra.Command{
|
var pwaCmd = &cobra.Command{
|
||||||
Use: "pwa",
|
Use: "pwa",
|
||||||
Short: "Build from a live PWA URL.",
|
Short: i18n.T("cmd.build.pwa.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if pwaURL == "" {
|
if pwaURL == "" {
|
||||||
return errURLRequired
|
return errURLRequired
|
||||||
|
|
@ -93,14 +86,8 @@ var pwaCmd = &cobra.Command{
|
||||||
|
|
||||||
var sdkBuildCmd = &cobra.Command{
|
var sdkBuildCmd = &cobra.Command{
|
||||||
Use: "sdk",
|
Use: "sdk",
|
||||||
Short: "Generate API SDKs from OpenAPI spec",
|
Short: i18n.T("cmd.build.sdk.short"),
|
||||||
Long: `Generates typed API clients from OpenAPI specifications.
|
Long: i18n.T("cmd.build.sdk.long"),
|
||||||
Supports TypeScript, Python, Go, and PHP.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core build sdk # Generate all configured SDKs
|
|
||||||
core build sdk --lang typescript # Generate only TypeScript SDK
|
|
||||||
core build sdk --spec api.yaml # Use specific OpenAPI spec`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
|
return runBuildSDK(sdkSpec, sdkLang, sdkVersion, sdkDryRun)
|
||||||
},
|
},
|
||||||
|
|
@ -108,34 +95,34 @@ Examples:
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Main build command flags
|
// Main build command flags
|
||||||
buildCmd.Flags().StringVar(&buildType, "type", "", "Builder type (go, wails, docker, linuxkit, taskfile) - auto-detected if not specified")
|
buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.build.flag.type"))
|
||||||
buildCmd.Flags().BoolVar(&ciMode, "ci", false, "CI mode - minimal output with JSON artifact list at the end")
|
buildCmd.Flags().BoolVar(&ciMode, "ci", false, i18n.T("cmd.build.flag.ci"))
|
||||||
buildCmd.Flags().StringVar(&targets, "targets", "", "Comma-separated OS/arch pairs (e.g., linux/amd64,darwin/arm64)")
|
buildCmd.Flags().StringVar(&targets, "targets", "", i18n.T("cmd.build.flag.targets"))
|
||||||
buildCmd.Flags().StringVar(&outputDir, "output", "", "Output directory for artifacts (default: dist)")
|
buildCmd.Flags().StringVar(&outputDir, "output", "", i18n.T("cmd.build.flag.output"))
|
||||||
buildCmd.Flags().BoolVar(&doArchive, "archive", true, "Create archives (tar.gz for linux/darwin, zip for windows)")
|
buildCmd.Flags().BoolVar(&doArchive, "archive", true, i18n.T("cmd.build.flag.archive"))
|
||||||
buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, "Generate SHA256 checksums and CHECKSUMS.txt")
|
buildCmd.Flags().BoolVar(&doChecksum, "checksum", true, i18n.T("cmd.build.flag.checksum"))
|
||||||
|
|
||||||
// Docker/LinuxKit specific
|
// Docker/LinuxKit specific
|
||||||
buildCmd.Flags().StringVar(&configPath, "config", "", "Config file path (for linuxkit: YAML config, for docker: Dockerfile)")
|
buildCmd.Flags().StringVar(&configPath, "config", "", i18n.T("cmd.build.flag.config"))
|
||||||
buildCmd.Flags().StringVar(&format, "format", "", "Output format for linuxkit (iso-bios, qcow2-bios, raw, vmdk)")
|
buildCmd.Flags().StringVar(&format, "format", "", i18n.T("cmd.build.flag.format"))
|
||||||
buildCmd.Flags().BoolVar(&push, "push", false, "Push Docker image after build")
|
buildCmd.Flags().BoolVar(&push, "push", false, i18n.T("cmd.build.flag.push"))
|
||||||
buildCmd.Flags().StringVar(&imageName, "image", "", "Docker image name (e.g., host-uk/core-devops)")
|
buildCmd.Flags().StringVar(&imageName, "image", "", i18n.T("cmd.build.flag.image"))
|
||||||
|
|
||||||
// Signing flags
|
// Signing flags
|
||||||
buildCmd.Flags().BoolVar(&noSign, "no-sign", false, "Skip all code signing")
|
buildCmd.Flags().BoolVar(&noSign, "no-sign", false, i18n.T("cmd.build.flag.no_sign"))
|
||||||
buildCmd.Flags().BoolVar(¬arize, "notarize", false, "Enable macOS notarization (requires Apple credentials)")
|
buildCmd.Flags().BoolVar(¬arize, "notarize", false, i18n.T("cmd.build.flag.notarize"))
|
||||||
|
|
||||||
// from-path subcommand flags
|
// from-path subcommand flags
|
||||||
fromPathCmd.Flags().StringVar(&fromPath, "path", "", "The path to the static web application files.")
|
fromPathCmd.Flags().StringVar(&fromPath, "path", "", i18n.T("cmd.build.from_path.flag.path"))
|
||||||
|
|
||||||
// pwa subcommand flags
|
// pwa subcommand flags
|
||||||
pwaCmd.Flags().StringVar(&pwaURL, "url", "", "The URL of the PWA to build.")
|
pwaCmd.Flags().StringVar(&pwaURL, "url", "", i18n.T("cmd.build.pwa.flag.url"))
|
||||||
|
|
||||||
// sdk subcommand flags
|
// sdk subcommand flags
|
||||||
sdkBuildCmd.Flags().StringVar(&sdkSpec, "spec", "", "Path to OpenAPI spec file")
|
sdkBuildCmd.Flags().StringVar(&sdkSpec, "spec", "", i18n.T("cmd.build.sdk.flag.spec"))
|
||||||
sdkBuildCmd.Flags().StringVar(&sdkLang, "lang", "", "Generate only this language (typescript, python, go, php)")
|
sdkBuildCmd.Flags().StringVar(&sdkLang, "lang", "", i18n.T("cmd.build.sdk.flag.lang"))
|
||||||
sdkBuildCmd.Flags().StringVar(&sdkVersion, "version", "", "Version to embed in generated SDKs")
|
sdkBuildCmd.Flags().StringVar(&sdkVersion, "version", "", i18n.T("cmd.build.sdk.flag.version"))
|
||||||
sdkBuildCmd.Flags().BoolVar(&sdkDryRun, "dry-run", false, "Show what would be generated without writing files")
|
sdkBuildCmd.Flags().BoolVar(&sdkDryRun, "dry-run", false, i18n.T("cmd.build.sdk.flag.dry_run"))
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
buildCmd.AddCommand(fromPathCmd)
|
buildCmd.AddCommand(fromPathCmd)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
buildpkg "github.com/host-uk/core/pkg/build"
|
buildpkg "github.com/host-uk/core/pkg/build"
|
||||||
"github.com/host-uk/core/pkg/build/builders"
|
"github.com/host-uk/core/pkg/build/builders"
|
||||||
"github.com/host-uk/core/pkg/build/signing"
|
"github.com/host-uk/core/pkg/build/signing"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runProjectBuild handles the main `core build` command with auto-detection.
|
// runProjectBuild handles the main `core build` command with auto-detection.
|
||||||
|
|
@ -24,13 +25,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
// Get current working directory as project root
|
// Get current working directory as project root
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration from .core/build.yaml (or defaults)
|
// Load configuration from .core/build.yaml (or defaults)
|
||||||
buildCfg, err := buildpkg.LoadConfig(projectDir)
|
buildCfg, err := buildpkg.LoadConfig(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect project type if not specified
|
// Detect project type if not specified
|
||||||
|
|
@ -40,11 +41,10 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
} else {
|
} else {
|
||||||
projectType, err = buildpkg.PrimaryType(projectDir)
|
projectType, err = buildpkg.PrimaryType(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to detect project type: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.detect_type"), err)
|
||||||
}
|
}
|
||||||
if projectType == "" {
|
if projectType == "" {
|
||||||
return fmt.Errorf("no supported project type detected in %s\n"+
|
return fmt.Errorf("%s", i18n.T("cmd.build.error.no_project_type", map[string]interface{}{"Dir": projectDir}))
|
||||||
"Supported types: go (go.mod), wails (wails.json), node (package.json), php (composer.json)", projectDir)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,11 +82,11 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
|
|
||||||
// Print build info (unless CI mode)
|
// Print build info (unless CI mode)
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Building project\n", buildHeaderStyle.Render("Build:"))
|
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.build")), i18n.T("cmd.build.building_project"))
|
||||||
fmt.Printf(" Type: %s\n", buildTargetStyle.Render(string(projectType)))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.type"), buildTargetStyle.Render(string(projectType)))
|
||||||
fmt.Printf(" Output: %s\n", buildTargetStyle.Render(outputDir))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.output"), buildTargetStyle.Render(outputDir))
|
||||||
fmt.Printf(" Binary: %s\n", buildTargetStyle.Render(binaryName))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.binary"), buildTargetStyle.Render(binaryName))
|
||||||
fmt.Printf(" Targets: %s\n", buildTargetStyle.Render(formatTargets(buildTargets)))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.label.targets"), buildTargetStyle.Render(formatTargets(buildTargets)))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,13 +120,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
artifacts, err := builder.Build(ctx, cfg, buildTargets)
|
artifacts, err := builder.Build(ctx, cfg, buildTargets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Build failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.build_failed"), err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Built %d artifact(s)\n", buildSuccessStyle.Render("Success:"), len(artifacts))
|
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.success")), i18n.T("cmd.build.built_artifacts", map[string]interface{}{"Count": len(artifacts)}))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
for _, artifact := range artifacts {
|
for _, artifact := range artifacts {
|
||||||
relPath, err := filepath.Rel(projectDir, artifact.Path)
|
relPath, err := filepath.Rel(projectDir, artifact.Path)
|
||||||
|
|
@ -153,7 +153,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
if signCfg.Enabled && runtime.GOOS == "darwin" {
|
if signCfg.Enabled && runtime.GOOS == "darwin" {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Signing binaries...\n", buildHeaderStyle.Render("Sign:"))
|
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert buildpkg.Artifact to signing.Artifact
|
// Convert buildpkg.Artifact to signing.Artifact
|
||||||
|
|
@ -164,7 +164,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
|
|
||||||
if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
if err := signing.SignBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Signing failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.signing_failed"), err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
if signCfg.MacOS.Notarize {
|
if signCfg.MacOS.Notarize {
|
||||||
if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
if err := signing.NotarizeBinaries(ctx, signCfg, signingArtifacts); err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Notarization failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -184,13 +184,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
if doArchive && len(artifacts) > 0 {
|
if doArchive && len(artifacts) > 0 {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Creating archives...\n", buildHeaderStyle.Render("Archive:"))
|
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.archive")), i18n.T("cmd.build.creating_archives"))
|
||||||
}
|
}
|
||||||
|
|
||||||
archivedArtifacts, err = buildpkg.ArchiveAll(artifacts)
|
archivedArtifacts, err = buildpkg.ArchiveAll(artifacts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Archive failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.archive_failed"), err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +240,7 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
// JSON output for CI
|
// JSON output for CI
|
||||||
output, err := json.MarshalIndent(outputArtifacts, "", " ")
|
output, err := json.MarshalIndent(outputArtifacts, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal artifacts: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.marshal_artifacts"), err)
|
||||||
}
|
}
|
||||||
fmt.Println(string(output))
|
fmt.Println(string(output))
|
||||||
}
|
}
|
||||||
|
|
@ -252,13 +252,13 @@ func runProjectBuild(buildType string, ciMode bool, targetsFlag string, outputDi
|
||||||
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) {
|
func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string, artifacts []buildpkg.Artifact, signCfg signing.SignConfig, ciMode bool) ([]buildpkg.Artifact, error) {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Computing checksums...\n", buildHeaderStyle.Render("Checksum:"))
|
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.checksum")), i18n.T("cmd.build.computing_checksums"))
|
||||||
}
|
}
|
||||||
|
|
||||||
checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts)
|
checksummedArtifacts, err := buildpkg.ChecksumAll(artifacts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Checksum failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.checksum_failed"), err)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +267,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
||||||
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
|
checksumPath := filepath.Join(outputDir, "CHECKSUMS.txt")
|
||||||
if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
|
if err := buildpkg.WriteChecksumFile(checksummedArtifacts, checksumPath); err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s Failed to write CHECKSUMS.txt: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.write_checksums"), err)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +276,7 @@ func computeAndWriteChecksums(ctx context.Context, projectDir, outputDir string,
|
||||||
if signCfg.Enabled {
|
if signCfg.Enabled {
|
||||||
if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil {
|
if err := signing.SignChecksums(ctx, signCfg, checksumPath); err != nil {
|
||||||
if !ciMode {
|
if !ciMode {
|
||||||
fmt.Printf("%s GPG signing failed: %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s: %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), i18n.T("cmd.build.error.gpg_signing_failed"), err)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +321,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
|
||||||
|
|
||||||
osArch := strings.Split(part, "/")
|
osArch := strings.Split(part, "/")
|
||||||
if len(osArch) != 2 {
|
if len(osArch) != 2 {
|
||||||
return nil, fmt.Errorf("invalid target format %q, expected OS/arch (e.g., linux/amd64)", part)
|
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.invalid_target", map[string]interface{}{"Target": part}))
|
||||||
}
|
}
|
||||||
|
|
||||||
targets = append(targets, buildpkg.Target{
|
targets = append(targets, buildpkg.Target{
|
||||||
|
|
@ -331,7 +331,7 @@ func parseTargets(targetsFlag string) ([]buildpkg.Target, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(targets) == 0 {
|
if len(targets) == 0 {
|
||||||
return nil, fmt.Errorf("no valid targets specified")
|
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.no_targets"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return targets, nil
|
return targets, nil
|
||||||
|
|
@ -360,10 +360,10 @@ func getBuilder(projectType buildpkg.ProjectType) (buildpkg.Builder, error) {
|
||||||
case buildpkg.ProjectTypeTaskfile:
|
case buildpkg.ProjectTypeTaskfile:
|
||||||
return builders.NewTaskfileBuilder(), nil
|
return builders.NewTaskfileBuilder(), nil
|
||||||
case buildpkg.ProjectTypeNode:
|
case buildpkg.ProjectTypeNode:
|
||||||
return nil, fmt.Errorf("Node.js builder not yet implemented")
|
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.node_not_implemented"))
|
||||||
case buildpkg.ProjectTypePHP:
|
case buildpkg.ProjectTypePHP:
|
||||||
return nil, fmt.Errorf("PHP builder not yet implemented")
|
return nil, fmt.Errorf("%s", i18n.T("cmd.build.error.php_not_implemented"))
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported project type: %s", projectType)
|
return nil, fmt.Errorf("%s: %s", i18n.T("cmd.build.error.unsupported_type"), projectType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/leaanthony/debme"
|
"github.com/leaanthony/debme"
|
||||||
"github.com/leaanthony/gosod"
|
"github.com/leaanthony/gosod"
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
|
@ -26,22 +27,22 @@ import (
|
||||||
// Error sentinels for build commands
|
// Error sentinels for build commands
|
||||||
var (
|
var (
|
||||||
errPathRequired = errors.New("the --path flag is required")
|
errPathRequired = errors.New("the --path flag is required")
|
||||||
errURLRequired = errors.New("a URL argument is required")
|
errURLRequired = errors.New("the --url flag is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
// runPwaBuild downloads a PWA from URL and builds it.
|
// runPwaBuild downloads a PWA from URL and builds it.
|
||||||
func runPwaBuild(pwaURL string) error {
|
func runPwaBuild(pwaURL string) error {
|
||||||
fmt.Printf("Starting PWA build from URL: %s\n", pwaURL)
|
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.starting"), pwaURL)
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "core-pwa-build-*")
|
tempDir, err := os.MkdirTemp("", "core-pwa-build-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temporary directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.create_temp_dir"), err)
|
||||||
}
|
}
|
||||||
// defer os.RemoveAll(tempDir) // Keep temp dir for debugging
|
// defer os.RemoveAll(tempDir) // Keep temp dir for debugging
|
||||||
fmt.Printf("Downloading PWA to temporary directory: %s\n", tempDir)
|
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.downloading_to"), tempDir)
|
||||||
|
|
||||||
if err := downloadPWA(pwaURL, tempDir); err != nil {
|
if err := downloadPWA(pwaURL, tempDir); err != nil {
|
||||||
return fmt.Errorf("failed to download PWA: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.download_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runBuild(tempDir)
|
return runBuild(tempDir)
|
||||||
|
|
@ -52,48 +53,48 @@ func downloadPWA(baseURL, destDir string) error {
|
||||||
// Fetch the main HTML page
|
// Fetch the main HTML page
|
||||||
resp, err := http.Get(baseURL)
|
resp, err := http.Get(baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch URL %s: %w", baseURL, err)
|
return fmt.Errorf("%s %s: %w", i18n.T("cmd.build.pwa.error.fetch_url"), baseURL, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read response body: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.read_response"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the manifest URL from the HTML
|
// Find the manifest URL from the HTML
|
||||||
manifestURL, err := findManifestURL(string(body), baseURL)
|
manifestURL, err := findManifestURL(string(body), baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If no manifest, it's not a PWA, but we can still try to package it as a simple site.
|
// If no manifest, it's not a PWA, but we can still try to package it as a simple site.
|
||||||
fmt.Println("Warning: no manifest file found. Proceeding with basic site download.")
|
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.warning"), i18n.T("cmd.build.pwa.no_manifest"))
|
||||||
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write index.html: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.write_index"), err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Found manifest: %s\n", manifestURL)
|
fmt.Printf("%s %s\n", i18n.T("cmd.build.pwa.found_manifest"), manifestURL)
|
||||||
|
|
||||||
// Fetch and parse the manifest
|
// Fetch and parse the manifest
|
||||||
manifest, err := fetchManifest(manifestURL)
|
manifest, err := fetchManifest(manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch or parse manifest: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.fetch_manifest"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download all assets listed in the manifest
|
// Download all assets listed in the manifest
|
||||||
assets := collectAssets(manifest, manifestURL)
|
assets := collectAssets(manifest, manifestURL)
|
||||||
for _, assetURL := range assets {
|
for _, assetURL := range assets {
|
||||||
if err := downloadAsset(assetURL, destDir); err != nil {
|
if err := downloadAsset(assetURL, destDir); err != nil {
|
||||||
fmt.Printf("Warning: failed to download asset %s: %v\n", assetURL, err)
|
fmt.Printf("%s %s %s: %v\n", i18n.T("cmd.build.pwa.warning"), i18n.T("cmd.build.pwa.asset_download_failed"), assetURL, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also save the root index.html
|
// Also save the root index.html
|
||||||
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(destDir, "index.html"), body, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write index.html: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.pwa.error.write_index"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("PWA download complete.")
|
fmt.Println(i18n.T("cmd.build.pwa.download_complete"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ func findManifestURL(htmlContent, baseURL string) (string, error) {
|
||||||
f(doc)
|
f(doc)
|
||||||
|
|
||||||
if manifestPath == "" {
|
if manifestPath == "" {
|
||||||
return "", fmt.Errorf("no <link rel=\"manifest\"> tag found")
|
return "", fmt.Errorf("%s", i18n.T("cmd.build.pwa.error.no_manifest_tag"))
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := url.Parse(baseURL)
|
base, err := url.Parse(baseURL)
|
||||||
|
|
@ -218,14 +219,14 @@ func downloadAsset(assetURL, destDir string) error {
|
||||||
|
|
||||||
// runBuild builds a desktop application from a local directory.
|
// runBuild builds a desktop application from a local directory.
|
||||||
func runBuild(fromPath string) error {
|
func runBuild(fromPath string) error {
|
||||||
fmt.Printf("Starting build from path: %s\n", fromPath)
|
fmt.Printf("%s %s\n", i18n.T("cmd.build.from_path.starting"), fromPath)
|
||||||
|
|
||||||
info, err := os.Stat(fromPath)
|
info, err := os.Stat(fromPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid path specified: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.invalid_path"), err)
|
||||||
}
|
}
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
return fmt.Errorf("path specified must be a directory")
|
return fmt.Errorf("%s", i18n.T("cmd.build.from_path.error.must_be_directory"))
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDir := ".core/build/app"
|
buildDir := ".core/build/app"
|
||||||
|
|
@ -237,33 +238,33 @@ func runBuild(fromPath string) error {
|
||||||
outputExe := appName
|
outputExe := appName
|
||||||
|
|
||||||
if err := os.RemoveAll(buildDir); err != nil {
|
if err := os.RemoveAll(buildDir); err != nil {
|
||||||
return fmt.Errorf("failed to clean build directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.clean_build_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Generate the project from the embedded template
|
// 1. Generate the project from the embedded template
|
||||||
fmt.Println("Generating application from template...")
|
fmt.Println(i18n.T("cmd.build.from_path.generating_template"))
|
||||||
templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
|
templateFS, err := debme.FS(guiTemplate, "tmpl/gui")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to anchor template filesystem: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.anchor_template"), err)
|
||||||
}
|
}
|
||||||
sod := gosod.New(templateFS)
|
sod := gosod.New(templateFS)
|
||||||
if sod == nil {
|
if sod == nil {
|
||||||
return fmt.Errorf("failed to create new sod instance")
|
return fmt.Errorf("%s", i18n.T("cmd.build.from_path.error.create_sod"))
|
||||||
}
|
}
|
||||||
|
|
||||||
templateData := map[string]string{"AppName": appName}
|
templateData := map[string]string{"AppName": appName}
|
||||||
if err := sod.Extract(buildDir, templateData); err != nil {
|
if err := sod.Extract(buildDir, templateData); err != nil {
|
||||||
return fmt.Errorf("failed to extract template: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.extract_template"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Copy the user's web app files
|
// 2. Copy the user's web app files
|
||||||
fmt.Println("Copying application files...")
|
fmt.Println(i18n.T("cmd.build.from_path.copying_files"))
|
||||||
if err := copyDir(fromPath, htmlDir); err != nil {
|
if err := copyDir(fromPath, htmlDir); err != nil {
|
||||||
return fmt.Errorf("failed to copy application files: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.copy_files"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Compile the application
|
// 3. Compile the application
|
||||||
fmt.Println("Compiling application...")
|
fmt.Println(i18n.T("cmd.build.from_path.compiling"))
|
||||||
|
|
||||||
// Run go mod tidy
|
// Run go mod tidy
|
||||||
cmd := exec.Command("go", "mod", "tidy")
|
cmd := exec.Command("go", "mod", "tidy")
|
||||||
|
|
@ -271,7 +272,7 @@ func runBuild(fromPath string) error {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("go mod tidy failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.go_mod_tidy"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run go build
|
// Run go build
|
||||||
|
|
@ -280,10 +281,10 @@ func runBuild(fromPath string) error {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("go build failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.from_path.error.go_build"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nBuild successful! Executable created at: %s/%s\n", buildDir, outputExe)
|
fmt.Printf("\n%s %s/%s\n", i18n.T("cmd.build.from_path.success"), buildDir, outputExe)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/sdk"
|
"github.com/host-uk/core/pkg/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
|
||||||
|
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.build.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
|
|
@ -34,48 +35,48 @@ func runBuildSDK(specPath, lang, version string, dryRun bool) error {
|
||||||
s.SetVersion(version)
|
s.SetVersion(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Generating SDKs\n", buildHeaderStyle.Render("Build SDK:"))
|
fmt.Printf("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.sdk.label")), i18n.T("cmd.build.sdk.generating"))
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf(" %s\n", buildDimStyle.Render("(dry-run mode)"))
|
fmt.Printf(" %s\n", buildDimStyle.Render(i18n.T("cmd.build.sdk.dry_run_mode")))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Detect spec
|
// Detect spec
|
||||||
detectedSpec, err := s.DetectSpec()
|
detectedSpec, err := s.DetectSpec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" Spec: %s\n", buildTargetStyle.Render(detectedSpec))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.spec_label"), buildTargetStyle.Render(detectedSpec))
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
if lang != "" {
|
if lang != "" {
|
||||||
fmt.Printf(" Language: %s\n", buildTargetStyle.Render(lang))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.language_label"), buildTargetStyle.Render(lang))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Languages: %s\n", buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.languages_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Would generate SDKs (dry-run)\n", buildSuccessStyle.Render("OK:"))
|
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.ok")), i18n.T("cmd.build.sdk.would_generate"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if lang != "" {
|
if lang != "" {
|
||||||
// Generate single language
|
// Generate single language
|
||||||
if err := s.GenerateLanguage(ctx, lang); err != nil {
|
if err := s.GenerateLanguage(ctx, lang); err != nil {
|
||||||
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" Generated: %s\n", buildTargetStyle.Render(lang))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(lang))
|
||||||
} else {
|
} else {
|
||||||
// Generate all
|
// Generate all
|
||||||
if err := s.Generate(ctx); err != nil {
|
if err := s.Generate(ctx); err != nil {
|
||||||
fmt.Printf("%s %v\n", buildErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", buildErrorStyle.Render(i18n.T("cmd.build.label.error")), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" Generated: %s\n", buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.build.sdk.generated_label"), buildTargetStyle.Render(strings.Join(config.Languages, ", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s SDK generation complete\n", buildSuccessStyle.Render("Success:"))
|
fmt.Printf("%s %s\n", buildSuccessStyle.Render(i18n.T("cmd.build.label.success")), i18n.T("cmd.build.sdk.complete"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/release"
|
"github.com/host-uk/core/pkg/release"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -11,19 +12,19 @@ import (
|
||||||
func runChangelog(fromRef, toRef string) error {
|
func runChangelog(fromRef, toRef string) error {
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config for changelog settings
|
// Load config for changelog settings
|
||||||
cfg, err := release.LoadConfig(projectDir)
|
cfg, err := release.LoadConfig(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate changelog
|
// Generate changelog
|
||||||
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog)
|
changelog, err := release.GenerateWithConfig(projectDir, fromRef, toRef, &cfg.Changelog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate changelog: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.generate_changelog"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(changelog)
|
fmt.Println(changelog)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/release"
|
"github.com/host-uk/core/pkg/release"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -14,33 +15,34 @@ import (
|
||||||
func runCIReleaseInit() error {
|
func runCIReleaseInit() error {
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if config already exists
|
// Check if config already exists
|
||||||
if release.ConfigExists(projectDir) {
|
if release.ConfigExists(projectDir) {
|
||||||
fmt.Printf("%s Configuration already exists at %s\n",
|
fmt.Printf("%s %s %s\n",
|
||||||
releaseDimStyle.Render("Note:"),
|
releaseDimStyle.Render(i18n.T("cmd.ci.label.note")),
|
||||||
|
i18n.T("cmd.ci.init.config_exists"),
|
||||||
release.ConfigPath(projectDir))
|
release.ConfigPath(projectDir))
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
fmt.Print("Overwrite? [y/N]: ")
|
fmt.Print(i18n.T("cmd.ci.init.overwrite_prompt"))
|
||||||
response, _ := reader.ReadString('\n')
|
response, _ := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(strings.ToLower(response))
|
response = strings.TrimSpace(strings.ToLower(response))
|
||||||
if response != "y" && response != "yes" {
|
if response != "y" && response != "yes" {
|
||||||
fmt.Println("Aborted.")
|
fmt.Println(i18n.T("cli.confirm.abort"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Creating release configuration\n", releaseHeaderStyle.Render("Init:"))
|
fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.init")), i18n.T("cmd.ci.init.creating"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
// Project name
|
// Project name
|
||||||
defaultName := filepath.Base(projectDir)
|
defaultName := filepath.Base(projectDir)
|
||||||
fmt.Printf("Project name [%s]: ", defaultName)
|
fmt.Printf("%s [%s]: ", i18n.T("cmd.ci.init.project_name"), defaultName)
|
||||||
name, _ := reader.ReadString('\n')
|
name, _ := reader.ReadString('\n')
|
||||||
name = strings.TrimSpace(name)
|
name = strings.TrimSpace(name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|
@ -48,7 +50,7 @@ func runCIReleaseInit() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repository
|
// Repository
|
||||||
fmt.Print("GitHub repository (owner/repo): ")
|
fmt.Printf("%s ", i18n.T("cmd.ci.init.github_repo"))
|
||||||
repo, _ := reader.ReadString('\n')
|
repo, _ := reader.ReadString('\n')
|
||||||
repo = strings.TrimSpace(repo)
|
repo = strings.TrimSpace(repo)
|
||||||
|
|
||||||
|
|
@ -59,12 +61,13 @@ func runCIReleaseInit() error {
|
||||||
|
|
||||||
// Write config
|
// Write config
|
||||||
if err := release.WriteConfig(cfg, projectDir); err != nil {
|
if err := release.WriteConfig(cfg, projectDir); err != nil {
|
||||||
return fmt.Errorf("failed to write config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.write_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Configuration written to %s\n",
|
fmt.Printf("%s %s %s\n",
|
||||||
releaseSuccessStyle.Render("Success:"),
|
releaseSuccessStyle.Render(i18n.T("cmd.ci.label.success")),
|
||||||
|
i18n.T("cmd.ci.init.config_written"),
|
||||||
release.ConfigPath(projectDir))
|
release.ConfigPath(projectDir))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/release"
|
"github.com/host-uk/core/pkg/release"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,13 +17,13 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
|
||||||
// Get current directory
|
// Get current directory
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := release.LoadConfig(projectDir)
|
cfg, err := release.LoadConfig(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load config: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.load_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply CLI overrides
|
// Apply CLI overrides
|
||||||
|
|
@ -43,35 +44,35 @@ func runCIPublish(dryRun bool, version string, draft, prerelease bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print header
|
// Print header
|
||||||
fmt.Printf("%s Publishing release\n", releaseHeaderStyle.Render("CI:"))
|
fmt.Printf("%s %s\n", releaseHeaderStyle.Render(i18n.T("cmd.ci.label.ci")), i18n.T("cmd.ci.publishing"))
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf(" %s\n", releaseDimStyle.Render("(dry-run) use --we-are-go-for-launch to publish"))
|
fmt.Printf(" %s\n", releaseDimStyle.Render(i18n.T("cmd.ci.dry_run_hint")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s\n", releaseSuccessStyle.Render("GO FOR LAUNCH"))
|
fmt.Printf(" %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.go_for_launch")))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Check for publishers
|
// Check for publishers
|
||||||
if len(cfg.Publishers) == 0 {
|
if len(cfg.Publishers) == 0 {
|
||||||
return fmt.Errorf("no publishers configured in .core/release.yaml")
|
return fmt.Errorf(i18n.T("cmd.ci.error.no_publishers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish pre-built artifacts
|
// Publish pre-built artifacts
|
||||||
rel, err := release.Publish(ctx, cfg, dryRun)
|
rel, err := release.Publish(ctx, cfg, dryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s %v\n", releaseErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", releaseErrorStyle.Render(i18n.T("cmd.ci.label.error")), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print summary
|
// Print summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Publish completed!\n", releaseSuccessStyle.Render("Success:"))
|
fmt.Printf("%s %s\n", releaseSuccessStyle.Render(i18n.T("cmd.ci.label.success")), i18n.T("cmd.ci.publish_completed"))
|
||||||
fmt.Printf(" Version: %s\n", releaseValueStyle.Render(rel.Version))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.version"), releaseValueStyle.Render(rel.Version))
|
||||||
fmt.Printf(" Artifacts: %d\n", len(rel.Artifacts))
|
fmt.Printf(" %s %d\n", i18n.T("cmd.ci.label.artifacts"), len(rel.Artifacts))
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
for _, pub := range cfg.Publishers {
|
for _, pub := range cfg.Publishers {
|
||||||
fmt.Printf(" Published: %s\n", releaseValueStyle.Render(pub.Type))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.ci.label.published"), releaseValueStyle.Render(pub.Type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package ci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -31,13 +32,8 @@ var (
|
||||||
|
|
||||||
var ciCmd = &cobra.Command{
|
var ciCmd = &cobra.Command{
|
||||||
Use: "ci",
|
Use: "ci",
|
||||||
Short: "Publish releases (dry-run by default)",
|
Short: i18n.T("cmd.ci.short"),
|
||||||
Long: `Publishes pre-built artifacts from dist/ to configured targets.
|
Long: i18n.T("cmd.ci.long"),
|
||||||
Run 'core build' first to create artifacts.
|
|
||||||
|
|
||||||
SAFE BY DEFAULT: Runs in dry-run mode unless --we-are-go-for-launch is specified.
|
|
||||||
|
|
||||||
Configuration: .core/release.yaml`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
dryRun := !ciGoForLaunch
|
dryRun := !ciGoForLaunch
|
||||||
return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
|
return runCIPublish(dryRun, ciVersion, ciDraft, ciPrerelease)
|
||||||
|
|
@ -46,8 +42,8 @@ Configuration: .core/release.yaml`,
|
||||||
|
|
||||||
var ciInitCmd = &cobra.Command{
|
var ciInitCmd = &cobra.Command{
|
||||||
Use: "init",
|
Use: "init",
|
||||||
Short: "Initialize release configuration",
|
Short: i18n.T("cmd.ci.init.short"),
|
||||||
Long: "Creates a .core/release.yaml configuration file interactively.",
|
Long: i18n.T("cmd.ci.init.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCIReleaseInit()
|
return runCIReleaseInit()
|
||||||
},
|
},
|
||||||
|
|
@ -55,8 +51,8 @@ var ciInitCmd = &cobra.Command{
|
||||||
|
|
||||||
var ciChangelogCmd = &cobra.Command{
|
var ciChangelogCmd = &cobra.Command{
|
||||||
Use: "changelog",
|
Use: "changelog",
|
||||||
Short: "Generate changelog",
|
Short: i18n.T("cmd.ci.changelog.short"),
|
||||||
Long: "Generates a changelog from conventional commits.",
|
Long: i18n.T("cmd.ci.changelog.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runChangelog(changelogFromRef, changelogToRef)
|
return runChangelog(changelogFromRef, changelogToRef)
|
||||||
},
|
},
|
||||||
|
|
@ -64,8 +60,8 @@ var ciChangelogCmd = &cobra.Command{
|
||||||
|
|
||||||
var ciVersionCmd = &cobra.Command{
|
var ciVersionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Show or set version",
|
Short: i18n.T("cmd.ci.version.short"),
|
||||||
Long: "Shows the determined version or validates a version string.",
|
Long: i18n.T("cmd.ci.version.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCIReleaseVersion()
|
return runCIReleaseVersion()
|
||||||
},
|
},
|
||||||
|
|
@ -73,14 +69,14 @@ var ciVersionCmd = &cobra.Command{
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Main ci command flags
|
// Main ci command flags
|
||||||
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, "Actually publish (default is dry-run for safety)")
|
ciCmd.Flags().BoolVar(&ciGoForLaunch, "we-are-go-for-launch", false, i18n.T("cmd.ci.flag.go_for_launch"))
|
||||||
ciCmd.Flags().StringVar(&ciVersion, "version", "", "Version to release (e.g., v1.2.3)")
|
ciCmd.Flags().StringVar(&ciVersion, "version", "", i18n.T("cmd.ci.flag.version"))
|
||||||
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, "Create release as a draft")
|
ciCmd.Flags().BoolVar(&ciDraft, "draft", false, i18n.T("cmd.ci.flag.draft"))
|
||||||
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, "Mark release as a prerelease")
|
ciCmd.Flags().BoolVar(&ciPrerelease, "prerelease", false, i18n.T("cmd.ci.flag.prerelease"))
|
||||||
|
|
||||||
// Changelog subcommand flags
|
// Changelog subcommand flags
|
||||||
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", "Starting ref (default: previous tag)")
|
ciChangelogCmd.Flags().StringVar(&changelogFromRef, "from", "", i18n.T("cmd.ci.changelog.flag.from"))
|
||||||
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", "Ending ref (default: HEAD)")
|
ciChangelogCmd.Flags().StringVar(&changelogToRef, "to", "", i18n.T("cmd.ci.changelog.flag.to"))
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
ciCmd.AddCommand(ciInitCmd)
|
ciCmd.AddCommand(ciInitCmd)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/release"
|
"github.com/host-uk/core/pkg/release"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -11,14 +12,14 @@ import (
|
||||||
func runCIReleaseVersion() error {
|
func runCIReleaseVersion() error {
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := release.DetermineVersion(projectDir)
|
version, err := release.DetermineVersion(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to determine version: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.ci.error.determine_version"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Version: %s\n", releaseValueStyle.Render(version))
|
fmt.Printf("%s %s\n", i18n.T("cmd.ci.label.version"), releaseValueStyle.Render(version))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ package dev
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,31 +56,8 @@ var (
|
||||||
func AddCommands(root *cobra.Command) {
|
func AddCommands(root *cobra.Command) {
|
||||||
devCmd := &cobra.Command{
|
devCmd := &cobra.Command{
|
||||||
Use: "dev",
|
Use: "dev",
|
||||||
Short: "Multi-repo development workflow",
|
Short: i18n.T("cmd.dev.short"),
|
||||||
Long: `Manage multiple git repositories and GitHub integration.
|
Long: i18n.T("cmd.dev.long"),
|
||||||
|
|
||||||
Uses repos.yaml to discover repositories. Falls back to scanning
|
|
||||||
the current directory if no registry is found.
|
|
||||||
|
|
||||||
Git Operations:
|
|
||||||
work Combined status -> commit -> push workflow
|
|
||||||
health Quick repo health summary
|
|
||||||
commit Claude-assisted commit messages
|
|
||||||
push Push repos with unpushed commits
|
|
||||||
pull Pull repos behind remote
|
|
||||||
|
|
||||||
GitHub Integration (requires gh CLI):
|
|
||||||
issues List open issues across repos
|
|
||||||
reviews List PRs awaiting review
|
|
||||||
ci Check GitHub Actions status
|
|
||||||
impact Analyse dependency impact
|
|
||||||
|
|
||||||
Dev Environment:
|
|
||||||
install Download dev environment image
|
|
||||||
boot Start dev environment VM
|
|
||||||
stop Stop dev environment VM
|
|
||||||
shell Open shell in dev VM
|
|
||||||
status Check dev VM status`,
|
|
||||||
}
|
}
|
||||||
root.AddCommand(devCmd)
|
root.AddCommand(devCmd)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dev
|
package dev
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -9,7 +10,7 @@ func addAPICommands(parent *cobra.Command) {
|
||||||
// Create the 'api' command
|
// Create the 'api' command
|
||||||
apiCmd := &cobra.Command{
|
apiCmd := &cobra.Command{
|
||||||
Use: "api",
|
Use: "api",
|
||||||
Short: "Tools for managing service APIs",
|
Short: i18n.T("cmd.dev.api.short"),
|
||||||
}
|
}
|
||||||
parent.AddCommand(apiCmd)
|
parent.AddCommand(apiCmd)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -46,10 +47,8 @@ var (
|
||||||
func addCICommand(parent *cobra.Command) {
|
func addCICommand(parent *cobra.Command) {
|
||||||
ciCmd := &cobra.Command{
|
ciCmd := &cobra.Command{
|
||||||
Use: "ci",
|
Use: "ci",
|
||||||
Short: "Check CI status across all repos",
|
Short: i18n.T("cmd.dev.ci.short"),
|
||||||
Long: `Fetches GitHub Actions workflow status for all repos.
|
Long: i18n.T("cmd.dev.ci.long"),
|
||||||
Shows latest run status for each repo.
|
|
||||||
Requires the 'gh' CLI to be installed and authenticated.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
branch := ciBranch
|
branch := ciBranch
|
||||||
if branch == "" {
|
if branch == "" {
|
||||||
|
|
@ -59,9 +58,9 @@ Requires the 'gh' CLI to be installed and authenticated.`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
ciCmd.Flags().StringVar(&ciRegistryPath, "registry", "", i18n.T("cmd.dev.ci.flag.registry"))
|
||||||
ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", "Filter by branch")
|
ciCmd.Flags().StringVarP(&ciBranch, "branch", "b", "main", i18n.T("cmd.dev.ci.flag.branch"))
|
||||||
ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, "Show only failed runs")
|
ciCmd.Flags().BoolVar(&ciFailedOnly, "failed", false, i18n.T("cmd.dev.ci.flag.failed"))
|
||||||
|
|
||||||
parent.AddCommand(ciCmd)
|
parent.AddCommand(ciCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +68,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
|
||||||
func runCI(registryPath string, branch string, failedOnly bool) error {
|
func runCI(registryPath string, branch string, failedOnly bool) error {
|
||||||
// Check gh is available
|
// Check gh is available
|
||||||
if _, err := exec.LookPath("gh"); err != nil {
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
return fmt.Errorf(i18n.T("error.gh_not_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or use provided registry
|
// Find or use provided registry
|
||||||
|
|
@ -105,7 +104,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
|
||||||
repoList := reg.List()
|
repoList := reg.List()
|
||||||
for i, repo := range repoList {
|
for i, repo := range repoList {
|
||||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Checking"), i+1, len(repoList), repo.Name)
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.checking")), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch)
|
runs, err := fetchWorkflowRuns(repoFullName, repo.Name, branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -147,18 +146,18 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
|
||||||
|
|
||||||
// Print summary
|
// Print summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%d repos checked", len(repoList))
|
fmt.Printf("%s", i18n.T("cmd.dev.ci.repos_checked", map[string]interface{}{"Count": len(repoList)}))
|
||||||
if success > 0 {
|
if success > 0 {
|
||||||
fmt.Printf(" * %s", ciSuccessStyle.Render(fmt.Sprintf("%d passing", success)))
|
fmt.Printf(" * %s", ciSuccessStyle.Render(i18n.T("cmd.dev.ci.passing", map[string]interface{}{"Count": success})))
|
||||||
}
|
}
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Printf(" * %s", ciFailureStyle.Render(fmt.Sprintf("%d failing", failed)))
|
fmt.Printf(" * %s", ciFailureStyle.Render(i18n.T("cmd.dev.ci.failing", map[string]interface{}{"Count": failed})))
|
||||||
}
|
}
|
||||||
if pending > 0 {
|
if pending > 0 {
|
||||||
fmt.Printf(" * %s", ciPendingStyle.Render(fmt.Sprintf("%d pending", pending)))
|
fmt.Printf(" * %s", ciPendingStyle.Render(i18n.T("cmd.dev.ci.pending", map[string]interface{}{"Count": pending})))
|
||||||
}
|
}
|
||||||
if len(noCI) > 0 {
|
if len(noCI) > 0 {
|
||||||
fmt.Printf(" * %s", ciSkippedStyle.Render(fmt.Sprintf("%d no CI", len(noCI))))
|
fmt.Printf(" * %s", ciSkippedStyle.Render(i18n.T("cmd.dev.ci.no_ci", map[string]interface{}{"Count": len(noCI)})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
@ -183,7 +182,7 @@ func runCI(registryPath string, branch string, failedOnly bool) error {
|
||||||
if len(fetchErrors) > 0 {
|
if len(fetchErrors) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
for _, err := range fetchErrors {
|
for _, err := range fetchErrors {
|
||||||
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.ci.error_label")), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -21,16 +22,15 @@ var (
|
||||||
func addCommitCommand(parent *cobra.Command) {
|
func addCommitCommand(parent *cobra.Command) {
|
||||||
commitCmd := &cobra.Command{
|
commitCmd := &cobra.Command{
|
||||||
Use: "commit",
|
Use: "commit",
|
||||||
Short: "Claude-assisted commits across repos",
|
Short: i18n.T("cmd.dev.commit.short"),
|
||||||
Long: `Uses Claude to create commits for dirty repos.
|
Long: i18n.T("cmd.dev.commit.long"),
|
||||||
Shows uncommitted changes and invokes Claude to generate commit messages.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCommit(commitRegistryPath, commitAll)
|
return runCommit(commitRegistryPath, commitAll)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
commitCmd.Flags().StringVar(&commitRegistryPath, "registry", "", i18n.T("cmd.dev.commit.flag.registry"))
|
||||||
commitCmd.Flags().BoolVar(&commitAll, "all", false, "Commit all dirty repos without prompting")
|
commitCmd.Flags().BoolVar(&commitAll, "all", false, i18n.T("cmd.dev.commit.flag.all"))
|
||||||
|
|
||||||
parent.AddCommand(commitCmd)
|
parent.AddCommand(commitCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ func runCommit(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
registryPath, err = repos.FindRegistry()
|
registryPath, err = repos.FindRegistry()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -55,7 +55,7 @@ func runCommit(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: scan current directory
|
// Fallback: scan current directory
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
@ -63,7 +63,7 @@ func runCommit(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to scan directory: %w", err)
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
|
||||||
registryPath = cwd
|
registryPath = cwd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +80,7 @@ func runCommit(registryPath string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
fmt.Println("No git repositories found.")
|
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,22 +99,22 @@ func runCommit(registryPath string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(dirtyRepos) == 0 {
|
if len(dirtyRepos) == 0 {
|
||||||
fmt.Println("No uncommitted changes found.")
|
fmt.Println(i18n.T("cmd.dev.no_changes"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show dirty repos
|
// Show dirty repos
|
||||||
fmt.Printf("\n%d repo(s) with uncommitted changes:\n\n", len(dirtyRepos))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.repos_with_changes", map[string]interface{}{"Count": len(dirtyRepos)}))
|
||||||
for _, s := range dirtyRepos {
|
for _, s := range dirtyRepos {
|
||||||
fmt.Printf(" %s: ", repoNameStyle.Render(s.Name))
|
fmt.Printf(" %s: ", repoNameStyle.Render(s.Name))
|
||||||
if s.Modified > 0 {
|
if s.Modified > 0 {
|
||||||
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d modified", s.Modified)))
|
fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.modified", map[string]interface{}{"Count": s.Modified})))
|
||||||
}
|
}
|
||||||
if s.Untracked > 0 {
|
if s.Untracked > 0 {
|
||||||
fmt.Printf("%s ", dirtyStyle.Render(fmt.Sprintf("%d untracked", s.Untracked)))
|
fmt.Printf("%s ", dirtyStyle.Render(i18n.T("cmd.dev.untracked", map[string]interface{}{"Count": s.Untracked})))
|
||||||
}
|
}
|
||||||
if s.Staged > 0 {
|
if s.Staged > 0 {
|
||||||
fmt.Printf("%s ", aheadStyle.Render(fmt.Sprintf("%d staged", s.Staged)))
|
fmt.Printf("%s ", aheadStyle.Render(i18n.T("cmd.dev.staged", map[string]interface{}{"Count": s.Staged})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +122,8 @@ func runCommit(registryPath string, all bool) error {
|
||||||
// Confirm unless --all
|
// Confirm unless --all
|
||||||
if !all {
|
if !all {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if !shared.Confirm("Have Claude commit these repos?") {
|
if !shared.Confirm(i18n.T("cmd.dev.confirm_claude_commit")) {
|
||||||
fmt.Println("Aborted.")
|
fmt.Println(i18n.T("cli.aborted"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,22 +133,22 @@ func runCommit(registryPath string, all bool) error {
|
||||||
// Commit each dirty repo
|
// Commit each dirty repo
|
||||||
var succeeded, failed int
|
var succeeded, failed int
|
||||||
for _, s := range dirtyRepos {
|
for _, s := range dirtyRepos {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Committing"), s.Name)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.committing")), s.Name)
|
||||||
|
|
||||||
if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil {
|
if err := claudeCommit(ctx, s.Path, s.Name, registryPath); err != nil {
|
||||||
fmt.Printf(" %s %s\n", errorStyle.Render("x"), err)
|
fmt.Printf(" %s %s\n", errorStyle.Render("x"), err)
|
||||||
failed++
|
failed++
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s committed\n", successStyle.Render("v"))
|
fmt.Printf(" %s %s\n", successStyle.Render("v"), i18n.T("cmd.dev.committed"))
|
||||||
succeeded++
|
succeeded++
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Printf("%s %d succeeded", successStyle.Render("Done:"), succeeded)
|
fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.done_succeeded", map[string]interface{}{"Count": succeeded})))
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -22,16 +23,15 @@ var (
|
||||||
func addHealthCommand(parent *cobra.Command) {
|
func addHealthCommand(parent *cobra.Command) {
|
||||||
healthCmd := &cobra.Command{
|
healthCmd := &cobra.Command{
|
||||||
Use: "health",
|
Use: "health",
|
||||||
Short: "Quick health check across all repos",
|
Short: i18n.T("cmd.dev.health.short"),
|
||||||
Long: `Shows a summary of repository health:
|
Long: i18n.T("cmd.dev.health.long"),
|
||||||
total repos, dirty repos, unpushed commits, etc.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runHealth(healthRegistryPath, healthVerbose)
|
return runHealth(healthRegistryPath, healthVerbose)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
healthCmd.Flags().StringVar(&healthRegistryPath, "registry", "", i18n.T("cmd.dev.health.flag.registry"))
|
||||||
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, "Show detailed breakdown")
|
healthCmd.Flags().BoolVarP(&healthVerbose, "verbose", "v", false, i18n.T("cmd.dev.health.flag.verbose"))
|
||||||
|
|
||||||
parent.AddCommand(healthCmd)
|
parent.AddCommand(healthCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ func runHealth(registryPath string, verbose bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
fmt.Println("No git repositories found.")
|
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,16 +125,16 @@ func runHealth(registryPath string, verbose bool) error {
|
||||||
// Verbose output
|
// Verbose output
|
||||||
if verbose {
|
if verbose {
|
||||||
if len(dirtyRepos) > 0 {
|
if len(dirtyRepos) > 0 {
|
||||||
fmt.Printf("%s %s\n", warningStyle.Render("Dirty:"), formatRepoList(dirtyRepos))
|
fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.dirty_label")), formatRepoList(dirtyRepos))
|
||||||
}
|
}
|
||||||
if len(aheadRepos) > 0 {
|
if len(aheadRepos) > 0 {
|
||||||
fmt.Printf("%s %s\n", successStyle.Render("Ahead:"), formatRepoList(aheadRepos))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.dev.health.ahead_label")), formatRepoList(aheadRepos))
|
||||||
}
|
}
|
||||||
if len(behindRepos) > 0 {
|
if len(behindRepos) > 0 {
|
||||||
fmt.Printf("%s %s\n", warningStyle.Render("Behind:"), formatRepoList(behindRepos))
|
fmt.Printf("%s %s\n", warningStyle.Render(i18n.T("cmd.dev.health.behind_label")), formatRepoList(behindRepos))
|
||||||
}
|
}
|
||||||
if len(errorRepos) > 0 {
|
if len(errorRepos) > 0 {
|
||||||
fmt.Printf("%s %s\n", errorStyle.Render("Errors:"), formatRepoList(errorRepos))
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.health.errors_label")), formatRepoList(errorRepos))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
@ -144,33 +144,33 @@ func runHealth(registryPath string, verbose bool) error {
|
||||||
|
|
||||||
func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
|
func printHealthSummary(total int, dirty, ahead, behind, errors []string) {
|
||||||
parts := []string{
|
parts := []string{
|
||||||
shared.StatusPart(total, "repos", shared.ValueStyle),
|
shared.StatusPart(total, i18n.T("cmd.dev.health.repos"), shared.ValueStyle),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dirty status
|
// Dirty status
|
||||||
if len(dirty) > 0 {
|
if len(dirty) > 0 {
|
||||||
parts = append(parts, shared.StatusPart(len(dirty), "dirty", shared.WarningStyle))
|
parts = append(parts, shared.StatusPart(len(dirty), i18n.T("cmd.dev.health.dirty"), shared.WarningStyle))
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, shared.StatusText("clean", shared.SuccessStyle))
|
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.status.clean"), shared.SuccessStyle))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push status
|
// Push status
|
||||||
if len(ahead) > 0 {
|
if len(ahead) > 0 {
|
||||||
parts = append(parts, shared.StatusPart(len(ahead), "to push", shared.ValueStyle))
|
parts = append(parts, shared.StatusPart(len(ahead), i18n.T("cmd.dev.health.to_push"), shared.ValueStyle))
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, shared.StatusText("synced", shared.SuccessStyle))
|
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.synced"), shared.SuccessStyle))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull status
|
// Pull status
|
||||||
if len(behind) > 0 {
|
if len(behind) > 0 {
|
||||||
parts = append(parts, shared.StatusPart(len(behind), "to pull", shared.WarningStyle))
|
parts = append(parts, shared.StatusPart(len(behind), i18n.T("cmd.dev.health.to_pull"), shared.WarningStyle))
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, shared.StatusText("up to date", shared.SuccessStyle))
|
parts = append(parts, shared.StatusText(i18n.T("cmd.dev.health.up_to_date"), shared.SuccessStyle))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errors (only if any)
|
// Errors (only if any)
|
||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
parts = append(parts, shared.StatusPart(len(errors), "errors", shared.ErrorStyle))
|
parts = append(parts, shared.StatusPart(len(errors), i18n.T("cmd.dev.health.errors"), shared.ErrorStyle))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(shared.StatusLine(parts...))
|
fmt.Println(shared.StatusLine(parts...))
|
||||||
|
|
@ -180,7 +180,7 @@ func formatRepoList(reposList []string) string {
|
||||||
if len(reposList) <= 5 {
|
if len(reposList) <= 5 {
|
||||||
return joinRepos(reposList)
|
return joinRepos(reposList)
|
||||||
}
|
}
|
||||||
return joinRepos(reposList[:5]) + fmt.Sprintf(" +%d more", len(reposList)-5)
|
return joinRepos(reposList[:5]) + " " + i18n.T("cmd.dev.health.more", map[string]interface{}{"Count": len(reposList) - 5})
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinRepos(reposList []string) string {
|
func joinRepos(reposList []string) string {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -23,16 +24,15 @@ var impactRegistryPath string
|
||||||
func addImpactCommand(parent *cobra.Command) {
|
func addImpactCommand(parent *cobra.Command) {
|
||||||
impactCmd := &cobra.Command{
|
impactCmd := &cobra.Command{
|
||||||
Use: "impact <repo-name>",
|
Use: "impact <repo-name>",
|
||||||
Short: "Show impact of changing a repo",
|
Short: i18n.T("cmd.dev.impact.short"),
|
||||||
Long: `Analyzes the dependency graph to show which repos
|
Long: i18n.T("cmd.dev.impact.long"),
|
||||||
would be affected by changes to the specified repo.`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runImpact(impactRegistryPath, args[0])
|
return runImpact(impactRegistryPath, args[0])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
impactCmd.Flags().StringVar(&impactRegistryPath, "registry", "", i18n.T("cmd.dev.impact.flag.registry"))
|
||||||
|
|
||||||
parent.AddCommand(impactCmd)
|
parent.AddCommand(impactCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -55,14 +55,14 @@ func runImpact(registryPath string, repoName string) error {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("impact analysis requires repos.yaml with dependency information")
|
return fmt.Errorf(i18n.T("cmd.dev.impact.requires_registry"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check repo exists
|
// Check repo exists
|
||||||
repo, exists := reg.Get(repoName)
|
repo, exists := reg.Get(repoName)
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("repo '%s' not found in registry", repoName)
|
return fmt.Errorf(i18n.T("error.repo_not_found", map[string]interface{}{"Name": repoName}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build reverse dependency graph
|
// Build reverse dependency graph
|
||||||
|
|
@ -91,22 +91,22 @@ func runImpact(registryPath string, repoName string) error {
|
||||||
|
|
||||||
// Print results
|
// Print results
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Impact analysis for"), repoNameStyle.Render(repoName))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.impact.analysis_for")), repoNameStyle.Render(repoName))
|
||||||
if repo.Description != "" {
|
if repo.Description != "" {
|
||||||
fmt.Printf("%s\n", dimStyle.Render(repo.Description))
|
fmt.Printf("%s\n", dimStyle.Render(repo.Description))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if len(allAffected) == 0 {
|
if len(allAffected) == 0 {
|
||||||
fmt.Printf("%s No repos depend on %s\n", impactSafeStyle.Render("v"), repoName)
|
fmt.Printf("%s %s\n", impactSafeStyle.Render("v"), i18n.T("cmd.dev.impact.no_dependents", map[string]interface{}{"Name": repoName}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct dependents
|
// Direct dependents
|
||||||
if len(direct) > 0 {
|
if len(direct) > 0 {
|
||||||
fmt.Printf("%s %d direct dependent(s):\n",
|
fmt.Printf("%s %s\n",
|
||||||
impactDirectStyle.Render("*"),
|
impactDirectStyle.Render("*"),
|
||||||
len(direct),
|
i18n.T("cmd.dev.impact.direct_dependents", map[string]interface{}{"Count": len(direct)}),
|
||||||
)
|
)
|
||||||
for _, d := range direct {
|
for _, d := range direct {
|
||||||
r, _ := reg.Get(d)
|
r, _ := reg.Get(d)
|
||||||
|
|
@ -121,9 +121,9 @@ func runImpact(registryPath string, repoName string) error {
|
||||||
|
|
||||||
// Indirect dependents
|
// Indirect dependents
|
||||||
if len(indirect) > 0 {
|
if len(indirect) > 0 {
|
||||||
fmt.Printf("%s %d transitive dependent(s):\n",
|
fmt.Printf("%s %s\n",
|
||||||
impactIndirectStyle.Render("o"),
|
impactIndirectStyle.Render("o"),
|
||||||
len(indirect),
|
i18n.T("cmd.dev.impact.transitive_dependents", map[string]interface{}{"Count": len(indirect)}),
|
||||||
)
|
)
|
||||||
for _, d := range indirect {
|
for _, d := range indirect {
|
||||||
r, _ := reg.Get(d)
|
r, _ := reg.Get(d)
|
||||||
|
|
@ -137,10 +137,13 @@ func runImpact(registryPath string, repoName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Printf("%s Changes to %s affect %s\n",
|
fmt.Printf("%s %s\n",
|
||||||
dimStyle.Render("Summary:"),
|
dimStyle.Render(i18n.T("cmd.dev.impact.summary")),
|
||||||
repoNameStyle.Render(repoName),
|
i18n.T("cmd.dev.impact.changes_affect", map[string]interface{}{
|
||||||
impactDirectStyle.Render(fmt.Sprintf("%d/%d repos", len(allAffected), len(reg.Repos)-1)),
|
"Repo": repoNameStyle.Render(repoName),
|
||||||
|
"Affected": len(allAffected),
|
||||||
|
"Total": len(reg.Repos) - 1,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -60,9 +61,8 @@ var (
|
||||||
func addIssuesCommand(parent *cobra.Command) {
|
func addIssuesCommand(parent *cobra.Command) {
|
||||||
issuesCmd := &cobra.Command{
|
issuesCmd := &cobra.Command{
|
||||||
Use: "issues",
|
Use: "issues",
|
||||||
Short: "List open issues across all repos",
|
Short: i18n.T("cmd.dev.issues.short"),
|
||||||
Long: `Fetches open issues from GitHub for all repos in the registry.
|
Long: i18n.T("cmd.dev.issues.long"),
|
||||||
Requires the 'gh' CLI to be installed and authenticated.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
limit := issuesLimit
|
limit := issuesLimit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
|
|
@ -72,9 +72,9 @@ Requires the 'gh' CLI to be installed and authenticated.`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
issuesCmd.Flags().StringVar(&issuesRegistryPath, "registry", "", i18n.T("cmd.dev.issues.flag.registry"))
|
||||||
issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, "Max issues per repo")
|
issuesCmd.Flags().IntVarP(&issuesLimit, "limit", "l", 10, i18n.T("cmd.dev.issues.flag.limit"))
|
||||||
issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", "Filter by assignee (use @me for yourself)")
|
issuesCmd.Flags().StringVarP(&issuesAssignee, "assignee", "a", "", i18n.T("cmd.dev.issues.flag.assignee"))
|
||||||
|
|
||||||
parent.AddCommand(issuesCmd)
|
parent.AddCommand(issuesCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +82,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
|
||||||
func runIssues(registryPath string, limit int, assignee string) error {
|
func runIssues(registryPath string, limit int, assignee string) error {
|
||||||
// Check gh is available
|
// Check gh is available
|
||||||
if _, err := exec.LookPath("gh"); err != nil {
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
return fmt.Errorf(i18n.T("error.gh_not_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or use provided registry, fall back to directory scan
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
|
@ -118,7 +118,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
|
||||||
repoList := reg.List()
|
repoList := reg.List()
|
||||||
for i, repo := range repoList {
|
for i, repo := range repoList {
|
||||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name)
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.fetching")), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee)
|
issues, err := fetchIssues(repoFullName, repo.Name, limit, assignee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -136,11 +136,11 @@ func runIssues(registryPath string, limit int, assignee string) error {
|
||||||
|
|
||||||
// Print issues
|
// Print issues
|
||||||
if len(allIssues) == 0 {
|
if len(allIssues) == 0 {
|
||||||
fmt.Println("No open issues found.")
|
fmt.Println(i18n.T("cmd.dev.issues.no_issues"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%d open issue(s):\n\n", len(allIssues))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.issues.open_issues", map[string]interface{}{"Count": len(allIssues)}))
|
||||||
|
|
||||||
for _, issue := range allIssues {
|
for _, issue := range allIssues {
|
||||||
printIssue(issue)
|
printIssue(issue)
|
||||||
|
|
@ -150,7 +150,7 @@ func runIssues(registryPath string, limit int, assignee string) error {
|
||||||
if len(fetchErrors) > 0 {
|
if len(fetchErrors) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
for _, err := range fetchErrors {
|
for _, err := range fetchErrors {
|
||||||
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.issues.error_label")), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -21,16 +22,15 @@ var (
|
||||||
func addPullCommand(parent *cobra.Command) {
|
func addPullCommand(parent *cobra.Command) {
|
||||||
pullCmd := &cobra.Command{
|
pullCmd := &cobra.Command{
|
||||||
Use: "pull",
|
Use: "pull",
|
||||||
Short: "Pull updates across all repos",
|
Short: i18n.T("cmd.dev.pull.short"),
|
||||||
Long: `Pulls updates for all repos.
|
Long: i18n.T("cmd.dev.pull.long"),
|
||||||
By default only pulls repos that are behind. Use --all to pull all repos.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPull(pullRegistryPath, pullAll)
|
return runPull(pullRegistryPath, pullAll)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
pullCmd.Flags().StringVar(&pullRegistryPath, "registry", "", i18n.T("cmd.dev.pull.flag.registry"))
|
||||||
pullCmd.Flags().BoolVar(&pullAll, "all", false, "Pull all repos, not just those behind")
|
pullCmd.Flags().BoolVar(&pullAll, "all", false, i18n.T("cmd.dev.pull.flag.all"))
|
||||||
|
|
||||||
parent.AddCommand(pullCmd)
|
parent.AddCommand(pullCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ func runPull(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
registryPath, err = repos.FindRegistry()
|
registryPath, err = repos.FindRegistry()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -55,7 +55,7 @@ func runPull(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: scan current directory
|
// Fallback: scan current directory
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
@ -63,7 +63,7 @@ func runPull(registryPath string, all bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to scan directory: %w", err)
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ func runPull(registryPath string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
fmt.Println("No git repositories found.")
|
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,19 +101,19 @@ func runPull(registryPath string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toPull) == 0 {
|
if len(toPull) == 0 {
|
||||||
fmt.Println("All repos up to date. Nothing to pull.")
|
fmt.Println(i18n.T("cmd.dev.pull.all_up_to_date"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show what we're pulling
|
// Show what we're pulling
|
||||||
if all {
|
if all {
|
||||||
fmt.Printf("\nPulling %d repo(s):\n\n", len(toPull))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.pulling_repos", map[string]interface{}{"Count": len(toPull)}))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%d repo(s) behind upstream:\n\n", len(toPull))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.pull.repos_behind", map[string]interface{}{"Count": len(toPull)}))
|
||||||
for _, s := range toPull {
|
for _, s := range toPull {
|
||||||
fmt.Printf(" %s: %s\n",
|
fmt.Printf(" %s: %s\n",
|
||||||
repoNameStyle.Render(s.Name),
|
repoNameStyle.Render(s.Name),
|
||||||
dimStyle.Render(fmt.Sprintf("%d commit(s) behind", s.Behind)),
|
dimStyle.Render(i18n.T("cmd.dev.pull.commits_behind", map[string]interface{}{"Count": s.Behind})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
@ -122,7 +122,7 @@ func runPull(registryPath string, all bool) error {
|
||||||
// Pull each repo
|
// Pull each repo
|
||||||
var succeeded, failed int
|
var succeeded, failed int
|
||||||
for _, s := range toPull {
|
for _, s := range toPull {
|
||||||
fmt.Printf(" %s %s... ", dimStyle.Render("Pulling"), s.Name)
|
fmt.Printf(" %s %s... ", dimStyle.Render(i18n.T("cmd.dev.pull.pulling")), s.Name)
|
||||||
|
|
||||||
err := gitPull(ctx, s.Path)
|
err := gitPull(ctx, s.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -136,9 +136,9 @@ func runPull(registryPath string, all bool) error {
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d pulled", successStyle.Render("Done:"), succeeded)
|
fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.pull.done_pulled", map[string]interface{}{"Count": succeeded})))
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -21,16 +22,15 @@ var (
|
||||||
func addPushCommand(parent *cobra.Command) {
|
func addPushCommand(parent *cobra.Command) {
|
||||||
pushCmd := &cobra.Command{
|
pushCmd := &cobra.Command{
|
||||||
Use: "push",
|
Use: "push",
|
||||||
Short: "Push commits across all repos",
|
Short: i18n.T("cmd.dev.push.short"),
|
||||||
Long: `Pushes unpushed commits for all repos.
|
Long: i18n.T("cmd.dev.push.long"),
|
||||||
Shows repos with commits to push and confirms before pushing.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPush(pushRegistryPath, pushForce)
|
return runPush(pushRegistryPath, pushForce)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
pushCmd.Flags().StringVar(&pushRegistryPath, "registry", "", i18n.T("cmd.dev.push.flag.registry"))
|
||||||
pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, "Skip confirmation prompt")
|
pushCmd.Flags().BoolVarP(&pushForce, "force", "f", false, i18n.T("cmd.dev.push.flag.force"))
|
||||||
|
|
||||||
parent.AddCommand(pushCmd)
|
parent.AddCommand(pushCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ func runPush(registryPath string, force bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
registryPath, err = repos.FindRegistry()
|
registryPath, err = repos.FindRegistry()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -55,7 +55,7 @@ func runPush(registryPath string, force bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: scan current directory
|
// Fallback: scan current directory
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
@ -63,7 +63,7 @@ func runPush(registryPath string, force bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to scan directory: %w", err)
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Scanning:"), cwd)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ func runPush(registryPath string, force bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
fmt.Println("No git repositories found.")
|
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,17 +98,17 @@ func runPush(registryPath string, force bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(aheadRepos) == 0 {
|
if len(aheadRepos) == 0 {
|
||||||
fmt.Println("All repos up to date. Nothing to push.")
|
fmt.Println(i18n.T("cmd.dev.push.all_up_to_date"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show repos to push
|
// Show repos to push
|
||||||
fmt.Printf("\n%d repo(s) with unpushed commits:\n\n", len(aheadRepos))
|
fmt.Printf("\n%s\n\n", i18n.T("cmd.dev.push.repos_with_unpushed", map[string]interface{}{"Count": len(aheadRepos)}))
|
||||||
totalCommits := 0
|
totalCommits := 0
|
||||||
for _, s := range aheadRepos {
|
for _, s := range aheadRepos {
|
||||||
fmt.Printf(" %s: %s\n",
|
fmt.Printf(" %s: %s\n",
|
||||||
repoNameStyle.Render(s.Name),
|
repoNameStyle.Render(s.Name),
|
||||||
aheadStyle.Render(fmt.Sprintf("%d commit(s)", s.Ahead)),
|
aheadStyle.Render(i18n.T("cmd.dev.push.commits_count", map[string]interface{}{"Count": s.Ahead})),
|
||||||
)
|
)
|
||||||
totalCommits += s.Ahead
|
totalCommits += s.Ahead
|
||||||
}
|
}
|
||||||
|
|
@ -116,8 +116,8 @@ func runPush(registryPath string, force bool) error {
|
||||||
// Confirm unless --force
|
// Confirm unless --force
|
||||||
if !force {
|
if !force {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if !shared.Confirm(fmt.Sprintf("Push %d commit(s) to %d repo(s)?", totalCommits, len(aheadRepos))) {
|
if !shared.Confirm(i18n.T("cmd.dev.push.confirm_push", map[string]interface{}{"Commits": totalCommits, "Repos": len(aheadRepos)})) {
|
||||||
fmt.Println("Aborted.")
|
fmt.Println(i18n.T("cli.aborted"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,9 +145,9 @@ func runPush(registryPath string, force bool) error {
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d pushed", successStyle.Render("Done:"), succeeded)
|
fmt.Printf("%s", successStyle.Render(i18n.T("cmd.dev.push.done_pushed", map[string]interface{}{"Count": succeeded})))
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.dev.count_failed", map[string]interface{}{"Count": failed})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -61,18 +62,16 @@ var (
|
||||||
func addReviewsCommand(parent *cobra.Command) {
|
func addReviewsCommand(parent *cobra.Command) {
|
||||||
reviewsCmd := &cobra.Command{
|
reviewsCmd := &cobra.Command{
|
||||||
Use: "reviews",
|
Use: "reviews",
|
||||||
Short: "List PRs needing review across all repos",
|
Short: i18n.T("cmd.dev.reviews.short"),
|
||||||
Long: `Fetches open PRs from GitHub for all repos in the registry.
|
Long: i18n.T("cmd.dev.reviews.long"),
|
||||||
Shows review status (approved, changes requested, pending).
|
|
||||||
Requires the 'gh' CLI to be installed and authenticated.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll)
|
return runReviews(reviewsRegistryPath, reviewsAuthor, reviewsShowAll)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
reviewsCmd.Flags().StringVar(&reviewsRegistryPath, "registry", "", i18n.T("cmd.dev.reviews.flag.registry"))
|
||||||
reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", "Filter by PR author")
|
reviewsCmd.Flags().StringVar(&reviewsAuthor, "author", "", i18n.T("cmd.dev.reviews.flag.author"))
|
||||||
reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, "Show all PRs including drafts")
|
reviewsCmd.Flags().BoolVar(&reviewsShowAll, "all", false, i18n.T("cmd.dev.reviews.flag.all"))
|
||||||
|
|
||||||
parent.AddCommand(reviewsCmd)
|
parent.AddCommand(reviewsCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +79,7 @@ Requires the 'gh' CLI to be installed and authenticated.`,
|
||||||
func runReviews(registryPath string, author string, showAll bool) error {
|
func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
// Check gh is available
|
// Check gh is available
|
||||||
if _, err := exec.LookPath("gh"); err != nil {
|
if _, err := exec.LookPath("gh"); err != nil {
|
||||||
return fmt.Errorf("'gh' CLI not found. Install from https://cli.github.com/")
|
return fmt.Errorf(i18n.T("error.gh_not_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or use provided registry, fall back to directory scan
|
// Find or use provided registry, fall back to directory scan
|
||||||
|
|
@ -116,7 +115,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
repoList := reg.List()
|
repoList := reg.List()
|
||||||
for i, repo := range repoList {
|
for i, repo := range repoList {
|
||||||
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
repoFullName := fmt.Sprintf("%s/%s", reg.Org, repo.Name)
|
||||||
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render("Fetching"), i+1, len(repoList), repo.Name)
|
fmt.Printf("\033[2K\r%s %d/%d %s", dimStyle.Render(i18n.T("cli.progress.fetching")), i+1, len(repoList), repo.Name)
|
||||||
|
|
||||||
prs, err := fetchPRs(repoFullName, repo.Name, author)
|
prs, err := fetchPRs(repoFullName, repo.Name, author)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -147,7 +146,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
|
|
||||||
// Print PRs
|
// Print PRs
|
||||||
if len(allPRs) == 0 {
|
if len(allPRs) == 0 {
|
||||||
fmt.Println("No open PRs found.")
|
fmt.Println(i18n.T("cmd.dev.reviews.no_prs"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,15 +164,15 @@ func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%d open PR(s)", len(allPRs))
|
fmt.Printf("%s", i18n.T("cmd.dev.reviews.open_prs", map[string]interface{}{"Count": len(allPRs)}))
|
||||||
if pending > 0 {
|
if pending > 0 {
|
||||||
fmt.Printf(" * %s", prPendingStyle.Render(fmt.Sprintf("%d pending", pending)))
|
fmt.Printf(" * %s", prPendingStyle.Render(i18n.T("cmd.dev.reviews.pending", map[string]interface{}{"Count": pending})))
|
||||||
}
|
}
|
||||||
if approved > 0 {
|
if approved > 0 {
|
||||||
fmt.Printf(" * %s", prApprovedStyle.Render(fmt.Sprintf("%d approved", approved)))
|
fmt.Printf(" * %s", prApprovedStyle.Render(i18n.T("cmd.dev.reviews.approved", map[string]interface{}{"Count": approved})))
|
||||||
}
|
}
|
||||||
if changesRequested > 0 {
|
if changesRequested > 0 {
|
||||||
fmt.Printf(" * %s", prChangesStyle.Render(fmt.Sprintf("%d changes requested", changesRequested)))
|
fmt.Printf(" * %s", prChangesStyle.Render(i18n.T("cmd.dev.reviews.changes_requested", map[string]interface{}{"Count": changesRequested})))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
@ -186,7 +185,7 @@ func runReviews(registryPath string, author string, showAll bool) error {
|
||||||
if len(fetchErrors) > 0 {
|
if len(fetchErrors) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
for _, err := range fetchErrors {
|
for _, err := range fetchErrors {
|
||||||
fmt.Printf("%s %s\n", errorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.dev.issues.error_label")), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,17 +241,17 @@ func printPR(pr GitHubPR) {
|
||||||
var status string
|
var status string
|
||||||
switch pr.ReviewDecision {
|
switch pr.ReviewDecision {
|
||||||
case "APPROVED":
|
case "APPROVED":
|
||||||
status = prApprovedStyle.Render("v approved")
|
status = prApprovedStyle.Render(i18n.T("cmd.dev.reviews.status_approved"))
|
||||||
case "CHANGES_REQUESTED":
|
case "CHANGES_REQUESTED":
|
||||||
status = prChangesStyle.Render("* changes requested")
|
status = prChangesStyle.Render(i18n.T("cmd.dev.reviews.status_changes"))
|
||||||
default:
|
default:
|
||||||
status = prPendingStyle.Render("o pending review")
|
status = prPendingStyle.Render(i18n.T("cmd.dev.reviews.status_pending"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draft indicator
|
// Draft indicator
|
||||||
draft := ""
|
draft := ""
|
||||||
if pr.IsDraft {
|
if pr.IsDraft {
|
||||||
draft = prDraftStyle.Render(" [draft]")
|
draft = prDraftStyle.Render(" " + i18n.T("cmd.dev.reviews.draft"))
|
||||||
}
|
}
|
||||||
|
|
||||||
age := shared.FormatAge(pr.CreatedAt)
|
age := shared.FormatAge(pr.CreatedAt)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
@ -19,15 +20,13 @@ import (
|
||||||
func addSyncCommand(parent *cobra.Command) {
|
func addSyncCommand(parent *cobra.Command) {
|
||||||
syncCmd := &cobra.Command{
|
syncCmd := &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Synchronizes the public service APIs with their internal implementations.",
|
Short: i18n.T("cmd.dev.sync.short"),
|
||||||
Long: `This command scans the 'pkg' directory for services and ensures that the
|
Long: i18n.T("cmd.dev.sync.long"),
|
||||||
top-level public API for each service is in sync with its internal implementation.
|
|
||||||
It automatically generates the necessary Go files with type aliases.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := runSync(); err != nil {
|
if err := runSync(); err != nil {
|
||||||
return fmt.Errorf("Error: %w", err)
|
return fmt.Errorf("%s %w", i18n.T("cmd.dev.sync.error_prefix"), err)
|
||||||
}
|
}
|
||||||
fmt.Println("Public APIs synchronized successfully.")
|
fmt.Println(i18n.T("cmd.dev.sync.success"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/devops"
|
"github.com/host-uk/core/pkg/devops"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,14 +29,8 @@ func addVMCommands(parent *cobra.Command) {
|
||||||
func addVMInstallCommand(parent *cobra.Command) {
|
func addVMInstallCommand(parent *cobra.Command) {
|
||||||
installCmd := &cobra.Command{
|
installCmd := &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Download and install the dev environment image",
|
Short: i18n.T("cmd.dev.vm.install.short"),
|
||||||
Long: `Downloads the platform-specific dev environment image.
|
Long: i18n.T("cmd.dev.vm.install.long"),
|
||||||
|
|
||||||
The image includes Go, PHP, Node.js, Python, Docker, and Claude CLI.
|
|
||||||
Downloads are cached at ~/.core/images/
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev install`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMInstall()
|
return runVMInstall()
|
||||||
},
|
},
|
||||||
|
|
@ -51,15 +46,15 @@ func runVMInstall() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.IsInstalled() {
|
if d.IsInstalled() {
|
||||||
fmt.Println(successStyle.Render("Dev environment already installed"))
|
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.already_installed")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Use %s to check for updates\n", dimStyle.Render("core dev update"))
|
fmt.Println(i18n.T("cmd.dev.vm.check_updates", map[string]interface{}{"Command": dimStyle.Render("core dev update")}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), devops.ImageName())
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.image_label")), devops.ImageName())
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Downloading dev environment...")
|
fmt.Println(i18n.T("cmd.dev.vm.downloading"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -70,7 +65,7 @@ func runVMInstall() error {
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
pct := int(float64(downloaded) / float64(total) * 100)
|
pct := int(float64(downloaded) / float64(total) * 100)
|
||||||
if pct != int(float64(lastProgress)/float64(total)*100) {
|
if pct != int(float64(lastProgress)/float64(total)*100) {
|
||||||
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct)
|
fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct)
|
||||||
lastProgress = downloaded
|
lastProgress = downloaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,9 +79,9 @@ func runVMInstall() error {
|
||||||
|
|
||||||
elapsed := time.Since(start).Round(time.Second)
|
elapsed := time.Since(start).Round(time.Second)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s in %s\n", successStyle.Render("Installed"), elapsed)
|
fmt.Println(i18n.T("cmd.dev.vm.installed_in", map[string]interface{}{"Duration": elapsed}))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot"))
|
fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -102,21 +97,16 @@ var (
|
||||||
func addVMBootCommand(parent *cobra.Command) {
|
func addVMBootCommand(parent *cobra.Command) {
|
||||||
bootCmd := &cobra.Command{
|
bootCmd := &cobra.Command{
|
||||||
Use: "boot",
|
Use: "boot",
|
||||||
Short: "Start the dev environment",
|
Short: i18n.T("cmd.dev.vm.boot.short"),
|
||||||
Long: `Boots the dev environment VM.
|
Long: i18n.T("cmd.dev.vm.boot.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev boot
|
|
||||||
core dev boot --memory 8192 --cpus 4
|
|
||||||
core dev boot --fresh`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh)
|
return runVMBoot(vmBootMemory, vmBootCPUs, vmBootFresh)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, "Memory in MB (default: 4096)")
|
bootCmd.Flags().IntVar(&vmBootMemory, "memory", 0, i18n.T("cmd.dev.vm.boot.flag.memory"))
|
||||||
bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, "Number of CPUs (default: 2)")
|
bootCmd.Flags().IntVar(&vmBootCPUs, "cpus", 0, i18n.T("cmd.dev.vm.boot.flag.cpus"))
|
||||||
bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, "Stop existing and start fresh")
|
bootCmd.Flags().BoolVar(&vmBootFresh, "fresh", false, i18n.T("cmd.dev.vm.boot.flag.fresh"))
|
||||||
|
|
||||||
parent.AddCommand(bootCmd)
|
parent.AddCommand(bootCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +118,7 @@ func runVMBoot(memory, cpus int, fresh bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !d.IsInstalled() {
|
if !d.IsInstalled() {
|
||||||
return fmt.Errorf("dev environment not installed (run 'core dev install' first)")
|
return fmt.Errorf(i18n.T("cmd.dev.vm.not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := devops.DefaultBootOptions()
|
opts := devops.DefaultBootOptions()
|
||||||
|
|
@ -140,9 +130,9 @@ func runVMBoot(memory, cpus int, fresh bool) error {
|
||||||
}
|
}
|
||||||
opts.Fresh = fresh
|
opts.Fresh = fresh
|
||||||
|
|
||||||
fmt.Printf("%s %dMB, %d CPUs\n", dimStyle.Render("Config:"), opts.Memory, opts.CPUs)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.config_label")), i18n.T("cmd.dev.vm.config_value", map[string]interface{}{"Memory": opts.Memory, "CPUs": opts.CPUs}))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Booting dev environment...")
|
fmt.Println(i18n.T("cmd.dev.vm.booting"))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := d.Boot(ctx, opts); err != nil {
|
if err := d.Boot(ctx, opts); err != nil {
|
||||||
|
|
@ -150,10 +140,10 @@ func runVMBoot(memory, cpus int, fresh bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println(successStyle.Render("Dev environment running"))
|
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.running")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Connect with: %s\n", dimStyle.Render("core dev shell"))
|
fmt.Println(i18n.T("cmd.dev.vm.connect_with", map[string]interface{}{"Command": dimStyle.Render("core dev shell")}))
|
||||||
fmt.Printf("SSH port: %s\n", dimStyle.Render("2222"))
|
fmt.Printf("%s %s\n", i18n.T("cmd.dev.vm.ssh_port"), dimStyle.Render("2222"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -162,11 +152,8 @@ func runVMBoot(memory, cpus int, fresh bool) error {
|
||||||
func addVMStopCommand(parent *cobra.Command) {
|
func addVMStopCommand(parent *cobra.Command) {
|
||||||
stopCmd := &cobra.Command{
|
stopCmd := &cobra.Command{
|
||||||
Use: "stop",
|
Use: "stop",
|
||||||
Short: "Stop the dev environment",
|
Short: i18n.T("cmd.dev.vm.stop.short"),
|
||||||
Long: `Stops the running dev environment VM.
|
Long: i18n.T("cmd.dev.vm.stop.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev stop`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMStop()
|
return runVMStop()
|
||||||
},
|
},
|
||||||
|
|
@ -188,17 +175,17 @@ func runVMStop() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !running {
|
if !running {
|
||||||
fmt.Println(dimStyle.Render("Dev environment is not running"))
|
fmt.Println(dimStyle.Render(i18n.T("cmd.dev.vm.not_running")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Stopping dev environment...")
|
fmt.Println(i18n.T("cmd.dev.vm.stopping"))
|
||||||
|
|
||||||
if err := d.Stop(ctx); err != nil {
|
if err := d.Stop(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(successStyle.Render("Stopped"))
|
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.stopped")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,11 +193,8 @@ func runVMStop() error {
|
||||||
func addVMStatusCommand(parent *cobra.Command) {
|
func addVMStatusCommand(parent *cobra.Command) {
|
||||||
statusCmd := &cobra.Command{
|
statusCmd := &cobra.Command{
|
||||||
Use: "vm-status",
|
Use: "vm-status",
|
||||||
Short: "Show dev environment status",
|
Short: i18n.T("cmd.dev.vm.status.short"),
|
||||||
Long: `Shows the current status of the dev environment.
|
Long: i18n.T("cmd.dev.vm.status.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev vm-status`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMStatus()
|
return runVMStatus()
|
||||||
},
|
},
|
||||||
|
|
@ -231,19 +215,19 @@ func runVMStatus() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(headerStyle.Render("Dev Environment Status"))
|
fmt.Println(headerStyle.Render(i18n.T("cmd.dev.vm.status_title")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Installation status
|
// Installation status
|
||||||
if status.Installed {
|
if status.Installed {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), successStyle.Render("Yes"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), successStyle.Render(i18n.T("cmd.dev.vm.installed_yes")))
|
||||||
if status.ImageVersion != "" {
|
if status.ImageVersion != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Version:"), status.ImageVersion)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.version_label")), status.ImageVersion)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Installed:"), errorStyle.Render("No"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.installed_label")), errorStyle.Render(i18n.T("cmd.dev.vm.installed_no")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Install with: %s\n", dimStyle.Render("core dev install"))
|
fmt.Println(i18n.T("cmd.dev.vm.install_with", map[string]interface{}{"Command": dimStyle.Render("core dev install")}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,16 +235,16 @@ func runVMStatus() error {
|
||||||
|
|
||||||
// Running status
|
// Running status
|
||||||
if status.Running {
|
if status.Running {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), successStyle.Render("Running"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.status_label")), successStyle.Render(i18n.T("cmd.dev.vm.status_running")))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Container:"), status.ContainerID[:8])
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.container_label")), status.ContainerID[:8])
|
||||||
fmt.Printf("%s %dMB\n", dimStyle.Render("Memory:"), status.Memory)
|
fmt.Printf("%s %dMB\n", dimStyle.Render(i18n.T("cmd.dev.vm.memory_label")), status.Memory)
|
||||||
fmt.Printf("%s %d\n", dimStyle.Render("CPUs:"), status.CPUs)
|
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.cpus_label")), status.CPUs)
|
||||||
fmt.Printf("%s %d\n", dimStyle.Render("SSH Port:"), status.SSHPort)
|
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.dev.vm.ssh_port")), status.SSHPort)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Uptime:"), formatVMUptime(status.Uptime))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.uptime_label")), formatVMUptime(status.Uptime))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), dimStyle.Render("Stopped"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.status_label")), dimStyle.Render(i18n.T("cmd.dev.vm.status_stopped")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Start with: %s\n", dimStyle.Render("core dev boot"))
|
fmt.Println(i18n.T("cmd.dev.vm.start_with", map[string]interface{}{"Command": dimStyle.Render("core dev boot")}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -286,21 +270,14 @@ var vmShellConsole bool
|
||||||
func addVMShellCommand(parent *cobra.Command) {
|
func addVMShellCommand(parent *cobra.Command) {
|
||||||
shellCmd := &cobra.Command{
|
shellCmd := &cobra.Command{
|
||||||
Use: "shell [-- command...]",
|
Use: "shell [-- command...]",
|
||||||
Short: "Connect to the dev environment",
|
Short: i18n.T("cmd.dev.vm.shell.short"),
|
||||||
Long: `Opens an interactive shell in the dev environment.
|
Long: i18n.T("cmd.dev.vm.shell.long"),
|
||||||
|
|
||||||
Uses SSH by default, or serial console with --console.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev shell
|
|
||||||
core dev shell --console
|
|
||||||
core dev shell -- ls -la`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMShell(vmShellConsole, args)
|
return runVMShell(vmShellConsole, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, "Use serial console instead of SSH")
|
shellCmd.Flags().BoolVar(&vmShellConsole, "console", false, i18n.T("cmd.dev.vm.shell.flag.console"))
|
||||||
|
|
||||||
parent.AddCommand(shellCmd)
|
parent.AddCommand(shellCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -330,22 +307,15 @@ var (
|
||||||
func addVMServeCommand(parent *cobra.Command) {
|
func addVMServeCommand(parent *cobra.Command) {
|
||||||
serveCmd := &cobra.Command{
|
serveCmd := &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Mount project and start dev server",
|
Short: i18n.T("cmd.dev.vm.serve.short"),
|
||||||
Long: `Mounts the current project into the dev environment and starts a dev server.
|
Long: i18n.T("cmd.dev.vm.serve.long"),
|
||||||
|
|
||||||
Auto-detects the appropriate serve command based on project files.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev serve
|
|
||||||
core dev serve --port 3000
|
|
||||||
core dev serve --path public`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMServe(vmServePort, vmServePath)
|
return runVMServe(vmServePort, vmServePath)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, "Port to serve on (default: 8000)")
|
serveCmd.Flags().IntVarP(&vmServePort, "port", "p", 0, i18n.T("cmd.dev.vm.serve.flag.port"))
|
||||||
serveCmd.Flags().StringVar(&vmServePath, "path", "", "Subdirectory to serve")
|
serveCmd.Flags().StringVar(&vmServePath, "path", "", i18n.T("cmd.dev.vm.serve.flag.path"))
|
||||||
|
|
||||||
parent.AddCommand(serveCmd)
|
parent.AddCommand(serveCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -377,21 +347,14 @@ var vmTestName string
|
||||||
func addVMTestCommand(parent *cobra.Command) {
|
func addVMTestCommand(parent *cobra.Command) {
|
||||||
testCmd := &cobra.Command{
|
testCmd := &cobra.Command{
|
||||||
Use: "test [-- command...]",
|
Use: "test [-- command...]",
|
||||||
Short: "Run tests in the dev environment",
|
Short: i18n.T("cmd.dev.vm.test.short"),
|
||||||
Long: `Runs tests in the dev environment.
|
Long: i18n.T("cmd.dev.vm.test.long"),
|
||||||
|
|
||||||
Auto-detects the test command based on project files, or uses .core/test.yaml.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev test
|
|
||||||
core dev test --name integration
|
|
||||||
core dev test -- go test -v ./...`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMTest(vmTestName, args)
|
return runVMTest(vmTestName, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", "Run named test command from .core/test.yaml")
|
testCmd.Flags().StringVarP(&vmTestName, "name", "n", "", i18n.T("cmd.dev.vm.test.flag.name"))
|
||||||
|
|
||||||
parent.AddCommand(testCmd)
|
parent.AddCommand(testCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -427,31 +390,16 @@ var (
|
||||||
func addVMClaudeCommand(parent *cobra.Command) {
|
func addVMClaudeCommand(parent *cobra.Command) {
|
||||||
claudeCmd := &cobra.Command{
|
claudeCmd := &cobra.Command{
|
||||||
Use: "claude",
|
Use: "claude",
|
||||||
Short: "Start sandboxed Claude session",
|
Short: i18n.T("cmd.dev.vm.claude.short"),
|
||||||
Long: `Starts a Claude Code session inside the dev environment sandbox.
|
Long: i18n.T("cmd.dev.vm.claude.long"),
|
||||||
|
|
||||||
Provides isolation while forwarding selected credentials.
|
|
||||||
Auto-boots the dev environment if not running.
|
|
||||||
|
|
||||||
Auth options (default: all):
|
|
||||||
gh - GitHub CLI auth
|
|
||||||
anthropic - Anthropic API key
|
|
||||||
ssh - SSH agent forwarding
|
|
||||||
git - Git config (name, email)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev claude
|
|
||||||
core dev claude --model opus
|
|
||||||
core dev claude --auth gh,anthropic
|
|
||||||
core dev claude --no-auth`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags)
|
return runVMClaude(vmClaudeNoAuth, vmClaudeModel, vmClaudeAuthFlags)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, "Don't forward any auth credentials")
|
claudeCmd.Flags().BoolVar(&vmClaudeNoAuth, "no-auth", false, i18n.T("cmd.dev.vm.claude.flag.no_auth"))
|
||||||
claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", "Model to use (opus, sonnet)")
|
claudeCmd.Flags().StringVarP(&vmClaudeModel, "model", "m", "", i18n.T("cmd.dev.vm.claude.flag.model"))
|
||||||
claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, "Selective auth forwarding (gh,anthropic,ssh,git)")
|
claudeCmd.Flags().StringSliceVar(&vmClaudeAuthFlags, "auth", nil, i18n.T("cmd.dev.vm.claude.flag.auth"))
|
||||||
|
|
||||||
parent.AddCommand(claudeCmd)
|
parent.AddCommand(claudeCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -484,18 +432,14 @@ var vmUpdateApply bool
|
||||||
func addVMUpdateCommand(parent *cobra.Command) {
|
func addVMUpdateCommand(parent *cobra.Command) {
|
||||||
updateCmd := &cobra.Command{
|
updateCmd := &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Check for and apply updates",
|
Short: i18n.T("cmd.dev.vm.update.short"),
|
||||||
Long: `Checks for dev environment updates and optionally applies them.
|
Long: i18n.T("cmd.dev.vm.update.long"),
|
||||||
|
|
||||||
Examples:
|
|
||||||
core dev update
|
|
||||||
core dev update --apply`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runVMUpdate(vmUpdateApply)
|
return runVMUpdate(vmUpdateApply)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, "Download and apply the update")
|
updateCmd.Flags().BoolVar(&vmUpdateApply, "apply", false, i18n.T("cmd.dev.vm.update.flag.apply"))
|
||||||
|
|
||||||
parent.AddCommand(updateCmd)
|
parent.AddCommand(updateCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +452,7 @@ func runVMUpdate(apply bool) error {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
fmt.Println("Checking for updates...")
|
fmt.Println(i18n.T("cmd.dev.vm.checking_updates"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
current, latest, hasUpdate, err := d.CheckUpdate(ctx)
|
current, latest, hasUpdate, err := d.CheckUpdate(ctx)
|
||||||
|
|
@ -516,38 +460,38 @@ func runVMUpdate(apply bool) error {
|
||||||
return fmt.Errorf("failed to check for updates: %w", err)
|
return fmt.Errorf("failed to check for updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Current:"), valueStyle.Render(current))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.current_label")), valueStyle.Render(current))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Latest:"), valueStyle.Render(latest))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.vm.latest_label")), valueStyle.Render(latest))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if !hasUpdate {
|
if !hasUpdate {
|
||||||
fmt.Println(successStyle.Render("Already up to date"))
|
fmt.Println(successStyle.Render(i18n.T("cmd.dev.vm.up_to_date")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(warningStyle.Render("Update available"))
|
fmt.Println(warningStyle.Render(i18n.T("cmd.dev.vm.update_available")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if !apply {
|
if !apply {
|
||||||
fmt.Printf("Run %s to update\n", dimStyle.Render("core dev update --apply"))
|
fmt.Println(i18n.T("cmd.dev.vm.run_to_update", map[string]interface{}{"Command": dimStyle.Render("core dev update --apply")}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop if running
|
// Stop if running
|
||||||
running, _ := d.IsRunning(ctx)
|
running, _ := d.IsRunning(ctx)
|
||||||
if running {
|
if running {
|
||||||
fmt.Println("Stopping current instance...")
|
fmt.Println(i18n.T("cmd.dev.vm.stopping_current"))
|
||||||
_ = d.Stop(ctx)
|
_ = d.Stop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Downloading update...")
|
fmt.Println(i18n.T("cmd.dev.vm.downloading_update"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err = d.Install(ctx, func(downloaded, total int64) {
|
err = d.Install(ctx, func(downloaded, total int64) {
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
pct := int(float64(downloaded) / float64(total) * 100)
|
pct := int(float64(downloaded) / float64(total) * 100)
|
||||||
fmt.Printf("\r%s %d%%", dimStyle.Render("Progress:"), pct)
|
fmt.Printf("\r%s %d%%", dimStyle.Render(i18n.T("cmd.dev.vm.progress_label")), pct)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -559,7 +503,7 @@ func runVMUpdate(apply bool) error {
|
||||||
|
|
||||||
elapsed := time.Since(start).Round(time.Second)
|
elapsed := time.Since(start).Round(time.Second)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s in %s\n", successStyle.Render("Updated"), elapsed)
|
fmt.Println(i18n.T("cmd.dev.vm.updated_in", map[string]interface{}{"Duration": elapsed}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
"github.com/host-uk/core/pkg/git"
|
"github.com/host-uk/core/pkg/git"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -26,19 +27,16 @@ var (
|
||||||
func addWorkCommand(parent *cobra.Command) {
|
func addWorkCommand(parent *cobra.Command) {
|
||||||
workCmd := &cobra.Command{
|
workCmd := &cobra.Command{
|
||||||
Use: "work",
|
Use: "work",
|
||||||
Short: "Multi-repo git operations",
|
Short: i18n.T("cmd.dev.work.short"),
|
||||||
Long: `Manage git status, commits, and pushes across multiple repositories.
|
Long: i18n.T("cmd.dev.work.long"),
|
||||||
|
|
||||||
Reads repos.yaml to discover repositories and their relationships.
|
|
||||||
Shows status, optionally commits with Claude, and pushes changes.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runWork(workRegistryPath, workStatusOnly, workAutoCommit)
|
return runWork(workRegistryPath, workStatusOnly, workAutoCommit)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
workCmd.Flags().BoolVar(&workStatusOnly, "status", false, "Show status only, don't push")
|
workCmd.Flags().BoolVar(&workStatusOnly, "status", false, i18n.T("cmd.dev.work.flag.status"))
|
||||||
workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, "Use Claude to commit dirty repos before pushing")
|
workCmd.Flags().BoolVar(&workAutoCommit, "commit", false, i18n.T("cmd.dev.work.flag.commit"))
|
||||||
workCmd.Flags().StringVar(&workRegistryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
workCmd.Flags().StringVar(&workRegistryPath, "registry", "", i18n.T("cmd.dev.work.flag.registry"))
|
||||||
|
|
||||||
parent.AddCommand(workCmd)
|
parent.AddCommand(workCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +53,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
registryPath, err = repos.FindRegistry()
|
registryPath, err = repos.FindRegistry()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -63,7 +61,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf("failed to load registry: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.registry_label")), registryPath)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: scan current directory
|
// Fallback: scan current directory
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
@ -71,7 +69,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to scan directory: %w", err)
|
return fmt.Errorf("failed to scan directory: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Scanning:"), cwd)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +85,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
fmt.Println("No git repositories found.")
|
fmt.Println(i18n.T("cmd.dev.no_git_repos"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +122,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
// Auto-commit dirty repos if requested
|
// Auto-commit dirty repos if requested
|
||||||
if autoCommit && len(dirtyRepos) > 0 {
|
if autoCommit && len(dirtyRepos) > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s\n", shared.TitleStyle.Render("Committing dirty repos with Claude..."))
|
fmt.Printf("%s\n", shared.TitleStyle.Render(i18n.T("cmd.dev.commit.committing")))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
for _, s := range dirtyRepos {
|
for _, s := range dirtyRepos {
|
||||||
|
|
@ -154,7 +152,7 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
if statusOnly {
|
if statusOnly {
|
||||||
if len(dirtyRepos) > 0 && !autoCommit {
|
if len(dirtyRepos) > 0 && !autoCommit {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s\n", dimStyle.Render("Use --commit to have Claude create commits"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.dev.work.use_commit_flag")))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -162,19 +160,19 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error {
|
||||||
// Push repos with unpushed commits
|
// Push repos with unpushed commits
|
||||||
if len(aheadRepos) == 0 {
|
if len(aheadRepos) == 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("All repos up to date.")
|
fmt.Println(i18n.T("cmd.dev.work.all_up_to_date"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%d repo(s) with unpushed commits:\n", len(aheadRepos))
|
fmt.Printf("%s\n", i18n.T("cmd.dev.work.repos_with_unpushed", map[string]interface{}{"Count": len(aheadRepos)}))
|
||||||
for _, s := range aheadRepos {
|
for _, s := range aheadRepos {
|
||||||
fmt.Printf(" %s: %d commit(s)\n", s.Name, s.Ahead)
|
fmt.Printf(" %s: %s\n", s.Name, i18n.T("cmd.dev.work.commits_count", map[string]interface{}{"Count": s.Ahead}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if !shared.Confirm("Push all?") {
|
if !shared.Confirm(i18n.T("cmd.dev.push.confirm")) {
|
||||||
fmt.Println("Aborted.")
|
fmt.Println(i18n.T("cli.aborted"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,11 +209,11 @@ func printStatusTable(statuses []git.RepoStatus) {
|
||||||
// Print header with fixed-width formatting
|
// Print header with fixed-width formatting
|
||||||
fmt.Printf("%-*s %8s %9s %6s %5s\n",
|
fmt.Printf("%-*s %8s %9s %6s %5s\n",
|
||||||
nameWidth,
|
nameWidth,
|
||||||
shared.TitleStyle.Render("Repo"),
|
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_repo")),
|
||||||
shared.TitleStyle.Render("Modified"),
|
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_modified")),
|
||||||
shared.TitleStyle.Render("Untracked"),
|
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_untracked")),
|
||||||
shared.TitleStyle.Render("Staged"),
|
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_staged")),
|
||||||
shared.TitleStyle.Render("Ahead"),
|
shared.TitleStyle.Render(i18n.T("cmd.dev.work.table_ahead")),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Print separator
|
// Print separator
|
||||||
|
|
@ -227,7 +225,7 @@ func printStatusTable(statuses []git.RepoStatus) {
|
||||||
paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name)
|
paddedName := fmt.Sprintf("%-*s", nameWidth, s.Name)
|
||||||
fmt.Printf("%s %s\n",
|
fmt.Printf("%s %s\n",
|
||||||
repoNameStyle.Render(paddedName),
|
repoNameStyle.Render(paddedName),
|
||||||
errorStyle.Render("error: "+s.Error.Error()),
|
errorStyle.Render(i18n.T("cmd.dev.work.error_prefix")+" "+s.Error.Error()),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package docs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,9 +22,8 @@ var (
|
||||||
|
|
||||||
var docsCmd = &cobra.Command{
|
var docsCmd = &cobra.Command{
|
||||||
Use: "docs",
|
Use: "docs",
|
||||||
Short: "Documentation management",
|
Short: i18n.T("cmd.docs.short"),
|
||||||
Long: `Manage documentation across all repos.
|
Long: i18n.T("cmd.docs.long"),
|
||||||
Scan for docs, check coverage, and sync to core-php/docs/packages/.`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -13,14 +14,15 @@ var docsListRegistryPath string
|
||||||
|
|
||||||
var docsListCmd = &cobra.Command{
|
var docsListCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List documentation across repos",
|
Short: i18n.T("cmd.docs.list.short"),
|
||||||
|
Long: i18n.T("cmd.docs.list.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDocsList(docsListRegistryPath)
|
return runDocsList(docsListRegistryPath)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", "Path to repos.yaml")
|
docsListCmd.Flags().StringVar(&docsListRegistryPath, "registry", "", i18n.T("cmd.docs.list.flag.registry"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDocsList(registryPath string) error {
|
func runDocsList(registryPath string) error {
|
||||||
|
|
@ -30,11 +32,11 @@ func runDocsList(registryPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n",
|
fmt.Printf("\n%-20s %-8s %-8s %-10s %s\n",
|
||||||
headerStyle.Render("Repo"),
|
headerStyle.Render(i18n.T("cmd.docs.list.header.repo")),
|
||||||
headerStyle.Render("README"),
|
headerStyle.Render(i18n.T("cmd.docs.list.header.readme")),
|
||||||
headerStyle.Render("CLAUDE"),
|
headerStyle.Render(i18n.T("cmd.docs.list.header.claude")),
|
||||||
headerStyle.Render("CHANGELOG"),
|
headerStyle.Render(i18n.T("cmd.docs.list.header.changelog")),
|
||||||
headerStyle.Render("docs/"),
|
headerStyle.Render(i18n.T("cmd.docs.list.header.docs")),
|
||||||
)
|
)
|
||||||
fmt.Println(strings.Repeat("─", 70))
|
fmt.Println(strings.Repeat("─", 70))
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ func runDocsList(registryPath string) error {
|
||||||
|
|
||||||
docsDir := shared.CheckMark(false)
|
docsDir := shared.CheckMark(false)
|
||||||
if len(info.DocsFiles) > 0 {
|
if len(info.DocsFiles) > 0 {
|
||||||
docsDir = docsFoundStyle.Render(fmt.Sprintf("%d files", len(info.DocsFiles)))
|
docsDir = docsFoundStyle.Render(i18n.T("cmd.docs.list.files_count", map[string]interface{}{"Count": len(info.DocsFiles)}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%-20s %-8s %-8s %-10s %s\n",
|
fmt.Printf("%-20s %-8s %-8s %-10s %s\n",
|
||||||
|
|
@ -67,10 +69,9 @@ func runDocsList(registryPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d with docs, %d without\n",
|
fmt.Printf("%s %s\n",
|
||||||
shared.Label("Coverage"),
|
shared.Label(i18n.T("cmd.docs.list.coverage_label")),
|
||||||
withDocs,
|
i18n.T("cmd.docs.list.coverage_summary", map[string]interface{}{"WithDocs": withDocs, "WithoutDocs": withoutDocs}),
|
||||||
withoutDocs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
|
||||||
if registryPath != "" {
|
if registryPath != "" {
|
||||||
reg, err = repos.LoadRegistry(registryPath)
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to load registry: %w", err)
|
return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.load_registry"), err)
|
||||||
}
|
}
|
||||||
basePath = filepath.Dir(registryPath)
|
basePath = filepath.Dir(registryPath)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -37,14 +38,14 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
reg, err = repos.LoadRegistry(registryPath)
|
reg, err = repos.LoadRegistry(registryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to load registry: %w", err)
|
return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.load_registry"), err)
|
||||||
}
|
}
|
||||||
basePath = filepath.Dir(registryPath)
|
basePath = filepath.Dir(registryPath)
|
||||||
} else {
|
} else {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
reg, err = repos.ScanDirectory(cwd)
|
reg, err = repos.ScanDirectory(cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to scan directory: %w", err)
|
return nil, "", fmt.Errorf("%s: %w", i18n.T("cmd.docs.error.scan_directory"), err)
|
||||||
}
|
}
|
||||||
basePath = cwd
|
basePath = cwd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,16 +19,17 @@ var (
|
||||||
|
|
||||||
var docsSyncCmd = &cobra.Command{
|
var docsSyncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync documentation to core-php/docs/packages/",
|
Short: i18n.T("cmd.docs.sync.short"),
|
||||||
|
Long: i18n.T("cmd.docs.sync.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
|
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", "Path to repos.yaml")
|
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("cmd.docs.sync.flag.registry"))
|
||||||
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, "Show what would be synced without copying")
|
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run"))
|
||||||
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", "Output directory (default: core-php/docs/packages)")
|
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// packageOutputName maps repo name to output folder name
|
// packageOutputName maps repo name to output folder name
|
||||||
|
|
@ -81,11 +83,11 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(docsInfo) == 0 {
|
if len(docsInfo) == 0 {
|
||||||
fmt.Println("No documentation found in any repos.")
|
fmt.Println(i18n.T("cmd.docs.sync.no_docs_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s %d repo(s) with docs/ directories\n\n", dimStyle.Render("Found"), len(docsInfo))
|
fmt.Printf("\n%s %s\n\n", dimStyle.Render(i18n.T("cmd.docs.sync.found_label")), i18n.T("cmd.docs.sync.repos_with_docs", map[string]interface{}{"Count": len(docsInfo)}))
|
||||||
|
|
||||||
// Show what will be synced
|
// Show what will be synced
|
||||||
var totalFiles int
|
var totalFiles int
|
||||||
|
|
@ -95,25 +97,26 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
fmt.Printf(" %s → %s %s\n",
|
fmt.Printf(" %s → %s %s\n",
|
||||||
repoNameStyle.Render(info.Name),
|
repoNameStyle.Render(info.Name),
|
||||||
docsFileStyle.Render("packages/"+outName+"/"),
|
docsFileStyle.Render("packages/"+outName+"/"),
|
||||||
dimStyle.Render(fmt.Sprintf("(%d files)", len(info.DocsFiles))))
|
dimStyle.Render(i18n.T("cmd.docs.sync.files_count", map[string]interface{}{"Count": len(info.DocsFiles)})))
|
||||||
|
|
||||||
for _, f := range info.DocsFiles {
|
for _, f := range info.DocsFiles {
|
||||||
fmt.Printf(" %s\n", dimStyle.Render(f))
|
fmt.Printf(" %s\n", dimStyle.Render(f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s %d files from %d repos → %s\n",
|
fmt.Printf("\n%s %s\n",
|
||||||
dimStyle.Render("Total:"), totalFiles, len(docsInfo), outputDir)
|
dimStyle.Render(i18n.T("cmd.docs.sync.total_label")),
|
||||||
|
i18n.T("cmd.docs.sync.total_summary", map[string]interface{}{"Files": totalFiles, "Repos": len(docsInfo), "Output": outputDir}))
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf("\n%s\n", dimStyle.Render("Dry run - no files copied"))
|
fmt.Printf("\n%s\n", dimStyle.Render(i18n.T("cmd.docs.sync.dry_run_notice")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm
|
// Confirm
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if !confirm("Sync?") {
|
if !confirm(i18n.T("cmd.docs.sync.confirm")) {
|
||||||
fmt.Println("Aborted.")
|
fmt.Println(i18n.T("cli.confirm.abort"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +150,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error {
|
||||||
synced++
|
synced++
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Synced %d packages\n", successStyle.Render("Done:"), synced)
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.docs.sync.done_label")), i18n.T("cmd.docs.sync.synced_packages", map[string]interface{}{"Count": synced}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package doctor
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// check represents a tool check configuration
|
// check represents a tool check configuration
|
||||||
|
|
@ -14,69 +16,73 @@ type check struct {
|
||||||
versionFlag string
|
versionFlag string
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiredChecks are tools that must be installed
|
// requiredChecks returns tools that must be installed
|
||||||
var requiredChecks = []check{
|
func requiredChecks() []check {
|
||||||
|
return []check{
|
||||||
{
|
{
|
||||||
name: "Git",
|
name: i18n.T("cmd.doctor.check.git.name"),
|
||||||
description: "Version control",
|
description: i18n.T("cmd.doctor.check.git.description"),
|
||||||
command: "git",
|
command: "git",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GitHub CLI",
|
name: i18n.T("cmd.doctor.check.gh.name"),
|
||||||
description: "GitHub integration (issues, PRs, CI)",
|
description: i18n.T("cmd.doctor.check.gh.description"),
|
||||||
command: "gh",
|
command: "gh",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PHP",
|
name: i18n.T("cmd.doctor.check.php.name"),
|
||||||
description: "Laravel packages",
|
description: i18n.T("cmd.doctor.check.php.description"),
|
||||||
command: "php",
|
command: "php",
|
||||||
args: []string{"-v"},
|
args: []string{"-v"},
|
||||||
versionFlag: "-v",
|
versionFlag: "-v",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Composer",
|
name: i18n.T("cmd.doctor.check.composer.name"),
|
||||||
description: "PHP dependencies",
|
description: i18n.T("cmd.doctor.check.composer.description"),
|
||||||
command: "composer",
|
command: "composer",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Node.js",
|
name: i18n.T("cmd.doctor.check.node.name"),
|
||||||
description: "Frontend builds",
|
description: i18n.T("cmd.doctor.check.node.description"),
|
||||||
command: "node",
|
command: "node",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// optionalChecks are tools that are nice to have
|
// optionalChecks returns tools that are nice to have
|
||||||
var optionalChecks = []check{
|
func optionalChecks() []check {
|
||||||
|
return []check{
|
||||||
{
|
{
|
||||||
name: "pnpm",
|
name: i18n.T("cmd.doctor.check.pnpm.name"),
|
||||||
description: "Fast package manager",
|
description: i18n.T("cmd.doctor.check.pnpm.description"),
|
||||||
command: "pnpm",
|
command: "pnpm",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Claude Code",
|
name: i18n.T("cmd.doctor.check.claude.name"),
|
||||||
description: "AI-assisted development",
|
description: i18n.T("cmd.doctor.check.claude.description"),
|
||||||
command: "claude",
|
command: "claude",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Docker",
|
name: i18n.T("cmd.doctor.check.docker.name"),
|
||||||
description: "Container runtime",
|
description: i18n.T("cmd.doctor.check.docker.description"),
|
||||||
command: "docker",
|
command: "docker",
|
||||||
args: []string{"--version"},
|
args: []string{"--version"},
|
||||||
versionFlag: "--version",
|
versionFlag: "--version",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runCheck executes a tool check and returns success status and version info
|
// runCheck executes a tool check and returns success status and version info
|
||||||
func runCheck(c check) (bool, string) {
|
func runCheck(c check) (bool, string) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,27 +21,26 @@ var doctorVerbose bool
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
Use: "doctor",
|
Use: "doctor",
|
||||||
Short: "Check development environment",
|
Short: i18n.T("cmd.doctor.short"),
|
||||||
Long: `Checks that all required tools are installed and configured.
|
Long: i18n.T("cmd.doctor.long"),
|
||||||
Run this before 'core setup' to ensure your environment is ready.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDoctor(doctorVerbose)
|
return runDoctor(doctorVerbose)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, "Show detailed version information")
|
doctorCmd.Flags().BoolVar(&doctorVerbose, "verbose", false, i18n.T("cmd.doctor.verbose_flag"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor(verbose bool) error {
|
func runDoctor(verbose bool) error {
|
||||||
fmt.Println("Checking development environment...")
|
fmt.Println(i18n.T("cmd.doctor.checking"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
var passed, failed, optional int
|
var passed, failed, optional int
|
||||||
|
|
||||||
// Check required tools
|
// Check required tools
|
||||||
fmt.Println("Required:")
|
fmt.Println(i18n.T("cmd.doctor.required"))
|
||||||
for _, c := range requiredChecks {
|
for _, c := range requiredChecks() {
|
||||||
ok, version := runCheck(c)
|
ok, version := runCheck(c)
|
||||||
if ok {
|
if ok {
|
||||||
if verbose {
|
if verbose {
|
||||||
|
|
@ -56,8 +56,8 @@ func runDoctor(verbose bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check optional tools
|
// Check optional tools
|
||||||
fmt.Println("\nOptional:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.optional"))
|
||||||
for _, c := range optionalChecks {
|
for _, c := range optionalChecks() {
|
||||||
ok, version := runCheck(c)
|
ok, version := runCheck(c)
|
||||||
if ok {
|
if ok {
|
||||||
if verbose {
|
if verbose {
|
||||||
|
|
@ -73,34 +73,34 @@ func runDoctor(verbose bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check GitHub access
|
// Check GitHub access
|
||||||
fmt.Println("\nGitHub Access:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.github"))
|
||||||
if checkGitHubSSH() {
|
if checkGitHubSSH() {
|
||||||
fmt.Println(shared.CheckResult(true, "SSH key found", ""))
|
fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.ssh_found"), ""))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s SSH key missing - run: ssh-keygen && gh ssh-key add\n", errorStyle.Render(shared.SymbolCross))
|
fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.ssh_missing"))
|
||||||
failed++
|
failed++
|
||||||
}
|
}
|
||||||
|
|
||||||
if checkGitHubCLI() {
|
if checkGitHubCLI() {
|
||||||
fmt.Println(shared.CheckResult(true, "CLI authenticated", ""))
|
fmt.Println(shared.CheckResult(true, i18n.T("cmd.doctor.cli_auth"), ""))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s CLI authentication - run: gh auth login\n", errorStyle.Render(shared.SymbolCross))
|
fmt.Printf(" %s %s\n", errorStyle.Render(shared.SymbolCross), i18n.T("cmd.doctor.cli_auth_missing"))
|
||||||
failed++
|
failed++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check workspace
|
// Check workspace
|
||||||
fmt.Println("\nWorkspace:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.workspace"))
|
||||||
checkWorkspace()
|
checkWorkspace()
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Println(shared.Error(fmt.Sprintf("Doctor: %d issues found", failed)))
|
fmt.Println(shared.Error(i18n.T("cmd.doctor.issues", map[string]interface{}{"Count": failed})))
|
||||||
fmt.Println("\nInstall missing tools:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.doctor.install_missing"))
|
||||||
printInstallInstructions()
|
printInstallInstructions()
|
||||||
return fmt.Errorf("%d required tools missing", failed)
|
return fmt.Errorf("%s", i18n.T("cmd.doctor.issues_error", map[string]interface{}{"Count": failed}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(shared.Success("Doctor: Environment ready"))
|
fmt.Println(shared.Success(i18n.T("cmd.doctor.ready")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ func checkGitHubCLI() bool {
|
||||||
func checkWorkspace() {
|
func checkWorkspace() {
|
||||||
registryPath, err := repos.FindRegistry()
|
registryPath, err := repos.FindRegistry()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf(" %s Found repos.yaml at %s\n", successStyle.Render("✓"), registryPath)
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_yaml_found", map[string]interface{}{"Path": registryPath}))
|
||||||
|
|
||||||
reg, err := repos.LoadRegistry(registryPath)
|
reg, err := repos.LoadRegistry(registryPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -69,9 +70,9 @@ func checkWorkspace() {
|
||||||
cloned++
|
cloned++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s %d/%d repos cloned\n", successStyle.Render("✓"), cloned, len(allRepos))
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.doctor.repos_cloned", map[string]interface{}{"Cloned": cloned, "Total": len(allRepos)}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s No repos.yaml found (run from workspace directory)\n", dimStyle.Render("○"))
|
fmt.Printf(" %s %s\n", dimStyle.Render("○"), i18n.T("cmd.doctor.no_repos_yaml"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,24 @@ package doctor
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// printInstallInstructions prints OS-specific installation instructions
|
// printInstallInstructions prints OS-specific installation instructions
|
||||||
func printInstallInstructions() {
|
func printInstallInstructions() {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
fmt.Println(" brew install git gh php composer node pnpm docker")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos"))
|
||||||
fmt.Println(" brew install --cask claude")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_macos_cask"))
|
||||||
case "linux":
|
case "linux":
|
||||||
fmt.Println(" # Install via your package manager or:")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_header"))
|
||||||
fmt.Println(" # Git: apt install git")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_git"))
|
||||||
fmt.Println(" # GitHub CLI: https://cli.github.com/")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_gh"))
|
||||||
fmt.Println(" # PHP: apt install php8.3-cli")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_php"))
|
||||||
fmt.Println(" # Node: https://nodejs.org/")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_node"))
|
||||||
fmt.Println(" # pnpm: npm install -g pnpm")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_linux_pnpm"))
|
||||||
default:
|
default:
|
||||||
fmt.Println(" See documentation for your OS")
|
fmt.Printf(" %s\n", i18n.T("cmd.doctor.install_other"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
cmd/go/go.go
13
cmd/go/go.go
|
|
@ -5,6 +5,7 @@ package gocmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,16 +20,8 @@ var (
|
||||||
func AddGoCommands(root *cobra.Command) {
|
func AddGoCommands(root *cobra.Command) {
|
||||||
goCmd := &cobra.Command{
|
goCmd := &cobra.Command{
|
||||||
Use: "go",
|
Use: "go",
|
||||||
Short: "Go development tools",
|
Short: i18n.T("cmd.go.short"),
|
||||||
Long: "Go development tools with enhanced output and environment setup.\n\n" +
|
Long: i18n.T("cmd.go.long"),
|
||||||
"Commands:\n" +
|
|
||||||
" test Run tests\n" +
|
|
||||||
" cov Run tests with coverage report\n" +
|
|
||||||
" fmt Format Go code\n" +
|
|
||||||
" lint Run golangci-lint\n" +
|
|
||||||
" install Install Go binary\n" +
|
|
||||||
" mod Module management (tidy, download, verify)\n" +
|
|
||||||
" work Workspace management",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.AddCommand(goCmd)
|
root.AddCommand(goCmd)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,12 +17,8 @@ var (
|
||||||
func addGoFmtCommand(parent *cobra.Command) {
|
func addGoFmtCommand(parent *cobra.Command) {
|
||||||
fmtCmd := &cobra.Command{
|
fmtCmd := &cobra.Command{
|
||||||
Use: "fmt",
|
Use: "fmt",
|
||||||
Short: "Format Go code",
|
Short: i18n.T("cmd.go.fmt.short"),
|
||||||
Long: "Format Go code using gofmt or goimports.\n\n" +
|
Long: i18n.T("cmd.go.fmt.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core go fmt # Check formatting\n" +
|
|
||||||
" core go fmt --fix # Fix formatting\n" +
|
|
||||||
" core go fmt --diff # Show diff",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
fmtArgs := []string{}
|
fmtArgs := []string{}
|
||||||
if fmtFix {
|
if fmtFix {
|
||||||
|
|
@ -49,9 +46,9 @@ func addGoFmtCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Fix formatting in place")
|
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.go.fmt.flag.fix"))
|
||||||
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes")
|
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("cmd.go.fmt.flag.diff"))
|
||||||
fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, "Check only, exit 1 if not formatted")
|
fmtCmd.Flags().BoolVar(&fmtCheck, "check", false, i18n.T("cmd.go.fmt.flag.check"))
|
||||||
|
|
||||||
parent.AddCommand(fmtCmd)
|
parent.AddCommand(fmtCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -61,11 +58,8 @@ var lintFix bool
|
||||||
func addGoLintCommand(parent *cobra.Command) {
|
func addGoLintCommand(parent *cobra.Command) {
|
||||||
lintCmd := &cobra.Command{
|
lintCmd := &cobra.Command{
|
||||||
Use: "lint",
|
Use: "lint",
|
||||||
Short: "Run golangci-lint",
|
Short: i18n.T("cmd.go.lint.short"),
|
||||||
Long: "Run golangci-lint on the codebase.\n\n" +
|
Long: i18n.T("cmd.go.lint.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core go lint\n" +
|
|
||||||
" core go lint --fix",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
lintArgs := []string{"run"}
|
lintArgs := []string{"run"}
|
||||||
if lintFix {
|
if lintFix {
|
||||||
|
|
@ -79,7 +73,7 @@ func addGoLintCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
lintCmd.Flags().BoolVar(&lintFix, "fix", false, "Fix issues automatically")
|
lintCmd.Flags().BoolVar(&lintFix, "fix", false, i18n.T("cmd.go.lint.flag.fix"))
|
||||||
|
|
||||||
parent.AddCommand(lintCmd)
|
parent.AddCommand(lintCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,27 +26,20 @@ var (
|
||||||
func addGoTestCommand(parent *cobra.Command) {
|
func addGoTestCommand(parent *cobra.Command) {
|
||||||
testCmd := &cobra.Command{
|
testCmd := &cobra.Command{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
Short: "Run tests with coverage",
|
Short: i18n.T("cmd.go.test.short"),
|
||||||
Long: "Run Go tests with coverage reporting.\n\n" +
|
Long: i18n.T("cmd.go.test.long"),
|
||||||
"Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings.\n" +
|
|
||||||
"Filters noisy output and provides colour-coded coverage.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core go test\n" +
|
|
||||||
" core go test --coverage\n" +
|
|
||||||
" core go test --pkg ./pkg/crypt\n" +
|
|
||||||
" core go test --run TestHash",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose)
|
return runGoTest(testCoverage, testPkg, testRun, testShort, testRace, testJSON, testVerbose)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage")
|
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.go.test.flag.coverage"))
|
||||||
testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package to test (default: ./...)")
|
testCmd.Flags().StringVar(&testPkg, "pkg", "", i18n.T("cmd.go.test.flag.pkg"))
|
||||||
testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching regexp")
|
testCmd.Flags().StringVar(&testRun, "run", "", i18n.T("cmd.go.test.flag.run"))
|
||||||
testCmd.Flags().BoolVar(&testShort, "short", false, "Run only short tests")
|
testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.go.test.flag.short"))
|
||||||
testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector")
|
testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.go.test.flag.race"))
|
||||||
testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON results")
|
testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.go.test.flag.json"))
|
||||||
testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, "Verbose output")
|
testCmd.Flags().BoolVarP(&testVerbose, "verbose", "v", false, i18n.T("cmd.go.test.flag.verbose"))
|
||||||
|
|
||||||
parent.AddCommand(testCmd)
|
parent.AddCommand(testCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -79,8 +73,8 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
|
||||||
args = append(args, pkg)
|
args = append(args, pkg)
|
||||||
|
|
||||||
if !jsonOut {
|
if !jsonOut {
|
||||||
fmt.Printf("%s Running tests\n", dimStyle.Render("Test:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.test.label")), i18n.T("cmd.go.test.running"))
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), pkg)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.test.package_label")), pkg)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,19 +113,19 @@ func runGoTest(coverage bool, pkg, run string, short, race, jsonOut, verbose boo
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf(" %s %d passed\n", successStyle.Render("✓"), passed)
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.go.test.passed", map[string]interface{}{"Count": passed}))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s %d passed, %d failed\n", errorStyle.Render("✗"), passed, failed)
|
fmt.Printf(" %s %s\n", errorStyle.Render("✗"), i18n.T("cmd.go.test.passed_failed", map[string]interface{}{"Passed": passed, "Failed": failed}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cov > 0 {
|
if cov > 0 {
|
||||||
fmt.Printf("\n %s %s\n", shared.ProgressLabel("Coverage"), shared.FormatCoverage(cov))
|
fmt.Printf("\n %s %s\n", shared.ProgressLabel(i18n.T("cmd.go.test.coverage")), shared.FormatCoverage(cov))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("\n%s\n", successStyle.Render("PASS All tests passed"))
|
fmt.Printf("\n%s\n", successStyle.Render(i18n.T("cmd.go.test.all_passed")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s\n", errorStyle.Render("FAIL Some tests failed"))
|
fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.test.some_failed")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
@ -174,23 +168,18 @@ var (
|
||||||
func addGoCovCommand(parent *cobra.Command) {
|
func addGoCovCommand(parent *cobra.Command) {
|
||||||
covCmd := &cobra.Command{
|
covCmd := &cobra.Command{
|
||||||
Use: "cov",
|
Use: "cov",
|
||||||
Short: "Run tests with coverage report",
|
Short: i18n.T("cmd.go.cov.short"),
|
||||||
Long: "Run tests and generate coverage report.\n\n" +
|
Long: i18n.T("cmd.go.cov.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core go cov # Run with coverage summary\n" +
|
|
||||||
" core go cov --html # Generate HTML report\n" +
|
|
||||||
" core go cov --open # Generate and open HTML report\n" +
|
|
||||||
" core go cov --threshold 80 # Fail if coverage < 80%",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
pkg := covPkg
|
pkg := covPkg
|
||||||
if pkg == "" {
|
if pkg == "" {
|
||||||
// Auto-discover packages with tests
|
// Auto-discover packages with tests
|
||||||
pkgs, err := findTestPackages(".")
|
pkgs, err := findTestPackages(".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to discover test packages: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.discover"), err)
|
||||||
}
|
}
|
||||||
if len(pkgs) == 0 {
|
if len(pkgs) == 0 {
|
||||||
return fmt.Errorf("no test packages found")
|
return fmt.Errorf(i18n.T("cmd.go.cov.error.no_packages"))
|
||||||
}
|
}
|
||||||
pkg = strings.Join(pkgs, " ")
|
pkg = strings.Join(pkgs, " ")
|
||||||
}
|
}
|
||||||
|
|
@ -198,19 +187,19 @@ func addGoCovCommand(parent *cobra.Command) {
|
||||||
// Create temp file for coverage data
|
// Create temp file for coverage data
|
||||||
covFile, err := os.CreateTemp("", "coverage-*.out")
|
covFile, err := os.CreateTemp("", "coverage-*.out")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create coverage file: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.create_file"), err)
|
||||||
}
|
}
|
||||||
covPath := covFile.Name()
|
covPath := covFile.Name()
|
||||||
covFile.Close()
|
covFile.Close()
|
||||||
defer os.Remove(covPath)
|
defer os.Remove(covPath)
|
||||||
|
|
||||||
fmt.Printf("%s Running tests with coverage\n", dimStyle.Render("Coverage:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.cov.label")), i18n.T("cmd.go.cov.running"))
|
||||||
// Truncate package list if too long for display
|
// Truncate package list if too long for display
|
||||||
displayPkg := pkg
|
displayPkg := pkg
|
||||||
if len(displayPkg) > 60 {
|
if len(displayPkg) > 60 {
|
||||||
displayPkg = displayPkg[:57] + "..."
|
displayPkg = displayPkg[:57] + "..."
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Package:"), displayPkg)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.test.package_label")), displayPkg)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Run tests with coverage
|
// Run tests with coverage
|
||||||
|
|
@ -232,7 +221,7 @@ func addGoCovCommand(parent *cobra.Command) {
|
||||||
if testErr != nil {
|
if testErr != nil {
|
||||||
return testErr
|
return testErr
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to get coverage: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.get_coverage"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse total coverage from last line
|
// Parse total coverage from last line
|
||||||
|
|
@ -252,16 +241,16 @@ func addGoCovCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
// Print coverage summary
|
// Print coverage summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf(" %s %s\n", shared.ProgressLabel("Total"), shared.FormatCoverage(totalCov))
|
fmt.Printf(" %s %s\n", shared.ProgressLabel(i18n.T("label.total")), shared.FormatCoverage(totalCov))
|
||||||
|
|
||||||
// Generate HTML if requested
|
// Generate HTML if requested
|
||||||
if covHTML || covOpen {
|
if covHTML || covOpen {
|
||||||
htmlPath := "coverage.html"
|
htmlPath := "coverage.html"
|
||||||
htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath)
|
htmlCmd := exec.Command("go", "tool", "cover", "-html="+covPath, "-o="+htmlPath)
|
||||||
if err := htmlCmd.Run(); err != nil {
|
if err := htmlCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to generate HTML: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.go.cov.error.generate_html"), err)
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("HTML:"), htmlPath)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.cov.html_label")), htmlPath)
|
||||||
|
|
||||||
if covOpen {
|
if covOpen {
|
||||||
// Open in browser
|
// Open in browser
|
||||||
|
|
@ -272,7 +261,7 @@ func addGoCovCommand(parent *cobra.Command) {
|
||||||
case exec.Command("which", "xdg-open").Run() == nil:
|
case exec.Command("which", "xdg-open").Run() == nil:
|
||||||
openCmd = exec.Command("xdg-open", htmlPath)
|
openCmd = exec.Command("xdg-open", htmlPath)
|
||||||
default:
|
default:
|
||||||
fmt.Printf(" %s\n", dimStyle.Render("(open manually)"))
|
fmt.Printf(" %s\n", dimStyle.Render(i18n.T("cmd.go.cov.open_manually")))
|
||||||
}
|
}
|
||||||
if openCmd != nil {
|
if openCmd != nil {
|
||||||
openCmd.Run()
|
openCmd.Run()
|
||||||
|
|
@ -282,24 +271,26 @@ func addGoCovCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
// Check threshold
|
// Check threshold
|
||||||
if covThreshold > 0 && totalCov < covThreshold {
|
if covThreshold > 0 && totalCov < covThreshold {
|
||||||
fmt.Printf("\n%s Coverage %.1f%% is below threshold %.1f%%\n",
|
fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.cov.below_threshold", map[string]interface{}{
|
||||||
errorStyle.Render("FAIL"), totalCov, covThreshold)
|
"Actual": fmt.Sprintf("%.1f", totalCov),
|
||||||
return fmt.Errorf("coverage below threshold")
|
"Threshold": fmt.Sprintf("%.1f", covThreshold),
|
||||||
|
})))
|
||||||
|
return fmt.Errorf(i18n.T("cmd.go.cov.error.below_threshold"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if testErr != nil {
|
if testErr != nil {
|
||||||
return testErr
|
return testErr
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s\n", successStyle.Render("OK"))
|
fmt.Printf("\n%s\n", successStyle.Render(i18n.T("cli.ok")))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
covCmd.Flags().StringVar(&covPkg, "pkg", "", "Package to test (default: ./...)")
|
covCmd.Flags().StringVar(&covPkg, "pkg", "", i18n.T("cmd.go.cov.flag.pkg"))
|
||||||
covCmd.Flags().BoolVar(&covHTML, "html", false, "Generate HTML coverage report")
|
covCmd.Flags().BoolVar(&covHTML, "html", false, i18n.T("cmd.go.cov.flag.html"))
|
||||||
covCmd.Flags().BoolVar(&covOpen, "open", false, "Generate and open HTML report in browser")
|
covCmd.Flags().BoolVar(&covOpen, "open", false, i18n.T("cmd.go.cov.flag.open"))
|
||||||
covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, "Minimum coverage percentage (exit 1 if below)")
|
covCmd.Flags().Float64Var(&covThreshold, "threshold", 0, i18n.T("cmd.go.cov.flag.threshold"))
|
||||||
|
|
||||||
parent.AddCommand(covCmd)
|
parent.AddCommand(covCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,13 +18,8 @@ var (
|
||||||
func addGoInstallCommand(parent *cobra.Command) {
|
func addGoInstallCommand(parent *cobra.Command) {
|
||||||
installCmd := &cobra.Command{
|
installCmd := &cobra.Command{
|
||||||
Use: "install [path]",
|
Use: "install [path]",
|
||||||
Short: "Install Go binary",
|
Short: i18n.T("cmd.go.install.short"),
|
||||||
Long: "Install Go binary to $GOPATH/bin.\n\n" +
|
Long: i18n.T("cmd.go.install.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core go install # Install current module\n" +
|
|
||||||
" core go install ./cmd/core # Install specific path\n" +
|
|
||||||
" core go install --no-cgo # Pure Go (no C dependencies)\n" +
|
|
||||||
" core go install -v # Verbose output",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// Get install path from args or default to current dir
|
// Get install path from args or default to current dir
|
||||||
installPath := "./..."
|
installPath := "./..."
|
||||||
|
|
@ -42,10 +38,10 @@ func addGoInstallCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Installing\n", dimStyle.Render("Install:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.go.install.label")), i18n.T("cmd.go.install.installing"))
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), installPath)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.install.path_label")), installPath)
|
||||||
if installNoCgo {
|
if installNoCgo {
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("CGO:"), "disabled")
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.go.install.cgo_label")), i18n.T("cmd.go.install.cgo_disabled"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"install"}
|
cmdArgs := []string{"install"}
|
||||||
|
|
@ -62,7 +58,7 @@ func addGoInstallCommand(parent *cobra.Command) {
|
||||||
execCmd.Stderr = os.Stderr
|
execCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
if err := execCmd.Run(); err != nil {
|
if err := execCmd.Run(); err != nil {
|
||||||
fmt.Printf("\n%s\n", errorStyle.Render("FAIL Install failed"))
|
fmt.Printf("\n%s\n", errorStyle.Render(i18n.T("cmd.go.install.failed")))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,13 +70,13 @@ func addGoInstallCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
binDir := filepath.Join(gopath, "bin")
|
binDir := filepath.Join(gopath, "bin")
|
||||||
|
|
||||||
fmt.Printf("\n%s Installed to %s\n", successStyle.Render("OK"), binDir)
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.go.install.success")), i18n.T("cmd.go.install.installed_to", map[string]interface{}{"Path": binDir}))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, "Verbose output")
|
installCmd.Flags().BoolVarP(&installVerbose, "verbose", "v", false, i18n.T("cmd.go.install.flag.verbose"))
|
||||||
installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, "Disable CGO (CGO_ENABLED=0)")
|
installCmd.Flags().BoolVar(&installNoCgo, "no-cgo", false, i18n.T("cmd.go.install.flag.no_cgo"))
|
||||||
|
|
||||||
parent.AddCommand(installCmd)
|
parent.AddCommand(installCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -88,19 +84,14 @@ func addGoInstallCommand(parent *cobra.Command) {
|
||||||
func addGoModCommand(parent *cobra.Command) {
|
func addGoModCommand(parent *cobra.Command) {
|
||||||
modCmd := &cobra.Command{
|
modCmd := &cobra.Command{
|
||||||
Use: "mod",
|
Use: "mod",
|
||||||
Short: "Module management",
|
Short: i18n.T("cmd.go.mod.short"),
|
||||||
Long: "Go module management commands.\n\n" +
|
Long: i18n.T("cmd.go.mod.long"),
|
||||||
"Commands:\n" +
|
|
||||||
" tidy Add missing and remove unused modules\n" +
|
|
||||||
" download Download modules to local cache\n" +
|
|
||||||
" verify Verify dependencies\n" +
|
|
||||||
" graph Print module dependency graph",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tidy
|
// tidy
|
||||||
tidyCmd := &cobra.Command{
|
tidyCmd := &cobra.Command{
|
||||||
Use: "tidy",
|
Use: "tidy",
|
||||||
Short: "Tidy go.mod",
|
Short: i18n.T("cmd.go.mod.tidy.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "mod", "tidy")
|
execCmd := exec.Command("go", "mod", "tidy")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -112,7 +103,7 @@ func addGoModCommand(parent *cobra.Command) {
|
||||||
// download
|
// download
|
||||||
downloadCmd := &cobra.Command{
|
downloadCmd := &cobra.Command{
|
||||||
Use: "download",
|
Use: "download",
|
||||||
Short: "Download modules",
|
Short: i18n.T("cmd.go.mod.download.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "mod", "download")
|
execCmd := exec.Command("go", "mod", "download")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -124,7 +115,7 @@ func addGoModCommand(parent *cobra.Command) {
|
||||||
// verify
|
// verify
|
||||||
verifyCmd := &cobra.Command{
|
verifyCmd := &cobra.Command{
|
||||||
Use: "verify",
|
Use: "verify",
|
||||||
Short: "Verify dependencies",
|
Short: i18n.T("cmd.go.mod.verify.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "mod", "verify")
|
execCmd := exec.Command("go", "mod", "verify")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -136,7 +127,7 @@ func addGoModCommand(parent *cobra.Command) {
|
||||||
// graph
|
// graph
|
||||||
graphCmd := &cobra.Command{
|
graphCmd := &cobra.Command{
|
||||||
Use: "graph",
|
Use: "graph",
|
||||||
Short: "Print dependency graph",
|
Short: i18n.T("cmd.go.mod.graph.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "mod", "graph")
|
execCmd := exec.Command("go", "mod", "graph")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -155,18 +146,14 @@ func addGoModCommand(parent *cobra.Command) {
|
||||||
func addGoWorkCommand(parent *cobra.Command) {
|
func addGoWorkCommand(parent *cobra.Command) {
|
||||||
workCmd := &cobra.Command{
|
workCmd := &cobra.Command{
|
||||||
Use: "work",
|
Use: "work",
|
||||||
Short: "Workspace management",
|
Short: i18n.T("cmd.go.work.short"),
|
||||||
Long: "Go workspace management commands.\n\n" +
|
Long: i18n.T("cmd.go.work.long"),
|
||||||
"Commands:\n" +
|
|
||||||
" sync Sync go.work with modules\n" +
|
|
||||||
" init Initialize go.work\n" +
|
|
||||||
" use Add module to workspace",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync
|
// sync
|
||||||
syncCmd := &cobra.Command{
|
syncCmd := &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync workspace",
|
Short: i18n.T("cmd.go.work.sync.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "work", "sync")
|
execCmd := exec.Command("go", "work", "sync")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -178,7 +165,7 @@ func addGoWorkCommand(parent *cobra.Command) {
|
||||||
// init
|
// init
|
||||||
initCmd := &cobra.Command{
|
initCmd := &cobra.Command{
|
||||||
Use: "init",
|
Use: "init",
|
||||||
Short: "Initialize workspace",
|
Short: i18n.T("cmd.go.work.init.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
execCmd := exec.Command("go", "work", "init")
|
execCmd := exec.Command("go", "work", "init")
|
||||||
execCmd.Stdout = os.Stdout
|
execCmd.Stdout = os.Stdout
|
||||||
|
|
@ -200,13 +187,13 @@ func addGoWorkCommand(parent *cobra.Command) {
|
||||||
// use
|
// use
|
||||||
useCmd := &cobra.Command{
|
useCmd := &cobra.Command{
|
||||||
Use: "use [modules...]",
|
Use: "use [modules...]",
|
||||||
Short: "Add module to workspace",
|
Short: i18n.T("cmd.go.work.use.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
// Auto-detect modules
|
// Auto-detect modules
|
||||||
modules := findGoModules(".")
|
modules := findGoModules(".")
|
||||||
if len(modules) == 0 {
|
if len(modules) == 0 {
|
||||||
return fmt.Errorf("no go.mod files found")
|
return fmt.Errorf(i18n.T("cmd.go.work.error.no_modules"))
|
||||||
}
|
}
|
||||||
for _, mod := range modules {
|
for _, mod := range modules {
|
||||||
execCmd := exec.Command("go", "work", "use", mod)
|
execCmd := exec.Command("go", "work", "use", mod)
|
||||||
|
|
@ -215,7 +202,7 @@ func addGoWorkCommand(parent *cobra.Command) {
|
||||||
if err := execCmd.Run(); err != nil {
|
if err := execCmd.Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("Added %s\n", mod)
|
fmt.Println(i18n.T("cmd.go.work.added", map[string]interface{}{"Module": mod}))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package php
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,14 +52,8 @@ var (
|
||||||
func AddPHPCommands(root *cobra.Command) {
|
func AddPHPCommands(root *cobra.Command) {
|
||||||
phpCmd := &cobra.Command{
|
phpCmd := &cobra.Command{
|
||||||
Use: "php",
|
Use: "php",
|
||||||
Short: "Laravel/PHP development tools",
|
Short: i18n.T("cmd.php.short"),
|
||||||
Long: "Manage Laravel development environment with FrankenPHP.\n\n" +
|
Long: i18n.T("cmd.php.long"),
|
||||||
"Services orchestrated:\n" +
|
|
||||||
" - FrankenPHP/Octane (port 8000, HTTPS on 443)\n" +
|
|
||||||
" - Vite dev server (port 5173)\n" +
|
|
||||||
" - Laravel Horizon (queue workers)\n" +
|
|
||||||
" - Laravel Reverb (WebSocket, port 8080)\n" +
|
|
||||||
" - Redis (port 6379)",
|
|
||||||
}
|
}
|
||||||
root.AddCommand(phpCmd)
|
root.AddCommand(phpCmd)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
phppkg "github.com/host-uk/core/pkg/php"
|
phppkg "github.com/host-uk/core/pkg/php"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -25,19 +26,12 @@ var (
|
||||||
func addPHPBuildCommand(parent *cobra.Command) {
|
func addPHPBuildCommand(parent *cobra.Command) {
|
||||||
buildCmd := &cobra.Command{
|
buildCmd := &cobra.Command{
|
||||||
Use: "build",
|
Use: "build",
|
||||||
Short: "Build Docker or LinuxKit image",
|
Short: i18n.T("cmd.php.build.short"),
|
||||||
Long: "Build a production-ready container image for the PHP project.\n\n" +
|
Long: i18n.T("cmd.php.build.long"),
|
||||||
"By default, builds a Docker image using FrankenPHP.\n" +
|
|
||||||
"Use --type linuxkit to build a LinuxKit VM image instead.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php build # Build Docker image\n" +
|
|
||||||
" core php build --name myapp --tag v1.0 # Build with custom name/tag\n" +
|
|
||||||
" core php build --type linuxkit # Build LinuxKit image\n" +
|
|
||||||
" core php build --type linuxkit --format iso # Build ISO image",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
@ -61,15 +55,15 @@ func addPHPBuildCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCmd.Flags().StringVar(&buildType, "type", "", "Build type: docker (default) or linuxkit")
|
buildCmd.Flags().StringVar(&buildType, "type", "", i18n.T("cmd.php.build.flag.type"))
|
||||||
buildCmd.Flags().StringVar(&buildImageName, "name", "", "Image name (default: project directory name)")
|
buildCmd.Flags().StringVar(&buildImageName, "name", "", i18n.T("cmd.php.build.flag.name"))
|
||||||
buildCmd.Flags().StringVar(&buildTag, "tag", "", "Image tag (default: latest)")
|
buildCmd.Flags().StringVar(&buildTag, "tag", "", i18n.T("cmd.php.build.flag.tag"))
|
||||||
buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64, linux/arm64)")
|
buildCmd.Flags().StringVar(&buildPlatform, "platform", "", i18n.T("cmd.php.build.flag.platform"))
|
||||||
buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", "Path to custom Dockerfile")
|
buildCmd.Flags().StringVar(&buildDockerfile, "dockerfile", "", i18n.T("cmd.php.build.flag.dockerfile"))
|
||||||
buildCmd.Flags().StringVar(&buildOutputPath, "output", "", "Output path for LinuxKit image")
|
buildCmd.Flags().StringVar(&buildOutputPath, "output", "", i18n.T("cmd.php.build.flag.output"))
|
||||||
buildCmd.Flags().StringVar(&buildFormat, "format", "", "LinuxKit output format: qcow2 (default), iso, raw, vmdk")
|
buildCmd.Flags().StringVar(&buildFormat, "format", "", i18n.T("cmd.php.build.flag.format"))
|
||||||
buildCmd.Flags().StringVar(&buildTemplate, "template", "", "LinuxKit template name (default: server-php)")
|
buildCmd.Flags().StringVar(&buildTemplate, "template", "", i18n.T("cmd.php.build.flag.template"))
|
||||||
buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, "Build without cache")
|
buildCmd.Flags().BoolVar(&buildNoCache, "no-cache", false, i18n.T("cmd.php.build.flag.no_cache"))
|
||||||
|
|
||||||
parent.AddCommand(buildCmd)
|
parent.AddCommand(buildCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -90,23 +84,23 @@ type linuxKitBuildOptions struct {
|
||||||
|
|
||||||
func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error {
|
func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildOptions) error {
|
||||||
if !phppkg.IsPHPProject(projectDir) {
|
if !phppkg.IsPHPProject(projectDir) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Building Docker image...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_docker"))
|
||||||
|
|
||||||
// Show detected configuration
|
// Show detected configuration
|
||||||
config, err := phppkg.DetectDockerfileConfig(projectDir)
|
config, err := phppkg.DetectDockerfileConfig(projectDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to detect project configuration: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.detect_config"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("PHP Version:"), config.PHPVersion)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.php_version")), config.PHPVersion)
|
||||||
fmt.Printf("%s %v\n", dimStyle.Render("Laravel:"), config.IsLaravel)
|
fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.laravel")), config.IsLaravel)
|
||||||
fmt.Printf("%s %v\n", dimStyle.Render("Octane:"), config.HasOctane)
|
fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.octane")), config.HasOctane)
|
||||||
fmt.Printf("%s %v\n", dimStyle.Render("Frontend:"), config.HasAssets)
|
fmt.Printf("%s %v\n", dimStyle.Render(i18n.T("cmd.php.build.frontend")), config.HasAssets)
|
||||||
if len(config.PHPExtensions) > 0 {
|
if len(config.PHPExtensions) > 0 {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Extensions:"), strings.Join(config.PHPExtensions, ", "))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.extensions")), strings.Join(config.PHPExtensions, ", "))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
|
@ -134,19 +128,19 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
|
||||||
buildOpts.Tag = "latest"
|
buildOpts.Tag = "latest"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), buildOpts.ImageName, buildOpts.Tag)
|
fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.T("cmd.php.build.image")), buildOpts.ImageName, buildOpts.Tag)
|
||||||
if opts.Platform != "" {
|
if opts.Platform != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Platform:"), opts.Platform)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.platform")), opts.Platform)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if err := phppkg.BuildDocker(ctx, buildOpts); err != nil {
|
if err := phppkg.BuildDocker(ctx, buildOpts); err != nil {
|
||||||
return fmt.Errorf("build failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.build_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Docker image built successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.build.docker_success"))
|
||||||
fmt.Printf("%s docker run -p 80:80 -p 443:443 %s:%s\n",
|
fmt.Printf("%s docker run -p 80:80 -p 443:443 %s:%s\n",
|
||||||
dimStyle.Render("Run with:"),
|
dimStyle.Render(i18n.T("cmd.php.build.docker_run_with")),
|
||||||
buildOpts.ImageName, buildOpts.Tag)
|
buildOpts.ImageName, buildOpts.Tag)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -154,10 +148,10 @@ func runPHPBuildDocker(ctx context.Context, projectDir string, opts dockerBuildO
|
||||||
|
|
||||||
func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error {
|
func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBuildOptions) error {
|
||||||
if !phppkg.IsPHPProject(projectDir) {
|
if !phppkg.IsPHPProject(projectDir) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Building LinuxKit image...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.build.building_linuxkit"))
|
||||||
|
|
||||||
buildOpts := phppkg.LinuxKitBuildOptions{
|
buildOpts := phppkg.LinuxKitBuildOptions{
|
||||||
ProjectDir: projectDir,
|
ProjectDir: projectDir,
|
||||||
|
|
@ -174,15 +168,15 @@ func runPHPBuildLinuxKit(ctx context.Context, projectDir string, opts linuxKitBu
|
||||||
buildOpts.Template = "server-php"
|
buildOpts.Template = "server-php"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Template:"), buildOpts.Template)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.template")), buildOpts.Template)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Format:"), buildOpts.Format)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.build.format")), buildOpts.Format)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil {
|
if err := phppkg.BuildLinuxKit(ctx, buildOpts); err != nil {
|
||||||
return fmt.Errorf("build failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.build_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s LinuxKit image built successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.build.linuxkit_success"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,13 +193,8 @@ var (
|
||||||
func addPHPServeCommand(parent *cobra.Command) {
|
func addPHPServeCommand(parent *cobra.Command) {
|
||||||
serveCmd := &cobra.Command{
|
serveCmd := &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Run production container",
|
Short: i18n.T("cmd.php.serve.short"),
|
||||||
Long: "Run a production PHP container.\n\n" +
|
Long: i18n.T("cmd.php.serve.long"),
|
||||||
"This starts the built Docker image in production mode.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php serve --name myapp # Run container\n" +
|
|
||||||
" core php serve --name myapp -d # Run detached\n" +
|
|
||||||
" core php serve --name myapp --port 8080 # Custom port",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
imageName := serveImageName
|
imageName := serveImageName
|
||||||
if imageName == "" {
|
if imageName == "" {
|
||||||
|
|
@ -218,7 +207,7 @@ func addPHPServeCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if imageName == "" {
|
if imageName == "" {
|
||||||
return fmt.Errorf("--name is required: specify the Docker image name")
|
return fmt.Errorf(i18n.T("cmd.php.serve.name_required"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,8 +224,8 @@ func addPHPServeCommand(parent *cobra.Command) {
|
||||||
Output: os.Stdout,
|
Output: os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Running production container...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.running"))
|
||||||
fmt.Printf("%s %s:%s\n", dimStyle.Render("Image:"), imageName, func() string {
|
fmt.Printf("%s %s:%s\n", dimStyle.Render(i18n.T("cmd.php.build.image")), imageName, func() string {
|
||||||
if serveTag == "" {
|
if serveTag == "" {
|
||||||
return "latest"
|
return "latest"
|
||||||
}
|
}
|
||||||
|
|
@ -257,24 +246,24 @@ func addPHPServeCommand(parent *cobra.Command) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if err := phppkg.ServeProduction(ctx, opts); err != nil {
|
if err := phppkg.ServeProduction(ctx, opts); err != nil {
|
||||||
return fmt.Errorf("failed to start container: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.start_container"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !serveDetach {
|
if !serveDetach {
|
||||||
fmt.Printf("\n%s Container stopped\n", dimStyle.Render("PHP:"))
|
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.serve.stopped"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
serveCmd.Flags().StringVar(&serveImageName, "name", "", "Docker image name (required)")
|
serveCmd.Flags().StringVar(&serveImageName, "name", "", i18n.T("cmd.php.serve.flag.name"))
|
||||||
serveCmd.Flags().StringVar(&serveTag, "tag", "", "Image tag (default: latest)")
|
serveCmd.Flags().StringVar(&serveTag, "tag", "", i18n.T("cmd.php.serve.flag.tag"))
|
||||||
serveCmd.Flags().StringVar(&serveContainerName, "container", "", "Container name")
|
serveCmd.Flags().StringVar(&serveContainerName, "container", "", i18n.T("cmd.php.serve.flag.container"))
|
||||||
serveCmd.Flags().IntVar(&servePort, "port", 0, "HTTP port (default: 80)")
|
serveCmd.Flags().IntVar(&servePort, "port", 0, i18n.T("cmd.php.serve.flag.port"))
|
||||||
serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, "HTTPS port (default: 443)")
|
serveCmd.Flags().IntVar(&serveHTTPSPort, "https-port", 0, i18n.T("cmd.php.serve.flag.https_port"))
|
||||||
serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, "Run in detached mode")
|
serveCmd.Flags().BoolVarP(&serveDetach, "detach", "d", false, i18n.T("cmd.php.serve.flag.detach"))
|
||||||
serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", "Path to environment file")
|
serveCmd.Flags().StringVar(&serveEnvFile, "env-file", "", i18n.T("cmd.php.serve.flag.env_file"))
|
||||||
|
|
||||||
parent.AddCommand(serveCmd)
|
parent.AddCommand(serveCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -282,19 +271,16 @@ func addPHPServeCommand(parent *cobra.Command) {
|
||||||
func addPHPShellCommand(parent *cobra.Command) {
|
func addPHPShellCommand(parent *cobra.Command) {
|
||||||
shellCmd := &cobra.Command{
|
shellCmd := &cobra.Command{
|
||||||
Use: "shell [container]",
|
Use: "shell [container]",
|
||||||
Short: "Open shell in running container",
|
Short: i18n.T("cmd.php.shell.short"),
|
||||||
Long: "Open an interactive shell in a running PHP container.\n\n" +
|
Long: i18n.T("cmd.php.shell.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core php shell abc123 # Shell into container by ID\n" +
|
|
||||||
" core php shell myapp # Shell into container by name",
|
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
fmt.Printf("%s Opening shell in container %s...\n", dimStyle.Render("PHP:"), args[0])
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.shell.opening", map[string]interface{}{"Container": args[0]}))
|
||||||
|
|
||||||
if err := phppkg.Shell(ctx, args[0]); err != nil {
|
if err := phppkg.Shell(ctx, args[0]); err != nil {
|
||||||
return fmt.Errorf("failed to open shell: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.open_shell"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
phppkg "github.com/host-uk/core/pkg/php"
|
phppkg "github.com/host-uk/core/pkg/php"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -41,22 +42,12 @@ var (
|
||||||
func addPHPDeployCommand(parent *cobra.Command) {
|
func addPHPDeployCommand(parent *cobra.Command) {
|
||||||
deployCmd := &cobra.Command{
|
deployCmd := &cobra.Command{
|
||||||
Use: "deploy",
|
Use: "deploy",
|
||||||
Short: "Deploy to Coolify",
|
Short: i18n.T("cmd.php.deploy.short"),
|
||||||
Long: "Deploy the PHP application to Coolify.\n\n" +
|
Long: i18n.T("cmd.php.deploy.long"),
|
||||||
"Requires configuration in .env:\n" +
|
|
||||||
" COOLIFY_URL=https://coolify.example.com\n" +
|
|
||||||
" COOLIFY_TOKEN=your-api-token\n" +
|
|
||||||
" COOLIFY_APP_ID=production-app-id\n" +
|
|
||||||
" COOLIFY_STAGING_APP_ID=staging-app-id (optional)\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php deploy # Deploy to production\n" +
|
|
||||||
" core php deploy --staging # Deploy to staging\n" +
|
|
||||||
" core php deploy --force # Force deployment\n" +
|
|
||||||
" core php deploy --wait # Wait for deployment to complete",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := phppkg.EnvProduction
|
env := phppkg.EnvProduction
|
||||||
|
|
@ -64,7 +55,7 @@ func addPHPDeployCommand(parent *cobra.Command) {
|
||||||
env = phppkg.EnvStaging
|
env = phppkg.EnvStaging
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Deploying to %s...\n\n", dimStyle.Render("Deploy:"), env)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy.deploying", map[string]interface{}{"Environment": env}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -77,28 +68,28 @@ func addPHPDeployCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
status, err := phppkg.Deploy(ctx, opts)
|
status, err := phppkg.Deploy(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deployment failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.deploy_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
printDeploymentStatus(status)
|
printDeploymentStatus(status)
|
||||||
|
|
||||||
if deployWait {
|
if deployWait {
|
||||||
if phppkg.IsDeploymentSuccessful(status.Status) {
|
if phppkg.IsDeploymentSuccessful(status.Status) {
|
||||||
fmt.Printf("\n%s Deployment completed successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy.success"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s Deployment ended with status: %s\n", errorStyle.Render("Warning:"), status.Status)
|
fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.deploy.warning_status", map[string]interface{}{"Status": status.Status}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s Deployment triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy.triggered"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
deployCmd.Flags().BoolVar(&deployStaging, "staging", false, "Deploy to staging environment")
|
deployCmd.Flags().BoolVar(&deployStaging, "staging", false, i18n.T("cmd.php.deploy.flag.staging"))
|
||||||
deployCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if no changes detected")
|
deployCmd.Flags().BoolVar(&deployForce, "force", false, i18n.T("cmd.php.deploy.flag.force"))
|
||||||
deployCmd.Flags().BoolVar(&deployWait, "wait", false, "Wait for deployment to complete")
|
deployCmd.Flags().BoolVar(&deployWait, "wait", false, i18n.T("cmd.php.deploy.flag.wait"))
|
||||||
|
|
||||||
parent.AddCommand(deployCmd)
|
parent.AddCommand(deployCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -111,16 +102,12 @@ var (
|
||||||
func addPHPDeployStatusCommand(parent *cobra.Command) {
|
func addPHPDeployStatusCommand(parent *cobra.Command) {
|
||||||
statusCmd := &cobra.Command{
|
statusCmd := &cobra.Command{
|
||||||
Use: "deploy:status",
|
Use: "deploy:status",
|
||||||
Short: "Show deployment status",
|
Short: i18n.T("cmd.php.deploy_status.short"),
|
||||||
Long: "Show the status of a deployment.\n\n" +
|
Long: i18n.T("cmd.php.deploy_status.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core php deploy:status # Latest production deployment\n" +
|
|
||||||
" core php deploy:status --staging # Latest staging deployment\n" +
|
|
||||||
" core php deploy:status --id abc123 # Specific deployment",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := phppkg.EnvProduction
|
env := phppkg.EnvProduction
|
||||||
|
|
@ -128,7 +115,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
|
||||||
env = phppkg.EnvStaging
|
env = phppkg.EnvStaging
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Checking %s deployment status...\n\n", dimStyle.Render("Deploy:"), env)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_status.checking", map[string]interface{}{"Environment": env}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -140,7 +127,7 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
status, err := phppkg.DeployStatus(ctx, opts)
|
status, err := phppkg.DeployStatus(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get status: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.status_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
printDeploymentStatus(status)
|
printDeploymentStatus(status)
|
||||||
|
|
@ -149,8 +136,8 @@ func addPHPDeployStatusCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, "Check staging environment")
|
statusCmd.Flags().BoolVar(&deployStatusStaging, "staging", false, i18n.T("cmd.php.deploy_status.flag.staging"))
|
||||||
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", "Specific deployment ID")
|
statusCmd.Flags().StringVar(&deployStatusDeploymentID, "id", "", i18n.T("cmd.php.deploy_status.flag.id"))
|
||||||
|
|
||||||
parent.AddCommand(statusCmd)
|
parent.AddCommand(statusCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -164,18 +151,12 @@ var (
|
||||||
func addPHPDeployRollbackCommand(parent *cobra.Command) {
|
func addPHPDeployRollbackCommand(parent *cobra.Command) {
|
||||||
rollbackCmd := &cobra.Command{
|
rollbackCmd := &cobra.Command{
|
||||||
Use: "deploy:rollback",
|
Use: "deploy:rollback",
|
||||||
Short: "Rollback to previous deployment",
|
Short: i18n.T("cmd.php.deploy_rollback.short"),
|
||||||
Long: "Rollback to a previous deployment.\n\n" +
|
Long: i18n.T("cmd.php.deploy_rollback.long"),
|
||||||
"If no deployment ID is specified, rolls back to the most recent\n" +
|
|
||||||
"successful deployment.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php deploy:rollback # Rollback to previous\n" +
|
|
||||||
" core php deploy:rollback --staging # Rollback staging\n" +
|
|
||||||
" core php deploy:rollback --id abc123 # Rollback to specific deployment",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := phppkg.EnvProduction
|
env := phppkg.EnvProduction
|
||||||
|
|
@ -183,7 +164,7 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
|
||||||
env = phppkg.EnvStaging
|
env = phppkg.EnvStaging
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Rolling back %s...\n\n", dimStyle.Render("Deploy:"), env)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_rollback.rolling_back", map[string]interface{}{"Environment": env}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -196,28 +177,28 @@ func addPHPDeployRollbackCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
status, err := phppkg.Rollback(ctx, opts)
|
status, err := phppkg.Rollback(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("rollback failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rollback_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
printDeploymentStatus(status)
|
printDeploymentStatus(status)
|
||||||
|
|
||||||
if rollbackWait {
|
if rollbackWait {
|
||||||
if phppkg.IsDeploymentSuccessful(status.Status) {
|
if phppkg.IsDeploymentSuccessful(status.Status) {
|
||||||
fmt.Printf("\n%s Rollback completed successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy_rollback.success"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s Rollback ended with status: %s\n", errorStyle.Render("Warning:"), status.Status)
|
fmt.Printf("\n%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.deploy_rollback.warning_status", map[string]interface{}{"Status": status.Status}))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s Rollback triggered. Use 'core php deploy:status' to check progress.\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.deploy_rollback.triggered"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, "Rollback staging environment")
|
rollbackCmd.Flags().BoolVar(&rollbackStaging, "staging", false, i18n.T("cmd.php.deploy_rollback.flag.staging"))
|
||||||
rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", "Specific deployment ID to rollback to")
|
rollbackCmd.Flags().StringVar(&rollbackDeploymentID, "id", "", i18n.T("cmd.php.deploy_rollback.flag.id"))
|
||||||
rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, "Wait for rollback to complete")
|
rollbackCmd.Flags().BoolVar(&rollbackWait, "wait", false, i18n.T("cmd.php.deploy_rollback.flag.wait"))
|
||||||
|
|
||||||
parent.AddCommand(rollbackCmd)
|
parent.AddCommand(rollbackCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -230,16 +211,12 @@ var (
|
||||||
func addPHPDeployListCommand(parent *cobra.Command) {
|
func addPHPDeployListCommand(parent *cobra.Command) {
|
||||||
listCmd := &cobra.Command{
|
listCmd := &cobra.Command{
|
||||||
Use: "deploy:list",
|
Use: "deploy:list",
|
||||||
Short: "List recent deployments",
|
Short: i18n.T("cmd.php.deploy_list.short"),
|
||||||
Long: "List recent deployments.\n\n" +
|
Long: i18n.T("cmd.php.deploy_list.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core php deploy:list # List production deployments\n" +
|
|
||||||
" core php deploy:list --staging # List staging deployments\n" +
|
|
||||||
" core php deploy:list --limit 20 # List more deployments",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := phppkg.EnvProduction
|
env := phppkg.EnvProduction
|
||||||
|
|
@ -252,17 +229,17 @@ func addPHPDeployListCommand(parent *cobra.Command) {
|
||||||
limit = 10
|
limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Recent %s deployments:\n\n", dimStyle.Render("Deploy:"), env)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.deploy")), i18n.T("cmd.php.deploy_list.recent", map[string]interface{}{"Environment": env}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit)
|
deployments, err := phppkg.ListDeployments(ctx, cwd, env, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list deployments: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.list_deployments"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(deployments) == 0 {
|
if len(deployments) == 0 {
|
||||||
fmt.Printf("%s No deployments found\n", dimStyle.Render("Info:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.deploy_list.none_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,8 +251,8 @@ func addPHPDeployListCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
listCmd.Flags().BoolVar(&deployListStaging, "staging", false, "List staging deployments")
|
listCmd.Flags().BoolVar(&deployListStaging, "staging", false, i18n.T("cmd.php.deploy_list.flag.staging"))
|
||||||
listCmd.Flags().IntVar(&deployListLimit, "limit", 0, "Number of deployments to list (default: 10)")
|
listCmd.Flags().IntVar(&deployListLimit, "limit", 0, i18n.T("cmd.php.deploy_list.flag.limit"))
|
||||||
|
|
||||||
parent.AddCommand(listCmd)
|
parent.AddCommand(listCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -290,18 +267,18 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
|
||||||
statusStyle = phpDeployFailedStyle
|
statusStyle = phpDeployFailedStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Status:"), statusStyle.Render(status.Status))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.status")), statusStyle.Render(status.Status))
|
||||||
|
|
||||||
if status.ID != "" {
|
if status.ID != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("ID:"), status.ID)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.id")), status.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.URL != "" {
|
if status.URL != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("URL:"), linkStyle.Render(status.URL))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.url")), linkStyle.Render(status.URL))
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Branch != "" {
|
if status.Branch != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Branch:"), status.Branch)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.branch")), status.Branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Commit != "" {
|
if status.Commit != "" {
|
||||||
|
|
@ -309,26 +286,26 @@ func printDeploymentStatus(status *phppkg.DeploymentStatus) {
|
||||||
if len(commit) > 7 {
|
if len(commit) > 7 {
|
||||||
commit = commit[:7]
|
commit = commit[:7]
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Commit:"), commit)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.commit")), commit)
|
||||||
if status.CommitMessage != "" {
|
if status.CommitMessage != "" {
|
||||||
// Truncate long messages
|
// Truncate long messages
|
||||||
msg := status.CommitMessage
|
msg := status.CommitMessage
|
||||||
if len(msg) > 60 {
|
if len(msg) > 60 {
|
||||||
msg = msg[:57] + "..."
|
msg = msg[:57] + "..."
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Message:"), msg)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.message")), msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !status.StartedAt.IsZero() {
|
if !status.StartedAt.IsZero() {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Started:"), status.StartedAt.Format(time.RFC3339))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.started")), status.StartedAt.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !status.CompletedAt.IsZero() {
|
if !status.CompletedAt.IsZero() {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Completed:"), status.CompletedAt.Format(time.RFC3339))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.completed")), status.CompletedAt.Format(time.RFC3339))
|
||||||
if !status.StartedAt.IsZero() {
|
if !status.StartedAt.IsZero() {
|
||||||
duration := status.CompletedAt.Sub(status.StartedAt)
|
duration := status.CompletedAt.Sub(status.StartedAt)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Duration:"), duration.Round(time.Second))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.duration")), duration.Round(time.Second))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -390,24 +367,24 @@ func formatTimeAgo(t time.Time) string {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case duration < time.Minute:
|
case duration < time.Minute:
|
||||||
return "just now"
|
return i18n.T("cli.time.just_now")
|
||||||
case duration < time.Hour:
|
case duration < time.Hour:
|
||||||
mins := int(duration.Minutes())
|
mins := int(duration.Minutes())
|
||||||
if mins == 1 {
|
if mins == 1 {
|
||||||
return "1 minute ago"
|
return i18n.T("cli.time.minute_ago")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%d minutes ago", mins)
|
return i18n.T("cli.time.minutes_ago", map[string]interface{}{"Count": mins})
|
||||||
case duration < 24*time.Hour:
|
case duration < 24*time.Hour:
|
||||||
hours := int(duration.Hours())
|
hours := int(duration.Hours())
|
||||||
if hours == 1 {
|
if hours == 1 {
|
||||||
return "1 hour ago"
|
return i18n.T("cli.time.hour_ago")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%d hours ago", hours)
|
return i18n.T("cli.time.hours_ago", map[string]interface{}{"Count": hours})
|
||||||
default:
|
default:
|
||||||
days := int(duration.Hours() / 24)
|
days := int(duration.Hours() / 24)
|
||||||
if days == 1 {
|
if days == 1 {
|
||||||
return "1 day ago"
|
return i18n.T("cli.time.day_ago")
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%d days ago", days)
|
return i18n.T("cli.time.days_ago", map[string]interface{}{"Count": days})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
phppkg "github.com/host-uk/core/pkg/php"
|
phppkg "github.com/host-uk/core/pkg/php"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -28,13 +29,8 @@ var (
|
||||||
func addPHPDevCommand(parent *cobra.Command) {
|
func addPHPDevCommand(parent *cobra.Command) {
|
||||||
devCmd := &cobra.Command{
|
devCmd := &cobra.Command{
|
||||||
Use: "dev",
|
Use: "dev",
|
||||||
Short: "Start Laravel development environment",
|
Short: i18n.T("cmd.php.dev.short"),
|
||||||
Long: "Starts all detected Laravel services.\n\n" +
|
Long: i18n.T("cmd.php.dev.long"),
|
||||||
"Auto-detects:\n" +
|
|
||||||
" - Vite (vite.config.js/ts)\n" +
|
|
||||||
" - Horizon (config/horizon.php)\n" +
|
|
||||||
" - Reverb (config/reverb.php)\n" +
|
|
||||||
" - Redis (from .env)",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPHPDev(phpDevOptions{
|
return runPHPDev(phpDevOptions{
|
||||||
NoVite: devNoVite,
|
NoVite: devNoVite,
|
||||||
|
|
@ -48,13 +44,13 @@ func addPHPDevCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, "Skip Vite dev server")
|
devCmd.Flags().BoolVar(&devNoVite, "no-vite", false, i18n.T("cmd.php.dev.flag.no_vite"))
|
||||||
devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, "Skip Laravel Horizon")
|
devCmd.Flags().BoolVar(&devNoHorizon, "no-horizon", false, i18n.T("cmd.php.dev.flag.no_horizon"))
|
||||||
devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, "Skip Laravel Reverb")
|
devCmd.Flags().BoolVar(&devNoReverb, "no-reverb", false, i18n.T("cmd.php.dev.flag.no_reverb"))
|
||||||
devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, "Skip Redis server")
|
devCmd.Flags().BoolVar(&devNoRedis, "no-redis", false, i18n.T("cmd.php.dev.flag.no_redis"))
|
||||||
devCmd.Flags().BoolVar(&devHTTPS, "https", false, "Enable HTTPS with mkcert")
|
devCmd.Flags().BoolVar(&devHTTPS, "https", false, i18n.T("cmd.php.dev.flag.https"))
|
||||||
devCmd.Flags().StringVar(&devDomain, "domain", "", "Domain for SSL certificate (default: from APP_URL or localhost)")
|
devCmd.Flags().StringVar(&devDomain, "domain", "", i18n.T("cmd.php.dev.flag.domain"))
|
||||||
devCmd.Flags().IntVar(&devPort, "port", 0, "FrankenPHP port (default: 8000)")
|
devCmd.Flags().IntVar(&devPort, "port", 0, i18n.T("cmd.php.dev.flag.port"))
|
||||||
|
|
||||||
parent.AddCommand(devCmd)
|
parent.AddCommand(devCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +73,7 @@ func runPHPDev(opts phpDevOptions) error {
|
||||||
|
|
||||||
// Check if this is a Laravel project
|
// Check if this is a Laravel project
|
||||||
if !phppkg.IsLaravelProject(cwd) {
|
if !phppkg.IsLaravelProject(cwd) {
|
||||||
return fmt.Errorf("not a Laravel project (missing artisan or laravel/framework)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get app name for display
|
// Get app name for display
|
||||||
|
|
@ -86,11 +82,11 @@ func runPHPDev(opts phpDevOptions) error {
|
||||||
appName = "Laravel"
|
appName = "Laravel"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Starting %s development environment\n\n", dimStyle.Render("PHP:"), appName)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.starting", map[string]interface{}{"AppName": appName}))
|
||||||
|
|
||||||
// Detect services
|
// Detect services
|
||||||
services := phppkg.DetectServices(cwd)
|
services := phppkg.DetectServices(cwd)
|
||||||
fmt.Printf("%s Detected services:\n", dimStyle.Render("Services:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.services")), i18n.T("cmd.php.dev.detected_services"))
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
fmt.Printf(" %s %s\n", successStyle.Render("*"), svc)
|
fmt.Printf(" %s %s\n", successStyle.Render("*"), svc)
|
||||||
}
|
}
|
||||||
|
|
@ -125,16 +121,16 @@ func runPHPDev(opts phpDevOptions) error {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-sigCh
|
<-sigCh
|
||||||
fmt.Printf("\n%s Shutting down...\n", dimStyle.Render("PHP:"))
|
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.dev.shutting_down"))
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := server.Start(ctx, devOpts); err != nil {
|
if err := server.Start(ctx, devOpts); err != nil {
|
||||||
return fmt.Errorf("failed to start services: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.start_services"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print status
|
// Print status
|
||||||
fmt.Printf("%s Services started:\n", successStyle.Render("Running:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.running")), i18n.T("cmd.php.dev.services_started"))
|
||||||
printServiceStatuses(server.Status())
|
printServiceStatuses(server.Status())
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
|
@ -147,19 +143,19 @@ func runPHPDev(opts phpDevOptions) error {
|
||||||
appURL = fmt.Sprintf("http://localhost:%d", port)
|
appURL = fmt.Sprintf("http://localhost:%d", port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("App URL:"), linkStyle.Render(appURL))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.app_url")), linkStyle.Render(appURL))
|
||||||
|
|
||||||
// Check for Vite
|
// Check for Vite
|
||||||
if !opts.NoVite && containsService(services, phppkg.ServiceVite) {
|
if !opts.NoVite && containsService(services, phppkg.ServiceVite) {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Vite:"), linkStyle.Render("http://localhost:5173"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.vite")), linkStyle.Render("http://localhost:5173"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s\n\n", dimStyle.Render("Press Ctrl+C to stop all services"))
|
fmt.Printf("\n%s\n\n", dimStyle.Render(i18n.T("cmd.php.dev.press_ctrl_c")))
|
||||||
|
|
||||||
// Stream unified logs
|
// Stream unified logs
|
||||||
logsReader, err := server.Logs("", true)
|
logsReader, err := server.Logs("", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s Failed to get logs: %v\n", errorStyle.Render("Warning:"), err)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.dev.logs_failed", map[string]interface{}{"Error": err}))
|
||||||
} else {
|
} else {
|
||||||
defer logsReader.Close()
|
defer logsReader.Close()
|
||||||
|
|
||||||
|
|
@ -178,10 +174,10 @@ func runPHPDev(opts phpDevOptions) error {
|
||||||
shutdown:
|
shutdown:
|
||||||
// Stop services
|
// Stop services
|
||||||
if err := server.Stop(); err != nil {
|
if err := server.Stop(); err != nil {
|
||||||
fmt.Printf("%s Error stopping services: %v\n", errorStyle.Render("Error:"), err)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.dev.stop_error", map[string]interface{}{"Error": err}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s All services stopped\n", successStyle.Render("Done:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.dev.all_stopped"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,16 +189,15 @@ var (
|
||||||
func addPHPLogsCommand(parent *cobra.Command) {
|
func addPHPLogsCommand(parent *cobra.Command) {
|
||||||
logsCmd := &cobra.Command{
|
logsCmd := &cobra.Command{
|
||||||
Use: "logs",
|
Use: "logs",
|
||||||
Short: "View service logs",
|
Short: i18n.T("cmd.php.logs.short"),
|
||||||
Long: "Stream logs from Laravel services.\n\n" +
|
Long: i18n.T("cmd.php.logs.long"),
|
||||||
"Services: frankenphp, vite, horizon, reverb, redis",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPHPLogs(logsService, logsFollow)
|
return runPHPLogs(logsService, logsFollow)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logsCmd.Flags().BoolVar(&logsFollow, "follow", false, "Follow log output")
|
logsCmd.Flags().BoolVar(&logsFollow, "follow", false, i18n.T("cmd.php.logs.flag.follow"))
|
||||||
logsCmd.Flags().StringVar(&logsService, "service", "", "Specific service (default: all)")
|
logsCmd.Flags().StringVar(&logsService, "service", "", i18n.T("cmd.php.logs.flag.service"))
|
||||||
|
|
||||||
parent.AddCommand(logsCmd)
|
parent.AddCommand(logsCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -214,7 +209,7 @@ func runPHPLogs(service string, follow bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsLaravelProject(cwd) {
|
if !phppkg.IsLaravelProject(cwd) {
|
||||||
return fmt.Errorf("not a Laravel project")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a minimal server just to access logs
|
// Create a minimal server just to access logs
|
||||||
|
|
@ -222,7 +217,7 @@ func runPHPLogs(service string, follow bool) error {
|
||||||
|
|
||||||
logsReader, err := server.Logs(service, follow)
|
logsReader, err := server.Logs(service, follow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get logs: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.get_logs"), err)
|
||||||
}
|
}
|
||||||
defer logsReader.Close()
|
defer logsReader.Close()
|
||||||
|
|
||||||
|
|
@ -254,7 +249,7 @@ func runPHPLogs(service string, follow bool) error {
|
||||||
func addPHPStopCommand(parent *cobra.Command) {
|
func addPHPStopCommand(parent *cobra.Command) {
|
||||||
stopCmd := &cobra.Command{
|
stopCmd := &cobra.Command{
|
||||||
Use: "stop",
|
Use: "stop",
|
||||||
Short: "Stop all Laravel services",
|
Short: i18n.T("cmd.php.stop.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPHPStop()
|
return runPHPStop()
|
||||||
},
|
},
|
||||||
|
|
@ -269,23 +264,23 @@ func runPHPStop() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Stopping services...\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.stop.stopping"))
|
||||||
|
|
||||||
// We need to find running processes
|
// We need to find running processes
|
||||||
// This is a simplified version - in practice you'd want to track PIDs
|
// This is a simplified version - in practice you'd want to track PIDs
|
||||||
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd})
|
server := phppkg.NewDevServer(phppkg.Options{Dir: cwd})
|
||||||
if err := server.Stop(); err != nil {
|
if err := server.Stop(); err != nil {
|
||||||
return fmt.Errorf("failed to stop services: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.stop_services"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s All services stopped\n", successStyle.Render("Done:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.dev.all_stopped"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPHPStatusCommand(parent *cobra.Command) {
|
func addPHPStatusCommand(parent *cobra.Command) {
|
||||||
statusCmd := &cobra.Command{
|
statusCmd := &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "Show service status",
|
Short: i18n.T("cmd.php.status.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPHPStatus()
|
return runPHPStatus()
|
||||||
},
|
},
|
||||||
|
|
@ -301,7 +296,7 @@ func runPHPStatus() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsLaravelProject(cwd) {
|
if !phppkg.IsLaravelProject(cwd) {
|
||||||
return fmt.Errorf("not a Laravel project")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_laravel_short"))
|
||||||
}
|
}
|
||||||
|
|
||||||
appName := phppkg.GetLaravelAppName(cwd)
|
appName := phppkg.GetLaravelAppName(cwd)
|
||||||
|
|
@ -309,11 +304,11 @@ func runPHPStatus() error {
|
||||||
appName = "Laravel"
|
appName = "Laravel"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Project:"), appName)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.status.project")), appName)
|
||||||
|
|
||||||
// Detect available services
|
// Detect available services
|
||||||
services := phppkg.DetectServices(cwd)
|
services := phppkg.DetectServices(cwd)
|
||||||
fmt.Printf("%s\n", dimStyle.Render("Detected services:"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.status.detected_services")))
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
style := getServiceStyle(string(svc))
|
style := getServiceStyle(string(svc))
|
||||||
fmt.Printf(" %s %s\n", style.Render("*"), svc)
|
fmt.Printf(" %s %s\n", style.Render("*"), svc)
|
||||||
|
|
@ -322,11 +317,11 @@ func runPHPStatus() error {
|
||||||
|
|
||||||
// Package manager
|
// Package manager
|
||||||
pm := phppkg.DetectPackageManager(cwd)
|
pm := phppkg.DetectPackageManager(cwd)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Package manager:"), pm)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.package_manager")), pm)
|
||||||
|
|
||||||
// FrankenPHP status
|
// FrankenPHP status
|
||||||
if phppkg.IsFrankenPHPProject(cwd) {
|
if phppkg.IsFrankenPHPProject(cwd) {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Octane server:"), "FrankenPHP")
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.octane_server")), "FrankenPHP")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL status
|
// SSL status
|
||||||
|
|
@ -334,9 +329,9 @@ func runPHPStatus() error {
|
||||||
if appURL != "" {
|
if appURL != "" {
|
||||||
domain := phppkg.ExtractDomainFromURL(appURL)
|
domain := phppkg.ExtractDomainFromURL(appURL)
|
||||||
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
|
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("SSL certificates:"), successStyle.Render("installed"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), successStyle.Render(i18n.T("cmd.php.status.ssl_installed")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("SSL certificates:"), dimStyle.Render("not setup"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.status.ssl_certs")), dimStyle.Render(i18n.T("cmd.php.status.ssl_not_setup")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,13 +343,13 @@ var sslDomain string
|
||||||
func addPHPSSLCommand(parent *cobra.Command) {
|
func addPHPSSLCommand(parent *cobra.Command) {
|
||||||
sslCmd := &cobra.Command{
|
sslCmd := &cobra.Command{
|
||||||
Use: "ssl",
|
Use: "ssl",
|
||||||
Short: "Setup SSL certificates with mkcert",
|
Short: i18n.T("cmd.php.ssl.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPHPSSL(sslDomain)
|
return runPHPSSL(sslDomain)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sslCmd.Flags().StringVar(&sslDomain, "domain", "", "Domain for certificate (default: from APP_URL)")
|
sslCmd.Flags().StringVar(&sslDomain, "domain", "", i18n.T("cmd.php.ssl.flag.domain"))
|
||||||
|
|
||||||
parent.AddCommand(sslCmd)
|
parent.AddCommand(sslCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -378,35 +373,35 @@ func runPHPSSL(domain string) error {
|
||||||
|
|
||||||
// Check if mkcert is installed
|
// Check if mkcert is installed
|
||||||
if !phppkg.IsMkcertInstalled() {
|
if !phppkg.IsMkcertInstalled() {
|
||||||
fmt.Printf("%s mkcert is not installed\n", errorStyle.Render("Error:"))
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.ssl.mkcert_not_installed"))
|
||||||
fmt.Println("\nInstall with:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.php.ssl.install_with"))
|
||||||
fmt.Println(" macOS: brew install mkcert")
|
fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_macos"))
|
||||||
fmt.Println(" Linux: see https://github.com/FiloSottile/mkcert")
|
fmt.Printf(" %s\n", i18n.T("cmd.php.ssl.install_linux"))
|
||||||
return fmt.Errorf("mkcert not installed")
|
return fmt.Errorf(i18n.T("cmd.php.error.mkcert_not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Setting up SSL for %s\n", dimStyle.Render("SSL:"), domain)
|
fmt.Printf("%s %s\n", dimStyle.Render("SSL:"), i18n.T("cmd.php.ssl.setting_up", map[string]interface{}{"Domain": domain}))
|
||||||
|
|
||||||
// Check if certs already exist
|
// Check if certs already exist
|
||||||
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
|
if phppkg.CertsExist(domain, phppkg.SSLOptions{}) {
|
||||||
fmt.Printf("%s Certificates already exist\n", dimStyle.Render("Skip:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.skip")), i18n.T("cmd.php.ssl.certs_exist"))
|
||||||
|
|
||||||
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
|
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup SSL
|
// Setup SSL
|
||||||
if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil {
|
if err := phppkg.SetupSSL(domain, phppkg.SSLOptions{}); err != nil {
|
||||||
return fmt.Errorf("failed to setup SSL: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.ssl_setup"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
|
certFile, keyFile, _ := phppkg.CertPaths(domain, phppkg.SSLOptions{})
|
||||||
|
|
||||||
fmt.Printf("%s SSL certificates created\n", successStyle.Render("Done:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.ssl.certs_created"))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Cert:"), certFile)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.cert_label")), certFile)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Key:"), keyFile)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.ssl.key_label")), keyFile)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -419,17 +414,17 @@ func printServiceStatuses(statuses []phppkg.ServiceStatus) {
|
||||||
var statusText string
|
var statusText string
|
||||||
|
|
||||||
if s.Error != nil {
|
if s.Error != nil {
|
||||||
statusText = phpStatusError.Render(fmt.Sprintf("error: %v", s.Error))
|
statusText = phpStatusError.Render(i18n.T("cmd.php.status.error", map[string]interface{}{"Error": s.Error}))
|
||||||
} else if s.Running {
|
} else if s.Running {
|
||||||
statusText = phpStatusRunning.Render("running")
|
statusText = phpStatusRunning.Render(i18n.T("cmd.php.status.running"))
|
||||||
if s.Port > 0 {
|
if s.Port > 0 {
|
||||||
statusText += dimStyle.Render(fmt.Sprintf(" (port %d)", s.Port))
|
statusText += dimStyle.Render(fmt.Sprintf(" (%s)", i18n.T("cmd.php.status.port", map[string]interface{}{"Port": s.Port})))
|
||||||
}
|
}
|
||||||
if s.PID > 0 {
|
if s.PID > 0 {
|
||||||
statusText += dimStyle.Render(fmt.Sprintf(" [pid %d]", s.PID))
|
statusText += dimStyle.Render(fmt.Sprintf(" [%s]", i18n.T("cmd.php.status.pid", map[string]interface{}{"PID": s.PID})))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
statusText = phpStatusStopped.Render("stopped")
|
statusText = phpStatusStopped.Render(i18n.T("cmd.php.status.stopped"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText)
|
fmt.Printf(" %s %s\n", style.Render(s.Name+":"), statusText)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
phppkg "github.com/host-uk/core/pkg/php"
|
phppkg "github.com/host-uk/core/pkg/php"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -11,15 +12,8 @@ import (
|
||||||
func addPHPPackagesCommands(parent *cobra.Command) {
|
func addPHPPackagesCommands(parent *cobra.Command) {
|
||||||
packagesCmd := &cobra.Command{
|
packagesCmd := &cobra.Command{
|
||||||
Use: "packages",
|
Use: "packages",
|
||||||
Short: "Manage local PHP packages",
|
Short: i18n.T("cmd.php.packages.short"),
|
||||||
Long: "Link and manage local PHP packages for development.\n\n" +
|
Long: i18n.T("cmd.php.packages.long"),
|
||||||
"Similar to npm link, this adds path repositories to composer.json\n" +
|
|
||||||
"for developing packages alongside your project.\n\n" +
|
|
||||||
"Commands:\n" +
|
|
||||||
" link - Link local packages by path\n" +
|
|
||||||
" unlink - Unlink packages by name\n" +
|
|
||||||
" update - Update linked packages\n" +
|
|
||||||
" list - List linked packages",
|
|
||||||
}
|
}
|
||||||
parent.AddCommand(packagesCmd)
|
parent.AddCommand(packagesCmd)
|
||||||
|
|
||||||
|
|
@ -32,27 +26,22 @@ func addPHPPackagesCommands(parent *cobra.Command) {
|
||||||
func addPHPPackagesLinkCommand(parent *cobra.Command) {
|
func addPHPPackagesLinkCommand(parent *cobra.Command) {
|
||||||
linkCmd := &cobra.Command{
|
linkCmd := &cobra.Command{
|
||||||
Use: "link [paths...]",
|
Use: "link [paths...]",
|
||||||
Short: "Link local packages",
|
Short: i18n.T("cmd.php.packages.link.short"),
|
||||||
Long: "Link local PHP packages for development.\n\n" +
|
Long: i18n.T("cmd.php.packages.link.long"),
|
||||||
"Adds path repositories to composer.json with symlink enabled.\n" +
|
|
||||||
"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),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Linking packages...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.link.linking"))
|
||||||
|
|
||||||
if err := phppkg.LinkPackages(cwd, args); err != nil {
|
if err := phppkg.LinkPackages(cwd, args); err != nil {
|
||||||
return fmt.Errorf("failed to link packages: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.link_packages"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Packages linked. Run 'composer update' to install.\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.link.done"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -63,26 +52,22 @@ func addPHPPackagesLinkCommand(parent *cobra.Command) {
|
||||||
func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
|
func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
|
||||||
unlinkCmd := &cobra.Command{
|
unlinkCmd := &cobra.Command{
|
||||||
Use: "unlink [packages...]",
|
Use: "unlink [packages...]",
|
||||||
Short: "Unlink packages",
|
Short: i18n.T("cmd.php.packages.unlink.short"),
|
||||||
Long: "Remove linked packages from composer.json.\n\n" +
|
Long: i18n.T("cmd.php.packages.unlink.long"),
|
||||||
"Removes path repositories by package name.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php packages unlink vendor/my-package\n" +
|
|
||||||
" core php packages unlink vendor/pkg-a vendor/pkg-b",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Unlinking packages...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.unlink.unlinking"))
|
||||||
|
|
||||||
if err := phppkg.UnlinkPackages(cwd, args); err != nil {
|
if err := phppkg.UnlinkPackages(cwd, args); err != nil {
|
||||||
return fmt.Errorf("failed to unlink packages: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.unlink_packages"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Packages unlinked. Run 'composer update' to remove.\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.unlink.done"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -93,25 +78,21 @@ func addPHPPackagesUnlinkCommand(parent *cobra.Command) {
|
||||||
func addPHPPackagesUpdateCommand(parent *cobra.Command) {
|
func addPHPPackagesUpdateCommand(parent *cobra.Command) {
|
||||||
updateCmd := &cobra.Command{
|
updateCmd := &cobra.Command{
|
||||||
Use: "update [packages...]",
|
Use: "update [packages...]",
|
||||||
Short: "Update linked packages",
|
Short: i18n.T("cmd.php.packages.update.short"),
|
||||||
Long: "Run composer update for linked packages.\n\n" +
|
Long: i18n.T("cmd.php.packages.update.long"),
|
||||||
"If no packages specified, updates all packages.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php packages update\n" +
|
|
||||||
" core php packages update vendor/my-package",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Updating packages...\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.update.updating"))
|
||||||
|
|
||||||
if err := phppkg.UpdatePackages(cwd, args); err != nil {
|
if err := phppkg.UpdatePackages(cwd, args); err != nil {
|
||||||
return fmt.Errorf("composer update failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.update_packages"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Packages updated\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.packages.update.done"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -122,31 +103,30 @@ func addPHPPackagesUpdateCommand(parent *cobra.Command) {
|
||||||
func addPHPPackagesListCommand(parent *cobra.Command) {
|
func addPHPPackagesListCommand(parent *cobra.Command) {
|
||||||
listCmd := &cobra.Command{
|
listCmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List linked packages",
|
Short: i18n.T("cmd.php.packages.list.short"),
|
||||||
Long: "List all locally linked packages.\n\n" +
|
Long: i18n.T("cmd.php.packages.list.long"),
|
||||||
"Shows package name, path, and version for each linked package.",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
packages, err := phppkg.ListLinkedPackages(cwd)
|
packages, err := phppkg.ListLinkedPackages(cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list packages: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.list_packages"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
fmt.Printf("%s No linked packages found\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.none_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Linked packages:\n\n", dimStyle.Render("PHP:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.packages.list.linked"))
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
name := pkg.Name
|
name := pkg.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = "(unknown)"
|
name = i18n.T("cmd.php.packages.list.unknown")
|
||||||
}
|
}
|
||||||
version := pkg.Version
|
version := pkg.Version
|
||||||
if version == "" {
|
if version == "" {
|
||||||
|
|
@ -154,8 +134,8 @@ func addPHPPackagesListCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s %s\n", successStyle.Render("*"), name)
|
fmt.Printf(" %s %s\n", successStyle.Render("*"), name)
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Path:"), pkg.Path)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.packages.list.path")), pkg.Path)
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Version:"), version)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.packages.list.version")), version)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
phppkg "github.com/host-uk/core/pkg/php"
|
phppkg "github.com/host-uk/core/pkg/php"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -24,27 +25,21 @@ var (
|
||||||
func addPHPTestCommand(parent *cobra.Command) {
|
func addPHPTestCommand(parent *cobra.Command) {
|
||||||
testCmd := &cobra.Command{
|
testCmd := &cobra.Command{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
Short: "Run PHP tests (PHPUnit/Pest)",
|
Short: i18n.T("cmd.php.test.short"),
|
||||||
Long: "Run PHP tests using PHPUnit or Pest.\n\n" +
|
Long: i18n.T("cmd.php.test.long"),
|
||||||
"Auto-detects Pest if tests/Pest.php exists, otherwise uses PHPUnit.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php test # Run all tests\n" +
|
|
||||||
" core php test --parallel # Run tests in parallel\n" +
|
|
||||||
" core php test --coverage # Run with coverage\n" +
|
|
||||||
" core php test --filter UserTest # Filter by test name",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect test runner
|
// Detect test runner
|
||||||
runner := phppkg.DetectTestRunner(cwd)
|
runner := phppkg.DetectTestRunner(cwd)
|
||||||
fmt.Printf("%s Running tests with %s\n\n", dimStyle.Render("PHP:"), runner)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.test.running", map[string]interface{}{"Runner": runner}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -61,17 +56,17 @@ func addPHPTestCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := phppkg.RunTests(ctx, opts); err != nil {
|
if err := phppkg.RunTests(ctx, opts); err != nil {
|
||||||
return fmt.Errorf("tests failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.tests_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCmd.Flags().BoolVar(&testParallel, "parallel", false, "Run tests in parallel")
|
testCmd.Flags().BoolVar(&testParallel, "parallel", false, i18n.T("cmd.php.test.flag.parallel"))
|
||||||
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Generate code coverage")
|
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.php.test.flag.coverage"))
|
||||||
testCmd.Flags().StringVar(&testFilter, "filter", "", "Filter tests by name pattern")
|
testCmd.Flags().StringVar(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter"))
|
||||||
testCmd.Flags().StringVar(&testGroup, "group", "", "Run only tests in specified group")
|
testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group"))
|
||||||
|
|
||||||
parent.AddCommand(testCmd)
|
parent.AddCommand(testCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -84,33 +79,31 @@ var (
|
||||||
func addPHPFmtCommand(parent *cobra.Command) {
|
func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
fmtCmd := &cobra.Command{
|
fmtCmd := &cobra.Command{
|
||||||
Use: "fmt [paths...]",
|
Use: "fmt [paths...]",
|
||||||
Short: "Format PHP code with Laravel Pint",
|
Short: i18n.T("cmd.php.fmt.short"),
|
||||||
Long: "Format PHP code using Laravel Pint.\n\n" +
|
Long: i18n.T("cmd.php.fmt.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core php fmt # Check formatting (dry-run)\n" +
|
|
||||||
" core php fmt --fix # Auto-fix formatting issues\n" +
|
|
||||||
" core php fmt --diff # Show diff of changes",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect formatter
|
// Detect formatter
|
||||||
formatter, found := phppkg.DetectFormatter(cwd)
|
formatter, found := phppkg.DetectFormatter(cwd)
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("no formatter found (install Laravel Pint: composer require laravel/pint --dev)")
|
return fmt.Errorf(i18n.T("cmd.php.fmt.no_formatter"))
|
||||||
}
|
}
|
||||||
|
|
||||||
action := "Checking"
|
var msg string
|
||||||
if fmtFix {
|
if fmtFix {
|
||||||
action = "Formatting"
|
msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter})
|
||||||
|
} else {
|
||||||
|
msg = i18n.T("cmd.php.fmt.checking", map[string]interface{}{"Formatter": formatter})
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s code with %s\n\n", dimStyle.Render("PHP:"), action, formatter)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -128,23 +121,23 @@ func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
if err := phppkg.Format(ctx, opts); err != nil {
|
if err := phppkg.Format(ctx, opts); err != nil {
|
||||||
if fmtFix {
|
if fmtFix {
|
||||||
return fmt.Errorf("formatting failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_failed"), err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("formatting issues found: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fmtFix {
|
if fmtFix {
|
||||||
fmt.Printf("\n%s Code formatted successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.fmt.success"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s No formatting issues found\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.fmt.no_issues"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, "Auto-fix formatting issues")
|
fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix"))
|
||||||
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, "Show diff of changes")
|
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("cmd.php.fmt.flag.diff"))
|
||||||
|
|
||||||
parent.AddCommand(fmtCmd)
|
parent.AddCommand(fmtCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -157,30 +150,25 @@ var (
|
||||||
func addPHPAnalyseCommand(parent *cobra.Command) {
|
func addPHPAnalyseCommand(parent *cobra.Command) {
|
||||||
analyseCmd := &cobra.Command{
|
analyseCmd := &cobra.Command{
|
||||||
Use: "analyse [paths...]",
|
Use: "analyse [paths...]",
|
||||||
Short: "Run PHPStan static analysis",
|
Short: i18n.T("cmd.php.analyse.short"),
|
||||||
Long: "Run PHPStan or Larastan static analysis.\n\n" +
|
Long: i18n.T("cmd.php.analyse.long"),
|
||||||
"Auto-detects Larastan if installed, otherwise uses PHPStan.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php analyse # Run analysis\n" +
|
|
||||||
" core php analyse --level 9 # Run at max strictness\n" +
|
|
||||||
" core php analyse --memory 2G # Increase memory limit",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect analyser
|
// Detect analyser
|
||||||
analyser, found := phppkg.DetectAnalyser(cwd)
|
analyser, found := phppkg.DetectAnalyser(cwd)
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("no static analyser found (install PHPStan: composer require phpstan/phpstan --dev)")
|
return fmt.Errorf(i18n.T("cmd.php.analyse.no_analyser"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Running static analysis with %s\n\n", dimStyle.Render("PHP:"), analyser)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.T("cmd.php.analyse.running", map[string]interface{}{"Analyser": analyser}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -197,16 +185,16 @@ func addPHPAnalyseCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := phppkg.Analyse(ctx, opts); err != nil {
|
if err := phppkg.Analyse(ctx, opts); err != nil {
|
||||||
return fmt.Errorf("analysis found issues: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.analyse.no_issues"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
analyseCmd.Flags().IntVar(&analyseLevel, "level", 0, "PHPStan analysis level (0-9)")
|
analyseCmd.Flags().IntVar(&analyseLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level"))
|
||||||
analyseCmd.Flags().StringVar(&analyseMemory, "memory", "", "Memory limit (e.g., 2G)")
|
analyseCmd.Flags().StringVar(&analyseMemory, "memory", "", i18n.T("cmd.php.analyse.flag.memory"))
|
||||||
|
|
||||||
parent.AddCommand(analyseCmd)
|
parent.AddCommand(analyseCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -225,39 +213,34 @@ var (
|
||||||
func addPHPPsalmCommand(parent *cobra.Command) {
|
func addPHPPsalmCommand(parent *cobra.Command) {
|
||||||
psalmCmd := &cobra.Command{
|
psalmCmd := &cobra.Command{
|
||||||
Use: "psalm",
|
Use: "psalm",
|
||||||
Short: "Run Psalm static analysis",
|
Short: i18n.T("cmd.php.psalm.short"),
|
||||||
Long: "Run Psalm deep static analysis with Laravel plugin support.\n\n" +
|
Long: i18n.T("cmd.php.psalm.long"),
|
||||||
"Psalm provides deeper type inference than PHPStan and catches\n" +
|
|
||||||
"different classes of bugs. Both should be run for best coverage.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php psalm # Run analysis\n" +
|
|
||||||
" core php psalm --fix # Auto-fix issues where possible\n" +
|
|
||||||
" core php psalm --level 3 # Run at specific level (1-8)\n" +
|
|
||||||
" core php psalm --baseline # Generate baseline file",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Psalm is available
|
// Check if Psalm is available
|
||||||
_, found := phppkg.DetectPsalm(cwd)
|
_, found := phppkg.DetectPsalm(cwd)
|
||||||
if !found {
|
if !found {
|
||||||
fmt.Printf("%s Psalm not found\n\n", errorStyle.Render("Error:"))
|
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.psalm.not_found"))
|
||||||
fmt.Printf("%s composer require --dev vimeo/psalm\n", dimStyle.Render("Install:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.psalm.install"))
|
||||||
fmt.Printf("%s ./vendor/bin/psalm --init\n", dimStyle.Render("Setup:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.psalm.setup"))
|
||||||
return fmt.Errorf("psalm not installed")
|
return fmt.Errorf(i18n.T("cmd.php.error.psalm_not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
action := "Analysing"
|
var msg string
|
||||||
if psalmFix {
|
if psalmFix {
|
||||||
action = "Analysing and fixing"
|
msg = i18n.T("cmd.php.psalm.analysing_fixing")
|
||||||
|
} else {
|
||||||
|
msg = i18n.T("cmd.php.psalm.analysing")
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s code with Psalm\n\n", dimStyle.Render("Psalm:"), action)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -271,18 +254,18 @@ func addPHPPsalmCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := phppkg.RunPsalm(ctx, opts); err != nil {
|
if err := phppkg.RunPsalm(ctx, opts); err != nil {
|
||||||
return fmt.Errorf("psalm found issues: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s No issues found\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.psalm.no_issues"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, "Error level (1=strictest, 8=most lenient)")
|
psalmCmd.Flags().IntVar(&psalmLevel, "level", 0, i18n.T("cmd.php.psalm.flag.level"))
|
||||||
psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, "Auto-fix issues where possible")
|
psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, i18n.T("cmd.php.psalm.flag.fix"))
|
||||||
psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, "Generate/update baseline file")
|
psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline"))
|
||||||
psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, "Show info-level issues")
|
psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, i18n.T("cmd.php.psalm.flag.show_info"))
|
||||||
|
|
||||||
parent.AddCommand(psalmCmd)
|
parent.AddCommand(psalmCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -295,24 +278,19 @@ var (
|
||||||
func addPHPAuditCommand(parent *cobra.Command) {
|
func addPHPAuditCommand(parent *cobra.Command) {
|
||||||
auditCmd := &cobra.Command{
|
auditCmd := &cobra.Command{
|
||||||
Use: "audit",
|
Use: "audit",
|
||||||
Short: "Security audit for dependencies",
|
Short: i18n.T("cmd.php.audit.short"),
|
||||||
Long: "Check PHP and JavaScript dependencies for known vulnerabilities.\n\n" +
|
Long: i18n.T("cmd.php.audit.long"),
|
||||||
"Runs composer audit and npm audit (if package.json exists).\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php audit # Check all dependencies\n" +
|
|
||||||
" core php audit --json # Output as JSON\n" +
|
|
||||||
" core php audit --fix # Auto-fix where possible (npm only)",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Scanning dependencies for vulnerabilities\n\n", dimStyle.Render("Audit:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.audit")), i18n.T("cmd.php.audit.scanning"))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -323,7 +301,7 @@ func addPHPAuditCommand(parent *cobra.Command) {
|
||||||
Output: os.Stdout,
|
Output: os.Stdout,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("audit failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.audit_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print results
|
// Print results
|
||||||
|
|
@ -332,15 +310,15 @@ func addPHPAuditCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
icon := successStyle.Render("✓")
|
icon := successStyle.Render("✓")
|
||||||
status := successStyle.Render("secure")
|
status := successStyle.Render(i18n.T("cmd.php.audit.secure"))
|
||||||
|
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
icon = errorStyle.Render("✗")
|
icon = errorStyle.Render("✗")
|
||||||
status = errorStyle.Render("error")
|
status = errorStyle.Render(i18n.T("cmd.php.audit.error"))
|
||||||
hasErrors = true
|
hasErrors = true
|
||||||
} else if result.Vulnerabilities > 0 {
|
} else if result.Vulnerabilities > 0 {
|
||||||
icon = errorStyle.Render("✗")
|
icon = errorStyle.Render("✗")
|
||||||
status = errorStyle.Render(fmt.Sprintf("%d vulnerabilities", result.Vulnerabilities))
|
status = errorStyle.Render(i18n.T("cmd.php.audit.vulnerabilities", map[string]interface{}{"Count": result.Vulnerabilities}))
|
||||||
totalVulns += result.Vulnerabilities
|
totalVulns += result.Vulnerabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,22 +341,22 @@ func addPHPAuditCommand(parent *cobra.Command) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
if totalVulns > 0 {
|
if totalVulns > 0 {
|
||||||
fmt.Printf("%s Found %d vulnerabilities across dependencies\n", errorStyle.Render("Warning:"), totalVulns)
|
fmt.Printf("%s %s\n", errorStyle.Render(i18n.T("cmd.php.label.warning")), i18n.T("cmd.php.audit.found_vulns", map[string]interface{}{"Count": totalVulns}))
|
||||||
fmt.Printf("%s composer update && npm update\n", dimStyle.Render("Fix:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.fix")), i18n.T("cmd.php.audit.fix_hint"))
|
||||||
return fmt.Errorf("vulnerabilities found")
|
return fmt.Errorf(i18n.T("cmd.php.error.vulns_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasErrors {
|
if hasErrors {
|
||||||
return fmt.Errorf("audit completed with errors")
|
return fmt.Errorf(i18n.T("cmd.php.audit.completed_errors"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s All dependencies are secure\n", successStyle.Render("Done:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.audit.all_secure"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, "Output in JSON format")
|
auditCmd.Flags().BoolVar(&auditJSONOutput, "json", false, i18n.T("cmd.php.audit.flag.json"))
|
||||||
auditCmd.Flags().BoolVar(&auditFix, "fix", false, "Auto-fix vulnerabilities (npm only)")
|
auditCmd.Flags().BoolVar(&auditFix, "fix", false, i18n.T("cmd.php.audit.flag.fix"))
|
||||||
|
|
||||||
parent.AddCommand(auditCmd)
|
parent.AddCommand(auditCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -393,25 +371,19 @@ var (
|
||||||
func addPHPSecurityCommand(parent *cobra.Command) {
|
func addPHPSecurityCommand(parent *cobra.Command) {
|
||||||
securityCmd := &cobra.Command{
|
securityCmd := &cobra.Command{
|
||||||
Use: "security",
|
Use: "security",
|
||||||
Short: "Security vulnerability scanning",
|
Short: i18n.T("cmd.php.security.short"),
|
||||||
Long: "Scan for security vulnerabilities in configuration and code.\n\n" +
|
Long: i18n.T("cmd.php.security.long"),
|
||||||
"Checks environment config, file permissions, code patterns,\n" +
|
|
||||||
"and runs security-focused static analysis.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php security # Run all checks\n" +
|
|
||||||
" core php security --severity=high # Only high+ severity\n" +
|
|
||||||
" core php security --json # JSON output",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Running security checks\n\n", dimStyle.Render("Security:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.security")), i18n.T("cmd.php.security.running"))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -424,7 +396,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
|
||||||
Output: os.Stdout,
|
Output: os.Stdout,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("security check failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.security_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print results by category
|
// Print results by category
|
||||||
|
|
@ -436,7 +408,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
currentCategory = category
|
currentCategory = category
|
||||||
fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+" CHECKS:"))
|
fmt.Printf(" %s\n", dimStyle.Render(strings.ToUpper(category)+i18n.T("cmd.php.security.checks_suffix")))
|
||||||
}
|
}
|
||||||
|
|
||||||
icon := successStyle.Render("✓")
|
icon := successStyle.Render("✓")
|
||||||
|
|
@ -448,7 +420,7 @@ func addPHPSecurityCommand(parent *cobra.Command) {
|
||||||
if !check.Passed && check.Message != "" {
|
if !check.Passed && check.Message != "" {
|
||||||
fmt.Printf(" %s\n", dimStyle.Render(check.Message))
|
fmt.Printf(" %s\n", dimStyle.Render(check.Message))
|
||||||
if check.Fix != "" {
|
if check.Fix != "" {
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("Fix:"), check.Fix)
|
fmt.Printf(" %s %s\n", dimStyle.Render(i18n.T("cmd.php.security.fix_label")), check.Fix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -456,34 +428,34 @@ func addPHPSecurityCommand(parent *cobra.Command) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Print summary
|
// Print summary
|
||||||
fmt.Printf("%s Security scan complete\n", dimStyle.Render("Summary:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.summary")), i18n.T("cmd.php.security.summary"))
|
||||||
fmt.Printf(" %s %d/%d\n", dimStyle.Render("Passed:"), result.Summary.Passed, result.Summary.Total)
|
fmt.Printf(" %s %d/%d\n", dimStyle.Render(i18n.T("cmd.php.security.passed")), result.Summary.Passed, result.Summary.Total)
|
||||||
|
|
||||||
if result.Summary.Critical > 0 {
|
if result.Summary.Critical > 0 {
|
||||||
fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render("Critical:"), result.Summary.Critical)
|
fmt.Printf(" %s %d\n", phpSecurityCriticalStyle.Render(i18n.T("cmd.php.security.critical")), result.Summary.Critical)
|
||||||
}
|
}
|
||||||
if result.Summary.High > 0 {
|
if result.Summary.High > 0 {
|
||||||
fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render("High:"), result.Summary.High)
|
fmt.Printf(" %s %d\n", phpSecurityHighStyle.Render(i18n.T("cmd.php.security.high")), result.Summary.High)
|
||||||
}
|
}
|
||||||
if result.Summary.Medium > 0 {
|
if result.Summary.Medium > 0 {
|
||||||
fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render("Medium:"), result.Summary.Medium)
|
fmt.Printf(" %s %d\n", phpSecurityMediumStyle.Render(i18n.T("cmd.php.security.medium")), result.Summary.Medium)
|
||||||
}
|
}
|
||||||
if result.Summary.Low > 0 {
|
if result.Summary.Low > 0 {
|
||||||
fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render("Low:"), result.Summary.Low)
|
fmt.Printf(" %s %d\n", phpSecurityLowStyle.Render(i18n.T("cmd.php.security.low")), result.Summary.Low)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Summary.Critical > 0 || result.Summary.High > 0 {
|
if result.Summary.Critical > 0 || result.Summary.High > 0 {
|
||||||
return fmt.Errorf("critical or high severity issues found")
|
return fmt.Errorf(i18n.T("cmd.php.error.critical_high_issues"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
securityCmd.Flags().StringVar(&securitySeverity, "severity", "", "Minimum severity (critical, high, medium, low)")
|
securityCmd.Flags().StringVar(&securitySeverity, "severity", "", i18n.T("cmd.php.security.flag.severity"))
|
||||||
securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, "Output in JSON format")
|
securityCmd.Flags().BoolVar(&securityJSONOutput, "json", false, i18n.T("cmd.php.security.flag.json"))
|
||||||
securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, "Output in SARIF format (for GitHub Security)")
|
securityCmd.Flags().BoolVar(&securitySarif, "sarif", false, i18n.T("cmd.php.security.flag.sarif"))
|
||||||
securityCmd.Flags().StringVar(&securityURL, "url", "", "URL to check HTTP headers (optional)")
|
securityCmd.Flags().StringVar(&securityURL, "url", "", i18n.T("cmd.php.security.flag.url"))
|
||||||
|
|
||||||
parent.AddCommand(securityCmd)
|
parent.AddCommand(securityCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -497,25 +469,16 @@ var (
|
||||||
func addPHPQACommand(parent *cobra.Command) {
|
func addPHPQACommand(parent *cobra.Command) {
|
||||||
qaCmd := &cobra.Command{
|
qaCmd := &cobra.Command{
|
||||||
Use: "qa",
|
Use: "qa",
|
||||||
Short: "Run full QA pipeline",
|
Short: i18n.T("cmd.php.qa.short"),
|
||||||
Long: "Run the complete quality assurance pipeline.\n\n" +
|
Long: i18n.T("cmd.php.qa.long"),
|
||||||
"Stages:\n" +
|
|
||||||
" quick: Security audit, code style, PHPStan\n" +
|
|
||||||
" standard: Psalm, tests\n" +
|
|
||||||
" full: Rector dry-run, mutation testing (slow)\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php qa # Run quick + standard stages\n" +
|
|
||||||
" core php qa --quick # Only quick checks\n" +
|
|
||||||
" core php qa --full # All stages including slow ones\n" +
|
|
||||||
" core php qa --fix # Auto-fix where possible",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine stages
|
// Determine stages
|
||||||
|
|
@ -532,18 +495,18 @@ func addPHPQACommand(parent *cobra.Command) {
|
||||||
for i, s := range stages {
|
for i, s := range stages {
|
||||||
stageNames[i] = string(s)
|
stageNames[i] = string(s)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s Running QA pipeline (%s)\n\n", dimStyle.Render("QA:"), strings.Join(stageNames, " → "))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.qa")), i18n.T("cmd.php.qa.running", map[string]interface{}{"Stages": strings.Join(stageNames, " -> ")}))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var allPassed = true
|
var allPassed = true
|
||||||
var results []phppkg.QACheckResult
|
var results []phppkg.QACheckResult
|
||||||
|
|
||||||
for _, stage := range stages {
|
for _, stage := range stages {
|
||||||
fmt.Printf("%s\n", phpQAStageStyle.Render("═══ "+strings.ToUpper(string(stage))+" STAGE ═══"))
|
fmt.Printf("%s\n", phpQAStageStyle.Render(i18n.T("cmd.php.qa.stage_prefix")+strings.ToUpper(string(stage))+i18n.T("cmd.php.qa.stage_suffix")))
|
||||||
|
|
||||||
checks := phppkg.GetQAChecks(cwd, stage)
|
checks := phppkg.GetQAChecks(cwd, stage)
|
||||||
if len(checks) == 0 {
|
if len(checks) == 0 {
|
||||||
fmt.Printf(" %s\n\n", dimStyle.Render("No checks available"))
|
fmt.Printf(" %s\n\n", dimStyle.Render(i18n.T("cmd.php.qa.no_checks")))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,10 +516,10 @@ func addPHPQACommand(parent *cobra.Command) {
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
|
|
||||||
icon := phpQAPassedStyle.Render("✓")
|
icon := phpQAPassedStyle.Render("✓")
|
||||||
status := phpQAPassedStyle.Render("passed")
|
status := phpQAPassedStyle.Render(i18n.T("cmd.php.qa.passed"))
|
||||||
if !result.Passed {
|
if !result.Passed {
|
||||||
icon = phpQAFailedStyle.Render("✗")
|
icon = phpQAFailedStyle.Render("✗")
|
||||||
status = phpQAFailedStyle.Render("failed")
|
status = phpQAFailedStyle.Render(i18n.T("cmd.php.qa.failed"))
|
||||||
allPassed = false
|
allPassed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,33 +540,33 @@ func addPHPQACommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if allPassed {
|
if allPassed {
|
||||||
fmt.Printf("%s All checks passed (%d/%d)\n", phpQAPassedStyle.Render("QA PASSED:"), passedCount, len(results))
|
fmt.Printf("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("cmd.php.qa.all_passed", map[string]interface{}{"Passed": passedCount, "Total": len(results)}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Some checks failed (%d/%d passed)\n\n", phpQAFailedStyle.Render("QA FAILED:"), passedCount, len(results))
|
fmt.Printf("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("cmd.php.qa.some_failed", map[string]interface{}{"Passed": passedCount, "Total": len(results)}))
|
||||||
|
|
||||||
// Show what needs fixing
|
// Show what needs fixing
|
||||||
fmt.Printf("%s\n", dimStyle.Render("To fix:"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.php.qa.to_fix")))
|
||||||
for _, check := range failedChecks {
|
for _, check := range failedChecks {
|
||||||
fixCmd := getQAFixCommand(check.Name, qaFix)
|
fixCmd := getQAFixCommand(check.Name, qaFix)
|
||||||
issue := check.Output
|
issue := check.Output
|
||||||
if issue == "" {
|
if issue == "" {
|
||||||
issue = "issues found"
|
issue = "issues found"
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("•"), check.Name+": "+issue)
|
fmt.Printf(" %s %s\n", phpQAFailedStyle.Render("*"), check.Name+": "+issue)
|
||||||
if fixCmd != "" {
|
if fixCmd != "" {
|
||||||
fmt.Printf(" %s %s\n", dimStyle.Render("→"), fixCmd)
|
fmt.Printf(" %s %s\n", dimStyle.Render("->"), fixCmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("QA pipeline failed")
|
return fmt.Errorf(i18n.T("cmd.php.qa.pipeline_failed"))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
qaCmd.Flags().BoolVar(&qaQuick, "quick", false, "Only run quick checks")
|
qaCmd.Flags().BoolVar(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick"))
|
||||||
qaCmd.Flags().BoolVar(&qaFull, "full", false, "Run all stages including slow checks")
|
qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full"))
|
||||||
qaCmd.Flags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible")
|
qaCmd.Flags().BoolVar(&qaFix, "fix", false, i18n.T("cmd.php.qa.flag.fix"))
|
||||||
|
|
||||||
parent.AddCommand(qaCmd)
|
parent.AddCommand(qaCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -611,25 +574,25 @@ func addPHPQACommand(parent *cobra.Command) {
|
||||||
func getQAFixCommand(checkName string, fixEnabled bool) string {
|
func getQAFixCommand(checkName string, fixEnabled bool) string {
|
||||||
switch checkName {
|
switch checkName {
|
||||||
case "audit":
|
case "audit":
|
||||||
return "composer update && npm update"
|
return i18n.T("cmd.php.qa.fix_audit")
|
||||||
case "fmt":
|
case "fmt":
|
||||||
if fixEnabled {
|
if fixEnabled {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "core php fmt --fix"
|
return "core php fmt --fix"
|
||||||
case "analyse":
|
case "analyse":
|
||||||
return "Fix PHPStan errors shown above"
|
return i18n.T("cmd.php.qa.fix_phpstan")
|
||||||
case "psalm":
|
case "psalm":
|
||||||
return "Fix Psalm errors shown above"
|
return i18n.T("cmd.php.qa.fix_psalm")
|
||||||
case "test":
|
case "test":
|
||||||
return "Fix failing tests shown above"
|
return i18n.T("cmd.php.qa.fix_tests")
|
||||||
case "rector":
|
case "rector":
|
||||||
if fixEnabled {
|
if fixEnabled {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return "core php rector --fix"
|
return "core php rector --fix"
|
||||||
case "infection":
|
case "infection":
|
||||||
return "Improve test coverage for mutated code"
|
return i18n.T("cmd.php.qa.fix_infection")
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -662,42 +625,42 @@ func runQACheck(ctx context.Context, dir string, checkName string, fix bool) php
|
||||||
err := phppkg.Format(ctx, phppkg.FormatOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
err := phppkg.Format(ctx, phppkg.FormatOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Code style issues found"
|
result.Output = i18n.T("cmd.php.qa.issue_style")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "analyse":
|
case "analyse":
|
||||||
err := phppkg.Analyse(ctx, phppkg.AnalyseOptions{Dir: dir, Output: &buf})
|
err := phppkg.Analyse(ctx, phppkg.AnalyseOptions{Dir: dir, Output: &buf})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Static analysis errors"
|
result.Output = i18n.T("cmd.php.qa.issue_analysis")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "psalm":
|
case "psalm":
|
||||||
err := phppkg.RunPsalm(ctx, phppkg.PsalmOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
err := phppkg.RunPsalm(ctx, phppkg.PsalmOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Type errors found"
|
result.Output = i18n.T("cmd.php.qa.issue_types")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "test":
|
case "test":
|
||||||
err := phppkg.RunTests(ctx, phppkg.TestOptions{Dir: dir, Output: io.Discard})
|
err := phppkg.RunTests(ctx, phppkg.TestOptions{Dir: dir, Output: io.Discard})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Test failures"
|
result.Output = i18n.T("cmd.php.qa.issue_tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "rector":
|
case "rector":
|
||||||
err := phppkg.RunRector(ctx, phppkg.RectorOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
err := phppkg.RunRector(ctx, phppkg.RectorOptions{Dir: dir, Fix: fix, Output: io.Discard})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Code improvements available"
|
result.Output = i18n.T("cmd.php.qa.issue_rector")
|
||||||
}
|
}
|
||||||
|
|
||||||
case "infection":
|
case "infection":
|
||||||
err := phppkg.RunInfection(ctx, phppkg.InfectionOptions{Dir: dir, Output: io.Discard})
|
err := phppkg.RunInfection(ctx, phppkg.InfectionOptions{Dir: dir, Output: io.Discard})
|
||||||
result.Passed = err == nil
|
result.Passed = err == nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Output = "Mutation score below threshold"
|
result.Output = i18n.T("cmd.php.qa.issue_mutation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,37 +677,33 @@ var (
|
||||||
func addPHPRectorCommand(parent *cobra.Command) {
|
func addPHPRectorCommand(parent *cobra.Command) {
|
||||||
rectorCmd := &cobra.Command{
|
rectorCmd := &cobra.Command{
|
||||||
Use: "rector",
|
Use: "rector",
|
||||||
Short: "Automated code refactoring",
|
Short: i18n.T("cmd.php.rector.short"),
|
||||||
Long: "Run Rector for automated code improvements and PHP upgrades.\n\n" +
|
Long: i18n.T("cmd.php.rector.long"),
|
||||||
"Rector can automatically upgrade PHP syntax, improve code quality,\n" +
|
|
||||||
"and apply framework-specific refactorings.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php rector # Dry-run (show changes)\n" +
|
|
||||||
" core php rector --fix # Apply changes\n" +
|
|
||||||
" core php rector --diff # Show detailed diff",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Rector is available
|
// Check if Rector is available
|
||||||
if !phppkg.DetectRector(cwd) {
|
if !phppkg.DetectRector(cwd) {
|
||||||
fmt.Printf("%s Rector not found\n\n", errorStyle.Render("Error:"))
|
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.rector.not_found"))
|
||||||
fmt.Printf("%s composer require --dev rector/rector\n", dimStyle.Render("Install:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.rector.install"))
|
||||||
fmt.Printf("%s ./vendor/bin/rector init\n", dimStyle.Render("Setup:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.setup")), i18n.T("cmd.php.rector.setup"))
|
||||||
return fmt.Errorf("rector not installed")
|
return fmt.Errorf(i18n.T("cmd.php.error.rector_not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
action := "Analysing"
|
var msg string
|
||||||
if rectorFix {
|
if rectorFix {
|
||||||
action = "Refactoring"
|
msg = i18n.T("cmd.php.rector.refactoring")
|
||||||
|
} else {
|
||||||
|
msg = i18n.T("cmd.php.rector.analysing")
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s code with Rector\n\n", dimStyle.Render("Rector:"), action)
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.rector")), msg)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -758,25 +717,25 @@ func addPHPRectorCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
if err := phppkg.RunRector(ctx, opts); err != nil {
|
if err := phppkg.RunRector(ctx, opts); err != nil {
|
||||||
if rectorFix {
|
if rectorFix {
|
||||||
return fmt.Errorf("rector failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.rector_failed"), err)
|
||||||
}
|
}
|
||||||
// Dry-run returns non-zero if changes would be made
|
// Dry-run returns non-zero if changes would be made
|
||||||
fmt.Printf("\n%s Changes suggested (use --fix to apply)\n", phpQAWarningStyle.Render("Info:"))
|
fmt.Printf("\n%s %s\n", phpQAWarningStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.rector.changes_suggested"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if rectorFix {
|
if rectorFix {
|
||||||
fmt.Printf("\n%s Code refactored successfully\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.rector.refactored"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s No changes needed\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.rector.no_changes"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, "Apply changes (default is dry-run)")
|
rectorCmd.Flags().BoolVar(&rectorFix, "fix", false, i18n.T("cmd.php.rector.flag.fix"))
|
||||||
rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, "Show detailed diff of changes")
|
rectorCmd.Flags().BoolVar(&rectorDiff, "diff", false, i18n.T("cmd.php.rector.flag.diff"))
|
||||||
rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, "Clear Rector cache before running")
|
rectorCmd.Flags().BoolVar(&rectorClearCache, "clear-cache", false, i18n.T("cmd.php.rector.flag.clear_cache"))
|
||||||
|
|
||||||
parent.AddCommand(rectorCmd)
|
parent.AddCommand(rectorCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -792,34 +751,27 @@ var (
|
||||||
func addPHPInfectionCommand(parent *cobra.Command) {
|
func addPHPInfectionCommand(parent *cobra.Command) {
|
||||||
infectionCmd := &cobra.Command{
|
infectionCmd := &cobra.Command{
|
||||||
Use: "infection",
|
Use: "infection",
|
||||||
Short: "Mutation testing for test quality",
|
Short: i18n.T("cmd.php.infection.short"),
|
||||||
Long: "Run Infection mutation testing to measure test suite quality.\n\n" +
|
Long: i18n.T("cmd.php.infection.long"),
|
||||||
"Mutation testing modifies your code and checks if tests catch\n" +
|
|
||||||
"the changes. High mutation score = high quality tests.\n\n" +
|
|
||||||
"Warning: This can be slow on large codebases.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core php infection # Run mutation testing\n" +
|
|
||||||
" core php infection --min-msi=70 # Require 70% mutation score\n" +
|
|
||||||
" core php infection --filter=User # Only test User* files",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !phppkg.IsPHPProject(cwd) {
|
if !phppkg.IsPHPProject(cwd) {
|
||||||
return fmt.Errorf("not a PHP project (missing composer.json)")
|
return fmt.Errorf(i18n.T("cmd.php.error.not_php"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Infection is available
|
// Check if Infection is available
|
||||||
if !phppkg.DetectInfection(cwd) {
|
if !phppkg.DetectInfection(cwd) {
|
||||||
fmt.Printf("%s Infection not found\n\n", errorStyle.Render("Error:"))
|
fmt.Printf("%s %s\n\n", errorStyle.Render(i18n.T("cmd.php.label.error")), i18n.T("cmd.php.infection.not_found"))
|
||||||
fmt.Printf("%s composer require --dev infection/infection\n", dimStyle.Render("Install:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.install")), i18n.T("cmd.php.infection.install"))
|
||||||
return fmt.Errorf("infection not installed")
|
return fmt.Errorf(i18n.T("cmd.php.error.infection_not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Running mutation testing\n", dimStyle.Render("Infection:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.php.label.infection")), i18n.T("cmd.php.infection.running"))
|
||||||
fmt.Printf("%s This may take a while...\n\n", dimStyle.Render("Note:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.info")), i18n.T("cmd.php.infection.note"))
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -834,19 +786,19 @@ func addPHPInfectionCommand(parent *cobra.Command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := phppkg.RunInfection(ctx, opts); err != nil {
|
if err := phppkg.RunInfection(ctx, opts); err != nil {
|
||||||
return fmt.Errorf("mutation testing failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.php.error.infection_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s Mutation testing complete\n", successStyle.Render("Done:"))
|
fmt.Printf("\n%s %s\n", successStyle.Render(i18n.T("cmd.php.label.done")), i18n.T("cmd.php.infection.complete"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, "Minimum mutation score indicator (0-100, default: 50)")
|
infectionCmd.Flags().IntVar(&infectionMinMSI, "min-msi", 0, i18n.T("cmd.php.infection.flag.min_msi"))
|
||||||
infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, "Minimum covered mutation score (0-100, default: 70)")
|
infectionCmd.Flags().IntVar(&infectionMinCoveredMSI, "min-covered-msi", 0, i18n.T("cmd.php.infection.flag.min_covered_msi"))
|
||||||
infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, "Number of parallel threads (default: 4)")
|
infectionCmd.Flags().IntVar(&infectionThreads, "threads", 0, i18n.T("cmd.php.infection.flag.threads"))
|
||||||
infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", "Filter files by pattern")
|
infectionCmd.Flags().StringVar(&infectionFilter, "filter", "", i18n.T("cmd.php.infection.flag.filter"))
|
||||||
infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, "Only mutate covered code")
|
infectionCmd.Flags().BoolVar(&infectionOnlyCovered, "only-covered", false, i18n.T("cmd.php.infection.flag.only_covered"))
|
||||||
|
|
||||||
parent.AddCommand(infectionCmd)
|
parent.AddCommand(infectionCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,14 +21,8 @@ var (
|
||||||
func AddPkgCommands(root *cobra.Command) {
|
func AddPkgCommands(root *cobra.Command) {
|
||||||
pkgCmd := &cobra.Command{
|
pkgCmd := &cobra.Command{
|
||||||
Use: "pkg",
|
Use: "pkg",
|
||||||
Short: "Package management for core-* repos",
|
Short: i18n.T("cmd.pkg.short"),
|
||||||
Long: "Manage host-uk/core-* packages and repositories.\n\n" +
|
Long: i18n.T("cmd.pkg.long"),
|
||||||
"Commands:\n" +
|
|
||||||
" search Search GitHub for packages\n" +
|
|
||||||
" install Clone a package from GitHub\n" +
|
|
||||||
" list List installed packages\n" +
|
|
||||||
" update Update installed packages\n" +
|
|
||||||
" outdated Check for outdated packages",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.AddCommand(pkgCmd)
|
root.AddCommand(pkgCmd)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -20,22 +21,18 @@ var (
|
||||||
func addPkgInstallCommand(parent *cobra.Command) {
|
func addPkgInstallCommand(parent *cobra.Command) {
|
||||||
installCmd := &cobra.Command{
|
installCmd := &cobra.Command{
|
||||||
Use: "install <org/repo>",
|
Use: "install <org/repo>",
|
||||||
Short: "Clone a package from GitHub",
|
Short: i18n.T("cmd.pkg.install.short"),
|
||||||
Long: "Clones a repository from GitHub.\n\n" +
|
Long: i18n.T("cmd.pkg.install.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core pkg install host-uk/core-php\n" +
|
|
||||||
" core pkg install host-uk/core-tenant --dir ./packages\n" +
|
|
||||||
" core pkg install host-uk/core-admin --add",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("repository is required (e.g., core pkg install host-uk/core-php)")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.repo_required"))
|
||||||
}
|
}
|
||||||
return runPkgInstall(args[0], installTargetDir, installAddToReg)
|
return runPkgInstall(args[0], installTargetDir, installAddToReg)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd.Flags().StringVar(&installTargetDir, "dir", "", "Target directory (default: ./packages or current dir)")
|
installCmd.Flags().StringVar(&installTargetDir, "dir", "", i18n.T("cmd.pkg.install.flag.dir"))
|
||||||
installCmd.Flags().BoolVar(&installAddToReg, "add", false, "Add to repos.yaml registry")
|
installCmd.Flags().BoolVar(&installAddToReg, "add", false, i18n.T("cmd.pkg.install.flag.add"))
|
||||||
|
|
||||||
parent.AddCommand(installCmd)
|
parent.AddCommand(installCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +43,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
// Parse org/repo
|
// Parse org/repo
|
||||||
parts := strings.Split(repoArg, "/")
|
parts := strings.Split(repoArg, "/")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return fmt.Errorf("invalid repo format: use org/repo (e.g., host-uk/core-php)")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.invalid_repo_format"))
|
||||||
}
|
}
|
||||||
org, repoName := parts[0], parts[1]
|
org, repoName := parts[0], parts[1]
|
||||||
|
|
||||||
|
|
@ -76,19 +73,19 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
repoPath := filepath.Join(targetDir, repoName)
|
repoPath := filepath.Join(targetDir, repoName)
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
|
if _, err := os.Stat(filepath.Join(repoPath, ".git")); err == nil {
|
||||||
fmt.Printf("%s %s already exists at %s\n", dimStyle.Render("Skip:"), repoName, repoPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.install.skip_label")), i18n.T("cmd.pkg.install.already_exists", map[string]string{"Name": repoName, "Path": repoPath}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %w", err)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.create_directory"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s/%s\n", dimStyle.Render("Installing:"), org, repoName)
|
fmt.Printf("%s %s/%s\n", dimStyle.Render(i18n.T("cmd.pkg.install.installing_label")), org, repoName)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Target:"), repoPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.install.target_label")), repoPath)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
fmt.Printf(" %s... ", dimStyle.Render("Cloning"))
|
fmt.Printf(" %s... ", dimStyle.Render(i18n.T("cmd.pkg.install.cloning")))
|
||||||
err := gitClone(ctx, org, repoName, repoPath)
|
err := gitClone(ctx, org, repoName, repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
fmt.Printf("%s\n", errorStyle.Render("✗ "+err.Error()))
|
||||||
|
|
@ -98,14 +95,14 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
|
|
||||||
if addToRegistry {
|
if addToRegistry {
|
||||||
if err := addToRegistryFile(org, repoName); err != nil {
|
if err := addToRegistryFile(org, repoName); err != nil {
|
||||||
fmt.Printf(" %s add to registry: %s\n", errorStyle.Render("✗"), err)
|
fmt.Printf(" %s %s: %s\n", errorStyle.Render("✗"), i18n.T("cmd.pkg.install.add_to_registry"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" %s added to repos.yaml\n", successStyle.Render("✓"))
|
fmt.Printf(" %s %s\n", successStyle.Render("✓"), i18n.T("cmd.pkg.install.added_to_registry"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Installed %s\n", successStyle.Render("Done:"), repoName)
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.pkg.install.done_label")), i18n.T("cmd.pkg.install.installed", map[string]string{"Name": repoName}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +110,7 @@ func runPkgInstall(repoArg, targetDir string, addToRegistry bool) error {
|
||||||
func addToRegistryFile(org, repoName string) error {
|
func addToRegistryFile(org, repoName string) error {
|
||||||
regPath, err := repos.FindRegistry()
|
regPath, err := repos.FindRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("no repos.yaml found")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := repos.LoadRegistry(regPath)
|
reg, err := repos.LoadRegistry(regPath)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -15,11 +16,8 @@ import (
|
||||||
func addPkgListCommand(parent *cobra.Command) {
|
func addPkgListCommand(parent *cobra.Command) {
|
||||||
listCmd := &cobra.Command{
|
listCmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List installed packages",
|
Short: i18n.T("cmd.pkg.list.short"),
|
||||||
Long: "Lists all packages in the current workspace.\n\n" +
|
Long: i18n.T("cmd.pkg.list.long"),
|
||||||
"Reads from repos.yaml or scans for git repositories.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core pkg list",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPkgList()
|
return runPkgList()
|
||||||
},
|
},
|
||||||
|
|
@ -31,12 +29,12 @@ func addPkgListCommand(parent *cobra.Command) {
|
||||||
func runPkgList() error {
|
func runPkgList() error {
|
||||||
regPath, err := repos.FindRegistry()
|
regPath, err := repos.FindRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("no repos.yaml found - run from workspace directory")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml_workspace"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := repos.LoadRegistry(regPath)
|
reg, err := repos.LoadRegistry(regPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
basePath := reg.BasePath
|
basePath := reg.BasePath
|
||||||
|
|
@ -49,11 +47,11 @@ func runPkgList() error {
|
||||||
|
|
||||||
allRepos := reg.List()
|
allRepos := reg.List()
|
||||||
if len(allRepos) == 0 {
|
if len(allRepos) == 0 {
|
||||||
fmt.Println("No packages in registry.")
|
fmt.Println(i18n.T("cmd.pkg.list.no_packages"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n\n", repoNameStyle.Render("Installed Packages"))
|
fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.pkg.list.title")))
|
||||||
|
|
||||||
var installed, missing int
|
var installed, missing int
|
||||||
for _, r := range allRepos {
|
for _, r := range allRepos {
|
||||||
|
|
@ -76,7 +74,7 @@ func runPkgList() error {
|
||||||
desc = desc[:37] + "..."
|
desc = desc[:37] + "..."
|
||||||
}
|
}
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = dimStyle.Render("(no description)")
|
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name))
|
fmt.Printf(" %s %s\n", status, repoNameStyle.Render(r.Name))
|
||||||
|
|
@ -84,10 +82,10 @@ func runPkgList() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d installed, %d missing\n", dimStyle.Render("Total:"), installed, missing)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.list.total_label")), i18n.T("cmd.pkg.list.summary", map[string]int{"Installed": installed, "Missing": missing}))
|
||||||
|
|
||||||
if missing > 0 {
|
if missing > 0 {
|
||||||
fmt.Printf("\nInstall missing: %s\n", dimStyle.Render("core setup"))
|
fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.list.install_missing"), dimStyle.Render("core setup"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -99,20 +97,17 @@ var updateAll bool
|
||||||
func addPkgUpdateCommand(parent *cobra.Command) {
|
func addPkgUpdateCommand(parent *cobra.Command) {
|
||||||
updateCmd := &cobra.Command{
|
updateCmd := &cobra.Command{
|
||||||
Use: "update [packages...]",
|
Use: "update [packages...]",
|
||||||
Short: "Update installed packages",
|
Short: i18n.T("cmd.pkg.update.short"),
|
||||||
Long: "Pulls latest changes for installed packages.\n\n" +
|
Long: i18n.T("cmd.pkg.update.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core pkg update core-php # Update specific package\n" +
|
|
||||||
" core pkg update --all # Update all packages",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if !updateAll && len(args) == 0 {
|
if !updateAll && len(args) == 0 {
|
||||||
return fmt.Errorf("specify package name or use --all")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.specify_package"))
|
||||||
}
|
}
|
||||||
return runPkgUpdate(args, updateAll)
|
return runPkgUpdate(args, updateAll)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd.Flags().BoolVar(&updateAll, "all", false, "Update all packages")
|
updateCmd.Flags().BoolVar(&updateAll, "all", false, i18n.T("cmd.pkg.update.flag.all"))
|
||||||
|
|
||||||
parent.AddCommand(updateCmd)
|
parent.AddCommand(updateCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -120,12 +115,12 @@ func addPkgUpdateCommand(parent *cobra.Command) {
|
||||||
func runPkgUpdate(packages []string, all bool) error {
|
func runPkgUpdate(packages []string, all bool) error {
|
||||||
regPath, err := repos.FindRegistry()
|
regPath, err := repos.FindRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("no repos.yaml found")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := repos.LoadRegistry(regPath)
|
reg, err := repos.LoadRegistry(regPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
basePath := reg.BasePath
|
basePath := reg.BasePath
|
||||||
|
|
@ -145,14 +140,14 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
toUpdate = packages
|
toUpdate = packages
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Updating %d package(s)\n\n", dimStyle.Render("Update:"), len(toUpdate))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.update.update_label")), i18n.T("cmd.pkg.update.updating", map[string]int{"Count": len(toUpdate)}))
|
||||||
|
|
||||||
var updated, skipped, failed int
|
var updated, skipped, failed int
|
||||||
for _, name := range toUpdate {
|
for _, name := range toUpdate {
|
||||||
repoPath := filepath.Join(basePath, name)
|
repoPath := filepath.Join(basePath, name)
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(repoPath, ".git")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(repoPath, ".git")); os.IsNotExist(err) {
|
||||||
fmt.Printf(" %s %s (not installed)\n", dimStyle.Render("○"), name)
|
fmt.Printf(" %s %s (%s)\n", dimStyle.Render("○"), name, i18n.T("cmd.pkg.update.not_installed"))
|
||||||
skipped++
|
skipped++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +164,7 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(string(output), "Already up to date") {
|
if strings.Contains(string(output), "Already up to date") {
|
||||||
fmt.Printf("%s\n", dimStyle.Render("up to date"))
|
fmt.Printf("%s\n", dimStyle.Render(i18n.T("cmd.pkg.update.up_to_date")))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s\n", successStyle.Render("✓"))
|
fmt.Printf("%s\n", successStyle.Render("✓"))
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +172,8 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d updated, %d skipped, %d failed\n",
|
fmt.Printf("%s %s\n",
|
||||||
dimStyle.Render("Done:"), updated, skipped, failed)
|
dimStyle.Render(i18n.T("cmd.pkg.update.done_label")), i18n.T("cmd.pkg.update.summary", map[string]int{"Updated": updated, "Skipped": skipped, "Failed": failed}))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -187,10 +182,8 @@ func runPkgUpdate(packages []string, all bool) error {
|
||||||
func addPkgOutdatedCommand(parent *cobra.Command) {
|
func addPkgOutdatedCommand(parent *cobra.Command) {
|
||||||
outdatedCmd := &cobra.Command{
|
outdatedCmd := &cobra.Command{
|
||||||
Use: "outdated",
|
Use: "outdated",
|
||||||
Short: "Check for outdated packages",
|
Short: i18n.T("cmd.pkg.outdated.short"),
|
||||||
Long: "Checks which packages have unpulled commits.\n\n" +
|
Long: i18n.T("cmd.pkg.outdated.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core pkg outdated",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runPkgOutdated()
|
return runPkgOutdated()
|
||||||
},
|
},
|
||||||
|
|
@ -202,12 +195,12 @@ func addPkgOutdatedCommand(parent *cobra.Command) {
|
||||||
func runPkgOutdated() error {
|
func runPkgOutdated() error {
|
||||||
regPath, err := repos.FindRegistry()
|
regPath, err := repos.FindRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("no repos.yaml found")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.no_repos_yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := repos.LoadRegistry(regPath)
|
reg, err := repos.LoadRegistry(regPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load registry: %w", err)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.load_registry"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
basePath := reg.BasePath
|
basePath := reg.BasePath
|
||||||
|
|
@ -218,7 +211,7 @@ func runPkgOutdated() error {
|
||||||
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
basePath = filepath.Join(filepath.Dir(regPath), basePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Checking for updates...\n\n", dimStyle.Render("Outdated:"))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.pkg.outdated.outdated_label")), i18n.T("cmd.pkg.outdated.checking"))
|
||||||
|
|
||||||
var outdated, upToDate, notInstalled int
|
var outdated, upToDate, notInstalled int
|
||||||
|
|
||||||
|
|
@ -242,8 +235,8 @@ func runPkgOutdated() error {
|
||||||
|
|
||||||
count := strings.TrimSpace(string(output))
|
count := strings.TrimSpace(string(output))
|
||||||
if count != "0" {
|
if count != "0" {
|
||||||
fmt.Printf(" %s %s (%s commits behind)\n",
|
fmt.Printf(" %s %s (%s)\n",
|
||||||
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), count)
|
errorStyle.Render("↓"), repoNameStyle.Render(r.Name), i18n.T("cmd.pkg.outdated.commits_behind", map[string]string{"Count": count}))
|
||||||
outdated++
|
outdated++
|
||||||
} else {
|
} else {
|
||||||
upToDate++
|
upToDate++
|
||||||
|
|
@ -252,11 +245,11 @@ func runPkgOutdated() error {
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if outdated == 0 {
|
if outdated == 0 {
|
||||||
fmt.Printf("%s All packages up to date\n", successStyle.Render("Done:"))
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.pkg.outdated.done_label")), i18n.T("cmd.pkg.outdated.all_up_to_date"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s %d outdated, %d up to date\n",
|
fmt.Printf("%s %s\n",
|
||||||
dimStyle.Render("Summary:"), outdated, upToDate)
|
dimStyle.Render(i18n.T("cmd.pkg.outdated.summary_label")), i18n.T("cmd.pkg.outdated.summary", map[string]int{"Outdated": outdated, "UpToDate": upToDate}))
|
||||||
fmt.Printf("\nUpdate with: %s\n", dimStyle.Render("core pkg update --all"))
|
fmt.Printf("\n%s %s\n", i18n.T("cmd.pkg.outdated.update_with"), dimStyle.Render("core pkg update --all"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/cache"
|
"github.com/host-uk/core/pkg/cache"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -27,14 +28,8 @@ var (
|
||||||
func addPkgSearchCommand(parent *cobra.Command) {
|
func addPkgSearchCommand(parent *cobra.Command) {
|
||||||
searchCmd := &cobra.Command{
|
searchCmd := &cobra.Command{
|
||||||
Use: "search",
|
Use: "search",
|
||||||
Short: "Search GitHub for packages",
|
Short: i18n.T("cmd.pkg.search.short"),
|
||||||
Long: "Searches GitHub for repositories matching a pattern.\n" +
|
Long: i18n.T("cmd.pkg.search.long"),
|
||||||
"Uses gh CLI for authenticated search. Results are cached for 1 hour.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core pkg search # List all host-uk repos\n" +
|
|
||||||
" core pkg search --pattern 'core-*' # Search for core-* repos\n" +
|
|
||||||
" core pkg search --org mycompany # Search different org\n" +
|
|
||||||
" core pkg search --refresh # Bypass cache",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
org := searchOrg
|
org := searchOrg
|
||||||
pattern := searchPattern
|
pattern := searchPattern
|
||||||
|
|
@ -52,11 +47,11 @@ func addPkgSearchCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
searchCmd.Flags().StringVar(&searchOrg, "org", "", "GitHub organization (default: host-uk)")
|
searchCmd.Flags().StringVar(&searchOrg, "org", "", i18n.T("cmd.pkg.search.flag.org"))
|
||||||
searchCmd.Flags().StringVar(&searchPattern, "pattern", "", "Repo name pattern (* for wildcard)")
|
searchCmd.Flags().StringVar(&searchPattern, "pattern", "", i18n.T("cmd.pkg.search.flag.pattern"))
|
||||||
searchCmd.Flags().StringVar(&searchType, "type", "", "Filter by type in name (mod, services, plug, website)")
|
searchCmd.Flags().StringVar(&searchType, "type", "", i18n.T("cmd.pkg.search.flag.type"))
|
||||||
searchCmd.Flags().IntVar(&searchLimit, "limit", 0, "Max results (default 50)")
|
searchCmd.Flags().IntVar(&searchLimit, "limit", 0, i18n.T("cmd.pkg.search.flag.limit"))
|
||||||
searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, "Bypass cache and fetch fresh data")
|
searchCmd.Flags().BoolVar(&searchRefresh, "refresh", false, i18n.T("cmd.pkg.search.flag.refresh"))
|
||||||
|
|
||||||
parent.AddCommand(searchCmd)
|
parent.AddCommand(searchCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -91,22 +86,22 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
if found, err := c.Get(cacheKey, &ghRepos); found && err == nil {
|
if found, err := c.Get(cacheKey, &ghRepos); found && err == nil {
|
||||||
fromCache = true
|
fromCache = true
|
||||||
age := c.Age(cacheKey)
|
age := c.Age(cacheKey)
|
||||||
fmt.Printf("%s %s %s\n", dimStyle.Render("Cache:"), org, dimStyle.Render(fmt.Sprintf("(%s ago)", age.Round(time.Second))))
|
fmt.Printf("%s %s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.cache_label")), org, dimStyle.Render(fmt.Sprintf("(%s ago)", age.Round(time.Second))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch from GitHub if not cached
|
// Fetch from GitHub if not cached
|
||||||
if !fromCache {
|
if !fromCache {
|
||||||
if !ghAuthenticated() {
|
if !ghAuthenticated() {
|
||||||
return fmt.Errorf("gh CLI not authenticated. Run: gh auth login")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.gh_not_authenticated"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("GH_TOKEN") != "" {
|
if os.Getenv("GH_TOKEN") != "" {
|
||||||
fmt.Printf("%s GH_TOKEN env var is set - this may cause auth issues\n", dimStyle.Render("Note:"))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.pkg.search.note_label")), i18n.T("cmd.pkg.search.gh_token_warning"))
|
||||||
fmt.Printf("%s Unset it with: unset GH_TOKEN\n\n", dimStyle.Render(""))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(""), i18n.T("cmd.pkg.search.gh_token_unset"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s... ", dimStyle.Render("Fetching:"), org)
|
fmt.Printf("%s %s... ", dimStyle.Render(i18n.T("cmd.pkg.search.fetching_label")), org)
|
||||||
|
|
||||||
cmd := exec.Command("gh", "repo", "list", org,
|
cmd := exec.Command("gh", "repo", "list", org,
|
||||||
"--json", "name,description,visibility,updatedAt,primaryLanguage",
|
"--json", "name,description,visibility,updatedAt,primaryLanguage",
|
||||||
|
|
@ -117,13 +112,13 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
errStr := strings.TrimSpace(string(output))
|
errStr := strings.TrimSpace(string(output))
|
||||||
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
|
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
|
||||||
return fmt.Errorf("authentication failed - try: unset GH_TOKEN && gh auth login")
|
return fmt.Errorf(i18n.T("cmd.pkg.error.auth_failed"))
|
||||||
}
|
}
|
||||||
return fmt.Errorf("search failed: %s", errStr)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.search_failed"), errStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(output, &ghRepos); err != nil {
|
if err := json.Unmarshal(output, &ghRepos); err != nil {
|
||||||
return fmt.Errorf("failed to parse results: %w", err)
|
return fmt.Errorf(i18n.T("cmd.pkg.error.parse_results"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
|
|
@ -146,7 +141,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
fmt.Println("No repositories found matching pattern.")
|
fmt.Println(i18n.T("cmd.pkg.search.no_repos_found"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,12 +149,12 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
return filtered[i].Name < filtered[j].Name
|
return filtered[i].Name < filtered[j].Name
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Printf("Found %d repositories:\n\n", len(filtered))
|
fmt.Printf(i18n.T("cmd.pkg.search.found_repos", map[string]int{"Count": len(filtered)}) + "\n\n")
|
||||||
|
|
||||||
for _, r := range filtered {
|
for _, r := range filtered {
|
||||||
visibility := ""
|
visibility := ""
|
||||||
if r.Visibility == "private" {
|
if r.Visibility == "private" {
|
||||||
visibility = dimStyle.Render(" [private]")
|
visibility = dimStyle.Render(" " + i18n.T("cmd.pkg.search.private_label"))
|
||||||
}
|
}
|
||||||
|
|
||||||
desc := r.Description
|
desc := r.Description
|
||||||
|
|
@ -167,7 +162,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
desc = desc[:47] + "..."
|
desc = desc[:47] + "..."
|
||||||
}
|
}
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = dimStyle.Render("(no description)")
|
desc = dimStyle.Render(i18n.T("cmd.pkg.no_description"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility)
|
fmt.Printf(" %s%s\n", repoNameStyle.Render(r.Name), visibility)
|
||||||
|
|
@ -175,7 +170,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Install with: %s\n", dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org)))
|
fmt.Printf("%s %s\n", i18n.T("cmd.pkg.search.install_with"), dimStyle.Render(fmt.Sprintf("core pkg install %s/<repo-name>", org)))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
sdkpkg "github.com/host-uk/core/pkg/sdk"
|
sdkpkg "github.com/host-uk/core/pkg/sdk"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -20,13 +21,8 @@ var (
|
||||||
|
|
||||||
var sdkCmd = &cobra.Command{
|
var sdkCmd = &cobra.Command{
|
||||||
Use: "sdk",
|
Use: "sdk",
|
||||||
Short: "SDK validation and API compatibility tools",
|
Short: i18n.T("cmd.sdk.short"),
|
||||||
Long: `Tools for validating OpenAPI specs and checking API compatibility.
|
Long: i18n.T("cmd.sdk.long"),
|
||||||
To generate SDKs, use: core build sdk
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
diff Check for breaking API changes
|
|
||||||
validate Validate OpenAPI spec syntax`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffBasePath string
|
var diffBasePath string
|
||||||
|
|
@ -34,7 +30,8 @@ var diffSpecPath string
|
||||||
|
|
||||||
var sdkDiffCmd = &cobra.Command{
|
var sdkDiffCmd = &cobra.Command{
|
||||||
Use: "diff",
|
Use: "diff",
|
||||||
Short: "Check for breaking API changes",
|
Short: i18n.T("cmd.sdk.diff.short"),
|
||||||
|
Long: i18n.T("cmd.sdk.diff.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runSDKDiff(diffBasePath, diffSpecPath)
|
return runSDKDiff(diffBasePath, diffSpecPath)
|
||||||
},
|
},
|
||||||
|
|
@ -44,7 +41,8 @@ var validateSpecPath string
|
||||||
|
|
||||||
var sdkValidateCmd = &cobra.Command{
|
var sdkValidateCmd = &cobra.Command{
|
||||||
Use: "validate",
|
Use: "validate",
|
||||||
Short: "Validate OpenAPI spec",
|
Short: i18n.T("cmd.sdk.validate.short"),
|
||||||
|
Long: i18n.T("cmd.sdk.validate.long"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runSDKValidate(validateSpecPath)
|
return runSDKValidate(validateSpecPath)
|
||||||
},
|
},
|
||||||
|
|
@ -52,11 +50,11 @@ var sdkValidateCmd = &cobra.Command{
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// sdk diff flags
|
// sdk diff flags
|
||||||
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", "Base spec (version tag or file)")
|
sdkDiffCmd.Flags().StringVar(&diffBasePath, "base", "", i18n.T("cmd.sdk.diff.flag.base"))
|
||||||
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", "Current spec file")
|
sdkDiffCmd.Flags().StringVar(&diffSpecPath, "spec", "", i18n.T("cmd.sdk.diff.flag.spec"))
|
||||||
|
|
||||||
// sdk validate flags
|
// sdk validate flags
|
||||||
sdkValidateCmd.Flags().StringVar(&validateSpecPath, "spec", "", "Path to OpenAPI spec file")
|
sdkValidateCmd.Flags().StringVar(&validateSpecPath, "spec", "", i18n.T("cmd.sdk.validate.flag.spec"))
|
||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
sdkCmd.AddCommand(sdkDiffCmd)
|
sdkCmd.AddCommand(sdkDiffCmd)
|
||||||
|
|
@ -66,7 +64,7 @@ func init() {
|
||||||
func runSDKDiff(basePath, specPath string) error {
|
func runSDKDiff(basePath, specPath string) error {
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.sdk.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect current spec if not provided
|
// Detect current spec if not provided
|
||||||
|
|
@ -79,49 +77,49 @@ func runSDKDiff(basePath, specPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
return fmt.Errorf("--base is required (version tag or file path)")
|
return fmt.Errorf(i18n.T("cmd.sdk.diff.error.base_required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Checking for breaking changes\n", sdkHeaderStyle.Render("SDK Diff:"))
|
fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.diff.label")), i18n.T("cmd.sdk.diff.checking"))
|
||||||
fmt.Printf(" Base: %s\n", sdkDimStyle.Render(basePath))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.base_label"), sdkDimStyle.Render(basePath))
|
||||||
fmt.Printf(" Current: %s\n", sdkDimStyle.Render(specPath))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.diff.current_label"), sdkDimStyle.Render(specPath))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
result, err := sdkpkg.Diff(basePath, specPath)
|
result, err := sdkpkg.Diff(basePath, specPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.label.error")), err)
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Breaking {
|
if result.Breaking {
|
||||||
fmt.Printf("%s %s\n", sdkErrorStyle.Render("Breaking:"), result.Summary)
|
fmt.Printf("%s %s\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.diff.breaking")), result.Summary)
|
||||||
for _, change := range result.Changes {
|
for _, change := range result.Changes {
|
||||||
fmt.Printf(" - %s\n", change)
|
fmt.Printf(" - %s\n", change)
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", sdkSuccessStyle.Render("OK:"), result.Summary)
|
fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), result.Summary)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSDKValidate(specPath string) error {
|
func runSDKValidate(specPath string) error {
|
||||||
projectDir, err := os.Getwd()
|
projectDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.sdk.error.working_dir"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath})
|
s := sdkpkg.New(projectDir, &sdkpkg.Config{Spec: specPath})
|
||||||
|
|
||||||
fmt.Printf("%s Validating OpenAPI spec\n", sdkHeaderStyle.Render("SDK:"))
|
fmt.Printf("%s %s\n", sdkHeaderStyle.Render(i18n.T("cmd.sdk.label.sdk")), i18n.T("cmd.sdk.validate.validating"))
|
||||||
|
|
||||||
detectedPath, err := s.DetectSpec()
|
detectedPath, err := s.DetectSpec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
|
fmt.Printf("%s %v\n", sdkErrorStyle.Render(i18n.T("cmd.sdk.label.error")), err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(" Spec: %s\n", sdkDimStyle.Render(detectedPath))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.sdk.validate.spec_label"), sdkDimStyle.Render(detectedPath))
|
||||||
fmt.Printf("%s Spec is valid\n", sdkSuccessStyle.Render("OK:"))
|
fmt.Printf("%s %s\n", sdkSuccessStyle.Render(i18n.T("cmd.sdk.label.ok")), i18n.T("cmd.sdk.validate.valid"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -33,31 +34,20 @@ var (
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Bootstrap workspace or clone packages from registry",
|
Short: i18n.T("cmd.setup.short"),
|
||||||
Long: `Sets up a development workspace.
|
Long: i18n.T("cmd.setup.long"),
|
||||||
|
|
||||||
REGISTRY MODE (repos.yaml exists):
|
|
||||||
Clones all repositories defined in repos.yaml into packages/.
|
|
||||||
Skips repos that already exist. Use --only to filter by type.
|
|
||||||
|
|
||||||
BOOTSTRAP MODE (no repos.yaml):
|
|
||||||
1. Clones core-devops to set up the workspace
|
|
||||||
2. Presents an interactive wizard to select packages
|
|
||||||
3. Clones selected packages
|
|
||||||
|
|
||||||
Use --all to skip the wizard and clone everything.`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runSetupOrchestrator(registryPath, only, dryRun, all, name, build)
|
return runSetupOrchestrator(registryPath, only, dryRun, all, name, build)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
setupCmd.Flags().StringVar(®istryPath, "registry", "", "Path to repos.yaml (auto-detected if not specified)")
|
setupCmd.Flags().StringVar(®istryPath, "registry", "", i18n.T("cmd.setup.flag.registry"))
|
||||||
setupCmd.Flags().StringVar(&only, "only", "", "Only clone repos of these types (comma-separated: foundation,module,product)")
|
setupCmd.Flags().StringVar(&only, "only", "", i18n.T("cmd.setup.flag.only"))
|
||||||
setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be cloned without cloning")
|
setupCmd.Flags().BoolVar(&dryRun, "dry-run", false, i18n.T("cmd.setup.flag.dry_run"))
|
||||||
setupCmd.Flags().BoolVar(&all, "all", false, "Skip wizard, clone all packages (non-interactive)")
|
setupCmd.Flags().BoolVar(&all, "all", false, i18n.T("cmd.setup.flag.all"))
|
||||||
setupCmd.Flags().StringVar(&name, "name", "", "Project directory name for bootstrap mode")
|
setupCmd.Flags().StringVar(&name, "name", "", i18n.T("cmd.setup.flag.name"))
|
||||||
setupCmd.Flags().BoolVar(&build, "build", false, "Run build after cloning")
|
setupCmd.Flags().BoolVar(&build, "build", false, i18n.T("cmd.setup.flag.build"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSetupCommand adds the 'setup' command to the given parent command.
|
// AddSetupCommand adds the 'setup' command to the given parent command.
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
|
||||||
return fmt.Errorf("failed to get working directory: %w", err)
|
return fmt.Errorf("failed to get working directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s Bootstrap mode (no repos.yaml found)\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.bootstrap_mode"))
|
||||||
|
|
||||||
var targetDir string
|
var targetDir string
|
||||||
|
|
||||||
|
|
@ -58,7 +59,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
|
||||||
if empty {
|
if empty {
|
||||||
// Clone into current directory
|
// Clone into current directory
|
||||||
targetDir = cwd
|
targetDir = cwd
|
||||||
fmt.Printf("%s Cloning into current directory\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.cloning_current_dir"))
|
||||||
} else {
|
} else {
|
||||||
// Directory has content - check if it's a git repo root
|
// Directory has content - check if it's a git repo root
|
||||||
isRepo := isGitRepoRoot(cwd)
|
isRepo := isGitRepoRoot(cwd)
|
||||||
|
|
@ -90,7 +91,7 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
|
||||||
}
|
}
|
||||||
|
|
||||||
targetDir = filepath.Join(cwd, projectName)
|
targetDir = filepath.Join(cwd, projectName)
|
||||||
fmt.Printf("%s Creating project directory: %s\n", dimStyle.Render(">>"), projectName)
|
fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.creating_project_dir"), projectName)
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
|
@ -102,25 +103,25 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
|
||||||
// Clone core-devops first
|
// Clone core-devops first
|
||||||
devopsPath := filepath.Join(targetDir, devopsRepo)
|
devopsPath := filepath.Join(targetDir, devopsRepo)
|
||||||
if _, err := os.Stat(filepath.Join(devopsPath, ".git")); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(devopsPath, ".git")); os.IsNotExist(err) {
|
||||||
fmt.Printf("%s Cloning %s...\n", dimStyle.Render(">>"), devopsRepo)
|
fmt.Printf("%s %s %s...\n", dimStyle.Render(">>"), i18n.T("cmd.setup.cloning"), devopsRepo)
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
if err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil {
|
if err := gitClone(ctx, defaultOrg, devopsRepo, devopsPath); err != nil {
|
||||||
return fmt.Errorf("failed to clone %s: %w", devopsRepo, err)
|
return fmt.Errorf("failed to clone %s: %w", devopsRepo, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s cloned\n", successStyle.Render(">>"), devopsRepo)
|
fmt.Printf("%s %s %s\n", successStyle.Render(">>"), devopsRepo, i18n.T("cmd.setup.cloned"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" Would clone %s/%s to %s\n", defaultOrg, devopsRepo, devopsPath)
|
fmt.Printf(" %s %s/%s to %s\n", i18n.T("cmd.setup.would_clone"), defaultOrg, devopsRepo, devopsPath)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s %s already exists\n", dimStyle.Render(">>"), devopsRepo)
|
fmt.Printf("%s %s %s\n", dimStyle.Render(">>"), devopsRepo, i18n.T("cmd.setup.already_exists"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the repos.yaml from core-devops
|
// Load the repos.yaml from core-devops
|
||||||
registryPath := filepath.Join(devopsPath, devopsReposYaml)
|
registryPath := filepath.Join(devopsPath, devopsReposYaml)
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf("\n%s Would load registry from %s and present package wizard\n", dimStyle.Render(">>"), registryPath)
|
fmt.Printf("\n%s %s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.would_load_registry"), registryPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -29,8 +30,8 @@ func runRegistrySetup(ctx context.Context, registryPath, only string, dryRun, al
|
||||||
|
|
||||||
// runRegistrySetupWithReg runs setup with an already-loaded registry.
|
// runRegistrySetupWithReg runs setup with an already-loaded registry.
|
||||||
func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryPath, only string, dryRun, all, runBuild bool) error {
|
func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryPath, only string, dryRun, all, runBuild bool) error {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Registry:"), registryPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.registry_label")), registryPath)
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Org:"), reg.Org)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.org_label")), reg.Org)
|
||||||
|
|
||||||
// Determine base path for cloning
|
// Determine base path for cloning
|
||||||
basePath := reg.BasePath
|
basePath := reg.BasePath
|
||||||
|
|
@ -47,7 +48,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
basePath = filepath.Join(home, basePath[2:])
|
basePath = filepath.Join(home, basePath[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Target:"), basePath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.target_label")), basePath)
|
||||||
|
|
||||||
// Parse type filter
|
// Parse type filter
|
||||||
var typeFilter []string
|
var typeFilter []string
|
||||||
|
|
@ -55,7 +56,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
for _, t := range strings.Split(only, ",") {
|
for _, t := range strings.Split(only, ",") {
|
||||||
typeFilter = append(typeFilter, strings.TrimSpace(t))
|
typeFilter = append(typeFilter, strings.TrimSpace(t))
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Filter:"), only)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.filter_label")), only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure base path exists
|
// Ensure base path exists
|
||||||
|
|
@ -136,15 +137,18 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%d to clone, %d exist, %d skipped\n", len(toClone), exists, skipped)
|
fmt.Printf("%s, %s, %s\n",
|
||||||
|
i18n.T("cmd.setup.to_clone", map[string]interface{}{"Count": len(toClone)}),
|
||||||
|
i18n.T("cmd.setup.exist", map[string]interface{}{"Count": exists}),
|
||||||
|
i18n.T("cmd.setup.skipped", map[string]interface{}{"Count": skipped}))
|
||||||
|
|
||||||
if len(toClone) == 0 {
|
if len(toClone) == 0 {
|
||||||
fmt.Println("\nNothing to clone.")
|
fmt.Printf("\n%s\n", i18n.T("cmd.setup.nothing_to_clone"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Println("\nWould clone:")
|
fmt.Printf("\n%s\n", i18n.T("cmd.setup.would_clone_list"))
|
||||||
for _, repo := range toClone {
|
for _, repo := range toClone {
|
||||||
fmt.Printf(" %s (%s)\n", repoNameStyle.Render(repo.Name), repo.Type)
|
fmt.Printf(" %s (%s)\n", repoNameStyle.Render(repo.Name), repo.Type)
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +162,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !confirmed {
|
if !confirmed {
|
||||||
fmt.Println("Cancelled.")
|
fmt.Println(i18n.T("cmd.setup.cancelled"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +172,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
var succeeded, failed int
|
var succeeded, failed int
|
||||||
|
|
||||||
for _, repo := range toClone {
|
for _, repo := range toClone {
|
||||||
fmt.Printf(" %s %s... ", dimStyle.Render("Cloning"), repo.Name)
|
fmt.Printf(" %s %s... ", dimStyle.Render(i18n.T("cmd.setup.cloning")), repo.Name)
|
||||||
|
|
||||||
repoPath := filepath.Join(basePath, repo.Name)
|
repoPath := filepath.Join(basePath, repo.Name)
|
||||||
|
|
||||||
|
|
@ -177,32 +181,32 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
|
||||||
fmt.Printf("%s\n", errorStyle.Render("x "+err.Error()))
|
fmt.Printf("%s\n", errorStyle.Render("x "+err.Error()))
|
||||||
failed++
|
failed++
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s\n", successStyle.Render("done"))
|
fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.setup.done")))
|
||||||
succeeded++
|
succeeded++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s %d cloned", successStyle.Render("Done:"), succeeded)
|
fmt.Printf("%s %s", successStyle.Render(i18n.T("cmd.setup.done_label")), i18n.T("cmd.setup.cloned_count", map[string]interface{}{"Count": succeeded}))
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
fmt.Printf(", %s", errorStyle.Render(fmt.Sprintf("%d failed", failed)))
|
fmt.Printf(", %s", errorStyle.Render(i18n.T("cmd.setup.failed_count", map[string]interface{}{"Count": failed})))
|
||||||
}
|
}
|
||||||
if exists > 0 {
|
if exists > 0 {
|
||||||
fmt.Printf(", %d already exist", exists)
|
fmt.Printf(", %s", i18n.T("cmd.setup.already_exist_count", map[string]interface{}{"Count": exists}))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Run build if requested
|
// Run build if requested
|
||||||
if runBuild && succeeded > 0 {
|
if runBuild && succeeded > 0 {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("%s Running build...\n", dimStyle.Render(">>"))
|
fmt.Printf("%s %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.running_build"))
|
||||||
buildCmd := exec.Command("core", "build")
|
buildCmd := exec.Command("core", "build")
|
||||||
buildCmd.Dir = basePath
|
buildCmd.Dir = basePath
|
||||||
buildCmd.Stdout = os.Stdout
|
buildCmd.Stdout = os.Stdout
|
||||||
buildCmd.Stderr = os.Stderr
|
buildCmd.Stderr = os.Stderr
|
||||||
if err := buildCmd.Run(); err != nil {
|
if err := buildCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("build failed: %w", err)
|
return fmt.Errorf("%s: %w", i18n.T("cmd.setup.error.build_failed"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,17 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runRepoSetup sets up the current repository with .core/ configuration.
|
// runRepoSetup sets up the current repository with .core/ configuration.
|
||||||
func runRepoSetup(repoPath string, dryRun bool) error {
|
func runRepoSetup(repoPath string, dryRun bool) error {
|
||||||
fmt.Printf("%s Setting up repository: %s\n", dimStyle.Render(">>"), repoPath)
|
fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.setting_up"), repoPath)
|
||||||
|
|
||||||
// Detect project type
|
// Detect project type
|
||||||
projectType := detectProjectType(repoPath)
|
projectType := detectProjectType(repoPath)
|
||||||
fmt.Printf("%s Detected project type: %s\n", dimStyle.Render(">>"), projectType)
|
fmt.Printf("%s %s: %s\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.detected_type"), projectType)
|
||||||
|
|
||||||
// Create .core directory
|
// Create .core directory
|
||||||
coreDir := filepath.Join(repoPath, ".core")
|
coreDir := filepath.Join(repoPath, ".core")
|
||||||
|
|
@ -39,7 +41,7 @@ func runRepoSetup(repoPath string, dryRun bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Printf("\n%s Would create:\n", dimStyle.Render(">>"))
|
fmt.Printf("\n%s %s:\n", dimStyle.Render(">>"), i18n.T("cmd.setup.repo.would_create"))
|
||||||
for filename, content := range configs {
|
for filename, content := range configs {
|
||||||
fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename))
|
fmt.Printf("\n %s:\n", filepath.Join(coreDir, filename))
|
||||||
// Indent content for display
|
// Indent content for display
|
||||||
|
|
@ -55,7 +57,7 @@ func runRepoSetup(repoPath string, dryRun bool) error {
|
||||||
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", filename, err)
|
return fmt.Errorf("failed to write %s: %w", filename, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s Created %s\n", successStyle.Render(">>"), configPath)
|
fmt.Printf("%s %s %s\n", successStyle.Render(">>"), i18n.T("cmd.setup.repo.created"), configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/host-uk/core/pkg/repos"
|
"github.com/host-uk/core/pkg/repos"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
@ -35,11 +36,11 @@ func promptSetupChoice() (string, error) {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewSelect[string]().
|
huh.NewSelect[string]().
|
||||||
Title("This directory is a git repository").
|
Title(i18n.T("cmd.setup.wizard.git_repo_title")).
|
||||||
Description("What would you like to do?").
|
Description(i18n.T("cmd.setup.wizard.what_to_do")).
|
||||||
Options(
|
Options(
|
||||||
huh.NewOption("Setup Working Directory", "setup").Selected(true),
|
huh.NewOption(i18n.T("cmd.setup.wizard.option_setup"), "setup").Selected(true),
|
||||||
huh.NewOption("Create Package (clone repos into subdirectory)", "package"),
|
huh.NewOption(i18n.T("cmd.setup.wizard.option_package"), "package"),
|
||||||
).
|
).
|
||||||
Value(&choice),
|
Value(&choice),
|
||||||
),
|
),
|
||||||
|
|
@ -59,8 +60,8 @@ func promptProjectName(defaultName string) (string, error) {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewInput().
|
huh.NewInput().
|
||||||
Title("Project directory name").
|
Title(i18n.T("cmd.setup.wizard.project_name_title")).
|
||||||
Description("Enter the name for your new workspace directory").
|
Description(i18n.T("cmd.setup.wizard.project_name_desc")).
|
||||||
Placeholder(defaultName).
|
Placeholder(defaultName).
|
||||||
Value(&name),
|
Value(&name),
|
||||||
),
|
),
|
||||||
|
|
@ -158,14 +159,14 @@ func runPackageWizard(reg *repos.Registry, preselectedTypes []string) ([]string,
|
||||||
// Header styling
|
// Header styling
|
||||||
headerStyle := shared.TitleStyle.MarginBottom(1)
|
headerStyle := shared.TitleStyle.MarginBottom(1)
|
||||||
|
|
||||||
fmt.Println(headerStyle.Render("Package Selection"))
|
fmt.Println(headerStyle.Render(i18n.T("cmd.setup.wizard.package_selection")))
|
||||||
fmt.Println("Use space to select/deselect, enter to confirm")
|
fmt.Println(i18n.T("cmd.setup.wizard.selection_hint"))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewMultiSelect[string]().
|
huh.NewMultiSelect[string]().
|
||||||
Title("Select packages to clone").
|
Title(i18n.T("cmd.setup.wizard.select_packages")).
|
||||||
Options(options...).
|
Options(options...).
|
||||||
Value(&selected).
|
Value(&selected).
|
||||||
Filterable(true).
|
Filterable(true).
|
||||||
|
|
@ -195,9 +196,9 @@ func confirmClone(count int, target string) (bool, error) {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewConfirm().
|
huh.NewConfirm().
|
||||||
Title(fmt.Sprintf("Clone %d packages to %s?", count, target)).
|
Title(i18n.T("cmd.setup.wizard.confirm_clone", map[string]interface{}{"Count": count, "Target": target})).
|
||||||
Affirmative("Yes, clone").
|
Affirmative(i18n.T("cmd.setup.wizard.confirm_yes")).
|
||||||
Negative("Cancel").
|
Negative(i18n.T("cmd.setup.wizard.confirm_cancel")).
|
||||||
Value(&confirmed),
|
Value(&confirmed),
|
||||||
),
|
),
|
||||||
).WithTheme(wizardTheme())
|
).WithTheme(wizardTheme())
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package testcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -33,31 +34,19 @@ var (
|
||||||
|
|
||||||
var testCmd = &cobra.Command{
|
var testCmd = &cobra.Command{
|
||||||
Use: "test",
|
Use: "test",
|
||||||
Short: "Run tests with coverage",
|
Short: i18n.T("cmd.test.short"),
|
||||||
Long: `Runs Go tests with coverage reporting.
|
Long: i18n.T("cmd.test.long"),
|
||||||
|
|
||||||
Sets MACOSX_DEPLOYMENT_TARGET=26.0 to suppress linker warnings on macOS.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
core test # Run all tests with coverage summary
|
|
||||||
core test --verbose # Show test output as it runs
|
|
||||||
core test --coverage # Show detailed per-package coverage
|
|
||||||
core test --pkg ./pkg/... # Test specific packages
|
|
||||||
core test --run TestName # Run specific test by name
|
|
||||||
core test --short # Skip long-running tests
|
|
||||||
core test --race # Enable race detector
|
|
||||||
core test --json # Output JSON for CI/agents`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON)
|
return runTest(testVerbose, testCoverage, testShort, testPkg, testRun, testRace, testJSON)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
testCmd.Flags().BoolVar(&testVerbose, "verbose", false, "Show test output as it runs (-v)")
|
testCmd.Flags().BoolVar(&testVerbose, "verbose", false, i18n.T("cmd.test.flag.verbose"))
|
||||||
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, "Show detailed per-package coverage")
|
testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.test.flag.coverage"))
|
||||||
testCmd.Flags().BoolVar(&testShort, "short", false, "Skip long-running tests (-short)")
|
testCmd.Flags().BoolVar(&testShort, "short", false, i18n.T("cmd.test.flag.short"))
|
||||||
testCmd.Flags().StringVar(&testPkg, "pkg", "", "Package pattern to test (default: ./...)")
|
testCmd.Flags().StringVar(&testPkg, "pkg", "", i18n.T("cmd.test.flag.pkg"))
|
||||||
testCmd.Flags().StringVar(&testRun, "run", "", "Run only tests matching this regex (-run)")
|
testCmd.Flags().StringVar(&testRun, "run", "", i18n.T("cmd.test.flag.run"))
|
||||||
testCmd.Flags().BoolVar(&testRace, "race", false, "Enable race detector (-race)")
|
testCmd.Flags().BoolVar(&testRace, "race", false, i18n.T("cmd.test.flag.race"))
|
||||||
testCmd.Flags().BoolVar(&testJSON, "json", false, "Output JSON for CI/agents")
|
testCmd.Flags().BoolVar(&testJSON, "json", false, i18n.T("cmd.test.flag.json"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
type packageCoverage struct {
|
type packageCoverage struct {
|
||||||
|
|
@ -84,19 +85,19 @@ func printTestSummary(results testResults, showCoverage bool) {
|
||||||
// Print pass/fail summary
|
// Print pass/fail summary
|
||||||
total := results.passed + results.failed
|
total := results.passed + results.failed
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
fmt.Printf(" %s %d passed", testPassStyle.Render("✓"), results.passed)
|
fmt.Printf(" %s %s", testPassStyle.Render("✓"), i18n.T("cmd.test.passed", map[string]interface{}{"Count": results.passed}))
|
||||||
if results.failed > 0 {
|
if results.failed > 0 {
|
||||||
fmt.Printf(" %s %d failed", testFailStyle.Render("✗"), results.failed)
|
fmt.Printf(" %s %s", testFailStyle.Render("✗"), i18n.T("cmd.test.failed", map[string]interface{}{"Count": results.failed}))
|
||||||
}
|
}
|
||||||
if results.skipped > 0 {
|
if results.skipped > 0 {
|
||||||
fmt.Printf(" %s %d skipped", testSkipStyle.Render("○"), results.skipped)
|
fmt.Printf(" %s %s", testSkipStyle.Render("○"), i18n.T("cmd.test.skipped", map[string]interface{}{"Count": results.skipped}))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print failed packages
|
// Print failed packages
|
||||||
if len(results.failedPkgs) > 0 {
|
if len(results.failedPkgs) > 0 {
|
||||||
fmt.Printf("\n Failed packages:\n")
|
fmt.Printf("\n %s\n", i18n.T("cmd.test.failed_packages"))
|
||||||
for _, pkg := range results.failedPkgs {
|
for _, pkg := range results.failedPkgs {
|
||||||
fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg)
|
fmt.Printf(" %s %s\n", testFailStyle.Render("✗"), pkg)
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +108,7 @@ func printTestSummary(results testResults, showCoverage bool) {
|
||||||
printCoverageSummary(results)
|
printCoverageSummary(results)
|
||||||
} else if results.covCount > 0 {
|
} else if results.covCount > 0 {
|
||||||
avgCov := results.totalCov / float64(results.covCount)
|
avgCov := results.totalCov / float64(results.covCount)
|
||||||
fmt.Printf("\n Coverage: %s\n", formatCoverage(avgCov))
|
fmt.Printf("\n %s %s\n", i18n.T("cmd.test.label.coverage"), formatCoverage(avgCov))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +117,7 @@ func printCoverageSummary(results testResults) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n %s\n", testHeaderStyle.Render("Coverage by package:"))
|
fmt.Printf("\n %s\n", testHeaderStyle.Render(i18n.T("cmd.test.coverage_by_package")))
|
||||||
|
|
||||||
// Sort packages by name
|
// Sort packages by name
|
||||||
sort.Slice(results.packages, func(i, j int) bool {
|
sort.Slice(results.packages, func(i, j int) bool {
|
||||||
|
|
@ -145,8 +146,9 @@ func printCoverageSummary(results testResults) {
|
||||||
// Print average
|
// Print average
|
||||||
if results.covCount > 0 {
|
if results.covCount > 0 {
|
||||||
avgCov := results.totalCov / float64(results.covCount)
|
avgCov := results.totalCov / float64(results.covCount)
|
||||||
padding := strings.Repeat(" ", maxLen-7+2)
|
avgLabel := i18n.T("cmd.test.label.average")
|
||||||
fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render("Average"), padding, formatCoverage(avgCov))
|
padding := strings.Repeat(" ", maxLen-len(avgLabel)+2)
|
||||||
|
fmt.Printf("\n %s%s%s\n", testHeaderStyle.Render(avgLabel), padding, formatCoverage(avgCov))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error {
|
func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bool) error {
|
||||||
// Detect if we're in a Go project
|
// Detect if we're in a Go project
|
||||||
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
|
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("no go.mod found - run from a Go project directory")
|
return fmt.Errorf(i18n.T("cmd.test.error.no_go_mod"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build command arguments
|
// Build command arguments
|
||||||
|
|
@ -52,10 +54,10 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
|
||||||
cmd.Env = append(os.Environ(), getMacOSDeploymentTarget())
|
cmd.Env = append(os.Environ(), getMacOSDeploymentTarget())
|
||||||
|
|
||||||
if !jsonOutput {
|
if !jsonOutput {
|
||||||
fmt.Printf("%s Running tests\n", testHeaderStyle.Render("Test:"))
|
fmt.Printf("%s %s\n", testHeaderStyle.Render(i18n.T("cmd.test.label.test")), i18n.T("cmd.test.running"))
|
||||||
fmt.Printf(" Package: %s\n", testDimStyle.Render(pkg))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.test.label.package"), testDimStyle.Render(pkg))
|
||||||
if run != "" {
|
if run != "" {
|
||||||
fmt.Printf(" Filter: %s\n", testDimStyle.Render(run))
|
fmt.Printf(" %s %s\n", i18n.T("cmd.test.label.filter"), testDimStyle.Render(run))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +93,7 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
|
||||||
// JSON output for CI/agents
|
// JSON output for CI/agents
|
||||||
printJSONResults(results, exitCode)
|
printJSONResults(results, exitCode)
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
return fmt.Errorf("tests failed")
|
return fmt.Errorf(i18n.T("cmd.test.error.tests_failed"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -106,11 +108,11 @@ func runTest(verbose, coverage, short bool, pkg, run string, race, jsonOutput bo
|
||||||
}
|
}
|
||||||
|
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
fmt.Printf("\n%s Tests failed\n", testFailStyle.Render("FAIL"))
|
fmt.Printf("\n%s %s\n", testFailStyle.Render(i18n.T("cli.fail")), i18n.T("cmd.test.tests_failed"))
|
||||||
return fmt.Errorf("tests failed")
|
return fmt.Errorf(i18n.T("cmd.test.error.tests_failed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\n%s All tests passed\n", testPassStyle.Render("PASS"))
|
fmt.Printf("\n%s %s\n", testPassStyle.Render(i18n.T("cli.pass")), i18n.T("cmd.test.all_passed"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/container"
|
"github.com/host-uk/core/pkg/container"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,17 +28,8 @@ var (
|
||||||
func addVMRunCommand(parent *cobra.Command) {
|
func addVMRunCommand(parent *cobra.Command) {
|
||||||
runCmd := &cobra.Command{
|
runCmd := &cobra.Command{
|
||||||
Use: "run [image]",
|
Use: "run [image]",
|
||||||
Short: "Run a LinuxKit image or template",
|
Short: i18n.T("cmd.vm.run.short"),
|
||||||
Long: "Runs a LinuxKit image as a VM using the available hypervisor.\n\n" +
|
Long: i18n.T("cmd.vm.run.long"),
|
||||||
"Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" +
|
|
||||||
"You can also run from a template using --template, which will build and run\n" +
|
|
||||||
"the image automatically. Use --var to set template variables.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core vm run image.iso\n" +
|
|
||||||
" core vm run -d image.qcow2\n" +
|
|
||||||
" core vm run --name myvm --memory 2048 --cpus 4 image.iso\n" +
|
|
||||||
" core vm run --template core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" +
|
|
||||||
" core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts := container.RunOptions{
|
opts := container.RunOptions{
|
||||||
Name: runName,
|
Name: runName,
|
||||||
|
|
@ -55,7 +47,7 @@ func addVMRunCommand(parent *cobra.Command) {
|
||||||
|
|
||||||
// Otherwise, require an image path
|
// Otherwise, require an image path
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("image path is required (or use --template)")
|
return fmt.Errorf(i18n.T("cmd.vm.run.error.image_required"))
|
||||||
}
|
}
|
||||||
image := args[0]
|
image := args[0]
|
||||||
|
|
||||||
|
|
@ -63,13 +55,13 @@ func addVMRunCommand(parent *cobra.Command) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runCmd.Flags().StringVar(&runName, "name", "", "Name for the container")
|
runCmd.Flags().StringVar(&runName, "name", "", i18n.T("cmd.vm.run.flag.name"))
|
||||||
runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, "Run in detached mode (background)")
|
runCmd.Flags().BoolVarP(&runDetach, "detach", "d", false, i18n.T("cmd.vm.run.flag.detach"))
|
||||||
runCmd.Flags().IntVar(&runMemory, "memory", 0, "Memory in MB (default: 1024)")
|
runCmd.Flags().IntVar(&runMemory, "memory", 0, i18n.T("cmd.vm.run.flag.memory"))
|
||||||
runCmd.Flags().IntVar(&runCPUs, "cpus", 0, "Number of CPUs (default: 1)")
|
runCmd.Flags().IntVar(&runCPUs, "cpus", 0, i18n.T("cmd.vm.run.flag.cpus"))
|
||||||
runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, "SSH port for exec commands (default: 2222)")
|
runCmd.Flags().IntVar(&runSSHPort, "ssh-port", 0, i18n.T("cmd.vm.run.flag.ssh_port"))
|
||||||
runCmd.Flags().StringVar(&runTemplateName, "template", "", "Run from a LinuxKit template (build + run)")
|
runCmd.Flags().StringVar(&runTemplateName, "template", "", i18n.T("cmd.vm.run.flag.template"))
|
||||||
runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, "Template variable in KEY=VALUE format (can be repeated)")
|
runCmd.Flags().StringArrayVar(&runVarFlags, "var", nil, i18n.T("cmd.vm.run.flag.var"))
|
||||||
|
|
||||||
parent.AddCommand(runCmd)
|
parent.AddCommand(runCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +69,7 @@ func addVMRunCommand(parent *cobra.Command) {
|
||||||
func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error {
|
func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error {
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := container.RunOptions{
|
opts := container.RunOptions{
|
||||||
|
|
@ -88,27 +80,27 @@ func runContainer(image, name string, detach bool, memory, cpus, sshPort int) er
|
||||||
SSHPort: sshPort,
|
SSHPort: sshPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), image)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.image")), image)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Name:"), name)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.name")), name)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Hypervisor:"), manager.Hypervisor().Name())
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
c, err := manager.Run(ctx, image, opts)
|
c, err := manager.Run(ctx, image, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run container: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.run_container")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if detach {
|
if detach {
|
||||||
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID)
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.vm.label.started")), c.ID)
|
||||||
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID)
|
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8])
|
fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]interface{}{"ID": c.ID[:8]}))
|
||||||
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8])
|
fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]interface{}{"ID": c.ID[:8]}))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s %s\n", dimStyle.Render("Container stopped:"), c.ID)
|
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -120,17 +112,14 @@ var psAll bool
|
||||||
func addVMPsCommand(parent *cobra.Command) {
|
func addVMPsCommand(parent *cobra.Command) {
|
||||||
psCmd := &cobra.Command{
|
psCmd := &cobra.Command{
|
||||||
Use: "ps",
|
Use: "ps",
|
||||||
Short: "List running VMs",
|
Short: i18n.T("cmd.vm.ps.short"),
|
||||||
Long: "Lists all VMs. By default, only shows running VMs.\n\n" +
|
Long: i18n.T("cmd.vm.ps.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core vm ps\n" +
|
|
||||||
" core vm ps -a",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return listContainers(psAll)
|
return listContainers(psAll)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
psCmd.Flags().BoolVarP(&psAll, "all", "a", false, "Show all containers (including stopped)")
|
psCmd.Flags().BoolVarP(&psAll, "all", "a", false, i18n.T("cmd.vm.ps.flag.all"))
|
||||||
|
|
||||||
parent.AddCommand(psCmd)
|
parent.AddCommand(psCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -138,13 +127,13 @@ func addVMPsCommand(parent *cobra.Command) {
|
||||||
func listContainers(all bool) error {
|
func listContainers(all bool) error {
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
containers, err := manager.List(ctx)
|
containers, err := manager.List(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list containers: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.list_containers")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter if not showing all
|
// Filter if not showing all
|
||||||
|
|
@ -160,15 +149,15 @@ func listContainers(all bool) error {
|
||||||
|
|
||||||
if len(containers) == 0 {
|
if len(containers) == 0 {
|
||||||
if all {
|
if all {
|
||||||
fmt.Println("No containers")
|
fmt.Println(i18n.T("cmd.vm.ps.no_containers"))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("No running containers")
|
fmt.Println(i18n.T("cmd.vm.ps.no_running"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintln(w, "ID\tNAME\tIMAGE\tSTATUS\tSTARTED\tPID")
|
fmt.Fprintln(w, i18n.T("cmd.vm.ps.header"))
|
||||||
fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---")
|
fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---")
|
||||||
|
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
|
|
@ -217,14 +206,11 @@ func formatDuration(d time.Duration) string {
|
||||||
func addVMStopCommand(parent *cobra.Command) {
|
func addVMStopCommand(parent *cobra.Command) {
|
||||||
stopCmd := &cobra.Command{
|
stopCmd := &cobra.Command{
|
||||||
Use: "stop <container-id>",
|
Use: "stop <container-id>",
|
||||||
Short: "Stop a running VM",
|
Short: i18n.T("cmd.vm.stop.short"),
|
||||||
Long: "Stops a running VM by ID.\n\n" +
|
Long: i18n.T("cmd.vm.stop.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core vm stop abc12345\n" +
|
|
||||||
" core vm stop abc1",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("container ID is required")
|
return fmt.Errorf(i18n.T("cmd.vm.error.id_required"))
|
||||||
}
|
}
|
||||||
return stopContainer(args[0])
|
return stopContainer(args[0])
|
||||||
},
|
},
|
||||||
|
|
@ -236,7 +222,7 @@ func addVMStopCommand(parent *cobra.Command) {
|
||||||
func stopContainer(id string) error {
|
func stopContainer(id string) error {
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support partial ID matching
|
// Support partial ID matching
|
||||||
|
|
@ -245,14 +231,14 @@ func stopContainer(id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Stopping:"), fullID[:8])
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8])
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := manager.Stop(ctx, fullID); err != nil {
|
if err := manager.Stop(ctx, fullID); err != nil {
|
||||||
return fmt.Errorf("failed to stop container: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.stop_container")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", successStyle.Render("Stopped"))
|
fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.stop.stopped")))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,11 +259,11 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s
|
||||||
|
|
||||||
switch len(matches) {
|
switch len(matches) {
|
||||||
case 0:
|
case 0:
|
||||||
return "", fmt.Errorf("no container found matching: %s", partialID)
|
return "", fmt.Errorf(i18n.T("cmd.vm.error.no_match", map[string]interface{}{"ID": partialID}))
|
||||||
case 1:
|
case 1:
|
||||||
return matches[0].ID, nil
|
return matches[0].ID, nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("multiple containers match '%s', be more specific", partialID)
|
return "", fmt.Errorf(i18n.T("cmd.vm.error.multiple_match", map[string]interface{}{"ID": partialID}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,20 +273,17 @@ var logsFollow bool
|
||||||
func addVMLogsCommand(parent *cobra.Command) {
|
func addVMLogsCommand(parent *cobra.Command) {
|
||||||
logsCmd := &cobra.Command{
|
logsCmd := &cobra.Command{
|
||||||
Use: "logs <container-id>",
|
Use: "logs <container-id>",
|
||||||
Short: "View VM logs",
|
Short: i18n.T("cmd.vm.logs.short"),
|
||||||
Long: "View logs from a VM.\n\n" +
|
Long: i18n.T("cmd.vm.logs.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core vm logs abc12345\n" +
|
|
||||||
" core vm logs -f abc1",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("container ID is required")
|
return fmt.Errorf(i18n.T("cmd.vm.error.id_required"))
|
||||||
}
|
}
|
||||||
return viewLogs(args[0], logsFollow)
|
return viewLogs(args[0], logsFollow)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, "Follow log output")
|
logsCmd.Flags().BoolVarP(&logsFollow, "follow", "f", false, i18n.T("cmd.vm.logs.flag.follow"))
|
||||||
|
|
||||||
parent.AddCommand(logsCmd)
|
parent.AddCommand(logsCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +291,7 @@ func addVMLogsCommand(parent *cobra.Command) {
|
||||||
func viewLogs(id string, follow bool) error {
|
func viewLogs(id string, follow bool) error {
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fullID, err := resolveContainerID(manager, id)
|
fullID, err := resolveContainerID(manager, id)
|
||||||
|
|
@ -319,7 +302,7 @@ func viewLogs(id string, follow bool) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
reader, err := manager.Logs(ctx, fullID, follow)
|
reader, err := manager.Logs(ctx, fullID, follow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get logs: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.get_logs")+": %w", err)
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
|
|
@ -331,14 +314,11 @@ func viewLogs(id string, follow bool) error {
|
||||||
func addVMExecCommand(parent *cobra.Command) {
|
func addVMExecCommand(parent *cobra.Command) {
|
||||||
execCmd := &cobra.Command{
|
execCmd := &cobra.Command{
|
||||||
Use: "exec <container-id> <command> [args...]",
|
Use: "exec <container-id> <command> [args...]",
|
||||||
Short: "Execute a command in a VM",
|
Short: i18n.T("cmd.vm.exec.short"),
|
||||||
Long: "Execute a command inside a running VM via SSH.\n\n" +
|
Long: i18n.T("cmd.vm.exec.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core vm exec abc12345 ls -la\n" +
|
|
||||||
" core vm exec abc1 /bin/sh",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return fmt.Errorf("container ID and command are required")
|
return fmt.Errorf(i18n.T("cmd.vm.error.id_and_cmd_required"))
|
||||||
}
|
}
|
||||||
return execInContainer(args[0], args[1:])
|
return execInContainer(args[0], args[1:])
|
||||||
},
|
},
|
||||||
|
|
@ -350,7 +330,7 @@ func addVMExecCommand(parent *cobra.Command) {
|
||||||
func execInContainer(id string, cmd []string) error {
|
func execInContainer(id string, cmd []string) error {
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fullID, err := resolveContainerID(manager, id)
|
fullID, err := resolveContainerID(manager, id)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/container"
|
"github.com/host-uk/core/pkg/container"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,14 +18,8 @@ import (
|
||||||
func addVMTemplatesCommand(parent *cobra.Command) {
|
func addVMTemplatesCommand(parent *cobra.Command) {
|
||||||
templatesCmd := &cobra.Command{
|
templatesCmd := &cobra.Command{
|
||||||
Use: "templates",
|
Use: "templates",
|
||||||
Short: "Manage LinuxKit templates",
|
Short: i18n.T("cmd.vm.templates.short"),
|
||||||
Long: "Manage LinuxKit YAML templates for building VMs.\n\n" +
|
Long: i18n.T("cmd.vm.templates.long"),
|
||||||
"Templates provide pre-configured LinuxKit configurations for common use cases.\n" +
|
|
||||||
"They support variable substitution with ${VAR} and ${VAR:-default} syntax.\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core vm templates # List available templates\n" +
|
|
||||||
" core vm templates show core-dev # Show template content\n" +
|
|
||||||
" core vm templates vars server-php # Show template variables",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return listTemplates()
|
return listTemplates()
|
||||||
},
|
},
|
||||||
|
|
@ -41,14 +36,11 @@ func addVMTemplatesCommand(parent *cobra.Command) {
|
||||||
func addTemplatesShowCommand(parent *cobra.Command) {
|
func addTemplatesShowCommand(parent *cobra.Command) {
|
||||||
showCmd := &cobra.Command{
|
showCmd := &cobra.Command{
|
||||||
Use: "show <template-name>",
|
Use: "show <template-name>",
|
||||||
Short: "Display template content",
|
Short: i18n.T("cmd.vm.templates.show.short"),
|
||||||
Long: "Display the content of a LinuxKit template.\n\n" +
|
Long: i18n.T("cmd.vm.templates.show.long"),
|
||||||
"Examples:\n" +
|
|
||||||
" core templates show core-dev\n" +
|
|
||||||
" core templates show server-php",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("template name is required")
|
return fmt.Errorf(i18n.T("cmd.vm.error.template_required"))
|
||||||
}
|
}
|
||||||
return showTemplate(args[0])
|
return showTemplate(args[0])
|
||||||
},
|
},
|
||||||
|
|
@ -61,15 +53,11 @@ func addTemplatesShowCommand(parent *cobra.Command) {
|
||||||
func addTemplatesVarsCommand(parent *cobra.Command) {
|
func addTemplatesVarsCommand(parent *cobra.Command) {
|
||||||
varsCmd := &cobra.Command{
|
varsCmd := &cobra.Command{
|
||||||
Use: "vars <template-name>",
|
Use: "vars <template-name>",
|
||||||
Short: "Show template variables",
|
Short: i18n.T("cmd.vm.templates.vars.short"),
|
||||||
Long: "Display all variables used in a template.\n\n" +
|
Long: i18n.T("cmd.vm.templates.vars.long"),
|
||||||
"Shows required variables (no default) and optional variables (with defaults).\n\n" +
|
|
||||||
"Examples:\n" +
|
|
||||||
" core templates vars core-dev\n" +
|
|
||||||
" core templates vars server-php",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("template name is required")
|
return fmt.Errorf(i18n.T("cmd.vm.error.template_required"))
|
||||||
}
|
}
|
||||||
return showTemplateVars(args[0])
|
return showTemplateVars(args[0])
|
||||||
},
|
},
|
||||||
|
|
@ -82,14 +70,14 @@ func listTemplates() error {
|
||||||
templates := container.ListTemplates()
|
templates := container.ListTemplates()
|
||||||
|
|
||||||
if len(templates) == 0 {
|
if len(templates) == 0 {
|
||||||
fmt.Println("No templates available.")
|
fmt.Println(i18n.T("cmd.vm.templates.no_templates"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n\n", repoNameStyle.Render("Available LinuxKit Templates"))
|
fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.vm.templates.title")))
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
fmt.Fprintln(w, "NAME\tDESCRIPTION")
|
fmt.Fprintln(w, i18n.T("cmd.vm.templates.header"))
|
||||||
fmt.Fprintln(w, "----\t-----------")
|
fmt.Fprintln(w, "----\t-----------")
|
||||||
|
|
||||||
for _, tmpl := range templates {
|
for _, tmpl := range templates {
|
||||||
|
|
@ -102,9 +90,9 @@ func listTemplates() error {
|
||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Show template: %s\n", dimStyle.Render("core vm templates show <name>"))
|
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.show"), dimStyle.Render("core vm templates show <name>"))
|
||||||
fmt.Printf("Show variables: %s\n", dimStyle.Render("core vm templates vars <name>"))
|
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars <name>"))
|
||||||
fmt.Printf("Run from template: %s\n", dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
|
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +103,7 @@ func showTemplate(name string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Template:"), repoNameStyle.Render(name))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(name))
|
||||||
fmt.Println(content)
|
fmt.Println(content)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -129,10 +117,10 @@ func showTemplateVars(name string) error {
|
||||||
|
|
||||||
required, optional := container.ExtractVariables(content)
|
required, optional := container.ExtractVariables(content)
|
||||||
|
|
||||||
fmt.Printf("%s %s\n\n", dimStyle.Render("Template:"), repoNameStyle.Render(name))
|
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(name))
|
||||||
|
|
||||||
if len(required) > 0 {
|
if len(required) > 0 {
|
||||||
fmt.Printf("%s\n", errorStyle.Render("Required Variables (no default):"))
|
fmt.Printf("%s\n", errorStyle.Render(i18n.T("cmd.vm.templates.vars.required")))
|
||||||
for _, v := range required {
|
for _, v := range required {
|
||||||
fmt.Printf(" %s\n", varStyle.Render("${"+v+"}"))
|
fmt.Printf(" %s\n", varStyle.Render("${"+v+"}"))
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +128,7 @@ func showTemplateVars(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(optional) > 0 {
|
if len(optional) > 0 {
|
||||||
fmt.Printf("%s\n", successStyle.Render("Optional Variables (with defaults):"))
|
fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.templates.vars.optional")))
|
||||||
for v, def := range optional {
|
for v, def := range optional {
|
||||||
fmt.Printf(" %s = %s\n",
|
fmt.Printf(" %s = %s\n",
|
||||||
varStyle.Render("${"+v+"}"),
|
varStyle.Render("${"+v+"}"),
|
||||||
|
|
@ -150,7 +138,7 @@ func showTemplateVars(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(required) == 0 && len(optional) == 0 {
|
if len(required) == 0 && len(optional) == 0 {
|
||||||
fmt.Println("No variables in this template.")
|
fmt.Println(i18n.T("cmd.vm.templates.vars.none"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -161,63 +149,63 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
|
||||||
// Apply template with variables
|
// Apply template with variables
|
||||||
content, err := container.ApplyTemplate(templateName, vars)
|
content, err := container.ApplyTemplate(templateName, vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to apply template: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.apply_template")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary directory for the build
|
// Create a temporary directory for the build
|
||||||
tmpDir, err := os.MkdirTemp("", "core-linuxkit-*")
|
tmpDir, err := os.MkdirTemp("", "core-linuxkit-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.create_temp")+": %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
// Write the YAML file
|
// Write the YAML file
|
||||||
yamlPath := filepath.Join(tmpDir, templateName+".yml")
|
yamlPath := filepath.Join(tmpDir, templateName+".yml")
|
||||||
if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(yamlPath, []byte(content), 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write template: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.write_template")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Template:"), repoNameStyle.Render(templateName))
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.template")), repoNameStyle.Render(templateName))
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Building:"), yamlPath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath)
|
||||||
|
|
||||||
// Build the image using linuxkit
|
// Build the image using linuxkit
|
||||||
outputPath := filepath.Join(tmpDir, templateName)
|
outputPath := filepath.Join(tmpDir, templateName)
|
||||||
if err := buildLinuxKitImage(yamlPath, outputPath); err != nil {
|
if err := buildLinuxKitImage(yamlPath, outputPath); err != nil {
|
||||||
return fmt.Errorf("failed to build image: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.build_image")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the built image (linuxkit creates .iso or other format)
|
// Find the built image (linuxkit creates .iso or other format)
|
||||||
imagePath := findBuiltImage(outputPath)
|
imagePath := findBuiltImage(outputPath)
|
||||||
if imagePath == "" {
|
if imagePath == "" {
|
||||||
return fmt.Errorf("no image found after build")
|
return fmt.Errorf(i18n.T("cmd.vm.error.no_image_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), imagePath)
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.image")), imagePath)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
// Run the image
|
// Run the image
|
||||||
manager, err := container.NewLinuxKitManager()
|
manager, err := container.NewLinuxKitManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize container manager: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.init_manager")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s\n", dimStyle.Render("Hypervisor:"), manager.Hypervisor().Name())
|
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
c, err := manager.Run(ctx, imagePath, runOpts)
|
c, err := manager.Run(ctx, imagePath, runOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to run container: %w", err)
|
return fmt.Errorf(i18n.T("cmd.vm.error.run_container")+": %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if runOpts.Detach {
|
if runOpts.Detach {
|
||||||
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID)
|
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("cmd.vm.label.started")), c.ID)
|
||||||
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID)
|
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8])
|
fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]interface{}{"ID": c.ID[:8]}))
|
||||||
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8])
|
fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]interface{}{"ID": c.ID[:8]}))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n%s %s\n", dimStyle.Render("Container stopped:"), c.ID)
|
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -298,7 +286,7 @@ func lookupLinuxKit() (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("linuxkit not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit")
|
return "", fmt.Errorf(i18n.T("cmd.vm.error.linuxkit_not_found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseVarFlags parses --var flags into a map.
|
// ParseVarFlags parses --var flags into a map.
|
||||||
|
|
|
||||||
14
cmd/vm/vm.go
14
cmd/vm/vm.go
|
|
@ -4,6 +4,7 @@ package vm
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/host-uk/core/cmd/shared"
|
"github.com/host-uk/core/cmd/shared"
|
||||||
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,17 +26,8 @@ var (
|
||||||
func AddVMCommands(root *cobra.Command) {
|
func AddVMCommands(root *cobra.Command) {
|
||||||
vmCmd := &cobra.Command{
|
vmCmd := &cobra.Command{
|
||||||
Use: "vm",
|
Use: "vm",
|
||||||
Short: "LinuxKit VM management",
|
Short: i18n.T("cmd.vm.short"),
|
||||||
Long: "Manage LinuxKit virtual machines.\n\n" +
|
Long: i18n.T("cmd.vm.long"),
|
||||||
"LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" +
|
|
||||||
"They run using qemu or hyperkit depending on your system.\n\n" +
|
|
||||||
"Commands:\n" +
|
|
||||||
" run Run a VM from image or template\n" +
|
|
||||||
" ps List running VMs\n" +
|
|
||||||
" stop Stop a running VM\n" +
|
|
||||||
" logs View VM logs\n" +
|
|
||||||
" exec Execute command in VM\n" +
|
|
||||||
" templates Manage LinuxKit templates",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.AddCommand(vmCmd)
|
root.AddCommand(vmCmd)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue