diff --git a/skills/skills.go b/skills/skills.go index 6fb922d31ee85bc857c3f15944aa406f549c88e3..99a84c21648f4c49d9192fc455c7db03d8a435f0 100644 --- a/skills/skills.go +++ b/skills/skills.go @@ -41,11 +41,12 @@ func Discover(dirs []string) []Skill { } for _, entry := range entries { - if !entry.IsDir() { + skillDir := filepath.Join(dir, entry.Name()) + // entry.IsDir() returns false for symlinks, even to directories; + // os.Stat follows symlinks so we can detect symlinked skill dirs + if info, err := os.Stat(skillDir); err != nil || !info.IsDir() { continue } - - skillDir := filepath.Join(dir, entry.Name()) skillMD := findSkillMD(skillDir) if skillMD == "" { continue diff --git a/skills/skills_test.go b/skills/skills_test.go index bcc65e7986e21b936e6b0569fbd5b4b60b78f85f..f5bf6ca6a5fd26cf778528a2a2b5f47d4e121202 100644 --- a/skills/skills_test.go +++ b/skills/skills_test.go @@ -560,3 +560,46 @@ func TestSkillsFoundRegardlessOfWorkingDir(t *testing.T) { _ = projectDir // used above } + +func TestDiscoverFollowsSymlinks(t *testing.T) { + tmpDir := t.TempDir() + + // Create a real skill directory (the symlink target) + realSkillDir := filepath.Join(tmpDir, "real-skills", "my-skill") + if err := os.MkdirAll(realSkillDir, 0o755); err != nil { + t.Fatal(err) + } + skillContent := `--- +name: my-skill +description: A symlinked skill. +--- + +Test instructions. +` + if err := os.WriteFile(filepath.Join(realSkillDir, "SKILL.md"), []byte(skillContent), 0o644); err != nil { + t.Fatal(err) + } + + // Directory containing only a symlink to the skill + symlinkParent := filepath.Join(tmpDir, "symlinked-skills") + if err := os.MkdirAll(symlinkParent, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(realSkillDir, filepath.Join(symlinkParent, "my-skill")); err != nil { + t.Fatal(err) + } + + // A broken symlink should be silently skipped + if err := os.Symlink(filepath.Join(tmpDir, "nonexistent"), filepath.Join(symlinkParent, "broken-skill")); err != nil { + t.Fatal(err) + } + + skills := Discover([]string{symlinkParent}) + + if len(skills) != 1 { + t.Fatalf("expected 1 skill via symlink, got %d", len(skills)) + } + if skills[0].Name != "my-skill" { + t.Errorf("skill name = %q, want %q", skills[0].Name, "my-skill") + } +}