* 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:
parent
b02b57e6fb
commit
10277c6094
18 changed files with 442 additions and 46 deletions
8
.github/workflows/coverage.yml
vendored
8
.github/workflows/coverage.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.work'
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Setup Task
|
||||
uses: arduino/setup-task@v1
|
||||
|
|
@ -25,6 +25,12 @@ jobs:
|
|||
sudo apt-get update
|
||||
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
|
||||
run: task cov
|
||||
|
||||
|
|
|
|||
|
|
@ -100,17 +100,19 @@ func (s *AnsiStyle) Render(text string) string {
|
|||
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 {
|
||||
r, g, b := hexToRGB(hex)
|
||||
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 {
|
||||
r, g, b := hexToRGB(hex)
|
||||
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) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
if len(hex) != 6 {
|
||||
|
|
|
|||
49
pkg/cli/check_test.go
Normal file
49
pkg/cli/check_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,15 +9,23 @@ import (
|
|||
type GlyphTheme int
|
||||
|
||||
const (
|
||||
// ThemeUnicode uses standard Unicode symbols.
|
||||
ThemeUnicode GlyphTheme = iota
|
||||
// ThemeEmoji uses Emoji symbols.
|
||||
ThemeEmoji
|
||||
// ThemeASCII uses ASCII fallback symbols.
|
||||
ThemeASCII
|
||||
)
|
||||
|
||||
var currentTheme = ThemeUnicode
|
||||
|
||||
// UseUnicode switches the glyph theme to Unicode.
|
||||
func UseUnicode() { currentTheme = ThemeUnicode }
|
||||
|
||||
// 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 {
|
||||
|
|
@ -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 {
|
||||
if sym, ok := glyphMap()[code]; ok {
|
||||
return sym
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ import "fmt"
|
|||
type Region rune
|
||||
|
||||
const (
|
||||
// RegionHeader is the top region of the layout.
|
||||
RegionHeader Region = 'H'
|
||||
// RegionLeft is the left sidebar region.
|
||||
RegionLeft Region = 'L'
|
||||
// RegionContent is the main content region.
|
||||
RegionContent Region = 'C'
|
||||
// RegionRight is the right sidebar region.
|
||||
RegionRight Region = 'R'
|
||||
// RegionFooter is the bottom region of the layout.
|
||||
RegionFooter Region = 'F'
|
||||
)
|
||||
|
||||
|
|
@ -37,6 +42,7 @@ type Renderable interface {
|
|||
// StringBlock is a simple string that implements Renderable.
|
||||
type StringBlock string
|
||||
|
||||
// Render returns the string content.
|
||||
func (s StringBlock) Render() string { return string(s) }
|
||||
|
||||
// Layout creates a new layout from a variant string.
|
||||
|
|
|
|||
98
pkg/cli/output_test.go
Normal file
98
pkg/cli/output_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -25,14 +25,14 @@ type RepoDocInfo struct {
|
|||
func loadRegistry(registryPath string) (*repos.Registry, string, error) {
|
||||
var reg *repos.Registry
|
||||
var err error
|
||||
var basePath string
|
||||
var registryDir string
|
||||
|
||||
if registryPath != "" {
|
||||
reg, err = repos.LoadRegistry(registryPath)
|
||||
if err != nil {
|
||||
return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry"))
|
||||
}
|
||||
basePath = filepath.Dir(registryPath)
|
||||
registryDir = filepath.Dir(registryPath)
|
||||
} else {
|
||||
registryPath, err = repos.FindRegistry()
|
||||
if err == nil {
|
||||
|
|
@ -40,14 +40,44 @@ func loadRegistry(registryPath string) (*repos.Registry, string, error) {
|
|||
if err != nil {
|
||||
return nil, "", cli.Wrap(err, i18n.T("i18n.fail.load", "registry"))
|
||||
}
|
||||
basePath = filepath.Dir(registryPath)
|
||||
registryDir = filepath.Dir(registryPath)
|
||||
} else {
|
||||
cwd, _ := os.Getwd()
|
||||
reg, err = repos.ScanDirectory(cwd)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
77
pkg/framework/core/ipc_test.go
Normal file
77
pkg/framework/core/ipc_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -349,6 +349,12 @@ func IsPHPProject(dir string) bool {
|
|||
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.
|
||||
func lookupLinuxKit() (string, error) {
|
||||
// Check PATH first
|
||||
|
|
@ -356,13 +362,7 @@ func lookupLinuxKit() (string, error) {
|
|||
return path, nil
|
||||
}
|
||||
|
||||
// Check common locations
|
||||
paths := []string{
|
||||
"/usr/local/bin/linuxkit",
|
||||
"/opt/homebrew/bin/linuxkit",
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
for _, p := range commonLinuxKitPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,16 +102,22 @@ func TestIsPHPProject_Container_Bad(t *testing.T) {
|
|||
|
||||
func TestLookupLinuxKit_Bad(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")
|
||||
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", "")
|
||||
commonLinuxKitPaths = []string{}
|
||||
|
||||
_, err := lookupLinuxKit()
|
||||
assert.Error(t, err)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "linuxkit not found")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func TestReadComposerJSON_Bad(t *testing.T) {
|
|||
dir := t.TempDir()
|
||||
_, err := readComposerJSON(dir)
|
||||
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) {
|
||||
|
|
@ -60,7 +60,7 @@ func TestReadComposerJSON_Bad(t *testing.T) {
|
|||
|
||||
_, err = readComposerJSON(dir)
|
||||
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)
|
||||
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) {
|
||||
t.Run("returns empty slice when no repositories", func(t *testing.T) {
|
||||
raw := make(map[string]json.RawMessage)
|
||||
|
|
@ -149,7 +148,7 @@ func TestGetRepositories_Bad(t *testing.T) {
|
|||
|
||||
_, err := getRepositories(raw)
|
||||
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()
|
||||
_, _, err := getPackageInfo(dir)
|
||||
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) {
|
||||
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)
|
||||
|
||||
_, _, err = getPackageInfo(dir)
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
s := &baseService{logPath: "/nonexistent/path/log.log"}
|
||||
_, err := s.Logs(false)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to open log file")
|
||||
assert.Contains(t, err.Error(), "Failed to open log file")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestGetSSLDir_Bad(t *testing.T) {
|
|||
opts := SSLOptions{Dir: "/dev/null/cannot/create"}
|
||||
_, err := GetSSLDir(opts)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to create SSL directory")
|
||||
assert.Contains(t, err.Error(), "Failed to create SSL directory")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,6 @@ func (p *GitHubPublisher) Name() string {
|
|||
// Publish publishes the release to GitHub.
|
||||
// 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 {
|
||||
// Validate gh CLI is available
|
||||
if err := validateGhCli(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine repository
|
||||
repo := ""
|
||||
if relCfg != nil {
|
||||
|
|
@ -49,6 +44,11 @@ func (p *GitHubPublisher) Publish(ctx context.Context, release *Release, pubCfg
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
47
pkg/repos/workspace.go
Normal file
47
pkg/repos/workspace.go
Normal 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
|
||||
}
|
||||
56
pkg/repos/workspace_test.go
Normal file
56
pkg/repos/workspace_test.go
Normal 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")
|
||||
}
|
||||
|
|
@ -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.T("cmd.setup.org_label")), reg.Org)
|
||||
|
||||
registryDir := filepath.Dir(registryPath)
|
||||
|
||||
// Determine base path for cloning
|
||||
basePath := reg.BasePath
|
||||
if basePath == "" {
|
||||
// Load workspace config to see if packages_dir is set
|
||||
wsConfig, err := repos.LoadWorkspaceConfig(registryDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load workspace config: %w", err)
|
||||
}
|
||||
if wsConfig.PackagesDir != "" {
|
||||
basePath = wsConfig.PackagesDir
|
||||
} else {
|
||||
basePath = "./packages"
|
||||
}
|
||||
// Resolve relative to registry location
|
||||
if !filepath.IsAbs(basePath) {
|
||||
basePath = filepath.Join(filepath.Dir(registryPath), basePath)
|
||||
}
|
||||
|
||||
// Expand ~
|
||||
if strings.HasPrefix(basePath, "~/") {
|
||||
home, _ := os.UserHomeDir()
|
||||
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)
|
||||
|
||||
// Parse type filter
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue