refactorai-cli 0.2.14__tar.gz → 0.2.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.
Files changed (32) hide show
  1. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/PKG-INFO +1 -1
  2. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/pyproject.toml +1 -1
  3. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/run_cmds.py +256 -23
  5. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/runtime_cmds.py +18 -1
  6. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/runtime_manager.py +38 -5
  7. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/PKG-INFO +1 -1
  8. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/README.md +0 -0
  9. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/auth.py +0 -0
  10. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/client.py +0 -0
  11. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/__init__.py +0 -0
  12. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/auth_cmds.py +0 -0
  13. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/engine_cmds.py +0 -0
  14. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/model_cmds.py +0 -0
  15. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/rules_cmds.py +0 -0
  16. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  17. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/commands/setup_cmds.py +0 -0
  18. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/control_plane.py +0 -0
  19. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/credentials.py +0 -0
  20. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/local_constitution.py +0 -0
  21. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/local_engine_runtime.py +0 -0
  22. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/local_paths.py +0 -0
  23. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/main.py +0 -0
  24. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/model_policy.py +0 -0
  25. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/settings.py +0 -0
  26. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli/setup_flow.py +0 -0
  27. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  28. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  29. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/entry_points.txt +0 -0
  30. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/requires.txt +0 -0
  31. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/refactorai_cli.egg-info/top_level.txt +0 -0
  32. {refactorai_cli-0.2.14 → refactorai_cli-0.2.16}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.14
3
+ Version: 0.2.16
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.14"
3
+ version = "0.2.16"
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.14"
8
+ __version__ = "0.2.16"
@@ -46,13 +46,20 @@ from refactor_core.budgeting import compute_budget
46
46
  from refactor_core.capabilities import resolve_capabilities
47
47
  from refactor_core.execution import resolve_mode, run_with_mode
48
48
  from refactor_core.execution.rr_executor import run_refactor_requests
49
+ from refactor_core.execution.structural_ops import structural_compatibility_matrix
49
50
  from refactor_core.gate import evaluate_gate
50
51
  from refactor_core.refactor_requests import (
52
+ DEFERRED,
53
+ RequestSuppression,
54
+ RequestWarning,
51
55
  clear_requests,
52
56
  create_requests,
53
57
  list_requests,
58
+ load_request,
54
59
  pending_requests,
60
+ save_request,
55
61
  )
62
+ from refactor_core.refactor_ops import remediation_hint
56
63
  from refactor_core.indexing import index_target
57
64
  from refactor_core.models import Finding, GeneratedTest, PatchGroup, RunArtifact
58
65
  from refactor_core.pipeline import build_refactor_prompt, build_review_prompt
@@ -486,6 +493,20 @@ def _rr_progress_logger(status=None) -> Callable[[dict], None]:
486
493
  elif ev == "run_done":
487
494
  if status:
488
495
  status.update("[green]code[/green] completed.")
496
+ elif ev == "rr_deferred":
497
+ if status:
498
+ status.update("[yellow]code[/yellow] deferred unsupported operation...")
499
+ reason = str(event.get("reason", "") or "")[:300]
500
+ hint = remediation_hint(reason)
501
+ suffix = f" [dim]hint: {hint}[/dim]" if hint else ""
502
+ console.print(f"[yellow] ↷ deferred:[/yellow] {reason}{suffix}")
503
+ elif ev == "rr_failed":
504
+ if status:
505
+ status.update("[red]code[/red] failed unsupported operation.")
506
+ reason = str(event.get("reason", "") or "")[:300]
507
+ hint = remediation_hint(reason)
508
+ suffix = f" [dim]hint: {hint}[/dim]" if hint else ""
509
+ console.print(f"[red] ✗ failed:[/red] {reason}{suffix}")
489
510
 
490
511
  return _log
491
512
 
@@ -1536,9 +1557,13 @@ def review(
1536
1557
  _print_findings(artifact.findings)
1537
1558
 
1538
1559
  # A fresh review supersedes prior refactor requests: replace them with one
1539
- # RR per finding (central-only; nothing is written into the project tree).
1540
- clear_requests(project_root)
1541
- requests = create_requests(project_root, artifact.findings, run_id=artifact.run_id)
1560
+ # RR per actionable finding (central-only; nothing is written into project tree).
1561
+ requests = _replace_requests_with_framework_guardrails(
1562
+ project_root=project_root,
1563
+ findings=artifact.findings,
1564
+ run_id=artifact.run_id,
1565
+ artifact=artifact,
1566
+ )
1542
1567
  if requests:
1543
1568
  console.print(
1544
1569
  f"\n[bold]Created {len(requests)} refactor request(s).[/bold] "
@@ -1573,6 +1598,16 @@ def code(
1573
1598
  "--auto-approve",
1574
1599
  help="Skip interactive issue approvals in agentic code mode.",
1575
1600
  ),
1601
+ on_rr_failure: str = typer.Option(
1602
+ "stop",
1603
+ "--on-rr-failure",
1604
+ help="RR failure behavior: stop | continue | defer.",
1605
+ ),
1606
+ equivalence: str = typer.Option(
1607
+ "strict",
1608
+ "--equivalence",
1609
+ help="Equivalence policy: strict | balanced.",
1610
+ ),
1576
1611
  ) -> None:
1577
1612
  """Work through refactor requests one by one, applying & verifying each.
1578
1613
 
@@ -1637,9 +1672,11 @@ def code(
1637
1672
  review_store = create_run(project_root, run_id=review_artifact.run_id)
1638
1673
  review_store.write_artifact(review_artifact, target=target, status="completed")
1639
1674
  _print_findings(review_artifact.findings)
1640
- clear_requests(project_root)
1641
- requests = create_requests(
1642
- project_root, review_artifact.findings, run_id=review_artifact.run_id
1675
+ requests = _replace_requests_with_framework_guardrails(
1676
+ project_root=project_root,
1677
+ findings=review_artifact.findings,
1678
+ run_id=review_artifact.run_id,
1679
+ artifact=review_artifact,
1643
1680
  )
1644
1681
  console.print(f"[bold]Created {len(requests)} refactor request(s).[/bold]")
1645
1682
 
@@ -1656,19 +1693,21 @@ def code(
1656
1693
  return
1657
1694
 
1658
1695
  with console.status("[cyan]code[/cyan] running...", spinner="dots") as status:
1696
+ rr_config = (
1697
+ dict(getattr(project_config, "settings", {}) or {})
1698
+ | ({"sandbox": {"enabled": True}} if sandbox_on else {})
1699
+ | ({"_sandbox_context": sandbox_ctx} if sandbox_ctx else {})
1700
+ ) or dict(getattr(constitution, "settings", {}) or {})
1659
1701
  artifact = run_refactor_requests(
1660
1702
  project_root=project_root,
1661
1703
  constitution=constitution,
1662
1704
  provider=selected_provider,
1663
1705
  requests=requests,
1664
- config=(
1665
- dict(getattr(project_config, "settings", {}) or {})
1666
- | ({"sandbox": {"enabled": True}} if sandbox_on else {})
1667
- | ({"_sandbox_context": sandbox_ctx} if sandbox_ctx else {})
1668
- )
1669
- or dict(getattr(constitution, "settings", {}) or {}),
1706
+ config=rr_config,
1670
1707
  progress_cb=_rr_progress_logger(status=status),
1671
1708
  approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
1709
+ on_rr_failure=on_rr_failure,
1710
+ equivalence_mode=equivalence,
1672
1711
  )
1673
1712
  except Exception as exc:
1674
1713
  console.print(f"[red]Refactor failed:[/red] {exc}")
@@ -1686,32 +1725,190 @@ def code(
1686
1725
  )
1687
1726
 
1688
1727
 
1689
- def requests() -> None:
1690
- """List refactor requests and their status for this project."""
1728
+ def requests(
1729
+ show_operation: bool = typer.Option(
1730
+ False,
1731
+ "--show-operation",
1732
+ help="Show operation scope details (source/target/split map).",
1733
+ ),
1734
+ retry_deferred: bool = typer.Option(
1735
+ False,
1736
+ "--retry-deferred",
1737
+ help="Re-run deferred requests immediately.",
1738
+ ),
1739
+ provider: str | None = typer.Option(
1740
+ None,
1741
+ "--provider",
1742
+ help="Provider override when --retry-deferred is used.",
1743
+ ),
1744
+ auto_approve: bool = typer.Option(
1745
+ False,
1746
+ "--auto-approve",
1747
+ help="Auto-approve retry-deferred proposals.",
1748
+ ),
1749
+ on_rr_failure: str = typer.Option(
1750
+ "stop",
1751
+ "--on-rr-failure",
1752
+ help="RR failure behavior for retry: stop | continue | defer.",
1753
+ ),
1754
+ equivalence: str = typer.Option(
1755
+ "strict",
1756
+ "--equivalence",
1757
+ help="Equivalence policy for retry: strict | balanced.",
1758
+ ),
1759
+ ) -> None:
1760
+ """List refactor requests and optionally re-run deferred requests."""
1691
1761
  _require_auth()
1692
- project_root, _constitution, _config = _load_project()
1762
+ project_root, constitution, project_config = _load_project()
1763
+ if retry_deferred:
1764
+ deferred_rrs = [rr for rr in list_requests(project_root) if rr.status == DEFERRED]
1765
+ if not deferred_rrs:
1766
+ console.print("[dim]No deferred requests to retry.[/dim]")
1767
+ else:
1768
+ selected_provider = get_provider(
1769
+ constitution, project_root=project_root, provider_name=provider
1770
+ )
1771
+ for rr in deferred_rrs:
1772
+ rr.status = "pending"
1773
+ loaded = load_request(project_root, rr.rr_id)
1774
+ if loaded is not None:
1775
+ loaded.status = "pending"
1776
+ save_request(project_root, loaded)
1777
+ with console.status("[cyan]requests[/cyan] retrying deferred...", spinner="dots") as status:
1778
+ _ = run_refactor_requests(
1779
+ project_root=project_root,
1780
+ constitution=constitution,
1781
+ provider=selected_provider,
1782
+ requests=[rr for rr in list_requests(project_root) if rr.status == "pending"],
1783
+ config=dict(getattr(project_config, "settings", {}) or {}),
1784
+ progress_cb=_rr_progress_logger(status=status),
1785
+ approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
1786
+ on_rr_failure=on_rr_failure,
1787
+ equivalence_mode=equivalence,
1788
+ )
1693
1789
  rrs = list_requests(project_root)
1694
1790
  if not rrs:
1695
1791
  console.print("[dim]No refactor requests. Run `refactor review` to create them.[/dim]")
1696
1792
  return
1697
1793
  table = Table(title="Refactor requests", show_lines=False)
1698
1794
  table.add_column("issue")
1795
+ table.add_column("operation", no_wrap=True)
1796
+ if show_operation:
1797
+ table.add_column("scope")
1699
1798
  table.add_column("severity", no_wrap=True)
1700
1799
  table.add_column("category")
1701
1800
  table.add_column("status", no_wrap=True)
1702
1801
  table.add_column("attempts", no_wrap=True)
1703
- color = {"applied": "green", "denied": "yellow", "failed": "red", "pending": "cyan", "in_progress": "cyan"}
1802
+ color = {
1803
+ "applied": "green",
1804
+ "denied": "yellow",
1805
+ "failed": "red",
1806
+ "deferred": "yellow",
1807
+ "pending": "cyan",
1808
+ "in_progress": "cyan",
1809
+ }
1704
1810
  for rr in rrs:
1705
- table.add_row(
1811
+ row = [
1706
1812
  rr.location,
1707
- rr.severity,
1708
- rr.category,
1709
- f"[{color.get(rr.status, 'white')}]{rr.status}[/{color.get(rr.status, 'white')}]",
1710
- str(rr.attempt_count),
1813
+ rr.operation_type,
1814
+ ]
1815
+ if show_operation:
1816
+ row.append(_format_operation_scope(rr))
1817
+ row.extend(
1818
+ [
1819
+ rr.severity,
1820
+ rr.category,
1821
+ f"[{color.get(rr.status, 'white')}]{rr.status}[/{color.get(rr.status, 'white')}]",
1822
+ str(rr.attempt_count),
1823
+ ]
1711
1824
  )
1825
+ table.add_row(*row)
1712
1826
  console.print(table)
1713
1827
 
1714
1828
 
1829
+ def _format_operation_scope(rr) -> str:
1830
+ scope = getattr(rr, "scope", None)
1831
+ if scope is None:
1832
+ return "-"
1833
+ source = str(getattr(scope, "source_path", "") or "").strip()
1834
+ target = str(getattr(scope, "target_path", "") or "").strip()
1835
+ split_map = dict(getattr(scope, "split_map", {}) or {})
1836
+ symbol = str(getattr(scope, "symbol", "") or "").strip()
1837
+ new_name = str(getattr(scope, "new_name", "") or "").strip()
1838
+ if split_map:
1839
+ if "__sources__" in split_map:
1840
+ srcs = ", ".join(str(item) for item in split_map.get("__sources__", []))
1841
+ return f"merge: [{srcs}] -> {target or '?'}"
1842
+ chunks = []
1843
+ for dst, symbols in split_map.items():
1844
+ if symbols:
1845
+ chunks.append(f"{dst}({','.join(str(s) for s in symbols)})")
1846
+ else:
1847
+ chunks.append(str(dst))
1848
+ src_label = source or "?"
1849
+ return f"split: {src_label} -> {', '.join(chunks)}"
1850
+ if source or target:
1851
+ if source and target:
1852
+ return f"{source} -> {target}"
1853
+ return source or target
1854
+ if symbol and new_name:
1855
+ return f"{symbol} -> {new_name}"
1856
+ return "-"
1857
+
1858
+
1859
+ def _replace_requests_with_framework_guardrails(
1860
+ *,
1861
+ project_root: Path,
1862
+ findings: list[Finding],
1863
+ run_id: str,
1864
+ artifact: RunArtifact | None = None,
1865
+ ) -> list:
1866
+ clear_requests(project_root)
1867
+ requests, suppressions, warnings = create_requests(
1868
+ project_root,
1869
+ findings,
1870
+ run_id=run_id,
1871
+ return_diagnostics=True,
1872
+ )
1873
+ _report_framework_suppressions(suppressions, artifact=artifact)
1874
+ _report_framework_warnings(warnings, artifact=artifact)
1875
+ return requests
1876
+
1877
+
1878
+ def _report_framework_suppressions(
1879
+ suppressions: list[RequestSuppression], *, artifact: RunArtifact | None = None
1880
+ ) -> None:
1881
+ if not suppressions:
1882
+ return
1883
+ console.print(
1884
+ f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) due to framework invariants."
1885
+ )
1886
+ for item in suppressions:
1887
+ console.print(
1888
+ f"[yellow]-[/yellow] {item.location}: {item.message} "
1889
+ f"[dim](framework={item.framework or 'unknown'}, rule={item.rule_id or 'unspecified'})[/dim]"
1890
+ )
1891
+ if artifact is not None:
1892
+ artifact.notes.append(item.note())
1893
+
1894
+
1895
+ def _report_framework_warnings(
1896
+ warnings: list[RequestWarning], *, artifact: RunArtifact | None = None
1897
+ ) -> None:
1898
+ if not warnings:
1899
+ return
1900
+ console.print(
1901
+ f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with framework caution."
1902
+ )
1903
+ for item in warnings:
1904
+ console.print(
1905
+ f"[yellow]-[/yellow] {item.location}: {item.message} "
1906
+ f"[dim](framework={item.framework or 'unknown'}, rule={item.rule_id or 'unspecified'})[/dim]"
1907
+ )
1908
+ if artifact is not None:
1909
+ artifact.notes.append(item.note())
1910
+
1911
+
1715
1912
  def apply(
1716
1913
  force: bool = typer.Option(False, "--force", help="Apply even if the quality gate blocks."),
1717
1914
  ) -> None:
@@ -2113,6 +2310,7 @@ def doctor(
2113
2310
  )
2114
2311
 
2115
2312
  _report_intelligence(project_root)
2313
+ _report_structural_readiness(project_root, project_config)
2116
2314
 
2117
2315
  if skip_ping:
2118
2316
  console.print("[dim]Skipping provider live ping (--skip-ping).[/dim]")
@@ -2246,6 +2444,39 @@ def _report_sandbox(project_root: Path, config) -> None:
2246
2444
  console.print(f" [dim]latest command results: {len(latest.get('results', []))} (failures={failures})[/dim]")
2247
2445
 
2248
2446
 
2447
+ def _report_structural_readiness(project_root: Path, config) -> None:
2448
+ settings = dict(getattr(config, "settings", {}) or {})
2449
+ refactor_cfg = settings.get("refactor", {})
2450
+ if not isinstance(refactor_cfg, dict):
2451
+ refactor_cfg = {}
2452
+ structural = refactor_cfg.get("structural", {})
2453
+ if not isinstance(structural, dict):
2454
+ structural = {}
2455
+ eq_mode = str(structural.get("equivalence_mode", "strict") or "strict")
2456
+ rr_policy = str(structural.get("on_rr_failure", "stop") or "stop")
2457
+ rewrite_mode = str(structural.get("reference_rewrite", "auto") or "auto")
2458
+ langs = structural.get("languages", [])
2459
+ rollout = str(structural.get("rollout", "default") or "default")
2460
+ canary_percent = int(structural.get("canary_percent", 20) or 20)
2461
+ if not isinstance(langs, list):
2462
+ langs = []
2463
+ console.print(
2464
+ f"[bold]structural mode[/bold]: equivalence={eq_mode} on_rr_failure={rr_policy} "
2465
+ f"rollout={rollout} canary_percent={canary_percent}"
2466
+ )
2467
+ console.print(f"[bold]reference rewrite[/bold]: {rewrite_mode} [{', '.join(str(x) for x in langs) or 'defaults'}]")
2468
+ matrix = structural_compatibility_matrix()
2469
+ enabled_langs = [name for name, ok in matrix.items() if ok]
2470
+ disabled_langs = [name for name, ok in matrix.items() if not ok]
2471
+ console.print(
2472
+ f"[bold]adapter matrix[/bold]: enabled={', '.join(enabled_langs) or '-'}; "
2473
+ f"degraded={', '.join(disabled_langs) or '-'}"
2474
+ )
2475
+ rrs = list_requests(project_root)
2476
+ deferred = sum(1 for rr in rrs if rr.status == DEFERRED)
2477
+ console.print(f"[bold]deferred requests[/bold]: {deferred}")
2478
+
2479
+
2249
2480
  def _maybe_build_graph(project_root: Path, config) -> None:
2250
2481
  """Build + persist the symbol graph when intelligence.graph is enabled."""
2251
2482
  settings = dict(getattr(config, "settings", {}) or {})
@@ -2275,20 +2506,22 @@ def _print_rr_summary(artifact) -> None:
2275
2506
  return
2276
2507
  table = Table(title="Refactor request outcomes", show_lines=False)
2277
2508
  table.add_column("issue")
2509
+ table.add_column("operation", no_wrap=True)
2278
2510
  table.add_column("outcome", no_wrap=True)
2279
2511
  table.add_column("attempts", no_wrap=True)
2280
- color = {"applied": "green", "denied": "yellow", "failed": "red"}
2512
+ color = {"applied": "green", "denied": "yellow", "failed": "red", "deferred": "yellow"}
2281
2513
  for o in outcomes:
2282
2514
  outcome = str(o.get("outcome", "?"))
2283
2515
  table.add_row(
2284
2516
  str(o.get("location", "?")),
2517
+ str(o.get("operation_type", "content_patch")),
2285
2518
  f"[{color.get(outcome, 'white')}]{outcome}[/{color.get(outcome, 'white')}]",
2286
2519
  str(o.get("attempts", 0)),
2287
2520
  )
2288
2521
  console.print(table)
2289
2522
  console.print(
2290
2523
  f"[dim]applied={trace.get('applied', 0)} denied={trace.get('denied', 0)} "
2291
- f"failed={trace.get('failed', 0)}[/dim]"
2524
+ f"failed={trace.get('failed', 0)} deferred={trace.get('deferred', 0)}[/dim]"
2292
2525
  )
2293
2526
 
2294
2527
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import typer
6
6
  from rich.console import Console
7
+ from rich.progress import BarColumn, DownloadColumn, Progress, TaskID, TextColumn, TimeRemainingColumn, TransferSpeedColumn
7
8
 
8
9
  from refactorai_cli.auth import AuthError, ensure_authenticated
9
10
  from refactorai_cli.control_plane import ensure_lease, heartbeat, read_cached_lease, resolve_policy
@@ -62,7 +63,23 @@ def update(
62
63
  f"[bold]resolved manifest[/bold]: version={manifest.runtime_version} "
63
64
  f"channel={channel}"
64
65
  )
65
- artifact = download_artifact(manifest.artifact_url)
66
+ progress_columns = (
67
+ TextColumn("[progress.description]{task.description}"),
68
+ BarColumn(),
69
+ DownloadColumn(),
70
+ TransferSpeedColumn(),
71
+ TimeRemainingColumn(),
72
+ )
73
+ with Progress(*progress_columns, console=console, transient=True) as progress:
74
+ task_id: TaskID = progress.add_task("Downloading runtime artifact", total=None)
75
+
76
+ def _on_progress(downloaded: int, total: int | None) -> None:
77
+ update_kwargs = {"completed": int(max(downloaded, 0))}
78
+ if total is not None and total > 0:
79
+ update_kwargs["total"] = total
80
+ progress.update(task_id, **update_kwargs)
81
+
82
+ artifact = download_artifact(manifest.artifact_url, progress_cb=_on_progress)
66
83
  console.print(f"[bold]downloaded artifact[/bold]: {len(artifact)} bytes")
67
84
  if dry_run:
68
85
  # Verification happens in activate_runtime; dry-run still checks
@@ -14,6 +14,7 @@ import platform
14
14
  from dataclasses import dataclass
15
15
  from datetime import datetime, timezone
16
16
  from pathlib import Path
17
+ from typing import Callable
17
18
  from urllib.parse import urlparse
18
19
 
19
20
  import httpx
@@ -153,17 +154,49 @@ def resolve_runtime_manifest(*, channel: str = "stable", timeout: float = 20.0)
153
154
  return manifest
154
155
 
155
156
 
156
- def download_artifact(url: str, *, timeout: float = 60.0) -> bytes:
157
+ def download_artifact(
158
+ url: str,
159
+ *,
160
+ timeout: float = 60.0,
161
+ progress_cb: Callable[[int, int | None], None] | None = None,
162
+ ) -> bytes:
157
163
  headers: dict[str, str] = {}
158
164
  if _should_send_runtime_auth(url):
159
165
  headers["Authorization"] = _runtime_auth_header()
166
+ resolved_timeout = max(float(timeout), 300.0)
167
+ if progress_cb is None:
168
+ try:
169
+ response = httpx.get(url, headers=headers, timeout=resolved_timeout)
170
+ except httpx.HTTPError as exc:
171
+ raise RuntimeError(f"Could not download runtime artifact: {exc}") from exc
172
+ if response.status_code >= 400:
173
+ raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
174
+ return response.content
160
175
  try:
161
- response = httpx.get(url, headers=headers, timeout=max(float(timeout), 300.0))
176
+ with httpx.stream("GET", url, headers=headers, timeout=resolved_timeout) as response:
177
+ if response.status_code >= 400:
178
+ raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
179
+ total: int | None = None
180
+ total_header = str(response.headers.get("Content-Length") or "").strip()
181
+ if total_header:
182
+ try:
183
+ parsed_total = int(total_header)
184
+ except ValueError:
185
+ parsed_total = -1
186
+ if parsed_total > 0:
187
+ total = parsed_total
188
+ downloaded = 0
189
+ chunks: list[bytes] = []
190
+ progress_cb(downloaded, total)
191
+ for chunk in response.iter_bytes(chunk_size=64 * 1024):
192
+ if not chunk:
193
+ continue
194
+ chunks.append(chunk)
195
+ downloaded += len(chunk)
196
+ progress_cb(downloaded, total)
162
197
  except httpx.HTTPError as exc:
163
198
  raise RuntimeError(f"Could not download runtime artifact: {exc}") from exc
164
- if response.status_code >= 400:
165
- raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
166
- return response.content
199
+ return b"".join(chunks)
167
200
 
168
201
 
169
202
  def _should_send_runtime_auth(url: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown