diff --git a/README.md b/README.md index 8ffbd9926e..0141a8d3ea 100644 --- a/README.md +++ b/README.md @@ -1774,6 +1774,8 @@ Discovery follows operator-managed directory symlinks, but skips links back to a Skill Markdown and bundled text resources use UTF-8. Skill-creator CLI and review utilities read and write text explicitly as UTF-8 so localized skills behave consistently across operating systems. +For standalone skill exports, the bundled skill creator's `scripts/package_skill.py` can be run by file path from any directory, or as `python -m scripts.package_skill` from the skill-creator directory. Both entry points validate the skill before writing a `.skill` archive. + Custom-skill writes and security-scan staging preserve submitted line endings on every platform, so the scanner checks the bytes that will be stored. diff --git a/skills/public/skill-creator/scripts/package_skill.py b/skills/public/skill-creator/scripts/package_skill.py index f48eac4446..73aa350cfa 100755 --- a/skills/public/skill-creator/scripts/package_skill.py +++ b/skills/public/skill-creator/scripts/package_skill.py @@ -3,18 +3,22 @@ Skill Packager - Creates a distributable .skill file of a skill folder Usage: - python utils/package_skill.py [output-directory] + python scripts/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 + python scripts/package_skill.py skills/public/my-skill + python scripts/package_skill.py skills/public/my-skill ./dist """ import fnmatch import sys import zipfile from pathlib import Path -from scripts.quick_validate import validate_skill + +if __name__ == "__main__" and not __package__: + from quick_validate import validate_skill +else: + from scripts.quick_validate import validate_skill # Patterns to exclude when packaging skills. EXCLUDE_DIRS = {"__pycache__", "node_modules"} @@ -88,9 +92,9 @@ def package_skill(skill_path, output_dir=None): # Create the .skill file (zip format) try: - with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf: # Walk through the skill directory, excluding build artifacts - for file_path in skill_path.rglob('*'): + for file_path in skill_path.rglob("*"): if not file_path.is_file(): continue arcname = file_path.relative_to(skill_path.parent) @@ -110,10 +114,10 @@ def package_skill(skill_path, output_dir=None): def main(): if len(sys.argv) < 2: - print("Usage: python utils/package_skill.py [output-directory]") + print("Usage: python scripts/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") + print(" python scripts/package_skill.py skills/public/my-skill") + print(" python scripts/package_skill.py skills/public/my-skill ./dist") sys.exit(1) skill_path = sys.argv[1] diff --git a/tests/skills/test_skill_packager_cli.py b/tests/skills/test_skill_packager_cli.py new file mode 100644 index 0000000000..a2dddc97a0 --- /dev/null +++ b/tests/skills/test_skill_packager_cli.py @@ -0,0 +1,50 @@ +"""Exercise the skill packager through its standalone and module entry points.""" + +import os +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest + +SKILL_CREATOR = Path(__file__).resolve().parents[2] / "skills/public/skill-creator" + + +@pytest.mark.parametrize("entrypoint", ["script", "module"]) +@pytest.mark.parametrize("valid", [True, False], ids=["valid", "invalid"]) +def test_packager_cli(tmp_path, entrypoint, valid): + skill = tmp_path / "example-skill" + skill.mkdir() + content = "---\nname: example-skill\ndescription: Example skill.\n---\n" + if not valid: + content = "# Missing frontmatter\n" + (skill / "SKILL.md").write_text(content, encoding="utf-8", newline="\n") + output = tmp_path / "dist" + if entrypoint == "script": + command = [str(SKILL_CREATOR / "scripts/package_skill.py")] + cwd = tmp_path + else: + command = ["-m", "scripts.package_skill"] + cwd = SKILL_CREATOR + env = {**os.environ, "PYTHONIOENCODING": "utf-8"} + env.pop("PYTHONPATH", None) + + result = subprocess.run( + [sys.executable, "-B", *command, str(skill), str(output)], + cwd=cwd, + env=env, + capture_output=True, + encoding="utf-8", + timeout=15, + ) + + assert result.returncode == (0 if valid else 1), result.stdout + result.stderr + assert "Traceback" not in result.stderr + archive = output / "example-skill.skill" + if valid: + with zipfile.ZipFile(archive) as package: + assert package.read("example-skill/SKILL.md").decode("utf-8") == content + else: + assert "No YAML frontmatter found" in result.stdout + assert not archive.exists()