"""Ensure repos track .claude/rules/ while ignoring other Claude Code local state.""" from __future__ import annotations import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SNIPPET_PATH = ROOT / "scripts" / "project_rules" / "gitignore-claude.template" MARKER = "# state-hub: track .claude/rules" TRACK_RULE = "!.claude/rules/" BLANKET_IGNORE_RE = re.compile(r"^\.claude/?\s*$", re.MULTILINE) def load_snippet() -> str: text = SNIPPET_PATH.read_text(encoding="utf-8").strip() return f"{MARKER}\n{text}\n" def snippet_present(text: str) -> bool: return TRACK_RULE in text def ensure_claude_gitignore(gitignore_path: Path) -> bool: """Return True when the file was created or modified.""" snippet = load_snippet() if gitignore_path.exists(): text = gitignore_path.read_text(encoding="utf-8") if snippet_present(text): return False if BLANKET_IGNORE_RE.search(text): updated = BLANKET_IGNORE_RE.sub(snippet.rstrip(), text, count=1) if updated != text: gitignore_path.write_text(_ensure_trailing_newline(updated), encoding="utf-8") return True separator = "" if text.endswith("\n") or not text else "\n" gitignore_path.write_text(f"{text}{separator}\n{snippet}", encoding="utf-8") return True gitignore_path.write_text(snippet, encoding="utf-8") return True def _ensure_trailing_newline(text: str) -> str: return text if text.endswith("\n") else f"{text}\n" def main() -> int: import argparse parser = argparse.ArgumentParser(description="Ensure .gitignore tracks .claude/rules/.") parser.add_argument("path", type=Path, help="Repo root or .gitignore file path") args = parser.parse_args() target = args.path gitignore = target if target.name == ".gitignore" else target / ".gitignore" changed = ensure_claude_gitignore(gitignore) print(f"{'updated' if changed else 'ok'}: {gitignore}") return 0 if __name__ == "__main__": raise SystemExit(main())