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.

59 lines · 2.4 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"}
20# Codex reads its own MCP config: Codex substitutes no ${user_config.*} values.
21SKIP_FILES = {"codex.mcp.json"}
22STAMP = (2026, 1, 1, 0, 0, 0)
23
24
25def files(plugin: pathlib.Path):
26 out = []
27 for p in plugin.rglob("*"):
28 rel = p.relative_to(plugin)
29 if not p.is_file() or any(part in SKIP_DIRS for part in rel.parts) or rel.as_posix() in SKIP_FILES:
30 continue
31 # The engine lays generated type files beside a mod it loads from a working folder.
32 if rel.parts[:2] == (".claude-plugin", "types"):
33 continue
34 out.append(rel)
35 manifest = pathlib.Path(".claude-plugin/plugin.json")
36 return sorted(out, key=lambda r: (r != manifest, r.as_posix()))
37
38
39def pack(plugin: pathlib.Path) -> pathlib.Path:
40 meta = json.loads((plugin / ".claude-plugin/plugin.json").read_text())
41 dest = ROOT / "dist" / f"{meta['name']}-{meta['version']}.zip"
42 dest.parent.mkdir(exist_ok=True)
43 with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
44 for rel in files(plugin):
45 info = zipfile.ZipInfo(rel.as_posix(), STAMP)
46 info.external_attr = 0o644 << 16
47 info.compress_type = zipfile.ZIP_DEFLATED
48 z.writestr(info, (plugin / rel).read_bytes())
49 return dest
50
51
52def main() -> None:
53 names = sys.argv[1:] or sorted(p.name for p in (ROOT / "plugins").iterdir() if (p / ".claude-plugin/plugin.json").is_file())
54 for name in names:
55 print(pack(ROOT / "plugins" / name).relative_to(ROOT))
56
57
58if __name__ == "__main__":
59 main()