cloudwright-ai-cli 1.5.0__tar.gz → 1.6.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 (54) hide show
  1. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/PKG-INFO +1 -1
  2. cloudwright_ai_cli-1.6.1/cloudwright_cli/__init__.py +1 -0
  3. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/compliance_cmd.py +50 -1
  4. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/cost.py +82 -5
  5. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/drift_cmd.py +74 -1
  6. cloudwright_ai_cli-1.6.1/cloudwright_cli/commands/review_cmd.py +78 -0
  7. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/main.py +2 -0
  8. cloudwright_ai_cli-1.5.0/cloudwright_cli/__init__.py +0 -1
  9. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/.gitignore +0 -0
  10. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/README.md +0 -0
  11. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/__main__.py +0 -0
  12. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/__init__.py +0 -0
  13. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/adr.py +0 -0
  14. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/analyze_cmd.py +0 -0
  15. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/catalog_cmd.py +0 -0
  16. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/chat.py +0 -0
  17. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/chat_session.py +0 -0
  18. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/chat_streaming.py +0 -0
  19. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/chat_ui.py +0 -0
  20. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/compare.py +0 -0
  21. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/databricks_cmd.py +0 -0
  22. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/design.py +0 -0
  23. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/diff.py +0 -0
  24. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/export.py +0 -0
  25. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/import_cmd.py +0 -0
  26. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/import_live_cmd.py +0 -0
  27. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/init_cmd.py +0 -0
  28. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/lint_cmd.py +0 -0
  29. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/mcp_cmd.py +0 -0
  30. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/modify_cmd.py +0 -0
  31. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/plan_cmd.py +0 -0
  32. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/policy.py +0 -0
  33. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/refresh_cmd.py +0 -0
  34. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/schema_cmd.py +0 -0
  35. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/score_cmd.py +0 -0
  36. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/security_cmd.py +0 -0
  37. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/commands/validate.py +0 -0
  38. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/completions.py +0 -0
  39. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/decorators.py +0 -0
  40. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/output.py +0 -0
  41. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/project.py +0 -0
  42. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/py.typed +0 -0
  43. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/cloudwright_cli/utils.py +0 -0
  44. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/pyproject.toml +0 -0
  45. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/__init__.py +0 -0
  46. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_chat_commands.py +0 -0
  47. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_chat_debug.py +0 -0
  48. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_chat_persistence.py +0 -0
  49. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_chat_streaming.py +0 -0
  50. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_cli.py +0 -0
  51. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_drift_cmd.py +0 -0
  52. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_init.py +0 -0
  53. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_modify_cmd.py +0 -0
  54. {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.1}/tests/test_project.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudwright-ai-cli
3
- Version: 1.5.0
3
+ Version: 1.6.1
4
4
  Summary: CLI for Cloudwright architecture intelligence
5
5
  Project-URL: Homepage, https://github.com/xmpuspus/cloudwright
6
6
  Project-URL: Repository, https://github.com/xmpuspus/cloudwright
@@ -0,0 +1 @@
1
+ __version__ = "1.6.1"
@@ -28,12 +28,24 @@ def compliance_scan(
28
28
  ] = None,
29
29
  fail_on: Annotated[str, typer.Option("--fail-on", help="Fail on: critical, high, medium, none")] = "high",
30
30
  output: Annotated[str | None, typer.Option("--output", "-o", help="Write a markdown control report")] = None,
31
+ oscal: Annotated[
32
+ bool,
33
+ typer.Option("--oscal", "-O", help="Emit OSCAL 1.1.2 component-definition JSON alongside the scan"),
34
+ ] = False,
35
+ traceability: Annotated[
36
+ bool,
37
+ typer.Option("--traceability", help="Show the control traceability chain: component -> resource -> control"),
38
+ ] = False,
31
39
  ) -> None:
32
40
  """Scan an ArchSpec and map every finding to framework control IDs.
33
41
 
34
42
  Maps design-stage findings to HIPAA / SOC 2 / PCI-DSS / FedRAMP / GDPR /
35
43
  ISO 27001 / NIST 800-53 controls before any infrastructure exists. Folds in
36
44
  a Checkov deep scan against the exported Terraform when Checkov is on PATH.
45
+
46
+ Use --oscal / -O to also emit an OSCAL 1.1.2 component-definition document.
47
+ When --output is set the OSCAL JSON is written to <output>.oscal.json;
48
+ otherwise it is printed to stdout after the standard report.
37
49
  """
38
50
  try:
39
51
  from cloudwright import ArchSpec
@@ -46,12 +58,34 @@ def compliance_scan(
46
58
  if output:
47
59
  Path(output).write_text(render_markdown(spec, report))
48
60
 
61
+ if oscal:
62
+ import json as _json
63
+
64
+ from cloudwright.oscal import to_oscal
65
+
66
+ scanner = ComplianceScanner()
67
+ resolved_fws = scanner.resolve_frameworks(spec, fw_list or None)
68
+ oscal_doc = to_oscal(spec, report, resolved_fws)
69
+ oscal_text = _json.dumps(oscal_doc, indent=2)
70
+ if output:
71
+ oscal_path = str(output) + ".oscal.json"
72
+ Path(oscal_path).write_text(oscal_text)
73
+ if not is_json_mode(ctx):
74
+ console.print(f"OSCAL document written to {oscal_path}", style="dim")
75
+ else:
76
+ print(oscal_text)
77
+
49
78
  if is_json_mode(ctx):
50
79
  if should_stream(ctx):
51
80
  for f in report.findings:
52
81
  emit_stream(f.as_dict())
53
82
  else:
54
- emit_success(ctx, report.as_dict())
83
+ payload = report.as_dict()
84
+ if traceability:
85
+ from cloudwright.compliance import build_traceability
86
+
87
+ payload["traceability"] = build_traceability(spec, report)
88
+ emit_success(ctx, payload)
55
89
  _maybe_exit(report, fail_on)
56
90
  return
57
91
 
@@ -79,6 +113,21 @@ def compliance_scan(
79
113
  console.print(f" Remediation: {f.remediation}", style="dim")
80
114
  console.print()
81
115
 
116
+ if traceability and report.findings:
117
+ from cloudwright.compliance import build_traceability
118
+
119
+ chain_table = Table("Component", "Resource", "Controls", "Status", title="Control Traceability")
120
+ for row in build_traceability(spec, report):
121
+ ctrls = ", ".join(f"{c['framework']} {c['control_id']}" for c in row["controls"]) or "-"
122
+ chain_table.add_row(
123
+ f"{row['label'] or row['component_id'] or '-'} ({row['service'] or '-'})",
124
+ row["resource_type"] or "-",
125
+ ctrls,
126
+ f"[red]{row['status']}[/red]",
127
+ )
128
+ console.print(chain_table)
129
+ console.print()
130
+
82
131
  crit = report.critical_count
83
132
  high = report.high_count
84
133
  console.print(f"{len(report.findings)} finding(s) ({crit} critical, {high} high)")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from pathlib import Path
4
5
  from typing import Annotated
5
6
 
@@ -30,6 +31,18 @@ def cost(
30
31
  "Sets realistic defaults for request volumes, storage, node counts, and data transfer.",
31
32
  ),
32
33
  ] = None,
34
+ carbon: Annotated[
35
+ bool,
36
+ typer.Option("--carbon", help="Include a CO2e carbon footprint estimate."),
37
+ ] = False,
38
+ focus: Annotated[
39
+ bool,
40
+ typer.Option("--focus", help="Output cost data as FinOps FOCUS 1.0 CSV."),
41
+ ] = False,
42
+ output: Annotated[
43
+ Path | None,
44
+ typer.Option("-o", "--output", help="Write FOCUS CSV to this file instead of stdout."),
45
+ ] = None,
33
46
  ) -> None:
34
47
  """Show cost breakdown for an architecture spec."""
35
48
  if workload_profile:
@@ -43,14 +56,32 @@ def cost(
43
56
  raise typer.Exit(1)
44
57
 
45
58
  spec = ArchSpec.from_file(spec_file)
59
+ tier = pricing_tier or "on_demand"
46
60
 
47
61
  # Compute cost estimate if not present
48
62
  if not spec.cost_estimate:
49
63
  engine = CostEngine()
50
- spec.cost_estimate = engine.estimate(spec, workload_profile=workload_profile)
64
+ spec.cost_estimate = engine.estimate(spec, pricing_tier=tier, workload_profile=workload_profile)
65
+
66
+ # --focus: emit FOCUS CSV and exit
67
+ if focus:
68
+ from cloudwright.focus import to_focus_csv
69
+
70
+ csv_text = to_focus_csv(spec.cost_estimate, pricing_tier=tier)
71
+ if output:
72
+ output.write_text(csv_text, encoding="utf-8")
73
+ console.print(f"FOCUS CSV written to {output}")
74
+ else:
75
+ sys.stdout.write(csv_text)
76
+ return
51
77
 
52
78
  if is_json_mode(ctx):
53
- emit_success(ctx, {"estimate": spec.cost_estimate.model_dump(exclude_none=True)})
79
+ payload: dict = {"estimate": spec.cost_estimate.model_dump(exclude_none=True)}
80
+ if carbon:
81
+ from cloudwright.carbon import estimate_carbon
82
+
83
+ payload["carbon"] = estimate_carbon(spec)
84
+ emit_success(ctx, payload)
54
85
  return
55
86
 
56
87
  if compare:
@@ -59,31 +90,51 @@ def cost(
59
90
  else:
60
91
  _print_single_cost_table(spec)
61
92
 
93
+ if carbon:
94
+ from cloudwright.carbon import estimate_carbon
95
+
96
+ _print_carbon_table(spec, estimate_carbon(spec))
97
+
62
98
 
63
99
  def _print_single_cost_table(spec: ArchSpec) -> None:
64
100
  if not spec.cost_estimate:
65
101
  console.print("[yellow]No cost estimate in spec. Run 'cloudwright design' to generate one.[/yellow]")
66
102
  return
67
103
 
68
- table = Table(title=f"Cost Breakdown — {spec.name}", show_footer=True)
104
+ est = spec.cost_estimate
105
+ region_note = ""
106
+ if est.region_multiplier != 1.0:
107
+ region_note = f" (region multiplier: {est.region_multiplier:.2f}x vs us-east-1)"
108
+
109
+ title = f"Cost Breakdown — {spec.name}{region_note}"
110
+ table = Table(title=title, show_footer=True)
69
111
  table.add_column("Component", style="cyan")
70
112
  table.add_column("Service")
71
- table.add_column("Monthly", justify="right", footer=f"${spec.cost_estimate.monthly_total:,.2f}")
113
+ table.add_column("Monthly", justify="right", footer=f"${est.monthly_total:,.2f}")
114
+ table.add_column("Confidence", justify="center")
72
115
  table.add_column("Notes", style="dim")
73
116
 
74
117
  comp_map = {c.id: c for c in spec.components}
75
- for item in spec.cost_estimate.breakdown:
118
+ for item in est.breakdown:
76
119
  comp = comp_map.get(item.component_id)
77
120
  svc_label = comp.service if comp else item.service
121
+ conf_style = "green" if item.confidence == "high" else "yellow"
78
122
  table.add_row(
79
123
  item.component_id,
80
124
  svc_label,
81
125
  f"${item.monthly:,.2f}",
126
+ f"[{conf_style}]{item.confidence}[/{conf_style}]",
82
127
  item.notes,
83
128
  )
84
129
 
85
130
  console.print(table)
86
131
 
132
+ if est.pricing_confidence != "high":
133
+ console.print(
134
+ "[yellow]Pricing confidence: low[/yellow] — one or more services used formula/fallback "
135
+ "pricing, not real catalog data. Treat totals as rough estimates."
136
+ )
137
+
87
138
 
88
139
  def _print_multi_cloud_table(spec: ArchSpec, providers: list[str]) -> None:
89
140
  all_providers = [spec.provider] + [p for p in providers if p != spec.provider]
@@ -129,3 +180,29 @@ def _print_multi_cloud_table(spec: ArchSpec, providers: list[str]) -> None:
129
180
  table.add_row("[bold]TOTAL[/bold]", *totals)
130
181
 
131
182
  console.print(table)
183
+
184
+
185
+ def _print_carbon_table(spec: ArchSpec, carbon: dict) -> None:
186
+ table = Table(title=f"Carbon Estimate — {spec.name} ({carbon['region']})")
187
+ table.add_column("Component", style="cyan")
188
+ table.add_column("Service")
189
+ table.add_column("Watts", justify="right")
190
+ table.add_column("kWh/mo", justify="right")
191
+ table.add_column("kgCO2e/mo", justify="right")
192
+
193
+ for item in carbon["breakdown"]:
194
+ table.add_row(
195
+ item["component_id"],
196
+ item["service"],
197
+ f"{item['watts']:.1f}",
198
+ f"{item['kwh_per_month']:.2f}",
199
+ f"{item['kg_co2e_per_month']:.3f}",
200
+ )
201
+
202
+ console.print(table)
203
+ console.print(
204
+ f"[bold]Total:[/bold] {carbon['total_kg_co2e_per_month']:.3f} kgCO2e/month "
205
+ f"[dim](grid: {carbon['grid_intensity_g_per_kwh']} gCO2e/kWh, "
206
+ f"PUE: {carbon['assumptions']['pue']})[/dim]"
207
+ )
208
+ console.print(f"[dim]{carbon['assumptions']['disclaimer']}[/dim]")
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import Annotated
6
+ from typing import Annotated, Any
7
7
 
8
8
  import typer
9
9
  from rich.console import Console
@@ -24,6 +24,17 @@ def drift(
24
24
  fmt: Annotated[
25
25
  str, typer.Option("--format", "-f", help="Infrastructure format: auto, terraform, cloudformation")
26
26
  ] = "auto",
27
+ remediate: Annotated[
28
+ bool,
29
+ typer.Option(
30
+ "--remediate",
31
+ help="Show cost/quality delta and terraform plan preview to close drift (read-only).",
32
+ ),
33
+ ] = False,
34
+ run_plan: Annotated[
35
+ bool,
36
+ typer.Option("--run-plan", help="With --remediate: attempt a real terraform plan (needs credentials)."),
37
+ ] = False,
27
38
  ) -> None:
28
39
  """Compare design spec against deployed infrastructure to detect drift."""
29
40
  try:
@@ -39,6 +50,10 @@ def drift(
39
50
  with console.status("Detecting drift..."):
40
51
  report = detect_drift(spec_file, infra_file, infra_format=fmt)
41
52
 
53
+ if remediate:
54
+ _run_remediate(ctx, report, run_plan=run_plan)
55
+ return
56
+
42
57
  if is_json_mode(ctx):
43
58
  result = {
44
59
  "drift_score": report.drift_score,
@@ -94,3 +109,61 @@ def drift(
94
109
  raise
95
110
  except Exception as e:
96
111
  handle_error(ctx, e)
112
+
113
+
114
+ def _run_remediate(ctx: typer.Context, report: Any, *, run_plan: bool) -> None:
115
+ from cloudwright.remediation import remediate as compute_remediation
116
+
117
+ with console.status("Computing remediation plan..."):
118
+ result = compute_remediation(report.deployed_spec, report.design_spec, run_plan=run_plan)
119
+
120
+ if is_json_mode(ctx):
121
+ emit_success(ctx, {"remediation": result})
122
+ return
123
+
124
+ console.print(Rule("[bold]Cloudwright Remediation Preview[/bold]"))
125
+ console.print(Panel(result["summary"], title="Summary"))
126
+
127
+ drift_items = result["drift"]
128
+ if drift_items:
129
+ table = Table(title=f"Changes to close drift ({len(drift_items)})")
130
+ table.add_column("Action", style="cyan")
131
+ table.add_column("Component")
132
+ table.add_column("Detail")
133
+ for item in drift_items:
134
+ action = item["change"]
135
+ cid = item.get("id", "")
136
+ detail = ""
137
+ if action == "modify":
138
+ detail = f"{item.get('field')}: {item.get('from')} -> {item.get('to')}"
139
+ elif action == "add":
140
+ detail = item.get("service", "")
141
+ elif action == "remove":
142
+ detail = item.get("service", "")
143
+ color = {"add": "green", "remove": "red", "modify": "yellow"}.get(action, "white")
144
+ table.add_row(f"[{color}]{action}[/{color}]", cid, detail)
145
+ console.print()
146
+ console.print(table)
147
+ else:
148
+ console.print("[green]No drift changes required.[/green]")
149
+
150
+ cd = result["cost_delta"]
151
+ sign = "+" if cd["delta"] >= 0 else ""
152
+ console.print(
153
+ f"\n[bold]Cost delta:[/bold] {sign}${cd['delta']:,.2f}/mo (${cd['current']:,.2f} -> ${cd['desired']:,.2f})"
154
+ )
155
+
156
+ qd = result["quality_delta"]
157
+ q_sign = "+" if qd["delta"] >= 0 else ""
158
+ q_color = "green" if qd["delta"] >= 0 else "red"
159
+ console.print(
160
+ f"[bold]Quality delta:[/bold] [{q_color}]{q_sign}{qd['delta']:.1f} pts[/{q_color}] "
161
+ f"(grade {qd['current_grade']} -> {qd['desired_grade']})"
162
+ )
163
+
164
+ plan = result["plan"]
165
+ plan_color = "green" if plan.get("ok") else "red"
166
+ plan_label = "valid" if plan.get("validated") else "not validated"
167
+ console.print(f"[bold]Plan ([/bold]{plan['tool']}[bold]):[/bold] [{plan_color}]{plan_label}[/{plan_color}]")
168
+ for msg in plan.get("messages", []):
169
+ console.print(f" [dim]{msg}[/dim]")
@@ -0,0 +1,78 @@
1
+ """Review an architecture with the deterministic critics (offline)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from cloudwright_cli.output import emit_success, is_json_mode
13
+ from cloudwright_cli.utils import handle_error
14
+
15
+ console = Console()
16
+
17
+ _SEV_COLOR = {"critical": "red", "high": "red", "medium": "yellow", "low": "cyan"}
18
+
19
+
20
+ def review(
21
+ ctx: typer.Context,
22
+ spec_file: Annotated[str, typer.Argument(help="Path to ArchSpec YAML/JSON file")],
23
+ compliance: Annotated[str, typer.Option("--compliance", help="Comma-separated frameworks, e.g. hipaa,soc2")] = "",
24
+ well_architected: Annotated[
25
+ bool, typer.Option("--well-architected", help="Include Well-Architected checks")
26
+ ] = False,
27
+ ) -> None:
28
+ """Review an architecture: unified scorer + linter + validator findings (no LLM, free)."""
29
+ try:
30
+ from cloudwright import ArchSpec
31
+ from cloudwright.critique import critique
32
+
33
+ spec = ArchSpec.from_file(spec_file)
34
+ frameworks = [f.strip() for f in compliance.split(",") if f.strip()] or None
35
+ report = critique(spec, compliance=frameworks, well_architected=well_architected)
36
+
37
+ if is_json_mode(ctx):
38
+ emit_success(ctx, {"review": report.as_dict()})
39
+ return
40
+
41
+ border = "green" if not report.blocking else "red"
42
+ console.print(
43
+ Panel(
44
+ f"[bold]{report.summary_line()}[/bold]",
45
+ title=f"Architecture Review: {spec.name}",
46
+ border_style=border,
47
+ )
48
+ )
49
+
50
+ if not report.findings:
51
+ console.print("[green]No findings. This architecture passes every critic.[/green]")
52
+ return
53
+
54
+ table = Table(title="Findings (severity-ranked)")
55
+ table.add_column("Severity")
56
+ table.add_column("Source", style="dim")
57
+ table.add_column("Finding")
58
+ table.add_column("Fix")
59
+ for f in report.findings:
60
+ color = _SEV_COLOR.get(f.severity, "white")
61
+ table.add_row(
62
+ f"[{color}]{f.severity}[/{color}]",
63
+ f.source,
64
+ f.message,
65
+ (f.recommendation or "")[:80],
66
+ )
67
+ console.print(table)
68
+
69
+ if report.blocking:
70
+ console.print(
71
+ f"\n[red]{len(report.blocking)} blocking finding(s).[/red] "
72
+ "Run `cloudwright design` (repair is on by default) or fix these before deploy."
73
+ )
74
+
75
+ except typer.Exit:
76
+ raise
77
+ except Exception as e:
78
+ handle_error(ctx, e)
@@ -22,6 +22,7 @@ from cloudwright_cli.commands.modify_cmd import modify
22
22
  from cloudwright_cli.commands.plan_cmd import plan
23
23
  from cloudwright_cli.commands.policy import policy
24
24
  from cloudwright_cli.commands.refresh_cmd import refresh
25
+ from cloudwright_cli.commands.review_cmd import review
25
26
  from cloudwright_cli.commands.schema_cmd import schema
26
27
  from cloudwright_cli.commands.score_cmd import score
27
28
  from cloudwright_cli.commands.security_cmd import security_scan
@@ -77,6 +78,7 @@ app.command()(init)
77
78
  app.command()(plan)
78
79
  app.command()(policy)
79
80
  app.command()(score)
81
+ app.command()(review)
80
82
  app.command()(analyze)
81
83
  app.command()(refresh)
82
84
  app.command()(lint)
@@ -1 +0,0 @@
1
- __version__ = "1.5.0"