163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
"""
|
|
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 = """\
|
|
<!-- This file is auto-generated by scripts/sync_agent_config.py from agent-config/rules/ -->
|
|
<!-- Do not edit directly. To update, run: make sync -->
|
|
"""
|
|
|
|
|
|
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()
|