package targets import ( "errors" "fmt" "os" "path/filepath" "sgg-ai-skill-manager/internal/domain" ) type Target struct { ID string Name string SkillsDir string } type Manager struct { Targets map[string]Target } func New(targets map[string]Target) *Manager { return &Manager{Targets: targets} } func DefaultTargets(home string) map[string]Target { return map[string]Target{ "codex": { ID: "codex", Name: "Codex", SkillsDir: filepath.Join(home, ".codex", "skills"), }, "claude": { ID: "claude", Name: "Claude", SkillsDir: filepath.Join(home, ".claude", "skills"), }, } } func (m *Manager) Install(skill domain.SkillState, targetID string) (domain.SkillState, error) { target, ok := m.Targets[targetID] if !ok { return skill, fmt.Errorf("unknown target %s", targetID) } if _, err := os.Stat(skill.LocalPath); err != nil { return skill, fmt.Errorf("local skill path is not accessible: %w", err) } if skill.InstalledTargets == nil { skill.InstalledTargets = map[string]domain.InstalledTarget{} } if err := os.MkdirAll(target.SkillsDir, 0o755); err != nil { return skill, err } linkPath := filepath.Join(target.SkillsDir, skill.Repo) if _, err := os.Lstat(linkPath); err == nil { actual, readErr := ReadJunctionTarget(linkPath) if readErr == nil && samePath(actual, skill.LocalPath) { skill.InstalledTargets[targetID] = installedTarget(linkPath, skill.LocalPath) return skill, nil } return skill, fmt.Errorf("target path conflict: %s", linkPath) } else if !errors.Is(err, os.ErrNotExist) { return skill, err } if err := CreateJunction(linkPath, skill.LocalPath); err != nil { return skill, err } skill.InstalledTargets[targetID] = installedTarget(linkPath, skill.LocalPath) return skill, nil } func (m *Manager) Uninstall(skill domain.SkillState, targetID string) (domain.SkillState, error) { record, ok := skill.InstalledTargets[targetID] if !ok { return skill, nil } actual, err := ReadJunctionTarget(record.Path) if err != nil { return skill, fmt.Errorf("cannot verify junction target: %w", err) } if !samePath(actual, record.TargetPath) || !samePath(actual, skill.LocalPath) { return skill, fmt.Errorf("junction target mismatch: %s", record.Path) } if err := RemoveJunction(record.Path); err != nil { return skill, err } delete(skill.InstalledTargets, targetID) return skill, nil } func (m *Manager) Status(skill domain.SkillState, targetID string) string { target, ok := m.Targets[targetID] if !ok { return "unknown" } linkPath := filepath.Join(target.SkillsDir, skill.Repo) actual, err := ReadJunctionTarget(linkPath) if errors.Is(err, os.ErrNotExist) { return "not_installed" } if err != nil { return "conflict" } if samePath(actual, skill.LocalPath) { return "installed" } return "conflict" } func installedTarget(linkPath, targetPath string) domain.InstalledTarget { return domain.InstalledTarget{ Path: linkPath, LinkType: "junction", TargetPath: targetPath, } }