feat: recursive skill discovery + tree-url type-root folding

- skills are found at any depth under the skills dir, so category-nested
  layouts (skills/<category>/<id>/SKILL.md) vendor correctly; the folder
  basename is the upstream id and the outermost SKILL.md wins over anything
  nested inside a skill folder
- web-tree urls pointing directly at a type directory (.../tree/main/skills)
  now map that directory as the type root instead of nesting it under the
  default layout
- verified against the real github.com/mattpocock/skills repository
  (36 skills across categories; include-selection by basename)
This commit is contained in:
2026-08-23 14:50:56 +02:00
parent 67cd4e4737
commit 1b2fcb6fbb
5 changed files with 165 additions and 16 deletions
+52 -12
View File
@@ -57,6 +57,7 @@ func Scan(tree Tree, cfg Config) ([]Item, error) {
dirEntries[d] = map[string]bool{}
fileEntries[d] = map[string]bool{}
}
var skillDescendants []string
err := tree.Walk(func(rel string, isDir bool) error {
parent, base := splitPath(rel)
@@ -64,6 +65,9 @@ func Scan(tree Tree, cfg Config) ([]Item, error) {
if set, ok := dirEntries[parent]; ok {
set[base] = true
}
if skillRoot != "" && (parent == skillRoot || strings.HasPrefix(parent, skillRoot+"/")) {
skillDescendants = append(skillDescendants, rel)
}
return nil
}
if set, ok := fileEntries[parent]; ok {
@@ -76,18 +80,7 @@ func Scan(tree Tree, cfg Config) ([]Item, error) {
}
var items []Item
for name := range dirEntries[skillRoot] {
skillMd := join(skillRoot, name, "SKILL.md")
data, err := tree.ReadFile(skillMd)
if err != nil {
continue
}
it := Item{Type: lockfile.TypeSkill, UpstreamID: name, RelPath: join(skillRoot, name)}
fm, warnings := ParseFrontmatter(data)
it.Frontmatter = &fm
for _, w := range warnings {
it.Warnings = append(it.Warnings, fmt.Sprintf("skills/%s: %s", name, w))
}
for _, it := range scanSkills(tree, skillRoot, skillDescendants) {
items = append(items, it)
}
items = append(items, scanFlatFiles(tree, fileEntries, commandRoot, lockfile.TypeCommand)...)
@@ -103,6 +96,53 @@ func Scan(tree Tree, cfg Config) ([]Item, error) {
return items, nil
}
// scanSkills finds skill folders at any depth under skillRoot (flat layouts
// and category-nested layouts alike). The upstream id is the folder's base
// name; a directory that is itself a skill wins over anything nested inside
// it, and same-basename folders collide at the vendor layer.
func scanSkills(tree Tree, skillRoot string, descendants []string) []Item {
if skillRoot == "" {
return nil
}
sorted := append([]string(nil), descendants...)
sort.Strings(sorted)
withSkillMd := map[string]bool{}
for _, d := range sorted {
if _, err := tree.ReadFile(join(d, "SKILL.md")); err == nil {
withSkillMd[d] = true
}
}
var items []Item
for _, d := range sorted {
if !withSkillMd[d] {
continue
}
nested := false
for other := range withSkillMd {
if other != d && strings.HasPrefix(d, other+"/") {
nested = true
break
}
}
if nested {
continue
}
data, err := tree.ReadFile(join(d, "SKILL.md"))
if err != nil {
continue
}
name := d[strings.LastIndex(d, "/")+1:]
it := Item{Type: lockfile.TypeSkill, UpstreamID: name, RelPath: d}
fm, warnings := ParseFrontmatter(data)
it.Frontmatter = &fm
for _, w := range warnings {
it.Warnings = append(it.Warnings, fmt.Sprintf("skills/%s: %s", name, w))
}
items = append(items, it)
}
return items
}
func scanFlatFiles(tree Tree, fileEntries map[string]map[string]bool, root, typ string) []Item {
var items []Item
for base := range fileEntries[root] {