nanobot-voice-interface/scripts/sync_card_templates.py

140 lines
4.5 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import filecmp
import os
import shutil
from pathlib import Path
LIVE_TEMPLATES_DIR = Path.home() / ".nanobot" / "cards" / "templates"
EXAMPLE_TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "examples" / "cards" / "templates"
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 sync_directory(source: Path, dest: Path) -> None:
dest.mkdir(parents=True, exist_ok=True)
source_entries = {path.name: path for path in source.iterdir()}
dest_entries = {path.name: path for path in dest.iterdir()} if dest.exists() else {}
for name, source_path in source_entries.items():
dest_path = dest / name
if source_path.is_dir():
sync_directory(source_path, dest_path)
continue
dest_path.parent.mkdir(parents=True, exist_ok=True)
if not dest_path.exists() or not filecmp.cmp(source_path, dest_path, shallow=False):
shutil.copy2(source_path, dest_path)
for name, dest_path in dest_entries.items():
if name in source_entries:
continue
if dest_path.is_dir():
shutil.rmtree(dest_path)
else:
dest_path.unlink(missing_ok=True)
def compare_directory(source: Path, dest: Path, drifts: list[str], *, prefix: str) -> None:
source_entries = {path.name: path for path in source.iterdir()} if source.exists() else {}
dest_entries = {path.name: path for path in dest.iterdir()} if dest.exists() else {}
for name, source_path in source_entries.items():
rel = f"{prefix}/{name}" if prefix else name
dest_path = dest / name
if name not in dest_entries:
drifts.append(f"missing mirror: {rel}")
continue
if source_path.is_dir():
if not dest_path.is_dir():
drifts.append(f"type mismatch: {rel}")
continue
compare_directory(source_path, dest_path, drifts, prefix=rel)
continue
if dest_path.is_dir():
drifts.append(f"type mismatch: {rel}")
continue
if not filecmp.cmp(source_path, dest_path, shallow=False):
drifts.append(f"content drift: {rel}")
for name in dest_entries:
if name not in source_entries:
rel = f"{prefix}/{name}" if prefix else name
drifts.append(f"stale mirror: {rel}")
def run_check(source_root: Path, dest_root: Path) -> int:
drifts: list[str] = []
source_templates = iter_template_dirs(source_root)
dest_templates = iter_template_dirs(dest_root)
for name, source_dir in source_templates.items():
dest_dir = dest_root / name
if name not in dest_templates:
drifts.append(f"missing mirror: {name}")
continue
compare_directory(source_dir, dest_dir, drifts, prefix=name)
for name in dest_templates:
if name not in source_templates:
drifts.append(f"stale mirror: {name}")
if drifts:
for drift in drifts:
print(drift)
return 1
print("template sync ok")
return 0
def run_sync(source_root: Path, dest_root: Path) -> int:
source_templates = iter_template_dirs(source_root)
dest_root.mkdir(parents=True, exist_ok=True)
for name, source_dir in source_templates.items():
sync_directory(source_dir, dest_root / name)
for child in list(dest_root.iterdir()):
if child.name in source_templates:
continue
if child.is_dir():
shutil.rmtree(child)
else:
child.unlink(missing_ok=True)
print(f"synced {len(source_templates)} template directories")
return 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true")
parser.add_argument("--source", type=Path, default=LIVE_TEMPLATES_DIR)
parser.add_argument("--dest", type=Path, default=EXAMPLE_TEMPLATES_DIR)
args = parser.parse_args()
source_root = args.source.expanduser()
dest_root = args.dest.expanduser()
if args.check:
return run_check(source_root, dest_root)
return run_sync(source_root, dest_root)
if __name__ == "__main__":
raise SystemExit(main())