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,412 @@
1
+ """Stage-based setup flow and checkpoints for local bootstrap (R16)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from refactor_core.engine_runtime import (
13
+ DEFAULT_ENGINE_CONTAINER,
14
+ engine_status,
15
+ ensure_engine_up,
16
+ pull_model,
17
+ resolve_runtime as resolve_engine_runtime,
18
+ )
19
+ from refactor_core.paths import ensure_dir, refactor_home
20
+
21
+ from refactor_cli.auth import ensure_authenticated
22
+ from refactor_cli.control_plane import ensure_lease, heartbeat, resolve_policy
23
+ from refactor_cli.model_policy import detect_machine_profile, evaluate_model, recommended_model_id
24
+ from refactor_cli.runtime_manager import activate_runtime, download_artifact, resolve_runtime_manifest, runtime_status
25
+
26
+ SETUP_DIR = "setup"
27
+ STATE_FILE = "state.json"
28
+ BACKEND_LOCAL_SERVER = "local_server"
29
+ BACKEND_BYOK = "byok"
30
+ BACKEND_CHOICES = (BACKEND_LOCAL_SERVER, BACKEND_BYOK)
31
+ STAGE_IDS = ("S1", "S2", "S3", "S4", "S5", "S6")
32
+ STAGE_NAMES = {
33
+ "S1": "precheck",
34
+ "S2": "auth",
35
+ "S3": "runtime",
36
+ "S4": "backend_choice",
37
+ "S5": "backend_bootstrap",
38
+ "S6": "validate",
39
+ }
40
+
41
+
42
+ class SetupError(RuntimeError):
43
+ """Raised for setup-stage failures with actionable messages."""
44
+
45
+
46
+ @dataclass
47
+ class SetupResult:
48
+ status: str
49
+ completed_stages: list[str]
50
+ last_error: str = ""
51
+ execution_backend: str = ""
52
+
53
+
54
+ def _now_iso() -> str:
55
+ return datetime.now(timezone.utc).isoformat()
56
+
57
+
58
+ def setup_root() -> Path:
59
+ return refactor_home() / SETUP_DIR
60
+
61
+
62
+ def setup_state_path() -> Path:
63
+ return setup_root() / STATE_FILE
64
+
65
+
66
+ def stage_output_path(stage_id: str) -> Path:
67
+ return setup_root() / f"stage-{stage_id}.json"
68
+
69
+
70
+ def _default_state() -> dict:
71
+ return {
72
+ "status": "idle",
73
+ "current_stage": "",
74
+ "execution_backend": "",
75
+ "completed_stages": [],
76
+ "last_error": "",
77
+ "updated_at": "",
78
+ }
79
+
80
+
81
+ def read_setup_state() -> dict:
82
+ path = setup_state_path()
83
+ if not path.is_file():
84
+ return _default_state()
85
+ try:
86
+ payload = json.loads(path.read_text(encoding="utf-8"))
87
+ except (json.JSONDecodeError, OSError):
88
+ return _default_state()
89
+ out = _default_state()
90
+ out.update({k: payload.get(k, v) for k, v in out.items()})
91
+ if not isinstance(out.get("completed_stages"), list):
92
+ out["completed_stages"] = []
93
+ return out
94
+
95
+
96
+ def write_setup_state(state: dict) -> None:
97
+ ensure_dir(setup_root())
98
+ merged = _default_state()
99
+ merged.update(state or {})
100
+ merged["updated_at"] = _now_iso()
101
+ path = setup_state_path()
102
+ tmp = path.with_suffix(path.suffix + ".tmp")
103
+ tmp.write_text(json.dumps(merged, indent=2), encoding="utf-8")
104
+ tmp.replace(path)
105
+
106
+
107
+ def reset_setup_state() -> None:
108
+ write_setup_state(_default_state())
109
+
110
+
111
+ def write_stage_output(stage_id: str, payload: dict) -> None:
112
+ ensure_dir(setup_root())
113
+ path = stage_output_path(stage_id)
114
+ tmp = path.with_suffix(path.suffix + ".tmp")
115
+ tmp.write_text(
116
+ json.dumps(
117
+ {
118
+ "stage_id": stage_id,
119
+ "stage": STAGE_NAMES.get(stage_id, stage_id),
120
+ "written_at": _now_iso(),
121
+ "output": payload,
122
+ },
123
+ indent=2,
124
+ ),
125
+ encoding="utf-8",
126
+ )
127
+ tmp.replace(path)
128
+
129
+
130
+ def get_setup_diagnostics() -> dict:
131
+ state = read_setup_state()
132
+ stages: dict[str, dict] = {}
133
+ for stage_id in STAGE_IDS:
134
+ path = stage_output_path(stage_id)
135
+ if not path.is_file():
136
+ continue
137
+ try:
138
+ stages[stage_id] = json.loads(path.read_text(encoding="utf-8"))
139
+ except (json.JSONDecodeError, OSError):
140
+ stages[stage_id] = {"error": "unreadable stage output"}
141
+ return {"state": state, "stages": stages}
142
+
143
+
144
+ def _stage_s1_precheck(_ask_approval: Callable[[str], bool]) -> dict:
145
+ profile = detect_machine_profile()
146
+ runtime, reason = resolve_engine_runtime("podman")
147
+ disk = shutil.disk_usage(str(refactor_home()))
148
+ blockers: list[str] = []
149
+ if disk.free < 2 * 1024 * 1024 * 1024:
150
+ blockers.append("Less than 2GB free disk available in REFACTOR_HOME filesystem.")
151
+ if blockers:
152
+ raise SetupError("Precheck failed: " + "; ".join(blockers))
153
+ return {
154
+ "profile": profile,
155
+ "podman_available": bool(runtime),
156
+ "podman_reason": reason,
157
+ "disk_free_mb": int(disk.free / (1024 * 1024)),
158
+ }
159
+
160
+
161
+ def _stage_s2_auth(_ask_approval: Callable[[str], bool]) -> dict:
162
+ auth = ensure_authenticated(force_remote=True)
163
+ lease = ensure_lease(force_refresh=True)
164
+ return {
165
+ "account_id": auth.account_id,
166
+ "quota_remaining": auth.quota_remaining,
167
+ "key_source": auth.key.source,
168
+ "lease_policy_revision": lease.policy_revision,
169
+ "lease_capabilities": lease.capabilities,
170
+ }
171
+
172
+
173
+ def _stage_s3_runtime(ask_approval: Callable[[str], bool]) -> dict:
174
+ manifest = resolve_runtime_manifest(channel="stable")
175
+ if not ask_approval(f"Install runtime artifact version {manifest.runtime_version}?"):
176
+ raise SetupError("Runtime installation was not approved.")
177
+ artifact = download_artifact(manifest.artifact_url)
178
+ path = activate_runtime(manifest, artifact, channel="stable")
179
+ status = runtime_status()
180
+ return {
181
+ "runtime_version": manifest.runtime_version,
182
+ "artifact_path": str(path),
183
+ "runtime_status": status,
184
+ }
185
+
186
+
187
+ def _normalize_backend(value: str | None) -> str:
188
+ candidate = str(value or "").strip().lower()
189
+ if candidate in BACKEND_CHOICES:
190
+ return candidate
191
+ return ""
192
+
193
+
194
+ def _selected_backend() -> str:
195
+ state = read_setup_state()
196
+ backend = _normalize_backend(state.get("execution_backend"))
197
+ if not backend:
198
+ raise SetupError("Backend choice is missing. Re-run setup from stage S4.")
199
+ return backend
200
+
201
+
202
+ def _stage_s4_backend_choice(ask_approval: Callable[[str], bool]) -> dict:
203
+ if ask_approval("Install local refactor-server and local model now?"):
204
+ backend = BACKEND_LOCAL_SERVER
205
+ else:
206
+ backend = BACKEND_BYOK
207
+ return {
208
+ "execution_backend": backend,
209
+ "available_backends": list(BACKEND_CHOICES),
210
+ }
211
+
212
+
213
+ def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool]) -> dict:
214
+ backend = _selected_backend()
215
+ if backend == BACKEND_BYOK:
216
+ return {
217
+ "execution_backend": BACKEND_BYOK,
218
+ "server_bootstrap": "skipped_by_choice",
219
+ "model_bootstrap": "skipped_by_choice",
220
+ "next": "Run `refactor init` and configure your provider credentials.",
221
+ }
222
+ if not ask_approval("Install/start local refactor-server and bootstrap model now?"):
223
+ raise SetupError("Local server bootstrap was not approved.")
224
+ runtime, reason = resolve_engine_runtime("podman")
225
+ if not runtime:
226
+ raise SetupError(reason or "Engine runtime unavailable.")
227
+ ok, message, state = ensure_engine_up(runtime=runtime)
228
+ if not ok:
229
+ raise SetupError(f"Local server bootstrap failed: {message}")
230
+ estatus = engine_status(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER)
231
+ if str(estatus.get("status") or "").lower() != "ready":
232
+ raise SetupError("Local server is not ready. Run `refactor engine status` and retry from S5.")
233
+
234
+ policy = resolve_policy(force_refresh=False)
235
+ profile = detect_machine_profile()
236
+ model_id = recommended_model_id(policy, profile=str(profile["profile"]))
237
+ if not model_id:
238
+ raise SetupError("No recommended model available for this machine profile.")
239
+ decision = evaluate_model(model_id=model_id, policy_bundle=policy, profile=str(profile["profile"]))
240
+ if not decision.allowed:
241
+ suffix = f" Suggested: {decision.suggested_model_id}" if decision.suggested_model_id else ""
242
+ raise SetupError(f"{decision.message}{suffix}")
243
+ if not ask_approval(f"Pull recommended model '{model_id}' in local engine?"):
244
+ raise SetupError("Model pull was not approved.")
245
+ runtime, reason = resolve_engine_runtime("podman")
246
+ if not runtime:
247
+ raise SetupError(reason or "Engine runtime unavailable.")
248
+ ok, message = pull_model(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER, model_id=model_id)
249
+ if not ok:
250
+ raise SetupError(f"Model pull failed: {message}")
251
+ return {
252
+ "execution_backend": BACKEND_LOCAL_SERVER,
253
+ "engine_status": estatus.get("status"),
254
+ "recommended_model_id": model_id,
255
+ "decision": decision.reason_code,
256
+ "profile": profile["profile"],
257
+ }
258
+
259
+
260
+ def _stage_s6_validate(_ask_approval: Callable[[str], bool]) -> dict:
261
+ backend = _selected_backend()
262
+ runtime_status_data = runtime_status()
263
+ if not runtime_status_data.get("active_version") or not runtime_status_data.get("active_artifact_exists"):
264
+ raise SetupError("Runtime is not active. Re-run setup from stage S3.")
265
+ if backend == BACKEND_LOCAL_SERVER:
266
+ runtime, reason = resolve_engine_runtime("podman")
267
+ if not runtime:
268
+ raise SetupError(reason or "Engine runtime unavailable.")
269
+ estatus = engine_status(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER)
270
+ if estatus.get("status") != "ready":
271
+ raise SetupError("Engine is not ready. Re-run setup from stage S5.")
272
+ heartbeat(
273
+ command="setup.validate.local_server",
274
+ result="ok",
275
+ duration_ms=0,
276
+ runtime_version=str(runtime_status_data.get("active_version") or ""),
277
+ )
278
+ return {
279
+ "execution_backend": backend,
280
+ "runtime_version": runtime_status_data.get("active_version"),
281
+ "engine_status": estatus.get("status"),
282
+ }
283
+ heartbeat(
284
+ command="setup.validate.byok",
285
+ result="ok",
286
+ duration_ms=0,
287
+ runtime_version=str(runtime_status_data.get("active_version") or ""),
288
+ )
289
+ return {
290
+ "execution_backend": backend,
291
+ "runtime_version": runtime_status_data.get("active_version"),
292
+ "provider_status": "pending_provider_configuration",
293
+ }
294
+
295
+
296
+ STAGE_HANDLERS: dict[str, Callable[[Callable[[str], bool]], dict]] = {
297
+ "S1": _stage_s1_precheck,
298
+ "S2": _stage_s2_auth,
299
+ "S3": _stage_s3_runtime,
300
+ "S4": _stage_s4_backend_choice,
301
+ "S5": _stage_s5_backend_bootstrap,
302
+ "S6": _stage_s6_validate,
303
+ }
304
+
305
+
306
+ def run_setup(
307
+ *,
308
+ auto_approve: bool = False,
309
+ resume: bool = False,
310
+ from_stage: str | None = None,
311
+ ask_approval: Callable[[str], bool] | None = None,
312
+ ) -> SetupResult:
313
+ if ask_approval is None:
314
+ ask_approval = lambda _prompt: bool(auto_approve)
315
+ if from_stage and from_stage not in STAGE_IDS:
316
+ raise SetupError(f"Unknown stage '{from_stage}'. Expected one of: {', '.join(STAGE_IDS)}")
317
+ state = read_setup_state()
318
+ if not resume and not from_stage:
319
+ reset_setup_state()
320
+ state = read_setup_state()
321
+
322
+ completed = list(state.get("completed_stages") or [])
323
+ if from_stage:
324
+ start_index = STAGE_IDS.index(from_stage)
325
+ for stage_id in STAGE_IDS[start_index:]:
326
+ path = stage_output_path(stage_id)
327
+ if path.is_file():
328
+ path.unlink()
329
+ elif resume:
330
+ pending = [stage for stage in STAGE_IDS if stage not in completed]
331
+ if not pending:
332
+ return SetupResult(status="already_completed", completed_stages=completed, last_error="")
333
+ start_index = STAGE_IDS.index(pending[0])
334
+ else:
335
+ start_index = 0
336
+
337
+ write_setup_state(
338
+ {
339
+ "status": "in_progress",
340
+ "current_stage": STAGE_IDS[start_index],
341
+ "execution_backend": state.get("execution_backend", ""),
342
+ "completed_stages": completed,
343
+ "last_error": "",
344
+ }
345
+ )
346
+ for stage_id in STAGE_IDS[start_index:]:
347
+ write_setup_state(
348
+ {
349
+ "status": "in_progress",
350
+ "current_stage": stage_id,
351
+ "execution_backend": read_setup_state().get("execution_backend", ""),
352
+ "completed_stages": completed,
353
+ "last_error": "",
354
+ }
355
+ )
356
+ handler = STAGE_HANDLERS[stage_id]
357
+ try:
358
+ output = handler(ask_approval)
359
+ except SetupError as exc:
360
+ write_setup_state(
361
+ {
362
+ "status": "failed",
363
+ "current_stage": stage_id,
364
+ "execution_backend": read_setup_state().get("execution_backend", ""),
365
+ "completed_stages": completed,
366
+ "last_error": str(exc),
367
+ }
368
+ )
369
+ raise
370
+ except Exception as exc: # pragma: no cover - defensive path
371
+ write_setup_state(
372
+ {
373
+ "status": "failed",
374
+ "current_stage": stage_id,
375
+ "execution_backend": read_setup_state().get("execution_backend", ""),
376
+ "completed_stages": completed,
377
+ "last_error": str(exc),
378
+ }
379
+ )
380
+ raise SetupError(f"Stage {stage_id} failed unexpectedly: {exc}") from exc
381
+ if stage_id == "S4":
382
+ chosen = _normalize_backend(output.get("execution_backend"))
383
+ if not chosen:
384
+ raise SetupError("Backend choice did not return a valid selection.")
385
+ write_setup_state(
386
+ {
387
+ "status": "in_progress",
388
+ "current_stage": stage_id,
389
+ "execution_backend": chosen,
390
+ "completed_stages": completed,
391
+ "last_error": "",
392
+ }
393
+ )
394
+ write_stage_output(stage_id, output)
395
+ if stage_id not in completed:
396
+ completed.append(stage_id)
397
+ final_backend = _normalize_backend(read_setup_state().get("execution_backend"))
398
+ write_setup_state(
399
+ {
400
+ "status": "completed",
401
+ "current_stage": "",
402
+ "execution_backend": final_backend,
403
+ "completed_stages": completed,
404
+ "last_error": "",
405
+ }
406
+ )
407
+ return SetupResult(
408
+ status="completed",
409
+ completed_stages=completed,
410
+ last_error="",
411
+ execution_backend=final_backend,
412
+ )
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: refactorai-cli
3
+ Version: 0.1.0
4
+ Summary: Local-first CLI for the refactor platform
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.12.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: rich>=13.7.0
10
+ Requires-Dist: PyYAML>=6.0.1
11
+ Requires-Dist: refactor-core>=0.1.0
12
+
13
+ # refactorai-cli
14
+
15
+ Public CLI package for Refactor.
16
+
17
+ - PyPI package name: `refactorai-cli`
18
+ - Installed command: `refactor`
19
+ - Python module package: `refactor_cli`
20
+
21
+ ## Local development install
22
+
23
+ From repository root:
24
+
25
+ ```bash
26
+ pip install -e refactor_core -e refactorai-cli
27
+ ```
28
+
29
+ ## Build
30
+
31
+ From repository root:
32
+
33
+ ```bash
34
+ python -m pip install --upgrade build twine
35
+ python -m build "./refactorai-cli"
36
+ ```
37
+
38
+ Artifacts are created in:
39
+
40
+ - `refactorai-cli/dist/*.whl`
41
+ - `refactorai-cli/dist/*.tar.gz`
42
+
43
+ ## Publish
44
+
45
+ ```bash
46
+ python -m twine check ./refactorai-cli/dist/*
47
+ python -m twine upload ./refactorai-cli/dist/*
48
+ ```
49
+
50
+ ## Install test (local)
51
+
52
+ ```bash
53
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.1.0-py3-none-any.whl
54
+ refactor --version
55
+ ```
@@ -0,0 +1,23 @@
1
+ refactor_cli/__init__.py,sha256=z48_Q9yH5k83qlBUPbd4sEDyCVY2fJyoKtpl0Hc1ZZg,273
2
+ refactor_cli/auth.py,sha256=qAjAizH3mDkTudRxODVwZtxjNdHMX8P_D8OTBbbqqAU,3616
3
+ refactor_cli/client.py,sha256=u145fBgX4DvM0RfDJJMAanI375dGD7ZrXNTJ6p8qlvQ,1682
4
+ refactor_cli/control_plane.py,sha256=xA1ch4M4FwZs_LyeqG0bBPsfPwN8UPUoJ0WMFrenQ5k,7994
5
+ refactor_cli/credentials.py,sha256=6SwDCeTNDg8EzfDrlcF81QZbsN3GGvkgEjqyJUoEKWs,2046
6
+ refactor_cli/main.py,sha256=74YzNvhpqeNNT-1JRfR-rAZpJi4Z_F_b1Q1Y8IJHPJs,1701
7
+ refactor_cli/model_policy.py,sha256=cevlBp6TOhUmc-7iYOy9eQq272c2mZSC3MBCRmSzBhI,5239
8
+ refactor_cli/runtime_manager.py,sha256=cs4bjbLBYLfRuj5FjdE6g8kj6gcDVM8OKgIStYqQvew,7904
9
+ refactor_cli/settings.py,sha256=EObjAnMFDXFF-nc4eEZ-MuuJnTjrycUmGPr46kDbpIY,867
10
+ refactor_cli/setup_flow.py,sha256=o0Vv7IDlB20Hr_8ZnXwcn_hg8RRUWef8A7bXh7fLo5g,14369
11
+ refactor_cli/commands/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
+ refactor_cli/commands/auth_cmds.py,sha256=OyX1B-4JW34QcC6XmV5su5T41yFZ_LQIVNIBKKgY4dU,2904
13
+ refactor_cli/commands/engine_cmds.py,sha256=8gPPFqxGAOI-rk5HlZMMtiUS2R5Hgy3YFlqePxU2j0k,5641
14
+ refactor_cli/commands/model_cmds.py,sha256=sDrf5Yoeu3SmJQBKu4dcOPySXaZBJMD_tXDp-1JfEKI,5410
15
+ refactor_cli/commands/rules_cmds.py,sha256=fs7Sk2Z7S-gG6rvx3lAEs-GPIm2m-GXBaZNS2Pe_yjY,4527
16
+ refactor_cli/commands/run_cmds.py,sha256=31NORRBSLZLFcmmLGOtdA4U2157nKgS50eAQbZnR1ng,87336
17
+ refactor_cli/commands/runtime_cmds.py,sha256=z75RAKcEzIT2FADIfJ8fTTmS9GFimEWOlZ6aJmO4A6c,6604
18
+ refactor_cli/commands/setup_cmds.py,sha256=7nVPEVq8hZowWEOyCi9XHV1RozelyLZmrfr08v3Iz1w,2960
19
+ refactorai_cli-0.1.0.dist-info/METADATA,sha256=4E1Q7UZELCZeNzufpmR4rZ6uoBq_H3YT8k03_vAMa4Y,1073
20
+ refactorai_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ refactorai_cli-0.1.0.dist-info/entry_points.txt,sha256=u1uvoDxjmWshkIOgSlOFVZOYyYMObSU56VYAUAGGLeE,51
22
+ refactorai_cli-0.1.0.dist-info/top_level.txt,sha256=6b9iP26oBAqYY_1PP-9r17jzX7q8Bf-e3wnOiCKnTLA,13
23
+ refactorai_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ refactor = refactor_cli.main:app
@@ -0,0 +1 @@
1
+ refactor_cli