refactorai-cli 0.4.0__tar.gz → 0.4.2__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.0 → refactorai_cli-0.4.2}/PKG-INFO +2 -2
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/pyproject.toml +2 -2
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/hook_cmds.py +59 -27
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/run_cmds.py +128 -27
- refactorai_cli-0.4.2/refactorai_cli/dotenv_loader.py +112 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/main.py +11 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/README.md +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commands/watch_cmds.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli/watch_state.py +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.0 → refactorai_cli-0.4.2}/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.2
|
|
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.1
|
|
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.2"
|
|
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.1",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -55,22 +55,15 @@ def _require_repo(project_root: Path) -> str:
|
|
|
55
55
|
return root
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"--force",
|
|
68
|
-
help="Overwrite an existing non-RefactorAI post-commit hook.",
|
|
69
|
-
),
|
|
70
|
-
) -> None:
|
|
71
|
-
"""Install the non-blocking post-commit hook and wire ``core.hooksPath``."""
|
|
72
|
-
root = Path(_require_repo(Path.cwd()))
|
|
73
|
-
|
|
58
|
+
def perform_install(
|
|
59
|
+
root: Path, *, hooks_dir: str | None = None, force: bool = False
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""Install the post-commit hook + wire ``core.hooksPath`` (no printing/exit).
|
|
62
|
+
|
|
63
|
+
Returns a result dict with ``status`` in ``{installed, exists_foreign, error}``
|
|
64
|
+
plus ``rel_dir``, ``hook_file`` and a human ``message``. Shared by the
|
|
65
|
+
``hook install`` command and ``refactor init`` so both stay in lockstep.
|
|
66
|
+
"""
|
|
74
67
|
existing_path = git_scope.get_config(root, "core.hooksPath")
|
|
75
68
|
if hooks_dir:
|
|
76
69
|
rel_dir = hooks_dir
|
|
@@ -83,29 +76,68 @@ def install(
|
|
|
83
76
|
hook_file = hooks_path / "post-commit"
|
|
84
77
|
|
|
85
78
|
if hook_file.exists() and _HOOK_MARKER not in _safe_read(hook_file) and not force:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
return {
|
|
80
|
+
"status": "exists_foreign",
|
|
81
|
+
"rel_dir": rel_dir,
|
|
82
|
+
"hook_file": hook_file,
|
|
83
|
+
"message": f"a post-commit hook already exists at {hook_file}",
|
|
84
|
+
}
|
|
91
85
|
|
|
92
86
|
try:
|
|
93
87
|
hooks_path.mkdir(parents=True, exist_ok=True)
|
|
94
88
|
hook_file.write_text(_POST_COMMIT_SCRIPT, encoding="utf-8")
|
|
95
89
|
hook_file.chmod(hook_file.stat().st_mode | 0o755)
|
|
96
90
|
except OSError as exc:
|
|
97
|
-
|
|
98
|
-
raise typer.Exit(code=1) from exc
|
|
91
|
+
return {"status": "error", "rel_dir": rel_dir, "hook_file": None, "message": str(exc)}
|
|
99
92
|
|
|
100
93
|
if not git_scope.set_config(root, "core.hooksPath", rel_dir):
|
|
101
|
-
|
|
94
|
+
return {
|
|
95
|
+
"status": "error",
|
|
96
|
+
"rel_dir": rel_dir,
|
|
97
|
+
"hook_file": hook_file,
|
|
98
|
+
"message": "could not set core.hooksPath",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"status": "installed",
|
|
103
|
+
"rel_dir": rel_dir,
|
|
104
|
+
"hook_file": hook_file,
|
|
105
|
+
"message": f"installed post-commit hook -> {hook_file}",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command("install")
|
|
110
|
+
def install(
|
|
111
|
+
hooks_dir: str | None = typer.Option(
|
|
112
|
+
None,
|
|
113
|
+
"--hooks-dir",
|
|
114
|
+
help=f"Directory to hold git hooks (default: existing core.hooksPath or {_DEFAULT_HOOKS_DIR}).",
|
|
115
|
+
),
|
|
116
|
+
force: bool = typer.Option(
|
|
117
|
+
False,
|
|
118
|
+
"--force",
|
|
119
|
+
help="Overwrite an existing non-RefactorAI post-commit hook.",
|
|
120
|
+
),
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Install the non-blocking post-commit hook and wire ``core.hooksPath``."""
|
|
123
|
+
root = Path(_require_repo(Path.cwd()))
|
|
124
|
+
result = perform_install(root, hooks_dir=hooks_dir, force=force)
|
|
125
|
+
|
|
126
|
+
if result["status"] == "exists_foreign":
|
|
127
|
+
console.print(
|
|
128
|
+
f"[yellow]hook:[/yellow] {result['message']}.\n"
|
|
129
|
+
"Re-run with [bold]--force[/bold] to overwrite, or point --hooks-dir elsewhere."
|
|
130
|
+
)
|
|
131
|
+
raise typer.Exit(code=1)
|
|
132
|
+
if result["status"] == "error":
|
|
133
|
+
console.print(f"[red]hook: could not install:[/red] {result['message']}")
|
|
102
134
|
raise typer.Exit(code=1)
|
|
103
135
|
|
|
104
136
|
console.print(
|
|
105
|
-
f"[green]Installed[/green] post-commit hook -> {hook_file}\n"
|
|
106
|
-
f"[dim]core.hooksPath = {rel_dir}[/dim]\n"
|
|
137
|
+
f"[green]Installed[/green] post-commit hook -> {result['hook_file']}\n"
|
|
138
|
+
f"[dim]core.hooksPath = {result['rel_dir']}[/dim]\n"
|
|
107
139
|
"Every commit now enqueues a local review event. "
|
|
108
|
-
"Open the Refactor Terminal with `refactor watch
|
|
140
|
+
"Open the Refactor Terminal with `refactor watch`."
|
|
109
141
|
)
|
|
110
142
|
|
|
111
143
|
|
|
@@ -72,7 +72,7 @@ from refactor_core.refactor_requests import (
|
|
|
72
72
|
save_request,
|
|
73
73
|
)
|
|
74
74
|
from refactor_core.refactor_ops import remediation_hint
|
|
75
|
-
from refactor_core.indexing import index_target
|
|
75
|
+
from refactor_core.indexing import index_paths, index_target
|
|
76
76
|
from refactor_core.models import Finding, GeneratedTest, PatchGroup, RunArtifact
|
|
77
77
|
from refactor_core.rules import annotate_documents_with_rules, filter_documents_by_rules
|
|
78
78
|
from refactor_core.security import (
|
|
@@ -101,6 +101,7 @@ from refactor_core.store import (
|
|
|
101
101
|
from refactor_core.testcmd import resolve_test_command
|
|
102
102
|
|
|
103
103
|
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
104
|
+
from refactorai_cli.dotenv_loader import DotenvResult, load_project_dotenv
|
|
104
105
|
from refactorai_cli.local_engine_runtime import DEFAULT_ENGINE_PORT, read_engine_state
|
|
105
106
|
from refactorai_cli.runtime_manager import runtime_status, runtime_version_dir
|
|
106
107
|
from refactorai_cli.settings import platform_url
|
|
@@ -679,12 +680,19 @@ def _cloud_inference(
|
|
|
679
680
|
target: str,
|
|
680
681
|
model_hint: str | None = None,
|
|
681
682
|
findings: list | None = None,
|
|
683
|
+
scope_paths: list[str] | None = None,
|
|
682
684
|
) -> RunArtifact:
|
|
683
685
|
_raw_alerts = constitution.get_setting("max_alerts_per_run", 5)
|
|
684
686
|
max_alerts = int(_raw_alerts if _raw_alerts is not None else 5)
|
|
685
687
|
gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
|
|
686
688
|
excludes = set(constitution.get_setting("exclude", []) or [])
|
|
687
|
-
|
|
689
|
+
# Commit-scoped cloud review (docs/41 `R28`): when a scope is supplied, index
|
|
690
|
+
# only the changed + graph-associated files instead of the whole target, so a
|
|
691
|
+
# BYOK/managed commit review sends just the relevant documents to the platform.
|
|
692
|
+
if scope_paths is not None:
|
|
693
|
+
documents = index_paths(Path(project_root), scope_paths, excludes=excludes)
|
|
694
|
+
else:
|
|
695
|
+
documents = index_target(Path(project_root), target, excludes=excludes)
|
|
688
696
|
documents, _scope_meta = filter_documents_by_rules(documents, project_root=Path(project_root))
|
|
689
697
|
rule_meta = annotate_documents_with_rules(documents, project_root=Path(project_root))
|
|
690
698
|
|
|
@@ -1029,6 +1037,59 @@ def init(
|
|
|
1029
1037
|
"all run state stays under ~/.refactor "
|
|
1030
1038
|
f"(encoded: {record['encoded']})."
|
|
1031
1039
|
)
|
|
1040
|
+
_offer_commit_hook(project_root)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _offer_commit_hook(project_root: Path) -> None:
|
|
1044
|
+
"""Offer to wire the local commit-review hook during `init` (docs/41 `R28`).
|
|
1045
|
+
|
|
1046
|
+
Interactive TTYs get a yes/no prompt (default yes); non-interactive runs
|
|
1047
|
+
(CI/agents) print a one-line tip instead of blocking. Never fails `init`.
|
|
1048
|
+
"""
|
|
1049
|
+
try:
|
|
1050
|
+
from refactorai_cli import git_scope
|
|
1051
|
+
from refactorai_cli.commands import hook_cmds
|
|
1052
|
+
except Exception:
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
if not git_scope.is_git_repo(project_root):
|
|
1056
|
+
console.print(
|
|
1057
|
+
"[dim]Tip:[/dim] this folder is not a git repo yet. After `git init`, run "
|
|
1058
|
+
"[bold]refactor hook install[/bold] to review each commit before push."
|
|
1059
|
+
)
|
|
1060
|
+
return
|
|
1061
|
+
|
|
1062
|
+
if not sys.stdin.isatty():
|
|
1063
|
+
console.print(
|
|
1064
|
+
"[dim]Tip:[/dim] run [bold]refactor hook install[/bold] to enable "
|
|
1065
|
+
"commit-triggered local review (docs/41)."
|
|
1066
|
+
)
|
|
1067
|
+
return
|
|
1068
|
+
|
|
1069
|
+
if not typer.confirm(
|
|
1070
|
+
"Install the post-commit review hook for this repo now?", default=True
|
|
1071
|
+
):
|
|
1072
|
+
console.print(
|
|
1073
|
+
"[dim]Skipped. Run [bold]refactor hook install[/bold] anytime to enable it.[/dim]"
|
|
1074
|
+
)
|
|
1075
|
+
return
|
|
1076
|
+
|
|
1077
|
+
result = hook_cmds.perform_install(project_root)
|
|
1078
|
+
status = result.get("status")
|
|
1079
|
+
if status == "installed":
|
|
1080
|
+
console.print(
|
|
1081
|
+
f"[green]Installed[/green] post-commit review hook "
|
|
1082
|
+
f"[dim](core.hooksPath = {result['rel_dir']})[/dim]. "
|
|
1083
|
+
"Open the Refactor Terminal with [bold]refactor watch[/bold]."
|
|
1084
|
+
)
|
|
1085
|
+
elif status == "exists_foreign":
|
|
1086
|
+
console.print(
|
|
1087
|
+
f"[yellow]A different post-commit hook already exists[/yellow] "
|
|
1088
|
+
f"({result['hook_file']}). Run [bold]refactor hook install --force[/bold] "
|
|
1089
|
+
"to replace it, or point [bold]--hooks-dir[/bold] elsewhere."
|
|
1090
|
+
)
|
|
1091
|
+
else:
|
|
1092
|
+
console.print(f"[yellow]Could not install hook:[/yellow] {result.get('message')}")
|
|
1032
1093
|
|
|
1033
1094
|
|
|
1034
1095
|
_ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
@@ -1949,14 +2010,14 @@ def review(
|
|
|
1949
2010
|
None,
|
|
1950
2011
|
"--commit",
|
|
1951
2012
|
help="Scope the review to a commit's changed files + graph-associated "
|
|
1952
|
-
"files (local
|
|
1953
|
-
"back to a full-project review.",
|
|
2013
|
+
"files (docs/41; works in local and cloud/BYOK modes). Use a SHA/ref; the "
|
|
2014
|
+
"repo root commit falls back to a full-project review.",
|
|
1954
2015
|
),
|
|
1955
2016
|
changed: bool = typer.Option(
|
|
1956
2017
|
False,
|
|
1957
2018
|
"--changed",
|
|
1958
2019
|
help="Scope the review to the working tree's changed files + "
|
|
1959
|
-
"graph-associated files
|
|
2020
|
+
"graph-associated files.",
|
|
1960
2021
|
),
|
|
1961
2022
|
intent: str | None = typer.Option(
|
|
1962
2023
|
None,
|
|
@@ -2026,14 +2087,10 @@ def review(
|
|
|
2026
2087
|
register_project(project_root, constitution_hash=constitution.content_hash)
|
|
2027
2088
|
_maybe_build_graph(project_root, project_config)
|
|
2028
2089
|
|
|
2029
|
-
# Commit-scoped
|
|
2030
|
-
#
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
"[red]review: --commit/--changed is local-only[/red] and is not "
|
|
2034
|
-
f"supported in {_cloud_execution_mode(constitution)} mode."
|
|
2035
|
-
)
|
|
2036
|
-
raise typer.Exit(code=2)
|
|
2090
|
+
# Commit-scoped review (docs/41 `R28`). Works in every execution mode: local
|
|
2091
|
+
# runs it through the runtime, while cloud/BYOK index only the changed +
|
|
2092
|
+
# associated files and send just those documents to the platform (so the
|
|
2093
|
+
# commit loop runs without a downloaded runtime artifact).
|
|
2037
2094
|
scope = _resolve_commit_scope(
|
|
2038
2095
|
project_root=project_root,
|
|
2039
2096
|
constitution=constitution,
|
|
@@ -2074,7 +2131,10 @@ def review(
|
|
|
2074
2131
|
constitution=constitution,
|
|
2075
2132
|
target=target,
|
|
2076
2133
|
model_hint=provider,
|
|
2134
|
+
scope_paths=scope_paths,
|
|
2077
2135
|
)
|
|
2136
|
+
if scope and scope.get("summary"):
|
|
2137
|
+
artifact.notes.append(f"Commit-scoped review intent: {scope['summary']}")
|
|
2078
2138
|
else:
|
|
2079
2139
|
from refactor_core.execution.orchestrator import run_with_mode
|
|
2080
2140
|
from refactor_core.providers import get_provider
|
|
@@ -2204,13 +2264,13 @@ def code(
|
|
|
2204
2264
|
None,
|
|
2205
2265
|
"--commit",
|
|
2206
2266
|
help="Scope to a commit's changed + graph-associated files and (re)create "
|
|
2207
|
-
"that commit's refactor requests before resolving them (
|
|
2208
|
-
"Used by the Refactor Terminal's [r] action.",
|
|
2267
|
+
"that commit's refactor requests before resolving them (docs/41; works in "
|
|
2268
|
+
"local and cloud/BYOK modes). Used by the Refactor Terminal's [r] action.",
|
|
2209
2269
|
),
|
|
2210
2270
|
changed: bool = typer.Option(
|
|
2211
2271
|
False,
|
|
2212
2272
|
"--changed",
|
|
2213
|
-
help="Scope to the working tree's changed + graph-associated files
|
|
2273
|
+
help="Scope to the working tree's changed + graph-associated files.",
|
|
2214
2274
|
),
|
|
2215
2275
|
intent: str | None = typer.Option(
|
|
2216
2276
|
None,
|
|
@@ -2299,16 +2359,11 @@ def code(
|
|
|
2299
2359
|
register_project(project_root, constitution_hash=constitution.content_hash)
|
|
2300
2360
|
_maybe_build_graph(project_root, project_config)
|
|
2301
2361
|
|
|
2302
|
-
# Commit-scoped resolve (docs/41 `R28`, Phase 4)
|
|
2303
|
-
#
|
|
2304
|
-
# review-first so `[r]` in the Refactor Terminal resolves
|
|
2305
|
-
# commit's requests rather than a stale pending set.
|
|
2306
|
-
|
|
2307
|
-
console.print(
|
|
2308
|
-
"[red]code: --commit/--changed is local-only[/red] and is not "
|
|
2309
|
-
f"supported in {_cloud_execution_mode(constitution)} mode."
|
|
2310
|
-
)
|
|
2311
|
-
raise typer.Exit(code=2)
|
|
2362
|
+
# Commit-scoped resolve (docs/41 `R28`, Phase 4), symmetric to `review` and
|
|
2363
|
+
# available in every execution mode. When scoped, we (re)create THIS commit's
|
|
2364
|
+
# requests via a scoped review-first so `[r]` in the Refactor Terminal resolves
|
|
2365
|
+
# the selected commit's requests rather than a stale pending set. In cloud/BYOK
|
|
2366
|
+
# the review-first sends only the scoped documents to the platform.
|
|
2312
2367
|
code_scope = _resolve_commit_scope(
|
|
2313
2368
|
project_root=project_root,
|
|
2314
2369
|
constitution=constitution,
|
|
@@ -2372,6 +2427,7 @@ def code(
|
|
|
2372
2427
|
constitution=constitution,
|
|
2373
2428
|
target=target,
|
|
2374
2429
|
model_hint=provider,
|
|
2430
|
+
scope_paths=code_scope_paths,
|
|
2375
2431
|
)
|
|
2376
2432
|
cloud_review_findings = list(review_artifact.findings or [])
|
|
2377
2433
|
else:
|
|
@@ -3570,6 +3626,25 @@ def _print_detected_credentials(provider_name: str) -> None:
|
|
|
3570
3626
|
break
|
|
3571
3627
|
|
|
3572
3628
|
|
|
3629
|
+
def _report_dotenv(result: DotenvResult, project_root: Path) -> None:
|
|
3630
|
+
"""Print a concise ``.env`` status line for `doctor` diagnostics.
|
|
3631
|
+
|
|
3632
|
+
The entrypoint loads ``.env`` before any command runs, so ``result.present``
|
|
3633
|
+
(not ``applied``) reflects what the file contributes. Values are never shown.
|
|
3634
|
+
"""
|
|
3635
|
+
if result.found:
|
|
3636
|
+
count = len(result.present)
|
|
3637
|
+
console.print(
|
|
3638
|
+
f"[green].env[/green]: loaded {count} var{'s' if count != 1 else ''} "
|
|
3639
|
+
f"from {result.path}"
|
|
3640
|
+
)
|
|
3641
|
+
else:
|
|
3642
|
+
console.print(
|
|
3643
|
+
f"[yellow].env[/yellow]: none found near {project_root} "
|
|
3644
|
+
"(export your provider vars, or create a .env next to refactor.config)."
|
|
3645
|
+
)
|
|
3646
|
+
|
|
3647
|
+
|
|
3573
3648
|
def _missing_requirements(provider_name: str) -> list[str]:
|
|
3574
3649
|
requirements = _REQUIRED_ENV_BY_PROVIDER.get(provider_name, [])
|
|
3575
3650
|
missing: list[str] = []
|
|
@@ -3589,6 +3664,7 @@ def _cloud_doctor(
|
|
|
3589
3664
|
constitution,
|
|
3590
3665
|
execution_mode: str,
|
|
3591
3666
|
skip_ping: bool,
|
|
3667
|
+
dotenv: DotenvResult | None = None,
|
|
3592
3668
|
) -> None:
|
|
3593
3669
|
"""Cloud-mode readiness checks (C7): auth, entitlement, endpoint, credential.
|
|
3594
3670
|
|
|
@@ -3637,6 +3713,11 @@ def _cloud_doctor(
|
|
|
3637
3713
|
"[red]cloud_byok[/red]: set either `provider_key` (preferred) "
|
|
3638
3714
|
"or `credential_ref` in refactor.config."
|
|
3639
3715
|
)
|
|
3716
|
+
if dotenv is not None and not dotenv.found:
|
|
3717
|
+
console.print(
|
|
3718
|
+
"[dim] Tip: if `provider_key` uses ${ENV_VAR}, put that var in a "
|
|
3719
|
+
".env next to refactor.config (refactor loads it automatically).[/dim]"
|
|
3720
|
+
)
|
|
3640
3721
|
raise typer.Exit(code=1)
|
|
3641
3722
|
|
|
3642
3723
|
if skip_ping:
|
|
@@ -3758,14 +3839,23 @@ def doctor(
|
|
|
3758
3839
|
)
|
|
3759
3840
|
raise typer.Exit(code=1)
|
|
3760
3841
|
consti_path, _ = files
|
|
3842
|
+
project_root = consti_path.parent
|
|
3843
|
+
|
|
3844
|
+
# Load ``.env`` BEFORE parsing config so ``${VAR}`` interpolation (for
|
|
3845
|
+
# example ``provider_key: ${OPENAI_API_KEY}``) resolves from it. The
|
|
3846
|
+
# entrypoint already loads it for real runs; here we also surface the status
|
|
3847
|
+
# so a missing/misconfigured ``.env`` is flagged during diagnostics.
|
|
3848
|
+
dotenv = load_project_dotenv(project_root)
|
|
3849
|
+
|
|
3761
3850
|
constitution = load_constitution(consti_path)
|
|
3762
3851
|
project_config = load_config(consti_path.parent / CONFIG_FILENAME)
|
|
3763
|
-
project_root = consti_path.parent
|
|
3764
3852
|
|
|
3765
3853
|
if sandbox:
|
|
3766
3854
|
_report_sandbox(project_root, project_config)
|
|
3767
3855
|
return
|
|
3768
3856
|
|
|
3857
|
+
_report_dotenv(dotenv, project_root)
|
|
3858
|
+
|
|
3769
3859
|
execution_mode = _cloud_execution_mode(constitution)
|
|
3770
3860
|
if execution_mode:
|
|
3771
3861
|
_cloud_doctor(
|
|
@@ -3773,6 +3863,7 @@ def doctor(
|
|
|
3773
3863
|
constitution=constitution,
|
|
3774
3864
|
execution_mode=execution_mode,
|
|
3775
3865
|
skip_ping=skip_ping,
|
|
3866
|
+
dotenv=dotenv,
|
|
3776
3867
|
)
|
|
3777
3868
|
return
|
|
3778
3869
|
|
|
@@ -3807,6 +3898,16 @@ def doctor(
|
|
|
3807
3898
|
console.print("[red]provider env[/red]: missing required values:")
|
|
3808
3899
|
for item in missing:
|
|
3809
3900
|
console.print(f" - {item}")
|
|
3901
|
+
if not dotenv.found:
|
|
3902
|
+
console.print(
|
|
3903
|
+
"[dim] Tip: create a .env next to refactor.config with these vars "
|
|
3904
|
+
"(refactor loads it automatically), or export them in your shell.[/dim]"
|
|
3905
|
+
)
|
|
3906
|
+
else:
|
|
3907
|
+
console.print(
|
|
3908
|
+
f"[dim] Tip: add these vars to {dotenv.path} "
|
|
3909
|
+
"(refactor loads it automatically).[/dim]"
|
|
3910
|
+
)
|
|
3810
3911
|
raise typer.Exit(code=1)
|
|
3811
3912
|
console.print("[green]provider env[/green]: OK")
|
|
3812
3913
|
_print_detected_credentials(provider_name)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Minimal, dependency-free ``.env`` loading for the CLI.
|
|
2
|
+
|
|
3
|
+
Developers configure BYOK/provider credentials in ``refactor.config`` using
|
|
4
|
+
``${VAR}`` interpolation (for example ``provider_key: ${OPENAI_API_KEY}``).
|
|
5
|
+
Those tokens are resolved from ``os.environ`` at config-parse time, which means
|
|
6
|
+
a developer would otherwise have to ``set -a; source .env; set +a`` in every
|
|
7
|
+
shell (and the git hook's shell) before ``refactor`` could see the values.
|
|
8
|
+
|
|
9
|
+
This module loads a project ``.env`` into ``os.environ`` so that interpolation
|
|
10
|
+
"just works". It is intentionally tiny and has no third-party dependency so the
|
|
11
|
+
thin client stays light and deterministic.
|
|
12
|
+
|
|
13
|
+
Precedence: the real process environment always wins. Existing variables are
|
|
14
|
+
never overwritten unless ``override=True`` is passed explicitly.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
DOTENV_FILENAME = ".env"
|
|
25
|
+
DISABLE_ENV = "REFACTOR_NO_DOTENV"
|
|
26
|
+
_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
27
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DotenvResult:
|
|
32
|
+
"""Outcome of a ``.env`` load attempt (used for diagnostics/reporting)."""
|
|
33
|
+
|
|
34
|
+
path: Path | None = None
|
|
35
|
+
present: list[str] = field(default_factory=list)
|
|
36
|
+
applied: list[str] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def found(self) -> bool:
|
|
40
|
+
return self.path is not None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _disabled() -> bool:
|
|
44
|
+
return os.environ.get(DISABLE_ENV, "").strip().lower() in _TRUTHY
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_dotenv(start: Path | None = None) -> Path | None:
|
|
48
|
+
"""Return the nearest ``.env`` searching ``start`` (or CWD) and its parents."""
|
|
49
|
+
try:
|
|
50
|
+
current = Path(start).resolve() if start is not None else Path.cwd().resolve()
|
|
51
|
+
except OSError:
|
|
52
|
+
return None
|
|
53
|
+
for candidate in [current, *current.parents]:
|
|
54
|
+
env_path = candidate / DOTENV_FILENAME
|
|
55
|
+
if env_path.is_file():
|
|
56
|
+
return env_path
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _strip_quotes(value: str) -> str:
|
|
61
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
|
62
|
+
return value[1:-1]
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_dotenv(text: str) -> dict[str, str]:
|
|
67
|
+
"""Parse ``KEY=VALUE`` lines. Supports ``export`` prefix and quoted values.
|
|
68
|
+
|
|
69
|
+
Comment lines (``#``) and blanks are ignored. Inline comments are *not*
|
|
70
|
+
stripped so secret values containing ``#`` are preserved verbatim.
|
|
71
|
+
"""
|
|
72
|
+
parsed: dict[str, str] = {}
|
|
73
|
+
for raw_line in text.replace("\r\n", "\n").split("\n"):
|
|
74
|
+
line = raw_line.strip()
|
|
75
|
+
if not line or line.startswith("#"):
|
|
76
|
+
continue
|
|
77
|
+
if line.startswith("export "):
|
|
78
|
+
line = line[len("export "):].lstrip()
|
|
79
|
+
if "=" not in line:
|
|
80
|
+
continue
|
|
81
|
+
key, _, value = line.partition("=")
|
|
82
|
+
key = key.strip()
|
|
83
|
+
if not _KEY_RE.match(key):
|
|
84
|
+
continue
|
|
85
|
+
parsed[key] = _strip_quotes(value.strip())
|
|
86
|
+
return parsed
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_project_dotenv(start: Path | None = None, *, override: bool = False) -> DotenvResult:
|
|
90
|
+
"""Load the nearest ``.env`` into ``os.environ`` (real env wins by default).
|
|
91
|
+
|
|
92
|
+
Returns a :class:`DotenvResult` describing what was found and applied so
|
|
93
|
+
callers such as ``refactor doctor`` can report it. Never raises: any I/O or
|
|
94
|
+
parse issue degrades to an empty/partial result.
|
|
95
|
+
"""
|
|
96
|
+
if _disabled():
|
|
97
|
+
return DotenvResult()
|
|
98
|
+
env_path = find_dotenv(start)
|
|
99
|
+
if env_path is None:
|
|
100
|
+
return DotenvResult()
|
|
101
|
+
try:
|
|
102
|
+
text = env_path.read_text(encoding="utf-8")
|
|
103
|
+
except OSError:
|
|
104
|
+
return DotenvResult(path=env_path)
|
|
105
|
+
parsed = parse_dotenv(text)
|
|
106
|
+
applied: list[str] = []
|
|
107
|
+
for key, value in parsed.items():
|
|
108
|
+
if not override and key in os.environ:
|
|
109
|
+
continue
|
|
110
|
+
os.environ[key] = value
|
|
111
|
+
applied.append(key)
|
|
112
|
+
return DotenvResult(path=env_path, present=list(parsed.keys()), applied=applied)
|
|
@@ -96,6 +96,17 @@ def main(
|
|
|
96
96
|
),
|
|
97
97
|
) -> None:
|
|
98
98
|
"""refactor CLI."""
|
|
99
|
+
# Load a project ``.env`` early so ``${VAR}`` interpolation in
|
|
100
|
+
# ``refactor.config`` (and provider SDKs) resolves for every command and for
|
|
101
|
+
# the git-hook-driven commit loop, without a manual ``source .env``. Real
|
|
102
|
+
# environment variables always win; failures never break dispatch.
|
|
103
|
+
try:
|
|
104
|
+
from refactorai_cli.dotenv_loader import load_project_dotenv
|
|
105
|
+
|
|
106
|
+
load_project_dotenv()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
99
110
|
command_name = (ctx.invoked_subcommand or "").strip().lower()
|
|
100
111
|
if command_name in _INTRO_COMMANDS:
|
|
101
112
|
_print_agent_intro(command_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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.1
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -7,6 +7,7 @@ refactorai_cli/cloud_rr.py
|
|
|
7
7
|
refactorai_cli/commit_queue.py
|
|
8
8
|
refactorai_cli/control_plane.py
|
|
9
9
|
refactorai_cli/credentials.py
|
|
10
|
+
refactorai_cli/dotenv_loader.py
|
|
10
11
|
refactorai_cli/git_scope.py
|
|
11
12
|
refactorai_cli/local_constitution.py
|
|
12
13
|
refactorai_cli/local_engine_runtime.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
|