zcbot/skills/ppt/scripts/icon_sync.py

96 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Icon validation + project sync (strategist GATE tool).
Strategist runs this while locking the icon inventory (design_spec.md §VI /
spec_lock.md `icons`). It does two things, in order of importance:
1. **Validate** each ``lib/name`` against the global icon library
(``templates/icons/``). A name that does not resolve to a real file is a
hard error — the script exits non-zero listing every miss, so the
strategist's "missing icon = re-pick now" GATE actually fires instead of
carrying a phantom icon into generation.
2. **Copy** each resolved icon into ``<project>/icons/<lib>/`` so the project
carries its own icon set. This is belt-and-suspenders: ``finalize_svg.py``
and the native ``svg_to_pptx`` converter both resolve project-first with a
fallback to the global library (see ``svg_finalize.embed_icons``), so the
copy is not strictly required for embedding — but it keeps the project
self-contained and validates existence on the spot.
Usage:
python3 scripts/icon_sync.py <project_path> <lib/name> [<lib/name> ...]
Example:
python3 scripts/icon_sync.py /workspace/ppt生成/deck tabler-outline/brain tabler-outline/cpu
Exit code:
0 every icon resolved (and copied)
1 one or more icons missing / unresolved
2 bad arguments
"""
from __future__ import annotations
import shutil
import sys
from pathlib import Path
# Windows GBK console safety (mirrors other ppt scripts).
try: # pragma: no cover - platform dependent
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception: # pragma: no cover
pass
# Reuse the single source of truth for name resolution (handles the chunk/
# alias and the un-prefixed legacy layout) so validation matches what the
# embedder will actually load at finalize / export time.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from svg_finalize.embed_icons import resolve_icon_path # noqa: E402
GLOBAL_ICONS_DIR = Path(__file__).resolve().parent.parent / "templates" / "icons"
def main(argv: list[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
if len(args) < 2:
print("usage: icon_sync.py <project_path> <lib/name> [<lib/name> ...]")
return 2
project_path = Path(args[0])
names = args[1:]
if not project_path.exists():
print(f"[ERROR] project path not found: {project_path}")
return 2
project_icons = project_path / "icons"
missing: list[str] = []
copied = 0
for name in names:
src, _base = resolve_icon_path(name, GLOBAL_ICONS_DIR)
if not src.exists():
missing.append(name)
print(f"[MISSING] {name} (no file under {GLOBAL_ICONS_DIR})")
continue
# Mirror into <project>/icons/<lib>/<name>.svg, preserving the lib dir.
rel = src.relative_to(GLOBAL_ICONS_DIR)
dst = project_icons / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(src, dst)
copied += 1
print(f"[OK] {name} -> {dst}")
print()
print(f"[Summary] {copied} copied, {len(missing)} missing")
if missing:
print("[GATE] missing icons — re-pick real filenames via "
"`ls templates/icons/<lib>/ | grep <keyword>`, fix spec_lock.md, re-run:")
for m in missing:
print(f" - {m}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())