From 75d4057fe007c93887c493eaef90132339967f85 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 1 Feb 2026 02:07:26 +0000 Subject: [PATCH] feat(workspace): implement workspace.yaml support - Add pkg/workspace package with config and commands - Integrate with pkg/php/cmd.go for context switching - Refactor pkg/repos to use pkg/workspace for config - Register workspace commands in full variant --- internal/variants/full.go | 1 + pkg/dev/cmd_commit.go | 31 ++--------- pkg/dev/cmd_health.go | 30 ++--------- pkg/dev/cmd_issues.go | 30 ++--------- pkg/dev/cmd_pull.go | 33 ++---------- pkg/dev/cmd_push.go | 29 ++--------- pkg/dev/cmd_reviews.go | 30 ++--------- pkg/dev/cmd_work.go | 57 ++++++-------------- pkg/dev/registry.go | 68 ++++++++++++++++++++++++ pkg/dev/service.go | 31 ++--------- pkg/docs/cmd_scan.go | 5 +- pkg/php/cmd.go | 48 ++++++++++++++++- pkg/repos/workspace.go | 47 ----------------- pkg/repos/workspace_test.go | 56 -------------------- pkg/setup/cmd_registry.go | 3 +- pkg/workspace/cmd.go | 7 +++ pkg/workspace/cmd_workspace.go | 82 +++++++++++++++++++++++++++++ pkg/workspace/config.go | 95 ++++++++++++++++++++++++++++++++++ 18 files changed, 346 insertions(+), 337 deletions(-) create mode 100644 pkg/dev/registry.go delete mode 100644 pkg/repos/workspace.go delete mode 100644 pkg/repos/workspace_test.go create mode 100644 pkg/workspace/cmd.go create mode 100644 pkg/workspace/cmd_workspace.go create mode 100644 pkg/workspace/config.go diff --git a/internal/variants/full.go b/internal/variants/full.go index e94a9277..e456fca9 100644 --- a/internal/variants/full.go +++ b/internal/variants/full.go @@ -36,4 +36,5 @@ import ( _ "github.com/host-uk/core/pkg/setup" _ "github.com/host-uk/core/pkg/test" _ "github.com/host-uk/core/pkg/vm" + _ "github.com/host-uk/core/pkg/workspace" ) diff --git a/pkg/dev/cmd_commit.go b/pkg/dev/cmd_commit.go index 3eae0449..55fad3f1 100644 --- a/pkg/dev/cmd_commit.go +++ b/pkg/dev/cmd_commit.go @@ -8,7 +8,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Commit command flags @@ -44,33 +43,11 @@ func runCommit(registryPath string, all bool) error { } // Multi-repo mode: find or use provided registry - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - // Fallback: scan current directory for repos - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) - registryPath = cwd - } + reg, regDir, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } + registryPath = regDir // Use resolved registry directory for relative paths // Build paths and names for git operations var paths []string diff --git a/pkg/dev/cmd_health.go b/pkg/dev/cmd_health.go index 018d6a5e..f1ed3602 100644 --- a/pkg/dev/cmd_health.go +++ b/pkg/dev/cmd_health.go @@ -3,14 +3,12 @@ package dev import ( "context" "fmt" - "os" "sort" "strings" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Health command flags @@ -39,30 +37,10 @@ func addHealthCommand(parent *cli.Command) { func runHealth(registryPath string, verbose bool) error { ctx := context.Background() - // Find or use provided registry, fall back to directory scan - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - } + // Load registry and get paths + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } // Build paths and names for git operations diff --git a/pkg/dev/cmd_issues.go b/pkg/dev/cmd_issues.go index 62a871f3..834a7b5b 100644 --- a/pkg/dev/cmd_issues.go +++ b/pkg/dev/cmd_issues.go @@ -3,7 +3,6 @@ package dev import ( "encoding/json" "errors" - "os" "os/exec" "sort" "strings" @@ -11,7 +10,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Issue-specific styles (aliases to shared) @@ -84,30 +82,10 @@ func runIssues(registryPath string, limit int, assignee string) error { return errors.New(i18n.T("error.gh_not_found")) } - // Find or use provided registry, fall back to directory scan - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - } + // Find or use provided registry + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } // Fetch issues sequentially (avoid GitHub rate limits) diff --git a/pkg/dev/cmd_pull.go b/pkg/dev/cmd_pull.go index af003fcf..1b29b7ff 100644 --- a/pkg/dev/cmd_pull.go +++ b/pkg/dev/cmd_pull.go @@ -2,13 +2,11 @@ package dev import ( "context" - "os" "os/exec" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Pull command flags @@ -37,33 +35,10 @@ func addPullCommand(parent *cli.Command) { func runPull(registryPath string, all bool) error { ctx := context.Background() - // Find or use provided registry, fall back to directory scan - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) - } + // Find or use provided registry + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } // Build paths and names for git operations diff --git a/pkg/dev/cmd_push.go b/pkg/dev/cmd_push.go index 11b753d0..173ed383 100644 --- a/pkg/dev/cmd_push.go +++ b/pkg/dev/cmd_push.go @@ -8,7 +8,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Push command flags @@ -44,31 +43,9 @@ func runPush(registryPath string, force bool) error { } // Multi-repo mode: find or use provided registry - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - // Fallback: scan current directory for repos - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - cli.Print("%s %s\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) - } + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } // Build paths and names for git operations diff --git a/pkg/dev/cmd_reviews.go b/pkg/dev/cmd_reviews.go index d37934a9..3289c9a1 100644 --- a/pkg/dev/cmd_reviews.go +++ b/pkg/dev/cmd_reviews.go @@ -3,7 +3,6 @@ package dev import ( "encoding/json" "errors" - "os" "os/exec" "sort" "strings" @@ -11,7 +10,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // PR-specific styles (aliases to shared) @@ -81,30 +79,10 @@ func runReviews(registryPath string, author string, showAll bool) error { return errors.New(i18n.T("error.gh_not_found")) } - // Find or use provided registry, fall back to directory scan - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return cli.Wrap(err, "failed to load registry") - } - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return cli.Wrap(err, "failed to scan directory") - } - } + // Find or use provided registry + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return err } // Fetch PRs sequentially (avoid GitHub rate limits) diff --git a/pkg/dev/cmd_work.go b/pkg/dev/cmd_work.go index 2d6ed332..07d98d59 100644 --- a/pkg/dev/cmd_work.go +++ b/pkg/dev/cmd_work.go @@ -11,7 +11,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" - "github.com/host-uk/core/pkg/repos" ) // Work command flags @@ -57,7 +56,21 @@ func runWork(registryPath string, statusOnly, autoCommit bool) error { defer bundle.Stop(ctx) // Load registry and get paths - paths, names, err := loadRegistry(registryPath) + paths, names, err := func() ([]string, map[string]string, error) { + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return nil, nil, err + } + var paths []string + names := make(map[string]string) + for _, repo := range reg.List() { + if repo.IsGitRepo() { + paths = append(paths, repo.Path) + names[repo.Path] = repo.Name + } + } + return paths, names, nil + }() if err != nil { return err } @@ -330,44 +343,4 @@ func claudeEditCommit(ctx context.Context, repoPath, repoName, registryPath stri return cmd.Run() } -func loadRegistry(registryPath string) ([]string, map[string]string, error) { - var reg *repos.Registry - var err error - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to load registry") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to scan directory") - } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) - } - } - - var paths []string - names := make(map[string]string) - - for _, repo := range reg.List() { - if repo.IsGitRepo() { - paths = append(paths, repo.Path) - names[repo.Path] = repo.Name - } - } - - return paths, names, nil -} diff --git a/pkg/dev/registry.go b/pkg/dev/registry.go new file mode 100644 index 00000000..f79bb738 --- /dev/null +++ b/pkg/dev/registry.go @@ -0,0 +1,68 @@ +package dev + +import ( + "os" + "path/filepath" + "strings" + + "github.com/host-uk/core/pkg/cli" + "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/repos" + "github.com/host-uk/core/pkg/workspace" +) + +// loadRegistryWithConfig loads the registry and applies workspace configuration. +func loadRegistryWithConfig(registryPath string) (*repos.Registry, string, error) { + var reg *repos.Registry + var err error + var registryDir string + + if registryPath != "" { + reg, err = repos.LoadRegistry(registryPath) + if err != nil { + return nil, "", cli.Wrap(err, "failed to load registry") + } + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) + registryDir = filepath.Dir(registryPath) + } else { + registryPath, err = repos.FindRegistry() + if err == nil { + reg, err = repos.LoadRegistry(registryPath) + if err != nil { + return nil, "", cli.Wrap(err, "failed to load registry") + } + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("registry")), registryPath) + registryDir = filepath.Dir(registryPath) + } else { + // Fallback: scan current directory + cwd, _ := os.Getwd() + reg, err = repos.ScanDirectory(cwd) + if err != nil { + return nil, "", cli.Wrap(err, "failed to scan directory") + } + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.dev.scanning_label")), cwd) + registryDir = cwd + } + } + // Load workspace config to respect packages_dir + if wsConfig, err := workspace.LoadConfig(registryDir); err == nil { + if wsConfig.PackagesDir != "" { + pkgDir := wsConfig.PackagesDir + // Expand ~ + if strings.HasPrefix(pkgDir, "~/") { + home, _ := os.UserHomeDir() + pkgDir = filepath.Join(home, pkgDir[2:]) + } + if !filepath.IsAbs(pkgDir) { + pkgDir = filepath.Join(registryDir, pkgDir) + } + + // Update repo paths + for _, repo := range reg.Repos { + repo.Path = filepath.Join(pkgDir, repo.Name) + } + } + } + + return reg, registryDir, nil +} diff --git a/pkg/dev/service.go b/pkg/dev/service.go index 54c86f16..a145cd90 100644 --- a/pkg/dev/service.go +++ b/pkg/dev/service.go @@ -2,7 +2,6 @@ package dev import ( "context" - "os" "sort" "strings" @@ -10,7 +9,6 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/framework" "github.com/host-uk/core/pkg/git" - "github.com/host-uk/core/pkg/repos" ) // Tasks for dev service @@ -242,32 +240,9 @@ func (s *Service) runStatus(task TaskStatus) error { } func (s *Service) loadRegistry(registryPath string) ([]string, map[string]string, error) { - var reg *repos.Registry - var err error - - if registryPath != "" { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to load registry") - } - cli.Print("Registry: %s\n\n", registryPath) - } else { - registryPath, err = repos.FindRegistry() - if err == nil { - reg, err = repos.LoadRegistry(registryPath) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to load registry") - } - cli.Print("Registry: %s\n\n", registryPath) - } else { - // Fallback: scan current directory - cwd, _ := os.Getwd() - reg, err = repos.ScanDirectory(cwd) - if err != nil { - return nil, nil, cli.Wrap(err, "failed to scan directory") - } - cli.Print("Scanning: %s\n\n", cwd) - } + reg, _, err := loadRegistryWithConfig(registryPath) + if err != nil { + return nil, nil, err } var paths []string diff --git a/pkg/docs/cmd_scan.go b/pkg/docs/cmd_scan.go index f2fc5284..105a133b 100644 --- a/pkg/docs/cmd_scan.go +++ b/pkg/docs/cmd_scan.go @@ -9,6 +9,7 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" + "github.com/host-uk/core/pkg/workspace" ) // RepoDocInfo holds documentation info for a repo @@ -52,14 +53,14 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) { } // Load workspace config to respect packages_dir - wsConfig, err := repos.LoadWorkspaceConfig(registryDir) + wsConfig, err := workspace.LoadConfig(registryDir) if err != nil { return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "workspace config")) } basePath := registryDir - if wsConfig.PackagesDir != "" { + if wsConfig.PackagesDir != "" && wsConfig.PackagesDir != "./packages" { pkgDir := wsConfig.PackagesDir // Expand ~ diff --git a/pkg/php/cmd.go b/pkg/php/cmd.go index a583c662..02531189 100644 --- a/pkg/php/cmd.go +++ b/pkg/php/cmd.go @@ -1,9 +1,12 @@ -// Package php provides Laravel/PHP development commands. package php import ( + "os" + "path/filepath" + "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/workspace" "github.com/spf13/cobra" ) @@ -57,9 +60,52 @@ func AddPHPCommands(root *cobra.Command) { Use: "php", Short: i18n.T("cmd.php.short"), Long: i18n.T("cmd.php.long"), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Check if we are in a workspace root + wsRoot, err := workspace.FindWorkspaceRoot() + if err != nil { + return nil // Not in a workspace, regular behavior + } + + // Load workspace config + config, err := workspace.LoadConfig(wsRoot) + if err != nil { + return nil // Failed to load, ignore + } + + if config.Active == "" { + return nil // No active package + } + + // Calculate package path + pkgDir := config.PackagesDir + if pkgDir == "" { + pkgDir = "./packages" + } + if !filepath.IsAbs(pkgDir) { + pkgDir = filepath.Join(wsRoot, pkgDir) + } + + targetDir := filepath.Join(pkgDir, config.Active) + + // Check if target directory exists + if _, err := os.Stat(targetDir); err != nil { + cli.Warnf("Active package directory not found: %s", targetDir) + return nil + } + + // Change working directory + if err := os.Chdir(targetDir); err != nil { + return cli.Err("failed to change directory to active package: %w", err) + } + + cli.Print("%s %s\n", dimStyle.Render("Workspace:"), config.Active) + return nil + }, } root.AddCommand(phpCmd) + // Development addPHPDevCommand(phpCmd) addPHPLogsCommand(phpCmd) diff --git a/pkg/repos/workspace.go b/pkg/repos/workspace.go deleted file mode 100644 index 5f7233c9..00000000 --- a/pkg/repos/workspace.go +++ /dev/null @@ -1,47 +0,0 @@ -package repos - -import ( - "fmt" - "os" - "path/filepath" - - "gopkg.in/yaml.v3" -) - -// WorkspaceConfig holds workspace-level configuration. -type WorkspaceConfig struct { - Version int `yaml:"version"` - Active string `yaml:"active"` - PackagesDir string `yaml:"packages_dir"` -} - -// DefaultWorkspaceConfig returns a config with default values. -func DefaultWorkspaceConfig() *WorkspaceConfig { - return &WorkspaceConfig{ - Version: 1, - PackagesDir: "./packages", - } -} - -// LoadWorkspaceConfig tries to load workspace.yaml from the given directory's .core subfolder. -func LoadWorkspaceConfig(dir string) (*WorkspaceConfig, error) { - path := filepath.Join(dir, ".core", "workspace.yaml") - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return DefaultWorkspaceConfig(), nil - } - return nil, fmt.Errorf("failed to read workspace config: %w", err) - } - - config := DefaultWorkspaceConfig() - if err := yaml.Unmarshal(data, config); err != nil { - return nil, fmt.Errorf("failed to parse workspace config: %w", err) - } - - if config.Version != 1 { - return nil, fmt.Errorf("unsupported workspace config version: %d", config.Version) - } - - return config, nil -} diff --git a/pkg/repos/workspace_test.go b/pkg/repos/workspace_test.go deleted file mode 100644 index fa3c1055..00000000 --- a/pkg/repos/workspace_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package repos - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadWorkspaceConfig_Good(t *testing.T) { - // Setup temp dir - tmpDir := t.TempDir() - coreDir := filepath.Join(tmpDir, ".core") - err := os.MkdirAll(coreDir, 0755) - assert.NoError(t, err) - - // Write valid config - configContent := ` -version: 1 -active: core-php -packages_dir: ./custom-packages -` - err = os.WriteFile(filepath.Join(coreDir, "workspace.yaml"), []byte(configContent), 0644) - assert.NoError(t, err) - - // Load - cfg, err := LoadWorkspaceConfig(tmpDir) - assert.NoError(t, err) - assert.Equal(t, 1, cfg.Version) - assert.Equal(t, "core-php", cfg.Active) - assert.Equal(t, "./custom-packages", cfg.PackagesDir) -} - -func TestLoadWorkspaceConfig_Default(t *testing.T) { - tmpDir := t.TempDir() - - // Load non-existent - cfg, err := LoadWorkspaceConfig(tmpDir) - assert.NoError(t, err) - assert.Equal(t, 1, cfg.Version) - assert.Equal(t, "./packages", cfg.PackagesDir) -} - -func TestLoadWorkspaceConfig_BadVersion(t *testing.T) { - tmpDir := t.TempDir() - coreDir := filepath.Join(tmpDir, ".core") - os.MkdirAll(coreDir, 0755) - - configContent := `version: 2` - os.WriteFile(filepath.Join(coreDir, "workspace.yaml"), []byte(configContent), 0644) - - _, err := LoadWorkspaceConfig(tmpDir) - assert.Error(t, err) - assert.Contains(t, err.Error(), "unsupported workspace config version") -} diff --git a/pkg/setup/cmd_registry.go b/pkg/setup/cmd_registry.go index d1008789..a38a29d2 100644 --- a/pkg/setup/cmd_registry.go +++ b/pkg/setup/cmd_registry.go @@ -16,6 +16,7 @@ import ( "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/host-uk/core/pkg/repos" + "github.com/host-uk/core/pkg/workspace" ) // runRegistrySetup loads a registry from path and runs setup. @@ -39,7 +40,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP basePath := reg.BasePath if basePath == "" { // Load workspace config to see if packages_dir is set - wsConfig, err := repos.LoadWorkspaceConfig(registryDir) + wsConfig, err := workspace.LoadConfig(registryDir) if err != nil { return fmt.Errorf("failed to load workspace config: %w", err) } diff --git a/pkg/workspace/cmd.go b/pkg/workspace/cmd.go new file mode 100644 index 00000000..8c45ff09 --- /dev/null +++ b/pkg/workspace/cmd.go @@ -0,0 +1,7 @@ +package workspace + +import "github.com/host-uk/core/pkg/cli" + +func init() { + cli.RegisterCommands(AddWorkspaceCommands) +} diff --git a/pkg/workspace/cmd_workspace.go b/pkg/workspace/cmd_workspace.go new file mode 100644 index 00000000..c538ba10 --- /dev/null +++ b/pkg/workspace/cmd_workspace.go @@ -0,0 +1,82 @@ +package workspace + +import ( + "fmt" + "strings" + + "github.com/host-uk/core/pkg/cli" + "github.com/spf13/cobra" +) + +func AddWorkspaceCommands(root *cobra.Command) { + wsCmd := &cobra.Command{ + Use: "workspace", + Short: "Manage workspace configuration", + RunE: runWorkspaceInfo, + } + + wsCmd.AddCommand(&cobra.Command{ + Use: "active [package]", + Short: "Show or set the active package", + RunE: runWorkspaceActive, + }) + + root.AddCommand(wsCmd) +} + +func runWorkspaceInfo(cmd *cobra.Command, args []string) error { + root, err := FindWorkspaceRoot() + if err != nil { + return cli.Err("not in a workspace") + } + + config, err := LoadConfig(root) + if err != nil { + return err + } + + cli.Print("Active: %s\n", cli.ValueStyle.Render(config.Active)) + cli.Print("Packages: %s\n", cli.DimStyle.Render(config.PackagesDir)) + if len(config.DefaultOnly) > 0 { + cli.Print("Types: %s\n", cli.DimStyle.Render(strings.Join(config.DefaultOnly, ", "))) + } + + return nil +} + +func runWorkspaceActive(cmd *cobra.Command, args []string) error { + root, err := FindWorkspaceRoot() + if err != nil { + return cli.Err("not in a workspace") + } + + config, err := LoadConfig(root) + if err != nil { + return err + } + + // If no args, show active + if len(args) == 0 { + if config.Active == "" { + cli.Println("No active package set") + return nil + } + cli.Println(config.Active) + return nil + } + + // Set active + target := args[0] + if target == config.Active { + cli.Print("Active package is already %s\n", cli.ValueStyle.Render(target)) + return nil + } + + config.Active = target + if err := SaveConfig(root, config); err != nil { + return err + } + + cli.Print("Active package set to %s\n", cli.SuccessStyle.Render(target)) + return nil +} diff --git a/pkg/workspace/config.go b/pkg/workspace/config.go new file mode 100644 index 00000000..d68222ea --- /dev/null +++ b/pkg/workspace/config.go @@ -0,0 +1,95 @@ +package workspace + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +// WorkspaceConfig holds workspace-level configuration from .core/workspace.yaml. +type WorkspaceConfig struct { + Version int `yaml:"version"` + Active string `yaml:"active"` // Active package name + DefaultOnly []string `yaml:"default_only"` // Default types for setup + PackagesDir string `yaml:"packages_dir"` // Where packages are cloned +} + +// DefaultConfig returns a config with default values. +func DefaultConfig() *WorkspaceConfig { + return &WorkspaceConfig{ + Version: 1, + PackagesDir: "./packages", + } +} + +// LoadConfig tries to load workspace.yaml from the given directory's .core subfolder. +func LoadConfig(dir string) (*WorkspaceConfig, error) { + path := filepath.Join(dir, ".core", "workspace.yaml") + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + // Try parent directory + parent := filepath.Dir(dir) + if parent != dir { + return LoadConfig(parent) + } + return DefaultConfig(), nil + } + return nil, fmt.Errorf("failed to read workspace config: %w", err) + } + + config := DefaultConfig() + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse workspace config: %w", err) + } + + if config.Version != 1 { + return nil, fmt.Errorf("unsupported workspace config version: %d", config.Version) + } + + return config, nil +} + +// SaveConfig saves the configuration to the given directory's .core/workspace.yaml. +func SaveConfig(dir string, config *WorkspaceConfig) error { + coreDir := filepath.Join(dir, ".core") + if err := os.MkdirAll(coreDir, 0755); err != nil { + return fmt.Errorf("failed to create .core directory: %w", err) + } + + path := filepath.Join(coreDir, "workspace.yaml") + data, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal workspace config: %w", err) + } + + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("failed to write workspace config: %w", err) + } + + return nil +} + +// FindWorkspaceRoot searches for the root directory containing .core/workspace.yaml. +func FindWorkspaceRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(dir, ".core", "workspace.yaml")); err == nil { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + + return "", fmt.Errorf("not in a workspace") +}