fix(skills): support direct invocation of the skill packager (#6938)

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
caitomato 2026-10-30 21:27:36 +08:00 committed by GitHub
parent 2488ba7c27
commit d8d0830e16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 65 additions and 9 deletions

View File

@ -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.

View File

@ -3,18 +3,22 @@
Skill Packager - Creates a distributable .skill file of a skill folder
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
python scripts/package_skill.py <path/to/skill-folder> [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 <path/to/skill-folder> [output-directory]")
print("Usage: python scripts/package_skill.py <path/to/skill-folder> [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]

View File

@ -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()