Files
gnoma/internal/skill/registry.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
}