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.
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/PKG-INFO +1 -1
- cloudwright_ai_cli-0.2.0/cloudwright_cli/__init__.py +1 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/chat.py +82 -7
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/design.py +43 -11
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/export.py +43 -2
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/utils.py +12 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_cli.py +40 -1
- cloudwright_ai_cli-0.1.0/cloudwright_cli/__init__.py +0 -1
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/.gitignore +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/CLAUDE.md +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/README.md +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/compare.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/cost.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/diff.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/import_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/init_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/policy.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/score_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/validate.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/main.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/project.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/pyproject.toml +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/tests/test_modify_cmd.py +0 -0
- {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.
|
|
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>
|
|
18
|
-
/
|
|
19
|
-
/
|
|
20
|
-
/
|
|
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(
|
|
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(
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
_print_cost_table(spec)
|
|
78
|
+
from cloudwright_cli.utils import auto_save_spec
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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[
|
|
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
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/analyze_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/catalog_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/import_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/modify_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.1.0 → cloudwright_ai_cli-0.2.0}/cloudwright_cli/commands/refresh_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
|