Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion control-plane/internal/skillkit/skillkit_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ func TestTargetSpecificEdgeCases(t *testing.T) {
{name: "aider", target: aiderTarget{}, path: filepath.Join(home, ".aider.conventions.md")},
{name: "codex", target: codexTarget{}, dir: filepath.Join(home, ".codex"), path: filepath.Join(home, ".codex", "AGENTS.override.md")},
{name: "gemini", target: geminiTarget{}, dir: filepath.Join(home, ".gemini"), path: filepath.Join(home, ".gemini", "GEMINI.md")},
{name: "opencode", target: opencodeTarget{}, dir: filepath.Join(home, ".config", "opencode"), path: filepath.Join(home, ".config", "opencode", "AGENTS.md")},
{name: "opencode", target: opencodeTarget{}, dir: filepath.Join(home, ".config", "opencode", "skills"), path: filepath.Join(home, ".config", "opencode", "skills", "agentfield")},
{name: "windsurf", target: windsurfTarget{}, dir: filepath.Join(home, ".codeium", "windsurf", "memories"), path: filepath.Join(home, ".codeium", "windsurf", "memories", "global_rules.md")},
}

Expand Down
2 changes: 1 addition & 1 deletion control-plane/internal/skillkit/skillkit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func TestHelpersAndTargets(t *testing.T) {
t.Fatalf("mkdir opencode dir: %v", err)
}
opencode := opencodeTarget{}
if opencode.DisplayName() != "OpenCode" || opencode.Method() != "marker-block" {
if opencode.DisplayName() != "OpenCode" || opencode.Method() != "symlink" {
t.Fatalf("unexpected opencode metadata: %q %q", opencode.DisplayName(), opencode.Method())
}
if !opencode.Detected() {
Expand Down
72 changes: 54 additions & 18 deletions control-plane/internal/skillkit/target_opencode.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ package skillkit

import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
)

// opencodeTarget installs into OpenCode by appending a marker block to
// ~/.config/opencode/AGENTS.md.
// opencodeTarget installs skills where OpenCode discovers them natively.
type opencodeTarget struct{}

func init() { RegisterTarget(opencodeTarget{}) }

func (opencodeTarget) Name() string { return "opencode" }
func (opencodeTarget) DisplayName() string { return "OpenCode" }
func (opencodeTarget) Method() string { return "marker-block" }
func (opencodeTarget) Method() string { return "symlink" }

func (opencodeTarget) Detected() bool {
return commandAvailable("opencode") || dirExists(filepath.Join(homeDir(), ".config", "opencode"))
Expand All @@ -24,43 +26,77 @@ func (opencodeTarget) TargetPath() (string, error) {
if h == "" {
return "", errors.New("could not resolve home directory")
}
return filepath.Join(h, ".config", "opencode", "AGENTS.md"), nil
return filepath.Join(h, ".config", "opencode", "skills"), nil
}

func (t opencodeTarget) skillLink(skill Skill) (string, error) {
root, err := t.TargetPath()
if err != nil {
return "", err
}
return filepath.Join(root, skill.Name), nil
}

func (t opencodeTarget) Install(skill Skill, canonicalCurrentDir string) (InstalledTarget, error) {
path, err := t.TargetPath()
root, err := t.TargetPath()
if err != nil {
return InstalledTarget{}, err
}
inst, err := installMarkerBlock(skill, canonicalCurrentDir, path)
if err := os.MkdirAll(root, 0o755); err != nil {
return InstalledTarget{}, fmt.Errorf("create %s: %w", root, err)
}
link, err := t.skillLink(skill)
if err != nil {
return InstalledTarget{}, err
}
inst.TargetName = t.Name()
return inst, nil
if info, err := os.Lstat(link); err == nil {
if info.Mode()&os.ModeSymlink != 0 || info.IsDir() || info.Mode().IsRegular() {
if err := os.RemoveAll(link); err != nil {
return InstalledTarget{}, fmt.Errorf("remove existing %s: %w", link, err)
}
}
} else if !os.IsNotExist(err) {
return InstalledTarget{}, fmt.Errorf("inspect %s: %w", link, err)
}
if err := os.Symlink(canonicalCurrentDir, link); err != nil {
return InstalledTarget{}, fmt.Errorf("symlink %s -> %s: %w", link, canonicalCurrentDir, err)
}
return InstalledTarget{TargetName: t.Name(), Method: t.Method(), Path: link, Version: skill.Version, InstalledAt: time.Now().UTC()}, nil
}

func (t opencodeTarget) Uninstall() error {
path, err := t.TargetPath()
if err != nil {
return err
}
for _, s := range Catalog {
if err := uninstallMarkerBlock(s, path); err != nil {
return err
link, err := t.skillLink(s)
if err != nil {
continue
Comment thread
AbirAbbas marked this conversation as resolved.
Outdated
}
if info, err := os.Lstat(link); err == nil && (info.Mode()&os.ModeSymlink != 0 || info.IsDir() || info.Mode().IsRegular()) {
if err := os.RemoveAll(link); err != nil {
return fmt.Errorf("remove %s: %w", link, err)
}
}
}
return nil
}

func (t opencodeTarget) Status() (bool, string, error) {
path, err := t.TargetPath()
link, err := t.skillLink(Catalog[0])
if err != nil {
return false, "", err
}
v := readMarkerVersion(Catalog[0], path)
if v == "" {
info, err := os.Lstat(link)
if os.IsNotExist(err) {
return false, "", nil
}
return true, v, nil
if err != nil {
return false, "", err
}
if info.Mode()&os.ModeSymlink == 0 {
return true, "manual", nil
}
dest, err := os.Readlink(link)
if err != nil {
return false, "", err
}
return true, filepath.Base(dest), nil
Comment thread
AbirAbbas marked this conversation as resolved.
Outdated
}
35 changes: 35 additions & 0 deletions control-plane/internal/skillkit/target_opencode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package skillkit

import (
"os"
"path/filepath"
"testing"
)

func TestOpenCodeTargetInstallsSkillSymlink(t *testing.T) {
home := withTempHome(t)
t.Setenv("USERPROFILE", home)
canonical := filepath.Join(home, ".agentfield", "skills", "agentfield", "current")
if err := os.MkdirAll(canonical, 0o755); err != nil {
t.Fatal(err)
}
target := opencodeTarget{}
installed, err := target.Install(Skill{Name: "agentfield", Version: "1.2.3"}, canonical)
if err != nil {
t.Fatal(err)
}
want := filepath.Join(home, ".config", "opencode", "skills", "agentfield")
if installed.Method != "symlink" || installed.Path != want {
t.Fatalf("installed target = %#v", installed)
}
got, err := os.Readlink(want)
if err != nil || got != canonical {
t.Fatalf("OpenCode link = %q, %v; want %q", got, err, canonical)
}
if err := target.Uninstall(); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(want); !os.IsNotExist(err) {
t.Fatalf("skill link still exists after uninstall: %v", err)
}
}
Loading