refactorai-cli 0.6.0__tar.gz → 0.7.0__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.6.0 → refactorai_cli-0.7.0}/PKG-INFO +1 -1
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/pyproject.toml +1 -1
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/cloud_rr.py +290 -30
- refactorai_cli-0.7.0/refactorai_cli/commands/workspace_cmds.py +79 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/main.py +4 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/PKG-INFO +1 -1
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/SOURCES.txt +2 -1
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/README.md +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/branch_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/pre_push_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/run_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commands/watch_cmds.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/commit_telemetry.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/pre_push_gate.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/refactor_branch_store.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/watch_ledger.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/watch_state.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli/watch_supervisor.py +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/requires.txt +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.6.0 → refactorai_cli-0.7.0}/setup.cfg +0 -0
|
@@ -68,6 +68,13 @@ from refactorai_cli.auth import ensure_authenticated
|
|
|
68
68
|
from refactorai_cli.settings import platform_url
|
|
69
69
|
|
|
70
70
|
_ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
71
|
+
_WORKSPACE_DIRNAME = "workspace"
|
|
72
|
+
_WORKSPACE_NAME = "host"
|
|
73
|
+
_WORKSPACE_CACHE_DIRNAME = "cache"
|
|
74
|
+
# Directory names never mirrored into (or pruned from) the managed workspace:
|
|
75
|
+
# git metadata, the central store, and heavy dependency dirs that are restored
|
|
76
|
+
# in-workspace and cached across runs.
|
|
77
|
+
_WORKSPACE_EXCLUDE = {".git", ".refactor", "node_modules"}
|
|
71
78
|
|
|
72
79
|
# Manifest/config files that reveal how a project runs its tests. Sent (truncated)
|
|
73
80
|
# to the server for agentic test-command discovery when deterministic detection
|
|
@@ -105,7 +112,7 @@ def _read_source(project_root: Path, rel: str) -> str:
|
|
|
105
112
|
|
|
106
113
|
|
|
107
114
|
def _run_test(
|
|
108
|
-
|
|
115
|
+
execution_root: Path, test_command: str | None, *, env: dict | None = None
|
|
109
116
|
) -> tuple[bool, str]:
|
|
110
117
|
"""Run the configured test command on the host; (passed, detail).
|
|
111
118
|
|
|
@@ -127,7 +134,7 @@ def _run_test(
|
|
|
127
134
|
proc = subprocess.run(
|
|
128
135
|
command,
|
|
129
136
|
shell=True,
|
|
130
|
-
cwd=str(
|
|
137
|
+
cwd=str(execution_root),
|
|
131
138
|
capture_output=True,
|
|
132
139
|
text=True,
|
|
133
140
|
timeout=600,
|
|
@@ -141,7 +148,7 @@ def _run_test(
|
|
|
141
148
|
|
|
142
149
|
|
|
143
150
|
def _run_command_with_progress(
|
|
144
|
-
|
|
151
|
+
execution_root: Path,
|
|
145
152
|
command: str,
|
|
146
153
|
*,
|
|
147
154
|
env: dict | None = None,
|
|
@@ -158,7 +165,7 @@ def _run_command_with_progress(
|
|
|
158
165
|
proc = subprocess.Popen(
|
|
159
166
|
command,
|
|
160
167
|
shell=True,
|
|
161
|
-
cwd=str(
|
|
168
|
+
cwd=str(execution_root),
|
|
162
169
|
stdout=subprocess.PIPE,
|
|
163
170
|
stderr=subprocess.STDOUT,
|
|
164
171
|
text=True,
|
|
@@ -403,13 +410,24 @@ def _write_json_atomic(path: Path, payload: dict) -> None:
|
|
|
403
410
|
|
|
404
411
|
|
|
405
412
|
def _node_lockfile(project_root: Path) -> Path | None:
|
|
406
|
-
for name in ("package-lock.json", "npm-shrinkwrap.json"):
|
|
413
|
+
for name in ("pnpm-lock.yaml", "yarn.lock", "package-lock.json", "npm-shrinkwrap.json"):
|
|
407
414
|
path = project_root / name
|
|
408
415
|
if path.is_file():
|
|
409
416
|
return path
|
|
410
417
|
return None
|
|
411
418
|
|
|
412
419
|
|
|
420
|
+
def _node_install_command(project_root: Path) -> tuple[str, str]:
|
|
421
|
+
lockfile = _node_lockfile(project_root)
|
|
422
|
+
if lockfile is None:
|
|
423
|
+
return "npm", "npm install"
|
|
424
|
+
if lockfile.name == "pnpm-lock.yaml":
|
|
425
|
+
return "corepack", "corepack pnpm install --frozen-lockfile"
|
|
426
|
+
if lockfile.name == "yarn.lock":
|
|
427
|
+
return "corepack", "corepack yarn install --immutable"
|
|
428
|
+
return "npm", "npm ci"
|
|
429
|
+
|
|
430
|
+
|
|
413
431
|
def _node_deps_state_path(project_root: Path) -> Path:
|
|
414
432
|
return resolve_store_dir(project_root) / "toolchain-state" / "node-deps.json"
|
|
415
433
|
|
|
@@ -492,7 +510,7 @@ def _merge_toolchain_env(base: dict | None, new: dict) -> dict:
|
|
|
492
510
|
|
|
493
511
|
|
|
494
512
|
def _provision_toolchain(
|
|
495
|
-
|
|
513
|
+
execution_root: Path,
|
|
496
514
|
test_command: str | None,
|
|
497
515
|
config: dict,
|
|
498
516
|
*,
|
|
@@ -517,6 +535,10 @@ def _provision_toolchain(
|
|
|
517
535
|
from refactor_core.host_toolchain import binary_to_runtime, guidance_for_binary
|
|
518
536
|
|
|
519
537
|
def _on_progress(msg: str) -> None:
|
|
538
|
+
# Warm-path reuse is expected on every run; suppress it to keep cloud RR
|
|
539
|
+
# progress noise low while still surfacing real downloads/extracts.
|
|
540
|
+
if str(msg).startswith("reusing "):
|
|
541
|
+
return
|
|
520
542
|
_emit(progress_cb, phase="rr", event="toolchain_provisioning", detail=msg)
|
|
521
543
|
|
|
522
544
|
merged_env: dict | None = None
|
|
@@ -546,7 +568,7 @@ def _provision_toolchain(
|
|
|
546
568
|
continue
|
|
547
569
|
_emit(progress_cb, phase="rr", event="toolchain_ready", runtime=runtime)
|
|
548
570
|
merged_env = _merge_toolchain_env(merged_env, env)
|
|
549
|
-
_restore_dependencies(
|
|
571
|
+
_restore_dependencies(execution_root, runtime, merged_env, progress_cb, config=config)
|
|
550
572
|
|
|
551
573
|
return merged_env, " ".join(n for n in notes if n).strip()
|
|
552
574
|
|
|
@@ -574,8 +596,7 @@ def _restore_dependencies(
|
|
|
574
596
|
)
|
|
575
597
|
return
|
|
576
598
|
if runtime == "node" and (project_root / "package.json").is_file():
|
|
577
|
-
|
|
578
|
-
install_command = "npm ci" if lockfile is not None else "npm install"
|
|
599
|
+
install_binary, install_command = _node_install_command(project_root)
|
|
579
600
|
marker_payload: dict | None = None
|
|
580
601
|
if policy == "auto":
|
|
581
602
|
should_restore, reason, marker_payload = _should_restore_node_dependencies(
|
|
@@ -595,6 +616,17 @@ def _restore_dependencies(
|
|
|
595
616
|
event="toolchain_provisioning",
|
|
596
617
|
detail=f"step 1/1: {install_command} (streaming output below)",
|
|
597
618
|
)
|
|
619
|
+
if not _binary_on_path(install_binary, env=env):
|
|
620
|
+
_emit(
|
|
621
|
+
progress_cb,
|
|
622
|
+
phase="rr",
|
|
623
|
+
event="toolchain_provisioning",
|
|
624
|
+
detail=(
|
|
625
|
+
f"skipping node dependency restore: '{install_binary}' is not available "
|
|
626
|
+
f"for {install_command}"
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
return
|
|
598
630
|
ok, detail = _run_command_with_progress(
|
|
599
631
|
project_root,
|
|
600
632
|
install_command,
|
|
@@ -700,6 +732,184 @@ def _collect_test_manifests(
|
|
|
700
732
|
return docs
|
|
701
733
|
|
|
702
734
|
|
|
735
|
+
def _workspace_root(project_root: Path) -> Path:
|
|
736
|
+
return resolve_store_dir(project_root) / _WORKSPACE_DIRNAME / _WORKSPACE_NAME
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _workspace_cache_root(project_root: Path) -> Path:
|
|
740
|
+
return resolve_store_dir(project_root) / _WORKSPACE_CACHE_DIRNAME
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _workspace_env(project_root: Path, base_env: dict | None) -> dict | None:
|
|
744
|
+
env = dict(base_env) if base_env is not None else None
|
|
745
|
+
cache_root = _workspace_cache_root(project_root)
|
|
746
|
+
npm_cache = cache_root / "npm"
|
|
747
|
+
yarn_cache = cache_root / "yarn"
|
|
748
|
+
pnpm_store = cache_root / "pnpm-store"
|
|
749
|
+
for directory in (npm_cache, yarn_cache, pnpm_store):
|
|
750
|
+
with contextlib.suppress(OSError):
|
|
751
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
752
|
+
if env is None:
|
|
753
|
+
return None
|
|
754
|
+
env.setdefault("npm_config_cache", str(npm_cache))
|
|
755
|
+
env.setdefault("YARN_CACHE_FOLDER", str(yarn_cache))
|
|
756
|
+
env.setdefault("PNPM_STORE_DIR", str(pnpm_store))
|
|
757
|
+
return env
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _has_excluded_component(rel: Path, exclude: set[str]) -> bool:
|
|
761
|
+
return any(part in exclude for part in rel.parts)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _needs_copy(src: Path, dst: Path) -> bool:
|
|
765
|
+
"""True when ``dst`` is missing or differs from ``src`` (size/mtime)."""
|
|
766
|
+
try:
|
|
767
|
+
s = src.stat()
|
|
768
|
+
except OSError:
|
|
769
|
+
return False
|
|
770
|
+
try:
|
|
771
|
+
d = dst.stat()
|
|
772
|
+
except OSError:
|
|
773
|
+
return True
|
|
774
|
+
return int(s.st_mtime) != int(d.st_mtime) or s.st_size != d.st_size
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _sync_working_tree(
|
|
778
|
+
src: Path, dst: Path, *, exclude: set[str], skip_abs: set[Path]
|
|
779
|
+
) -> None:
|
|
780
|
+
"""Incrementally mirror the working tree ``src`` into ``dst``.
|
|
781
|
+
|
|
782
|
+
Copies tracked *and* untracked files (so gitignored inputs a test needs — a
|
|
783
|
+
local ``.env``, fixtures — are present) while skipping heavy/managed dirs
|
|
784
|
+
(``exclude`` by name, ``skip_abs`` by absolute path). Unchanged files are
|
|
785
|
+
left alone (size+mtime check) so re-runs are cheap, and files deleted in the
|
|
786
|
+
source are removed from the mirror. Excluded dirs (e.g. the workspace's own
|
|
787
|
+
``node_modules``) are never copied *or* deleted, so dependency caches persist.
|
|
788
|
+
"""
|
|
789
|
+
src = Path(src)
|
|
790
|
+
dst = Path(dst)
|
|
791
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
792
|
+
|
|
793
|
+
seen_files: set[str] = set()
|
|
794
|
+
seen_dirs: set[str] = set()
|
|
795
|
+
for root, dirs, files in os.walk(src):
|
|
796
|
+
root_path = Path(root)
|
|
797
|
+
# Prune excluded/managed dirs in place so os.walk never descends them.
|
|
798
|
+
dirs[:] = [
|
|
799
|
+
d
|
|
800
|
+
for d in dirs
|
|
801
|
+
if d not in exclude and (root_path / d).resolve() not in skip_abs
|
|
802
|
+
]
|
|
803
|
+
rel_root = root_path.relative_to(src)
|
|
804
|
+
for d in dirs:
|
|
805
|
+
rel = rel_root / d
|
|
806
|
+
seen_dirs.add(rel.as_posix())
|
|
807
|
+
with contextlib.suppress(OSError):
|
|
808
|
+
(dst / rel).mkdir(parents=True, exist_ok=True)
|
|
809
|
+
for f in files:
|
|
810
|
+
rel = rel_root / f
|
|
811
|
+
seen_files.add(rel.as_posix())
|
|
812
|
+
source_file = src / rel
|
|
813
|
+
target_file = dst / rel
|
|
814
|
+
if _needs_copy(source_file, target_file):
|
|
815
|
+
with contextlib.suppress(OSError):
|
|
816
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
817
|
+
shutil.copy2(source_file, target_file)
|
|
818
|
+
|
|
819
|
+
# Deletion phase: drop mirror entries whose source no longer exists, never
|
|
820
|
+
# touching anything under an excluded component (node_modules, caches, .git).
|
|
821
|
+
for root, dirs, files in os.walk(dst, topdown=False):
|
|
822
|
+
rel_root = Path(root).relative_to(dst)
|
|
823
|
+
if _has_excluded_component(rel_root, exclude):
|
|
824
|
+
continue
|
|
825
|
+
for f in files:
|
|
826
|
+
rel = rel_root / f
|
|
827
|
+
if _has_excluded_component(rel, exclude):
|
|
828
|
+
continue
|
|
829
|
+
if rel.as_posix() not in seen_files:
|
|
830
|
+
with contextlib.suppress(OSError):
|
|
831
|
+
(Path(root) / f).unlink()
|
|
832
|
+
for d in dirs:
|
|
833
|
+
if d in exclude:
|
|
834
|
+
continue
|
|
835
|
+
rel = rel_root / d
|
|
836
|
+
if rel.as_posix() not in seen_dirs:
|
|
837
|
+
with contextlib.suppress(OSError):
|
|
838
|
+
(Path(root) / d).rmdir()
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _prepare_managed_workspace(project_root: Path, *, progress_cb: Callable[[dict], None] | None) -> Path:
|
|
842
|
+
"""Mirror the project's working tree into the managed workspace and return it.
|
|
843
|
+
|
|
844
|
+
The mirror (not a HEAD worktree) reflects exactly what the developer has on
|
|
845
|
+
disk, so behavior verification matches reality even with uncommitted or
|
|
846
|
+
gitignored files. Installs/tests run here; the real repo is never touched.
|
|
847
|
+
"""
|
|
848
|
+
workspace = _workspace_root(project_root)
|
|
849
|
+
with contextlib.suppress(OSError):
|
|
850
|
+
workspace.parent.mkdir(parents=True, exist_ok=True)
|
|
851
|
+
skip_abs: set[Path] = set()
|
|
852
|
+
with contextlib.suppress(Exception):
|
|
853
|
+
skip_abs.add(resolve_store_dir(project_root).resolve())
|
|
854
|
+
with contextlib.suppress(Exception):
|
|
855
|
+
skip_abs.add(workspace.resolve())
|
|
856
|
+
try:
|
|
857
|
+
_sync_working_tree(
|
|
858
|
+
project_root, workspace, exclude=set(_WORKSPACE_EXCLUDE), skip_abs=skip_abs
|
|
859
|
+
)
|
|
860
|
+
except OSError as exc:
|
|
861
|
+
raise CloudRRError(f"failed to prepare managed workspace: {exc}") from exc
|
|
862
|
+
_emit(
|
|
863
|
+
progress_cb,
|
|
864
|
+
phase="rr",
|
|
865
|
+
event="workspace_ready",
|
|
866
|
+
detail=f"managed workspace ready at {workspace}",
|
|
867
|
+
)
|
|
868
|
+
return workspace
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def prune_workspace(project_root: Path, *, include_cache: bool = False) -> list[str]:
|
|
872
|
+
"""Remove the managed workspace mirror (and optionally its dep caches).
|
|
873
|
+
|
|
874
|
+
Returns the list of removed absolute paths. Safe to call when nothing has
|
|
875
|
+
been provisioned yet (returns an empty list).
|
|
876
|
+
"""
|
|
877
|
+
removed: list[str] = []
|
|
878
|
+
base = resolve_store_dir(project_root) / _WORKSPACE_DIRNAME
|
|
879
|
+
if base.exists():
|
|
880
|
+
shutil.rmtree(base, ignore_errors=True)
|
|
881
|
+
removed.append(str(base))
|
|
882
|
+
if include_cache:
|
|
883
|
+
cache = _workspace_cache_root(project_root)
|
|
884
|
+
if cache.exists():
|
|
885
|
+
shutil.rmtree(cache, ignore_errors=True)
|
|
886
|
+
removed.append(str(cache))
|
|
887
|
+
return removed
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def workspace_paths(project_root: Path) -> dict:
|
|
891
|
+
"""Return the managed workspace + cache locations for ``project_root``."""
|
|
892
|
+
return {
|
|
893
|
+
"workspace": _workspace_root(project_root),
|
|
894
|
+
"cache": _workspace_cache_root(project_root),
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _git_status_porcelain(project_root: Path) -> str:
|
|
899
|
+
try:
|
|
900
|
+
proc = subprocess.run(
|
|
901
|
+
["git", "-C", str(project_root), "status", "--porcelain"],
|
|
902
|
+
capture_output=True,
|
|
903
|
+
text=True,
|
|
904
|
+
timeout=30,
|
|
905
|
+
)
|
|
906
|
+
except (subprocess.SubprocessError, OSError):
|
|
907
|
+
return ""
|
|
908
|
+
if proc.returncode != 0:
|
|
909
|
+
return ""
|
|
910
|
+
return (proc.stdout or "").strip()
|
|
911
|
+
|
|
912
|
+
|
|
703
913
|
def _discover_test_command_via_cloud(
|
|
704
914
|
*,
|
|
705
915
|
project_root: Path,
|
|
@@ -967,6 +1177,18 @@ def run_cloud_refactor_requests(
|
|
|
967
1177
|
if requests is None:
|
|
968
1178
|
requests = pending_requests(project_root)
|
|
969
1179
|
|
|
1180
|
+
run_store = create_run(project_root)
|
|
1181
|
+
run_id = run_store.run_id
|
|
1182
|
+
resolved_model = model_hint or str(
|
|
1183
|
+
constitution.get_setting("model_id", "managed-default") or "managed-default"
|
|
1184
|
+
)
|
|
1185
|
+
outcomes: list[dict] = []
|
|
1186
|
+
counts = {"applied": 0, "denied": 0, "failed": 0, "deferred": 0}
|
|
1187
|
+
notes: list[str] = []
|
|
1188
|
+
stopped = False
|
|
1189
|
+
stop_reason = ""
|
|
1190
|
+
model_id = resolved_model
|
|
1191
|
+
|
|
970
1192
|
_emit(
|
|
971
1193
|
progress_cb,
|
|
972
1194
|
phase="rr",
|
|
@@ -975,18 +1197,51 @@ def run_cloud_refactor_requests(
|
|
|
975
1197
|
test_command=test_command or "",
|
|
976
1198
|
execution_env="cloud",
|
|
977
1199
|
)
|
|
1200
|
+
execution_root = _prepare_managed_workspace(project_root, progress_cb=progress_cb)
|
|
1201
|
+
status_before = _git_status_porcelain(project_root)
|
|
978
1202
|
|
|
979
1203
|
# Managed host toolchain: when the resolved command's runtime is missing from
|
|
980
1204
|
# the host PATH, download a pinned runtime into the central store and run
|
|
981
1205
|
# tests with it (no container, no system changes). ``toolchain_env`` is None
|
|
982
1206
|
# when nothing was provisioned (already runnable / no managed runtime).
|
|
983
1207
|
toolchain_env, toolchain_note = _provision_toolchain(
|
|
984
|
-
|
|
1208
|
+
execution_root,
|
|
985
1209
|
test_command,
|
|
986
1210
|
config,
|
|
987
1211
|
consent_cb=toolchain_consent_cb,
|
|
988
1212
|
progress_cb=progress_cb,
|
|
989
1213
|
)
|
|
1214
|
+
toolchain_env = _workspace_env(project_root, toolchain_env)
|
|
1215
|
+
status_after_toolchain = _git_status_porcelain(project_root)
|
|
1216
|
+
if status_after_toolchain != status_before:
|
|
1217
|
+
reason = (
|
|
1218
|
+
"managed workspace invariant violated: toolchain/dependency setup changed the project "
|
|
1219
|
+
"working tree before any RR patch was applied; aborting run."
|
|
1220
|
+
)
|
|
1221
|
+
_emit(
|
|
1222
|
+
progress_cb,
|
|
1223
|
+
phase="rr",
|
|
1224
|
+
event="workspace_dirty_abort",
|
|
1225
|
+
reason=reason,
|
|
1226
|
+
)
|
|
1227
|
+
notes = []
|
|
1228
|
+
if testcmd_note:
|
|
1229
|
+
notes.append(testcmd_note)
|
|
1230
|
+
if toolchain_note:
|
|
1231
|
+
notes.append(toolchain_note)
|
|
1232
|
+
notes.append(reason)
|
|
1233
|
+
return _build_artifact(
|
|
1234
|
+
run_store,
|
|
1235
|
+
run_id,
|
|
1236
|
+
constitution,
|
|
1237
|
+
model_id,
|
|
1238
|
+
outcomes,
|
|
1239
|
+
counts,
|
|
1240
|
+
notes,
|
|
1241
|
+
stopped=True,
|
|
1242
|
+
stop_reason=reason,
|
|
1243
|
+
test_command=test_command,
|
|
1244
|
+
)
|
|
990
1245
|
|
|
991
1246
|
# python3-only hosts: rewrite a bare `python` test command to `python3` when
|
|
992
1247
|
# we did NOT provision a managed runtime (managed provisioning already
|
|
@@ -1003,23 +1258,10 @@ def run_cloud_refactor_requests(
|
|
|
1003
1258
|
previous=test_command,
|
|
1004
1259
|
)
|
|
1005
1260
|
test_command = normalized
|
|
1006
|
-
|
|
1007
|
-
run_store = create_run(project_root)
|
|
1008
|
-
run_id = run_store.run_id
|
|
1009
|
-
resolved_model = model_hint or str(
|
|
1010
|
-
constitution.get_setting("model_id", "managed-default") or "managed-default"
|
|
1011
|
-
)
|
|
1012
|
-
|
|
1013
|
-
outcomes: list[dict] = []
|
|
1014
|
-
counts = {"applied": 0, "denied": 0, "failed": 0, "deferred": 0}
|
|
1015
|
-
notes: list[str] = []
|
|
1016
1261
|
if testcmd_note:
|
|
1017
1262
|
notes.append(testcmd_note)
|
|
1018
1263
|
if toolchain_note:
|
|
1019
1264
|
notes.append(toolchain_note)
|
|
1020
|
-
stopped = False
|
|
1021
|
-
stop_reason = ""
|
|
1022
|
-
model_id = resolved_model
|
|
1023
1265
|
|
|
1024
1266
|
if not requests:
|
|
1025
1267
|
return _build_artifact(
|
|
@@ -1130,7 +1372,7 @@ def run_cloud_refactor_requests(
|
|
|
1130
1372
|
current_rr_id = action.get("rr_id", "") or current_rr_id
|
|
1131
1373
|
_emit(progress_cb, phase="rr", event="test_generating", rr_id=current_rr_id)
|
|
1132
1374
|
_write_generated_test(project_root, current_rr_id, action, by_id)
|
|
1133
|
-
passed, detail = _run_test(
|
|
1375
|
+
passed, detail = _run_test(execution_root, test_command, env=toolchain_env)
|
|
1134
1376
|
_emit(progress_cb, phase="rr", event="baseline_test", rr_id=current_rr_id,
|
|
1135
1377
|
passed=passed, detail=detail,
|
|
1136
1378
|
execution_env="host+toolchain" if toolchain_env else "host")
|
|
@@ -1207,7 +1449,12 @@ def run_cloud_refactor_requests(
|
|
|
1207
1449
|
approved=True,
|
|
1208
1450
|
)
|
|
1209
1451
|
passed, detail, files = _apply_and_verify(
|
|
1210
|
-
project_root,
|
|
1452
|
+
project_root,
|
|
1453
|
+
execution_root,
|
|
1454
|
+
run_store.run_dir,
|
|
1455
|
+
diff_text,
|
|
1456
|
+
test_command,
|
|
1457
|
+
env=toolchain_env,
|
|
1211
1458
|
)
|
|
1212
1459
|
pending.applied = True
|
|
1213
1460
|
if passed:
|
|
@@ -1307,7 +1554,13 @@ def _write_generated_test(project_root: Path, rr_id: str, action: dict, by_id: d
|
|
|
1307
1554
|
|
|
1308
1555
|
|
|
1309
1556
|
def _apply_and_verify(
|
|
1310
|
-
project_root: Path,
|
|
1557
|
+
project_root: Path,
|
|
1558
|
+
execution_root: Path,
|
|
1559
|
+
run_dir: Path,
|
|
1560
|
+
diff_text: str,
|
|
1561
|
+
test_command,
|
|
1562
|
+
*,
|
|
1563
|
+
env: dict | None = None,
|
|
1311
1564
|
) -> tuple[bool, str, list[str]]:
|
|
1312
1565
|
"""Apply the diff (with backup), run the test; revert on red.
|
|
1313
1566
|
|
|
@@ -1325,17 +1578,24 @@ def _apply_and_verify(
|
|
|
1325
1578
|
if not changes:
|
|
1326
1579
|
return False, "diff produced no file changes", []
|
|
1327
1580
|
changed_files = list(changes.keys())
|
|
1581
|
+
verify_backup_dir = run_dir / "workspace-backup"
|
|
1582
|
+
verify_backup_dir.mkdir(parents=True, exist_ok=True)
|
|
1328
1583
|
try:
|
|
1329
|
-
apply_changes(
|
|
1584
|
+
apply_changes(execution_root, verify_backup_dir, changes)
|
|
1330
1585
|
except Exception as exc:
|
|
1331
1586
|
return False, f"apply failed: {exc}", changed_files
|
|
1332
|
-
passed, detail = _run_test(
|
|
1587
|
+
passed, detail = _run_test(execution_root, test_command, env=env)
|
|
1333
1588
|
if not passed:
|
|
1334
1589
|
try:
|
|
1335
|
-
revert_from_backup(
|
|
1590
|
+
revert_from_backup(execution_root, verify_backup_dir)
|
|
1336
1591
|
except Exception as exc: # keep going; report both
|
|
1337
1592
|
detail = f"{detail}; revert error: {exc}"
|
|
1338
|
-
|
|
1593
|
+
return passed, detail, changed_files
|
|
1594
|
+
try:
|
|
1595
|
+
apply_changes(project_root, run_dir, changes)
|
|
1596
|
+
except Exception as exc:
|
|
1597
|
+
return False, f"apply to project failed after successful verify: {exc}", changed_files
|
|
1598
|
+
return True, detail, changed_files
|
|
1339
1599
|
|
|
1340
1600
|
|
|
1341
1601
|
def _record_outcome(project_root, by_id, rr_id, verdict, message, attempts,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Managed per-project workspace commands: inspect and prune.
|
|
2
|
+
|
|
3
|
+
Cloud modes run dependency install + tests inside a managed workspace mirror
|
|
4
|
+
under the central store (``~/.refactor/projects/<id>/workspace``) so the real
|
|
5
|
+
repo is never mutated. These commands let developers see and reclaim that space.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from refactorai_cli import cloud_rr
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
app = typer.Typer(help="Inspect and manage the per-project managed workspace.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _dir_size_bytes(path: Path) -> int:
|
|
24
|
+
total = 0
|
|
25
|
+
for root, _dirs, files in os.walk(path):
|
|
26
|
+
for name in files:
|
|
27
|
+
try:
|
|
28
|
+
total += (Path(root) / name).stat().st_size
|
|
29
|
+
except OSError:
|
|
30
|
+
continue
|
|
31
|
+
return total
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _human(num_bytes: int) -> str:
|
|
35
|
+
size = float(num_bytes)
|
|
36
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
37
|
+
if size < 1024 or unit == "TB":
|
|
38
|
+
return f"{size:.0f}{unit}" if unit == "B" else f"{size:.1f}{unit}"
|
|
39
|
+
size /= 1024
|
|
40
|
+
return f"{size:.1f}TB"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("list")
|
|
44
|
+
def list_workspace() -> None:
|
|
45
|
+
"""Show the managed workspace + dependency cache for this project."""
|
|
46
|
+
paths = cloud_rr.workspace_paths(Path.cwd())
|
|
47
|
+
table = Table(title="Managed workspace")
|
|
48
|
+
table.add_column("kind")
|
|
49
|
+
table.add_column("exists")
|
|
50
|
+
table.add_column("size")
|
|
51
|
+
table.add_column("path", overflow="fold")
|
|
52
|
+
for kind in ("workspace", "cache"):
|
|
53
|
+
path = Path(paths[kind])
|
|
54
|
+
exists = path.is_dir()
|
|
55
|
+
size = _human(_dir_size_bytes(path)) if exists else "-"
|
|
56
|
+
table.add_row(kind, "yes" if exists else "no", size, str(path))
|
|
57
|
+
console.print(table)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("prune")
|
|
61
|
+
def prune_workspace(
|
|
62
|
+
cache: bool = typer.Option(
|
|
63
|
+
False, "--cache", help="Also remove the dependency cache (npm/yarn/pnpm)."
|
|
64
|
+
),
|
|
65
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Remove the managed workspace mirror to reclaim disk space."""
|
|
68
|
+
target = "workspace + dependency cache" if cache else "workspace"
|
|
69
|
+
if not yes:
|
|
70
|
+
confirm = typer.prompt(f"Remove managed {target} for this project? (y/N)", default="N")
|
|
71
|
+
if confirm.strip().lower() not in {"y", "yes"}:
|
|
72
|
+
console.print("[dim]Aborted.[/dim]")
|
|
73
|
+
raise typer.Exit(code=0)
|
|
74
|
+
removed = cloud_rr.prune_workspace(Path.cwd(), include_cache=cache)
|
|
75
|
+
if not removed:
|
|
76
|
+
console.print("[dim]Nothing to remove.[/dim]")
|
|
77
|
+
return
|
|
78
|
+
for path in removed:
|
|
79
|
+
console.print(f"[green]removed[/green] {path}")
|
|
@@ -23,6 +23,7 @@ from refactorai_cli.commands import (
|
|
|
23
23
|
setup_cmds,
|
|
24
24
|
toolchains_cmds,
|
|
25
25
|
watch_cmds,
|
|
26
|
+
workspace_cmds,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
app = typer.Typer(
|
|
@@ -153,6 +154,9 @@ app.add_typer(cloud_cmds.app, name="cloud")
|
|
|
153
154
|
# Managed host toolchains (R24 Phase 2).
|
|
154
155
|
app.add_typer(toolchains_cmds.app, name="toolchains")
|
|
155
156
|
|
|
157
|
+
# Managed per-project workspace (isolated install/test mirror).
|
|
158
|
+
app.add_typer(workspace_cmds.app, name="workspace")
|
|
159
|
+
|
|
156
160
|
# Local commit-triggered review hook (R28 Phase 2, docs/41).
|
|
157
161
|
app.add_typer(hook_cmds.app, name="hook")
|
|
158
162
|
|
|
@@ -46,4 +46,5 @@ refactorai_cli/commands/runtime_cmds.py
|
|
|
46
46
|
refactorai_cli/commands/runtime_proxy_cmds.py
|
|
47
47
|
refactorai_cli/commands/setup_cmds.py
|
|
48
48
|
refactorai_cli/commands/toolchains_cmds.py
|
|
49
|
-
refactorai_cli/commands/watch_cmds.py
|
|
49
|
+
refactorai_cli/commands/watch_cmds.py
|
|
50
|
+
refactorai_cli/commands/workspace_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
|
|
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
|