""" Unified sync script: agent-config/ is the source of truth. Synchronizes: - agent-config/ -> .cursor/, .agents/ (for Cursor / Antigravity) - agent-config/rules/ -> CLAUDE.md, CLAUDE.local.md - agent-config/skills/ -> .claude/skills/ - agent-config/ignore -> .cursorignore, .aiignore, .claudeignore Run: make sync """ import re import shutil import sys from pathlib import Path FRONTMATTER_PATTERN = re.compile(r"^---\n.*?\n---\n?", re.DOTALL) ORDERED_RULES = [ "base.mdc", "node.mdc", "web.mdc", "api-routing-and-module-layout.mdc", "architecture-map.mdc", "ui/css-policy.mdc", "ui/keybinding-propagation-policy.mdc", "ui/modal-escape-policy.mdc", "ui/z-index-layer-policy.mdc", ] CLAUDE_MD_HEADER = """\ """ def sync_directories(src_dir: Path, dst_dir: Path) -> None: """ Synchronize src_dir to dst_dir, mirroring its content. Similar to rsync -av --delete. """ if not src_dir.exists(): print(f"Error: Source directory {src_dir} does not exist.") sys.exit(1) dst_dir.mkdir(parents=True, exist_ok=True) src_items = {p.relative_to(src_dir) for p in src_dir.rglob("*")} dst_items = {p.relative_to(dst_dir) for p in dst_dir.rglob("*")} for item in sorted(dst_items - src_items, reverse=True): p = dst_dir / item if p.is_dir(): shutil.rmtree(p) print(f"Removed directory: {p}") else: p.unlink() print(f"Removed file: {p}") for item in sorted(src_items): src_p = src_dir / item dst_p = dst_dir / item if src_p.is_dir(): dst_p.mkdir(parents=True, exist_ok=True) else: src_stat = src_p.stat() dst_stat = dst_p.stat() if dst_p.exists() else None if dst_stat is None or src_stat.st_mtime > dst_stat.st_mtime or src_stat.st_size != dst_stat.st_size: dst_p.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src_p, dst_p) print(f"Synced: {item}") def strip_frontmatter(text: str) -> str: return FRONTMATTER_PATTERN.sub("", text, count=1).strip() def build_claude_md(rules_dir: Path, output_path: Path) -> None: sections = [] for rule_rel in ORDERED_RULES: rule_path = rules_dir / rule_rel if not rule_path.exists(): print(f"Error: Rule file not found: {rule_path}", file=sys.stderr) sys.exit(1) content = strip_frontmatter(rule_path.read_text(encoding="utf-8")) if content: sections.append(content) body = "\n\n".join(sections) output = CLAUDE_MD_HEADER + "\n" + body + "\n" output_path.write_text(output, encoding="utf-8", newline="\n") print(f"Generated: {output_path.name}") def build_claude_local_md(rules_dir: Path, output_path: Path) -> None: local_rule = rules_dir / "base.local.mdc" if not local_rule.exists(): return content = strip_frontmatter(local_rule.read_text(encoding="utf-8")) output_path.write_text(content + "\n", encoding="utf-8", newline="\n") print(f"Generated: {output_path.name}") def copy_ignore_file(project_root: Path, src: Path, dst_name: str) -> None: """Copy agent-config/ignore (master) to dst_name at project root.""" dst = project_root / dst_name if src.exists(): shutil.copy2(src, dst) print(f"Copied: agent-config/ignore -> {dst_name}") elif dst.exists(): dst.unlink() print(f"Removed: {dst_name} (source agent-config/ignore missing)") def main() -> None: project_root = Path(__file__).resolve().parent.parent agent_config = project_root / "agent-config" rules_dir = agent_config / "rules" skills_src = agent_config / "skills" ignore_src = agent_config / "ignore" if not agent_config.exists(): print(f"Error: agent-config/ not found at {agent_config}") sys.exit(1) print("Synchronizing from agent-config/ (source of truth)...") # agent-config/rules/ -> .cursor/rules/, .agents/rules/ cursor_rules = project_root / ".cursor" / "rules" agent_rules = project_root / ".agents" / "rules" sync_directories(rules_dir, cursor_rules) print("Synced: agent-config/rules/ -> .cursor/rules/") sync_directories(rules_dir, agent_rules) print("Synced: agent-config/rules/ -> .agents/rules/") # agent-config/skills/ -> .cursor/skills/, .agents/skills/, .claude/skills/ cursor_skills = project_root / ".cursor" / "skills" agent_skills = project_root / ".agents" / "skills" claude_skills = project_root / ".claude" / "skills" sync_directories(skills_src, cursor_skills) print("Synced: agent-config/skills/ -> .cursor/skills/") sync_directories(skills_src, agent_skills) print("Synced: agent-config/skills/ -> .agents/skills/") sync_directories(skills_src, claude_skills) print("Synced: agent-config/skills/ -> .claude/skills/") # agent-config/rules/ -> CLAUDE.md, CLAUDE.local.md build_claude_md(rules_dir, project_root / "CLAUDE.md") build_claude_local_md(rules_dir, project_root / "CLAUDE.local.md") # agent-config/ignore -> .cursorignore, .aiignore, .claudeignore copy_ignore_file(project_root, ignore_src, ".cursorignore") copy_ignore_file(project_root, ignore_src, ".aiignore") copy_ignore_file(project_root, ignore_src, ".claudeignore") print("Done.") if __name__ == "__main__": main()