#!/usr/bin/env python3 """Generate catalog.json — the reviewable list of every skill in this repo. Usage: python3 scripts/generate-catalog.py [--check] (default) write catalog.json --check fail if catalog.json is out of date (for CI) """ from __future__ import annotations import hashlib import json import re import sys from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parent.parent CATALOG = ROOT / "catalog.json" def frontmatter(text: str) -> dict: if not text.startswith("---"): return {} end = text.find("\n---", 3) if end == -1: return {} data: dict = {} metadata: dict = {} key = None for line in text[3:end].splitlines(): if not line.strip(): continue m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line) if m: key = m.group(1) data[key] = m.group(2).strip().strip('"').strip("'") continue m = re.match(r"^\s+([A-Za-z0-9_-]+):\s*(.*)$", line) if m and key == "metadata": metadata[m.group(1)] = m.group(2).strip().strip('"').strip("'") if metadata: data["metadata"] = metadata return data def build() -> dict: skills = [] for plugin_json in sorted(ROOT.glob("skills-*/*-plugin/.claude-plugin/plugin.json")): plugin_dir = plugin_json.parent.parent meta = json.loads(plugin_json.read_text(encoding="utf-8")) skill_files = sorted((plugin_dir / "skills").rglob("SKILL.md")) if not skill_files: continue skill_md = skill_files[0] fm = frontmatter(skill_md.read_text(encoding="utf-8", errors="replace")) desc = fm.get("description", "") body = skill_md.read_text(encoding="utf-8", errors="replace") skills.append( { "name": meta.get("install_name") or fm.get("name"), "category": meta.get("dir_category"), "version": meta.get("version"), "status": meta.get("status", "promoted"), "invocation": (fm.get("metadata") or {}).get("invocation", "model-invoked"), "description": desc, "description_len": len(desc), "path": str(skill_md.relative_to(ROOT)), "digest": hashlib.sha256(body.encode("utf-8")).hexdigest()[:16], } ) counts: dict = {} for skill in skills: key = f"{skill['status']}/{skill['invocation']}" counts[key] = counts.get(key, 0) + 1 return { "repo": "ai-proj-helper", "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "total": len(skills), "counts": counts, "skills": skills, } def main() -> int: catalog = build() text = json.dumps(catalog, ensure_ascii=False, indent=2) + "\n" if "--check" in sys.argv: if not CATALOG.is_file(): print("catalog.json 不存在,请运行 python3 scripts/generate-catalog.py") return 1 current = json.loads(CATALOG.read_text(encoding="utf-8")) current.pop("generated_at", None) fresh = dict(catalog) fresh.pop("generated_at", None) if current != fresh: print("catalog.json 已过期,请重新生成") return 1 print("catalog.json 是最新的") return 0 CATALOG.write_text(text, encoding="utf-8") print(f"写入 {CATALOG.relative_to(ROOT)}:{catalog['total']} 个技能 {catalog['counts']}") return 0 if __name__ == "__main__": raise SystemExit(main())