Skip to content

Commit 7976d15

Browse files
committed
feat: validate skills installation
1 parent cf0f5b5 commit 7976d15

2 files changed

Lines changed: 430 additions & 0 deletions

File tree

internal/ai/skills/validate.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package skills
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// SkillInstallStatus reports the installation state of one (agent, skill) pair.
10+
type SkillInstallStatus struct {
11+
SkillName string
12+
AgentID string
13+
LinkPath string
14+
Status string // "ok" | "missing" | "broken_symlink" | "invalid_skill" | "copy" | "unknown"
15+
Error string
16+
}
17+
18+
// ValidateInstall checks each skill in skills for the given agent.
19+
// sourcePluginDir is the directory containing skill subdirectories (pluginDir/skills/).
20+
func ValidateInstall(agentID, agentSkillsDir, sourcePluginDir string, skills []string) []SkillInstallStatus {
21+
out := make([]SkillInstallStatus, 0, len(skills))
22+
for _, skillName := range skills {
23+
expectedSource := filepath.Join(sourcePluginDir, skillName)
24+
linkPath := filepath.Join(agentSkillsDir, skillName)
25+
26+
s := SkillInstallStatus{
27+
SkillName: skillName,
28+
AgentID: agentID,
29+
LinkPath: linkPath,
30+
}
31+
32+
switch CheckSkillLink(agentSkillsDir, skillName, expectedSource) {
33+
case "missing":
34+
s.Status = "missing"
35+
case "broken":
36+
s.Status = "broken_symlink"
37+
s.Error = "symlink target does not exist or is inaccessible"
38+
case "wrong_target":
39+
s.Status = "broken_symlink"
40+
s.Error = "symlink points to wrong target"
41+
case "ok":
42+
if err := checkSkillMeta(expectedSource, skillName); err != nil {
43+
s.Status = "invalid_skill"
44+
s.Error = err.Error()
45+
} else {
46+
s.Status = "ok"
47+
}
48+
case "copy":
49+
fi, statErr := os.Stat(linkPath)
50+
if statErr != nil || !fi.IsDir() {
51+
s.Status = "invalid_skill"
52+
s.Error = fmt.Sprintf("%s is a regular file, not a skill directory", linkPath)
53+
} else if err := checkSkillMeta(linkPath, skillName); err != nil {
54+
s.Status = "invalid_skill"
55+
s.Error = err.Error()
56+
} else {
57+
s.Status = "copy"
58+
}
59+
default:
60+
s.Status = "unknown"
61+
s.Error = "unexpected link state"
62+
}
63+
64+
out = append(out, s)
65+
}
66+
return out
67+
}
68+
69+
// checkSkillMeta verifies that skillDir contains a readable SKILL.md whose name field matches skillName.
70+
func checkSkillMeta(skillDir, skillName string) error {
71+
meta, err := ParseSkillMeta(skillDir)
72+
if err != nil {
73+
return fmt.Errorf("read SKILL.md: %w", err)
74+
}
75+
if meta.Name == "" {
76+
return fmt.Errorf("SKILL.md has no frontmatter or empty name field")
77+
}
78+
if meta.Name != skillName {
79+
return fmt.Errorf("SKILL.md name %q does not match directory name %q", meta.Name, skillName)
80+
}
81+
return nil
82+
}

0 commit comments

Comments
 (0)