ver 1.0.0 initial commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Package the openproject plugin into a distributable `.plugin` file.
|
||||
|
||||
A `.plugin` is just a zip archive of the plugin tree with
|
||||
`.claude-plugin/plugin.json` at the root. Timestamps are normalized and entries
|
||||
are sorted so builds are deterministic (identical input -> identical bytes).
|
||||
|
||||
Usage:
|
||||
python3 build.py # credential-free build (no config.json)
|
||||
python3 build.py --bake # personal build WITH config.json (your API token!)
|
||||
|
||||
Output: <name>-<version>.plugin (or <name>-<version>-baked.plugin with --bake)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
FIXED_DATE = (2026, 1, 1, 0, 0, 0) # deterministic mtime for every entry
|
||||
|
||||
# Directories/files never included in any build.
|
||||
EXCLUDE_DIRS = {".git", "__pycache__", ".claude"}
|
||||
EXCLUDE_FILES = {".DS_Store", ".gitignore", "build.py"}
|
||||
# Excluded unless --bake (carries the API token).
|
||||
SECRET_FILES = {"config.json"}
|
||||
|
||||
|
||||
def iter_files(bake):
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
dirnames[:] = sorted(d for d in dirnames if d not in EXCLUDE_DIRS)
|
||||
for fn in sorted(filenames):
|
||||
if fn in EXCLUDE_FILES:
|
||||
continue
|
||||
if fn in SECRET_FILES and not bake:
|
||||
continue
|
||||
full = os.path.join(dirpath, fn)
|
||||
rel = os.path.relpath(full, ROOT)
|
||||
if rel.endswith(".plugin"):
|
||||
continue # never nest a previous artifact
|
||||
yield full, rel
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bake", action="store_true",
|
||||
help="include config.json (your API token) in the artifact")
|
||||
args = ap.parse_args()
|
||||
|
||||
with open(os.path.join(ROOT, ".claude-plugin", "plugin.json")) as fh:
|
||||
meta = json.load(fh)
|
||||
name, version = meta["name"], meta["version"]
|
||||
suffix = "-baked" if args.bake else ""
|
||||
out = os.path.join(ROOT, f"{name}-{version}{suffix}.plugin")
|
||||
|
||||
files = sorted(iter_files(args.bake), key=lambda x: x[1])
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for full, rel in files:
|
||||
with open(full, "rb") as fh:
|
||||
data = fh.read()
|
||||
info = zipfile.ZipInfo(rel.replace(os.sep, "/"), date_time=FIXED_DATE)
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o644 << 16
|
||||
zf.writestr(info, data)
|
||||
|
||||
size = os.path.getsize(out)
|
||||
print(f"Built {os.path.basename(out)} ({size:,} bytes, {len(files)} entries)")
|
||||
if args.bake:
|
||||
print(" ⚠ This artifact contains your API token — do not share it.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user