fix: make builtin skills install from embedded FS

Previously, 'skills install-builtin' and 'skills list-builtin' relied on a relative path './picoclaw/skills' which fails for globally installed binaries. This change uses the existing embedded FS (which already contains workspace/skills) to ensure consistent behavior regardless of execution path.
This commit is contained in:
sjhddh 2026-02-16 13:05:59 +00:00
parent 13e4028d42
commit 0733bd9e50

View file

@ -85,40 +85,6 @@ func printVersion() {
} }
} }
func copyDirectory(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(src, path)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
})
}
func main() { func main() {
if len(os.Args) < 2 { if len(os.Args) < 2 {
printHelp() printHelp()
@ -1293,55 +1259,87 @@ func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
} }
func skillsInstallBuiltinCmd(workspace string) { func skillsInstallBuiltinCmd(workspace string) {
builtinSkillsDir := "./picoclaw/skills"
workspaceSkillsDir := filepath.Join(workspace, "skills")
fmt.Printf("Copying builtin skills to workspace...\n") fmt.Printf("Copying builtin skills to workspace...\n")
skillsToInstall := []string{ // Get available skills from embedded FS
"weather", entries, err := embeddedFiles.ReadDir("workspace/skills")
"news", if err != nil {
"stock", fmt.Printf("Error reading embedded skills: %v\n", err)
"calculator", return
} }
for _, skillName := range skillsToInstall { workspaceSkillsDir := filepath.Join(workspace, "skills")
builtinPath := filepath.Join(builtinSkillsDir, skillName) if err := os.MkdirAll(workspaceSkillsDir, 0755); err != nil {
workspacePath := filepath.Join(workspaceSkillsDir, skillName) fmt.Printf("Failed to create skills directory: %v\n", err)
return
}
if _, err := os.Stat(builtinPath); err != nil { for _, entry := range entries {
fmt.Printf("⊘ Builtin skill '%s' not found: %v\n", skillName, err) if !entry.IsDir() {
continue continue
} }
if err := os.MkdirAll(workspacePath, 0755); err != nil { skillName := entry.Name()
fmt.Printf("✗ Failed to create directory for %s: %v\n", skillName, err) skillSrcPath := filepath.Join("workspace", "skills", skillName)
skillDstPath := filepath.Join(workspaceSkillsDir, skillName)
// Check if skill already exists
if _, err := os.Stat(skillDstPath); err == nil {
fmt.Printf("✓ Skill '%s' already exists (skipping)\n", skillName)
continue continue
} }
if err := copyDirectory(builtinPath, workspacePath); err != nil { fmt.Printf("Installing skill '%s'...\n", skillName)
fmt.Printf("✗ Failed to copy %s: %v\n", skillName, err)
if err := copyEmbeddedDir(skillSrcPath, skillDstPath); err != nil {
fmt.Printf("✗ Failed to install %s: %v\n", skillName, err)
} else {
fmt.Printf("✓ Installed %s\n", skillName)
} }
} }
fmt.Println("\n✓ All builtin skills installed!") fmt.Println("\nAll builtin skills processed!")
fmt.Println("Now you can use them in your workspace.") }
// copyEmbeddedDir recursively copies a directory from embedded FS to disk
func copyEmbeddedDir(srcPath, dstPath string) error {
if err := os.MkdirAll(dstPath, 0755); err != nil {
return err
}
entries, err := embeddedFiles.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
srcFile := filepath.Join(srcPath, entry.Name())
dstFile := filepath.Join(dstPath, entry.Name())
if entry.IsDir() {
if err := copyEmbeddedDir(srcFile, dstFile); err != nil {
return err
}
} else {
data, err := embeddedFiles.ReadFile(srcFile)
if err != nil {
return err
}
if err := os.WriteFile(dstFile, data, 0644); err != nil {
return err
}
}
}
return nil
} }
func skillsListBuiltinCmd() { func skillsListBuiltinCmd() {
cfg, err := loadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
return
}
builtinSkillsDir := filepath.Join(filepath.Dir(cfg.WorkspacePath()), "picoclaw", "skills")
fmt.Println("\nAvailable Builtin Skills:") fmt.Println("\nAvailable Builtin Skills:")
fmt.Println("-----------------------") fmt.Println("-----------------------")
entries, err := os.ReadDir(builtinSkillsDir) entries, err := embeddedFiles.ReadDir("workspace/skills")
if err != nil { if err != nil {
fmt.Printf("Error reading builtin skills: %v\n", err) fmt.Printf("Error reading embedded skills: %v\n", err)
return return
} }
@ -1353,24 +1351,22 @@ func skillsListBuiltinCmd() {
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() {
skillName := entry.Name() skillName := entry.Name()
skillFile := filepath.Join(builtinSkillsDir, skillName, "SKILL.md")
description := "No description" description := "No description"
if _, err := os.Stat(skillFile); err == nil {
data, err := os.ReadFile(skillFile) // Try to read SKILL.md from embedded FS
skillFile := filepath.Join("workspace", "skills", skillName, "SKILL.md")
data, err := embeddedFiles.ReadFile(skillFile)
if err == nil { if err == nil {
content := string(data) content := string(data)
if idx := strings.Index(content, "\n"); idx > 0 { // Simple parsing for description
firstLine := content[:idx] if idx := strings.Index(content, "description:"); idx > 0 {
if strings.Contains(firstLine, "description:") { rest := content[idx+12:] // len("description:")
descLine := strings.Index(content[idx:], "\n") if endIdx := strings.Index(rest, "\n"); endIdx > 0 {
if descLine > 0 { description = strings.TrimSpace(rest[:endIdx])
description = strings.TrimSpace(content[idx+descLine : idx+descLine])
}
}
} }
} }
} }
status := "✓" status := "✓"
fmt.Printf(" %s %s\n", status, entry.Name()) fmt.Printf(" %s %s\n", status, entry.Name())
if description != "" { if description != "" {