addSkillFileWithTracking installs a single skill file from a package to the agentic engine skill directory. The file's path relative to the skill directory is preserved so that nested files (e.g. scripts/ subdirectories) are written with their full structure intact.
(resolved *ResolvedWorkflow, tracker *FileTracker, opts AddOptions, gitRoot string)
| 711 | // skill directory. The file's path relative to the skill directory is preserved so that |
| 712 | // nested files (e.g. scripts/ subdirectories) are written with their full structure intact. |
| 713 | func addSkillFileWithTracking(resolved *ResolvedWorkflow, tracker *FileTracker, opts AddOptions, gitRoot string) error { |
| 714 | engineSkillDir := workflow.GetEngineSkillDir(opts.EngineOverride) |
| 715 | skillDir := filepath.Join(gitRoot, engineSkillDir, resolved.SkillName) |
| 716 | relPath, err := resolveSkillRelativePath(resolved) |
| 717 | if err != nil { |
| 718 | return err |
| 719 | } |
| 720 | |
| 721 | destFile := filepath.Join(skillDir, relPath) |
| 722 | relToSkillDir, err := filepath.Rel(skillDir, destFile) |
| 723 | if err != nil { |
| 724 | return fmt.Errorf("failed to validate destination path %q for skill %q: %w", destFile, resolved.SkillName, err) |
| 725 | } |
| 726 | if relToSkillDir == ".." || strings.HasPrefix(relToSkillDir, ".."+string(os.PathSeparator)) { |
| 727 | return fmt.Errorf("skill file path %q escapes destination skill directory %q", relPath, skillDir) |
| 728 | } |
| 729 | |
| 730 | // Ensure the destination directory exists (handles nested subdirectories). |
| 731 | if err := os.MkdirAll(filepath.Dir(destFile), constants.DirPermPublic); err != nil { |
| 732 | return fmt.Errorf("failed to create skill directory %s: %w", filepath.Dir(destFile), err) |
| 733 | } |
| 734 | |
| 735 | addLog.Printf("Adding skill file: dest=%s, skill=%s, content_size=%d bytes", destFile, resolved.SkillName, len(resolved.Content)) |
| 736 | if opts.Verbose { |
| 737 | fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Adding skill file to %s: %s", engineSkillDir+"/"+resolved.SkillName, relPath))) |
| 738 | } |
| 739 | |
| 740 | fileExists := false |
| 741 | if fileutil.FileExists(destFile) { |
| 742 | fileExists = true |
| 743 | if !opts.Force { |
| 744 | if opts.Verbose { |
| 745 | fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Skill file '%s' already exists. Skipping.", destFile))) |
| 746 | } |
| 747 | return nil |
| 748 | } |
| 749 | fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Overwriting existing skill file: "+destFile)) |
| 750 | } |
| 751 | if tracker != nil { |
| 752 | if fileExists { |
| 753 | tracker.TrackModified(destFile) |
| 754 | } else { |
| 755 | tracker.TrackCreated(destFile) |
| 756 | } |
| 757 | } |
| 758 | if err := os.WriteFile(destFile, resolved.Content, constants.FilePermPublic); err != nil { |
| 759 | return fmt.Errorf("failed to write skill file '%s': %w", destFile, err) |
| 760 | } |
| 761 | if !opts.Quiet { |
| 762 | fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added skill file: %s/%s/%s", engineSkillDir, resolved.SkillName, relPath))) |
| 763 | } |
| 764 | return nil |
| 765 | } |
| 766 | |
| 767 | func resolveSkillRelativePath(resolved *ResolvedWorkflow) (string, error) { |
| 768 | // Determine the relative path under the skill directory so nested files preserve |