"""CLI entry point ``rmgr``.""" from __future__ import annotations import argparse import json from pathlib import Path from repo_manager.commands.rapp import add_rapp_parser from repo_manager.commands.rapp import init as rapp_init from repo_manager.commands.rapp import pin_image as rapp_pin_image from repo_manager.commands.rapp import place as rapp_place from repo_manager.commands.rapp import skeleton as rapp_skeleton from repo_manager.commands.rapp import validate as rapp_validate from repo_manager.commands.rapp import wrap as rapp_wrap def _json_object(raw: str) -> dict: try: value = json.loads(raw) except json.JSONDecodeError as exc: raise argparse.ArgumentTypeError(str(exc)) from exc if not isinstance(value, dict): raise argparse.ArgumentTypeError("value must be a JSON object") return value def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="rmgr", description="Repo Manager CLI (helixforge.repo-manager)", ) parser.add_argument("--version", action="store_true", help="Print package version") sub = parser.add_subparsers(dest="command") sub.add_parser("version", help="Print version") sub.add_parser("dual-run-status", help="Show dual-run flags and mutation meter") p_obs = sub.add_parser("observe", help="Observe repository + print snapshot JSON") p_obs.add_argument("--path", default=".", help="Repository checkout path") p_obs.add_argument("--slug", default=None, help="Override repo slug") p_rec = sub.add_parser("reconcile", help="Rebuild local work-record index from files") p_rec.add_argument("--path", default=".", help="Repository checkout path") p_rec.add_argument("--slug", default=None) p_rec.add_argument("--no-write-index", action="store_true", help="Do not write index file") p_cmd = sub.add_parser( "update-task-status", help="Command repo.work.update_task_status (file + git commit)", ) p_cmd.add_argument("--path", default=".", help="Repository checkout path") p_cmd.add_argument( "--task-id", required=True, help="Canonical task id or State Hub task UUID", ) p_cmd.add_argument( "--status", required=True, choices=["wait", "todo", "progress", "done", "cancel"], ) p_cmd.add_argument("--reason", default="rmgr CLI") p_cmd.add_argument("--correlation-id", default=None) p_cmd.add_argument("--idempotency-key", default=None) p_cmd.add_argument("--expected-head-sha", default=None) p_cmd.add_argument("--slug", default=None) p_cmd.add_argument("--push", action="store_true", help="git push after commit (push-seal)") p_cmd.add_argument( "--no-commit", action="store_true", help="Patch file only (invalid as full applied evidence; for tests)", ) p_wp = sub.add_parser("workplan", help="Governed file-backed workplan mutations") wp_sub = p_wp.add_subparsers(dest="workplan_command") p_wp_create = wp_sub.add_parser("create", help="Create a workplan file") p_wp_create.add_argument("--path", default=".") p_wp_create.add_argument("--workplan-id", required=True) p_wp_create.add_argument("--title", required=True) p_wp_create.add_argument("--goal", required=True) p_wp_create.add_argument( "--status", choices=["proposed", "ready", "active", "blocked", "backlog", "finished", "archived"], default="proposed", ) p_wp_create.add_argument("--owner", default="codex") p_wp_create.add_argument("--domain", default=None) p_wp_create.add_argument("--topic-slug", default=None) p_wp_create.add_argument("--filename", default=None) p_wp_update = wp_sub.add_parser("update", help="Update workplan metadata or status") p_wp_update.add_argument("--path", default=".") p_wp_update.add_argument("--workplan-id", required=True) p_wp_update.add_argument("--title", default=None) p_wp_update.add_argument( "--status", choices=["proposed", "ready", "active", "blocked", "backlog", "finished", "archived"], default=None, ) p_wp_update.add_argument("--owner", default=None) p_wp_update.add_argument("--domain", default=None) p_wp_update.add_argument("--topic-slug", default=None) p_wp_archive = wp_sub.add_parser( "delete", help="Recoverable delete: set archived and move to workplans/archived", ) p_wp_archive.add_argument("--path", default=".") p_wp_archive.add_argument("--workplan-id", required=True) p_wp_archive.add_argument( "--confirm", action="store_true", help="Confirm moving the workplan to the dated archive", ) for wp_parser in (p_wp_create, p_wp_update, p_wp_archive): wp_parser.add_argument("--reason", default="rmgr CLI") wp_parser.add_argument("--correlation-id", default=None) wp_parser.add_argument("--idempotency-key", default=None) wp_parser.add_argument("--expected-head-sha", default=None) wp_parser.add_argument("--slug", default=None) wp_parser.add_argument("--push", action="store_true") wp_parser.add_argument("--no-commit", action="store_true") register_kinds = [ "sbom-inventory", "repo-goals", "upstream-contributions", "technical-debt", "extension-points", "register-entries", ] p_register = sub.add_parser("register", help="Repository-owned register spine") register_sub = p_register.add_subparsers(dest="register_command") p_reg_list = register_sub.add_parser("list", help="List indexed register entries") p_reg_list.add_argument("--path", default=".") p_reg_list.add_argument("--kind", choices=register_kinds, default=None) p_reg_list.add_argument("--slug", default=None) p_reg_put = register_sub.add_parser("put", help="Create or update a register entry") p_reg_put.add_argument("--path", default=".") p_reg_put.add_argument("--kind", required=True, choices=register_kinds) p_reg_put.add_argument("--entry-id", required=True) p_reg_put.add_argument("--title", default=None) p_reg_put.add_argument("--status", default=None) p_reg_put.add_argument("--data-json", type=_json_object, default=None) p_reg_defer = register_sub.add_parser("defer", help="Defer a register entry") p_reg_defer.add_argument("--path", default=".") p_reg_defer.add_argument("--kind", required=True, choices=register_kinds) p_reg_defer.add_argument("--entry-id", required=True) p_reg_defer.add_argument("--status", default="deferred") p_reg_note = register_sub.add_parser("note", help="Append a note to a register entry") p_reg_note.add_argument("--path", default=".") p_reg_note.add_argument("--kind", required=True, choices=register_kinds) p_reg_note.add_argument("--entry-id", required=True) p_reg_note.add_argument("--note", required=True) p_reg_note.add_argument("--author", default="codex") for register_parser in (p_reg_put, p_reg_defer, p_reg_note): register_parser.add_argument("--reason", default="rmgr CLI") register_parser.add_argument("--correlation-id", default=None) register_parser.add_argument("--idempotency-key", default=None) register_parser.add_argument("--expected-head-sha", default=None) register_parser.add_argument("--slug", default=None) register_parser.add_argument("--push", action="store_true") register_parser.add_argument("--no-commit", action="store_true") add_rapp_parser(sub) p_conf = sub.add_parser("conform", help="Check a repository against flavor standards") p_conf.add_argument("--path", default=".", help="Repository checkout path") p_conf.add_argument("--slug", default=None) p_pref = sub.add_parser( "prefix-uniqueness", help="Detect shared workplan prefixes and reused identifiers (ADR-007)", ) p_pref.add_argument("--root", default=".", help="Fleet root or a single repository") p_pref.add_argument("--registry", default=None, help="Override prefix registry YAML") p_scaf = sub.add_parser("scaffold", help="Create flavor-correct repository baseline files") p_scaf.add_argument("--path", required=True) p_scaf.add_argument("--flavor", required=True, choices=["experimental", "research", "project", "tooling", "product", "business"]) p_scaf.add_argument("--slug", default=None) p_scaf.add_argument("--domain", default="infotech") p_scaf.add_argument("--wp-prefix", default=None) p_scaf.add_argument("--force", action="store_true") p_scaf.add_argument("--no-commit", action="store_true") args = parser.parse_args(argv) if args.version or args.command in (None, "version"): from repo_manager import __version__ print(__version__) return 0 if args.command == "dual-run-status": from repo_manager.dual_run import flags_status print(json.dumps(flags_status(), indent=2)) return 0 if args.command == "observe": from repo_manager.observe import observe_repository snap, _idx = observe_repository(Path(args.path), slug=args.slug) print(json.dumps(snap, indent=2)) return 0 if args.command == "reconcile": from repo_manager.dual_run import record_mutation from repo_manager.index_store import append_event, save_index from repo_manager.observe import observe_repository root = Path(args.path) snap, index = observe_repository(root, slug=args.slug) append_event( index, { "type": "repo.reconciled", "workplan_count": snap["index"]["workplan_count"], "task_count": snap["index"]["task_count"], "source": "repo-manager", }, ) record_mutation( source="repo-manager", kind="reconcile", repo_slug=snap.get("slug"), detail=snap["index"], ) if not args.no_write_index: path = save_index(index) print(json.dumps({"ok": True, "index_path": str(path), "snapshot": snap}, indent=2)) else: print(json.dumps({"ok": True, "snapshot": snap, "index": index.to_dict()}, indent=2)) return 0 if args.command == "update-task-status": from repo_manager.commands.task_status import update_task_status result = update_task_status( Path(args.path), args.task_id, args.status, correlation_id=args.correlation_id, reason=args.reason, commit=not args.no_commit, push=args.push, expected_head_sha=args.expected_head_sha, idempotency_key=args.idempotency_key, repo_slug=args.slug, ) print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 if args.command == "workplan": from repo_manager.commands.workplan import mutate_workplan if not args.workplan_command: p_wp.print_help() return 2 operation = "archive" if args.workplan_command == "delete" else args.workplan_command result = mutate_workplan( Path(args.path), args.workplan_id, operation=operation, title=getattr(args, "title", None), goal=getattr(args, "goal", None), status=getattr(args, "status", None), owner=getattr(args, "owner", None), topic_slug=getattr(args, "topic_slug", None), domain=getattr(args, "domain", None), filename=getattr(args, "filename", None), confirm_archive=getattr(args, "confirm", False), correlation_id=args.correlation_id, reason=args.reason, commit=not args.no_commit, push=args.push, expected_head_sha=args.expected_head_sha, idempotency_key=args.idempotency_key, repo_slug=args.slug, ) print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 if args.command == "register": if not args.register_command: p_register.print_help() return 2 if args.register_command == "list": from repo_manager.observe import observe_repository _snapshot, index = observe_repository(Path(args.path), slug=args.slug) prefix = f"register:{args.kind}" if args.kind else "register:" entries = [r.__dict__ for r in index.work_records if r.kind.startswith(prefix)] print(json.dumps({"ok": True, "entries": entries}, indent=2)) return 0 from repo_manager.commands.register import mutate_register_entry result = mutate_register_entry( Path(args.path), args.kind, args.entry_id, operation="upsert" if args.register_command == "put" else args.register_command, title=getattr(args, "title", None), status=getattr(args, "status", None), data=getattr(args, "data_json", None), note=getattr(args, "note", None), note_author=getattr(args, "author", None), correlation_id=args.correlation_id, reason=args.reason, commit=not args.no_commit, push=args.push, expected_head_sha=args.expected_head_sha, idempotency_key=args.idempotency_key, repo_slug=args.slug, ) print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 if args.command == "rapp": if args.rapp_command == "init": result = rapp_init( Path(args.path), app=args.app, ownership_repo=args.ownership_repo, rail=args.rail, classification=args.classification, criticality=args.criticality, package_type=args.package_type, purpose=args.purpose, force=args.force, ) elif args.rapp_command == "validate": result = rapp_validate( Path(args.path), family_root=Path(args.family_root) if args.family_root else None, ) elif args.rapp_command == "skeleton": result = rapp_skeleton( Path(args.path), from_app=Path(args.from_app), app=args.app, package_type=args.package_type, force=args.force, dedicated_postgres=args.dedicated_postgres, ) elif args.rapp_command == "wrap": result = rapp_wrap( Path(args.path), app=args.app, ownership_repo=args.ownership_repo, from_app=Path(args.from_app), rail=args.rail, classification=args.classification, criticality=args.criticality, package_type=args.package_type, purpose=args.purpose, force=args.force, dedicated_postgres=args.dedicated_postgres, family_root=Path(args.family_root) if args.family_root else None, ) elif args.rapp_command == "place": result = rapp_place( Path(args.path), reef=args.reef, family_root=Path(args.family_root) if args.family_root else None, ) elif args.rapp_command == "pin-image": result = rapp_pin_image(Path(args.path), args.digest) else: parser.print_help() return 2 print(json.dumps(result, indent=2)) return 0 if result.get("ok") else 1 if args.command == "conform": from repo_manager.standards import check_repository report = check_repository(Path(args.path), slug=args.slug) print(json.dumps(report.to_dict(), indent=2)) return 0 if report.ok else 1 if args.command == "prefix-uniqueness": from repo_manager.prefix_registry import scan_prefixes registry = Path(args.registry) if args.registry else None report = scan_prefixes(Path(args.root), registry_path=registry) print(json.dumps(report, indent=2)) return 0 if report.get("ok") else 1 if args.command == "scaffold": from repo_manager.commands.scaffold import scaffold_repository result = scaffold_repository( Path(args.path), flavor=args.flavor, slug=args.slug, domain=args.domain, wp_prefix=args.wp_prefix, force=args.force, commit=not args.no_commit, ) print(json.dumps(result.to_dict(), indent=2)) return 0 if result.status == "applied" else 1 parser.print_help() return 0 if __name__ == "__main__": raise SystemExit(main())