fix(docs): respect workspace.yaml packages_dir setting (fixes #46) (#55)

* fix(docs): respect workspace.yaml packages_dir setting (fixes #46)

* fix(workspace): improve config loading logic (CR feedback)

- Expand ~ before resolving relative paths in cmd_registry
- Handle LoadWorkspaceConfig errors properly
- Update Repo.Path when PackagesDir overrides default
- Validate workspace config version
- Add unit tests for workspace config loading

* docs: add comments and increase test coverage (CR feedback)

- Add docstrings to exported functions in pkg/cli
- Add unit tests for Semantic Output (pkg/cli/output.go)
- Add unit tests for CheckBuilder (pkg/cli/check.go)
- Add unit tests for IPC Query/Perform (pkg/framework/core)

* fix(test): fix panics and failures in php package tests

- Fix panic in TestLookupLinuxKit_Bad by mocking paths
- Fix assertion errors in TestGetSSLDir_Bad and TestGetPackageInfo_Bad
- Fix formatting in test files

* fix(test): correct syntax in services_extended_test.go

* fix(ci): point coverage workflow to go.mod instead of go.work

* fix(ci): build CLI before running coverage

* fix(ci): run go generate for updater package in coverage workflow

* fix(github): allow dry-run publish without gh CLI authentication

Moves validation check after dry-run check so tests can verify dry-run behavior in CI environments.
This commit is contained in:
Snider 2026-02-01 01:59:27 +00:00 committed by GitHub
parent b02b57e6fb
commit 10277c6094
18 changed files with 442 additions and 46 deletions

View file

@ -15,7 +15,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: 'go.work' go-version-file: 'go.mod'
- name: Setup Task - name: Setup Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v1
@ -25,6 +25,12 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
- name: Build CLI
run: |
go generate ./pkg/updater/...
task cli:build
echo "$(pwd)/bin" >> $GITHUB_PATH
- name: Run coverage - name: Run coverage
run: task cov run: task cov

View file

@ -100,17 +100,19 @@ func (s *AnsiStyle) Render(text string) string {
return strings.Join(codes, "") + text + ansiReset return strings.Join(codes, "") + text + ansiReset
} }
// Hex color support // fgColorHex converts a hex string to an ANSI foreground color code.
func fgColorHex(hex string) string { func fgColorHex(hex string) string {
r, g, b := hexToRGB(hex) r, g, b := hexToRGB(hex)
return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b) return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b)
} }
// bgColorHex converts a hex string to an ANSI background color code.
func bgColorHex(hex string) string { func bgColorHex(hex string) string {
r, g, b := hexToRGB(hex) r, g, b := hexToRGB(hex)
return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b) return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b)
} }
// hexToRGB converts a hex string to RGB values.
func hexToRGB(hex string) (int, int, int) { func hexToRGB(hex string) (int, int, int) {
hex = strings.TrimPrefix(hex, "#") hex = strings.TrimPrefix(hex, "#")
if len(hex) != 6 { if len(hex) != 6 {
@ -120,4 +122,4 @@ func hexToRGB(hex string) (int, int, int) {
g, _ := strconv.ParseInt(hex[2:4], 16, 64) g, _ := strconv.ParseInt(hex[2:4], 16, 64)
b, _ := strconv.ParseInt(hex[4:6], 16, 64) b, _ := strconv.ParseInt(hex[4:6], 16, 64)
return int(r), int(g), int(b) return int(r), int(g), int(b)
} }

View file

@ -88,4 +88,4 @@ func (c *CheckBuilder) String() string {
// Print outputs the check result. // Print outputs the check result.
func (c *CheckBuilder) Print() { func (c *CheckBuilder) Print() {
fmt.Println(c.String()) fmt.Println(c.String())
} }

49
pkg/cli/check_test.go Normal file
View file

@ -0,0 +1,49 @@
package cli
import "testing"
func TestCheckBuilder(t *testing.T) {
UseASCII() // Deterministic output
// Pass
c := Check("foo").Pass()
got := c.String()
if got == "" {
t.Error("Empty output for Pass")
}
// Fail
c = Check("foo").Fail()
got = c.String()
if got == "" {
t.Error("Empty output for Fail")
}
// Skip
c = Check("foo").Skip()
got = c.String()
if got == "" {
t.Error("Empty output for Skip")
}
// Warn
c = Check("foo").Warn()
got = c.String()
if got == "" {
t.Error("Empty output for Warn")
}
// Duration
c = Check("foo").Pass().Duration("1s")
got = c.String()
if got == "" {
t.Error("Empty output for Duration")
}
// Message
c = Check("foo").Message("status")
got = c.String()
if got == "" {
t.Error("Empty output for Message")
}
}

View file

@ -9,16 +9,24 @@ import (
type GlyphTheme int type GlyphTheme int
const ( const (
// ThemeUnicode uses standard Unicode symbols.
ThemeUnicode GlyphTheme = iota ThemeUnicode GlyphTheme = iota
// ThemeEmoji uses Emoji symbols.
ThemeEmoji ThemeEmoji
// ThemeASCII uses ASCII fallback symbols.
ThemeASCII ThemeASCII
) )
var currentTheme = ThemeUnicode var currentTheme = ThemeUnicode
// UseUnicode switches the glyph theme to Unicode.
func UseUnicode() { currentTheme = ThemeUnicode } func UseUnicode() { currentTheme = ThemeUnicode }
func UseEmoji() { currentTheme = ThemeEmoji }
func UseASCII() { currentTheme = ThemeASCII } // UseEmoji switches the glyph theme to Emoji.
func UseEmoji() { currentTheme = ThemeEmoji }
// UseASCII switches the glyph theme to ASCII.
func UseASCII() { currentTheme = ThemeASCII }
func glyphMap() map[string]string { func glyphMap() map[string]string {
switch currentTheme { switch currentTheme {
@ -31,7 +39,7 @@ func glyphMap() map[string]string {
} }
} }
// Glyph converts a shortcode to its symbol. // Glyph converts a shortcode (e.g. ":check:") to its symbol based on the current theme.
func Glyph(code string) string { func Glyph(code string) string {
if sym, ok := glyphMap()[code]; ok { if sym, ok := glyphMap()[code]; ok {
return sym return sym
@ -78,4 +86,4 @@ func replaceGlyph(input *bytes.Buffer) string {
return Glyph(code.String()) return Glyph(code.String())
} }
} }
} }

View file

@ -6,10 +6,15 @@ import "fmt"
type Region rune type Region rune
const ( const (
// RegionHeader is the top region of the layout.
RegionHeader Region = 'H' RegionHeader Region = 'H'
// RegionLeft is the left sidebar region.
RegionLeft Region = 'L' RegionLeft Region = 'L'
// RegionContent is the main content region.
RegionContent Region = 'C' RegionContent Region = 'C'
// RegionRight is the right sidebar region.
RegionRight Region = 'R' RegionRight Region = 'R'
// RegionFooter is the bottom region of the layout.
RegionFooter Region = 'F' RegionFooter Region = 'F'
) )
@ -37,6 +42,7 @@ type Renderable interface {
// StringBlock is a simple string that implements Renderable. // StringBlock is a simple string that implements Renderable.
type StringBlock string type StringBlock string
// Render returns the string content.
func (s StringBlock) Render() string { return string(s) } func (s StringBlock) Render() string { return string(s) }
// Layout creates a new layout from a variant string. // Layout creates a new layout from a variant string.
@ -138,4 +144,4 @@ func toRenderable(item any) Renderable {
default: default:
return StringBlock(fmt.Sprint(v)) return StringBlock(fmt.Sprint(v))
} }
} }

98
pkg/cli/output_test.go Normal file
View file

@ -0,0 +1,98 @@
package cli
import (
"bytes"
"io"
"os"
"testing"
)
func captureOutput(f func()) string {
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}
func TestSemanticOutput(t *testing.T) {
UseASCII()
// Test Success
out := captureOutput(func() {
Success("done")
})
if out == "" {
t.Error("Success output empty")
}
// Test Error
out = captureOutput(func() {
Error("fail")
})
if out == "" {
t.Error("Error output empty")
}
// Test Warn
out = captureOutput(func() {
Warn("warn")
})
if out == "" {
t.Error("Warn output empty")
}
// Test Info
out = captureOutput(func() {
Info("info")
})
if out == "" {
t.Error("Info output empty")
}
// Test Task
out = captureOutput(func() {
Task("task", "msg")
})
if out == "" {
t.Error("Task output empty")
}
// Test Section
out = captureOutput(func() {
Section("section")
})
if out == "" {
t.Error("Section output empty")
}
// Test Hint
out = captureOutput(func() {
Hint("hint", "msg")
})
if out == "" {
t.Error("Hint output empty")
}
// Test Result
out = captureOutput(func() {
Result(true, "pass")
})
if out == "" {
t.Error("Result(true) output empty")
}
out = captureOutput(func() {
Result(false, "fail")
})
if out == "" {
t.Error("Result(false) output empty")
}
}

View file

@ -25,14 +25,14 @@ type RepoDocInfo struct {
func loadRegistry(registryPath string) (*repos.Registry, string, error) { func loadRegistry(registryPath string) (*repos.Registry, string, error) {
var reg *repos.Registry var reg *repos.Registry
var err error var err error
var basePath string var registryDir string
if registryPath != "" { if registryPath != "" {
reg, err = repos.LoadRegistry(registryPath) reg, err = repos.LoadRegistry(registryPath)
if err != nil { if err != nil {
return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry")) return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry"))
} }
basePath = filepath.Dir(registryPath) registryDir = filepath.Dir(registryPath)
} else { } else {
registryPath, err = repos.FindRegistry() registryPath, err = repos.FindRegistry()
if err == nil { if err == nil {
@ -40,14 +40,44 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
if err != nil { if err != nil {
return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry")) return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry"))
} }
basePath = filepath.Dir(registryPath) registryDir = 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, "", cli.Wrap(err, i18n.T("i18n.fail.scan", "directory")) return nil, "", cli.Wrap(err, i18n.T("i18n.fail.scan", "directory"))
} }
basePath = cwd registryDir = cwd
}
}
// Load workspace config to respect packages_dir
wsConfig, err := repos.LoadWorkspaceConfig(registryDir)
if err != nil {
return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "workspace config"))
}
basePath := registryDir
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)
}
basePath = pkgDir
// Update repo paths if they were relative to registry
// This ensures consistency when packages_dir overrides the default
reg.BasePath = basePath
for _, repo := range reg.Repos {
repo.Path = filepath.Join(basePath, repo.Name)
} }
} }

View file

@ -0,0 +1,77 @@
package core
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
type IPCTestQuery struct{ Value string }
type IPCTestTask struct{ Value string }
func TestIPC_Query(t *testing.T) {
c, _ := New()
// No handler
res, handled, err := c.QUERY(IPCTestQuery{})
assert.False(t, handled)
assert.Nil(t, res)
assert.Nil(t, err)
// With handler
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
if tq, ok := q.(IPCTestQuery); ok {
return tq.Value + "-response", true, nil
}
return nil, false, nil
})
res, handled, err = c.QUERY(IPCTestQuery{Value: "test"})
assert.True(t, handled)
assert.Nil(t, err)
assert.Equal(t, "test-response", res)
}
func TestIPC_QueryAll(t *testing.T) {
c, _ := New()
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
return "h1", true, nil
})
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
return "h2", true, nil
})
results, err := c.QUERYALL(IPCTestQuery{})
assert.Nil(t, err)
assert.Len(t, results, 2)
assert.Contains(t, results, "h1")
assert.Contains(t, results, "h2")
}
func TestIPC_Perform(t *testing.T) {
c, _ := New()
c.RegisterTask(func(c *Core, task Task) (any, bool, error) {
if tt, ok := task.(IPCTestTask); ok {
if tt.Value == "error" {
return nil, true, errors.New("task error")
}
return "done", true, nil
}
return nil, false, nil
})
// Success
res, handled, err := c.PERFORM(IPCTestTask{Value: "run"})
assert.True(t, handled)
assert.Nil(t, err)
assert.Equal(t, "done", res)
// Error
res, handled, err = c.PERFORM(IPCTestTask{Value: "error"})
assert.True(t, handled)
assert.Error(t, err)
assert.Nil(t, res)
}

View file

@ -349,6 +349,12 @@ func IsPHPProject(dir string) bool {
return err == nil return err == nil
} }
// commonLinuxKitPaths defines default search locations for linuxkit.
var commonLinuxKitPaths = []string{
"/usr/local/bin/linuxkit",
"/opt/homebrew/bin/linuxkit",
}
// lookupLinuxKit finds the linuxkit binary. // lookupLinuxKit finds the linuxkit binary.
func lookupLinuxKit() (string, error) { func lookupLinuxKit() (string, error) {
// Check PATH first // Check PATH first
@ -356,13 +362,7 @@ func lookupLinuxKit() (string, error) {
return path, nil return path, nil
} }
// Check common locations for _, p := range commonLinuxKitPaths {
paths := []string{
"/usr/local/bin/linuxkit",
"/opt/homebrew/bin/linuxkit",
}
for _, p := range paths {
if _, err := os.Stat(p); err == nil { if _, err := os.Stat(p); err == nil {
return p, nil return p, nil
} }

View file

@ -102,16 +102,22 @@ func TestIsPHPProject_Container_Bad(t *testing.T) {
func TestLookupLinuxKit_Bad(t *testing.T) { func TestLookupLinuxKit_Bad(t *testing.T) {
t.Run("returns error when linuxkit not found", func(t *testing.T) { t.Run("returns error when linuxkit not found", func(t *testing.T) {
// Save original PATH and restore after test // Save original PATH and paths
origPath := os.Getenv("PATH") origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath) origCommonPaths := commonLinuxKitPaths
defer func() {
os.Setenv("PATH", origPath)
commonLinuxKitPaths = origCommonPaths
}()
// Set PATH to empty to ensure linuxkit isn't found // Set PATH to empty and clear common paths
os.Setenv("PATH", "") os.Setenv("PATH", "")
commonLinuxKitPaths = []string{}
_, err := lookupLinuxKit() _, err := lookupLinuxKit()
assert.Error(t, err) if assert.Error(t, err) {
assert.Contains(t, err.Error(), "linuxkit not found") assert.Contains(t, err.Error(), "linuxkit not found")
}
}) })
} }

View file

@ -50,7 +50,7 @@ func TestReadComposerJSON_Bad(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
_, err := readComposerJSON(dir) _, err := readComposerJSON(dir)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read composer.json") assert.Contains(t, err.Error(), "Failed to read composer.json")
}) })
t.Run("invalid JSON", func(t *testing.T) { t.Run("invalid JSON", func(t *testing.T) {
@ -60,7 +60,7 @@ func TestReadComposerJSON_Bad(t *testing.T) {
_, err = readComposerJSON(dir) _, err = readComposerJSON(dir)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse composer.json") assert.Contains(t, err.Error(), "Failed to parse composer.json")
}) })
} }
@ -104,10 +104,9 @@ func TestWriteComposerJSON_Bad(t *testing.T) {
err := writeComposerJSON("/non/existent/path", raw) err := writeComposerJSON("/non/existent/path", raw)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to write composer.json") assert.Contains(t, err.Error(), "Failed to write composer.json")
}) })
} }
func TestGetRepositories_Good(t *testing.T) { func TestGetRepositories_Good(t *testing.T) {
t.Run("returns empty slice when no repositories", func(t *testing.T) { t.Run("returns empty slice when no repositories", func(t *testing.T) {
raw := make(map[string]json.RawMessage) raw := make(map[string]json.RawMessage)
@ -149,7 +148,7 @@ func TestGetRepositories_Bad(t *testing.T) {
_, err := getRepositories(raw) _, err := getRepositories(raw)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse repositories") assert.Contains(t, err.Error(), "Failed to parse repositories")
}) })
} }
@ -212,17 +211,17 @@ func TestGetPackageInfo_Bad(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
_, _, err := getPackageInfo(dir) _, _, err := getPackageInfo(dir)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read package composer.json") assert.Contains(t, err.Error(), "Failed to read package composer.json")
}) })
t.Run("invalid JSON", func(t *testing.T) { t.Run("invalid JSON", func(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("invalid{"), 0644) err := os.WriteFile(filepath.Join(dir, "composer.json"), []byte("not json{"), 0644)
require.NoError(t, err) require.NoError(t, err)
_, _, err = getPackageInfo(dir) _, _, err = getPackageInfo(dir)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse package composer.json") assert.Contains(t, err.Error(), "Failed to parse package composer.json")
}) })
t.Run("missing name", func(t *testing.T) { t.Run("missing name", func(t *testing.T) {

View file

@ -99,9 +99,8 @@ func TestBaseService_Logs_Bad(t *testing.T) {
t.Run("returns error when log file doesn't exist", func(t *testing.T) { t.Run("returns error when log file doesn't exist", func(t *testing.T) {
s := &baseService{logPath: "/nonexistent/path/log.log"} s := &baseService{logPath: "/nonexistent/path/log.log"}
_, err := s.Logs(false) _, err := s.Logs(false)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to open log file") assert.Contains(t, err.Error(), "Failed to open log file")
}) })
} }

View file

@ -22,7 +22,7 @@ func TestGetSSLDir_Bad(t *testing.T) {
opts := SSLOptions{Dir: "/dev/null/cannot/create"} opts := SSLOptions{Dir: "/dev/null/cannot/create"}
_, err := GetSSLDir(opts) _, err := GetSSLDir(opts)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to create SSL directory") assert.Contains(t, err.Error(), "Failed to create SSL directory")
}) })
} }

View file

@ -26,11 +26,6 @@ func (p *GitHubPublisher) Name() string {
// Publish publishes the release to GitHub. // Publish publishes the release to GitHub.
// Uses the gh CLI for creating releases and uploading assets. // Uses the gh CLI for creating releases and uploading assets.
func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error { func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg PublisherConfig, relCfg ReleaseConfig, dryRun bool) error {
// Validate gh CLI is available
if err := validateGhCli(); err != nil {
return err
}
// Determine repository // Determine repository
repo := "" repo := ""
if relCfg != nil { if relCfg != nil {
@ -49,6 +44,11 @@ func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg
return p.dryRunPublish(release, pubCfg, repo) return p.dryRunPublish(release, pubCfg, repo)
} }
// Validate gh CLI is available and authenticated for actual publish
if err := validateGhCli(); err != nil {
return err
}
return p.executePublish(ctx, release, pubCfg, repo) return p.executePublish(ctx, release, pubCfg, repo)
} }

47
pkg/repos/workspace.go Normal file
View file

@ -0,0 +1,47 @@
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

@ -0,0 +1,56 @@
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

@ -33,21 +33,34 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("registry")), registryPath)
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.org_label")), reg.Org) fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.setup.org_label")), reg.Org)
registryDir := filepath.Dir(registryPath)
// Determine base path for cloning // Determine base path for cloning
basePath := reg.BasePath basePath := reg.BasePath
if basePath == "" { if basePath == "" {
basePath = "./packages" // Load workspace config to see if packages_dir is set
} wsConfig, err := repos.LoadWorkspaceConfig(registryDir)
// Resolve relative to registry location if err != nil {
if !filepath.IsAbs(basePath) { return fmt.Errorf("failed to load workspace config: %w", err)
basePath = filepath.Join(filepath.Dir(registryPath), basePath) }
if wsConfig.PackagesDir != "" {
basePath = wsConfig.PackagesDir
} else {
basePath = "./packages"
}
} }
// Expand ~ // Expand ~
if strings.HasPrefix(basePath, "~/") { if strings.HasPrefix(basePath, "~/") {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
basePath = filepath.Join(home, basePath[2:]) basePath = filepath.Join(home, basePath[2:])
} }
// Resolve relative to registry location
if !filepath.IsAbs(basePath) {
basePath = filepath.Join(registryDir, basePath)
}
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("target")), basePath) fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("target")), basePath)
// Parse type filter // Parse type filter