Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

57 lines · 2.2 KBRaw History
1#!/usr/bin/env python3
2"""Packs each Claude Code plugin under plugins/ into dist/<name>-<version>.zip.
3
4The zip has the plugin's files at its root (.claude-plugin/plugin.json first), which is what
5claude.ai's Customize > Plugins > Add > Upload plugin and `claude --plugin-dir <zip>` take.
6Tests and the Codex-only parts stay out. The output is byte-for-byte reproducible: sorted
7entries, a fixed timestamp and fixed permissions, so a rebuild of the same sources is the
8same file (Diffinery serves a copy of these zips; see apps/diffinery/claude-plugins/README.md).
9
10 python3 scripts/pack-plugins.py # every plugin
11 python3 scripts/pack-plugins.py diffinery-ci
12"""
13import json
14import pathlib
15import sys
16import zipfile
17
18ROOT = pathlib.Path(__file__).resolve().parent.parent
19SKIP_DIRS = {"tests", ".codex-plugin", "codex-skills", "node_modules"}
20STAMP = (2026, 1, 1, 0, 0, 0)
21
22
23def files(plugin: pathlib.Path):
24 out = []
25 for p in plugin.rglob("*"):
26 rel = p.relative_to(plugin)
27 if not p.is_file() or any(part in SKIP_DIRS for part in rel.parts):
28 continue
29 # The engine lays generated type files beside a mod it loads from a working folder.
30 if rel.parts[:2] == (".claude-plugin", "types"):
31 continue
32 out.append(rel)
33 manifest = pathlib.Path(".claude-plugin/plugin.json")
34 return sorted(out, key=lambda r: (r != manifest, r.as_posix()))
35
36
37def pack(plugin: pathlib.Path) -> pathlib.Path:
38 meta = json.loads((plugin / ".claude-plugin/plugin.json").read_text())
39 dest = ROOT / "dist" / f"{meta['name']}-{meta['version']}.zip"
40 dest.parent.mkdir(exist_ok=True)
41 with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
42 for rel in files(plugin):
43 info = zipfile.ZipInfo(rel.as_posix(), STAMP)
44 info.external_attr = 0o644 << 16
45 info.compress_type = zipfile.ZIP_DEFLATED
46 z.writestr(info, (plugin / rel).read_bytes())
47 return dest
48
49
50def main() -> None:
51 names = sys.argv[1:] or sorted(p.name for p in (ROOT / "plugins").iterdir() if (p / ".claude-plugin/plugin.json").is_file())
52 for name in names:
53 print(pack(ROOT / "plugins" / name).relative_to(ROOT))
54
55
56if __name__ == "__main__":
57 main()