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
This commit is contained in:
Snider 2026-02-01 02:07:26 +00:00
parent 699c0933f6
commit edace890f3
18 changed files with 346 additions and 337 deletions

View file

@ -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"
)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
}

68
pkg/dev/registry.go Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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 ~

View file

@ -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)

View file

@ -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
}

View file

@ -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")
}

View file

@ -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)
}

7
pkg/workspace/cmd.go Normal file
View file

@ -0,0 +1,7 @@
package workspace
import "github.com/host-uk/core/pkg/cli"
func init() {
cli.RegisterCommands(AddWorkspaceCommands)
}

View file

@ -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
}

95
pkg/workspace/config.go Normal file
View file

@ -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")
}