refactorai-cli 0.2.3__tar.gz → 0.2.5__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 (31) hide show
  1. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/PKG-INFO +1 -1
  2. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/pyproject.toml +1 -1
  3. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/setup_cmds.py +1 -0
  5. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/local_engine_runtime.py +27 -5
  6. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/setup_flow.py +63 -9
  7. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/PKG-INFO +1 -1
  8. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/README.md +0 -0
  9. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/auth.py +0 -0
  10. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/client.py +0 -0
  11. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/__init__.py +0 -0
  12. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/auth_cmds.py +0 -0
  13. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/engine_cmds.py +0 -0
  14. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/model_cmds.py +0 -0
  15. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/rules_cmds.py +0 -0
  16. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/run_cmds.py +0 -0
  17. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/commands/runtime_cmds.py +0 -0
  18. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/control_plane.py +0 -0
  19. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/credentials.py +0 -0
  20. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/local_constitution.py +0 -0
  21. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/local_paths.py +0 -0
  22. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/main.py +0 -0
  23. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/model_policy.py +0 -0
  24. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/runtime_manager.py +0 -0
  25. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli/settings.py +0 -0
  26. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  27. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  28. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/entry_points.txt +0 -0
  29. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/requires.txt +0 -0
  30. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/refactorai_cli.egg-info/top_level.txt +0 -0
  31. {refactorai_cli-0.2.3 → refactorai_cli-0.2.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.2.3"
8
+ __version__ = "0.2.5"
@@ -53,6 +53,7 @@ def setup(
53
53
  resume=resume,
54
54
  from_stage=from_stage,
55
55
  ask_approval=lambda prompt: _ask_approval(yes, prompt),
56
+ on_progress=lambda message: console.print(f"[cyan]{message}[/cyan]"),
56
57
  )
57
58
  except SetupError as exc:
58
59
  console.print(f"[red]Setup failed:[/red] {exc}")
@@ -7,6 +7,7 @@ remain usable without a separate ``refactor-core`` package dependency.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
+ import re
10
11
  import shutil
11
12
  import subprocess
12
13
  from datetime import datetime, timezone
@@ -14,7 +15,7 @@ from pathlib import Path
14
15
 
15
16
  from refactorai_cli.local_paths import ensure_dir, refactor_home
16
17
 
17
- DEFAULT_ENGINE_IMAGE = "docker.io/library/alpine:3.20"
18
+ DEFAULT_ENGINE_IMAGE = "docker.io/ollama/ollama:latest"
18
19
  DEFAULT_ENGINE_CONTAINER = "refactor-engine"
19
20
  DEFAULT_ENGINE_PORT = 17777
20
21
  DEFAULT_ENGINE_PROFILE = "cpu-small"
@@ -22,6 +23,8 @@ ENGINE_DIR = "engine"
22
23
  ENGINE_STATE_FILE = "engine.json"
23
24
  ENGINE_MODELS_DIR = "models"
24
25
  ENGINE_CACHE_DIR = "cache/ollama"
26
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
27
+ _CONTROL_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
25
28
 
26
29
 
27
30
  def _now_iso() -> str:
@@ -85,6 +88,13 @@ def _run(cmd: list[str]) -> subprocess.CompletedProcess:
85
88
  return subprocess.run(cmd, capture_output=True, text=True)
86
89
 
87
90
 
91
+ def _clean_output(text: str) -> str:
92
+ cleaned = _ANSI_RE.sub("", text or "")
93
+ cleaned = cleaned.replace("\r", "\n")
94
+ cleaned = _CONTROL_RE.sub("", cleaned)
95
+ return "\n".join(line.strip() for line in cleaned.splitlines() if line.strip())
96
+
97
+
88
98
  def _exists(runtime: str, name: str) -> bool:
89
99
  proc = _run([runtime, "container", "exists", name])
90
100
  return proc.returncode == 0
@@ -96,10 +106,22 @@ def _running(runtime: str, name: str) -> bool:
96
106
 
97
107
 
98
108
  def _health(runtime: str, name: str) -> bool:
99
- proc = _run([runtime, "exec", name, "sh", "-lc", "command -v ollama >/dev/null 2>&1"])
109
+ has_binary = _run([runtime, "exec", name, "sh", "-lc", "command -v ollama >/dev/null 2>&1"])
110
+ if has_binary.returncode != 0:
111
+ return False
112
+ proc = _run([runtime, "exec", name, "sh", "-lc", "ollama list >/dev/null 2>&1"])
100
113
  return proc.returncode == 0
101
114
 
102
115
 
116
+ def _run_args_for_image(image: str) -> list[str]:
117
+ normalized = str(image or "").strip().lower()
118
+ if "ollama/ollama" in normalized:
119
+ # Let the container run its default command (ollama serve), which keeps
120
+ # the model API available for pull/show operations.
121
+ return []
122
+ return ["sleep", "infinity"]
123
+
124
+
103
125
  def ensure_engine_up(
104
126
  *,
105
127
  runtime: str,
@@ -114,6 +136,7 @@ def ensure_engine_up(
114
136
  if rebuild and _exists(runtime, container_name):
115
137
  _run([runtime, "rm", "-f", container_name])
116
138
  if not _exists(runtime, container_name):
139
+ command_args = _run_args_for_image(image)
117
140
  proc = _run(
118
141
  [
119
142
  runtime,
@@ -128,8 +151,7 @@ def ensure_engine_up(
128
151
  "-v",
129
152
  f"{cache_dir}:/root/.cache/ollama:Z",
130
153
  image,
131
- "sleep",
132
- "infinity",
154
+ *command_args,
133
155
  ]
134
156
  )
135
157
  if proc.returncode != 0:
@@ -223,7 +245,7 @@ def pull_model(*, runtime: str, container_name: str, model_id: str) -> tuple[boo
223
245
  if probe.returncode != 0:
224
246
  return False, "Engine image does not include `ollama`; use a model-capable engine image."
225
247
  proc = _run([runtime, "exec", container_name, "sh", "-lc", f"ollama pull {model_id}"])
226
- text = (proc.stdout or proc.stderr or "").strip()
248
+ text = _clean_output(proc.stdout or proc.stderr or "")
227
249
  if proc.returncode != 0:
228
250
  return False, text or "Model pull failed."
229
251
  return True, text or "Model pull complete."
@@ -7,10 +7,12 @@ import shutil
7
7
  from dataclasses import dataclass
8
8
  from datetime import datetime, timezone
9
9
  from pathlib import Path
10
+ from inspect import signature
10
11
  from typing import Callable
11
12
 
12
13
  from refactorai_cli.local_engine_runtime import (
13
14
  DEFAULT_ENGINE_CONTAINER,
15
+ DEFAULT_ENGINE_IMAGE,
14
16
  engine_status,
15
17
  ensure_engine_up,
16
18
  pull_model,
@@ -141,7 +143,7 @@ def get_setup_diagnostics() -> dict:
141
143
  return {"state": state, "stages": stages}
142
144
 
143
145
 
144
- def _stage_s1_precheck(_ask_approval: Callable[[str], bool]) -> dict:
146
+ def _stage_s1_precheck(_ask_approval: Callable[[str], bool], _emit: Callable[[str], None] | None = None) -> dict:
145
147
  profile = detect_machine_profile()
146
148
  runtime, reason = resolve_engine_runtime("podman")
147
149
  disk = shutil.disk_usage(str(refactor_home()))
@@ -158,7 +160,7 @@ def _stage_s1_precheck(_ask_approval: Callable[[str], bool]) -> dict:
158
160
  }
159
161
 
160
162
 
161
- def _stage_s2_auth(_ask_approval: Callable[[str], bool]) -> dict:
163
+ def _stage_s2_auth(_ask_approval: Callable[[str], bool], _emit: Callable[[str], None] | None = None) -> dict:
162
164
  auth = ensure_authenticated(force_remote=True)
163
165
  lease = ensure_lease(force_refresh=True)
164
166
  return {
@@ -170,11 +172,17 @@ def _stage_s2_auth(_ask_approval: Callable[[str], bool]) -> dict:
170
172
  }
171
173
 
172
174
 
173
- def _stage_s3_runtime(ask_approval: Callable[[str], bool]) -> dict:
175
+ def _stage_s3_runtime(ask_approval: Callable[[str], bool], emit: Callable[[str], None] | None = None) -> dict:
176
+ if emit:
177
+ emit("Resolving runtime manifest from control plane...")
174
178
  manifest = resolve_runtime_manifest(channel="stable")
175
179
  if not ask_approval(f"Install runtime artifact version {manifest.runtime_version}?"):
176
180
  raise SetupError("Runtime installation was not approved.")
181
+ if emit:
182
+ emit(f"Downloading runtime artifact {manifest.runtime_version}...")
177
183
  artifact = download_artifact(manifest.artifact_url)
184
+ if emit:
185
+ emit("Verifying and activating runtime artifact...")
178
186
  path = activate_runtime(manifest, artifact, channel="stable")
179
187
  status = runtime_status()
180
188
  return {
@@ -199,7 +207,9 @@ def _selected_backend() -> str:
199
207
  return backend
200
208
 
201
209
 
202
- def _stage_s4_backend_choice(ask_approval: Callable[[str], bool]) -> dict:
210
+ def _stage_s4_backend_choice(
211
+ ask_approval: Callable[[str], bool], _emit: Callable[[str], None] | None = None
212
+ ) -> dict:
203
213
  if ask_approval("Install local refactor-server and local model now?"):
204
214
  backend = BACKEND_LOCAL_SERVER
205
215
  else:
@@ -210,7 +220,7 @@ def _stage_s4_backend_choice(ask_approval: Callable[[str], bool]) -> dict:
210
220
  }
211
221
 
212
222
 
213
- def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool]) -> dict:
223
+ def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool], emit: Callable[[str], None] | None = None) -> dict:
214
224
  backend = _selected_backend()
215
225
  if backend == BACKEND_BYOK:
216
226
  return {
@@ -221,16 +231,39 @@ def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool]) -> dict:
221
231
  }
222
232
  if not ask_approval("Install/start local refactor-server and bootstrap model now?"):
223
233
  raise SetupError("Local server bootstrap was not approved.")
234
+ if emit:
235
+ emit("Checking local engine runtime availability...")
224
236
  runtime, reason = resolve_engine_runtime("podman")
225
237
  if not runtime:
226
238
  raise SetupError(reason or "Engine runtime unavailable.")
239
+ if emit:
240
+ emit("Starting local refactor-engine container...")
227
241
  ok, message, state = ensure_engine_up(runtime=runtime)
228
242
  if not ok:
229
243
  raise SetupError(f"Local server bootstrap failed: {message}")
230
244
  estatus = engine_status(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER)
231
245
  if str(estatus.get("status") or "").lower() != "ready":
232
- raise SetupError("Local server is not ready. Run `refactor engine status` and retry from S5.")
246
+ # If a stale container from older installs exists (for example an image
247
+ # without ollama), rebuild once with the default engine image.
248
+ if emit:
249
+ emit("Detected non-ready engine container. Rebuilding once with default model runtime image...")
250
+ ok, message, _state = ensure_engine_up(
251
+ runtime=runtime,
252
+ image=DEFAULT_ENGINE_IMAGE,
253
+ container_name=DEFAULT_ENGINE_CONTAINER,
254
+ rebuild=True,
255
+ )
256
+ if not ok:
257
+ raise SetupError(f"Local server bootstrap failed: {message}")
258
+ estatus = engine_status(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER)
259
+ if str(estatus.get("status") or "").lower() != "ready":
260
+ raise SetupError(
261
+ f"Local server is not ready (status={estatus.get('status')}). "
262
+ "Run `refactor engine status` and retry from S5."
263
+ )
233
264
 
265
+ if emit:
266
+ emit("Resolving signed model policy...")
234
267
  policy = resolve_policy(force_refresh=False)
235
268
  profile = detect_machine_profile()
236
269
  model_id = recommended_model_id(policy, profile=str(profile["profile"]))
@@ -245,6 +278,11 @@ def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool]) -> dict:
245
278
  runtime, reason = resolve_engine_runtime("podman")
246
279
  if not runtime:
247
280
  raise SetupError(reason or "Engine runtime unavailable.")
281
+ if emit:
282
+ emit(
283
+ f"Pulling recommended model '{model_id}' in local engine "
284
+ "(this can take several minutes on first download)..."
285
+ )
248
286
  ok, message = pull_model(runtime=runtime, container_name=DEFAULT_ENGINE_CONTAINER, model_id=model_id)
249
287
  if not ok:
250
288
  raise SetupError(f"Model pull failed: {message}")
@@ -257,7 +295,7 @@ def _stage_s5_backend_bootstrap(ask_approval: Callable[[str], bool]) -> dict:
257
295
  }
258
296
 
259
297
 
260
- def _stage_s6_validate(_ask_approval: Callable[[str], bool]) -> dict:
298
+ def _stage_s6_validate(_ask_approval: Callable[[str], bool], _emit: Callable[[str], None] | None = None) -> dict:
261
299
  backend = _selected_backend()
262
300
  runtime_status_data = runtime_status()
263
301
  if not runtime_status_data.get("active_version") or not runtime_status_data.get("active_artifact_exists"):
@@ -293,7 +331,7 @@ def _stage_s6_validate(_ask_approval: Callable[[str], bool]) -> dict:
293
331
  }
294
332
 
295
333
 
296
- STAGE_HANDLERS: dict[str, Callable[[Callable[[str], bool]], dict]] = {
334
+ STAGE_HANDLERS: dict[str, Callable[..., dict]] = {
297
335
  "S1": _stage_s1_precheck,
298
336
  "S2": _stage_s2_auth,
299
337
  "S3": _stage_s3_runtime,
@@ -303,12 +341,24 @@ STAGE_HANDLERS: dict[str, Callable[[Callable[[str], bool]], dict]] = {
303
341
  }
304
342
 
305
343
 
344
+ def _invoke_stage_handler(
345
+ handler: Callable,
346
+ ask_approval: Callable[[str], bool],
347
+ on_progress: Callable[[str], None] | None,
348
+ ) -> dict:
349
+ param_names = list(signature(handler).parameters.keys())
350
+ if any(name in {"emit", "_emit", "on_progress"} for name in param_names):
351
+ return handler(ask_approval, on_progress)
352
+ return handler(ask_approval)
353
+
354
+
306
355
  def run_setup(
307
356
  *,
308
357
  auto_approve: bool = False,
309
358
  resume: bool = False,
310
359
  from_stage: str | None = None,
311
360
  ask_approval: Callable[[str], bool] | None = None,
361
+ on_progress: Callable[[str], None] | None = None,
312
362
  ) -> SetupResult:
313
363
  if ask_approval is None:
314
364
  ask_approval = lambda _prompt: bool(auto_approve)
@@ -344,6 +394,8 @@ def run_setup(
344
394
  }
345
395
  )
346
396
  for stage_id in STAGE_IDS[start_index:]:
397
+ if on_progress:
398
+ on_progress(f"Running stage {stage_id} ({STAGE_NAMES.get(stage_id, stage_id)})...")
347
399
  write_setup_state(
348
400
  {
349
401
  "status": "in_progress",
@@ -355,7 +407,7 @@ def run_setup(
355
407
  )
356
408
  handler = STAGE_HANDLERS[stage_id]
357
409
  try:
358
- output = handler(ask_approval)
410
+ output = _invoke_stage_handler(handler, ask_approval, on_progress)
359
411
  except SetupError as exc:
360
412
  write_setup_state(
361
413
  {
@@ -394,6 +446,8 @@ def run_setup(
394
446
  write_stage_output(stage_id, output)
395
447
  if stage_id not in completed:
396
448
  completed.append(stage_id)
449
+ if on_progress:
450
+ on_progress(f"Completed stage {stage_id}.")
397
451
  final_backend = _normalize_backend(read_setup_state().get("execution_backend"))
398
452
  write_setup_state(
399
453
  {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
File without changes
File without changes