refactorai-cli 0.3.5__tar.gz → 0.3.9__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.
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/PKG-INFO +2 -2
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/pyproject.toml +2 -2
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/cloud_rr.py +192 -30
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/run_cmds.py +34 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/runtime_proxy_cmds.py +11 -1
- refactorai_cli-0.3.9/refactorai_cli/commands/toolchains_cmds.py +57 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/main.py +19 -8
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/SOURCES.txt +2 -1
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/README.md +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.3.5 → refactorai_cli-0.3.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
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
|
|
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.
|
|
3
|
+
version = "0.3.9"
|
|
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
|
|
15
|
+
"refactorai-core>=3.1.0",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -50,8 +50,14 @@ 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
|
-
from refactor_core.testcmd import
|
|
55
|
+
from refactor_core.testcmd import (
|
|
56
|
+
_SKIP_DIRS,
|
|
57
|
+
_configured_command,
|
|
58
|
+
discover_test_command,
|
|
59
|
+
resolve_test_command,
|
|
60
|
+
)
|
|
55
61
|
|
|
56
62
|
from refactorai_cli.auth import ensure_authenticated
|
|
57
63
|
from refactorai_cli.settings import platform_url
|
|
@@ -93,7 +99,9 @@ def _read_source(project_root: Path, rel: str) -> str:
|
|
|
93
99
|
return ""
|
|
94
100
|
|
|
95
101
|
|
|
96
|
-
def _run_test(
|
|
102
|
+
def _run_test(
|
|
103
|
+
project_root: Path, test_command: str | None, *, env: dict | None = None
|
|
104
|
+
) -> tuple[bool, str]:
|
|
97
105
|
"""Run the configured test command on the host; (passed, detail).
|
|
98
106
|
|
|
99
107
|
Cloud runs execute tests client-side because the developer's code + deps live
|
|
@@ -101,6 +109,9 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
|
|
|
101
109
|
command is configured we return ``(False, ...)`` so the server defers the RR
|
|
102
110
|
rather than applying an unverifiable change. This differs from local's
|
|
103
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.
|
|
104
115
|
"""
|
|
105
116
|
if not test_command or not str(test_command).strip():
|
|
106
117
|
return False, "no test command configured; cannot verify behavior preservation"
|
|
@@ -115,6 +126,7 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
|
|
|
115
126
|
capture_output=True,
|
|
116
127
|
text=True,
|
|
117
128
|
timeout=600,
|
|
129
|
+
env=env,
|
|
118
130
|
)
|
|
119
131
|
except (subprocess.SubprocessError, OSError) as exc:
|
|
120
132
|
return False, f"test command error: {exc}"
|
|
@@ -123,31 +135,132 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
|
|
|
123
135
|
return False, (proc.stdout or proc.stderr or "")[-500:]
|
|
124
136
|
|
|
125
137
|
|
|
126
|
-
def
|
|
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:
|
|
127
162
|
"""Return True when ``command`` looks runnable on this host.
|
|
128
163
|
|
|
129
164
|
Composite/shell commands (``&&``, subshells, pipes) run under ``shell=True``
|
|
130
165
|
and can't be reduced to one binary, so we trust them and let the real exit
|
|
131
166
|
code gate. Simple commands are validated by checking the leading binary is on
|
|
132
167
|
PATH — this guards against running (and charging for) a server-suggested
|
|
133
|
-
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).
|
|
134
170
|
"""
|
|
135
171
|
if not command or not str(command).strip():
|
|
136
172
|
return False
|
|
137
173
|
if any(op in command for op in ("&&", "||", ";", "|", "(", ")")):
|
|
138
174
|
return True
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
except ValueError:
|
|
142
|
-
return False
|
|
143
|
-
while tokens and _ENV_ASSIGN_RE.match(tokens[0]):
|
|
144
|
-
tokens.pop(0)
|
|
145
|
-
if not tokens:
|
|
175
|
+
binary = _leading_binary(command)
|
|
176
|
+
if not binary:
|
|
146
177
|
return False
|
|
147
|
-
binary = tokens[0]
|
|
148
178
|
if "/" in binary:
|
|
149
179
|
return Path(binary).expanduser().exists()
|
|
150
|
-
|
|
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 _provision_toolchain(
|
|
197
|
+
project_root: Path,
|
|
198
|
+
test_command: str | None,
|
|
199
|
+
config: dict,
|
|
200
|
+
*,
|
|
201
|
+
consent_cb: Callable[[dict], bool] | None,
|
|
202
|
+
progress_cb: Callable[[dict], None] | None,
|
|
203
|
+
) -> tuple[dict | None, str]:
|
|
204
|
+
"""Provision a managed runtime for ``test_command`` when it's missing locally.
|
|
205
|
+
|
|
206
|
+
Returns ``(env, note)``. ``env`` (with the toolchain on ``PATH``) is returned
|
|
207
|
+
only when a runtime was actually provisioned; otherwise ``None`` (the caller
|
|
208
|
+
then runs with the inherited environment). No-ops when the command is already
|
|
209
|
+
runnable, has no managed runtime, or provisioning is disabled/declined.
|
|
210
|
+
"""
|
|
211
|
+
if not test_command or not _managed_toolchain_enabled(config):
|
|
212
|
+
return None, ""
|
|
213
|
+
if _client_preflight_command(project_root, test_command):
|
|
214
|
+
return None, "" # already runnable on the host
|
|
215
|
+
binary = _leading_binary(test_command)
|
|
216
|
+
if not binary:
|
|
217
|
+
return None, ""
|
|
218
|
+
from refactor_core.host_toolchain import binary_to_runtime, guidance_for_binary
|
|
219
|
+
|
|
220
|
+
runtime = binary_to_runtime(binary)
|
|
221
|
+
if runtime is None:
|
|
222
|
+
# No self-provisionable runtime -> Tier 3 guided-install guidance so the
|
|
223
|
+
# developer gets an actionable message instead of a silent defer.
|
|
224
|
+
guidance = guidance_for_binary(binary)
|
|
225
|
+
if guidance:
|
|
226
|
+
return None, f"'{binary}' is not installed and cannot be auto-provisioned. {guidance}"
|
|
227
|
+
return None, ""
|
|
228
|
+
|
|
229
|
+
if consent_cb is not None:
|
|
230
|
+
approved = bool(consent_cb({"binary": binary, "runtime": runtime}))
|
|
231
|
+
if not approved:
|
|
232
|
+
return None, f"managed toolchain for '{binary}' declined; skipping provisioning."
|
|
233
|
+
|
|
234
|
+
def _on_progress(msg: str) -> None:
|
|
235
|
+
_emit(progress_cb, phase="rr", event="toolchain_provisioning", detail=msg)
|
|
236
|
+
|
|
237
|
+
env, note = provision_for_command(binary, on_progress=_on_progress)
|
|
238
|
+
if env is None:
|
|
239
|
+
return None, note
|
|
240
|
+
_emit(progress_cb, phase="rr", event="toolchain_ready", runtime=runtime)
|
|
241
|
+
_restore_dependencies(project_root, runtime, env, progress_cb)
|
|
242
|
+
return env, note
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _restore_dependencies(
|
|
246
|
+
project_root: Path, runtime: str, env: dict, progress_cb: Callable[[dict], None] | None
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Restore project dependencies with the provisioned toolchain (best effort).
|
|
249
|
+
|
|
250
|
+
Mirrors the sandbox dependency-restore step so the test command has its deps
|
|
251
|
+
present. Failures are non-fatal: the test run surfaces the real error.
|
|
252
|
+
"""
|
|
253
|
+
if runtime == "node" and (project_root / "package.json").is_file():
|
|
254
|
+
_emit(progress_cb, phase="rr", event="toolchain_provisioning", detail="npm install")
|
|
255
|
+
_run_test(project_root, "npm install", env=env)
|
|
256
|
+
elif runtime == "python":
|
|
257
|
+
if (project_root / "requirements.txt").is_file():
|
|
258
|
+
_emit(progress_cb, phase="rr", event="toolchain_provisioning",
|
|
259
|
+
detail="pip install -r requirements.txt")
|
|
260
|
+
_run_test(project_root, "python -m pip install -r requirements.txt", env=env)
|
|
261
|
+
elif (project_root / "pyproject.toml").is_file() or (project_root / "setup.py").is_file():
|
|
262
|
+
_emit(progress_cb, phase="rr", event="toolchain_provisioning", detail="pip install -e .")
|
|
263
|
+
_run_test(project_root, "python -m pip install -e .", env=env)
|
|
151
264
|
|
|
152
265
|
|
|
153
266
|
def _collect_test_manifests(
|
|
@@ -257,18 +370,47 @@ def _resolve_cloud_test_command(
|
|
|
257
370
|
"""Resolve a test command for a cloud run; return ``(command, note)``.
|
|
258
371
|
|
|
259
372
|
Layered resolution so the developer usually needs no config:
|
|
260
|
-
1. configured ``verification.tests.command``
|
|
261
|
-
2. deterministic monorepo discovery (subproject
|
|
262
|
-
|
|
373
|
+
1. configured ``verification.tests.command`` (explicit developer intent),
|
|
374
|
+
2. root-marker detection / deterministic monorepo discovery (subproject
|
|
375
|
+
test setups),
|
|
376
|
+
3. agentic discovery (server infers from manifests).
|
|
377
|
+
|
|
378
|
+
Cloud runs execute tests on the HOST (no sandbox), so every candidate is
|
|
379
|
+
preflight-validated against the host ``PATH`` before it is committed to. An
|
|
380
|
+
explicitly *configured* command is honored even when preflight fails (the
|
|
381
|
+
developer may know something we don't, e.g. a shell-resolved binary), but the
|
|
382
|
+
mismatch is surfaced as a note. Marker/monorepo-*detected* commands that fail
|
|
383
|
+
preflight are NOT returned: resolution falls through to server discovery so a
|
|
384
|
+
runnable alternative can be suggested instead of locking in an un-runnable
|
|
385
|
+
command (e.g. ``npm test`` when ``npm`` is not installed on the host).
|
|
263
386
|
"""
|
|
264
|
-
configured
|
|
387
|
+
# 1) Explicitly configured command: honor developer intent, but warn if the
|
|
388
|
+
# toolchain is not visible on the host PATH.
|
|
389
|
+
configured = _configured_command(config)
|
|
265
390
|
if configured:
|
|
266
|
-
|
|
391
|
+
if _client_preflight_command(project_root, configured):
|
|
392
|
+
return configured, ""
|
|
393
|
+
return configured, (
|
|
394
|
+
f"configured test command '{configured}' was not found on the host PATH; "
|
|
395
|
+
"cloud runs execute tests on the host, so it may fail. Install the "
|
|
396
|
+
"toolchain or update verification.tests.command."
|
|
397
|
+
)
|
|
267
398
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
399
|
+
# 2) Root-marker / monorepo detection: only usable if runnable on the host.
|
|
400
|
+
detected = resolve_test_command(project_root, config) or discover_test_command(project_root)
|
|
401
|
+
if detected:
|
|
402
|
+
if _client_preflight_command(project_root, detected):
|
|
403
|
+
return detected, f"auto-detected test command: {detected}"
|
|
404
|
+
# Detected but not runnable on the host -> fall through to server
|
|
405
|
+
# discovery rather than committing to an un-runnable command.
|
|
406
|
+
detected_note = (
|
|
407
|
+
f"auto-detected test command '{detected}' but its toolchain was not "
|
|
408
|
+
"found on the host PATH; trying server discovery."
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
detected_note = ""
|
|
271
412
|
|
|
413
|
+
# 3) Server-assisted discovery, preflight-validated.
|
|
272
414
|
suggested = _discover_test_command_via_cloud(
|
|
273
415
|
project_root=project_root,
|
|
274
416
|
constitution=constitution,
|
|
@@ -280,11 +422,12 @@ def _resolve_cloud_test_command(
|
|
|
280
422
|
if suggested and _client_preflight_command(project_root, suggested):
|
|
281
423
|
return suggested, f"server-suggested test command: {suggested}"
|
|
282
424
|
if suggested:
|
|
283
|
-
|
|
425
|
+
note = (
|
|
284
426
|
f"server suggested test command '{suggested}' but its toolchain was not "
|
|
285
|
-
"found on PATH; skipped."
|
|
427
|
+
"found on the host PATH; skipped."
|
|
286
428
|
)
|
|
287
|
-
|
|
429
|
+
return None, f"{detected_note} {note}".strip() if detected_note else note
|
|
430
|
+
return None, detected_note
|
|
288
431
|
|
|
289
432
|
|
|
290
433
|
class _Session:
|
|
@@ -375,6 +518,7 @@ def run_cloud_refactor_requests(
|
|
|
375
518
|
progress_cb: Callable[[dict], None] | None = None,
|
|
376
519
|
approval_cb: Callable[[dict], bool] | None = None,
|
|
377
520
|
apply_byok_payload: Callable[[dict, object], None] | None = None,
|
|
521
|
+
toolchain_consent_cb: Callable[[dict], bool] | None = None,
|
|
378
522
|
) -> RunArtifact:
|
|
379
523
|
"""Drive pending RRs through the cloud server-run loop and return a summary.
|
|
380
524
|
|
|
@@ -382,6 +526,8 @@ def run_cloud_refactor_requests(
|
|
|
382
526
|
(``_rr_progress_logger`` / ``_rr_approval_prompt``), so the run streams the
|
|
383
527
|
same events and prompts per RR mid-run (FR8). ``apply_byok_payload`` is the
|
|
384
528
|
shared BYOK resolver injected by the caller to avoid a circular import.
|
|
529
|
+
``toolchain_consent_cb`` gates first-run managed-toolchain provisioning; when
|
|
530
|
+
None, provisioning proceeds without prompting (e.g. ``--auto-approve``).
|
|
385
531
|
"""
|
|
386
532
|
project_root = Path(project_root)
|
|
387
533
|
progress_cb = progress_cb or (lambda e: None)
|
|
@@ -399,6 +545,18 @@ def run_cloud_refactor_requests(
|
|
|
399
545
|
apply_byok_payload=apply_byok_payload,
|
|
400
546
|
)
|
|
401
547
|
|
|
548
|
+
# Managed host toolchain: when the resolved command's runtime is missing from
|
|
549
|
+
# the host PATH, download a pinned runtime into the central store and run
|
|
550
|
+
# tests with it (no container, no system changes). ``toolchain_env`` is None
|
|
551
|
+
# when nothing was provisioned (already runnable / no managed runtime).
|
|
552
|
+
toolchain_env, toolchain_note = _provision_toolchain(
|
|
553
|
+
project_root,
|
|
554
|
+
test_command,
|
|
555
|
+
config,
|
|
556
|
+
consent_cb=toolchain_consent_cb,
|
|
557
|
+
progress_cb=progress_cb,
|
|
558
|
+
)
|
|
559
|
+
|
|
402
560
|
if requests is None:
|
|
403
561
|
requests = pending_requests(project_root)
|
|
404
562
|
|
|
@@ -416,6 +574,8 @@ def run_cloud_refactor_requests(
|
|
|
416
574
|
notes: list[str] = []
|
|
417
575
|
if testcmd_note:
|
|
418
576
|
notes.append(testcmd_note)
|
|
577
|
+
if toolchain_note:
|
|
578
|
+
notes.append(toolchain_note)
|
|
419
579
|
stopped = False
|
|
420
580
|
stop_reason = ""
|
|
421
581
|
model_id = resolved_model
|
|
@@ -499,9 +659,10 @@ def run_cloud_refactor_requests(
|
|
|
499
659
|
current_rr_id = action.get("rr_id", "") or current_rr_id
|
|
500
660
|
_emit(progress_cb, phase="rr", event="test_generating", rr_id=current_rr_id)
|
|
501
661
|
_write_generated_test(project_root, current_rr_id, action, by_id)
|
|
502
|
-
passed, detail = _run_test(project_root, test_command)
|
|
662
|
+
passed, detail = _run_test(project_root, test_command, env=toolchain_env)
|
|
503
663
|
_emit(progress_cb, phase="rr", event="baseline_test", rr_id=current_rr_id,
|
|
504
|
-
passed=passed, detail=detail,
|
|
664
|
+
passed=passed, detail=detail,
|
|
665
|
+
execution_env="host+toolchain" if toolchain_env else "host")
|
|
505
666
|
action = session.step(
|
|
506
667
|
_step_payload(token, current_rr_id, "baseline_result", by_id, project_root,
|
|
507
668
|
baseline_passed=passed, baseline_detail=detail)
|
|
@@ -550,7 +711,7 @@ def run_cloud_refactor_requests(
|
|
|
550
711
|
current_rr_id = action.get("rr_id", "") or current_rr_id
|
|
551
712
|
diff_text = _PENDING_DIFF.get(current_rr_id, "")
|
|
552
713
|
passed, detail, files = _apply_and_verify(
|
|
553
|
-
project_root, run_store.run_dir, diff_text, test_command
|
|
714
|
+
project_root, run_store.run_dir, diff_text, test_command, env=toolchain_env
|
|
554
715
|
)
|
|
555
716
|
if passed:
|
|
556
717
|
_emit(progress_cb, phase="rr", event="rr_applied", rr_id=current_rr_id, files=files)
|
|
@@ -626,13 +787,14 @@ def _write_generated_test(project_root: Path, rr_id: str, action: dict, by_id: d
|
|
|
626
787
|
|
|
627
788
|
|
|
628
789
|
def _apply_and_verify(
|
|
629
|
-
project_root: Path, run_dir: Path, diff_text: str, test_command
|
|
790
|
+
project_root: Path, run_dir: Path, diff_text: str, test_command, *, env: dict | None = None
|
|
630
791
|
) -> tuple[bool, str, list[str]]:
|
|
631
792
|
"""Apply the diff (with backup), run the test; revert on red.
|
|
632
793
|
|
|
633
794
|
Returns ``(passed, detail, changed_files)``. ``changed_files`` is computed
|
|
634
795
|
from the diff BEFORE applying (the tree is mutated in place afterward, so it
|
|
635
|
-
cannot be recomputed against the modified files).
|
|
796
|
+
cannot be recomputed against the modified files). ``env`` optionally supplies
|
|
797
|
+
the managed-toolchain environment used to run the test.
|
|
636
798
|
"""
|
|
637
799
|
if not diff_text:
|
|
638
800
|
return False, "empty diff", []
|
|
@@ -647,7 +809,7 @@ def _apply_and_verify(
|
|
|
647
809
|
apply_changes(project_root, run_dir, changes)
|
|
648
810
|
except Exception as exc:
|
|
649
811
|
return False, f"apply failed: {exc}", changed_files
|
|
650
|
-
passed, detail = _run_test(project_root, test_command)
|
|
812
|
+
passed, detail = _run_test(project_root, test_command, env=env)
|
|
651
813
|
if not passed:
|
|
652
814
|
try:
|
|
653
815
|
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
|
|
@@ -75,7 +75,17 @@ def _delegate_to_run_cmds(command_name: str, passthrough_args: list[str]) -> Non
|
|
|
75
75
|
real command signature (options, arguments, help) is honored without manual
|
|
76
76
|
argument parsing. Exit codes propagate to the caller.
|
|
77
77
|
"""
|
|
78
|
-
|
|
78
|
+
try:
|
|
79
|
+
from refactorai_cli.commands import run_cmds
|
|
80
|
+
except ModuleNotFoundError as exc:
|
|
81
|
+
if exc.name == "refactor_core.merge_gate":
|
|
82
|
+
console.print(
|
|
83
|
+
"[red]Installed refactor core is too old for this CLI.[/red]\n"
|
|
84
|
+
"Please upgrade both packages:\n"
|
|
85
|
+
" pip install --upgrade refactorai-core refactorai-cli"
|
|
86
|
+
)
|
|
87
|
+
raise typer.Exit(code=1) from exc
|
|
88
|
+
raise
|
|
79
89
|
|
|
80
90
|
func = getattr(run_cmds, command_name, None)
|
|
81
91
|
if func is None: # pragma: no cover - defensive; all cloud verbs exist
|
|
@@ -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(
|
|
@@ -40,6 +41,12 @@ _TITLE_BY_COMMAND = {
|
|
|
40
41
|
"review": "Refactor Review Agent",
|
|
41
42
|
"code": "Refactor Code Agent",
|
|
42
43
|
}
|
|
44
|
+
_COLOR_BY_COMMAND = {
|
|
45
|
+
"setup": ("245", "250", "247"), # base, soft, art
|
|
46
|
+
"doctor": ("39", "117", "81"),
|
|
47
|
+
"review": ("214", "222", "220"),
|
|
48
|
+
"code": ("46", "120", "82"),
|
|
49
|
+
}
|
|
43
50
|
|
|
44
51
|
|
|
45
52
|
def _print_agent_intro(command_name: str) -> None:
|
|
@@ -48,19 +55,20 @@ def _print_agent_intro(command_name: str) -> None:
|
|
|
48
55
|
return
|
|
49
56
|
title = _TITLE_BY_COMMAND.get(command_name, "Refactor Agent")
|
|
50
57
|
tip = _TIP_BY_COMMAND.get(command_name, "Next: refactor review .")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
base, soft, art = _COLOR_BY_COMMAND.get(command_name, ("46", "120", "82"))
|
|
59
|
+
color_bold = f"\033[1;38;5;{base}m"
|
|
60
|
+
color_soft = f"\033[38;5;{soft}m"
|
|
61
|
+
color_art = f"\033[38;5;{art}m"
|
|
54
62
|
reset = "\033[0m"
|
|
55
63
|
typer.echo("")
|
|
56
|
-
typer.echo(f"{
|
|
57
|
-
typer.echo(f"{
|
|
64
|
+
typer.echo(f"{color_bold}{title}{reset}")
|
|
65
|
+
typer.echo(f"{color_soft}Model: Sonnet 4.5{reset}")
|
|
58
66
|
typer.echo(f"Welcome back, {os.environ.get('USER', 'developer')}")
|
|
59
67
|
typer.echo("")
|
|
60
|
-
typer.echo(f"{
|
|
61
|
-
typer.echo(f"{
|
|
68
|
+
typer.echo(f"{color_art} * [■ ■] *{reset}")
|
|
69
|
+
typer.echo(f"{color_art} * \\_/ *{reset}")
|
|
62
70
|
typer.echo("")
|
|
63
|
-
typer.echo(f"{
|
|
71
|
+
typer.echo(f"{color_bold}Tip:{reset} {tip}")
|
|
64
72
|
typer.echo("")
|
|
65
73
|
|
|
66
74
|
|
|
@@ -123,6 +131,9 @@ app.add_typer(model_cmds.app, name="model")
|
|
|
123
131
|
# Cloud (BYOK) credential management (R20).
|
|
124
132
|
app.add_typer(cloud_cmds.app, name="cloud")
|
|
125
133
|
|
|
134
|
+
# Managed host toolchains (R24 Phase 2).
|
|
135
|
+
app.add_typer(toolchains_cmds.app, name="toolchains")
|
|
136
|
+
|
|
126
137
|
|
|
127
138
|
if __name__ == "__main__":
|
|
128
139
|
app()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|