169 lines
4.1 KiB
Go
169 lines
4.1 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"ollie/pkg/paths"
|
|
"ollie/pkg/skills"
|
|
)
|
|
|
|
// NewSkillStore returns a DirStore configured for the skill directory layout.
|
|
// Skills are directories containing SKILL.md; virtual names are "skillname.md".
|
|
func NewSkillStore() *DirStore {
|
|
return NewSkillStoreWith(SkillStoreConfig{})
|
|
}
|
|
|
|
// SkillStoreConfig holds optional dependencies for NewSkillStoreWith.
|
|
type SkillStoreConfig struct {
|
|
Dirs []string
|
|
ReadDir func(string) ([]os.DirEntry, error)
|
|
Open func(string) (*os.File, error)
|
|
ReadFile func(string) ([]byte, error)
|
|
WriteFile func(string, []byte, os.FileMode) error
|
|
MkdirAll func(string, os.FileMode) error
|
|
RemoveAll func(string) error
|
|
Rename func(string, string) error
|
|
}
|
|
|
|
func NewSkillStoreWith(cfg SkillStoreConfig) *DirStore {
|
|
if cfg.Dirs == nil {
|
|
cfg.Dirs = skillDirs()
|
|
}
|
|
if cfg.ReadDir == nil {
|
|
cfg.ReadDir = os.ReadDir
|
|
}
|
|
if cfg.Open == nil {
|
|
cfg.Open = os.Open
|
|
}
|
|
if cfg.ReadFile == nil {
|
|
cfg.ReadFile = os.ReadFile
|
|
}
|
|
if cfg.WriteFile == nil {
|
|
cfg.WriteFile = os.WriteFile
|
|
}
|
|
if cfg.MkdirAll == nil {
|
|
cfg.MkdirAll = os.MkdirAll
|
|
}
|
|
if cfg.RemoveAll == nil {
|
|
cfg.RemoveAll = os.RemoveAll
|
|
}
|
|
if cfg.Rename == nil {
|
|
cfg.Rename = os.Rename
|
|
}
|
|
|
|
ss := &skillHelper{cfg: cfg}
|
|
|
|
return NewDirStore(cfg.Dirs, 0644,
|
|
WithIndex(ss.index),
|
|
WithResolver(ss.resolve),
|
|
WithLister(ss.list),
|
|
WithCreator(ss.create),
|
|
WithDeleter(ss.del),
|
|
WithRenamer(ss.rename),
|
|
)
|
|
}
|
|
|
|
func skillDirs() []string {
|
|
if env := os.Getenv("OLLIE_SKILLS_PATH"); env != "" {
|
|
return strings.Split(env, ":")
|
|
}
|
|
return []string{paths.CfgDir() + "/skills"}
|
|
}
|
|
|
|
type skillHelper struct {
|
|
cfg SkillStoreConfig
|
|
}
|
|
|
|
func (ss *skillHelper) listSkills() []skills.Meta {
|
|
seen := make(map[string]bool)
|
|
var result []skills.Meta
|
|
for _, dir := range ss.cfg.Dirs {
|
|
entries, err := ss.cfg.ReadDir(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() || seen[e.Name()] {
|
|
continue
|
|
}
|
|
skillDir := filepath.Join(dir, e.Name())
|
|
f, err := ss.cfg.Open(filepath.Join(skillDir, "SKILL.md"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
meta, err := skills.ParseFrontMatter(f, filepath.Base(skillDir), skillDir)
|
|
f.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
seen[meta.Name] = true
|
|
result = append(result, *meta)
|
|
}
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
|
return result
|
|
}
|
|
|
|
func (ss *skillHelper) resolve(_ []string, name string) (string, error) {
|
|
skillName := strings.TrimSuffix(name, ".md")
|
|
for _, m := range ss.listSkills() {
|
|
if m.Name == skillName {
|
|
return filepath.Join(m.Dir, "SKILL.md"), nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("%s: not found", name)
|
|
}
|
|
|
|
func (ss *skillHelper) list(_ []string) ([]os.DirEntry, error) {
|
|
var result []os.DirEntry
|
|
for _, m := range ss.listSkills() {
|
|
result = append(result, FileEntry(m.Name+".md", 0666))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (ss *skillHelper) create(_ []string, name string, _ os.FileMode) error {
|
|
skillName := strings.TrimSuffix(name, ".md")
|
|
dir := filepath.Join(ss.cfg.Dirs[0], skillName)
|
|
if err := ss.cfg.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
return ss.cfg.WriteFile(filepath.Join(dir, "SKILL.md"), nil, 0644)
|
|
}
|
|
|
|
func (ss *skillHelper) del(_ []string, name string) error {
|
|
skillName := strings.TrimSuffix(name, ".md")
|
|
for _, m := range ss.listSkills() {
|
|
if m.Name == skillName {
|
|
return ss.cfg.RemoveAll(m.Dir)
|
|
}
|
|
}
|
|
return fmt.Errorf("skill not found: %s", skillName)
|
|
}
|
|
|
|
func (ss *skillHelper) rename(_ []string, old, new string) error {
|
|
oldSkill := strings.TrimSuffix(old, ".md")
|
|
newSkill := strings.TrimSuffix(new, ".md")
|
|
for _, m := range ss.listSkills() {
|
|
if m.Name == oldSkill {
|
|
newDir := filepath.Join(filepath.Dir(m.Dir), newSkill)
|
|
return ss.cfg.Rename(m.Dir, newDir)
|
|
}
|
|
}
|
|
return fmt.Errorf("skill not found: %s", oldSkill)
|
|
}
|
|
|
|
func (ss *skillHelper) index(_ Store) ([]byte, error) {
|
|
var sb strings.Builder
|
|
for _, m := range ss.listSkills() {
|
|
fmt.Fprintf(&sb, "## %s\n", m.Name)
|
|
fmt.Fprintf(&sb, "description: %s\n", m.Description)
|
|
sb.WriteString("\n")
|
|
}
|
|
return []byte(sb.String()), nil
|
|
}
|