cloudwright-ai-cli 1.4.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.4.0 → cloudwright_ai_cli-1.6.0}/.gitignore +2 -0
- {cloudwright_ai_cli-1.4.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.6.0/cloudwright_cli/commands/compliance_cmd.py +153 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/cost.py +82 -5
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/drift_cmd.py +74 -1
- cloudwright_ai_cli-1.6.0/cloudwright_cli/commands/import_live_cmd.py +172 -0
- cloudwright_ai_cli-1.6.0/cloudwright_cli/commands/plan_cmd.py +75 -0
- cloudwright_ai_cli-1.6.0/cloudwright_cli/commands/review_cmd.py +78 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/main.py +6 -0
- cloudwright_ai_cli-1.4.0/cloudwright_cli/__init__.py +0 -1
- cloudwright_ai_cli-1.4.0/cloudwright_cli/commands/import_live_cmd.py +0 -136
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/README.md +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/adr.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_session.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_streaming.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_ui.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/compare.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/databricks_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/design.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/diff.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/export.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/init_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/mcp_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/policy.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/schema_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/score_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/security_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/validate.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/completions.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/decorators.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/output.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/project.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/utils.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/pyproject.toml +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_commands.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_debug.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_persistence.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_chat_streaming.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_cli.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/tests/test_modify_cmd.py +0 -0
- {cloudwright_ai_cli-1.4.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"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from cloudwright_cli.output import emit_stream, emit_success, is_json_mode, should_stream
|
|
11
|
+
from cloudwright_cli.utils import handle_error
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "none": 4}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compliance_scan(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
spec_file: Annotated[Path, typer.Argument(help="Path to ArchSpec YAML file", exists=True)],
|
|
21
|
+
frameworks: Annotated[
|
|
22
|
+
str | None,
|
|
23
|
+
typer.Option("--frameworks", "-f", help="Comma list: hipaa,soc2,pci-dss,fedramp,gdpr,iso27001,nist"),
|
|
24
|
+
] = None,
|
|
25
|
+
checkov: Annotated[
|
|
26
|
+
bool | None,
|
|
27
|
+
typer.Option("--checkov/--no-checkov", help="Force or skip the Checkov deep scan (auto-detected by default)"),
|
|
28
|
+
] = None,
|
|
29
|
+
fail_on: Annotated[str, typer.Option("--fail-on", help="Fail on: critical, high, medium, none")] = "high",
|
|
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,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Scan an ArchSpec and map every finding to framework control IDs.
|
|
41
|
+
|
|
42
|
+
Maps design-stage findings to HIPAA / SOC 2 / PCI-DSS / FedRAMP / GDPR /
|
|
43
|
+
ISO 27001 / NIST 800-53 controls before any infrastructure exists. Folds in
|
|
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.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from cloudwright import ArchSpec
|
|
52
|
+
from cloudwright.compliance import ComplianceScanner, render_markdown
|
|
53
|
+
|
|
54
|
+
spec = ArchSpec.from_file(spec_file)
|
|
55
|
+
fw_list = [f for f in (frameworks.split(",") if frameworks else []) if f.strip()]
|
|
56
|
+
report = ComplianceScanner().scan(spec, frameworks=fw_list or None, run_checkov=checkov)
|
|
57
|
+
|
|
58
|
+
if output:
|
|
59
|
+
Path(output).write_text(render_markdown(spec, report))
|
|
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
|
+
|
|
78
|
+
if is_json_mode(ctx):
|
|
79
|
+
if should_stream(ctx):
|
|
80
|
+
for f in report.findings:
|
|
81
|
+
emit_stream(f.as_dict())
|
|
82
|
+
else:
|
|
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)
|
|
89
|
+
_maybe_exit(report, fail_on)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
console.print(f"\nCompliance Scan: {spec_file.name} (scanner: {report.scanner})\n")
|
|
93
|
+
|
|
94
|
+
posture = Table("Framework", "Satisfied", "Violated controls", "Findings", "Status")
|
|
95
|
+
for s in report.frameworks:
|
|
96
|
+
sat = f"{s.controls_total - len(s.controls_violated)}/{s.controls_total}"
|
|
97
|
+
viol = ", ".join(s.controls_violated) if s.controls_violated else "-"
|
|
98
|
+
style = "green" if s.status == "pass" else "red"
|
|
99
|
+
posture.add_row(s.framework, sat, viol, str(s.findings), f"[{style}]{s.status.upper()}[/{style}]")
|
|
100
|
+
console.print(posture)
|
|
101
|
+
console.print()
|
|
102
|
+
|
|
103
|
+
if not report.findings:
|
|
104
|
+
console.print("[green][PASS][/green] No control violations detected.")
|
|
105
|
+
else:
|
|
106
|
+
for f in report.findings:
|
|
107
|
+
sev = f.severity.upper()
|
|
108
|
+
color = {"CRITICAL": "bold red", "HIGH": "red", "MEDIUM": "yellow"}.get(sev, "dim")
|
|
109
|
+
console.print(f" [{color}][{sev}][/{color}] {f.message} [dim]({f.source})[/dim]")
|
|
110
|
+
if f.controls:
|
|
111
|
+
ctrl = ", ".join(f"{c.framework} {c.control_id}" for c in f.controls)
|
|
112
|
+
console.print(f" Controls: {ctrl}", style="cyan")
|
|
113
|
+
console.print(f" Remediation: {f.remediation}", style="dim")
|
|
114
|
+
console.print()
|
|
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
|
+
|
|
131
|
+
crit = report.critical_count
|
|
132
|
+
high = report.high_count
|
|
133
|
+
console.print(f"{len(report.findings)} finding(s) ({crit} critical, {high} high)")
|
|
134
|
+
threshold = _SEVERITY_ORDER.get(fail_on, 1)
|
|
135
|
+
worst = min((_SEVERITY_ORDER.get(f.severity, 4) for f in report.findings), default=4)
|
|
136
|
+
status = "PASSED" if worst > threshold else "FAILED"
|
|
137
|
+
console.print(f"Status: [{'green' if status == 'PASSED' else 'red'}]{status}[/] (fail-on={fail_on})")
|
|
138
|
+
if output:
|
|
139
|
+
console.print(f"\nReport written to {output}", style="dim")
|
|
140
|
+
|
|
141
|
+
_maybe_exit(report, fail_on)
|
|
142
|
+
|
|
143
|
+
except typer.Exit:
|
|
144
|
+
raise
|
|
145
|
+
except Exception as e:
|
|
146
|
+
handle_error(ctx, e)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _maybe_exit(report, fail_on: str) -> None:
|
|
150
|
+
threshold = _SEVERITY_ORDER.get(fail_on, 1)
|
|
151
|
+
for f in report.findings:
|
|
152
|
+
if _SEVERITY_ORDER.get(f.severity, 4) <= threshold:
|
|
153
|
+
raise typer.Exit(1)
|
|
@@ -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,172 @@
|
|
|
1
|
+
"""Import live cloud infrastructure into an ArchSpec via provider APIs.
|
|
2
|
+
|
|
3
|
+
Supports AWS (boto3), GCP (google-cloud SDKs), and Azure (azure-mgmt SDKs):
|
|
4
|
+
|
|
5
|
+
cloudwright import-live --provider aws --region us-east-1
|
|
6
|
+
cloudwright import-live --provider gcp --project my-project
|
|
7
|
+
cloudwright import-live --provider azure --subscription <SUB_ID>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from cloudwright_cli.output import emit_success, err_console, is_json_mode, validate_output_path
|
|
21
|
+
from cloudwright_cli.utils import handle_error
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def import_live(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
provider: Annotated[
|
|
29
|
+
str,
|
|
30
|
+
typer.Option("--provider", help="Cloud provider to scan: aws | gcp | azure"),
|
|
31
|
+
] = "aws",
|
|
32
|
+
region: Annotated[
|
|
33
|
+
str,
|
|
34
|
+
typer.Option("--region", help="Cloud region (e.g. us-east-1). Recorded in metadata for gcp/azure."),
|
|
35
|
+
] = "us-east-1",
|
|
36
|
+
profile: Annotated[
|
|
37
|
+
str | None,
|
|
38
|
+
typer.Option("--profile", help="AWS named profile (~/.aws/credentials)"),
|
|
39
|
+
] = None,
|
|
40
|
+
project: Annotated[
|
|
41
|
+
str | None,
|
|
42
|
+
typer.Option("--project", help="GCP project ID (or set GOOGLE_CLOUD_PROJECT)"),
|
|
43
|
+
] = None,
|
|
44
|
+
subscription: Annotated[
|
|
45
|
+
str | None,
|
|
46
|
+
typer.Option("--subscription", help="Azure subscription ID (or set AZURE_SUBSCRIPTION_ID)"),
|
|
47
|
+
] = None,
|
|
48
|
+
services: Annotated[
|
|
49
|
+
str | None,
|
|
50
|
+
typer.Option(
|
|
51
|
+
"--services",
|
|
52
|
+
help="Comma-separated subset of services to scan. Default: all for the provider.",
|
|
53
|
+
),
|
|
54
|
+
] = None,
|
|
55
|
+
output: Annotated[
|
|
56
|
+
str | None,
|
|
57
|
+
typer.Option("--output", "-o", help="Write ArchSpec YAML to this file instead of stdout"),
|
|
58
|
+
] = None,
|
|
59
|
+
name: Annotated[
|
|
60
|
+
str | None,
|
|
61
|
+
typer.Option("--name", help="Override the architecture name"),
|
|
62
|
+
] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Walk live cloud APIs and produce an ArchSpec from running infrastructure."""
|
|
65
|
+
try:
|
|
66
|
+
provider_norm = (provider or "aws").lower()
|
|
67
|
+
if provider_norm == "google":
|
|
68
|
+
provider_norm = "gcp"
|
|
69
|
+
if provider_norm not in {"aws", "gcp", "azure"}:
|
|
70
|
+
raise typer.BadParameter(f"Unknown --provider {provider!r}. Supported: aws, gcp, azure.")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
from cloudwright.importer.live_aws import LiveImportError
|
|
74
|
+
except ImportError as exc:
|
|
75
|
+
err_console.print(
|
|
76
|
+
"[red]Live import core is unavailable.[/red] Install with: pip install 'cloudwright-ai[live-import]'"
|
|
77
|
+
)
|
|
78
|
+
raise typer.Exit(code=1) from exc
|
|
79
|
+
|
|
80
|
+
json_mode = is_json_mode(ctx)
|
|
81
|
+
|
|
82
|
+
def _progress(msg: str) -> None:
|
|
83
|
+
if not json_mode:
|
|
84
|
+
err_console.print(msg)
|
|
85
|
+
|
|
86
|
+
services_list: list[str] | None = None
|
|
87
|
+
if services:
|
|
88
|
+
services_list = [s.strip().lower() for s in services.split(",") if s.strip()]
|
|
89
|
+
|
|
90
|
+
scope_label = region
|
|
91
|
+
try:
|
|
92
|
+
if provider_norm == "aws":
|
|
93
|
+
from cloudwright.importer.live_aws import SUPPORTED_SERVICES, import_live_aws
|
|
94
|
+
|
|
95
|
+
_check_services(services_list, SUPPORTED_SERVICES)
|
|
96
|
+
spec = import_live_aws(
|
|
97
|
+
region=region,
|
|
98
|
+
profile=profile,
|
|
99
|
+
services=services_list,
|
|
100
|
+
progress=_progress,
|
|
101
|
+
name=name,
|
|
102
|
+
)
|
|
103
|
+
scope_label = region
|
|
104
|
+
elif provider_norm == "gcp":
|
|
105
|
+
from cloudwright.importer.live_gcp import SUPPORTED_SERVICES, import_live_gcp
|
|
106
|
+
|
|
107
|
+
_check_services(services_list, SUPPORTED_SERVICES)
|
|
108
|
+
spec = import_live_gcp(
|
|
109
|
+
project=project,
|
|
110
|
+
region=region,
|
|
111
|
+
services=services_list,
|
|
112
|
+
progress=_progress,
|
|
113
|
+
name=name,
|
|
114
|
+
)
|
|
115
|
+
scope_label = project or "project"
|
|
116
|
+
else: # azure
|
|
117
|
+
from cloudwright.importer.live_azure import SUPPORTED_SERVICES, import_live_azure
|
|
118
|
+
|
|
119
|
+
_check_services(services_list, SUPPORTED_SERVICES)
|
|
120
|
+
spec = import_live_azure(
|
|
121
|
+
subscription=subscription,
|
|
122
|
+
region=region,
|
|
123
|
+
services=services_list,
|
|
124
|
+
progress=_progress,
|
|
125
|
+
name=name,
|
|
126
|
+
)
|
|
127
|
+
scope_label = subscription or "subscription"
|
|
128
|
+
except LiveImportError as exc:
|
|
129
|
+
err_console.print(f"[red]error:[/red] {exc}")
|
|
130
|
+
raise typer.Exit(code=1) from exc
|
|
131
|
+
|
|
132
|
+
if name:
|
|
133
|
+
spec = spec.model_copy(update={"name": name})
|
|
134
|
+
|
|
135
|
+
if json_mode:
|
|
136
|
+
emit_success(ctx, {"spec": json.loads(spec.to_json())})
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
content = spec.to_yaml()
|
|
140
|
+
n_comps = len(spec.components)
|
|
141
|
+
n_bounds = len(spec.boundaries)
|
|
142
|
+
|
|
143
|
+
if output:
|
|
144
|
+
validate_output_path(output)
|
|
145
|
+
Path(output).write_text(content)
|
|
146
|
+
err_console.print()
|
|
147
|
+
err_console.print(
|
|
148
|
+
f"[green]Imported[/green] {n_comps} component(s), {n_bounds} boundary(ies) "
|
|
149
|
+
f"from {scope_label} -> [bold]{output}[/bold]"
|
|
150
|
+
)
|
|
151
|
+
err_console.print(f"Run [bold]cloudwright cost {output}[/bold] to estimate.")
|
|
152
|
+
else:
|
|
153
|
+
sys.stdout.write(content)
|
|
154
|
+
err_console.print()
|
|
155
|
+
err_console.print(
|
|
156
|
+
f"[green]Imported[/green] {n_comps} component(s), {n_bounds} boundary(ies) from {scope_label}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
except typer.Exit:
|
|
160
|
+
raise
|
|
161
|
+
except typer.BadParameter:
|
|
162
|
+
raise
|
|
163
|
+
except Exception as e:
|
|
164
|
+
handle_error(ctx, e)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_services(services_list: list[str] | None, supported: tuple[str, ...]) -> None:
|
|
168
|
+
if not services_list:
|
|
169
|
+
return
|
|
170
|
+
unknown = [s for s in services_list if s not in supported]
|
|
171
|
+
if unknown:
|
|
172
|
+
raise typer.BadParameter(f"Unknown service(s): {sorted(set(unknown))}. Supported: {list(supported)}")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from cloudwright_cli.output import emit_success, is_json_mode
|
|
10
|
+
from cloudwright_cli.utils import handle_error
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plan(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
spec_file: Annotated[Path, typer.Argument(help="Path to ArchSpec YAML file", exists=True)],
|
|
18
|
+
target: Annotated[
|
|
19
|
+
str,
|
|
20
|
+
typer.Option("--target", "-t", help="terraform | pulumi-python | pulumi-ts"),
|
|
21
|
+
] = "terraform",
|
|
22
|
+
no_plan: Annotated[
|
|
23
|
+
bool,
|
|
24
|
+
typer.Option("--no-plan", help="Validate only; skip the credential-requiring plan/preview step"),
|
|
25
|
+
] = False,
|
|
26
|
+
timeout: Annotated[int, typer.Option("--timeout", help="Seconds before the plan step is aborted")] = 180,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Prove the exported infrastructure is deployable.
|
|
29
|
+
|
|
30
|
+
Runs `terraform validate`/`plan` or `pulumi preview` against the generated
|
|
31
|
+
artifact. Read-only — nothing is applied. `validate` needs no credentials
|
|
32
|
+
and is the offline proof of deployability; `plan` adds a real resource
|
|
33
|
+
diff when cloud credentials are available.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
from cloudwright import ArchSpec
|
|
37
|
+
from cloudwright.planner import plan as run_plan
|
|
38
|
+
|
|
39
|
+
spec = ArchSpec.from_file(spec_file)
|
|
40
|
+
result = run_plan(spec, target=target, run_plan=not no_plan, timeout=timeout)
|
|
41
|
+
|
|
42
|
+
if is_json_mode(ctx):
|
|
43
|
+
emit_success(ctx, result.as_dict())
|
|
44
|
+
if not result.ok:
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
console.print(f"\nPlan ({result.tool}): {spec_file.name}\n")
|
|
49
|
+
if not result.available:
|
|
50
|
+
console.print(f"[yellow][SKIP][/yellow] {result.messages[0]}")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
for msg in result.messages:
|
|
54
|
+
console.print(f" {msg}")
|
|
55
|
+
if result.summary is not None:
|
|
56
|
+
s = result.summary
|
|
57
|
+
console.print(
|
|
58
|
+
f"\n Resource diff: [green]+{s['add']}[/green] "
|
|
59
|
+
f"[yellow]~{s['change']}[/yellow] [red]-{s['destroy']}[/red]"
|
|
60
|
+
)
|
|
61
|
+
if result.output_tail:
|
|
62
|
+
console.print("\n[dim]--- tool output (tail) ---[/dim]")
|
|
63
|
+
console.print(f"[dim]{result.output_tail}[/dim]")
|
|
64
|
+
|
|
65
|
+
verdict = "DEPLOYABLE" if result.ok else "NOT DEPLOYABLE"
|
|
66
|
+
style = "green" if result.ok else "red"
|
|
67
|
+
plan_note = "" if result.plan_ran else " (validate only — no credentials for full plan)"
|
|
68
|
+
console.print(f"\nVerdict: [{style}]{verdict}[/{style}]{plan_note}")
|
|
69
|
+
if not result.ok:
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
|
|
72
|
+
except typer.Exit:
|
|
73
|
+
raise
|
|
74
|
+
except Exception as e:
|
|
75
|
+
handle_error(ctx, e)
|
|
@@ -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)
|
|
@@ -6,6 +6,7 @@ from cloudwright_cli.commands.analyze_cmd import analyze
|
|
|
6
6
|
from cloudwright_cli.commands.catalog_cmd import catalog_app
|
|
7
7
|
from cloudwright_cli.commands.chat import chat
|
|
8
8
|
from cloudwright_cli.commands.compare import compare
|
|
9
|
+
from cloudwright_cli.commands.compliance_cmd import compliance_scan
|
|
9
10
|
from cloudwright_cli.commands.cost import cost
|
|
10
11
|
from cloudwright_cli.commands.databricks_cmd import databricks_validate
|
|
11
12
|
from cloudwright_cli.commands.design import design
|
|
@@ -18,8 +19,10 @@ from cloudwright_cli.commands.init_cmd import init
|
|
|
18
19
|
from cloudwright_cli.commands.lint_cmd import lint
|
|
19
20
|
from cloudwright_cli.commands.mcp_cmd import mcp_serve
|
|
20
21
|
from cloudwright_cli.commands.modify_cmd import modify
|
|
22
|
+
from cloudwright_cli.commands.plan_cmd import plan
|
|
21
23
|
from cloudwright_cli.commands.policy import policy
|
|
22
24
|
from cloudwright_cli.commands.refresh_cmd import refresh
|
|
25
|
+
from cloudwright_cli.commands.review_cmd import review
|
|
23
26
|
from cloudwright_cli.commands.schema_cmd import schema
|
|
24
27
|
from cloudwright_cli.commands.score_cmd import score
|
|
25
28
|
from cloudwright_cli.commands.security_cmd import security_scan
|
|
@@ -72,13 +75,16 @@ app.command(name="import")(import_infra)
|
|
|
72
75
|
app.command(name="import-live")(import_live)
|
|
73
76
|
app.command()(chat)
|
|
74
77
|
app.command()(init)
|
|
78
|
+
app.command()(plan)
|
|
75
79
|
app.command()(policy)
|
|
76
80
|
app.command()(score)
|
|
81
|
+
app.command()(review)
|
|
77
82
|
app.command()(analyze)
|
|
78
83
|
app.command()(refresh)
|
|
79
84
|
app.command()(lint)
|
|
80
85
|
app.command()(databricks_validate)
|
|
81
86
|
app.command(name="security")(security_scan)
|
|
87
|
+
app.command(name="compliance")(compliance_scan)
|
|
82
88
|
app.command(name="adr")(adr)
|
|
83
89
|
app.command()(schema)
|
|
84
90
|
app.command(name="mcp")(mcp_serve)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.4.0"
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"""Import live cloud infrastructure into an ArchSpec via provider APIs.
|
|
2
|
-
|
|
3
|
-
Currently supports AWS via boto3 (``cloudwright import-live --provider aws``).
|
|
4
|
-
GCP and Azure surface a clear ``not yet implemented`` error.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Annotated
|
|
13
|
-
|
|
14
|
-
import typer
|
|
15
|
-
from rich.console import Console
|
|
16
|
-
|
|
17
|
-
from cloudwright_cli.output import emit_success, err_console, is_json_mode, validate_output_path
|
|
18
|
-
from cloudwright_cli.utils import handle_error
|
|
19
|
-
|
|
20
|
-
console = Console()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def import_live(
|
|
24
|
-
ctx: typer.Context,
|
|
25
|
-
provider: Annotated[
|
|
26
|
-
str,
|
|
27
|
-
typer.Option("--provider", help="Cloud provider to scan: aws (gcp, azure not yet implemented)"),
|
|
28
|
-
] = "aws",
|
|
29
|
-
region: Annotated[
|
|
30
|
-
str,
|
|
31
|
-
typer.Option("--region", help="Cloud region (e.g. us-east-1)"),
|
|
32
|
-
] = "us-east-1",
|
|
33
|
-
profile: Annotated[
|
|
34
|
-
str | None,
|
|
35
|
-
typer.Option("--profile", help="AWS named profile (~/.aws/credentials)"),
|
|
36
|
-
] = None,
|
|
37
|
-
services: Annotated[
|
|
38
|
-
str | None,
|
|
39
|
-
typer.Option(
|
|
40
|
-
"--services",
|
|
41
|
-
help="Comma-separated subset of services to scan (e.g. ec2,rds,s3). Default: all.",
|
|
42
|
-
),
|
|
43
|
-
] = None,
|
|
44
|
-
output: Annotated[
|
|
45
|
-
str | None,
|
|
46
|
-
typer.Option("--output", "-o", help="Write ArchSpec YAML to this file instead of stdout"),
|
|
47
|
-
] = None,
|
|
48
|
-
name: Annotated[
|
|
49
|
-
str | None,
|
|
50
|
-
typer.Option("--name", help="Override the architecture name"),
|
|
51
|
-
] = None,
|
|
52
|
-
) -> None:
|
|
53
|
-
"""Walk live AWS APIs and produce an ArchSpec from running infrastructure."""
|
|
54
|
-
try:
|
|
55
|
-
provider_norm = (provider or "aws").lower()
|
|
56
|
-
if provider_norm in {"gcp", "google", "azure"}:
|
|
57
|
-
raise typer.BadParameter(
|
|
58
|
-
f"--provider {provider!r} is not yet implemented. Only --provider aws is supported in v1.4."
|
|
59
|
-
)
|
|
60
|
-
if provider_norm != "aws":
|
|
61
|
-
raise typer.BadParameter(f"Unknown --provider {provider!r}. Supported: aws.")
|
|
62
|
-
|
|
63
|
-
try:
|
|
64
|
-
from cloudwright.importer.live_aws import (
|
|
65
|
-
SUPPORTED_SERVICES,
|
|
66
|
-
LiveImportError,
|
|
67
|
-
import_live_aws,
|
|
68
|
-
)
|
|
69
|
-
except ImportError as exc:
|
|
70
|
-
# boto3 not installed at all (cloudwright.importer.live_aws import boto3 lazily,
|
|
71
|
-
# but a missing core import surfaces here as a final fallback).
|
|
72
|
-
err_console.print(
|
|
73
|
-
"[red]boto3 is required for live AWS import.[/red] "
|
|
74
|
-
"Install with: pip install 'cloudwright-ai[live-import]'"
|
|
75
|
-
)
|
|
76
|
-
raise typer.Exit(code=1) from exc
|
|
77
|
-
|
|
78
|
-
services_list: list[str] | None = None
|
|
79
|
-
if services:
|
|
80
|
-
services_list = [s.strip().lower() for s in services.split(",") if s.strip()]
|
|
81
|
-
unknown = [s for s in services_list if s not in SUPPORTED_SERVICES]
|
|
82
|
-
if unknown:
|
|
83
|
-
raise typer.BadParameter(
|
|
84
|
-
f"Unknown service(s): {sorted(set(unknown))}. Supported: {list(SUPPORTED_SERVICES)}"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
json_mode = is_json_mode(ctx)
|
|
88
|
-
|
|
89
|
-
def _progress(msg: str) -> None:
|
|
90
|
-
if not json_mode:
|
|
91
|
-
err_console.print(msg)
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
spec = import_live_aws(
|
|
95
|
-
region=region,
|
|
96
|
-
profile=profile,
|
|
97
|
-
services=services_list,
|
|
98
|
-
progress=_progress,
|
|
99
|
-
name=name,
|
|
100
|
-
)
|
|
101
|
-
except LiveImportError as exc:
|
|
102
|
-
# Clean error path — credentials missing, profile not found, etc.
|
|
103
|
-
err_console.print(f"[red]error:[/red] {exc}")
|
|
104
|
-
raise typer.Exit(code=1) from exc
|
|
105
|
-
|
|
106
|
-
if name:
|
|
107
|
-
spec = spec.model_copy(update={"name": name})
|
|
108
|
-
|
|
109
|
-
if json_mode:
|
|
110
|
-
emit_success(ctx, {"spec": json.loads(spec.to_json())})
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
content = spec.to_yaml()
|
|
114
|
-
n_comps = len(spec.components)
|
|
115
|
-
n_bounds = len(spec.boundaries)
|
|
116
|
-
|
|
117
|
-
if output:
|
|
118
|
-
validate_output_path(output)
|
|
119
|
-
Path(output).write_text(content)
|
|
120
|
-
err_console.print()
|
|
121
|
-
err_console.print(
|
|
122
|
-
f"[green]Imported[/green] {n_comps} component(s), {n_bounds} boundary(ies) "
|
|
123
|
-
f"from {region} -> [bold]{output}[/bold]"
|
|
124
|
-
)
|
|
125
|
-
err_console.print(f"Run [bold]cloudwright cost {output}[/bold] to estimate.")
|
|
126
|
-
else:
|
|
127
|
-
sys.stdout.write(content)
|
|
128
|
-
err_console.print()
|
|
129
|
-
err_console.print(f"[green]Imported[/green] {n_comps} component(s), {n_bounds} boundary(ies) from {region}")
|
|
130
|
-
|
|
131
|
-
except typer.Exit:
|
|
132
|
-
raise
|
|
133
|
-
except typer.BadParameter:
|
|
134
|
-
raise
|
|
135
|
-
except Exception as e:
|
|
136
|
-
handle_error(ctx, e)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/analyze_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/catalog_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/chat_session.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.4.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.4.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.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/import_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/modify_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/refresh_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-1.4.0 → cloudwright_ai_cli-1.6.0}/cloudwright_cli/commands/schema_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-1.4.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
|