- Plan is a pure function (inventory, manifest, disk facts, injected path mapping) emitting exactly six actions: deploy, update, skip+warn, leave+warn, remove, keep — deterministic order, no filesystem access - full 3x3 matrix (disk unmodified/user-modified/never-managed x upstream added/changed/removed) table-tested plus missing-disk restore cases, a mixed fixture and a purity/determinism guarantee test - executor applies plans item by item (atomic deploy/remove per item), rewrites the manifest once, appends timestamped SKIP/LEAVE lines with item name and reason to the sync log - sync now plans before mutating: unmanaged occupants block new deploys, upstream deletions remove only unmodified copies, local modifications always win and are warned about
72 lines
2.1 KiB
Go
72 lines
2.1 KiB
Go
// Package target maps content types to runtime-specific deployment paths.
|
|
// The OpenCode target is the thin v2 implementation: a type filter plus
|
|
// path mapping, leaving the door open for future targets.
|
|
package target
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/m3tam3re/agent-lib/internal/lockfile"
|
|
)
|
|
|
|
// OpenCode maps items into the OpenCode runtime layout, rooted at the
|
|
// user's home directory.
|
|
type OpenCode struct {
|
|
Home string
|
|
}
|
|
|
|
// NewOpenCode resolves the home directory once for all path mappings.
|
|
func NewOpenCode() (*OpenCode, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &OpenCode{Home: home}, nil
|
|
}
|
|
|
|
// DeployPath returns the absolute destination for an item; ok is false for
|
|
// types this target never deploys (MCP stays under fleet control).
|
|
func (o *OpenCode) DeployPath(typ, name string) (path string, ok bool) {
|
|
switch typ {
|
|
case lockfile.TypeSkill:
|
|
return filepath.Join(o.Home, ".agents", "skills", name), true
|
|
case lockfile.TypeCommand:
|
|
return filepath.Join(o.Home, ".config", "opencode", "commands", name+".md"), true
|
|
case lockfile.TypeAgent:
|
|
return filepath.Join(o.Home, ".config", "opencode", "agents", name+".md"), true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
// ScanTargetDirs lists every item currently present in the target dirs,
|
|
// keyed "<type>/<name>" — the raw material for detecting unmanaged occupants.
|
|
func (o *OpenCode) ScanTargetDirs() map[string]bool {
|
|
out := map[string]bool{}
|
|
skillsDir := filepath.Join(o.Home, ".agents", "skills")
|
|
if entries, err := os.ReadDir(skillsDir); err == nil {
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
out[lockfile.TypeSkill+"/"+e.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
for typ, dir := range map[string]string{
|
|
lockfile.TypeCommand: filepath.Join(o.Home, ".config", "opencode", "commands"),
|
|
lockfile.TypeAgent: filepath.Join(o.Home, ".config", "opencode", "agents"),
|
|
} {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".md") {
|
|
out[typ+"/"+strings.TrimSuffix(e.Name(), ".md")] = true
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|