mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
166 lines
5.1 KiB
Python
166 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from planoai.init_cmd import BUILTIN_TEMPLATES
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SyncEntry:
|
|
template_id: str
|
|
template_file: str
|
|
demo_configs: tuple[str, ...]
|
|
transform: str = "none"
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
TEMPLATES_DIR = REPO_ROOT / "cli" / "planoai" / "templates"
|
|
SYNC_MAP_PATH = TEMPLATES_DIR / "template_sync_map.yaml"
|
|
|
|
|
|
def _load_sync_entries() -> list[SyncEntry]:
|
|
payload = yaml.safe_load(SYNC_MAP_PATH.read_text(encoding="utf-8")) or {}
|
|
rows = payload.get("templates", [])
|
|
entries: list[SyncEntry] = []
|
|
for row in rows:
|
|
entries.append(
|
|
SyncEntry(
|
|
template_id=row["template_id"],
|
|
template_file=row["template_file"],
|
|
demo_configs=tuple(row.get("demo_configs", [])),
|
|
transform=row.get("transform", "none"),
|
|
)
|
|
)
|
|
return entries
|
|
|
|
|
|
def _normalize_yaml(text: str) -> Any:
|
|
return yaml.safe_load(text) if text.strip() else None
|
|
|
|
|
|
def _render_for_demo(template_text: str, transform: str) -> str:
|
|
if transform == "none":
|
|
rendered = template_text
|
|
else:
|
|
raise ValueError(f"Unknown transform profile: {transform}")
|
|
|
|
return rendered if rendered.endswith("\n") else f"{rendered}\n"
|
|
|
|
|
|
def _validate_manifest(entries: list[SyncEntry]) -> list[str]:
|
|
errors: list[str] = []
|
|
builtin_ids = {t.id for t in BUILTIN_TEMPLATES}
|
|
manifest_ids = {entry.template_id for entry in entries}
|
|
|
|
missing = sorted(builtin_ids - manifest_ids)
|
|
extra = sorted(manifest_ids - builtin_ids)
|
|
if missing:
|
|
errors.append(f"Missing template IDs in sync map: {', '.join(missing)}")
|
|
if extra:
|
|
errors.append(f"Unknown template IDs in sync map: {', '.join(extra)}")
|
|
|
|
for entry in entries:
|
|
template_path = TEMPLATES_DIR / entry.template_file
|
|
if not template_path.exists():
|
|
errors.append(
|
|
f"template_file does not exist for '{entry.template_id}': {template_path}"
|
|
)
|
|
for demo_rel_path in entry.demo_configs:
|
|
demo_path = REPO_ROOT / demo_rel_path
|
|
if not demo_path.exists():
|
|
errors.append(
|
|
f"demo config does not exist for '{entry.template_id}': {demo_path}"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def run_sync(*, write: bool, verbose: bool = False) -> int:
|
|
entries = _load_sync_entries()
|
|
manifest_errors = _validate_manifest(entries)
|
|
if manifest_errors:
|
|
for error in manifest_errors:
|
|
print(f"[manifest] {error}")
|
|
return 2
|
|
|
|
drift_count = 0
|
|
for entry in entries:
|
|
template_text = (TEMPLATES_DIR / entry.template_file).read_text(
|
|
encoding="utf-8"
|
|
)
|
|
expected_text = _render_for_demo(template_text, entry.transform)
|
|
expected_yaml = _normalize_yaml(expected_text)
|
|
|
|
for demo_rel_path in entry.demo_configs:
|
|
demo_path = REPO_ROOT / demo_rel_path
|
|
actual_text = demo_path.read_text(encoding="utf-8")
|
|
actual_yaml = _normalize_yaml(actual_text)
|
|
|
|
if actual_yaml == expected_yaml:
|
|
if verbose:
|
|
print(f"[ok] {demo_rel_path}")
|
|
continue
|
|
|
|
drift_count += 1
|
|
print(
|
|
f"[drift] {demo_rel_path} differs from template '{entry.template_id}' "
|
|
f"({entry.template_file})"
|
|
)
|
|
|
|
if write:
|
|
demo_path.write_text(expected_text, encoding="utf-8")
|
|
print(f"[fixed] wrote {demo_rel_path}")
|
|
elif verbose:
|
|
actual_repr = json.dumps(actual_yaml, indent=2, sort_keys=True)
|
|
expected_repr = json.dumps(expected_yaml, indent=2, sort_keys=True)
|
|
print(f"[actual]\n{actual_repr}\n[expected]\n{expected_repr}")
|
|
|
|
if drift_count == 0:
|
|
print("All mapped demo configs are in sync with CLI templates.")
|
|
return 0
|
|
|
|
if write:
|
|
print(f"Updated {drift_count} out-of-sync demo config(s).")
|
|
return 0
|
|
|
|
print(
|
|
f"Found {drift_count} out-of-sync demo config(s). "
|
|
"Run `python -m planoai.template_sync --write` to update."
|
|
)
|
|
return 1
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Check or sync CLI templates to demo config.yaml files."
|
|
)
|
|
mode_group = parser.add_mutually_exclusive_group()
|
|
mode_group.add_argument(
|
|
"--write",
|
|
action="store_true",
|
|
help="Write template content to mapped demo configs when drift is found.",
|
|
)
|
|
mode_group.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="Check for drift and return non-zero if any mapped demos are out of sync.",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Print per-file status and parsed YAML when drift is detected.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
write_mode = bool(args.write)
|
|
return run_sync(write=write_mode, verbose=bool(args.verbose))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|