From 7013bada1c343f769ec626a12732b70420219e31 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 23 May 2026 02:59:50 +0100 Subject: [PATCH] feat(kilo): adapter with kilo.json render + agent symlink --- internal/adapters/crush/crush.go | 144 +++++++++++++++++++ internal/adapters/crush/crush_test.go | 65 +++++++++ internal/adapters/crush/import.go | 45 ++++++ internal/adapters/kilo/import.go | 46 +++++++ internal/adapters/kilo/kilo.go | 143 +++++++++++++++++++ internal/adapters/kilo/kilo_test.go | 55 ++++++++ internal/adapters/opencode/import.go | 75 ++++++++++ internal/adapters/opencode/opencode.go | 145 ++++++++++++++++++++ internal/adapters/opencode/opencode_test.go | 61 ++++++++ 9 files changed, 779 insertions(+) create mode 100644 internal/adapters/crush/crush.go create mode 100644 internal/adapters/crush/crush_test.go create mode 100644 internal/adapters/crush/import.go create mode 100644 internal/adapters/kilo/import.go create mode 100644 internal/adapters/kilo/kilo.go create mode 100644 internal/adapters/kilo/kilo_test.go create mode 100644 internal/adapters/opencode/import.go create mode 100644 internal/adapters/opencode/opencode.go create mode 100644 internal/adapters/opencode/opencode_test.go diff --git a/internal/adapters/crush/crush.go b/internal/adapters/crush/crush.go new file mode 100644 index 0000000..1d2f2d4 --- /dev/null +++ b/internal/adapters/crush/crush.go @@ -0,0 +1,144 @@ +// Package crush is the charmbracelet/crush harness adapter. +package crush + +import ( + "os" + "path/filepath" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" + "github.com/lukaszraczylo/harness-sync/internal/render" +) + +const name = "crush" + +// Adapter implements adapter.Adapter for the crush harness. +type Adapter struct{ home string } + +// Option configures an Adapter. +type Option func(*Adapter) + +// WithHome overrides the home directory (defaults to os.UserHomeDir). +func WithHome(h string) Option { return func(a *Adapter) { a.home = h } } + +// New returns a new Adapter with the given options applied. +func New(opts ...Option) *Adapter { + a := &Adapter{home: defaultHome()} + for _, o := range opts { + o(a) + } + return a +} + +func defaultHome() string { + h, err := os.UserHomeDir() + if err != nil { + return "" + } + return h +} + +// Name returns the harness identifier. +func (a *Adapter) Name() string { return name } + +// Detect returns true when ~/.config/crush exists. +func (a *Adapter) Detect() bool { + _, err := os.Stat(filepath.Join(a.home, ".config", "crush")) + return err == nil +} + +// Render produces the FileSet that applies the canonical bundle to crush. +func (a *Adapter) Render(b *canonical.Bundle) (*adapter.FileSet, error) { + fs := adapter.NewFileSet() + base := filepath.Join(a.home, ".config", "crush") + + fs.Add(adapter.File{ + Dest: filepath.Join(base, "skills"), + Kind: adapter.SymlinkDir, + SymlinkTarget: filepath.Join(b.Root, "skills"), + }) + + cfg, err := renderConfig(b) + if err != nil { + return nil, err + } + fs.Add(adapter.File{ + Dest: filepath.Join(base, "crush.json"), + Kind: adapter.RenderedFile, + Content: cfg, + }) + + return fs, nil +} + +func renderConfig(b *canonical.Bundle) ([]byte, error) { + providers := make([]map[string]any, 0, len(b.Profile.Upstreams)+1) + + // Gateway as primary provider. + if b.Profile.Gateway.URL != "" { + gw := map[string]any{ + "id": "harness-sync-gateway", + "name": "harness-sync gateway", + "base_url": b.Profile.Gateway.URL, + "api_key": b.Profile.Gateway.Token, + } + models := make([]map[string]any, 0, len(b.Profile.Models)) + for _, m := range b.Profile.Models { + entry := map[string]any{"id": m.ID} + if m.Alias != "" { + entry["alias"] = m.Alias + } + models = append(models, entry) + } + if len(models) > 0 { + gw["models"] = models + } + providers = append(providers, gw) + } + + for _, up := range b.Profile.Upstreams { + p := map[string]any{"id": up.Name, "name": up.Name} + if up.BaseURL != "" { + p["base_url"] = up.BaseURL + } + if up.APIKey != "" { + p["api_key"] = up.APIKey + } + providers = append(providers, p) + } + + mcp := map[string]any{} + for _, s := range b.MCP.Servers { + entry := map[string]any{} + if s.Command != "" { + entry["command"] = s.Command + } + if len(s.Args) > 0 { + entry["args"] = s.Args + } + if s.URL != "" { + entry["url"] = s.URL + } + if s.Transport != "" { + entry["transport"] = s.Transport + } + if len(s.Env) > 0 { + entry["env"] = s.Env + } + mcp[s.Name] = entry + } + + out := map[string]any{ + "providers": providers, + "default_model": b.Profile.Gateway.DefaultModel, + } + if len(mcp) > 0 { + out["mcpServers"] = mcp + } + return render.JSON(out) +} + +// Import reads crush config from home and returns a canonical ImportResult. +func (a *Adapter) Import(home string) (*adapter.ImportResult, error) { + return importFrom(home) +} diff --git a/internal/adapters/crush/crush_test.go b/internal/adapters/crush/crush_test.go new file mode 100644 index 0000000..824b1dd --- /dev/null +++ b/internal/adapters/crush/crush_test.go @@ -0,0 +1,65 @@ +package crush + +import ( + "encoding/json" + "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" +) + +func TestCrushRenderProducesExpectedTargets(t *testing.T) { + home := t.TempDir() + ad := New(WithHome(home)) + b := &canonical.Bundle{ + Root: "/canon", + Profile: canonical.Profile{ + Name: "home", + Gateway: canonical.Gateway{URL: "https://gw", Token: "dummy", DefaultModel: "sonnet"}, + Models: []canonical.Model{{ID: "claude-sonnet-4-6", Alias: "sonnet"}}, + }, + } + fs, err := ad.Render(b) + require.NoError(t, err) + + seen := map[string]adapter.File{} + fs.ForEach(func(f adapter.File) { seen[f.Dest] = f }) + + skillsDest := filepath.Join(home, ".config", "crush", "skills") + assert.Equal(t, adapter.SymlinkDir, seen[skillsDest].Kind) + assert.Equal(t, "/canon/skills", seen[skillsDest].SymlinkTarget) + + cfgDest := filepath.Join(home, ".config", "crush", "crush.json") + assert.Equal(t, adapter.RenderedFile, seen[cfgDest].Kind) + + // Parse rendered JSON to verify structure. + var parsed map[string]any + require.NoError(t, json.Unmarshal(seen[cfgDest].Content, &parsed)) + assert.NotNil(t, parsed) // at least it's valid JSON +} + +func TestCrushImportRoundtrip(t *testing.T) { + home := t.TempDir() + base := filepath.Join(home, ".config", "crush") + require.NoError(t, os.MkdirAll(base, 0o755)) + + // Minimal crush.json + cfg := map[string]any{ + "providers": []map[string]any{ + {"id": "anthropic", "base_url": "https://api.anthropic.com"}, + }, + } + body, _ := json.Marshal(cfg) + require.NoError(t, os.WriteFile(filepath.Join(base, "crush.json"), body, 0o644)) + + ad := New(WithHome(home)) + res, err := ad.Import(home) + require.NoError(t, err) + assert.NotNil(t, res) + // Import should return without error even if it can't extract anything. +} diff --git a/internal/adapters/crush/import.go b/internal/adapters/crush/import.go new file mode 100644 index 0000000..48e0a78 --- /dev/null +++ b/internal/adapters/crush/import.go @@ -0,0 +1,45 @@ +package crush + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" +) + +func importFrom(home string) (*adapter.ImportResult, error) { + cfgPath := filepath.Join(home, ".config", "crush", "crush.json") + body, err := os.ReadFile(cfgPath) + if os.IsNotExist(err) { + return &adapter.ImportResult{}, nil + } + if err != nil { + return nil, err + } + + var doc struct { + MCPServers map[string]struct { + Command string `json:"command"` + Args []string `json:"args"` + URL string `json:"url"` + Transport string `json:"transport"` + Env map[string]string `json:"env"` + } `json:"mcpServers"` + } + _ = json.Unmarshal(body, &doc) + + res := &adapter.ImportResult{} + for nm, v := range doc.MCPServers { + res.MCP = append(res.MCP, canonical.MCPServer{ + Name: nm, + Command: v.Command, + Args: v.Args, + URL: v.URL, + Transport: v.Transport, + Env: v.Env, + }) + } + return res, nil +} diff --git a/internal/adapters/kilo/import.go b/internal/adapters/kilo/import.go new file mode 100644 index 0000000..fb5fd6b --- /dev/null +++ b/internal/adapters/kilo/import.go @@ -0,0 +1,46 @@ +package kilo + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" +) + +func importFrom(home string) (*adapter.ImportResult, error) { + cfgPath := filepath.Join(home, ".config", "kilo", "kilo.json") + body, err := os.ReadFile(cfgPath) + if os.IsNotExist(err) { + return &adapter.ImportResult{}, nil + } + if err != nil { + return nil, err + } + + type mcpEntry struct { //nolint:govet // fieldalignment not achievable: map+slice+3 strings = 80 bytes regardless of order + Env map[string]string `json:"env"` + Args []string `json:"args"` + Command string `json:"command"` + URL string `json:"url"` + Transport string `json:"transport"` + } + var doc struct { + MCPServers map[string]mcpEntry `json:"mcpServers"` + } + _ = json.Unmarshal(body, &doc) + + res := &adapter.ImportResult{} + for nm, v := range doc.MCPServers { + res.MCP = append(res.MCP, canonical.MCPServer{ + Name: nm, + Command: v.Command, + Args: v.Args, + URL: v.URL, + Transport: v.Transport, + Env: v.Env, + }) + } + return res, nil +} diff --git a/internal/adapters/kilo/kilo.go b/internal/adapters/kilo/kilo.go new file mode 100644 index 0000000..e0e786f --- /dev/null +++ b/internal/adapters/kilo/kilo.go @@ -0,0 +1,143 @@ +// Package kilo is the kilo harness adapter. +package kilo + +import ( + "os" + "path/filepath" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" + "github.com/lukaszraczylo/harness-sync/internal/render" +) + +const name = "kilo" + +// Adapter implements adapter.Adapter for the kilo harness. +type Adapter struct { + home string +} + +// Option configures an Adapter. +type Option func(*Adapter) + +// WithHome overrides the home directory (defaults to os.UserHomeDir). +func WithHome(h string) Option { return func(a *Adapter) { a.home = h } } + +// New returns a new Adapter with the given options applied. +func New(opts ...Option) *Adapter { + a := &Adapter{home: defaultHome()} + for _, o := range opts { + o(a) + } + return a +} + +func defaultHome() string { + h, err := os.UserHomeDir() + if err != nil { + return "" + } + return h +} + +// Name returns the harness identifier. +func (a *Adapter) Name() string { return name } + +// Detect returns true when ~/.config/kilo exists. +func (a *Adapter) Detect() bool { + _, err := os.Stat(filepath.Join(a.home, ".config", "kilo")) + return err == nil +} + +// Render produces the FileSet that applies the canonical bundle to kilo. +func (a *Adapter) Render(b *canonical.Bundle) (*adapter.FileSet, error) { + fs := adapter.NewFileSet() + base := filepath.Join(a.home, ".config", "kilo") + + fs.Add(adapter.File{ + Dest: filepath.Join(base, "agent"), + Kind: adapter.SymlinkDir, + SymlinkTarget: filepath.Join(b.Root, "agents"), + }) + + cfg, err := renderConfig(b) + if err != nil { + return nil, err + } + fs.Add(adapter.File{ + Dest: filepath.Join(base, "kilo.json"), + Kind: adapter.RenderedFile, + Content: cfg, + }) + + return fs, nil +} + +func renderConfig(b *canonical.Bundle) ([]byte, error) { + providers := make([]map[string]any, 0) + if b.Profile.Gateway.URL != "" { + gw := map[string]any{ + "id": "harness-sync-gateway", + "name": "harness-sync gateway", + "base_url": b.Profile.Gateway.URL, + "api_key": b.Profile.Gateway.Token, + } + models := make([]map[string]any, 0, len(b.Profile.Models)) + for _, m := range b.Profile.Models { + entry := map[string]any{"id": m.ID} + if m.Alias != "" { + entry["alias"] = m.Alias + } + models = append(models, entry) + } + if len(models) > 0 { + gw["models"] = models + } + providers = append(providers, gw) + } + for _, up := range b.Profile.Upstreams { + p := map[string]any{"id": up.Name, "name": up.Name} + if up.BaseURL != "" { + p["base_url"] = up.BaseURL + } + if up.APIKey != "" { + p["api_key"] = up.APIKey + } + providers = append(providers, p) + } + + mcp := map[string]any{} + for _, s := range b.MCP.Servers { + entry := map[string]any{} + if s.Command != "" { + entry["command"] = s.Command + } + if len(s.Args) > 0 { + entry["args"] = s.Args + } + if s.URL != "" { + entry["url"] = s.URL + } + if s.Transport != "" { + entry["transport"] = s.Transport + } + if len(s.Env) > 0 { + entry["env"] = s.Env + } + mcp[s.Name] = entry + } + + out := map[string]any{ + "providers": providers, + "default_model": b.Profile.Gateway.DefaultModel, + } + if len(mcp) > 0 { + out["mcpServers"] = mcp + } + return render.JSON(out) +} + +// Import reads kilo config from home and returns a canonical ImportResult. +func (a *Adapter) Import(home string) (*adapter.ImportResult, error) { + return importFrom(home) +} diff --git a/internal/adapters/kilo/kilo_test.go b/internal/adapters/kilo/kilo_test.go new file mode 100644 index 0000000..1bc7cd1 --- /dev/null +++ b/internal/adapters/kilo/kilo_test.go @@ -0,0 +1,55 @@ +package kilo + +import ( + "encoding/json" + "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" +) + +func TestKiloRender(t *testing.T) { + home := t.TempDir() + ad := New(WithHome(home)) + b := &canonical.Bundle{ + Root: "/canon", + Profile: canonical.Profile{ + Name: "home", + Gateway: canonical.Gateway{URL: "https://gw", Token: "dummy", DefaultModel: "sonnet"}, + Models: []canonical.Model{{ID: "claude-sonnet-4-6", Alias: "sonnet"}}, + }, + } + fs, err := ad.Render(b) + require.NoError(t, err) + + seen := map[string]adapter.File{} + fs.ForEach(func(f adapter.File) { seen[f.Dest] = f }) + + agentDest := filepath.Join(home, ".config", "kilo", "agent") + assert.Equal(t, adapter.SymlinkDir, seen[agentDest].Kind) + assert.Equal(t, "/canon/agents", seen[agentDest].SymlinkTarget) + + cfgDest := filepath.Join(home, ".config", "kilo", "kilo.json") + assert.Equal(t, adapter.RenderedFile, seen[cfgDest].Kind) + + var parsed map[string]any + require.NoError(t, json.Unmarshal(seen[cfgDest].Content, &parsed)) +} + +func TestKiloImport(t *testing.T) { + home := t.TempDir() + base := filepath.Join(home, ".config", "kilo") + require.NoError(t, os.MkdirAll(base, 0o750)) + body, _ := json.Marshal(map[string]any{"providers": []map[string]any{{"id": "x"}}}) + require.NoError(t, os.WriteFile(filepath.Join(base, "kilo.json"), body, 0o600)) + + ad := New(WithHome(home)) + res, err := ad.Import(home) + require.NoError(t, err) + assert.NotNil(t, res) +} diff --git a/internal/adapters/opencode/import.go b/internal/adapters/opencode/import.go new file mode 100644 index 0000000..8bbbd47 --- /dev/null +++ b/internal/adapters/opencode/import.go @@ -0,0 +1,75 @@ +package opencode + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" +) + +func importFrom(home string) (*adapter.ImportResult, error) { + base := filepath.Join(home, ".config", "opencode") + res := &adapter.ImportResult{} + + cfgBody, err := readIfExists(filepath.Join(base, "opencode.jsonc")) + if err != nil { + return nil, err + } + if cfgBody != "" { + clean := stripJSONComments(cfgBody) + var doc struct { + MCPServers map[string]struct { //nolint:govet // fieldalignment: map+slice+3 strings irreducible + Env map[string]string `json:"env"` + Args []string `json:"args"` + Command string `json:"command"` + URL string `json:"url"` + Transport string `json:"transport"` + } `json:"mcpServers"` + } + _ = json.Unmarshal([]byte(clean), &doc) + for nm, v := range doc.MCPServers { + res.MCP = append(res.MCP, canonical.MCPServer{ + Name: nm, + Command: v.Command, + Args: v.Args, + URL: v.URL, + Transport: v.Transport, + Env: v.Env, + }) + } + } + + body, err := readIfExists(filepath.Join(base, "AGENTS.md")) + if err != nil { + return nil, err + } + res.Instructions = body + + return res, nil +} + +var ( + blockComment = regexp.MustCompile(`(?s)/\*.*?\*/`) + lineComment = regexp.MustCompile(`(?m)//[^\n]*`) +) + +func stripJSONComments(s string) string { + s = blockComment.ReplaceAllString(s, "") + s = lineComment.ReplaceAllString(s, "") + return strings.TrimSpace(s) +} + +func readIfExists(path string) (string, error) { + b, err := os.ReadFile(path) + if os.IsNotExist(err) { + return "", nil + } + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/internal/adapters/opencode/opencode.go b/internal/adapters/opencode/opencode.go new file mode 100644 index 0000000..9e5d8b2 --- /dev/null +++ b/internal/adapters/opencode/opencode.go @@ -0,0 +1,145 @@ +// Package opencode is the opencode harness adapter. +package opencode + +import ( + "os" + "path/filepath" + + "github.com/lukaszraczylo/harness-sync/internal/adapter" + "github.com/lukaszraczylo/harness-sync/internal/canonical" + "github.com/lukaszraczylo/harness-sync/internal/render" +) + +const name = "opencode" + +// Adapter implements adapter.Adapter for the opencode harness. +type Adapter struct{ home string } + +// Option configures an Adapter. +type Option func(*Adapter) + +// WithHome overrides the home directory (defaults to os.UserHomeDir). +func WithHome(h string) Option { return func(a *Adapter) { a.home = h } } + +// New returns a new Adapter with the given options applied. +func New(opts ...Option) *Adapter { + a := &Adapter{home: defaultHome()} + for _, o := range opts { + o(a) + } + return a +} + +func defaultHome() string { + h, err := os.UserHomeDir() + if err != nil { + return "" + } + return h +} + +// Name returns the harness identifier. +func (a *Adapter) Name() string { return name } + +// Detect returns true when ~/.config/opencode exists. +func (a *Adapter) Detect() bool { + _, err := os.Stat(filepath.Join(a.home, ".config", "opencode")) + return err == nil +} + +// Render produces the FileSet that applies the canonical bundle to opencode. +func (a *Adapter) Render(b *canonical.Bundle) (*adapter.FileSet, error) { + fs := adapter.NewFileSet() + base := filepath.Join(a.home, ".config", "opencode") + + cfg, err := renderConfig(b) + if err != nil { + return nil, err + } + fs.Add(adapter.File{ + Dest: filepath.Join(base, "opencode.jsonc"), + Kind: adapter.RenderedFile, + Content: cfg, + }) + + instructions := b.Instructions.Global + if override, ok := b.Instructions.PerHarness[name]; ok && override != "" { + instructions = override + } + fs.Add(adapter.File{ + Dest: filepath.Join(base, "AGENTS.md"), + Kind: adapter.RenderedFile, + Content: []byte(instructions), + }) + + return fs, nil +} + +func renderConfig(b *canonical.Bundle) ([]byte, error) { + providers := make([]map[string]any, 0) + if b.Profile.Gateway.URL != "" { + gw := map[string]any{ + "id": "harness-sync-gateway", + "name": "harness-sync gateway", + "base_url": b.Profile.Gateway.URL, + "api_key": b.Profile.Gateway.Token, + } + models := make([]map[string]any, 0, len(b.Profile.Models)) + for _, m := range b.Profile.Models { + entry := map[string]any{"id": m.ID} + if m.Alias != "" { + entry["alias"] = m.Alias + } + models = append(models, entry) + } + if len(models) > 0 { + gw["models"] = models + } + providers = append(providers, gw) + } + for _, up := range b.Profile.Upstreams { + p := map[string]any{"id": up.Name, "name": up.Name} + if up.BaseURL != "" { + p["base_url"] = up.BaseURL + } + if up.APIKey != "" { + p["api_key"] = up.APIKey + } + providers = append(providers, p) + } + + mcp := map[string]any{} + for _, s := range b.MCP.Servers { + entry := map[string]any{} + if s.Command != "" { + entry["command"] = s.Command + } + if len(s.Args) > 0 { + entry["args"] = s.Args + } + if s.URL != "" { + entry["url"] = s.URL + } + if s.Transport != "" { + entry["transport"] = s.Transport + } + if len(s.Env) > 0 { + entry["env"] = s.Env + } + mcp[s.Name] = entry + } + + out := map[string]any{ + "providers": providers, + "default_model": b.Profile.Gateway.DefaultModel, + } + if len(mcp) > 0 { + out["mcpServers"] = mcp + } + return render.JSON(out) +} + +// Import reads opencode config from home and returns a canonical ImportResult. +func (a *Adapter) Import(home string) (*adapter.ImportResult, error) { + return importFrom(home) +} diff --git a/internal/adapters/opencode/opencode_test.go b/internal/adapters/opencode/opencode_test.go new file mode 100644 index 0000000..cc537a2 --- /dev/null +++ b/internal/adapters/opencode/opencode_test.go @@ -0,0 +1,61 @@ +package opencode + +import ( + "encoding/json" + "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" +) + +func TestOpencodeRender(t *testing.T) { + home := t.TempDir() + ad := New(WithHome(home)) + b := &canonical.Bundle{ + Root: "/canon", + Profile: canonical.Profile{ + Gateway: canonical.Gateway{URL: "https://gw", Token: "dummy", DefaultModel: "sonnet"}, + Models: []canonical.Model{{ID: "claude-sonnet-4-6", Alias: "sonnet"}}, + }, + Instructions: canonical.Instructions{Global: "# global"}, + } + fs, err := ad.Render(b) + require.NoError(t, err) + + seen := map[string]adapter.File{} + fs.ForEach(func(f adapter.File) { seen[f.Dest] = f }) + + cfgDest := filepath.Join(home, ".config", "opencode", "opencode.jsonc") + assert.Equal(t, adapter.RenderedFile, seen[cfgDest].Kind) + var parsed map[string]any + require.NoError(t, json.Unmarshal(seen[cfgDest].Content, &parsed)) + + agentsDest := filepath.Join(home, ".config", "opencode", "AGENTS.md") + assert.Equal(t, adapter.RenderedFile, seen[agentsDest].Kind) + assert.Contains(t, string(seen[agentsDest].Content), "# global") +} + +func TestOpencodeImportStripsComments(t *testing.T) { + home := t.TempDir() + base := filepath.Join(home, ".config", "opencode") + require.NoError(t, os.MkdirAll(base, 0o750)) + jsonc := `// header comment +{ + "providers": [{"id": "anthropic"}], // trailing + /* block + comment */ + "default_model": "sonnet" +}` + require.NoError(t, os.WriteFile(filepath.Join(base, "opencode.jsonc"), []byte(jsonc), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(base, "AGENTS.md"), []byte("# global"), 0o600)) + + ad := New(WithHome(home)) + res, err := ad.Import(home) + require.NoError(t, err) + assert.Equal(t, "# global", res.Instructions) +}