package skills import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "time" "github.com/sipeed/picoclaw/pkg/fileutil" "github.com/sipeed/picoclaw/pkg/utils" ) type SkillInstaller struct { workspace string } func NewSkillInstaller(workspace string) *SkillInstaller { return &SkillInstaller{ workspace: workspace, } } func (si *SkillInstaller) InstallFromGitHub(ctx context.Context, repo string) error { skillDir := filepath.Join(si.workspace, "skills", filepath.Base(repo)) if _, err := os.Stat(skillDir); err == nil { return fmt.Errorf("skill '%s' already exists", filepath.Base(repo)) } url := fmt.Sprintf("https://raw.githubusercontent.com/%s/main/SKILL.md", repo) client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } resp, err := utils.DoRequestWithRetry(client, req) if err != nil { return fmt.Errorf("failed to fetch skill: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("failed to fetch skill: HTTP %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response: %w", err) } if err := os.MkdirAll(skillDir, 0o755); err != nil { return fmt.Errorf("failed to create skill directory: %w", err) } skillPath := filepath.Join(skillDir, "SKILL.md") // Use unified atomic write utility with explicit sync for flash storage reliability. if err := fileutil.WriteFileAtomic(skillPath, body, 0o600); err != nil { return fmt.Errorf("failed to write skill file: %w", err) } return nil } func (si *SkillInstaller) Uninstall(skillName string) error { skillDir := filepath.Join(si.workspace, "skills", skillName) if _, err := os.Stat(skillDir); os.IsNotExist(err) { return fmt.Errorf("skill '%s' not found", skillName) } if err := os.RemoveAll(skillDir); err != nil { return fmt.Errorf("failed to remove skill: %w", err) } return nil }