feat: three-state sync planner with warning semantics

- 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
This commit is contained in:
2026-08-23 10:36:44 +02:00
parent 49dbcf469e
commit 63619b90a4
7 changed files with 751 additions and 101 deletions
+30
View File
@@ -6,6 +6,7 @@ package target
import (
"os"
"path/filepath"
"strings"
"github.com/m3tam3re/agent-lib/internal/lockfile"
)
@@ -39,3 +40,32 @@ func (o *OpenCode) DeployPath(typ, name string) (path string, ok bool) {
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
}