#!/usr/bin/env python3 """ Skill Packager - Creates a distributable .skill file of a skill folder Usage: python utils/package_skill.py [output-directory] Example: python utils/package_skill.py skills/public/my-skill python utils/package_skill.py skills/public/my-skill ./dist """ import sys import zipfile import tempfile from pathlib import Path def safe_user_path(path_value, base_dir="."): """Resolve a CLI path under the current workspace.""" if base_dir != ".": raise ValueError("Custom base directories are not supported for CLI paths") base_path = Path.cwd().resolve() resolved_path = Path(path_value).expanduser().resolve() try: resolved_path.relative_to(base_path) except ValueError as exc: raise ValueError(f"Path escapes allowed directory: {path_value}") from exc return resolved_path from quick_validate import validate_skill def should_include(file_path: Path, skill_path: Path) -> bool: if file_path.is_symlink(): return False try: file_path.resolve(strict=True).relative_to(skill_path.resolve(strict=True)) except (OSError, ValueError): return False return file_path.is_file() def package_skill(skill_path, output_dir=None): """ Package a skill folder into a .skill file. Args: skill_path: Path to the skill folder output_dir: Optional output directory for the .skill file (defaults to current directory) Returns: Path to the created .skill file, or None if error """ skill_path = safe_user_path(skill_path).resolve() # Validate skill folder exists if not skill_path.exists(): print(f"āŒ Error: Skill folder not found: {skill_path}") return None if not skill_path.is_dir(): print(f"āŒ Error: Path is not a directory: {skill_path}") return None # Validate SKILL.md exists skill_md = skill_path / "SKILL.md" if not skill_md.exists(): print(f"āŒ Error: SKILL.md not found in {skill_path}") return None # Run validation before packaging print("šŸ” Validating skill...") valid, message = validate_skill(skill_path) if not valid: print(f"āŒ Validation failed: {message}") print(" Please fix the validation errors before packaging.") return None print(f"āœ… {message}\n") # Determine output location skill_name = skill_path.name if output_dir: output_path = safe_user_path(output_dir).resolve() output_path.mkdir(parents=True, exist_ok=True) else: output_path = Path.cwd() skill_filename = output_path / f"{skill_name}.skill" # Create the .skill file (zip format) try: with tempfile.TemporaryDirectory() as temp_dir: temp_zip_path = Path(temp_dir) / "skill.zip" with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: # Walk through the skill directory for file_path in skill_path.rglob('*'): if should_include(file_path, skill_path): # Calculate the relative path within the zip arcname = file_path.relative_to(skill_path.parent) zipf.write(file_path, arcname) print(f" Added: {arcname}") skill_filename.write_bytes(temp_zip_path.read_bytes()) print(f"\nāœ… Successfully packaged skill to: {skill_filename}") return skill_filename except Exception as e: print(f"āŒ Error creating .skill file: {e}") return None def main(): if len(sys.argv) < 2: print("Usage: python utils/package_skill.py [output-directory]") print("\nExample:") print(" python utils/package_skill.py skills/public/my-skill") print(" python utils/package_skill.py skills/public/my-skill ./dist") sys.exit(1) skill_path = safe_user_path(sys.argv[1]) output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else None print(f"šŸ“¦ Packaging skill: {skill_path}") if output_dir: print(f" Output directory: {output_dir}") print() result = package_skill(skill_path, output_dir) if result: sys.exit(0) else: sys.exit(1) if __name__ == "__main__": main()