refactorai-cli 0.4.12__tar.gz → 0.4.16__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.4.12 → refactorai_cli-0.4.16}/PKG-INFO +2 -2
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/pyproject.toml +2 -2
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/hook_cmds.py +40 -2
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/run_cmds.py +74 -5
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/watch_cmds.py +139 -3
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/main.py +1 -1
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/watch_state.py +36 -9
- refactorai_cli-0.4.16/refactorai_cli/watch_supervisor.py +234 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/README.md +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/branch_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/refactor_branch_store.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.16
|
|
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.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.9
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.16"
|
|
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.2.
|
|
15
|
+
"refactorai-core>=3.2.9",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -16,7 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
import typer
|
|
17
17
|
from rich.console import Console
|
|
18
18
|
|
|
19
|
-
from refactorai_cli import commit_queue, git_scope
|
|
19
|
+
from refactorai_cli import commit_queue, git_scope, watch_supervisor
|
|
20
20
|
|
|
21
21
|
console = Console()
|
|
22
22
|
app = typer.Typer(
|
|
@@ -137,7 +137,9 @@ def install(
|
|
|
137
137
|
f"[green]Installed[/green] post-commit hook -> {result['hook_file']}\n"
|
|
138
138
|
f"[dim]core.hooksPath = {result['rel_dir']}[/dim]\n"
|
|
139
139
|
"Every commit now enqueues a local review event. "
|
|
140
|
-
"Open the Refactor Terminal with `refactor watch
|
|
140
|
+
"Open the Refactor Terminal with `refactor watch`.\n"
|
|
141
|
+
"[dim]Tip: set watch.auto_open + watch.terminal (or REFACTOR_WATCH_AUTO_OPEN / "
|
|
142
|
+
"REFACTOR_WATCH_TERMINAL) to open it automatically on each commit.[/dim]"
|
|
141
143
|
)
|
|
142
144
|
|
|
143
145
|
|
|
@@ -195,6 +197,42 @@ def enqueue(
|
|
|
195
197
|
else:
|
|
196
198
|
console.print(f"[dim]hook: not enqueued ({reason}).[/dim]")
|
|
197
199
|
|
|
200
|
+
# Opt-in auto-open: mirror how a dev server opens its own terminal. Best-effort
|
|
201
|
+
# and fail-open so it never affects the commit flow (docs/42 `R29`, Phase 4).
|
|
202
|
+
if enqueued:
|
|
203
|
+
_maybe_auto_open(project_root, verbose)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _watch_settings(project_root: Path) -> dict:
|
|
207
|
+
"""Best-effort project-config settings dict (never raises)."""
|
|
208
|
+
try:
|
|
209
|
+
from refactor_core.constitution import (
|
|
210
|
+
CONFIG_FILENAME,
|
|
211
|
+
find_project_files,
|
|
212
|
+
load_config,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
consti_path, _ = find_project_files(project_root)
|
|
216
|
+
if consti_path is None:
|
|
217
|
+
return {}
|
|
218
|
+
cfg = load_config(consti_path.parent / CONFIG_FILENAME)
|
|
219
|
+
return dict(getattr(cfg, "settings", {}) or {})
|
|
220
|
+
except Exception:
|
|
221
|
+
return {}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _maybe_auto_open(project_root: Path, verbose: bool) -> None:
|
|
225
|
+
try:
|
|
226
|
+
config = _watch_settings(project_root)
|
|
227
|
+
if not watch_supervisor.auto_open_enabled(config):
|
|
228
|
+
return
|
|
229
|
+
result = watch_supervisor.ensure_watch_open(project_root, config=config)
|
|
230
|
+
if verbose:
|
|
231
|
+
console.print(f"[dim]hook: watch ensure -> {result.get('action')}.[/dim]")
|
|
232
|
+
except Exception as exc: # fail-open
|
|
233
|
+
if verbose:
|
|
234
|
+
console.print(f"[yellow]hook: auto-open skipped ({exc}).[/yellow]")
|
|
235
|
+
|
|
198
236
|
|
|
199
237
|
@app.command("status")
|
|
200
238
|
def status() -> None:
|
|
@@ -64,11 +64,13 @@ from refactor_core.refactor_requests import (
|
|
|
64
64
|
DEFERRED,
|
|
65
65
|
RequestSuppression,
|
|
66
66
|
RequestWarning,
|
|
67
|
+
clear_open_requests,
|
|
67
68
|
clear_requests,
|
|
68
69
|
create_requests,
|
|
69
70
|
list_requests,
|
|
70
71
|
load_request,
|
|
71
72
|
pending_requests,
|
|
73
|
+
pending_requests_for_paths,
|
|
72
74
|
save_request,
|
|
73
75
|
)
|
|
74
76
|
from refactor_core.refactor_ops import remediation_hint
|
|
@@ -651,6 +653,26 @@ def _is_cloud_mode(constitution) -> bool:
|
|
|
651
653
|
return bool(_cloud_execution_mode(constitution))
|
|
652
654
|
|
|
653
655
|
|
|
656
|
+
def _documents_digest(documents: list | None) -> str:
|
|
657
|
+
"""Stable digest of the exact documents sent to the platform.
|
|
658
|
+
|
|
659
|
+
Folds each document's path + content so a commit-scoped review keys on the
|
|
660
|
+
code it actually reviews. Without this, two commits share the same
|
|
661
|
+
idempotency key (same account/stage/constitution/target) and the server
|
|
662
|
+
replays the FIRST commit's findings for the second commit -- re-suggesting
|
|
663
|
+
changes that were already applied/merged (docs/41 `R28`).
|
|
664
|
+
"""
|
|
665
|
+
hasher = hashlib.sha256()
|
|
666
|
+
for doc in sorted(documents or [], key=lambda d: str((d or {}).get("path", ""))):
|
|
667
|
+
if not isinstance(doc, dict):
|
|
668
|
+
continue
|
|
669
|
+
hasher.update(str(doc.get("path", "")).encode("utf-8"))
|
|
670
|
+
hasher.update(b"\x00")
|
|
671
|
+
hasher.update(str(doc.get("content", "")).encode("utf-8"))
|
|
672
|
+
hasher.update(b"\x1f")
|
|
673
|
+
return hasher.hexdigest()
|
|
674
|
+
|
|
675
|
+
|
|
654
676
|
def _stable_idempotency_key(
|
|
655
677
|
*,
|
|
656
678
|
account_id: str,
|
|
@@ -659,15 +681,29 @@ def _stable_idempotency_key(
|
|
|
659
681
|
target: str,
|
|
660
682
|
max_alerts: int,
|
|
661
683
|
execution_mode: str,
|
|
684
|
+
scope_digest: str = "",
|
|
662
685
|
) -> str:
|
|
663
686
|
"""Deterministic idempotency key so retries replay instead of re-charging (C10).
|
|
664
687
|
|
|
665
|
-
The key MUST change when inputs change (target/constitution/stage/mode)
|
|
666
|
-
distinct runs are not incorrectly deduplicated, and MUST be stable across
|
|
688
|
+
The key MUST change when inputs change (target/constitution/stage/mode/scope)
|
|
689
|
+
so distinct runs are not incorrectly deduplicated, and MUST be stable across
|
|
667
690
|
retries of the same logical run so managed quota is not double-charged.
|
|
691
|
+
|
|
692
|
+
``scope_digest`` folds the actual reviewed documents into the key so a
|
|
693
|
+
commit-scoped review of a different commit (or of changed code) produces a
|
|
694
|
+
distinct key and a fresh server run, instead of replaying an earlier commit's
|
|
695
|
+
cached findings.
|
|
668
696
|
"""
|
|
669
697
|
seed = "|".join(
|
|
670
|
-
[
|
|
698
|
+
[
|
|
699
|
+
account_id,
|
|
700
|
+
execution_mode,
|
|
701
|
+
stage,
|
|
702
|
+
constitution_hash,
|
|
703
|
+
str(target),
|
|
704
|
+
str(int(max_alerts)),
|
|
705
|
+
scope_digest,
|
|
706
|
+
]
|
|
671
707
|
)
|
|
672
708
|
return hashlib.sha256(seed.encode("utf-8")).hexdigest()
|
|
673
709
|
|
|
@@ -707,6 +743,10 @@ def _cloud_inference(
|
|
|
707
743
|
|
|
708
744
|
execution_mode = _cloud_execution_mode(constitution) or "cloud_managed"
|
|
709
745
|
auth_ctx = ensure_authenticated(project_root)
|
|
746
|
+
# Digest the exact documents so the idempotency key keys on the reviewed
|
|
747
|
+
# code, not just account/stage/target. (The backend independently recomputes
|
|
748
|
+
# a documents hash for its own replay guard, so this need not be sent.)
|
|
749
|
+
documents_hash = _documents_digest(documents)
|
|
710
750
|
payload = {
|
|
711
751
|
"stage": stage,
|
|
712
752
|
"execution_mode": execution_mode,
|
|
@@ -728,6 +768,7 @@ def _cloud_inference(
|
|
|
728
768
|
target=target,
|
|
729
769
|
max_alerts=max_alerts,
|
|
730
770
|
execution_mode=execution_mode,
|
|
771
|
+
scope_digest=documents_hash,
|
|
731
772
|
)
|
|
732
773
|
try:
|
|
733
774
|
response = httpx.post(
|
|
@@ -2725,6 +2766,8 @@ def review(
|
|
|
2725
2766
|
compliance_canary_percent=compliance_canary_pct,
|
|
2726
2767
|
compliance_resolution=compliance_resolution,
|
|
2727
2768
|
artifact=artifact,
|
|
2769
|
+
scope_paths=scope_paths,
|
|
2770
|
+
scoped_replace=True,
|
|
2728
2771
|
)
|
|
2729
2772
|
_apply_compliance_gate_to_artifact(artifact)
|
|
2730
2773
|
_apply_merge_contract_to_artifact(artifact, project_root, project_config)
|
|
@@ -2959,7 +3002,20 @@ def code(
|
|
|
2959
3002
|
# *creating* a large request set; when we are resolving an existing pending
|
|
2960
3003
|
# queue (the common `[r]` resume case) there is nothing to confirm, so a root
|
|
2961
3004
|
# commit no longer re-prompts every run (docs/42 `R29`, Part L).
|
|
2962
|
-
|
|
3005
|
+
#
|
|
3006
|
+
# Reuse is *commit-scoped*: a change-scoped `code --commit`/`[r]` only resumes
|
|
3007
|
+
# pending RRs whose file lives in THIS commit's scope, so a leftover pending RR
|
|
3008
|
+
# from an unrelated commit can never bleed into this commit's resolution
|
|
3009
|
+
# (docs/41 `R28`, Phase 1). A full-project scope (root commit) has no path
|
|
3010
|
+
# restriction, so it resumes the whole pending queue as before.
|
|
3011
|
+
if code_scope and refresh_requests:
|
|
3012
|
+
reused_requests = []
|
|
3013
|
+
elif code_scope and code_scope.get("scope_paths") is not None:
|
|
3014
|
+
reused_requests = pending_requests_for_paths(
|
|
3015
|
+
project_root, code_scope.get("scope_paths")
|
|
3016
|
+
)
|
|
3017
|
+
else:
|
|
3018
|
+
reused_requests = pending_requests(project_root)
|
|
2963
3019
|
will_generate_requests = bool(refresh_requests) or not reused_requests
|
|
2964
3020
|
if will_generate_requests:
|
|
2965
3021
|
_confirm_full_project_scope(code_scope, command="code", assume_yes=yes_full_project)
|
|
@@ -3115,6 +3171,8 @@ def code(
|
|
|
3115
3171
|
compliance_canary_percent=compliance_canary_pct,
|
|
3116
3172
|
compliance_resolution=compliance_resolution,
|
|
3117
3173
|
artifact=review_artifact,
|
|
3174
|
+
scope_paths=code_scope_paths,
|
|
3175
|
+
scoped_replace=True,
|
|
3118
3176
|
)
|
|
3119
3177
|
_apply_compliance_gate_to_artifact(review_artifact)
|
|
3120
3178
|
_apply_merge_contract_to_artifact(review_artifact, project_root, project_config)
|
|
@@ -3667,8 +3725,19 @@ def _replace_requests_with_framework_guardrails(
|
|
|
3667
3725
|
compliance_canary_percent: int = 20,
|
|
3668
3726
|
compliance_resolution: CompliancePackResolution | None = None,
|
|
3669
3727
|
artifact: RunArtifact | None = None,
|
|
3728
|
+
scope_paths: list[str] | None = None,
|
|
3729
|
+
scoped_replace: bool = False,
|
|
3670
3730
|
) -> list:
|
|
3671
|
-
|
|
3731
|
+
# A fresh review supersedes the OPEN requests it re-derives, but must preserve
|
|
3732
|
+
# terminal (applied/denied/failed/...) history so an already-resolved finding is
|
|
3733
|
+
# not re-suggested on a later commit (docs/42 `R29`, Phase 2). A commit-scoped
|
|
3734
|
+
# review clears only its own scope's open queue; a full-project review clears all
|
|
3735
|
+
# open requests. ``scoped_replace`` opts into this history-preserving path so
|
|
3736
|
+
# callers that still want a total reset can pass ``scoped_replace=False``.
|
|
3737
|
+
if scoped_replace:
|
|
3738
|
+
clear_open_requests(project_root, scope_paths)
|
|
3739
|
+
else:
|
|
3740
|
+
clear_requests(project_root)
|
|
3672
3741
|
requests, suppressions, warnings = create_requests(
|
|
3673
3742
|
project_root,
|
|
3674
3743
|
findings,
|
|
@@ -25,7 +25,13 @@ from typing import Callable
|
|
|
25
25
|
import typer
|
|
26
26
|
from rich.console import Console
|
|
27
27
|
|
|
28
|
-
from refactorai_cli import
|
|
28
|
+
from refactorai_cli import (
|
|
29
|
+
commit_queue,
|
|
30
|
+
git_scope,
|
|
31
|
+
refactor_branch_store,
|
|
32
|
+
watch_state,
|
|
33
|
+
watch_supervisor,
|
|
34
|
+
)
|
|
29
35
|
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
30
36
|
from refactorai_cli.review_runner import ReviewRunner, SubprocessReviewRunner
|
|
31
37
|
from refactorai_cli.watch_state import (
|
|
@@ -638,9 +644,38 @@ def _dispatch_key(live, project_root: Path, key: str, state: WatchState) -> None
|
|
|
638
644
|
_run_action(live, project_root, "diff", node, state)
|
|
639
645
|
return
|
|
640
646
|
if not can_review:
|
|
647
|
+
preferred = next(
|
|
648
|
+
(
|
|
649
|
+
n
|
|
650
|
+
for n in state.nodes_newest_first()
|
|
651
|
+
if n.state == watch_state.REVIEWED
|
|
652
|
+
and (n.phase == watch_state.PHASE_REVIEW or n.sha in state.forced_review)
|
|
653
|
+
and n.rr_count > 0
|
|
654
|
+
),
|
|
655
|
+
None,
|
|
656
|
+
)
|
|
657
|
+
fallback = preferred or next(
|
|
658
|
+
(
|
|
659
|
+
n
|
|
660
|
+
for n in state.nodes_newest_first()
|
|
661
|
+
if n.state == watch_state.REVIEWED
|
|
662
|
+
and (n.phase == watch_state.PHASE_REVIEW or n.sha in state.forced_review)
|
|
663
|
+
),
|
|
664
|
+
None,
|
|
665
|
+
)
|
|
666
|
+
if fallback is not None and fallback.sha != node.sha:
|
|
667
|
+
state.selected_sha = fallback.sha
|
|
668
|
+
console.print(
|
|
669
|
+
f"[dim]watch: selected {node.short_sha} ({node.branch or 'detached'} / "
|
|
670
|
+
f"{node.phase}, requests={node.rr_count}) is not reviewable; moved "
|
|
671
|
+
f"selection to {fallback.short_sha} (requests={fallback.rr_count}). "
|
|
672
|
+
f"Press [{key}] again.[/dim]"
|
|
673
|
+
)
|
|
674
|
+
return
|
|
641
675
|
console.print(
|
|
642
|
-
f"[dim]watch:
|
|
643
|
-
f"
|
|
676
|
+
f"[dim]watch: selected {node.short_sha} ({node.branch or 'detached'} / "
|
|
677
|
+
f"phase={node.phase}, requests={node.rr_count}) has nothing to "
|
|
678
|
+
"review. Use [↑/↓] to select a commit with open requests.[/dim]"
|
|
644
679
|
)
|
|
645
680
|
return
|
|
646
681
|
if node.state == watch_state.REVIEWED:
|
|
@@ -695,7 +730,17 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
|
|
|
695
730
|
live.stop()
|
|
696
731
|
|
|
697
732
|
|
|
733
|
+
watch_app = typer.Typer(
|
|
734
|
+
name="watch",
|
|
735
|
+
help="Open the persistent Refactor Terminal (docs/41). Requires a valid key.",
|
|
736
|
+
invoke_without_command=True,
|
|
737
|
+
add_completion=False,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
@watch_app.callback(invoke_without_command=True)
|
|
698
742
|
def watch(
|
|
743
|
+
ctx: typer.Context,
|
|
699
744
|
json_output: bool = typer.Option(
|
|
700
745
|
False,
|
|
701
746
|
"--json",
|
|
@@ -714,6 +759,10 @@ def watch(
|
|
|
714
759
|
),
|
|
715
760
|
) -> None:
|
|
716
761
|
"""Open the persistent Refactor Terminal (docs/41). Requires a valid key."""
|
|
762
|
+
# A subcommand (e.g. `watch ensure`) was invoked -> let it run instead.
|
|
763
|
+
if ctx.invoked_subcommand is not None:
|
|
764
|
+
return
|
|
765
|
+
|
|
717
766
|
project_root = _resolve_repo(Path.cwd())
|
|
718
767
|
_ensure_auth_in_window(json_output)
|
|
719
768
|
|
|
@@ -743,8 +792,95 @@ def watch(
|
|
|
743
792
|
console.print(f"[dim]watch: reviewed {reviewed} commit(s).[/dim]")
|
|
744
793
|
return
|
|
745
794
|
|
|
795
|
+
_open_watch_window(project_root, state, runner)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _open_watch_window(
|
|
799
|
+
project_root: Path, state: WatchState, runner: ReviewRunner
|
|
800
|
+
) -> None:
|
|
801
|
+
"""Run the interactive TUI while holding the single-instance lock.
|
|
802
|
+
|
|
803
|
+
The lock lets ``watch ensure`` (auto-open) detect an already-open window for
|
|
804
|
+
this repo and never spawn a duplicate (docs/42 Phase 4).
|
|
805
|
+
"""
|
|
806
|
+
my_pid = os.getpid()
|
|
807
|
+
watch_supervisor.write_lock(project_root, my_pid)
|
|
746
808
|
try:
|
|
747
809
|
_run_tui(project_root, state, runner)
|
|
748
810
|
except KeyboardInterrupt:
|
|
749
811
|
pass
|
|
812
|
+
finally:
|
|
813
|
+
watch_supervisor.release_lock(project_root, pid=my_pid)
|
|
750
814
|
console.print("[dim]watch: closed (the commit hook/queue are unaffected).[/dim]")
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _load_watch_config(project_root: Path) -> dict:
|
|
818
|
+
"""Best-effort project-config settings dict (never raises)."""
|
|
819
|
+
try:
|
|
820
|
+
from refactor_core.constitution import (
|
|
821
|
+
CONFIG_FILENAME,
|
|
822
|
+
find_project_files,
|
|
823
|
+
load_config,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
consti_path, _ = find_project_files(project_root)
|
|
827
|
+
if consti_path is None:
|
|
828
|
+
return {}
|
|
829
|
+
cfg = load_config(consti_path.parent / CONFIG_FILENAME)
|
|
830
|
+
return dict(getattr(cfg, "settings", {}) or {})
|
|
831
|
+
except Exception:
|
|
832
|
+
return {}
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
@watch_app.command("ensure")
|
|
836
|
+
def ensure(
|
|
837
|
+
force: bool = typer.Option(
|
|
838
|
+
False, "--force", help="Open even if auto-open is disabled in config/env."
|
|
839
|
+
),
|
|
840
|
+
foreground: bool = typer.Option(
|
|
841
|
+
False,
|
|
842
|
+
"--foreground",
|
|
843
|
+
"-f",
|
|
844
|
+
help="Run the Refactor Terminal inline instead of spawning a new window.",
|
|
845
|
+
),
|
|
846
|
+
) -> None:
|
|
847
|
+
"""Open the Refactor Terminal if enabled and not already running (auto-open).
|
|
848
|
+
|
|
849
|
+
Invoked by the ``post-commit`` hook when auto-open is on, mirroring how a dev
|
|
850
|
+
server opens its own terminal. Fail-open and idempotent: a single-instance lock
|
|
851
|
+
prevents duplicate windows.
|
|
852
|
+
"""
|
|
853
|
+
project_root = _resolve_repo(Path.cwd())
|
|
854
|
+
|
|
855
|
+
if foreground:
|
|
856
|
+
if watch_supervisor.is_watch_running(project_root) and not force:
|
|
857
|
+
console.print("[dim]watch: a Refactor Terminal is already open.[/dim]")
|
|
858
|
+
return
|
|
859
|
+
_ensure_auth_in_window(False)
|
|
860
|
+
state = WatchState(repo_name=project_root.name)
|
|
861
|
+
try:
|
|
862
|
+
state.current_branch = git_scope.current_branch(project_root) or ""
|
|
863
|
+
except Exception:
|
|
864
|
+
state.current_branch = ""
|
|
865
|
+
for event in _load_initial_events(project_root, None, 20):
|
|
866
|
+
sha = str(event.get("sha") or "")
|
|
867
|
+
if sha and not state.has(sha):
|
|
868
|
+
state.upsert_from_event(event)
|
|
869
|
+
runner: ReviewRunner = SubprocessReviewRunner(project_root)
|
|
870
|
+
_open_watch_window(project_root, state, runner)
|
|
871
|
+
return
|
|
872
|
+
|
|
873
|
+
config = _load_watch_config(project_root)
|
|
874
|
+
result = watch_supervisor.ensure_watch_open(
|
|
875
|
+
project_root, config=config, force=force
|
|
876
|
+
)
|
|
877
|
+
action = result.get("action")
|
|
878
|
+
message = result.get("message", "")
|
|
879
|
+
if action == "launched":
|
|
880
|
+
console.print(f"[green]watch:[/green] {message}")
|
|
881
|
+
elif action in ("already_running", "disabled"):
|
|
882
|
+
console.print(f"[dim]watch: {message}.[/dim]")
|
|
883
|
+
elif action == "no_launcher":
|
|
884
|
+
console.print(f"[yellow]watch:[/yellow] {message}.")
|
|
885
|
+
else: # launch_failed / unknown
|
|
886
|
+
console.print(f"[yellow]watch: {message}.[/yellow]")
|
|
@@ -156,7 +156,7 @@ app.add_typer(toolchains_cmds.app, name="toolchains")
|
|
|
156
156
|
app.add_typer(hook_cmds.app, name="hook")
|
|
157
157
|
|
|
158
158
|
# Persistent Refactor Terminal (R28 Phase 3, docs/41).
|
|
159
|
-
app.
|
|
159
|
+
app.add_typer(watch_cmds.watch_app, name="watch")
|
|
160
160
|
|
|
161
161
|
# Local refactor branch + per-request commits (R29 Phase 3, docs/42).
|
|
162
162
|
app.add_typer(branch_cmds.app, name="branch")
|
|
@@ -447,8 +447,9 @@ def _render_review_view(state: WatchState, *, now: datetime | None = None, width
|
|
|
447
447
|
age = humanize_age(node.enqueued_at, now=now)
|
|
448
448
|
gate = _gate_markup(node)
|
|
449
449
|
selected = node.sha == state.selected_sha
|
|
450
|
+
node_lines: list[Text] = []
|
|
450
451
|
# Row 1: node marker + branch + age (+ gate, right aligned). The selected
|
|
451
|
-
# node gets a filled marker
|
|
452
|
+
# node gets a filled marker + a visual box so `[r]` targets are explicit.
|
|
452
453
|
if selected:
|
|
453
454
|
left = Text.from_markup(
|
|
454
455
|
f"[bold bright_cyan]●[/bold bright_cyan] [bold bright_cyan]{node.branch or '(detached)'}[/bold bright_cyan]"
|
|
@@ -461,25 +462,25 @@ def _render_review_view(state: WatchState, *, now: datetime | None = None, width
|
|
|
461
462
|
pad = max(1, inner_width - len(left.plain) - len(gate_text.plain))
|
|
462
463
|
left.append(" " * pad)
|
|
463
464
|
left.append_text(gate_text)
|
|
464
|
-
|
|
465
|
+
node_lines.append(left)
|
|
465
466
|
# Row 2: pipe + sha + subject.
|
|
466
467
|
row2 = Text.from_markup(f"[cyan]|[/cyan] [yellow]{node.short_sha}[/yellow] ")
|
|
467
468
|
row2.append(node.subject or "(no subject)")
|
|
468
|
-
|
|
469
|
+
node_lines.append(row2)
|
|
469
470
|
# Row 3: pipe + status.
|
|
470
471
|
row3 = Text.from_markup("[cyan]|[/cyan] ")
|
|
471
472
|
row3.append_text(Text.from_markup(_status_line(node)))
|
|
472
|
-
|
|
473
|
+
node_lines.append(row3)
|
|
473
474
|
if node.state == REVIEWED and node.rr_preview:
|
|
474
475
|
for item in node.rr_preview:
|
|
475
476
|
req = Text.from_markup("[cyan]|[/cyan] ")
|
|
476
477
|
req.append(f"- {item}", style="magenta")
|
|
477
|
-
|
|
478
|
+
node_lines.append(req)
|
|
478
479
|
# Row 4: intent summary (only once reviewed and present).
|
|
479
480
|
if node.state in (REVIEWED,) and node.intent_summary:
|
|
480
481
|
intent = Text.from_markup("[cyan]|[/cyan] ")
|
|
481
482
|
intent.append_text(Text.from_markup(f"[dim]intent: {node.intent_summary}[/dim]"))
|
|
482
|
-
|
|
483
|
+
node_lines.append(intent)
|
|
483
484
|
# Once a commit is resolved onto a live refactor branch, hint the finalize
|
|
484
485
|
# view rather than the (now empty) review actions (docs/43 Part D).
|
|
485
486
|
if node.phase == PHASE_FINALIZE and node.finalize is not None:
|
|
@@ -489,7 +490,7 @@ def _render_review_view(state: WatchState, *, now: datetime | None = None, width
|
|
|
489
490
|
"-> select + press [m] to finalize",
|
|
490
491
|
style="green",
|
|
491
492
|
)
|
|
492
|
-
|
|
493
|
+
node_lines.append(hint)
|
|
493
494
|
elif node.phase == PHASE_DONE and node.finalize is not None:
|
|
494
495
|
done = Text.from_markup("[cyan]|[/cyan] ")
|
|
495
496
|
if node.finalize.outcome == "merged":
|
|
@@ -500,10 +501,36 @@ def _render_review_view(state: WatchState, *, now: datetime | None = None, width
|
|
|
500
501
|
done.append("refactor branch dropped", style="dim")
|
|
501
502
|
else:
|
|
502
503
|
done.append("refactor branch finalized", style="dim")
|
|
503
|
-
|
|
504
|
-
|
|
504
|
+
node_lines.append(done)
|
|
505
|
+
node_lines.append(Text.from_markup("[cyan]|[/cyan]"))
|
|
506
|
+
|
|
507
|
+
if selected and node_lines:
|
|
508
|
+
# Strong visual target: render the selected commit in a distinct box so
|
|
509
|
+
# `[r]`/`[f]` actions are unambiguous in long commit stacks.
|
|
510
|
+
for idx, row in enumerate(node_lines):
|
|
511
|
+
if idx == 0:
|
|
512
|
+
glyph = "┏ "
|
|
513
|
+
elif idx == len(node_lines) - 1:
|
|
514
|
+
glyph = "┗ "
|
|
515
|
+
else:
|
|
516
|
+
glyph = "┃ "
|
|
517
|
+
wrapped = Text(glyph, style="bold bright_cyan")
|
|
518
|
+
wrapped.append_text(row)
|
|
519
|
+
lines.append(_with_pad(wrapped))
|
|
520
|
+
else:
|
|
521
|
+
for row in node_lines:
|
|
522
|
+
lines.append(_with_pad(row))
|
|
505
523
|
lines.append(_with_pad(Text.from_markup("[cyan]+[/cyan]")))
|
|
506
524
|
lines.append(_with_pad(Text("")))
|
|
525
|
+
selected = state.selected()
|
|
526
|
+
if selected is not None:
|
|
527
|
+
context = Text.from_markup("[bold bright_cyan]selected[/bold bright_cyan]: ")
|
|
528
|
+
context.append(selected.short_sha, style="yellow")
|
|
529
|
+
context.append(f" {selected.subject or '(no subject)'}", style="dim")
|
|
530
|
+
context.append(f" phase={selected.phase}", style="dim")
|
|
531
|
+
context.append(f" requests={selected.rr_count}", style="dim")
|
|
532
|
+
lines.append(_with_pad(context))
|
|
533
|
+
lines.append(_with_pad(Text("")))
|
|
507
534
|
lines.append(
|
|
508
535
|
_with_pad(
|
|
509
536
|
Text.from_markup(
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Auto-open supervisor for the Refactor Terminal (docs/42 `R29`, Phase 4).
|
|
2
|
+
|
|
3
|
+
Opt-in: after a commit is enqueued, the ``post-commit`` hook can call
|
|
4
|
+
``refactor watch ensure`` to open the Refactor Terminal in a new terminal window
|
|
5
|
+
when one is not already running -- so review results appear automatically (like a
|
|
6
|
+
Metro / dev-server window) instead of the developer having to remember to run
|
|
7
|
+
``refactor watch``.
|
|
8
|
+
|
|
9
|
+
Everything here is best-effort and fail-open: it must never delay or fail a commit.
|
|
10
|
+
Auto-open is disabled unless the developer opts in, and opening a *new* window
|
|
11
|
+
needs a configured terminal launcher (there is no reliable cross-platform default),
|
|
12
|
+
so both are surfaced as an env var and a matching ``watch`` config key.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shlex
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from refactorai_cli import commit_queue
|
|
26
|
+
|
|
27
|
+
LOCK_FILENAME = "watch.lock"
|
|
28
|
+
|
|
29
|
+
# Env knobs (project-config keys under a ``watch`` table mirror these names).
|
|
30
|
+
ENV_AUTO_OPEN = "REFACTOR_WATCH_AUTO_OPEN" # "1"/"true" to enable auto-open
|
|
31
|
+
ENV_TERMINAL = "REFACTOR_WATCH_TERMINAL" # launcher template, uses ``{cmd}``
|
|
32
|
+
|
|
33
|
+
_TRUE_TOKENS = {"1", "true", "yes", "on"}
|
|
34
|
+
# A freshly-launched window that has not yet written its own pid still counts as
|
|
35
|
+
# "running" for this many seconds, so two commits in quick succession do not each
|
|
36
|
+
# spawn a window before the first has come up.
|
|
37
|
+
_LAUNCH_GRACE_SECONDS = 45
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _now_iso() -> str:
|
|
41
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _as_bool(value: object) -> bool:
|
|
45
|
+
return str(value).strip().lower() in _TRUE_TOKENS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def lock_path(project_root: Path | str) -> Path:
|
|
49
|
+
"""Per-repo lock file. Lives beside the commit queue (never tracked)."""
|
|
50
|
+
directory = commit_queue.runs_dir(project_root)
|
|
51
|
+
if directory is None:
|
|
52
|
+
# Outside a git dir (or tests): fall back to the central project store.
|
|
53
|
+
from refactorai_cli.local_paths import project_store_dir
|
|
54
|
+
|
|
55
|
+
directory = project_store_dir(project_root)
|
|
56
|
+
return Path(directory) / LOCK_FILENAME
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _pid_alive(pid: int) -> bool:
|
|
60
|
+
if pid <= 0:
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
os.kill(pid, 0)
|
|
64
|
+
except ProcessLookupError:
|
|
65
|
+
return False
|
|
66
|
+
except PermissionError:
|
|
67
|
+
return True
|
|
68
|
+
except OSError:
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_lock(project_root: Path | str) -> dict | None:
|
|
74
|
+
path = lock_path(project_root)
|
|
75
|
+
if not path.is_file():
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
79
|
+
except (json.JSONDecodeError, OSError):
|
|
80
|
+
return None
|
|
81
|
+
return data if isinstance(data, dict) else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def write_lock(project_root: Path | str, pid: int, *, launching: bool = False) -> None:
|
|
85
|
+
path = lock_path(project_root)
|
|
86
|
+
try:
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
path.write_text(
|
|
89
|
+
json.dumps({"pid": int(pid), "launching": bool(launching), "at": _now_iso()}),
|
|
90
|
+
encoding="utf-8",
|
|
91
|
+
)
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def release_lock(project_root: Path | str, *, pid: int | None = None) -> None:
|
|
97
|
+
"""Remove the lock. If ``pid`` is given, only remove our own lock."""
|
|
98
|
+
data = read_lock(project_root)
|
|
99
|
+
if data is None:
|
|
100
|
+
return
|
|
101
|
+
if pid is not None and int(data.get("pid", 0) or 0) != int(pid):
|
|
102
|
+
return
|
|
103
|
+
try:
|
|
104
|
+
lock_path(project_root).unlink()
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _within_grace(data: dict) -> bool:
|
|
110
|
+
if not data.get("launching"):
|
|
111
|
+
return False
|
|
112
|
+
stamp = str(data.get("at", "") or "")
|
|
113
|
+
if not stamp:
|
|
114
|
+
return False
|
|
115
|
+
try:
|
|
116
|
+
started = datetime.strptime(stamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
117
|
+
except ValueError:
|
|
118
|
+
return False
|
|
119
|
+
return (datetime.now(timezone.utc) - started).total_seconds() < _LAUNCH_GRACE_SECONDS
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_watch_running(project_root: Path | str) -> bool:
|
|
123
|
+
"""True if a Refactor Terminal is (believed to be) open for this repo."""
|
|
124
|
+
data = read_lock(project_root)
|
|
125
|
+
if not data:
|
|
126
|
+
return False
|
|
127
|
+
if _pid_alive(int(data.get("pid", 0) or 0)):
|
|
128
|
+
return True
|
|
129
|
+
# A just-launched window may not have replaced the launch lock with its own pid
|
|
130
|
+
# yet; treat the launch marker as live for a short grace window.
|
|
131
|
+
return _within_grace(data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _watch_config(config: dict | None) -> dict:
|
|
135
|
+
if not isinstance(config, dict):
|
|
136
|
+
return {}
|
|
137
|
+
watch = config.get("watch")
|
|
138
|
+
return watch if isinstance(watch, dict) else {}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def auto_open_enabled(config: dict | None = None, env: dict | None = None) -> bool:
|
|
142
|
+
"""Whether auto-open is enabled (env wins over config; default off)."""
|
|
143
|
+
env = os.environ if env is None else env
|
|
144
|
+
raw = env.get(ENV_AUTO_OPEN)
|
|
145
|
+
if raw is not None and str(raw).strip() != "":
|
|
146
|
+
return _as_bool(raw)
|
|
147
|
+
return bool(_watch_config(config).get("auto_open", False))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve_launcher(config: dict | None = None, env: dict | None = None) -> str | None:
|
|
151
|
+
"""Terminal launcher template, or ``None`` if unconfigured.
|
|
152
|
+
|
|
153
|
+
The template opens a new terminal window running a command. ``{cmd}`` is
|
|
154
|
+
substituted with the (shell-quoted) ``refactor watch`` invocation; if absent the
|
|
155
|
+
command is appended. Examples::
|
|
156
|
+
|
|
157
|
+
gnome-terminal -- bash -lc {cmd}
|
|
158
|
+
xterm -e {cmd}
|
|
159
|
+
wt.exe {cmd}
|
|
160
|
+
"""
|
|
161
|
+
env = os.environ if env is None else env
|
|
162
|
+
raw = env.get(ENV_TERMINAL)
|
|
163
|
+
if raw and str(raw).strip():
|
|
164
|
+
return str(raw).strip()
|
|
165
|
+
template = _watch_config(config).get("terminal")
|
|
166
|
+
return str(template).strip() if template and str(template).strip() else None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def watch_command(executable: str = "refactor") -> list[str]:
|
|
170
|
+
"""The inner command a launched window should run."""
|
|
171
|
+
return [executable, "watch"]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_launch_command(template: str, inner_cmd: list[str]) -> list[str]:
|
|
175
|
+
"""Render a launcher ``template`` + inner command into an argv list."""
|
|
176
|
+
inner = " ".join(shlex.quote(part) for part in inner_cmd)
|
|
177
|
+
rendered = template.replace("{cmd}", inner) if "{cmd}" in template else f"{template} {inner}"
|
|
178
|
+
return shlex.split(rendered)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def spawn_detached(cmd: list[str], cwd: Path | str) -> int | None:
|
|
182
|
+
"""Launch ``cmd`` fully detached from the current process. Returns its pid."""
|
|
183
|
+
try:
|
|
184
|
+
proc = subprocess.Popen(
|
|
185
|
+
cmd,
|
|
186
|
+
cwd=str(cwd),
|
|
187
|
+
stdin=subprocess.DEVNULL,
|
|
188
|
+
stdout=subprocess.DEVNULL,
|
|
189
|
+
stderr=subprocess.DEVNULL,
|
|
190
|
+
start_new_session=True,
|
|
191
|
+
)
|
|
192
|
+
except (OSError, ValueError):
|
|
193
|
+
return None
|
|
194
|
+
return proc.pid
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def ensure_watch_open(
|
|
198
|
+
project_root: Path | str,
|
|
199
|
+
*,
|
|
200
|
+
config: dict | None = None,
|
|
201
|
+
env: dict | None = None,
|
|
202
|
+
executable: str = "refactor",
|
|
203
|
+
force: bool = False,
|
|
204
|
+
) -> dict:
|
|
205
|
+
"""Open a Refactor Terminal if auto-open is enabled and none is running.
|
|
206
|
+
|
|
207
|
+
Pure-ish orchestration returning a result dict with ``action`` in
|
|
208
|
+
``{disabled, already_running, no_launcher, launched, launch_failed}`` plus a
|
|
209
|
+
human ``message``. ``force`` bypasses the auto-open gate (used by an explicit
|
|
210
|
+
``watch ensure --force``).
|
|
211
|
+
"""
|
|
212
|
+
if not force and not auto_open_enabled(config, env):
|
|
213
|
+
return {"action": "disabled", "message": "auto-open is disabled"}
|
|
214
|
+
if is_watch_running(project_root):
|
|
215
|
+
return {"action": "already_running", "message": "Refactor Terminal already open"}
|
|
216
|
+
|
|
217
|
+
launcher = resolve_launcher(config, env)
|
|
218
|
+
if not launcher:
|
|
219
|
+
return {
|
|
220
|
+
"action": "no_launcher",
|
|
221
|
+
"message": (
|
|
222
|
+
f"set {ENV_TERMINAL} (or watch.terminal) to a terminal launcher "
|
|
223
|
+
"template to auto-open the Refactor Terminal"
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
cmd = build_launch_command(launcher, watch_command(executable))
|
|
228
|
+
pid = spawn_detached(cmd, project_root)
|
|
229
|
+
if pid is None:
|
|
230
|
+
return {"action": "launch_failed", "message": f"could not launch: {' '.join(cmd)}"}
|
|
231
|
+
# Record a launch marker so a rapid second commit does not double-open before the
|
|
232
|
+
# window comes up and writes its own pid.
|
|
233
|
+
write_lock(project_root, pid, launching=True)
|
|
234
|
+
return {"action": "launched", "message": f"opened Refactor Terminal (pid {pid})", "pid": pid}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.16
|
|
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.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.9
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -20,6 +20,7 @@ refactorai_cli/runtime_manager.py
|
|
|
20
20
|
refactorai_cli/settings.py
|
|
21
21
|
refactorai_cli/setup_flow.py
|
|
22
22
|
refactorai_cli/watch_state.py
|
|
23
|
+
refactorai_cli/watch_supervisor.py
|
|
23
24
|
refactorai_cli.egg-info/PKG-INFO
|
|
24
25
|
refactorai_cli.egg-info/SOURCES.txt
|
|
25
26
|
refactorai_cli.egg-info/dependency_links.txt
|
|
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
|
{refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli/commands/runtime_proxy_cmds.py
RENAMED
|
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
|
{refactorai_cli-0.4.12 → refactorai_cli-0.4.16}/refactorai_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|