| 249 | } |
| 250 | |
| 251 | func installSkill(opts *Options, skill discovery.Skill, baseDir string) error { |
| 252 | // Use skill.Name (not InstallName) for a flat directory layout. |
| 253 | skillDir := filepath.Join(baseDir, skill.Name) |
| 254 | if err := os.MkdirAll(skillDir, 0o755); err != nil { |
| 255 | return fmt.Errorf("could not create directory %s: %w", skillDir, err) |
| 256 | } |
| 257 | |
| 258 | files, err := discovery.DiscoverSkillFiles(opts.Client, opts.Host, opts.Owner, opts.Repo, skill.TreeSHA, skill.Path) |
| 259 | if err != nil { |
| 260 | return fmt.Errorf("could not list skill files: %w", err) |
| 261 | } |
| 262 | |
| 263 | safeSkillDir, err := safepaths.ParseAbsolute(skillDir) |
| 264 | if err != nil { |
| 265 | return fmt.Errorf("could not resolve skill directory path: %w", err) |
| 266 | } |
| 267 | |
| 268 | for _, file := range files { |
| 269 | content, err := discovery.FetchBlob(opts.Client, opts.Host, opts.Owner, opts.Repo, file.SHA) |
| 270 | if err != nil { |
| 271 | return fmt.Errorf("could not fetch %s: %w", file.Path, err) |
| 272 | } |
| 273 | |
| 274 | relPath := strings.TrimPrefix(file.Path, skill.Path+"/") |
| 275 | |
| 276 | safeDest, err := safeSkillDir.Join(relPath) |
| 277 | if err != nil { |
| 278 | var traversalErr safepaths.PathTraversalError |
| 279 | if errors.As(err, &traversalErr) { |
| 280 | return fmt.Errorf("blocked path traversal in %q", relPath) |
| 281 | } |
| 282 | return fmt.Errorf("could not resolve destination path: %w", err) |
| 283 | } |
| 284 | destPath := safeDest.String() |
| 285 | |
| 286 | if dir := filepath.Dir(destPath); dir != skillDir { |
| 287 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 288 | return fmt.Errorf("could not create directory: %w", err) |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | if filepath.Base(relPath) == "SKILL.md" { |
| 293 | content, err = frontmatter.InjectGitHubMetadata(content, opts.Host, opts.Owner, opts.Repo, opts.Ref, skill.TreeSHA, opts.PinnedRef, skill.Path) |
| 294 | if err != nil { |
| 295 | return fmt.Errorf("could not inject metadata: %w", err) |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | if err := os.WriteFile(destPath, []byte(content), 0o644); err != nil { |
| 300 | return fmt.Errorf("could not write %s: %w", destPath, err) |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | return nil |
| 305 | } |
| 306 | |
| 307 | // ResolveGitRoot returns the git repository root using the provided client, |
| 308 | // falling back to the current working directory on error. |