(sourceRoot string, skill discovery.Skill, baseDir string)
| 178 | } |
| 179 | |
| 180 | func installLocalSkill(sourceRoot string, skill discovery.Skill, baseDir string) error { |
| 181 | // Use skill.Name (not InstallName) so skills are always installed flat. |
| 182 | // Most agent clients only discover immediate subdirectories of their |
| 183 | // skills folder and do not find skills nested under namespace directories. |
| 184 | skillDir := filepath.Join(baseDir, skill.Name) |
| 185 | if err := os.MkdirAll(skillDir, 0o755); err != nil { |
| 186 | return fmt.Errorf("could not create directory %s: %w", skillDir, err) |
| 187 | } |
| 188 | |
| 189 | srcDir := filepath.Join(sourceRoot, filepath.FromSlash(skill.Path)) |
| 190 | absSource, err := filepath.Abs(srcDir) |
| 191 | if err != nil { |
| 192 | return fmt.Errorf("could not resolve source path: %w", err) |
| 193 | } |
| 194 | |
| 195 | safeSkillDir, err := safepaths.ParseAbsolute(skillDir) |
| 196 | if err != nil { |
| 197 | return fmt.Errorf("could not resolve target path: %w", err) |
| 198 | } |
| 199 | |
| 200 | return filepath.WalkDir(srcDir, func(p string, d os.DirEntry, walkErr error) error { |
| 201 | if walkErr != nil { |
| 202 | return walkErr |
| 203 | } |
| 204 | if d.Type()&os.ModeSymlink != 0 { |
| 205 | return nil |
| 206 | } |
| 207 | if d.IsDir() { |
| 208 | return nil |
| 209 | } |
| 210 | |
| 211 | relPath, err := filepath.Rel(srcDir, p) |
| 212 | if err != nil { |
| 213 | return err |
| 214 | } |
| 215 | |
| 216 | // Defensive: filepath.WalkDir cannot produce traversal paths, but we |
| 217 | // guard against it in case the walk input is ever changed. |
| 218 | safeDest, err := safeSkillDir.Join(relPath) |
| 219 | if err != nil { |
| 220 | var traversalErr safepaths.PathTraversalError |
| 221 | if errors.As(err, &traversalErr) { |
| 222 | return fmt.Errorf("blocked path traversal in %q", relPath) |
| 223 | } |
| 224 | return fmt.Errorf("could not resolve destination path: %w", err) |
| 225 | } |
| 226 | destPath := safeDest.String() |
| 227 | |
| 228 | if dir := filepath.Dir(destPath); dir != skillDir { |
| 229 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 230 | return fmt.Errorf("could not create directory: %w", err) |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | content, err := os.ReadFile(p) |
| 235 | if err != nil { |
| 236 | return fmt.Errorf("could not read %s: %w", p, err) |
| 237 | } |
no test coverage detected