refactorai-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ """Runtime and license commands for proprietary local runtime management (R12)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from refactor_cli.auth import AuthError, ensure_authenticated
9
+ from refactor_cli.control_plane import ensure_lease, heartbeat, read_cached_lease, resolve_policy
10
+ from refactor_cli.runtime_manager import (
11
+ activate_runtime,
12
+ download_artifact,
13
+ resolve_runtime_manifest,
14
+ rollback_runtime,
15
+ runtime_status,
16
+ )
17
+
18
+ console = Console()
19
+ app = typer.Typer(help="Manage local proprietary runtime artifacts.")
20
+ license_app = typer.Typer(help="Inspect developer-key license/auth state.")
21
+
22
+
23
+ def _require_auth(*, force_remote: bool = False):
24
+ try:
25
+ return ensure_authenticated(force_remote=force_remote)
26
+ except AuthError as exc:
27
+ console.print(f"[red]Blocked:[/red] {exc}")
28
+ raise typer.Exit(code=1) from exc
29
+
30
+
31
+ @app.command("status")
32
+ def status() -> None:
33
+ """Show active runtime version and artifact state."""
34
+ data = runtime_status()
35
+ active = data.get("active_version") or "<none>"
36
+ rollback = data.get("rollback_version") or "<none>"
37
+ exists = bool(data.get("active_artifact_exists"))
38
+ icon = "yes" if exists else "no"
39
+ console.print(f"[bold]active runtime[/bold]: {active}")
40
+ console.print(f"[bold]rollback runtime[/bold]: {rollback}")
41
+ console.print(f"[bold]channel[/bold]: {data.get('channel')}")
42
+ console.print(f"[bold]updated at[/bold]: {data.get('updated_at') or '<never>'}")
43
+ console.print(f"[bold]active artifact present[/bold]: {icon}")
44
+ if active and not exists:
45
+ console.print(
46
+ "[yellow]Active runtime points to a missing artifact.[/yellow] "
47
+ "Run `refactor runtime update` to recover."
48
+ )
49
+
50
+
51
+ @app.command("update")
52
+ def update(
53
+ channel: str = typer.Option("stable", "--channel", help="Runtime release channel."),
54
+ dry_run: bool = typer.Option(False, "--dry-run", help="Resolve/verify only; do not activate."),
55
+ ) -> None:
56
+ """Resolve and install the latest runtime artifact for the channel."""
57
+ _require_auth(force_remote=True)
58
+ outcome = "error"
59
+ try:
60
+ manifest = resolve_runtime_manifest(channel=channel)
61
+ console.print(
62
+ f"[bold]resolved manifest[/bold]: version={manifest.runtime_version} "
63
+ f"channel={channel}"
64
+ )
65
+ artifact = download_artifact(manifest.artifact_url)
66
+ console.print(f"[bold]downloaded artifact[/bold]: {len(artifact)} bytes")
67
+ if dry_run:
68
+ # Verification happens in activate_runtime; dry-run still checks
69
+ # integrity by attempting activation in-memory style via duplicate call.
70
+ # We use a no-op by verifying digest directly in manager on activation.
71
+ from refactor_cli.runtime_manager import verify_sha256 # local import to keep module surface small
72
+
73
+ ok = verify_sha256(artifact, manifest.sha256)
74
+ if not ok:
75
+ raise RuntimeError("Runtime artifact checksum verification failed")
76
+ console.print("[green]Dry run OK:[/green] manifest resolved and artifact verified.")
77
+ outcome = "ok"
78
+ return
79
+ artifact_path = activate_runtime(manifest, artifact, channel=channel)
80
+ console.print(
81
+ f"[green]Runtime activated.[/green] version={manifest.runtime_version} "
82
+ f"path={artifact_path}"
83
+ )
84
+ outcome = "ok"
85
+ except RuntimeError as exc:
86
+ console.print(f"[red]Runtime update failed:[/red] {exc}")
87
+ raise typer.Exit(code=1) from exc
88
+ finally:
89
+ heartbeat(
90
+ command="runtime.update",
91
+ result=outcome,
92
+ duration_ms=0,
93
+ runtime_version=str(runtime_status().get("active_version") or ""),
94
+ )
95
+
96
+
97
+ @app.command("rollback")
98
+ def rollback() -> None:
99
+ """Switch active runtime to the recorded rollback version."""
100
+ _require_auth()
101
+ try:
102
+ version = rollback_runtime()
103
+ except RuntimeError as exc:
104
+ console.print(f"[red]Runtime rollback failed:[/red] {exc}")
105
+ raise typer.Exit(code=1) from exc
106
+ console.print(f"[green]Runtime rollback complete.[/green] active_version={version}")
107
+
108
+
109
+ @license_app.command("status")
110
+ def license_status(
111
+ force_remote: bool = typer.Option(
112
+ False,
113
+ "--force-remote",
114
+ help="Bypass local validation cache and revalidate with control plane.",
115
+ )
116
+ ) -> None:
117
+ """Show resolved auth/license state from developer key validation."""
118
+ auth_ctx = _require_auth(force_remote=force_remote)
119
+ try:
120
+ lease = ensure_lease(force_refresh=True) if force_remote else read_cached_lease()
121
+ except RuntimeError as exc:
122
+ lease = None
123
+ console.print(f"[yellow]lease[/yellow]: unavailable ({exc})")
124
+ quota = auth_ctx.quota_remaining
125
+ quota_text = str(quota) if quota is not None else "unknown"
126
+ source = auth_ctx.key.source
127
+ cached = "yes" if auth_ctx.cached else "no"
128
+ console.print(f"[bold]account[/bold]: {auth_ctx.account_id}")
129
+ console.print(f"[bold]key source[/bold]: {source}")
130
+ console.print(f"[bold]cached validation[/bold]: {cached}")
131
+ console.print(f"[bold]quota remaining[/bold]: {quota_text}")
132
+ if lease is not None:
133
+ expires = int(max(0, lease.expires_at_epoch))
134
+ console.print(f"[bold]lease cached[/bold]: {'yes' if lease.cached else 'no'}")
135
+ console.print(f"[bold]lease expires (epoch)[/bold]: {expires}")
136
+ console.print(f"[bold]lease capabilities[/bold]: {', '.join(lease.capabilities) or '<none>'}")
137
+ console.print(f"[bold]policy revision[/bold]: {lease.policy_revision or '<unknown>'}")
138
+
139
+
140
+ @license_app.command("refresh")
141
+ def license_refresh() -> None:
142
+ """Refresh lease and policy from control plane."""
143
+ _require_auth(force_remote=True)
144
+ try:
145
+ lease = ensure_lease(force_refresh=True)
146
+ policy = resolve_policy(force_refresh=True)
147
+ except RuntimeError as exc:
148
+ console.print(f"[red]License refresh failed:[/red] {exc}")
149
+ heartbeat(command="license.refresh", result="error", duration_ms=0)
150
+ raise typer.Exit(code=1) from exc
151
+ revision = str((policy.get("bundle") or {}).get("revision_id") or lease.policy_revision or "<unknown>")
152
+ console.print(
153
+ f"[green]License refreshed.[/green] account={lease.account_id} "
154
+ f"capabilities={len(lease.capabilities)} policy={revision}"
155
+ )
156
+ heartbeat(
157
+ command="license.refresh",
158
+ result="ok",
159
+ duration_ms=0,
160
+ policy_revision=revision,
161
+ )
162
+
163
+
164
+ app.add_typer(license_app, name="license")
@@ -0,0 +1,69 @@
1
+ """Setup bootstrap command (R16)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from refactor_cli.setup_flow import STAGE_IDS, SetupError, get_setup_diagnostics, run_setup
9
+
10
+ console = Console()
11
+
12
+
13
+ def _ask_approval(auto_approve: bool, prompt: str) -> bool:
14
+ if auto_approve:
15
+ return True
16
+ if not typer.confirm(prompt, default=False):
17
+ return False
18
+ return True
19
+
20
+
21
+ def setup(
22
+ yes: bool = typer.Option(False, "--yes", help="Auto-approve setup actions."),
23
+ resume: bool = typer.Option(False, "--resume", help="Resume from last incomplete stage."),
24
+ from_stage: str | None = typer.Option(
25
+ None,
26
+ "--from-stage",
27
+ help=f"Start from a specific stage ({', '.join(STAGE_IDS)}).",
28
+ ),
29
+ diagnostics: bool = typer.Option(False, "--diagnostics", help="Print setup checkpoint diagnostics and exit."),
30
+ ) -> None:
31
+ """Bootstrap local machine/runtime and execution backend for refactor."""
32
+ if diagnostics:
33
+ payload = get_setup_diagnostics()
34
+ state = payload.get("state") or {}
35
+ console.print(f"[bold]status[/bold]: {state.get('status', 'unknown')}")
36
+ console.print(f"[bold]current stage[/bold]: {state.get('current_stage') or '<none>'}")
37
+ console.print(f"[bold]backend[/bold]: {state.get('execution_backend') or '<unset>'}")
38
+ console.print(f"[bold]completed[/bold]: {', '.join(state.get('completed_stages') or []) or '<none>'}")
39
+ if state.get("last_error"):
40
+ console.print(f"[yellow]last error[/yellow]: {state['last_error']}")
41
+ stages = payload.get("stages") or {}
42
+ if stages:
43
+ console.print("[bold]stage outputs[/bold]:")
44
+ for stage_id in STAGE_IDS:
45
+ if stage_id not in stages:
46
+ continue
47
+ data = stages[stage_id] or {}
48
+ console.print(f" - {stage_id}: {data.get('stage', stage_id)}")
49
+ return
50
+ try:
51
+ result = run_setup(
52
+ auto_approve=yes,
53
+ resume=resume,
54
+ from_stage=from_stage,
55
+ ask_approval=lambda prompt: _ask_approval(yes, prompt),
56
+ )
57
+ except SetupError as exc:
58
+ console.print(f"[red]Setup failed:[/red] {exc}")
59
+ console.print("[yellow]Tip:[/yellow] Re-run with `refactor setup --resume` after addressing the issue.")
60
+ raise typer.Exit(code=1) from exc
61
+ if result.status == "already_completed":
62
+ console.print("[green]Setup already completed.[/green]")
63
+ console.print("Run `refactor setup --from-stage S3` to re-run a subset.")
64
+ return
65
+ console.print("[green]Setup completed successfully.[/green]")
66
+ if result.execution_backend:
67
+ console.print(f"[bold]backend[/bold]: {result.execution_backend}")
68
+ console.print(f"[bold]completed stages[/bold]: {', '.join(result.completed_stages)}")
69
+ console.print("[bold]next[/bold]: run `refactor init` and then `refactor review .`")
@@ -0,0 +1,240 @@
1
+ """Lease/policy/heartbeat client for control-plane contracts (R14)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import time
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+ import httpx
14
+
15
+ from refactor_core.paths import ensure_dir, refactor_home
16
+
17
+ from refactor_cli.credentials import resolve_developer_key
18
+ from refactor_cli.settings import platform_url, policy_signing_key
19
+
20
+ LEASE_DIR = "leases"
21
+ LEASE_FILE = "current.json"
22
+ POLICY_DIR = "policy"
23
+ POLICY_FILE = "current.json"
24
+
25
+
26
+ @dataclass
27
+ class LeaseContext:
28
+ token: str
29
+ account_id: str
30
+ expires_at_epoch: float
31
+ refresh_after_epoch: float
32
+ capabilities: list[str]
33
+ policy_revision: str
34
+ offline_grace_seconds: int
35
+ cached: bool
36
+
37
+
38
+ def _lease_path() -> Path:
39
+ return refactor_home() / LEASE_DIR / LEASE_FILE
40
+
41
+
42
+ def _policy_path() -> Path:
43
+ return refactor_home() / POLICY_DIR / POLICY_FILE
44
+
45
+
46
+ def _parse_iso_epoch(value: str | None) -> float:
47
+ if not value:
48
+ return 0.0
49
+ try:
50
+ dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
51
+ except ValueError:
52
+ return 0.0
53
+ if dt.tzinfo is None:
54
+ dt = dt.replace(tzinfo=timezone.utc)
55
+ return dt.timestamp()
56
+
57
+
58
+ def _read_json(path: Path) -> dict | None:
59
+ if not path.is_file():
60
+ return None
61
+ try:
62
+ return json.loads(path.read_text(encoding="utf-8"))
63
+ except (json.JSONDecodeError, OSError):
64
+ return None
65
+
66
+
67
+ def _write_json(path: Path, payload: dict) -> None:
68
+ ensure_dir(path.parent)
69
+ data = json.dumps(payload, indent=2).encode("utf-8")
70
+ tmp = path.with_suffix(path.suffix + ".tmp")
71
+ with tmp.open("wb") as handle:
72
+ handle.write(data)
73
+ handle.flush()
74
+ tmp.replace(path)
75
+
76
+
77
+ def _policy_signature(bundle: dict) -> str:
78
+ canonical = json.dumps(bundle, sort_keys=True, separators=(",", ":")).encode("utf-8")
79
+ digest = hmac.new(policy_signing_key().encode("utf-8"), canonical, hashlib.sha256).hexdigest()
80
+ return f"hmac-sha256:{digest}"
81
+
82
+
83
+ def _verify_policy_payload(payload: dict) -> bool:
84
+ bundle = dict(payload.get("bundle") or {})
85
+ signature = str(payload.get("signature") or "")
86
+ if not bundle or not signature:
87
+ return False
88
+ return hmac.compare_digest(signature, _policy_signature(bundle))
89
+
90
+
91
+ def _from_cached(payload: dict) -> LeaseContext | None:
92
+ token = str(payload.get("lease_token") or "")
93
+ account_id = str(payload.get("account_id") or "unknown")
94
+ expires_at = float(payload.get("expires_at_epoch") or 0.0)
95
+ refresh_after = float(payload.get("refresh_after_epoch") or 0.0)
96
+ now = time.time()
97
+ if not token or expires_at <= now:
98
+ return None
99
+ return LeaseContext(
100
+ token=token,
101
+ account_id=account_id,
102
+ expires_at_epoch=expires_at,
103
+ refresh_after_epoch=refresh_after,
104
+ capabilities=list(payload.get("capabilities") or []),
105
+ policy_revision=str(payload.get("policy_revision") or ""),
106
+ offline_grace_seconds=int(payload.get("offline_grace_seconds") or 0),
107
+ cached=True,
108
+ )
109
+
110
+
111
+ def read_cached_lease() -> LeaseContext | None:
112
+ payload = _read_json(_lease_path())
113
+ if not payload:
114
+ return None
115
+ return _from_cached(payload)
116
+
117
+
118
+ def request_lease(*, timeout: float = 20.0, cli_version: str = "0.1.0") -> LeaseContext:
119
+ resolved = resolve_developer_key()
120
+ if not resolved:
121
+ raise RuntimeError(
122
+ "No developer key configured. Run `refactor login`, set REFACTOR_API_KEY, "
123
+ "or add developer_key to refactor.config."
124
+ )
125
+ try:
126
+ response = httpx.post(
127
+ f"{platform_url()}/v1/license/lease",
128
+ headers={"Authorization": f"Bearer {resolved.key}", "Content-Type": "application/json"},
129
+ json={"cli_version": cli_version},
130
+ timeout=timeout,
131
+ )
132
+ except httpx.HTTPError as exc:
133
+ raise RuntimeError(f"Could not request lease: {exc}") from exc
134
+ if response.status_code >= 400:
135
+ raise RuntimeError(f"Lease request failed ({response.status_code}): {response.text[:500]}")
136
+ payload = response.json() if response.content else {}
137
+ token = str(payload.get("lease_token") or "")
138
+ expires_at_epoch = _parse_iso_epoch(payload.get("expires_at"))
139
+ refresh_after_epoch = _parse_iso_epoch(payload.get("refresh_after"))
140
+ account_id = str(payload.get("account_id") or "unknown")
141
+ context = LeaseContext(
142
+ token=token,
143
+ account_id=account_id,
144
+ expires_at_epoch=expires_at_epoch,
145
+ refresh_after_epoch=refresh_after_epoch,
146
+ capabilities=list(payload.get("capabilities") or []),
147
+ policy_revision=str(payload.get("policy_revision") or ""),
148
+ offline_grace_seconds=int(payload.get("offline_grace_seconds") or 0),
149
+ cached=False,
150
+ )
151
+ _write_json(
152
+ _lease_path(),
153
+ {
154
+ "lease_token": context.token,
155
+ "account_id": context.account_id,
156
+ "expires_at_epoch": context.expires_at_epoch,
157
+ "refresh_after_epoch": context.refresh_after_epoch,
158
+ "capabilities": context.capabilities,
159
+ "policy_revision": context.policy_revision,
160
+ "offline_grace_seconds": context.offline_grace_seconds,
161
+ "issued_at_epoch": time.time(),
162
+ },
163
+ )
164
+ return context
165
+
166
+
167
+ def ensure_lease(*, force_refresh: bool = False) -> LeaseContext:
168
+ if not force_refresh:
169
+ cached = read_cached_lease()
170
+ if cached:
171
+ now = time.time()
172
+ if cached.refresh_after_epoch <= 0 or now < cached.refresh_after_epoch:
173
+ return cached
174
+ try:
175
+ return request_lease()
176
+ except RuntimeError:
177
+ # Best-effort refresh; keep using non-expired lease.
178
+ return cached
179
+ return request_lease()
180
+
181
+
182
+ def resolve_policy(*, force_refresh: bool = False, project_id: str | None = None, timeout: float = 20.0) -> dict:
183
+ if not force_refresh:
184
+ cached = _read_json(_policy_path())
185
+ if cached and _verify_policy_payload(cached):
186
+ return cached
187
+ lease = ensure_lease()
188
+ try:
189
+ response = httpx.post(
190
+ f"{platform_url()}/v1/policy/resolve",
191
+ headers={"Authorization": f"Bearer {lease.token}", "Content-Type": "application/json"},
192
+ json={"project_id": project_id},
193
+ timeout=timeout,
194
+ )
195
+ except httpx.HTTPError as exc:
196
+ raise RuntimeError(f"Could not resolve policy bundle: {exc}") from exc
197
+ if response.status_code >= 400:
198
+ raise RuntimeError(f"Policy resolve failed ({response.status_code}): {response.text[:500]}")
199
+ payload = response.json() if response.content else {}
200
+ if not _verify_policy_payload(payload):
201
+ raise RuntimeError("Policy signature verification failed")
202
+ _write_json(_policy_path(), payload)
203
+ return payload
204
+
205
+
206
+ def heartbeat(
207
+ *,
208
+ command: str,
209
+ result: str,
210
+ duration_ms: int,
211
+ model_id: str = "",
212
+ runtime_version: str = "",
213
+ policy_revision: str = "",
214
+ timeout: float = 10.0,
215
+ ) -> bool:
216
+ try:
217
+ lease = ensure_lease()
218
+ except RuntimeError:
219
+ return False
220
+ account_hash = hashlib.sha256(lease.account_id.encode("utf-8")).hexdigest()[:16]
221
+ payload = {
222
+ "account_id_hash": account_hash,
223
+ "command": command,
224
+ "result": result,
225
+ "duration_ms": max(0, int(duration_ms)),
226
+ "model_id": model_id,
227
+ "runtime_version": runtime_version,
228
+ "policy_revision": policy_revision or lease.policy_revision,
229
+ "timestamp": datetime.now(timezone.utc).isoformat(),
230
+ }
231
+ try:
232
+ response = httpx.post(
233
+ f"{platform_url()}/v1/license/heartbeat",
234
+ headers={"Authorization": f"Bearer {lease.token}", "Content-Type": "application/json"},
235
+ json=payload,
236
+ timeout=timeout,
237
+ )
238
+ except httpx.HTTPError:
239
+ return False
240
+ return response.status_code < 400
@@ -0,0 +1,71 @@
1
+ """Developer key storage and resolution.
2
+
3
+ Resolution precedence (highest wins), per ADR-203:
4
+ 1. ``REFACTOR_API_KEY`` environment variable
5
+ 2. ``developer_key`` in ``refactor.config`` (``${ENV}`` interpolated)
6
+ 3. central credentials file ``~/.refactor/credentials.json``
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from refactor_core.constitution import find_config, load_config
17
+ from refactor_core.paths import credentials_path, ensure_dir, refactor_home
18
+
19
+ from refactor_cli.settings import env_api_key
20
+
21
+
22
+ @dataclass
23
+ class ResolvedKey:
24
+ key: str
25
+ source: str # "env" | "config" | "credentials"
26
+
27
+
28
+ def load_credentials() -> dict:
29
+ path = credentials_path()
30
+ if not path.is_file():
31
+ return {}
32
+ try:
33
+ return json.loads(path.read_text(encoding="utf-8"))
34
+ except (json.JSONDecodeError, OSError):
35
+ return {}
36
+
37
+
38
+ def save_credentials(data: dict) -> Path:
39
+ ensure_dir(refactor_home())
40
+ path = credentials_path()
41
+ path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8")
42
+ os.chmod(path, 0o600)
43
+ return path
44
+
45
+
46
+ def _config_key(project_root: Path | None) -> str | None:
47
+ config_path = find_config(project_root)
48
+ if not config_path:
49
+ return None
50
+ config = load_config(config_path)
51
+ value = config.get_setting("developer_key")
52
+ if isinstance(value, str) and value.strip():
53
+ return value.strip()
54
+ return None
55
+
56
+
57
+ def resolve_developer_key(project_root: Path | None = None) -> ResolvedKey | None:
58
+ """Resolve the developer key using the documented precedence."""
59
+ env_key = env_api_key()
60
+ if env_key:
61
+ return ResolvedKey(key=env_key, source="env")
62
+
63
+ config_key = _config_key(project_root)
64
+ if config_key:
65
+ return ResolvedKey(key=config_key, source="config")
66
+
67
+ stored = load_credentials().get("developer_key")
68
+ if isinstance(stored, str) and stored.strip():
69
+ return ResolvedKey(key=stored.strip(), source="credentials")
70
+
71
+ return None
refactor_cli/main.py ADDED
@@ -0,0 +1,68 @@
1
+ """`refactor` CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from refactor_cli import __version__
8
+ from refactor_cli.commands import auth_cmds, engine_cmds, model_cmds, rules_cmds, run_cmds, runtime_cmds, setup_cmds
9
+
10
+ app = typer.Typer(
11
+ name="refactor",
12
+ help="Local-first AI code review and refactor, run from your project folder.",
13
+ no_args_is_help=True,
14
+ add_completion=False,
15
+ )
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ typer.echo(f"refactor {__version__}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def main(
26
+ version: bool = typer.Option(
27
+ False,
28
+ "--version",
29
+ "-V",
30
+ help="Show the CLI version and exit.",
31
+ callback=_version_callback,
32
+ is_eager=True,
33
+ ),
34
+ ) -> None:
35
+ """refactor CLI."""
36
+
37
+
38
+ # Auth (R7.1)
39
+ app.command()(auth_cmds.login)
40
+ app.command()(auth_cmds.whoami)
41
+
42
+ # Project / run surface (R7.0 scaffolding; auth-guarded where required)
43
+ app.command()(run_cmds.init)
44
+ app.command()(run_cmds.start)
45
+ app.command()(run_cmds.stop)
46
+ app.command()(run_cmds.shell)
47
+ app.command()(run_cmds.review)
48
+ app.command()(run_cmds.code)
49
+ app.command()(run_cmds.requests)
50
+ app.command()(run_cmds.apply)
51
+ app.command()(run_cmds.revert)
52
+ app.command()(run_cmds.status)
53
+ app.command()(run_cmds.diff)
54
+ app.command()(run_cmds.gc)
55
+ app.command()(run_cmds.check)
56
+ app.command()(run_cmds.doctor)
57
+ app.command()(run_cmds.config)
58
+ app.command()(setup_cmds.setup)
59
+
60
+ # Rule policy introspection (R12)
61
+ app.add_typer(rules_cmds.app, name="rules")
62
+ app.add_typer(runtime_cmds.app, name="runtime")
63
+ app.add_typer(engine_cmds.app, name="engine")
64
+ app.add_typer(model_cmds.app, name="model")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ app()