110 lines
2.4 KiB
Go
110 lines
2.4 KiB
Go
package skill
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Registry holds loaded skills keyed by name.
|
|
// Thread-safe; safe for concurrent Get while Load methods run serially.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
skills map[string]*Skill
|
|
}
|
|
|
|
// NewRegistry returns an empty Registry.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{skills: make(map[string]*Skill)}
|
|
}
|
|
|
|
// LoadBundled loads all skills embedded in the binary.
|
|
// Later calls to LoadDir or LoadBundled can override bundled skills by name.
|
|
func (r *Registry) LoadBundled() error {
|
|
skills, err := BundledSkills()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for _, s := range skills {
|
|
r.skills[s.Frontmatter.Name] = s
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LoadDir scans dir for *.md files and loads each as a skill with the given source tag.
|
|
// Non-existent directories are silently skipped.
|
|
// Skills loaded later override earlier ones with the same name.
|
|
func (r *Registry) LoadDir(dir, source string) error {
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
|
|
continue
|
|
}
|
|
path := filepath.Join(dir, entry.Name())
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s, err := Parse(data, source)
|
|
if err != nil {
|
|
// Skip unparseable files rather than aborting the whole load.
|
|
continue
|
|
}
|
|
s.FilePath = path
|
|
r.skills[s.Frontmatter.Name] = s
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get returns the skill with the given name, or nil if not found.
|
|
func (r *Registry) Get(name string) *Skill {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.skills[name]
|
|
}
|
|
|
|
// Names returns all skill names in sorted order.
|
|
func (r *Registry) Names() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
names := make([]string, 0, len(r.skills))
|
|
for name := range r.skills {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// All returns all skills sorted by name.
|
|
func (r *Registry) All() []*Skill {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
skills := make([]*Skill, 0, len(r.skills))
|
|
for _, s := range r.skills {
|
|
skills = append(skills, s)
|
|
}
|
|
sort.Slice(skills, func(i, j int) bool {
|
|
return skills[i].Frontmatter.Name < skills[j].Frontmatter.Name
|
|
})
|
|
return skills
|
|
}
|