cloudwright-ai-cli 0.1.0__tar.gz → 0.2.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 (36) hide show
  1. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/PKG-INFO +1 -1
  2. cloudwright_ai_cli-0.2.0/cloudwright_cli/__init__.py +1 -0
  3. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/chat.py +82 -7
  4. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/design.py +43 -11
  5. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/export.py +43 -2
  6. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/utils.py +12 -0
  7. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_cli.py +40 -1
  8. cloudwright_ai_cli-0.1.0/cloudwright_cli/__init__.py +0 -1
  9. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/.gitignore +0 -0
  10. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/CLAUDE.md +0 -0
  11. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/README.md +0 -0
  12. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/__main__.py +0 -0
  13. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/__init__.py +0 -0
  14. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
  15. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
  16. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/compare.py +0 -0
  17. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/cost.py +0 -0
  18. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/diff.py +0 -0
  19. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/drift_cmd.py +0 -0
  20. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/import_cmd.py +0 -0
  21. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/init_cmd.py +0 -0
  22. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
  23. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
  24. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/policy.py +0 -0
  25. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
  26. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/score_cmd.py +0 -0
  27. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/validate.py +0 -0
  28. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/main.py +0 -0
  29. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/project.py +0 -0
  30. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/py.typed +0 -0
  31. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/pyproject.toml +0 -0
  32. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/__init__.py +0 -0
  33. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_drift_cmd.py +0 -0
  34. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_init.py +0 -0
  35. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_modify_cmd.py +0 -0
  36. {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.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: 0.1.0
3
+ Version: 0.2.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__ = "0.2.0"
@@ -4,6 +4,7 @@ from typing import Annotated
4
4
 
5
5
  import typer
6
6
  from cloudwright import Architect, ArchSpec
7
+ from cloudwright.ascii_diagram import render_ascii
7
8
  from rich.console import Console
8
9
  from rich.panel import Panel
9
10
  from rich.prompt import Prompt
@@ -14,10 +15,14 @@ console = Console()
14
15
 
15
16
  _HELP = """\
16
17
  Commands:
17
- /save <file> Save last architecture to YAML file
18
- /cost Show cost estimate for last architecture
19
- /export <fmt> Export last architecture (terraform, mermaid, sbom, aibom)
20
- /quit Exit
18
+ /save <file> Save last architecture to YAML file
19
+ /diagram Show ASCII diagram for last architecture
20
+ /yaml Show YAML for last architecture
21
+ /cost Show cost estimate for last architecture
22
+ /validate [fw] Run compliance check (hipaa, pci-dss, soc2, fedramp, gdpr)
23
+ /export <fmt> Export last architecture (terraform, mermaid, d2, cloudformation, sbom, aibom)
24
+ /terraform Export last architecture as Terraform
25
+ /quit Exit
21
26
  """
22
27
 
23
28
 
@@ -85,6 +90,22 @@ def _run_terminal_chat() -> None:
85
90
  console.print(f"[green]Saved to {path}[/green]")
86
91
  continue
87
92
 
93
+ if text == "/diagram":
94
+ if not last_spec:
95
+ console.print("[yellow]No architecture yet.[/yellow]")
96
+ else:
97
+ console.print(Rule(f"[bold cyan]{last_spec.name}[/bold cyan]"))
98
+ console.print(render_ascii(last_spec))
99
+ continue
100
+
101
+ if text == "/yaml":
102
+ if not last_spec:
103
+ console.print("[yellow]No architecture yet.[/yellow]")
104
+ else:
105
+ console.print(Rule(f"[bold cyan]{last_spec.name}[/bold cyan]"))
106
+ console.print(Syntax(last_spec.to_yaml(), "yaml", theme="monokai", word_wrap=True))
107
+ continue
108
+
88
109
  if text == "/cost":
89
110
  if not last_spec:
90
111
  console.print("[yellow]No architecture yet.[/yellow]")
@@ -94,6 +115,26 @@ def _run_terminal_chat() -> None:
94
115
  _print_cost_summary(last_spec)
95
116
  continue
96
117
 
118
+ if text.startswith("/validate"):
119
+ if not last_spec:
120
+ console.print("[yellow]No architecture yet.[/yellow]")
121
+ else:
122
+ parts = text.split(None, 1)
123
+ framework = parts[1].strip() if len(parts) > 1 else None
124
+ _run_validate(last_spec, framework)
125
+ continue
126
+
127
+ if text == "/terraform":
128
+ if not last_spec:
129
+ console.print("[yellow]No architecture to export yet.[/yellow]")
130
+ else:
131
+ try:
132
+ content = last_spec.export("terraform")
133
+ console.print(Syntax(content, "hcl", theme="monokai", word_wrap=True))
134
+ except ValueError as e:
135
+ console.print(f"[red]Error:[/red] {e}")
136
+ continue
137
+
97
138
  if text.startswith("/export "):
98
139
  fmt = text[8:].strip()
99
140
  if not last_spec:
@@ -101,7 +142,9 @@ def _run_terminal_chat() -> None:
101
142
  else:
102
143
  try:
103
144
  content = last_spec.export(fmt)
104
- lang = {"terraform": "hcl", "mermaid": "text"}.get(fmt, "json")
145
+ lang = {"terraform": "hcl", "mermaid": "text", "d2": "text", "cloudformation": "yaml"}.get(
146
+ fmt, "json"
147
+ )
105
148
  console.print(Syntax(content, lang, theme="monokai", word_wrap=True))
106
149
  except ValueError as e:
107
150
  console.print(f"[red]Error:[/red] {e}")
@@ -124,13 +167,45 @@ def _run_terminal_chat() -> None:
124
167
  last_spec = spec
125
168
  history.append({"role": "assistant", "content": f"Designed: {spec.name}"})
126
169
 
127
- yaml_str = spec.to_yaml()
128
170
  console.print(Rule(f"[bold cyan]{spec.name}[/bold cyan]"))
129
- console.print(Syntax(yaml_str, "yaml", theme="monokai", word_wrap=True))
171
+ console.print(render_ascii(spec))
130
172
 
131
173
  if spec.cost_estimate:
132
174
  _print_cost_summary(spec)
133
175
 
176
+ from cloudwright_cli.utils import auto_save_spec
177
+
178
+ save_path = auto_save_spec(spec)
179
+ console.print(f"[dim]Saved: {save_path}[/dim]")
180
+
181
+ suggestions = spec.metadata.get("suggestions", [])
182
+ if suggestions:
183
+ console.print(f"[dim]Try: {' | '.join(repr(s) for s in suggestions[:3])}[/dim]")
184
+
185
+
186
+ def _run_validate(spec: ArchSpec, framework: str | None) -> None:
187
+ from cloudwright.validator import Validator
188
+
189
+ if framework:
190
+ results = Validator().validate(spec, compliance=[framework])
191
+ else:
192
+ results = Validator().validate(spec, well_architected=True)
193
+
194
+ if not results:
195
+ console.print("[yellow]No validation results.[/yellow]")
196
+ return
197
+
198
+ for result in results:
199
+ passed = sum(1 for c in result.checks if c.passed)
200
+ total = len(result.checks)
201
+ status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
202
+ console.print(f"{result.framework}: {status} ({passed}/{total} checks passed)")
203
+ for check in result.checks:
204
+ icon = "[green]+[/green]" if check.passed else "[red]-[/red]"
205
+ console.print(f" {icon} {check.name}")
206
+ if not check.passed and check.recommendation:
207
+ console.print(f" [dim]{check.recommendation}[/dim]")
208
+
134
209
 
135
210
  def _looks_like_modification(text: str) -> bool:
136
211
  mod_verbs = (
@@ -5,6 +5,9 @@ from typing import Annotated
5
5
 
6
6
  import typer
7
7
  from cloudwright import Architect, Constraints
8
+ from cloudwright.ascii_diagram import render_ascii, render_next_steps
9
+ from cloudwright.cost import CostEngine
10
+ from cloudwright.validator import Validator
8
11
  from rich.console import Console
9
12
  from rich.panel import Panel
10
13
  from rich.syntax import Syntax
@@ -23,6 +26,7 @@ def design(
23
26
  list[str] | None, typer.Option(help="Compliance frameworks (hipaa, pci-dss, soc2, fedramp, gdpr)")
24
27
  ] = None,
25
28
  output: Annotated[Path | None, typer.Option("--output", "-o", help="Write YAML to file")] = None,
29
+ yaml_output: Annotated[bool, typer.Option("--yaml")] = False,
26
30
  ) -> None:
27
31
  """Design a cloud architecture from a natural language description."""
28
32
  constraints = Constraints(
@@ -47,20 +51,48 @@ def design(
47
51
 
48
52
  yaml_str = spec.to_yaml()
49
53
 
50
- console.print(
51
- Panel(
52
- Syntax(yaml_str, "yaml", theme="monokai", word_wrap=True),
53
- title=f"[bold cyan]{spec.name}[/bold cyan]",
54
- subtitle=f"{spec.provider.upper()} / {spec.region}",
54
+ if yaml_output:
55
+ console.print(
56
+ Panel(
57
+ Syntax(yaml_str, "yaml", theme="monokai", word_wrap=True),
58
+ title=f"[bold cyan]{spec.name}[/bold cyan]",
59
+ subtitle=f"{spec.provider.upper()} / {spec.region}",
60
+ )
55
61
  )
56
- )
62
+ if spec.cost_estimate:
63
+ _print_cost_table(spec)
64
+ if output:
65
+ output.write_text(yaml_str)
66
+ console.print(f"[green]Saved to {output}[/green]")
67
+ return
68
+
69
+ # Default: ASCII diagram + auto-save + next steps
70
+ console.print(render_ascii(spec))
71
+
72
+ if not spec.cost_estimate:
73
+ spec = spec.model_copy(update={"cost_estimate": CostEngine().estimate(spec)})
74
+ _print_cost_table(spec)
75
+
76
+ _print_compliance_flags(spec)
57
77
 
58
- if spec.cost_estimate:
59
- _print_cost_table(spec)
78
+ from cloudwright_cli.utils import auto_save_spec
60
79
 
61
- if output:
62
- output.write_text(yaml_str)
63
- console.print(f"[green]Saved to {output}[/green]")
80
+ save_path = auto_save_spec(spec, output)
81
+ console.print(f"[dim]Saved: {save_path}[/dim]")
82
+ console.print(f"[dim]{render_next_steps()}[/dim]")
83
+
84
+
85
+ def _print_compliance_flags(spec) -> None:
86
+ results = Validator().validate(spec, well_architected=True)
87
+ if not results:
88
+ return
89
+ wa = results[0]
90
+ total = len(wa.checks)
91
+ passed = sum(1 for c in wa.checks if c.passed)
92
+ console.print(
93
+ f"[dim]Well-Architected: {passed}/{total} checks passed | "
94
+ "Run 'cloudwright validate --compliance hipaa' for full report[/dim]"
95
+ )
64
96
 
65
97
 
66
98
  def _print_cost_table(spec) -> None:
@@ -15,6 +15,10 @@ _SYNTAX_MAP = {
15
15
  "terraform": "hcl",
16
16
  "cloudformation": "yaml",
17
17
  "mermaid": "text",
18
+ "d2": "text",
19
+ "svg": "xml",
20
+ "png": "text",
21
+ "c4": "text",
18
22
  "sbom": "json",
19
23
  "aibom": "json",
20
24
  }
@@ -23,10 +27,17 @@ _SYNTAX_MAP = {
23
27
  def export(
24
28
  ctx: typer.Context,
25
29
  spec_file: Annotated[Path, typer.Argument(help="Path to spec YAML file", exists=True)],
26
- format: Annotated[str, typer.Option("--format", "-f", help=f"Export format: {', '.join(FORMATS)}")],
30
+ format: Annotated[
31
+ str,
32
+ typer.Option(
33
+ "--format",
34
+ "-f",
35
+ help=f"Export format: {', '.join(FORMATS)}. svg/png require the D2 binary (https://d2lang.com).",
36
+ ),
37
+ ],
27
38
  output: Annotated[Path | None, typer.Option("--output", "-o", help="Output file or directory")] = None,
28
39
  ) -> None:
29
- """Export an architecture spec to Terraform, CloudFormation, Mermaid, SBOM, or AIBOM."""
40
+ """Export an architecture spec to Terraform, CloudFormation, Mermaid, SVG, PNG, SBOM, or AIBOM."""
30
41
  fmt = format.lower().strip()
31
42
  if fmt not in FORMATS and fmt != "cfn":
32
43
  console.print(f"[red]Error:[/red] Unknown format {fmt!r}. Supported: {', '.join(FORMATS)}")
@@ -46,6 +57,36 @@ def export(
46
57
  output_dir_str = output_str
47
58
  output_str = None
48
59
 
60
+ # PNG is binary — handle separately before the text-oriented path
61
+ if fmt == "png":
62
+ from cloudwright.exporter.renderer import DiagramRenderer
63
+
64
+ if not DiagramRenderer.is_available():
65
+ console.print(
66
+ "[red]Error:[/red] D2 binary not found. Install: curl -fsSL https://d2lang.com/install.sh | sh"
67
+ )
68
+ raise typer.Exit(1)
69
+
70
+ with console.status("Rendering PNG via D2..."):
71
+ data = DiagramRenderer().render_png(spec)
72
+
73
+ if output:
74
+ output.write_bytes(data)
75
+ console.print(f"[green]Written to {output}[/green]")
76
+ else:
77
+ console.print(f"[green]PNG rendered: {len(data)} bytes (use --output to save)[/green]")
78
+ return
79
+
80
+ # Warn when svg/c4 requested but D2 not installed — render still proceeds with fallback
81
+ if fmt in ("svg", "c4"):
82
+ from cloudwright.exporter.renderer import DiagramRenderer
83
+
84
+ if not DiagramRenderer.is_available():
85
+ console.print(
86
+ "[yellow]Warning:[/yellow] D2 binary not found — returning D2 source text. "
87
+ "Install: curl -fsSL https://d2lang.com/install.sh | sh"
88
+ )
89
+
49
90
  with console.status(f"Exporting as {fmt}..."):
50
91
  content = spec.export(fmt, output=output_str, output_dir=output_dir_str)
51
92
 
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import re
5
+ from pathlib import Path
4
6
 
5
7
  import typer
6
8
  from rich.console import Console
@@ -35,3 +37,13 @@ def handle_error(ctx: typer.Context, e: Exception) -> None:
35
37
  _err_console.print_exception()
36
38
 
37
39
  raise typer.Exit(1)
40
+
41
+
42
+ def auto_save_spec(spec, explicit_output: Path | None = None) -> Path:
43
+ if explicit_output:
44
+ explicit_output.write_text(spec.to_yaml())
45
+ return explicit_output
46
+ slug = re.sub(r"[^a-z0-9]+", "-", spec.name.lower()).strip("-")
47
+ path = Path(f"{slug}.yaml")
48
+ path.write_text(spec.to_yaml())
49
+ return path
@@ -65,7 +65,7 @@ def spec_pair(tmp_path: Path) -> tuple[Path, Path]:
65
65
  class TestAppHelp:
66
66
  def test_no_args_shows_help(self):
67
67
  result = runner.invoke(app, [])
68
- assert result.exit_code == 0
68
+ assert result.exit_code in (0, 2) # Typer returns 2 for missing required command
69
69
  assert "cloudwright" in result.output.lower() or "architecture" in result.output.lower()
70
70
 
71
71
  def test_help_flag(self):
@@ -263,6 +263,45 @@ class TestJsonOutput:
263
263
  assert "comparison" in data
264
264
 
265
265
 
266
+ class TestRenderNextSteps:
267
+ def test_render_next_steps_content(self):
268
+ from cloudwright.ascii_diagram import render_next_steps
269
+
270
+ result = render_next_steps()
271
+ assert "cloudwright cost" in result
272
+ assert "cloudwright validate" in result
273
+
274
+ def test_render_next_steps_returns_string(self):
275
+ from cloudwright.ascii_diagram import render_next_steps
276
+
277
+ result = render_next_steps()
278
+ assert isinstance(result, str)
279
+ assert len(result) > 0
280
+
281
+
282
+ class TestAutoSaveSpec:
283
+ def test_auto_save_explicit_output(self, tmp_path: Path):
284
+ from cloudwright import ArchSpec
285
+ from cloudwright_cli.utils import auto_save_spec
286
+
287
+ spec = ArchSpec.from_yaml(_SPEC_YAML)
288
+ out = tmp_path / "out.yaml"
289
+ result = auto_save_spec(spec, out)
290
+ assert result == out
291
+ assert out.exists()
292
+
293
+ def test_auto_save_slug_path(self, tmp_path: Path, monkeypatch):
294
+ from cloudwright import ArchSpec
295
+ from cloudwright_cli.utils import auto_save_spec
296
+
297
+ monkeypatch.chdir(tmp_path)
298
+ spec = ArchSpec.from_yaml(_SPEC_YAML)
299
+ result = auto_save_spec(spec)
300
+ assert result.exists()
301
+ assert result.suffix == ".yaml"
302
+ assert "test" in result.stem
303
+
304
+
266
305
  class TestErrorHandling:
267
306
  def test_error_handling_invalid_yaml(self, tmp_path: Path):
268
307
  bad_yaml = tmp_path / "bad.yaml"
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"