refactorai-cli 0.3.8__tar.gz → 0.3.10__tar.gz

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.
Files changed (36) hide show
  1. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/PKG-INFO +2 -2
  2. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/pyproject.toml +2 -2
  3. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/cloud_rr.py +172 -21
  5. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/run_cmds.py +34 -0
  6. refactorai_cli-0.3.10/refactorai_cli/commands/toolchains_cmds.py +57 -0
  7. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/main.py +4 -0
  8. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/PKG-INFO +2 -2
  9. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/SOURCES.txt +2 -1
  10. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/requires.txt +1 -1
  11. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/README.md +0 -0
  12. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/auth.py +0 -0
  13. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/client.py +0 -0
  14. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/__init__.py +0 -0
  15. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/account_cmds.py +0 -0
  16. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/auth_cmds.py +0 -0
  17. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/cloud_cmds.py +0 -0
  18. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/engine_cmds.py +0 -0
  19. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/model_cmds.py +0 -0
  20. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/rules_cmds.py +0 -0
  21. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/runtime_cmds.py +0 -0
  22. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  23. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/commands/setup_cmds.py +0 -0
  24. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/control_plane.py +0 -0
  25. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/credentials.py +0 -0
  26. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/local_constitution.py +0 -0
  27. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/local_engine_runtime.py +0 -0
  28. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/local_paths.py +0 -0
  29. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/model_policy.py +0 -0
  30. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/runtime_manager.py +0 -0
  31. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/settings.py +0 -0
  32. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli/setup_flow.py +0 -0
  33. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  34. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/entry_points.txt +0 -0
  35. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/refactorai_cli.egg-info/top_level.txt +0 -0
  36. {refactorai_cli-0.3.8 → refactorai_cli-0.3.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
8
8
  Requires-Dist: httpx>=0.27.0
9
9
  Requires-Dist: rich>=13.7.0
10
10
  Requires-Dist: PyYAML>=6.0.1
11
- Requires-Dist: refactorai-core>=3.0.5
11
+ Requires-Dist: refactorai-core>=3.1.0
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.3.8"
3
+ version = "0.3.10"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "httpx>=0.27.0",
13
13
  "rich>=13.7.0",
14
14
  "PyYAML>=6.0.1",
15
- "refactorai-core>=3.0.5",
15
+ "refactorai-core>=3.1.0",
16
16
  ]
17
17
 
18
18
  [project.scripts]
@@ -5,4 +5,4 @@ the shared `refactor_core` pipeline from a project folder while staying
5
5
  authenticated to the hosted platform via a developer key.
6
6
  """
7
7
 
8
- __version__ = "0.3.8"
8
+ __version__ = "0.3.10"
@@ -50,6 +50,7 @@ from refactor_core.refactor_requests import (
50
50
  rr_test_dir,
51
51
  save_request,
52
52
  )
53
+ from refactor_core.host_toolchain import provision_for_command
53
54
  from refactor_core.store import create_run
54
55
  from refactor_core.testcmd import (
55
56
  _SKIP_DIRS,
@@ -98,7 +99,9 @@ def _read_source(project_root: Path, rel: str) -> str:
98
99
  return ""
99
100
 
100
101
 
101
- def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
102
+ def _run_test(
103
+ project_root: Path, test_command: str | None, *, env: dict | None = None
104
+ ) -> tuple[bool, str]:
102
105
  """Run the configured test command on the host; (passed, detail).
103
106
 
104
107
  Cloud runs execute tests client-side because the developer's code + deps live
@@ -106,6 +109,9 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
106
109
  command is configured we return ``(False, ...)`` so the server defers the RR
107
110
  rather than applying an unverifiable change. This differs from local's
108
111
  permissive skip on purpose -- cloud never applies an unverified change.
112
+
113
+ ``env`` optionally overrides the process environment (e.g. a managed host
114
+ toolchain prepended to ``PATH``); when None the inherited environment is used.
109
115
  """
110
116
  if not test_command or not str(test_command).strip():
111
117
  return False, "no test command configured; cannot verify behavior preservation"
@@ -120,6 +126,7 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
120
126
  capture_output=True,
121
127
  text=True,
122
128
  timeout=600,
129
+ env=env,
123
130
  )
124
131
  except (subprocess.SubprocessError, OSError) as exc:
125
132
  return False, f"test command error: {exc}"
@@ -128,31 +135,147 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
128
135
  return False, (proc.stdout or proc.stderr or "")[-500:]
129
136
 
130
137
 
131
- def _client_preflight_command(project_root: Path, command: str | None) -> bool:
138
+ def _leading_binary(command: str | None) -> str | None:
139
+ """Return the leading executable of a simple command, or None.
140
+
141
+ Returns None for composite/shell commands (``&&``, subshells, pipes) that
142
+ can't be reduced to a single binary, and for env-only prefixes.
143
+ """
144
+ if not command or not str(command).strip():
145
+ return None
146
+ if any(op in command for op in ("&&", "||", ";", "|", "(", ")")):
147
+ return None
148
+ try:
149
+ tokens = shlex.split(command, posix=True)
150
+ except ValueError:
151
+ return None
152
+ while tokens and _ENV_ASSIGN_RE.match(tokens[0]):
153
+ tokens.pop(0)
154
+ if not tokens:
155
+ return None
156
+ return tokens[0]
157
+
158
+
159
+ def _client_preflight_command(
160
+ project_root: Path, command: str | None, *, env: dict | None = None
161
+ ) -> bool:
132
162
  """Return True when ``command`` looks runnable on this host.
133
163
 
134
164
  Composite/shell commands (``&&``, subshells, pipes) run under ``shell=True``
135
165
  and can't be reduced to one binary, so we trust them and let the real exit
136
166
  code gate. Simple commands are validated by checking the leading binary is on
137
167
  PATH — this guards against running (and charging for) a server-suggested
138
- command whose toolchain isn't installed.
168
+ command whose toolchain isn't installed. ``env`` optionally supplies the
169
+ PATH to check against (e.g. after a managed toolchain is provisioned).
139
170
  """
140
171
  if not command or not str(command).strip():
141
172
  return False
142
173
  if any(op in command for op in ("&&", "||", ";", "|", "(", ")")):
143
174
  return True
144
- try:
145
- tokens = shlex.split(command, posix=True)
146
- except ValueError:
147
- return False
148
- while tokens and _ENV_ASSIGN_RE.match(tokens[0]):
149
- tokens.pop(0)
150
- if not tokens:
175
+ binary = _leading_binary(command)
176
+ if not binary:
151
177
  return False
152
- binary = tokens[0]
153
178
  if "/" in binary:
154
179
  return Path(binary).expanduser().exists()
155
- return shutil.which(binary) is not None
180
+ search_path = (env or {}).get("PATH") if env else None
181
+ return shutil.which(binary, path=search_path) is not None
182
+
183
+
184
+ def _managed_toolchain_enabled(config: dict) -> bool:
185
+ """Whether managed host-toolchain provisioning is enabled (default on).
186
+
187
+ Reads ``verification.managed_toolchain`` from the project config; absent =>
188
+ enabled, so cloud modes provision runtimes for the developer by default.
189
+ """
190
+ verification = (config or {}).get("verification")
191
+ if isinstance(verification, dict) and "managed_toolchain" in verification:
192
+ return bool(verification.get("managed_toolchain"))
193
+ return True
194
+
195
+
196
+ def _detected_is_provisionable(command: str | None) -> bool:
197
+ """True when ``command``'s leading binary maps to a managed runtime.
198
+
199
+ Lets the resolver keep a detected command (e.g. ``npm test``) whose runtime
200
+ is missing from the host PATH but which the managed toolchain can download,
201
+ instead of discarding it and deferring the run.
202
+ """
203
+ from refactor_core.host_toolchain import binary_to_runtime
204
+
205
+ binary = _leading_binary(command)
206
+ if not binary:
207
+ return False
208
+ return binary_to_runtime(binary) is not None
209
+
210
+
211
+ def _provision_toolchain(
212
+ project_root: Path,
213
+ test_command: str | None,
214
+ config: dict,
215
+ *,
216
+ consent_cb: Callable[[dict], bool] | None,
217
+ progress_cb: Callable[[dict], None] | None,
218
+ ) -> tuple[dict | None, str]:
219
+ """Provision a managed runtime for ``test_command`` when it's missing locally.
220
+
221
+ Returns ``(env, note)``. ``env`` (with the toolchain on ``PATH``) is returned
222
+ only when a runtime was actually provisioned; otherwise ``None`` (the caller
223
+ then runs with the inherited environment). No-ops when the command is already
224
+ runnable, has no managed runtime, or provisioning is disabled/declined.
225
+ """
226
+ if not test_command or not _managed_toolchain_enabled(config):
227
+ return None, ""
228
+ if _client_preflight_command(project_root, test_command):
229
+ return None, "" # already runnable on the host
230
+ binary = _leading_binary(test_command)
231
+ if not binary:
232
+ return None, ""
233
+ from refactor_core.host_toolchain import binary_to_runtime, guidance_for_binary
234
+
235
+ runtime = binary_to_runtime(binary)
236
+ if runtime is None:
237
+ # No self-provisionable runtime -> Tier 3 guided-install guidance so the
238
+ # developer gets an actionable message instead of a silent defer.
239
+ guidance = guidance_for_binary(binary)
240
+ if guidance:
241
+ return None, f"'{binary}' is not installed and cannot be auto-provisioned. {guidance}"
242
+ return None, ""
243
+
244
+ if consent_cb is not None:
245
+ approved = bool(consent_cb({"binary": binary, "runtime": runtime}))
246
+ if not approved:
247
+ return None, f"managed toolchain for '{binary}' declined; skipping provisioning."
248
+
249
+ def _on_progress(msg: str) -> None:
250
+ _emit(progress_cb, phase="rr", event="toolchain_provisioning", detail=msg)
251
+
252
+ env, note = provision_for_command(binary, on_progress=_on_progress)
253
+ if env is None:
254
+ return None, note
255
+ _emit(progress_cb, phase="rr", event="toolchain_ready", runtime=runtime)
256
+ _restore_dependencies(project_root, runtime, env, progress_cb)
257
+ return env, note
258
+
259
+
260
+ def _restore_dependencies(
261
+ project_root: Path, runtime: str, env: dict, progress_cb: Callable[[dict], None] | None
262
+ ) -> None:
263
+ """Restore project dependencies with the provisioned toolchain (best effort).
264
+
265
+ Mirrors the sandbox dependency-restore step so the test command has its deps
266
+ present. Failures are non-fatal: the test run surfaces the real error.
267
+ """
268
+ if runtime == "node" and (project_root / "package.json").is_file():
269
+ _emit(progress_cb, phase="rr", event="toolchain_provisioning", detail="npm install")
270
+ _run_test(project_root, "npm install", env=env)
271
+ elif runtime == "python":
272
+ if (project_root / "requirements.txt").is_file():
273
+ _emit(progress_cb, phase="rr", event="toolchain_provisioning",
274
+ detail="pip install -r requirements.txt")
275
+ _run_test(project_root, "python -m pip install -r requirements.txt", env=env)
276
+ elif (project_root / "pyproject.toml").is_file() or (project_root / "setup.py").is_file():
277
+ _emit(progress_cb, phase="rr", event="toolchain_provisioning", detail="pip install -e .")
278
+ _run_test(project_root, "python -m pip install -e .", env=env)
156
279
 
157
280
 
158
281
  def _collect_test_manifests(
@@ -288,13 +411,22 @@ def _resolve_cloud_test_command(
288
411
  "toolchain or update verification.tests.command."
289
412
  )
290
413
 
291
- # 2) Root-marker / monorepo detection: only usable if runnable on the host.
414
+ # 2) Root-marker / monorepo detection. Usable if runnable on the host OR if
415
+ # its runtime can be provisioned by the managed toolchain (the caller then
416
+ # downloads it before the baseline). Only when neither holds do we fall
417
+ # through to server discovery, so we never lock onto a command that is
418
+ # truly un-runnable (e.g. a Tier-3 binary with no managed runtime).
292
419
  detected = resolve_test_command(project_root, config) or discover_test_command(project_root)
293
420
  if detected:
294
421
  if _client_preflight_command(project_root, detected):
295
422
  return detected, f"auto-detected test command: {detected}"
296
- # Detected but not runnable on the host -> fall through to server
297
- # discovery rather than committing to an un-runnable command.
423
+ if _managed_toolchain_enabled(config) and _detected_is_provisionable(detected):
424
+ return detected, (
425
+ f"auto-detected test command: {detected} "
426
+ "(runtime not on host PATH; will provision a managed toolchain)"
427
+ )
428
+ # Detected but neither runnable nor provisionable -> fall through to
429
+ # server discovery rather than committing to an un-runnable command.
298
430
  detected_note = (
299
431
  f"auto-detected test command '{detected}' but its toolchain was not "
300
432
  "found on the host PATH; trying server discovery."
@@ -410,6 +542,7 @@ def run_cloud_refactor_requests(
410
542
  progress_cb: Callable[[dict], None] | None = None,
411
543
  approval_cb: Callable[[dict], bool] | None = None,
412
544
  apply_byok_payload: Callable[[dict, object], None] | None = None,
545
+ toolchain_consent_cb: Callable[[dict], bool] | None = None,
413
546
  ) -> RunArtifact:
414
547
  """Drive pending RRs through the cloud server-run loop and return a summary.
415
548
 
@@ -417,6 +550,8 @@ def run_cloud_refactor_requests(
417
550
  (``_rr_progress_logger`` / ``_rr_approval_prompt``), so the run streams the
418
551
  same events and prompts per RR mid-run (FR8). ``apply_byok_payload`` is the
419
552
  shared BYOK resolver injected by the caller to avoid a circular import.
553
+ ``toolchain_consent_cb`` gates first-run managed-toolchain provisioning; when
554
+ None, provisioning proceeds without prompting (e.g. ``--auto-approve``).
420
555
  """
421
556
  project_root = Path(project_root)
422
557
  progress_cb = progress_cb or (lambda e: None)
@@ -434,6 +569,18 @@ def run_cloud_refactor_requests(
434
569
  apply_byok_payload=apply_byok_payload,
435
570
  )
436
571
 
572
+ # Managed host toolchain: when the resolved command's runtime is missing from
573
+ # the host PATH, download a pinned runtime into the central store and run
574
+ # tests with it (no container, no system changes). ``toolchain_env`` is None
575
+ # when nothing was provisioned (already runnable / no managed runtime).
576
+ toolchain_env, toolchain_note = _provision_toolchain(
577
+ project_root,
578
+ test_command,
579
+ config,
580
+ consent_cb=toolchain_consent_cb,
581
+ progress_cb=progress_cb,
582
+ )
583
+
437
584
  if requests is None:
438
585
  requests = pending_requests(project_root)
439
586
 
@@ -451,6 +598,8 @@ def run_cloud_refactor_requests(
451
598
  notes: list[str] = []
452
599
  if testcmd_note:
453
600
  notes.append(testcmd_note)
601
+ if toolchain_note:
602
+ notes.append(toolchain_note)
454
603
  stopped = False
455
604
  stop_reason = ""
456
605
  model_id = resolved_model
@@ -534,9 +683,10 @@ def run_cloud_refactor_requests(
534
683
  current_rr_id = action.get("rr_id", "") or current_rr_id
535
684
  _emit(progress_cb, phase="rr", event="test_generating", rr_id=current_rr_id)
536
685
  _write_generated_test(project_root, current_rr_id, action, by_id)
537
- passed, detail = _run_test(project_root, test_command)
686
+ passed, detail = _run_test(project_root, test_command, env=toolchain_env)
538
687
  _emit(progress_cb, phase="rr", event="baseline_test", rr_id=current_rr_id,
539
- passed=passed, detail=detail, execution_env="host")
688
+ passed=passed, detail=detail,
689
+ execution_env="host+toolchain" if toolchain_env else "host")
540
690
  action = session.step(
541
691
  _step_payload(token, current_rr_id, "baseline_result", by_id, project_root,
542
692
  baseline_passed=passed, baseline_detail=detail)
@@ -585,7 +735,7 @@ def run_cloud_refactor_requests(
585
735
  current_rr_id = action.get("rr_id", "") or current_rr_id
586
736
  diff_text = _PENDING_DIFF.get(current_rr_id, "")
587
737
  passed, detail, files = _apply_and_verify(
588
- project_root, run_store.run_dir, diff_text, test_command
738
+ project_root, run_store.run_dir, diff_text, test_command, env=toolchain_env
589
739
  )
590
740
  if passed:
591
741
  _emit(progress_cb, phase="rr", event="rr_applied", rr_id=current_rr_id, files=files)
@@ -661,13 +811,14 @@ def _write_generated_test(project_root: Path, rr_id: str, action: dict, by_id: d
661
811
 
662
812
 
663
813
  def _apply_and_verify(
664
- project_root: Path, run_dir: Path, diff_text: str, test_command
814
+ project_root: Path, run_dir: Path, diff_text: str, test_command, *, env: dict | None = None
665
815
  ) -> tuple[bool, str, list[str]]:
666
816
  """Apply the diff (with backup), run the test; revert on red.
667
817
 
668
818
  Returns ``(passed, detail, changed_files)``. ``changed_files`` is computed
669
819
  from the diff BEFORE applying (the tree is mutated in place afterward, so it
670
- cannot be recomputed against the modified files).
820
+ cannot be recomputed against the modified files). ``env`` optionally supplies
821
+ the managed-toolchain environment used to run the test.
671
822
  """
672
823
  if not diff_text:
673
824
  return False, "empty diff", []
@@ -682,7 +833,7 @@ def _apply_and_verify(
682
833
  apply_changes(project_root, run_dir, changes)
683
834
  except Exception as exc:
684
835
  return False, f"apply failed: {exc}", changed_files
685
- passed, detail = _run_test(project_root, test_command)
836
+ passed, detail = _run_test(project_root, test_command, env=env)
686
837
  if not passed:
687
838
  try:
688
839
  revert_from_backup(project_root, run_dir)
@@ -460,6 +460,29 @@ def _rr_approval_prompt(*, auto_approve: bool) -> Callable[[dict], bool]:
460
460
  return _ask
461
461
 
462
462
 
463
+ def _toolchain_consent_prompt(*, auto_approve: bool) -> Callable[[dict], bool]:
464
+ """First-run consent for downloading a managed language runtime.
465
+
466
+ Managed toolchains are downloaded into the central store (``~/.refactor``),
467
+ never system paths, so this is low-risk; we still confirm once per run before
468
+ fetching. ``--auto-approve`` and non-interactive terminals proceed silently.
469
+ """
470
+
471
+ def _ask(info: dict) -> bool:
472
+ binary = str(info.get("binary", "?"))
473
+ runtime = str(info.get("runtime", "?"))
474
+ if auto_approve or not sys.stdin.isatty():
475
+ return True
476
+ console.print(
477
+ f"[dim]'{binary}' was not found on your PATH. refactor can download a "
478
+ f"pinned {runtime} runtime into ~/.refactor (no system changes, no container).[/dim]"
479
+ )
480
+ raw = typer.prompt("Download managed toolchain? (Y/n)", default="Y")
481
+ return raw.strip().lower() in {"", "y", "yes"}
482
+
483
+ return _ask
484
+
485
+
463
486
  def _rr_progress_logger(status=None) -> Callable[[dict], None]:
464
487
  """Render the per-RR interactive flow with clear, dynamic messages."""
465
488
 
@@ -482,6 +505,16 @@ def _rr_progress_logger(status=None) -> Callable[[dict], None]:
482
505
  f"[bold cyan]code:[/bold cyan] running in sandbox: [dim]{cmd}[/dim] "
483
506
  f"(container={container or '?'})"
484
507
  )
508
+ elif ev == "toolchain_provisioning":
509
+ detail = str(event.get("detail", "") or "")
510
+ if status:
511
+ status.update(f"[cyan]code[/cyan] provisioning toolchain: {detail}")
512
+ console.print(f"[dim] toolchain: {detail}[/dim]")
513
+ elif ev == "toolchain_ready":
514
+ console.print(
515
+ f"[green] toolchain ready:[/green] {event.get('runtime', '?')} "
516
+ "(managed; no container required)"
517
+ )
485
518
  elif ev == "rr_start":
486
519
  if status:
487
520
  status.update(f"[cyan]code[/cyan] working on {event.get('location', '?')}...")
@@ -2211,6 +2244,7 @@ def code(
2211
2244
  progress_cb=_rr_progress_logger(status=status),
2212
2245
  approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
2213
2246
  apply_byok_payload=_apply_byok_payload,
2247
+ toolchain_consent_cb=_toolchain_consent_prompt(auto_approve=auto_approve),
2214
2248
  )
2215
2249
  else:
2216
2250
  # Local mode is unchanged: dispatch to the local executor with the
@@ -0,0 +1,57 @@
1
+ """Managed host-toolchain commands: inspect and prune provisioned runtimes.
2
+
3
+ Managed toolchains are pinned language runtimes (Node/Python/Go) downloaded into
4
+ the central store (``~/.refactor/toolchains``) so cloud modes can run tests on
5
+ the host without a container. These commands let developers see and clean up
6
+ what has been provisioned.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from refactor_core import host_toolchain as ht
16
+
17
+ console = Console()
18
+ app = typer.Typer(help="Inspect and manage managed language toolchains.")
19
+
20
+
21
+ @app.command("list")
22
+ def list_toolchains() -> None:
23
+ """List runtimes provisioned into the central store."""
24
+ installed = ht.list_installed()
25
+ if not installed:
26
+ console.print("[dim]No managed toolchains provisioned.[/dim]")
27
+ return
28
+ table = Table(title="Managed toolchains")
29
+ table.add_column("runtime")
30
+ table.add_column("version")
31
+ table.add_column("platform")
32
+ table.add_column("path", overflow="fold")
33
+ for entry in installed:
34
+ table.add_row(entry["runtime"], entry["version"], entry["platform"], entry["path"])
35
+ console.print(table)
36
+
37
+
38
+ @app.command("prune")
39
+ def prune_toolchains(
40
+ runtime: str = typer.Option(
41
+ None, "--runtime", help="Only remove this runtime (e.g. node, python, go)."
42
+ ),
43
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
44
+ ) -> None:
45
+ """Remove provisioned toolchains to reclaim disk space."""
46
+ target = runtime or "all managed toolchains"
47
+ if not yes:
48
+ confirm = typer.prompt(f"Remove {target}? (y/N)", default="N")
49
+ if confirm.strip().lower() not in {"y", "yes"}:
50
+ console.print("[dim]Aborted.[/dim]")
51
+ raise typer.Exit(code=0)
52
+ removed = ht.prune(runtime=runtime)
53
+ if not removed:
54
+ console.print("[dim]Nothing to remove.[/dim]")
55
+ return
56
+ for path in removed:
57
+ console.print(f"[green]removed[/green] {path}")
@@ -17,6 +17,7 @@ from refactorai_cli.commands import (
17
17
  runtime_cmds,
18
18
  runtime_proxy_cmds,
19
19
  setup_cmds,
20
+ toolchains_cmds,
20
21
  )
21
22
 
22
23
  app = typer.Typer(
@@ -130,6 +131,9 @@ app.add_typer(model_cmds.app, name="model")
130
131
  # Cloud (BYOK) credential management (R20).
131
132
  app.add_typer(cloud_cmds.app, name="cloud")
132
133
 
134
+ # Managed host toolchains (R24 Phase 2).
135
+ app.add_typer(toolchains_cmds.app, name="toolchains")
136
+
133
137
 
134
138
  if __name__ == "__main__":
135
139
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
8
8
  Requires-Dist: httpx>=0.27.0
9
9
  Requires-Dist: rich>=13.7.0
10
10
  Requires-Dist: PyYAML>=6.0.1
11
- Requires-Dist: refactorai-core>=3.0.5
11
+ Requires-Dist: refactorai-core>=3.1.0
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -30,4 +30,5 @@ refactorai_cli/commands/rules_cmds.py
30
30
  refactorai_cli/commands/run_cmds.py
31
31
  refactorai_cli/commands/runtime_cmds.py
32
32
  refactorai_cli/commands/runtime_proxy_cmds.py
33
- refactorai_cli/commands/setup_cmds.py
33
+ refactorai_cli/commands/setup_cmds.py
34
+ refactorai_cli/commands/toolchains_cmds.py
@@ -2,4 +2,4 @@ typer>=0.12.0
2
2
  httpx>=0.27.0
3
3
  rich>=13.7.0
4
4
  PyYAML>=6.0.1
5
- refactorai-core>=3.0.5
5
+ refactorai-core>=3.1.0