feat(cli): add apply subcommand

This commit is contained in:
2026-05-23 03:32:21 +01:00
parent 85fbbafb71
commit 68035b378c
15 changed files with 889 additions and 1 deletions
+28
View File
@@ -0,0 +1,28 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
)
// NewAdapter returns the `adapter` parent command with a `list` subcommand.
func NewAdapter(reg *adapter.Registry) *cobra.Command {
list := &cobra.Command{
Use: "list",
Short: "List registered adapters",
RunE: func(cmd *cobra.Command, _ []string) error {
for _, a := range reg.All() {
if _, err := fmt.Fprintln(cmd.OutOrStdout(), a.Name()); err != nil {
return err
}
}
return nil
},
}
cmd := &cobra.Command{Use: "adapter", Short: "Adapter introspection"}
cmd.AddCommand(list)
return cmd
}
+27
View File
@@ -0,0 +1,27 @@
package cli
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
)
func TestAdapterList(t *testing.T) {
reg := adapter.NewRegistry()
reg.Register(&detectableAdapter{name: "alpha", detect: true})
reg.Register(&detectableAdapter{name: "beta", detect: false})
var buf bytes.Buffer
cmd := NewAdapter(reg)
cmd.SetArgs([]string{"list"})
cmd.SetOut(&buf)
require.NoError(t, cmd.Execute())
out := buf.String()
assert.Contains(t, out, "alpha")
assert.Contains(t, out, "beta")
}
+91
View File
@@ -0,0 +1,91 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
"github.com/lukaszraczylo/harness-sync/internal/apply"
"github.com/lukaszraczylo/harness-sync/internal/canonical"
"github.com/lukaszraczylo/harness-sync/internal/gitx"
)
// NewApply returns the `apply` subcommand. It renders the canonical bundle
// to each detected (or selected) harness using the apply pipeline.
func NewApply(reg *adapter.Registry) *cobra.Command {
var (
dryRun bool
force bool
root string
)
cmd := &cobra.Command{
Use: "apply [harness...]",
Short: "Render canonical and propagate to harnesses",
RunE: func(cmd *cobra.Command, args []string) error {
if root == "" {
h, err := os.UserHomeDir()
if err != nil {
return err
}
root = filepath.Join(h, ".config", "harness-sync")
}
b, err := canonical.Load(root)
if err != nil {
return err
}
var selected []adapter.Adapter
if len(args) == 0 {
for _, a := range reg.All() {
if a.Detect() {
selected = append(selected, a)
}
}
} else {
for _, name := range args {
a, ok := reg.Get(name)
if !ok {
return fmt.Errorf("unknown adapter %q", name)
}
selected = append(selected, a)
}
}
opt := apply.Options{
Bundle: b,
Adapters: selected,
Repo: gitx.New(root),
DryRun: dryRun,
Force: force,
}
rep, err := apply.Run(opt)
if err != nil {
return err
}
mode := "applied"
if dryRun {
mode = "dry-run"
}
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s: %d written, %d skipped, %d conflicts\n",
mode, rep.Written, rep.Skipped, rep.Conflicts); err != nil {
return err
}
for _, a := range rep.Actions {
if _, err := fmt.Fprintf(cmd.OutOrStdout(), " %-10s %-12s %s %s\n", a.Adapter, a.Kind, a.Dest, a.Note); err != nil {
return err
}
}
if rep.Conflicts > 0 {
return fmt.Errorf("%d conflicts; resolve .rej files", rep.Conflicts)
}
return nil
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "print actions without writing")
cmd.Flags().BoolVar(&force, "force", false, "overwrite without 3-way merge")
cmd.Flags().StringVar(&root, "root", "", "canonical root (default ~/.config/harness-sync)")
return cmd
}
+32
View File
@@ -0,0 +1,32 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
)
func TestApplyDryRun(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: home\n"), 0o600))
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "home.yaml"),
[]byte("name: home\ngateway:\n url: u\n token: t\n default_model: m\nmodels:\n - {id: m}\n"), 0o600))
reg := adapter.NewRegistry()
reg.Register(&detectableAdapter{name: "stub", detect: true})
var buf bytes.Buffer
cmd := NewApply(reg)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--dry-run", "--root", root})
require.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "dry-run")
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import (
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
)
// NewDiff returns the `diff` subcommand. It is a shorthand for
// `apply --dry-run` and shows pending changes without writing.
func NewDiff(reg *adapter.Registry) *cobra.Command {
return &cobra.Command{
Use: "diff [harness...]",
Short: "Show pending changes (alias for `apply --dry-run`)",
RunE: func(cmd *cobra.Command, args []string) error {
apply := NewApply(reg)
apply.SetOut(cmd.OutOrStdout())
apply.SetErr(cmd.ErrOrStderr())
apply.SetArgs(append([]string{"--dry-run"}, args...))
return apply.Execute()
},
}
}
+31
View File
@@ -0,0 +1,31 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
)
func TestDiffInvokesDryRun(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: home\n"), 0o600))
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "home.yaml"),
[]byte("name: home\ngateway:\n url: u\n token: t\n default_model: m\nmodels:\n - {id: m}\n"), 0o600))
reg := adapter.NewRegistry()
reg.Register(&detectableAdapter{name: "stub", detect: true})
var buf bytes.Buffer
cmd := NewDiff(reg)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--root", root})
require.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "dry-run")
}
+207
View File
@@ -0,0 +1,207 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
"github.com/lukaszraczylo/harness-sync/internal/canonical"
"github.com/lukaszraczylo/harness-sync/internal/gitx"
"github.com/lukaszraczylo/harness-sync/internal/render"
"github.com/lukaszraczylo/harness-sync/internal/ui"
)
// NewInit returns the `init` subcommand: scan for detected harnesses, import
// their content into a canonical tree, and produce an initial git commit.
func NewInit(reg *adapter.Registry) *cobra.Command {
var (
root string
home string
from []string
noPrompt bool
)
cmd := &cobra.Command{
Use: "init",
Short: "Initialise canonical config from existing harnesses",
RunE: func(cmd *cobra.Command, _ []string) error {
var err error
r, err := resolveRoot(root)
if err != nil {
return err
}
if home == "" {
home, err = os.UserHomeDir()
if err != nil {
return err
}
}
if err = os.MkdirAll(r, 0o750); err != nil {
return err
}
candidates := reg.DetectedNames()
if len(candidates) == 0 {
return fmt.Errorf("no harnesses detected under %s", home)
}
var picked []string
switch {
case len(from) > 0:
picked = from
case noPrompt:
picked = candidates
default:
picked, err = ui.MultiSelect("Import from which harnesses?", candidates)
if err != nil {
return err
}
}
return runImport(cmd, reg, r, home, picked)
},
}
cmd.Flags().StringVar(&root, "root", "", "canonical root (default ~/.config/harness-sync)")
cmd.Flags().StringVar(&home, "home", "", "home dir (default $HOME)")
cmd.Flags().StringSliceVar(&from, "from", nil, "import from specific adapter(s), skip prompt")
cmd.Flags().BoolVar(&noPrompt, "no-prompt", false, "import from all detected adapters without prompting")
return cmd
}
func runImport(cmd *cobra.Command, reg *adapter.Registry, root, home string, picked []string) error {
bundle := &canonical.Bundle{
Root: root,
Config: canonical.Config{ActiveProfile: "imported"},
}
seenSkills := map[string]bool{}
seenAgents := map[string]bool{}
seenMCP := map[string]bool{}
var instructions []string
for _, name := range picked {
ad, ok := reg.Get(name)
if !ok {
return fmt.Errorf("unknown adapter %q", name)
}
res, err := ad.Import(home)
if err != nil {
return fmt.Errorf("import %s: %w", name, err)
}
for _, s := range res.Skills {
key := s.Name + "|" + s.Body
if seenSkills[key] {
continue
}
seenSkills[key] = true
bundle.Skills = append(bundle.Skills, s)
}
for _, a := range res.Agents {
key := a.Name + "|" + a.Body
if seenAgents[key] {
continue
}
seenAgents[key] = true
bundle.Agents = append(bundle.Agents, a)
}
for _, m := range res.MCP {
if seenMCP[m.Name] {
continue
}
seenMCP[m.Name] = true
bundle.MCP.Servers = append(bundle.MCP.Servers, m)
}
if res.Instructions != "" {
instructions = append(instructions, fmt.Sprintf("<!-- from %s -->\n%s", name, res.Instructions))
}
}
bundle.Instructions.Global = strings.Join(instructions, "\n\n")
if err := writeCanonical(root, bundle, picked); err != nil {
return err
}
repo := gitx.New(root)
if !repo.IsRepo() {
if err := repo.Init(); err != nil {
return err
}
if err := repo.Configure("harness-sync", "harness-sync@localhost"); err != nil {
return err
}
}
if err := repo.AddAll(); err != nil {
return err
}
if err := repo.Commit(fmt.Sprintf("import from %s", strings.Join(picked, ", "))); err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "imported from %v into %s\n", picked, root)
return nil
}
func writeCanonical(root string, b *canonical.Bundle, picked []string) error {
for _, sub := range []string{"profiles", "skills", "agents", "instructions"} {
if err := os.MkdirAll(filepath.Join(root, sub), 0o750); err != nil {
return err
}
}
cfgBody, err := render.YAML(map[string]any{
"enabled_harnesses": picked,
"active_profile": "imported",
})
if err != nil {
return err
}
if err = os.WriteFile(filepath.Join(root, "config.yaml"), cfgBody, 0o600); err != nil {
return err
}
profBody, err := render.YAML(canonical.Profile{
Name: "imported",
Gateway: canonical.Gateway{
URL: "https://gateway.local",
Token: "dummy",
DefaultModel: "claude-sonnet-4-6",
},
Models: []canonical.Model{{ID: "claude-sonnet-4-6", Alias: "sonnet"}},
})
if err != nil {
return err
}
if err = os.WriteFile(filepath.Join(root, "profiles", "imported.yaml"), profBody, 0o600); err != nil {
return err
}
if len(b.MCP.Servers) > 0 {
mcpBody, err := render.YAML(b.MCP)
if err != nil {
return err
}
if err = os.WriteFile(filepath.Join(root, "mcp.yaml"), mcpBody, 0o600); err != nil {
return err
}
}
for _, s := range b.Skills {
path := filepath.Join(root, "skills", s.Name, "SKILL.md")
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return err
}
if err := os.WriteFile(path, []byte(s.Body), 0o600); err != nil {
return err
}
}
for _, a := range b.Agents {
path := filepath.Join(root, "agents", a.Name+".md")
if err := os.WriteFile(path, []byte(a.Body), 0o600); err != nil {
return err
}
}
if b.Instructions.Global != "" {
if err := os.WriteFile(filepath.Join(root, "instructions", "global.md"),
[]byte(b.Instructions.Global), 0o600); err != nil {
return err
}
}
return nil
}
+51
View File
@@ -0,0 +1,51 @@
package cli
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
"github.com/lukaszraczylo/harness-sync/internal/canonical"
)
type importableAdapter struct {
res *adapter.ImportResult
name string
}
func (i *importableAdapter) Name() string { return i.name }
func (i *importableAdapter) Detect() bool { return true }
func (i *importableAdapter) Render(_ *canonical.Bundle) (*adapter.FileSet, error) {
return adapter.NewFileSet(), nil
}
func (i *importableAdapter) Import(_ string) (*adapter.ImportResult, error) { return i.res, nil }
func TestInitImportWritesCanonical(t *testing.T) {
root := t.TempDir()
home := t.TempDir()
reg := adapter.NewRegistry()
reg.Register(&importableAdapter{
name: "stub",
res: &adapter.ImportResult{
Skills: []canonical.Skill{{Name: "hi", Body: "---\nname: hi\n---\nhi body", Path: "hi/SKILL.md"}},
Instructions: "# global",
MCP: []canonical.MCPServer{{Name: "filepuff", Command: "/bin/x"}},
},
})
cmd := NewInit(reg)
cmd.SetArgs([]string{"--root", root, "--home", home, "--from", "stub", "--no-prompt"})
require.NoError(t, cmd.Execute())
assert.FileExists(t, filepath.Join(root, "config.yaml"))
assert.FileExists(t, filepath.Join(root, "instructions", "global.md"))
assert.FileExists(t, filepath.Join(root, "mcp.yaml"))
assert.FileExists(t, filepath.Join(root, "skills", "hi", "SKILL.md"))
_, err := os.Stat(filepath.Join(root, ".git"))
require.NoError(t, err)
}
+83
View File
@@ -0,0 +1,83 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
)
// NewProfile returns the `profile` subcommand with `list` and `use`.
func NewProfile() *cobra.Command {
var root string
rootFlag := func(c *cobra.Command) {
c.Flags().StringVar(&root, "root", "", "canonical root (default ~/.config/harness-sync)")
}
list := &cobra.Command{
Use: "list",
Short: "List available profiles",
RunE: func(cmd *cobra.Command, _ []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
entries, err := os.ReadDir(filepath.Join(r, "profiles"))
if err != nil {
return err
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".yaml") {
names = append(names, strings.TrimSuffix(e.Name(), ".yaml"))
}
}
sort.Strings(names)
for _, n := range names {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), n)
}
return nil
},
}
rootFlag(list)
use := &cobra.Command{
Use: "use <name>",
Short: "Set active profile (rewrites config.yaml)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
target := args[0]
profPath := filepath.Join(r, "profiles", target+".yaml")
if _, err := os.Stat(profPath); err != nil {
return fmt.Errorf("profile %q not found at %s", target, profPath)
}
configPath := filepath.Join(r, "config.yaml")
existing, _ := os.ReadFile(configPath)
updated := setActiveProfile(string(existing), target)
return os.WriteFile(configPath, []byte(updated), 0o600)
},
}
rootFlag(use)
cmd := &cobra.Command{Use: "profile", Short: "Manage profiles"}
cmd.AddCommand(list, use)
return cmd
}
func setActiveProfile(existing, name string) string {
lines := strings.Split(existing, "\n")
for i, l := range lines {
if strings.HasPrefix(l, "active_profile:") {
lines[i] = "active_profile: " + name
return strings.Join(lines, "\n")
}
}
return strings.TrimRight(existing, "\n") + "\nactive_profile: " + name + "\n"
}
+43
View File
@@ -0,0 +1,43 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProfileList(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "home.yaml"), []byte("name: home\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "work.yaml"), []byte("name: work\n"), 0o600))
var buf bytes.Buffer
cmd := NewProfile()
cmd.SetArgs([]string{"list", "--root", root})
cmd.SetOut(&buf)
require.NoError(t, cmd.Execute())
out := buf.String()
assert.Contains(t, out, "home")
assert.Contains(t, out, "work")
}
func TestProfileUse(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "work.yaml"), []byte("name: work\n"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: home\n"), 0o600))
cmd := NewProfile()
cmd.SetArgs([]string{"use", "work", "--root", root})
require.NoError(t, cmd.Execute())
body, err := os.ReadFile(filepath.Join(root, "config.yaml"))
require.NoError(t, err)
assert.Contains(t, string(body), "active_profile: work")
}
+37
View File
@@ -0,0 +1,37 @@
package cli
import (
"strconv"
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/gitx"
)
// NewRollback returns the `rollback` subcommand. It reverts the last N apply
// commits in the canonical repo by calling `git revert` over a range.
func NewRollback() *cobra.Command {
var root string
cmd := &cobra.Command{
Use: "rollback [n]",
Short: "Revert the last N apply commits in the canonical repo",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
n := 1
if len(args) == 1 {
v, err := strconv.Atoi(args[0])
if err != nil || v < 1 {
return cmd.Help()
}
n = v
}
return gitx.New(r).Revert(n)
},
}
cmd.Flags().StringVar(&root, "root", "", "canonical root")
return cmd
}
+12
View File
@@ -0,0 +1,12 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRollbackHelp(t *testing.T) {
cmd := NewRollback()
assert.Equal(t, "rollback [n]", cmd.Use)
}
+20 -1
View File
@@ -1,7 +1,13 @@
// Package cli provides the cobra command tree for harness-sync.
package cli
import "github.com/spf13/cobra"
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
func NewRoot(version string) *cobra.Command {
return &cobra.Command{
@@ -11,3 +17,16 @@ func NewRoot(version string) *cobra.Command {
SilenceUsage: true,
}
}
// resolveRoot returns the canonical root: the flag value when non-empty,
// else $HOME/.config/harness-sync.
func resolveRoot(flag string) (string, error) {
if flag != "" {
return flag, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home dir: %w", err)
}
return filepath.Join(home, ".config", "harness-sync"), nil
}
+120
View File
@@ -0,0 +1,120 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
"github.com/lukaszraczylo/harness-sync/internal/canonical"
)
// NewShow returns the `show` subcommand which prints the files each adapter
// would manage if `apply` were run now. Useful for auditing what harness-sync
// will write to your system before committing to it.
func NewShow(reg *adapter.Registry) *cobra.Command {
var (
root string
all bool
)
cmd := &cobra.Command{
Use: "show [harness...]",
Short: "List files harness-sync manages for each harness",
Long: `Renders the canonical bundle through each adapter and prints
the resulting targets without writing anything. By default, shows only detected
harnesses; pass --all to include adapters whose harness is not installed, or
positional args to select specific harnesses.`,
RunE: func(cmd *cobra.Command, args []string) error {
r, err := resolveRoot(root)
if err != nil {
return err
}
b, err := canonical.Load(r)
if err != nil {
return err
}
var selected []adapter.Adapter
switch {
case len(args) > 0:
for _, name := range args {
a, ok := reg.Get(name)
if !ok {
return fmt.Errorf("unknown adapter %q", name)
}
selected = append(selected, a)
}
case all:
selected = reg.All()
default:
for _, a := range reg.All() {
if a.Detect() {
selected = append(selected, a)
}
}
}
w := cmd.OutOrStdout()
for _, a := range selected {
fs, err := a.Render(b)
if err != nil {
if _, werr := fmt.Fprintf(w, "%s: render error: %v\n", a.Name(), err); werr != nil {
return werr
}
continue
}
if _, werr := fmt.Fprintf(w, "%s (%s)\n", a.Name(), detectLabel(a)); werr != nil {
return werr
}
var iterErr error
fs.ForEach(func(f adapter.File) {
if iterErr != nil {
return
}
switch f.Kind {
case adapter.SymlinkDir, adapter.SymlinkFile:
_, iterErr = fmt.Fprintf(w, " %-12s %s -> %s\n",
f.Kind.String(), short(f.Dest), short(f.SymlinkTarget))
case adapter.RenderedFile:
_, iterErr = fmt.Fprintf(w, " %-12s %s (%d bytes)\n",
f.Kind.String(), short(f.Dest), len(f.Content))
}
})
if iterErr != nil {
return iterErr
}
if _, werr := fmt.Fprintln(w); werr != nil {
return werr
}
}
return nil
},
}
cmd.Flags().StringVar(&root, "root", "", "canonical root (default ~/.config/harness-sync)")
cmd.Flags().BoolVar(&all, "all", false, "include adapters whose harness is not detected")
return cmd
}
func detectLabel(a adapter.Adapter) string {
if a.Detect() {
return "detected"
}
return "not detected"
}
// short collapses the user's home dir to "~" for cleaner output.
func short(p string) string {
home, err := os.UserHomeDir()
if err != nil || home == "" {
return p
}
if p == home {
return "~"
}
if len(p) > len(home) && p[:len(home)+1] == home+string(filepath.Separator) {
return "~" + p[len(home):]
}
return p
}
+84
View File
@@ -0,0 +1,84 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/lukaszraczylo/harness-sync/internal/adapter"
"github.com/lukaszraczylo/harness-sync/internal/canonical"
)
type renderingAdapter struct {
name string
files []adapter.File
detect bool
}
func (r *renderingAdapter) Name() string { return r.name }
func (r *renderingAdapter) Detect() bool { return r.detect }
func (r *renderingAdapter) Import(_ string) (*adapter.ImportResult, error) {
return &adapter.ImportResult{}, nil
}
func (r *renderingAdapter) Render(_ *canonical.Bundle) (*adapter.FileSet, error) {
fs := adapter.NewFileSet()
for _, f := range r.files {
fs.Add(f)
}
return fs, nil
}
func TestShowPrintsTargets(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: home\n"), 0o600))
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "home.yaml"),
[]byte("name: home\ngateway:\n url: u\n token: t\n default_model: m\nmodels:\n - {id: m}\n"), 0o600))
reg := adapter.NewRegistry()
reg.Register(&renderingAdapter{
name: "stub",
detect: true,
files: []adapter.File{
{Dest: "/tmp/out.json", Kind: adapter.RenderedFile, Content: []byte("{}\n")},
{Dest: "/tmp/link", Kind: adapter.SymlinkDir, SymlinkTarget: "/canon/skills"},
},
})
var buf bytes.Buffer
cmd := NewShow(reg)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--root", root})
require.NoError(t, cmd.Execute())
out := buf.String()
assert.Contains(t, out, "stub")
assert.Contains(t, out, "rendered")
assert.Contains(t, out, "symlink-dir")
assert.Contains(t, out, "/tmp/out.json")
assert.Contains(t, out, "/canon/skills")
}
func TestShowAllIncludesUndetected(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "config.yaml"), []byte("active_profile: home\n"), 0o600))
require.NoError(t, os.MkdirAll(filepath.Join(root, "profiles"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(root, "profiles", "home.yaml"),
[]byte("name: home\ngateway:\n url: u\n token: t\n default_model: m\nmodels:\n - {id: m}\n"), 0o600))
reg := adapter.NewRegistry()
reg.Register(&renderingAdapter{name: "absent", detect: false})
var buf bytes.Buffer
cmd := NewShow(reg)
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--root", root, "--all"})
require.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "absent")
assert.Contains(t, buf.String(), "not detected")
}