cloudwright-ai-cli 1.5.0__tar.gz → 1.6.0__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.
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/PKG-INFO +1 -1
- cloudwright_ai_cli-1.6.0/cloudwright_cli/__init__.py +1 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/compliance_cmd.py +50 -1
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/cost.py +82 -5
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/drift_cmd.py +74 -1
- cloudwright_ai_cli-1.6.0/cloudwright_cli/commands/review_cmd.py +78 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/main.py +2 -0
- cloudwright_ai_cli-1.5.0/cloudwright_cli/__init__.py +0 -1
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/.gitignore +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/README.md +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/adr.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_session.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_streaming.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_ui.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/compare.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/databricks_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/design.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/diff.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/export.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_live_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/init_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/mcp_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/plan_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/policy.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/schema_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/score_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/security_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/validate.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/completions.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/decorators.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/output.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/project.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/utils.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/pyproject.toml +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_commands.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_debug.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_persistence.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_streaming.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_cli.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/tests/test_modify_cmd.py +0 -0
- {cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/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.
|
|
3
|
+
Version: 1.6.0
|
|
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.0"
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/compliance_cmd.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"${
|
|
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
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/analyze_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/catalog_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_session.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_streaming.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/databricks_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_live_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/modify_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/refresh_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/schema_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.5.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/security_cmd.py
RENAMED
|
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
|