package main import ( "bufio" "bytes" "fmt" "io/fs" "os" "path/filepath" "regexp" "sort" "strings" ) type Skill struct { Name string // Path is the skill's directory relative to the source root. Path string Description string } var validName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // discover finds every skill in root: a directory holding a SKILL.md. It does // not look for skills nested inside another skill. func discover(root string) ([]Skill, error) { var skills []Skill seen := map[string]string{} err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() { return nil } if path != root && (strings.HasPrefix(d.Name(), ".") || d.Name() == "node_modules") { return filepath.SkipDir } data, err := os.ReadFile(filepath.Join(path, "SKILL.md")) if err != nil { return nil } rel, _ := filepath.Rel(root, path) fm := frontmatter(data) name := fm["name"] if name == "" { name = filepath.Base(path) } if !validName.MatchString(name) { return fmt.Errorf("%s/SKILL.md: invalid skill name %q", rel, name) } if prev, ok := seen[name]; ok { return fmt.Errorf("skill %q is defined twice, in %s and %s", name, prev, rel) } seen[name] = rel skills = append(skills, Skill{Name: name, Path: filepath.ToSlash(rel), Description: fm["description"]}) return filepath.SkipDir }) sort.Slice(skills, func(i, j int) bool { return skills[i].Name < skills[j].Name }) return skills, err } // frontmatter reads the top-level scalar keys of a SKILL.md's YAML front // matter. That is all skills need from it, so it skips a YAML dependency. func frontmatter(data []byte) map[string]string { fm := map[string]string{} sc := bufio.NewScanner(bytes.NewReader(data)) if !sc.Scan() || strings.TrimSpace(sc.Text()) != "---" { return fm } for sc.Scan() { line := sc.Text() if strings.TrimSpace(line) == "---" { break } if line == "" || line[0] == ' ' || line[0] == '\t' { continue } k, v, ok := strings.Cut(line, ":") if !ok { continue } v = strings.TrimSpace(v) if len(v) >= 2 && (v[0] == '"' || v[0] == '\'') && v[len(v)-1] == v[0] { v = v[1 : len(v)-1] } fm[strings.TrimSpace(k)] = v } return fm } func selectSkills(found []Skill, only []string) ([]Skill, error) { if len(only) == 0 { return found, nil } byName := map[string]Skill{} for _, s := range found { byName[s.Name] = s } var out []Skill for _, n := range only { s, ok := byName[n] if !ok { return nil, fmt.Errorf("no skill named %q (found: %s)", n, skillNames(found)) } out = append(out, s) } return out, nil } func skillNames(skills []Skill) string { names := make([]string, len(skills)) for i, s := range skills { names[i] = s.Name } return strings.Join(names, ", ") } func skillPaths(skills []Skill) map[string]string { m := make(map[string]string, len(skills)) for _, s := range skills { m[s.Name] = s.Path } return m }