Files
Klotzkette f0c41a2d53 Validator: Description-Laengenlimit und Komma-Zahl-Doppelpruefung
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)
2026-05-19 19:34:06 +00:00

93 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()