返回 JoyAI-Echo
package_skill.py
1 #!/usr/bin/env python3
2 """
3 Skill Packager - Creates a distributable .skill file of a skill folder
4
5 Usage:
6 python package_skill.py <path/to/skill-folder> [output-directory]
7
8 Example:
9 python package_skill.py skills/public/my-skill
10 python package_skill.py skills/public/my-skill ./dist
11 """
12
13 import sys
14 import zipfile
15 from pathlib import Path
16
17 from quick_validate import validate_skill
18
19
20 def _is_within(path: Path, root: Path) -> bool:
21 try:
22 path.relative_to(root)
23 return True
24 except ValueError:
25 return False
26
27
28 def _cleanup_partial_archive(skill_filename: Path) -> None:
29 try:
30 if skill_filename.exists():
31 skill_filename.unlink()
32 except OSError:
33 pass
34
35
36 def package_skill(skill_path, output_dir=None):
37 """
38 Package a skill folder into a .skill file.
39
40 Args:
41 skill_path: Path to the skill folder
42 output_dir: Optional output directory for the .skill file (defaults to current directory)
43
44 Returns:
45 Path to the created .skill file, or None if error
46 """
47 skill_path = Path(skill_path).resolve()
48
49 # Validate skill folder exists
50 if not skill_path.exists():
51 print(f"[ERROR] Skill folder not found: {skill_path}")
52 return None
53
54 if not skill_path.is_dir():
55 print(f"[ERROR] Path is not a directory: {skill_path}")
56 return None
57
58 # Validate SKILL.md exists
59 skill_md = skill_path / "SKILL.md"
60 if not skill_md.exists():
61 print(f"[ERROR] SKILL.md not found in {skill_path}")
62 return None
63
64 # Run validation before packaging
65 print("Validating skill...")
66 valid, message = validate_skill(skill_path)
67 if not valid:
68 print(f"[ERROR] Validation failed: {message}")
69 print(" Please fix the validation errors before packaging.")
70 return None
71 print(f"[OK] {message}\n")
72
73 # Determine output location
74 skill_name = skill_path.name
75 if output_dir:
76 output_path = Path(output_dir).resolve()
77 output_path.mkdir(parents=True, exist_ok=True)
78 else:
79 output_path = Path.cwd()
80
81 skill_filename = output_path / f"{skill_name}.skill"
82
83 EXCLUDED_DIRS = {".git", ".svn", ".hg", "__pycache__", "node_modules"}
84
85 files_to_package = []
86 resolved_archive = skill_filename.resolve()
87
88 for file_path in skill_path.rglob("*"):
89 # Fail closed on symlinks so the packaged contents are explicit and predictable.
90 if file_path.is_symlink():
91 print(f"[ERROR] Symlink not allowed in packaged skill: {file_path}")
92 _cleanup_partial_archive(skill_filename)
93 return None
94
95 rel_parts = file_path.relative_to(skill_path).parts
96 if any(part in EXCLUDED_DIRS for part in rel_parts):
97 continue
98
99 if file_path.is_file():
100 resolved_file = file_path.resolve()
101 if not _is_within(resolved_file, skill_path):
102 print(f"[ERROR] File escapes skill root: {file_path}")
103 _cleanup_partial_archive(skill_filename)
104 return None
105 # If output lives under skill_path, avoid writing archive into itself.
106 if resolved_file == resolved_archive:
107 print(f"[WARN] Skipping output archive: {file_path}")
108 continue
109 files_to_package.append(file_path)
110
111 # Create the .skill file (zip format)
112 try:
113 with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
114 for file_path in files_to_package:
115 # Calculate the relative path within the zip.
116 arcname = Path(skill_name) / file_path.relative_to(skill_path)
117 zipf.write(file_path, arcname)
118 print(f" Added: {arcname}")
119
120 print(f"\n[OK] Successfully packaged skill to: {skill_filename}")
121 return skill_filename
122
123 except Exception as e:
124 _cleanup_partial_archive(skill_filename)
125 print(f"[ERROR] Error creating .skill file: {e}")
126 return None
127
128
129 def main():
130 if len(sys.argv) < 2:
131 print("Usage: python package_skill.py <path/to/skill-folder> [output-directory]")
132 print("\nExample:")
133 print(" python package_skill.py skills/public/my-skill")
134 print(" python package_skill.py skills/public/my-skill ./dist")
135 sys.exit(1)
136
137 skill_path = sys.argv[1]
138 output_dir = sys.argv[2] if len(sys.argv) > 2 else None
139
140 print(f"Packaging skill: {skill_path}")
141 if output_dir:
142 print(f" Output directory: {output_dir}")
143 print()
144
145 result = package_skill(skill_path, output_dir)
146
147 if result:
148 sys.exit(0)
149 else:
150 sys.exit(1)
151
152
153 if __name__ == "__main__":
154 main()
155
155 lines PYTHON