#!/usr/bin/env python3 """TC Init — Initialize TC tracking inside a project. Creates docs/TC/ with tc_config.json, tc_registry.json, records/, and evidence/. Idempotent: re-running on an already-initialized project reports current stats and exits cleanly. Usage: python3 tc_init.py --project "My Project" --root . python3 tc_init.py --project "My Project" --root /path/to/project --json Exit codes: 0 = initialized OR already initialized 1 = warnings (e.g. partial state) 2 = bad CLI args / I/O error """ from __future__ import annotations import argparse import json import sys from datetime import datetime, timezone from pathlib import Path VALID_STATUSES = ("planned", "in_progress", "blocked", "implemented", "tested", "deployed") VALID_SCOPES = ("feature", "bugfix", "refactor", "infrastructure", "documentation", "hotfix", "enhancement") VALID_PRIORITIES = ("critical", "high", "medium", "low") def now_iso() -> str: return datetime.now(timezone.utc).isoformat(timespec="seconds") def detect_project_name(root: Path) -> str: """Try CLAUDE.md heading, package.json name, pyproject.toml name, then directory basename.""" claude_md = root / "CLAUDE.md" if claude_md.exists(): try: for line in claude_md.read_text(encoding="utf-8").splitlines(): line = line.strip() if line.startswith("# "): return line[2:].strip() except OSError: pass pkg = root / "package.json" if pkg.exists(): try: data = json.loads(pkg.read_text(encoding="utf-8")) name = data.get("name") if isinstance(name, str) and name.strip(): return name.strip() except (OSError, json.JSONDecodeError): pass pyproject = root / "pyproject.toml" if pyproject.exists(): try: for line in pyproject.read_text(encoding="utf-8").splitlines(): stripped = line.strip() if stripped.startswith("name") and "=" in stripped: value = stripped.split("=", 1)[1].strip().strip('"').strip("'") if value: return value except OSError: pass return root.resolve().name def build_config(project_name: str) -> dict: return { "project_name": project_name, "tc_root": "docs/TC", "created": now_iso(), "auto_track": True, "default_author": "Claude", "categories": list(VALID_SCOPES), } def build_registry(project_name: str) -> dict: return { "project_name": project_name, "created": now_iso(), "updated": now_iso(), "next_tc_number": 1, "records": [], "statistics": { "total": 0, "by_status": {s: 0 for s in VALID_STATUSES}, "by_scope": {s: 0 for s in VALID_SCOPES}, "by_priority": {p: 0 for p in VALID_PRIORITIES}, }, } def write_json_atomic(path: Path, data: dict) -> None: """Write JSON to a temp file and rename, to avoid partial writes.""" tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") tmp.replace(path) def main() -> int: parser = argparse.ArgumentParser(description="Initialize TC tracking in a project.") parser.add_argument("--root", default=".", help="Project root directory (default: current directory)") parser.add_argument("--project", help="Project name (auto-detected if omitted)") parser.add_argument("--force", action="store_true", help="Re-initialize even if config exists (preserves registry)") parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args() root = Path(args.root).resolve() if not root.exists() or not root.is_dir(): msg = f"Project root does not exist or is not a directory: {root}" print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}") return 2 tc_dir = root / "docs" / "TC" config_path = tc_dir / "tc_config.json" registry_path = tc_dir / "tc_registry.json" if config_path.exists() and not args.force: try: cfg = json.loads(config_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as e: msg = f"Existing tc_config.json is unreadable: {e}" print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}") return 2 stats = {} if registry_path.exists(): try: reg = json.loads(registry_path.read_text(encoding="utf-8")) stats = reg.get("statistics", {}) except (OSError, json.JSONDecodeError): stats = {} result = { "status": "already_initialized", "project_name": cfg.get("project_name"), "tc_root": str(tc_dir), "statistics": stats, } if args.json: print(json.dumps(result, indent=2)) else: print(f"TC tracking already initialized for project '{cfg.get('project_name')}'.") print(f" TC root: {tc_dir}") if stats: print(f" Total TCs: {stats.get('total', 0)}") return 0 project_name = args.project or detect_project_name(root) try: tc_dir.mkdir(parents=True, exist_ok=True) (tc_dir / "records").mkdir(exist_ok=True) (tc_dir / "evidence").mkdir(exist_ok=True) write_json_atomic(config_path, build_config(project_name)) if not registry_path.exists() or args.force: write_json_atomic(registry_path, build_registry(project_name)) except OSError as e: msg = f"Failed to create TC directories or files: {e}" print(json.dumps({"status": "error", "error": msg}) if args.json else f"ERROR: {msg}") return 2 result = { "status": "initialized", "project_name": project_name, "tc_root": str(tc_dir), "files_created": [ str(config_path), str(registry_path), str(tc_dir / "records"), str(tc_dir / "evidence"), ], } if args.json: print(json.dumps(result, indent=2)) else: print(f"Initialized TC tracking for project '{project_name}'") print(f" TC root: {tc_dir}") print(f" Config: {config_path}") print(f" Registry: {registry_path}") print(f" Records: {tc_dir / 'records'}") print(f" Evidence: {tc_dir / 'evidence'}") print() print("Next: python3 tc_create.py --root . --name --title --scope <scope> ...") return 0 if __name__ == "__main__": sys.exit(main())