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
+31 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"strings"
@@ -51,6 +52,7 @@ func Add(workDir string, opts AddOptions, stdout io.Writer) error {
if err != nil {
return err
}
cfg := discoveryFromTreeRoot(root)
repo, err := gitsource.Clone(normalized, opts.Ref)
if err != nil {
@@ -61,7 +63,6 @@ func Add(workDir string, opts AddOptions, stdout io.Writer) error {
return err
}
cfg := lockfile.Discovery{Root: root}
items, err := discovery.Scan(tree, discovery.FromLockfileDiscovery(cfg))
if err != nil {
return err
@@ -117,6 +118,35 @@ func Add(workDir string, opts AddOptions, stdout io.Writer) error {
return nil
}
// discoveryFromTreeRoot maps a pasted tree sub-path onto discovery config.
// A sub-path ending in a standard type directory (e.g. ".../tree/main/skills")
// selects that directory as the type root; anything else becomes the
// discovery root under which the standard layout is expected.
func discoveryFromTreeRoot(root string) lockfile.Discovery {
if root == "" {
return lockfile.Discovery{}
}
parent, base := path.Split(strings.TrimSuffix(root, "/"))
parent = strings.TrimSuffix(parent, "/")
d := lockfile.Discovery{Root: parent}
switch base {
case lockfile.TypeSkill:
d.SkillsDir = base
case lockfile.TypeCommand:
d.CommandsDir = base
case lockfile.TypeAgent:
d.AgentsDir = base
case lockfile.TypeMcp:
d.McpDir = base
default:
return lockfile.Discovery{Root: root}
}
if d == (lockfile.Discovery{}) {
return lockfile.Discovery{}
}
return d
}
func checkIncludeEntries(items []discovery.Item, include []string, name string) error {
available := map[string]bool{}
for _, it := range items {