#!/usr/bin/env python3 """Synchronize the frontend package.json version with pyproject.toml. pyproject.toml is the single source of truth for the project version. Run this script before building the frontend or deploying so the two files never drift apart. Usage: scripts/sync_version.py # read pyproject, write package.json scripts/sync_version.py --check # exit 1 if they already differ """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PYPROJECT = ROOT / "pyproject.toml" PACKAGE_JSON = ROOT / "frontend" / "web" / "package.json" _VERSION_RE = re.compile(r'^version\s*=\s*"([^"]+)"', re.MULTILINE) def read_pyproject_version() -> str: text = PYPROJECT.read_text(encoding="utf-8") match = _VERSION_RE.search(text) if not match: raise RuntimeError(f"could not find version= in {PYPROJECT}") return match.group(1) def write_package_json_version(version: str) -> bool: """Update package.json's version field. Returns True if changed.""" data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8")) old = data.get("version") if old == version: return False data["version"] = version PACKAGE_JSON.write_text( json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) return True def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--check", action="store_true", help="Exit with status 1 if versions already match (CI-friendly).", ) args = parser.parse_args() version = read_pyproject_version() changed = write_package_json_version(version) if args.check: if changed: print(f"OUT_OF_SYNC pyproject={version} package.json was different") return 1 print(f"OK version={version}") return 0 if changed: print(f"synced package.json -> {version}") else: print(f"already in sync: {version}") return 0 if __name__ == "__main__": sys.exit(main())