nanobot-voice-interface/scripts/check_card_runtime.py
kacper 4dfb7ca3cc
Some checks failed
CI / Backend Checks (push) Failing after 36s
CI / Frontend Checks (push) Failing after 40s
feat: unify card runtime and event-driven web ui
2026-04-06 15:42:53 -04:00

119 lines
3.6 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import subprocess
from pathlib import Path
LIVE_TEMPLATES_DIR = Path.home() / ".nanobot" / "cards" / "templates"
EXAMPLE_TEMPLATES_DIR = (
Path(__file__).resolve().parent.parent / "examples" / "cards" / "templates"
)
LEGACY_MARKERS = (
"__nanobot",
"document.currentScript",
"mountLegacyTemplate",
"legacy-template-module",
)
def iter_template_dirs(root: Path) -> dict[str, Path]:
templates: dict[str, Path] = {}
if not root.exists():
return templates
for child in sorted(root.iterdir()):
if not child.is_dir():
continue
if not (child / "manifest.json").exists():
continue
templates[child.name] = child
return templates
def validate_template_dir(template_dir: Path, failures: list[str]) -> None:
manifest_path = template_dir / "manifest.json"
template_path = template_dir / "template.html"
runtime_path = template_dir / "card.js"
for required in (manifest_path, template_path, runtime_path):
if not required.exists():
failures.append(f"{template_dir}: missing {required.name}")
return
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception as exc:
failures.append(f"{manifest_path}: invalid JSON ({exc})")
return
if not isinstance(manifest, dict):
failures.append(f"{manifest_path}: manifest must be an object")
template_html = template_path.read_text(encoding="utf-8")
if "<script" in template_html.lower():
failures.append(f"{template_path}: inline script tags are not allowed")
runtime_js = runtime_path.read_text(encoding="utf-8")
if "export function mount" not in runtime_js:
failures.append(f"{runtime_path}: missing `export function mount`")
for marker in LEGACY_MARKERS:
if marker in runtime_js:
failures.append(f"{runtime_path}: legacy marker `{marker}` still present")
result = subprocess.run(
["node", "--check", str(runtime_path)],
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
failures.append(f"{runtime_path}: node --check failed: {detail}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--live-root", type=Path, default=LIVE_TEMPLATES_DIR)
parser.add_argument("--example-root", type=Path, default=EXAMPLE_TEMPLATES_DIR)
args = parser.parse_args()
live_root = args.live_root.expanduser()
example_root = args.example_root.expanduser()
failures: list[str] = []
for template_dir in iter_template_dirs(live_root).values():
validate_template_dir(template_dir, failures)
for template_dir in iter_template_dirs(example_root).values():
validate_template_dir(template_dir, failures)
sync_check = subprocess.run(
[
"python3",
str(Path(__file__).resolve().parent / "sync_card_templates.py"),
"--check",
"--source",
str(live_root),
"--dest",
str(example_root),
],
capture_output=True,
text=True,
)
if sync_check.returncode != 0:
detail = (sync_check.stdout + sync_check.stderr).strip()
if detail:
print(detail)
failures.append("template mirror drift detected")
if failures:
for failure in failures:
print(failure)
return 1
print("card runtime ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())