fix(i18n): restore missing translation keys for health command (#65)

* fix(i18n): restore missing translation keys for health command

The locale consolidation in 39de3c2 removed keys still used by
cmd_health.go. Added back:
- cmd.dev.health.* keys (long, repos, to_push, to_pull, etc.)
- common.status.* keys (dirty, clean, synced, up_to_date)
- common.flag.registry

Also fixed workspace.LoadConfig() returning default PackagesDir
when no .core/workspace.yaml exists, which was overriding repo
paths from repos.yaml.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add nil checks for workspace.LoadConfig callers

LoadConfig now returns nil when no .core/workspace.yaml exists.
Added defensive nil checks to all callers to prevent panics.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: align workspace.LoadConfig error handling

Both call sites now gracefully ignore errors and fall back to defaults,
since workspace config is optional for setup commands.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-01 03:55:01 +00:00 committed by GitHub
parent e813c1f07e
commit a0088a34a8
8 changed files with 55 additions and 14 deletions

View file

@ -44,8 +44,8 @@ func loadRegistryWithConfig(registryPath string) (*repos.Registry, string, error
registryDir = cwd registryDir = cwd
} }
} }
// Load workspace config to respect packages_dir // Load workspace config to respect packages_dir (only if config exists)
if wsConfig, err := workspace.LoadConfig(registryDir); err == nil { if wsConfig, err := workspace.LoadConfig(registryDir); err == nil && wsConfig != nil {
if wsConfig.PackagesDir != "" { if wsConfig.PackagesDir != "" {
pkgDir := wsConfig.PackagesDir pkgDir := wsConfig.PackagesDir
// Expand ~ // Expand ~

View file

@ -60,7 +60,7 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
basePath := registryDir basePath := registryDir
if wsConfig.PackagesDir != "" && wsConfig.PackagesDir != "./packages" { if wsConfig != nil && wsConfig.PackagesDir != "" && wsConfig.PackagesDir != "./packages" {
pkgDir := wsConfig.PackagesDir pkgDir := wsConfig.PackagesDir
// Expand ~ // Expand ~

View file

@ -157,6 +157,18 @@
"no_git_repos": "No git repositories found.", "no_git_repos": "No git repositories found.",
"confirm_claude_commit": "Have Claude commit these repos?", "confirm_claude_commit": "Have Claude commit these repos?",
"health.short": "Quick health check across all repos", "health.short": "Quick health check across all repos",
"health.long": "Shows a summary of repository health across all repos in the workspace.",
"health.flag.verbose": "Show detailed breakdown",
"health.repos": "repos",
"health.to_push": "to push",
"health.to_pull": "to pull",
"health.errors": "errors",
"health.more": "+{{.Count}} more",
"health.dirty_label": "Dirty:",
"health.ahead_label": "Ahead:",
"health.behind_label": "Behind:",
"health.errors_label": "Errors:",
"status.clean": "clean",
"commit.short": "Claude-assisted commits across repos", "commit.short": "Claude-assisted commits across repos",
"push.short": "Push commits across all repos", "push.short": "Push commits across all repos",
"push.diverged": "branch has diverged from remote", "push.diverged": "branch has diverged from remote",
@ -293,6 +305,12 @@
} }
}, },
"common": { "common": {
"status": {
"dirty": "dirty",
"clean": "clean",
"synced": "synced",
"up_to_date": "up to date"
},
"label": { "label": {
"done": "Done", "done": "Done",
"error": "Error", "error": "Error",
@ -309,7 +327,8 @@
"fix": "Auto-fix issues where possible", "fix": "Auto-fix issues where possible",
"diff": "Show diff of changes", "diff": "Show diff of changes",
"json": "Output as JSON", "json": "Output as JSON",
"verbose": "Show detailed output" "verbose": "Show detailed output",
"registry": "Path to repos.yaml registry file"
}, },
"progress": { "progress": {
"running": "Running {{.Task}}...", "running": "Running {{.Task}}...",

View file

@ -69,8 +69,8 @@ func AddPHPCommands(root *cobra.Command) {
// Load workspace config // Load workspace config
config, err := workspace.LoadConfig(wsRoot) config, err := workspace.LoadConfig(wsRoot)
if err != nil { if err != nil || config == nil {
return nil // Failed to load, ignore return nil // Failed to load or no config, ignore
} }
if config.Active == "" { if config.Active == "" {

View file

@ -11,9 +11,11 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/i18n"
"github.com/host-uk/core/pkg/repos" "github.com/host-uk/core/pkg/repos"
"github.com/host-uk/core/pkg/workspace"
) )
// runSetupOrchestrator decides between registry mode and bootstrap mode. // runSetupOrchestrator decides between registry mode and bootstrap mode.
@ -133,6 +135,13 @@ func runBootstrap(ctx context.Context, only string, dryRun, all bool, projectNam
// Override base path to target directory // Override base path to target directory
reg.BasePath = targetDir reg.BasePath = targetDir
// Check workspace config for default_only if no filter specified
if only == "" {
if wsConfig, err := workspace.LoadConfig(devopsPath); err == nil && wsConfig != nil && len(wsConfig.DefaultOnly) > 0 {
only = strings.Join(wsConfig.DefaultOnly, ",")
}
}
// Now run the regular setup with the loaded registry // Now run the regular setup with the loaded registry
return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild) return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild)
} }

View file

@ -26,6 +26,14 @@ func runRegistrySetup(ctx context.Context, registryPath, only string, dryRun, al
return fmt.Errorf("failed to load registry: %w", err) return fmt.Errorf("failed to load registry: %w", err)
} }
// Check workspace config for default_only if no filter specified
if only == "" {
registryDir := filepath.Dir(registryPath)
if wsConfig, err := workspace.LoadConfig(registryDir); err == nil && wsConfig != nil && len(wsConfig.DefaultOnly) > 0 {
only = strings.Join(wsConfig.DefaultOnly, ",")
}
}
return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild) return runRegistrySetupWithReg(ctx, reg, registryPath, only, dryRun, all, runBuild)
} }
@ -39,12 +47,9 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
// Determine base path for cloning // Determine base path for cloning
basePath := reg.BasePath basePath := reg.BasePath
if basePath == "" { if basePath == "" {
// Load workspace config to see if packages_dir is set // Load workspace config to see if packages_dir is set (ignore errors, fall back to default)
wsConfig, err := workspace.LoadConfig(registryDir) wsConfig, _ := workspace.LoadConfig(registryDir)
if err != nil { if wsConfig != nil && wsConfig.PackagesDir != "" {
return fmt.Errorf("failed to load workspace config: %w", err)
}
if wsConfig.PackagesDir != "" {
basePath = wsConfig.PackagesDir basePath = wsConfig.PackagesDir
} else { } else {
basePath = "./packages" basePath = "./packages"

View file

@ -33,6 +33,9 @@ func runWorkspaceInfo(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
if config == nil {
return cli.Err("workspace config not found")
}
cli.Print("Active: %s\n", cli.ValueStyle.Render(config.Active)) cli.Print("Active: %s\n", cli.ValueStyle.Render(config.Active))
cli.Print("Packages: %s\n", cli.DimStyle.Render(config.PackagesDir)) cli.Print("Packages: %s\n", cli.DimStyle.Render(config.PackagesDir))
@ -53,6 +56,9 @@ func runWorkspaceActive(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
if config == nil {
config = DefaultConfig()
}
// If no args, show active // If no args, show active
if len(args) == 0 { if len(args) == 0 {
@ -60,7 +66,7 @@ func runWorkspaceActive(cmd *cobra.Command, args []string) error {
cli.Println("No active package set") cli.Println("No active package set")
return nil return nil
} }
cli.Println(config.Active) cli.Text(config.Active)
return nil return nil
} }

View file

@ -25,6 +25,7 @@ func DefaultConfig() *WorkspaceConfig {
} }
// LoadConfig tries to load workspace.yaml from the given directory's .core subfolder. // LoadConfig tries to load workspace.yaml from the given directory's .core subfolder.
// Returns nil if no config file exists (caller should check for nil).
func LoadConfig(dir string) (*WorkspaceConfig, error) { func LoadConfig(dir string) (*WorkspaceConfig, error) {
path := filepath.Join(dir, ".core", "workspace.yaml") path := filepath.Join(dir, ".core", "workspace.yaml")
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@ -35,7 +36,8 @@ func LoadConfig(dir string) (*WorkspaceConfig, error) {
if parent != dir { if parent != dir {
return LoadConfig(parent) return LoadConfig(parent)
} }
return DefaultConfig(), nil // No workspace.yaml found anywhere - return nil to indicate no config
return nil, nil
} }
return nil, fmt.Errorf("failed to read workspace config: %w", err) return nil, fmt.Errorf("failed to read workspace config: %w", err)
} }