This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-9p/fs/skill.go

169 lines
4.1 KiB
Go

package fs
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"ollie/pkg/paths"
"ollie/pkg/skills"
)
// NewSkillTree returns a Tree configured for the skill directory layout.
// Skills are directories containing SKILL.md; virtual names are "skillname.md".
func NewSkillTree() *Tree {
return NewSkillTreeWith(SkillConfig{})
}
// SkillConfig holds optional dependencies for NewSkillTreeWith.
type SkillConfig 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 NewSkillTreeWith(cfg SkillConfig) *Tree {
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 NewTree(cfg.Dirs, Perms[PathSkills].FileMode,
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 SkillConfig
}
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", Perms[PathSkills].FileMode))
}
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, PermMkdir); err != nil {
return err
}
return ss.cfg.WriteFile(filepath.Join(dir, "SKILL.md"), nil, Perms[PathSkills].FileMode)
}
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(_ FileTree) ([]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
}