Skip to content

Commit c902cc1

Browse files
committed
feat: support installing root-level skills
1 parent 916c52e commit c902cc1

File tree

2 files changed

+70
-33
lines changed

2 files changed

+70
-33
lines changed

internal/installer/installer.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,15 +339,38 @@ func Install(input string, opts InstallOptions) error {
339339
var skillDescription string
340340
if skill.FindSkillMD(tempSkillPath) {
341341
meta, err := skill.ParseSkillMD(tempSkillPath)
342-
if err == nil && meta != nil && meta.Description != "" {
343-
skillDescription = meta.Description
342+
if err == nil && meta != nil {
343+
if meta.Description != "" {
344+
skillDescription = meta.Description
345+
}
346+
// If SKILL.md defines a name, use it as the directory name
347+
// This is important for root-level skills where the repo name might differ
348+
if meta.Name != "" {
349+
// Sanitize name to be safe file path
350+
safeName := strings.ReplaceAll(meta.Name, "/", "-")
351+
safeName = strings.ReplaceAll(safeName, "\\", "-")
352+
safeName = strings.ReplaceAll(safeName, " ", "-")
353+
if safeName != "" {
354+
skillName = safeName
355+
}
356+
}
344357
}
345358
}
346359
if skillDescription == "" {
347360
skillDescription = "Skill installed from " + originalInput
348361
}
349362

350-
// Validating skill.md name matches (optional, but good practice)
363+
// Environment setup
364+
// Check for .env.example and create .env if it doesn't exist
365+
envExamplePath := filepath.Join(tempSkillPath, ".env.example")
366+
envPath := filepath.Join(tempSkillPath, ".env")
367+
if _, err := os.Stat(envExamplePath); err == nil {
368+
if _, err := os.Stat(envPath); os.IsNotExist(err) {
369+
if err := filesystem.CopyFile(envExamplePath, envPath); err == nil {
370+
fmt.Printf("Created .env from .env.example\n")
371+
}
372+
}
373+
}
351374

352375
// Central storage
353376
centralDir := config.DefaultSkillsDir

internal/repository/fetch.go

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,48 @@ func ScanSkills(baseDir, subPath, owner, repoName string) ([]github.Repository,
8989
baseSearchPath = filepath.Join(baseDir, subPath)
9090
}
9191

92-
// Helper function for recursion
92+
// Helper to create a repository entry from a skill path
93+
createSkillRepo := func(fullPath string) (github.Repository, bool) {
94+
if !skill.FindSkillMD(fullPath) {
95+
return github.Repository{}, false
96+
}
97+
98+
var desc string
99+
if meta, err := skill.ParseSkillMD(fullPath); err == nil && meta != nil {
100+
desc = meta.Description
101+
}
102+
103+
// Rel path from baseDir (root of repo)
104+
relPathFromRoot, _ := filepath.Rel(baseDir, fullPath)
105+
if relPathFromRoot == "." {
106+
relPathFromRoot = ""
107+
}
108+
109+
installArg := fmt.Sprintf("%s/%s", owner, repoName)
110+
if relPathFromRoot != "" {
111+
installArg = fmt.Sprintf("%s/%s/%s", owner, repoName, relPathFromRoot)
112+
}
113+
114+
// Skill name is the directory name
115+
name := filepath.Base(fullPath)
116+
// If base path is root, name might be repoName
117+
if fullPath == baseDir {
118+
name = repoName
119+
}
120+
121+
return github.Repository{
122+
Name: name,
123+
Description: desc,
124+
HTMLURL: installArg,
125+
}, true
126+
}
127+
128+
// 1. Check if the root search path itself is a skill
129+
if repo, ok := createSkillRepo(baseSearchPath); ok {
130+
return []github.Repository{repo}, nil
131+
}
132+
133+
// 2. If not, scan subdirectories recursively
93134
var findSkillsRecursive func(currentPath string, depth int) ([]github.Repository, error)
94135
findSkillsRecursive = func(currentPath string, depth int) ([]github.Repository, error) {
95136
if depth > 2 { // Max recursion depth
@@ -103,13 +144,6 @@ func ScanSkills(baseDir, subPath, owner, repoName string) ([]github.Repository,
103144

104145
var foundSkills []github.Repository
105146

106-
// First checks: is this directory ITSELF a skill?
107-
// Note: The original logic looked for SKILL.md inside subdirectories of the distinct path.
108-
// Use standard approach:
109-
// 1. If currentPath has SKILL.md, it IS a skill (unless it's the root repo dir? maybe allowed).
110-
// 2. Iterate entries. If entry is dir, check if it's a skill.
111-
// Actually, let's look at how FindSkillMD works. It checks for SKILL.md in the given path.
112-
113147
for _, entry := range entries {
114148
if !entry.IsDir() {
115149
continue
@@ -124,30 +158,10 @@ func ScanSkills(baseDir, subPath, owner, repoName string) ([]github.Repository,
124158

125159
fullPath := filepath.Join(currentPath, entry.Name())
126160

127-
if skill.FindSkillMD(fullPath) {
128-
// Found a skill!
129-
var desc string
130-
if meta, err := skill.ParseSkillMD(fullPath); err == nil && meta != nil {
131-
desc = meta.Description
132-
}
133-
134-
// Calculate relative path from the base search path (subPath) to this skill
135-
// installArg should be: owner/repo/subPath/relPathToSkill
136-
// If subPath was empty, it's owner/repo/relPathToSkill
137-
138-
// Rel path from baseDir (root of repo)
139-
relPathFromRoot, _ := filepath.Rel(baseDir, fullPath)
140-
141-
installArg := fmt.Sprintf("%s/%s/%s", owner, repoName, relPathFromRoot)
142-
143-
foundSkills = append(foundSkills, github.Repository{
144-
Name: entry.Name(),
145-
Description: desc,
146-
HTMLURL: installArg,
147-
})
161+
if repo, ok := createSkillRepo(fullPath); ok {
162+
foundSkills = append(foundSkills, repo)
148163
} else {
149164
// Not a skill, recurse if depth allows
150-
// E.g. .curated -> recurse to find skills inside
151165
nestedSkills, err := findSkillsRecursive(fullPath, depth+1)
152166
if err == nil {
153167
foundSkills = append(foundSkills, nestedSkills...)

0 commit comments

Comments
 (0)