cloudwright-ai-cli 1.3.0__tar.gz → 1.5.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.
Files changed (53) hide show
  1. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/.gitignore +2 -0
  2. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/PKG-INFO +1 -1
  3. cloudwright_ai_cli-1.5.0/cloudwright_cli/__init__.py +1 -0
  4. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/chat_ui.py +1 -1
  5. cloudwright_ai_cli-1.5.0/cloudwright_cli/commands/compliance_cmd.py +104 -0
  6. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/export.py +32 -6
  7. cloudwright_ai_cli-1.5.0/cloudwright_cli/commands/import_live_cmd.py +172 -0
  8. cloudwright_ai_cli-1.5.0/cloudwright_cli/commands/plan_cmd.py +75 -0
  9. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/completions.py +2 -0
  10. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/main.py +6 -0
  11. cloudwright_ai_cli-1.3.0/cloudwright_cli/__init__.py +0 -1
  12. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/README.md +0 -0
  13. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/__main__.py +0 -0
  14. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/__init__.py +0 -0
  15. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/adr.py +0 -0
  16. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
  17. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
  18. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/chat.py +0 -0
  19. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/chat_session.py +0 -0
  20. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/chat_streaming.py +0 -0
  21. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/compare.py +0 -0
  22. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/cost.py +0 -0
  23. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/databricks_cmd.py +0 -0
  24. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/design.py +0 -0
  25. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/diff.py +0 -0
  26. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/drift_cmd.py +0 -0
  27. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/import_cmd.py +0 -0
  28. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/init_cmd.py +0 -0
  29. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
  30. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/mcp_cmd.py +0 -0
  31. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
  32. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/policy.py +0 -0
  33. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
  34. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/schema_cmd.py +0 -0
  35. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/score_cmd.py +0 -0
  36. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/security_cmd.py +0 -0
  37. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/commands/validate.py +0 -0
  38. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/decorators.py +0 -0
  39. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/output.py +0 -0
  40. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/project.py +0 -0
  41. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/py.typed +0 -0
  42. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/cloudwright_cli/utils.py +0 -0
  43. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/pyproject.toml +0 -0
  44. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/__init__.py +0 -0
  45. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_chat_commands.py +0 -0
  46. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_chat_debug.py +0 -0
  47. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_chat_persistence.py +0 -0
  48. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_chat_streaming.py +0 -0
  49. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_cli.py +0 -0
  50. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_drift_cmd.py +0 -0
  51. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_init.py +0 -0
  52. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_modify_cmd.py +0 -0
  53. {cloudwright_ai_cli-1.3.0 → cloudwright_ai_cli-1.5.0}/tests/test_project.py +0 -0
@@ -18,3 +18,5 @@ node_modules/
18
18
  CLAUDE.md
19
19
  packages/*/CLAUDE.md
20
20
  docs/fix-plan.md
21
+ tmp/
22
+ _recording*/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudwright-ai-cli
3
- Version: 1.3.0
3
+ Version: 1.5.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.5.0"
@@ -16,7 +16,7 @@ Commands:
16
16
  /yaml Show YAML for last architecture
17
17
  /cost Show cost estimate for last architecture
18
18
  /validate [fw] Run compliance check (hipaa, pci-dss, soc2, fedramp, gdpr)
19
- /export <fmt> Export last architecture (terraform, mermaid, d2, cloudformation, sbom, aibom)
19
+ /export <fmt> Export last architecture (terraform, pulumi-ts, pulumi-python, mermaid, d2, cloudformation, sbom, aibom)
20
20
  /terraform Export last architecture as Terraform
21
21
  /new Start a new architecture from scratch
22
22
  /help, /? Show this help
@@ -0,0 +1,104 @@
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
+ ) -> None:
32
+ """Scan an ArchSpec and map every finding to framework control IDs.
33
+
34
+ Maps design-stage findings to HIPAA / SOC 2 / PCI-DSS / FedRAMP / GDPR /
35
+ ISO 27001 / NIST 800-53 controls before any infrastructure exists. Folds in
36
+ a Checkov deep scan against the exported Terraform when Checkov is on PATH.
37
+ """
38
+ try:
39
+ from cloudwright import ArchSpec
40
+ from cloudwright.compliance import ComplianceScanner, render_markdown
41
+
42
+ spec = ArchSpec.from_file(spec_file)
43
+ fw_list = [f for f in (frameworks.split(",") if frameworks else []) if f.strip()]
44
+ report = ComplianceScanner().scan(spec, frameworks=fw_list or None, run_checkov=checkov)
45
+
46
+ if output:
47
+ Path(output).write_text(render_markdown(spec, report))
48
+
49
+ if is_json_mode(ctx):
50
+ if should_stream(ctx):
51
+ for f in report.findings:
52
+ emit_stream(f.as_dict())
53
+ else:
54
+ emit_success(ctx, report.as_dict())
55
+ _maybe_exit(report, fail_on)
56
+ return
57
+
58
+ console.print(f"\nCompliance Scan: {spec_file.name} (scanner: {report.scanner})\n")
59
+
60
+ posture = Table("Framework", "Satisfied", "Violated controls", "Findings", "Status")
61
+ for s in report.frameworks:
62
+ sat = f"{s.controls_total - len(s.controls_violated)}/{s.controls_total}"
63
+ viol = ", ".join(s.controls_violated) if s.controls_violated else "-"
64
+ style = "green" if s.status == "pass" else "red"
65
+ posture.add_row(s.framework, sat, viol, str(s.findings), f"[{style}]{s.status.upper()}[/{style}]")
66
+ console.print(posture)
67
+ console.print()
68
+
69
+ if not report.findings:
70
+ console.print("[green][PASS][/green] No control violations detected.")
71
+ else:
72
+ for f in report.findings:
73
+ sev = f.severity.upper()
74
+ color = {"CRITICAL": "bold red", "HIGH": "red", "MEDIUM": "yellow"}.get(sev, "dim")
75
+ console.print(f" [{color}][{sev}][/{color}] {f.message} [dim]({f.source})[/dim]")
76
+ if f.controls:
77
+ ctrl = ", ".join(f"{c.framework} {c.control_id}" for c in f.controls)
78
+ console.print(f" Controls: {ctrl}", style="cyan")
79
+ console.print(f" Remediation: {f.remediation}", style="dim")
80
+ console.print()
81
+
82
+ crit = report.critical_count
83
+ high = report.high_count
84
+ console.print(f"{len(report.findings)} finding(s) ({crit} critical, {high} high)")
85
+ threshold = _SEVERITY_ORDER.get(fail_on, 1)
86
+ worst = min((_SEVERITY_ORDER.get(f.severity, 4) for f in report.findings), default=4)
87
+ status = "PASSED" if worst > threshold else "FAILED"
88
+ console.print(f"Status: [{'green' if status == 'PASSED' else 'red'}]{status}[/] (fail-on={fail_on})")
89
+ if output:
90
+ console.print(f"\nReport written to {output}", style="dim")
91
+
92
+ _maybe_exit(report, fail_on)
93
+
94
+ except typer.Exit:
95
+ raise
96
+ except Exception as e:
97
+ handle_error(ctx, e)
98
+
99
+
100
+ def _maybe_exit(report, fail_on: str) -> None:
101
+ threshold = _SEVERITY_ORDER.get(fail_on, 1)
102
+ for f in report.findings:
103
+ if _SEVERITY_ORDER.get(f.severity, 4) <= threshold:
104
+ raise typer.Exit(1)
@@ -15,6 +15,10 @@ console = Console()
15
15
 
16
16
  _SYNTAX_MAP = {
17
17
  "terraform": "hcl",
18
+ "pulumi-ts": "typescript",
19
+ "pulumi-typescript": "typescript",
20
+ "pulumi-python": "python",
21
+ "pulumi-py": "python",
18
22
  "cloudformation": "yaml",
19
23
  "mermaid": "text",
20
24
  "d2": "text",
@@ -25,6 +29,26 @@ _SYNTAX_MAP = {
25
29
  "aibom": "json",
26
30
  }
27
31
 
32
+ # Formats that produce a multi-file project layout when --output points at a
33
+ # directory (or any extensionless path). Keep in sync with the dispatch in
34
+ # cloudwright.exporter.export_spec.
35
+ _DIRECTORY_FORMATS = {
36
+ "terraform",
37
+ "pulumi-ts",
38
+ "pulumi-typescript",
39
+ "pulumi-python",
40
+ "pulumi-py",
41
+ }
42
+
43
+ # Primary entry filename per directory-format, used only for status messages.
44
+ _DIRECTORY_ENTRY = {
45
+ "terraform": "main.tf",
46
+ "pulumi-ts": "index.ts",
47
+ "pulumi-typescript": "index.ts",
48
+ "pulumi-python": "__main__.py",
49
+ "pulumi-py": "__main__.py",
50
+ }
51
+
28
52
 
29
53
  def export(
30
54
  ctx: typer.Context,
@@ -39,9 +63,10 @@ def export(
39
63
  ],
40
64
  output: Annotated[Path | None, typer.Option("--output", "-o", help="Output file or directory")] = None,
41
65
  ) -> None:
42
- """Export an architecture spec to Terraform, CloudFormation, Mermaid, SVG, PNG, SBOM, or AIBOM."""
66
+ """Export an architecture spec to Terraform, Pulumi (TS/Python), CloudFormation, Mermaid, SVG, PNG, SBOM, or AIBOM."""
43
67
  fmt = format.lower().strip()
44
- if fmt not in FORMATS and fmt != "cfn":
68
+ _aliases = {"cfn", "pulumi-typescript", "pulumi-py"}
69
+ if fmt not in FORMATS and fmt not in _aliases:
45
70
  emit_error(ctx, ValueError(f"Unknown format {fmt!r}"), action=f"Use one of: {', '.join(FORMATS)}")
46
71
 
47
72
  if output:
@@ -55,11 +80,11 @@ def export(
55
80
  output_str = str(output) if output else None
56
81
  output_dir_str = None
57
82
 
58
- # Terraform with a directory target writes main.tf inside the dir
59
- if fmt == "terraform" and output and output.is_dir():
83
+ # Terraform / Pulumi with a directory target writes a project layout in the dir
84
+ if fmt in _DIRECTORY_FORMATS and output and output.is_dir():
60
85
  output_dir_str = output_str
61
86
  output_str = None
62
- elif fmt == "terraform" and output and not output.suffix:
87
+ elif fmt in _DIRECTORY_FORMATS and output and not output.suffix:
63
88
  # Treat extensionless output as a directory path
64
89
  output_dir_str = output_str
65
90
  output_str = None
@@ -104,7 +129,8 @@ def export(
104
129
 
105
130
  if output:
106
131
  if output_dir_str:
107
- console.print(f"[green]Written to {output_dir_str}/main.tf[/green]")
132
+ entry = _DIRECTORY_ENTRY.get(fmt, "main.tf")
133
+ console.print(f"[green]Written to {output_dir_str}/{entry}[/green]")
108
134
  else:
109
135
  console.print(f"[green]Written to {output}[/green]")
110
136
  else:
@@ -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)
@@ -47,6 +47,8 @@ def complete_workload_profile(incomplete: str) -> list[tuple[str, str]]:
47
47
  def complete_export_format(incomplete: str) -> list[tuple[str, str]]:
48
48
  formats = [
49
49
  ("terraform", "HashiCorp Terraform HCL"),
50
+ ("pulumi-ts", "Pulumi TypeScript (@pulumi/aws, @pulumi/gcp, @pulumi/azure-native)"),
51
+ ("pulumi-python", "Pulumi Python (pulumi_aws, pulumi_gcp, pulumi_azure_native)"),
50
52
  ("cloudformation", "AWS CloudFormation YAML"),
51
53
  ("mermaid", "Mermaid diagram syntax"),
52
54
  ("d2", "D2 diagram language"),
@@ -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
@@ -13,10 +14,12 @@ from cloudwright_cli.commands.diff import diff
13
14
  from cloudwright_cli.commands.drift_cmd import drift
14
15
  from cloudwright_cli.commands.export import export
15
16
  from cloudwright_cli.commands.import_cmd import import_infra
17
+ from cloudwright_cli.commands.import_live_cmd import import_live
16
18
  from cloudwright_cli.commands.init_cmd import init
17
19
  from cloudwright_cli.commands.lint_cmd import lint
18
20
  from cloudwright_cli.commands.mcp_cmd import mcp_serve
19
21
  from cloudwright_cli.commands.modify_cmd import modify
22
+ from cloudwright_cli.commands.plan_cmd import plan
20
23
  from cloudwright_cli.commands.policy import policy
21
24
  from cloudwright_cli.commands.refresh_cmd import refresh
22
25
  from cloudwright_cli.commands.schema_cmd import schema
@@ -68,8 +71,10 @@ app.command()(diff)
68
71
  app.command()(drift)
69
72
  app.command()(modify)
70
73
  app.command(name="import")(import_infra)
74
+ app.command(name="import-live")(import_live)
71
75
  app.command()(chat)
72
76
  app.command()(init)
77
+ app.command()(plan)
73
78
  app.command()(policy)
74
79
  app.command()(score)
75
80
  app.command()(analyze)
@@ -77,6 +82,7 @@ app.command()(refresh)
77
82
  app.command()(lint)
78
83
  app.command()(databricks_validate)
79
84
  app.command(name="security")(security_scan)
85
+ app.command(name="compliance")(compliance_scan)
80
86
  app.command(name="adr")(adr)
81
87
  app.command()(schema)
82
88
  app.command(name="mcp")(mcp_serve)
@@ -1 +0,0 @@
1
- __version__ = "1.3.0"