package skillstore import ( "encoding/json" "errors" "os" "path/filepath" "strings" "sgg-ai-skill-manager/internal/domain" ) type Store struct { statePath string reposRoot string } func New(statePath, reposRoot string) *Store { return &Store{statePath: statePath, reposRoot: reposRoot} } func (s *Store) Load() (domain.State, error) { state := domain.State{Skills: []domain.SkillState{}} raw, err := os.ReadFile(s.statePath) if errors.Is(err, os.ErrNotExist) { return state, nil } if err != nil { return state, err } if strings.TrimSpace(string(raw)) == "" { return state, nil } if err := json.Unmarshal(raw, &state); err != nil { return state, err } if state.Skills == nil { state.Skills = []domain.SkillState{} } return state, nil } func (s *Store) Save(state domain.State) error { if state.Skills == nil { state.Skills = []domain.SkillState{} } if err := os.MkdirAll(filepath.Dir(s.statePath), 0o755); err != nil { return err } raw, err := json.MarshalIndent(state, "", " ") if err != nil { return err } return os.WriteFile(s.statePath, append(raw, '\n'), 0o600) } func (s *Store) LocalRepoPath(org, repo string) string { return filepath.Join(s.reposRoot, org, repo) } func (s *Store) SafeLocalRepoPath(org, repo string) (string, error) { if err := validateName(org); err != nil { return "", err } if err := validateName(repo); err != nil { return "", err } return s.LocalRepoPath(org, repo), nil } func UpsertSkill(state domain.State, skill domain.SkillState) domain.State { for i, existing := range state.Skills { if existing.Org == skill.Org && existing.Repo == skill.Repo { state.Skills[i] = skill return state } } state.Skills = append(state.Skills, skill) return state } func RemoveSkill(state domain.State, org, repo string) domain.State { next := state.Skills[:0] for _, skill := range state.Skills { if skill.Org == org && skill.Repo == repo { continue } next = append(next, skill) } state.Skills = next return state } func FindSkill(state domain.State, org, repo string) (domain.SkillState, bool) { for _, skill := range state.Skills { if skill.Org == org && skill.Repo == repo { return skill, true } } return domain.SkillState{}, false } func validateName(name string) error { if strings.TrimSpace(name) == "" { return errors.New("name is required") } if name == "." || name == ".." { return errors.New("name cannot be . or ..") } if strings.ContainsAny(name, `/\`) { return errors.New("name cannot contain path separators") } return nil }