refactorai-cli 0.3.13__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/PKG-INFO +2 -2
  2. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/pyproject.toml +2 -2
  3. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/__init__.py +1 -1
  4. refactorai_cli-0.4.1/refactorai_cli/commands/hook_cmds.py +229 -0
  5. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/run_cmds.py +332 -5
  6. refactorai_cli-0.4.1/refactorai_cli/commands/watch_cmds.py +321 -0
  7. refactorai_cli-0.4.1/refactorai_cli/commit_queue.py +138 -0
  8. refactorai_cli-0.4.1/refactorai_cli/dotenv_loader.py +112 -0
  9. refactorai_cli-0.4.1/refactorai_cli/git_scope.py +215 -0
  10. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/main.py +22 -0
  11. refactorai_cli-0.4.1/refactorai_cli/review_runner.py +137 -0
  12. refactorai_cli-0.4.1/refactorai_cli/watch_state.py +318 -0
  13. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/PKG-INFO +2 -2
  14. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/SOURCES.txt +8 -1
  15. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/requires.txt +1 -1
  16. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/README.md +0 -0
  17. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/auth.py +0 -0
  18. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/client.py +0 -0
  19. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/cloud_rr.py +0 -0
  20. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/__init__.py +0 -0
  21. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/account_cmds.py +0 -0
  22. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/auth_cmds.py +0 -0
  23. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/cloud_cmds.py +0 -0
  24. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/engine_cmds.py +0 -0
  25. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/model_cmds.py +0 -0
  26. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/rules_cmds.py +0 -0
  27. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/runtime_cmds.py +0 -0
  28. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  29. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/setup_cmds.py +0 -0
  30. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  31. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/control_plane.py +0 -0
  32. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/credentials.py +0 -0
  33. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/local_constitution.py +0 -0
  34. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/local_engine_runtime.py +0 -0
  35. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/local_paths.py +0 -0
  36. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/model_policy.py +0 -0
  37. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/runtime_manager.py +0 -0
  38. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/settings.py +0 -0
  39. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli/setup_flow.py +0 -0
  40. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  41. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/entry_points.txt +0 -0
  42. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/refactorai_cli.egg-info/top_level.txt +0 -0
  43. {refactorai_cli-0.3.13 → refactorai_cli-0.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.3.13
3
+ Version: 0.4.1
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.1.1
11
+ Requires-Dist: refactorai-core>=3.2.0
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.3.13"
3
+ version = "0.4.1"
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.1.1",
15
+ "refactorai-core>=3.2.0",
16
16
  ]
17
17
 
18
18
  [project.scripts]
@@ -5,4 +5,4 @@ the shared `refactor_core` pipeline from a project folder while staying
5
5
  authenticated to the hosted platform via a developer key.
6
6
  """
7
7
 
8
- __version__ = "0.3.13"
8
+ __version__ = "0.4.1"
@@ -0,0 +1,229 @@
1
+ """`refactor hook` commands: local commit-trigger install + enqueue (docs/41 `R28`).
2
+
3
+ Phase 2 of the commit-triggered Refactor Terminal. ``install`` wires a
4
+ non-blocking ``post-commit`` git hook (via ``core.hooksPath``) that enqueues each
5
+ commit; ``enqueue`` is what that hook calls; ``status``/``uninstall`` manage it.
6
+
7
+ The hook and these commands live in the thin CLI (not the proprietary runtime)
8
+ so enqueuing is fast, offline, and never requires auth or a project config.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ import typer
17
+ from rich.console import Console
18
+
19
+ from refactorai_cli import commit_queue, git_scope
20
+
21
+ console = Console()
22
+ app = typer.Typer(
23
+ name="hook",
24
+ help="Manage the local commit-triggered review hook (docs/41).",
25
+ no_args_is_help=True,
26
+ add_completion=False,
27
+ )
28
+
29
+ _HOOK_MARKER = "RefactorAI post-commit hook"
30
+ _DEFAULT_HOOKS_DIR = ".githooks"
31
+
32
+ # Non-blocking + fail-open: capture HEAD, then detach the enqueue so `git commit`
33
+ # returns immediately. Skip-detection (merge/rebase/opt-out) happens in `enqueue`.
34
+ _POST_COMMIT_SCRIPT = """#!/usr/bin/env bash
35
+ # RefactorAI post-commit hook (docs/41 R28, Phase 2).
36
+ # Non-blocking + fail-open: this hook must never delay or fail `git commit`.
37
+ if ! command -v refactor >/dev/null 2>&1; then
38
+ exit 0
39
+ fi
40
+ _refactor_sha="$(git rev-parse HEAD 2>/dev/null)"
41
+ if [ -z "$_refactor_sha" ]; then
42
+ exit 0
43
+ fi
44
+ # Detach so the commit returns instantly; the enqueue does its own skip checks.
45
+ ( refactor hook enqueue --sha "$_refactor_sha" >/dev/null 2>&1 & ) >/dev/null 2>&1 || true
46
+ exit 0
47
+ """
48
+
49
+
50
+ def _require_repo(project_root: Path) -> str:
51
+ root = git_scope.repo_root(project_root)
52
+ if not root:
53
+ console.print("[red]hook:[/red] not inside a git repository.")
54
+ raise typer.Exit(code=1)
55
+ return root
56
+
57
+
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
+ """
67
+ existing_path = git_scope.get_config(root, "core.hooksPath")
68
+ if hooks_dir:
69
+ rel_dir = hooks_dir
70
+ elif existing_path:
71
+ rel_dir = existing_path
72
+ else:
73
+ rel_dir = _DEFAULT_HOOKS_DIR
74
+
75
+ hooks_path = (root / rel_dir).resolve()
76
+ hook_file = hooks_path / "post-commit"
77
+
78
+ if hook_file.exists() and _HOOK_MARKER not in _safe_read(hook_file) and not force:
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
+ }
85
+
86
+ try:
87
+ hooks_path.mkdir(parents=True, exist_ok=True)
88
+ hook_file.write_text(_POST_COMMIT_SCRIPT, encoding="utf-8")
89
+ hook_file.chmod(hook_file.stat().st_mode | 0o755)
90
+ except OSError as exc:
91
+ return {"status": "error", "rel_dir": rel_dir, "hook_file": None, "message": str(exc)}
92
+
93
+ if not git_scope.set_config(root, "core.hooksPath", rel_dir):
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']}")
134
+ raise typer.Exit(code=1)
135
+
136
+ console.print(
137
+ f"[green]Installed[/green] post-commit hook -> {result['hook_file']}\n"
138
+ f"[dim]core.hooksPath = {result['rel_dir']}[/dim]\n"
139
+ "Every commit now enqueues a local review event. "
140
+ "Open the Refactor Terminal with `refactor watch`."
141
+ )
142
+
143
+
144
+ @app.command("uninstall")
145
+ def uninstall(
146
+ reset_hooks_path: bool = typer.Option(
147
+ False,
148
+ "--reset-hooks-path",
149
+ help="Also unset core.hooksPath (only do this if RefactorAI set it).",
150
+ ),
151
+ ) -> None:
152
+ """Remove the RefactorAI post-commit hook."""
153
+ root = Path(_require_repo(Path.cwd()))
154
+ rel_dir = git_scope.get_config(root, "core.hooksPath") or _DEFAULT_HOOKS_DIR
155
+ hook_file = (root / rel_dir / "post-commit").resolve()
156
+
157
+ removed = False
158
+ if hook_file.exists() and _HOOK_MARKER in _safe_read(hook_file):
159
+ try:
160
+ hook_file.unlink()
161
+ removed = True
162
+ except OSError as exc:
163
+ console.print(f"[red]hook: could not remove hook:[/red] {exc}")
164
+ raise typer.Exit(code=1) from exc
165
+
166
+ if reset_hooks_path:
167
+ git_scope.unset_config(root, "core.hooksPath")
168
+
169
+ if removed:
170
+ console.print("[green]Removed[/green] the RefactorAI post-commit hook.")
171
+ else:
172
+ console.print("[dim]hook: no RefactorAI post-commit hook found to remove.[/dim]")
173
+
174
+
175
+ @app.command("enqueue")
176
+ def enqueue(
177
+ sha: str = typer.Option("HEAD", "--sha", help="Commit SHA/ref to enqueue."),
178
+ verbose: bool = typer.Option(False, "--verbose", help="Print the enqueue result."),
179
+ ) -> None:
180
+ """Enqueue a commit event (invoked by the post-commit hook). Always exits 0."""
181
+ project_root = Path(os.environ.get("REFACTOR_PROJECT_ROOT") or Path.cwd())
182
+ try:
183
+ enqueued, reason, event = commit_queue.enqueue_commit_event(project_root, sha)
184
+ except Exception as exc: # fail-open: never let enqueue affect the commit flow
185
+ if verbose:
186
+ console.print(f"[yellow]hook enqueue error (ignored):[/yellow] {exc}")
187
+ return
188
+ if verbose:
189
+ if enqueued and event:
190
+ console.print(
191
+ f"[green]enqueued[/green] {event['sha'][:10]} "
192
+ f"({len(event['changed_files'])} changed file(s)) -> "
193
+ f"{commit_queue.queue_path(project_root)}"
194
+ )
195
+ else:
196
+ console.print(f"[dim]hook: not enqueued ({reason}).[/dim]")
197
+
198
+
199
+ @app.command("status")
200
+ def status() -> None:
201
+ """Show hook installation state and the pending commit-event count."""
202
+ root_str = git_scope.repo_root(Path.cwd())
203
+ if not root_str:
204
+ console.print("[yellow]hook:[/yellow] not inside a git repository.")
205
+ raise typer.Exit(code=1)
206
+ root = Path(root_str)
207
+
208
+ rel_dir = git_scope.get_config(root, "core.hooksPath")
209
+ hook_file = (root / (rel_dir or _DEFAULT_HOOKS_DIR) / "post-commit").resolve()
210
+ is_ours = hook_file.exists() and _HOOK_MARKER in _safe_read(hook_file)
211
+ q_path = commit_queue.queue_path(root)
212
+
213
+ console.print(f"Repository: [bold]{root}[/bold]")
214
+ console.print(f"core.hooksPath: {rel_dir or '[dim](unset)[/dim]'}")
215
+ if is_ours:
216
+ console.print(f"post-commit hook: [green]installed[/green] ({hook_file})")
217
+ elif hook_file.exists():
218
+ console.print(f"post-commit hook: [yellow]present but not RefactorAI[/yellow] ({hook_file})")
219
+ else:
220
+ console.print("post-commit hook: [red]not installed[/red] (run `refactor hook install`)")
221
+ console.print(f"Queue: {q_path}")
222
+ console.print(f"Pending events: [bold]{commit_queue.pending_count(root)}[/bold]")
223
+
224
+
225
+ def _safe_read(path: Path) -> str:
226
+ try:
227
+ return path.read_text(encoding="utf-8")
228
+ except OSError:
229
+ return ""