build-skill-zip.py 1.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. #!/usr/bin/env python3
  2. """Build the symlink-free archive used by Claude Desktop."""
  3. from __future__ import annotations
  4. import argparse
  5. import stat
  6. from pathlib import Path
  7. from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
  8. ROOT = Path(__file__).resolve().parent.parent
  9. SOURCE = ROOT / "SKILL.md"
  10. ARCHIVE_PATH = "humanizer/SKILL.md"
  11. def build_archive(output: Path) -> None:
  12. source_bytes = SOURCE.read_bytes()
  13. output.parent.mkdir(parents=True, exist_ok=True)
  14. skill = ZipInfo(ARCHIVE_PATH)
  15. skill.compress_type = ZIP_DEFLATED
  16. skill.create_system = 3
  17. skill.external_attr = (stat.S_IFREG | 0o644) << 16
  18. with ZipFile(output, "w") as archive:
  19. archive.writestr(skill, source_bytes)
  20. with ZipFile(output) as archive:
  21. entries = archive.infolist()
  22. if [entry.filename for entry in entries] != [ARCHIVE_PATH]:
  23. raise SystemExit(f"Archive must contain only {ARCHIVE_PATH}")
  24. mode = entries[0].external_attr >> 16
  25. if not stat.S_ISREG(mode):
  26. raise SystemExit(f"{ARCHIVE_PATH} must be a regular file")
  27. if archive.read(ARCHIVE_PATH) != source_bytes:
  28. raise SystemExit(f"{ARCHIVE_PATH} must match the root SKILL.md")
  29. def main() -> None:
  30. parser = argparse.ArgumentParser(description=__doc__)
  31. parser.add_argument("output", type=Path, help="Path for the generated ZIP file")
  32. args = parser.parse_args()
  33. build_archive(args.output)
  34. print(f"Built Claude Desktop package: {args.output}")
  35. if __name__ == "__main__":
  36. main()