mirror of
https://github.com/Klotzkette/claude-fuer-deutsches-recht
synced 2026-06-09 10:03:19 +00:00
f0c41a2d53
Uebernahme nuetzlicher Validator-Erweiterungen aus dem parallelen Codex- Vorschlag, ohne die flache ZIP-Struktur aufzugeben (STEP1-11 hatten empirisch bewiesen, dass Cowork mit flachem '.claude-plugin/plugin.json' an der ZIP-Wurzel laeuft). - description-Feld in plugin.json darf max. 300 Zeichen haben - Zahl-Komma-Zahl-Sequenz wird auch beim Release-ZIP-Build erneut geprueft (doppelte Verteidigung neben validate-plugin-structure.mjs)
93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Validate plugin release ZIPs before publishing them."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
|
||
def fail(message: str) -> None:
|
||
print(f"validate-release-zips failed: {message}", file=sys.stderr)
|
||
raise SystemExit(1)
|
||
|
||
|
||
def zip_names(zip_path: Path) -> set[str]:
|
||
try:
|
||
with zipfile.ZipFile(zip_path) as archive:
|
||
return {name.replace("\\", "/") for name in archive.namelist()}
|
||
except zipfile.BadZipFile as exc:
|
||
fail(f"{zip_path}: invalid ZIP: {exc}")
|
||
|
||
|
||
# Cowork-/Marketplace-Upload-Limit liegt offiziell bei 50 MB. Die frühere
|
||
# Hypothese eines 1-MB-Limits war falsch — der eigentliche Bug war eine
|
||
# Zahl-Komma-Zahl-Sequenz im description-Feld (siehe validate-plugin-structure.mjs).
|
||
# Wir bleiben mit einem komfortablen Sicherheitsabstand bei 10 MB.
|
||
MAX_ZIP_BYTES = 10 * 1024 * 1024
|
||
|
||
|
||
def validate_plugin_zip(dist_dir: Path, plugin_name: str) -> None:
|
||
zip_path = dist_dir / f"{plugin_name}.zip"
|
||
if not zip_path.exists():
|
||
fail(f"{zip_path}: missing plugin ZIP")
|
||
|
||
size = zip_path.stat().st_size
|
||
if size > MAX_ZIP_BYTES:
|
||
fail(
|
||
f"{zip_path}: {size} bytes überschreitet Cowork-Uploadgrenze ({MAX_ZIP_BYTES} bytes). "
|
||
"Große Binärdateien (z. B. PDFs) entfernen und durch Online-Verweise ersetzen."
|
||
)
|
||
|
||
names = zip_names(zip_path)
|
||
if ".claude-plugin/plugin.json" not in names:
|
||
fail(f"{zip_path}: .claude-plugin/plugin.json must be at ZIP root")
|
||
if f"{plugin_name}/.claude-plugin/plugin.json" in names:
|
||
fail(f"{zip_path}: ZIP is nested under {plugin_name}/; upload ZIPs must be flat")
|
||
if any(name.startswith(f"{plugin_name}/") for name in names):
|
||
fail(f"{zip_path}: contains nested {plugin_name}/ root")
|
||
if "CLAUDE.md" in names:
|
||
fail(f"{zip_path}: root CLAUDE.md must not be shipped; Claude Code treats it as a warning and Cowork upload may reject it")
|
||
if any("__pycache__/" in name or name.endswith(".pyc") for name in names):
|
||
fail(f"{zip_path}: contains Python cache files")
|
||
|
||
with zipfile.ZipFile(zip_path) as archive:
|
||
manifest = json.loads(archive.read(".claude-plugin/plugin.json"))
|
||
if manifest.get("name") != plugin_name:
|
||
fail(f"{zip_path}: manifest name {manifest.get('name')!r} does not match {plugin_name!r}")
|
||
description = manifest.get("description", "")
|
||
if len(description) > 300:
|
||
fail(f"{zip_path}: manifest description has {len(description)} chars; Cowork bevorzugt <= 300")
|
||
# Letzte Verteidigung gegen den 'Zahl-Komma-Zahl'-Validator-Bug.
|
||
import re
|
||
if re.search(r"\d\s*,\s*\d", description):
|
||
fail(f"{zip_path}: manifest description enthaelt Zahl-Komma-Zahl-Sequenz; nutze 'Rn', 'und' oder '/'")
|
||
|
||
if plugin_name == "liquiditaetsplanung":
|
||
generator = "skills/liquiditaetsvorschau-3-6-12-monate/werkzeuge/build_liquiditaetsplan.py"
|
||
if generator not in names:
|
||
fail(f"{zip_path}: missing standalone Liquiditätsplan generator {generator}")
|
||
|
||
|
||
def main() -> None:
|
||
dist_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("dist")
|
||
marketplace_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".claude-plugin/marketplace.json")
|
||
|
||
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
||
plugins = [plugin["name"] for plugin in marketplace["plugins"]]
|
||
for plugin_name in plugins:
|
||
validate_plugin_zip(dist_dir, plugin_name)
|
||
|
||
marketplace_zip_copy = dist_dir / "marketplace.json"
|
||
if not marketplace_zip_copy.exists():
|
||
fail(f"{marketplace_zip_copy}: missing marketplace.json release asset")
|
||
json.loads(marketplace_zip_copy.read_text(encoding="utf-8"))
|
||
|
||
print(f"validate-release-zips OK ({len(plugins)} plugin ZIPs)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|