cloudwright-ai-cli 0.2.27__tar.gz → 0.3.1__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.2.27 → cloudwright_ai_cli-0.3.1}/PKG-INFO +1 -1
- cloudwright_ai_cli-0.3.1/cloudwright_cli/__init__.py +1 -0
- cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/adr.py +213 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/analyze_cmd.py +8 -6
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/catalog_cmd.py +5 -7
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/compare.py +24 -1
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/cost.py +4 -4
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/design.py +22 -7
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/diff.py +4 -4
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/drift_cmd.py +5 -7
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/export.py +15 -9
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/import_cmd.py +4 -4
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/init_cmd.py +2 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/lint_cmd.py +13 -4
- cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/mcp_cmd.py +34 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/modify_cmd.py +19 -9
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/policy.py +7 -7
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/refresh_cmd.py +3 -5
- cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/schema_cmd.py +178 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/score_cmd.py +3 -3
- cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/security_cmd.py +108 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/validate.py +16 -5
- cloudwright_ai_cli-0.3.1/cloudwright_cli/completions.py +39 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/main.py +12 -0
- cloudwright_ai_cli-0.3.1/cloudwright_cli/output.py +147 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/project.py +28 -0
- cloudwright_ai_cli-0.3.1/cloudwright_cli/utils.py +26 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/tests/test_cli.py +17 -11
- cloudwright_ai_cli-0.2.27/cloudwright_cli/__init__.py +0 -1
- cloudwright_ai_cli-0.2.27/cloudwright_cli/utils.py +0 -49
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/.gitignore +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/README.md +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/chat.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/databricks_cmd.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/pyproject.toml +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/tests/test_modify_cmd.py +0 -0
- {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/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.3.1
|
|
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.3.1"
|
|
@@ -0,0 +1,213 @@
|
|
|
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_dry_run, emit_error, validate_output_path
|
|
10
|
+
from cloudwright_cli.utils import handle_error
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
_ADR_SYSTEM = """You generate Architecture Decision Records (ADRs) in MADR format.
|
|
15
|
+
|
|
16
|
+
Given an architecture spec as JSON, produce a markdown ADR with this exact structure:
|
|
17
|
+
|
|
18
|
+
# ADR: {name} — {key decision}
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
Proposed
|
|
22
|
+
|
|
23
|
+
## Context
|
|
24
|
+
{problem and why a decision is needed}
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
{the chosen architecture and key choices}
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
| ID | Service | Provider | Purpose |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
{component rows}
|
|
33
|
+
|
|
34
|
+
## Consequences
|
|
35
|
+
### Positive
|
|
36
|
+
- {benefits}
|
|
37
|
+
|
|
38
|
+
### Negative
|
|
39
|
+
- {trade-offs and risks}
|
|
40
|
+
|
|
41
|
+
## Alternatives Considered
|
|
42
|
+
{alternatives if any, otherwise note none documented}
|
|
43
|
+
|
|
44
|
+
## Cost Estimate
|
|
45
|
+
{monthly cost if available, otherwise omit}
|
|
46
|
+
|
|
47
|
+
Be concise. Focus on the WHY, not just what the architecture contains.
|
|
48
|
+
Respond with ONLY the markdown — no explanation, no code fences."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def adr(
|
|
52
|
+
ctx: typer.Context,
|
|
53
|
+
spec_file: Annotated[Path, typer.Argument(help="Path to ArchSpec YAML file", exists=True)],
|
|
54
|
+
output: Annotated[str | None, typer.Option("--output", "-o", help="Write ADR to this file")] = None,
|
|
55
|
+
title: Annotated[str | None, typer.Option("--title", help="ADR title (default: auto-generated)")] = None,
|
|
56
|
+
decision: Annotated[str | None, typer.Option("--decision", help="Specific decision to document")] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Generate an Architecture Decision Record from an ArchSpec."""
|
|
59
|
+
try:
|
|
60
|
+
from cloudwright import ArchSpec
|
|
61
|
+
|
|
62
|
+
if output:
|
|
63
|
+
try:
|
|
64
|
+
validate_output_path(output)
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
emit_error(ctx, e)
|
|
67
|
+
|
|
68
|
+
spec = ArchSpec.from_file(spec_file)
|
|
69
|
+
|
|
70
|
+
if ctx.obj and ctx.obj.get("dry_run"):
|
|
71
|
+
from cloudwright.llm.anthropic import GENERATE_MODEL
|
|
72
|
+
|
|
73
|
+
spec_json = spec.model_dump_json(indent=2, exclude_none=True)
|
|
74
|
+
emit_dry_run(ctx, {
|
|
75
|
+
"model": GENERATE_MODEL,
|
|
76
|
+
"estimated_tokens": len(spec_json + _ADR_SYSTEM) // 4,
|
|
77
|
+
"max_tokens": 2000,
|
|
78
|
+
"system_prompt_preview": _ADR_SYSTEM,
|
|
79
|
+
"user_prompt_preview": f"Generate ADR for: {spec.name}",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
text = _generate_adr(spec, title=title, decision=decision)
|
|
83
|
+
|
|
84
|
+
if output:
|
|
85
|
+
Path(output).write_text(text)
|
|
86
|
+
console.print(f"[green]ADR written to {output}[/green]")
|
|
87
|
+
else:
|
|
88
|
+
print(text)
|
|
89
|
+
|
|
90
|
+
except typer.Exit:
|
|
91
|
+
raise
|
|
92
|
+
except Exception as e:
|
|
93
|
+
handle_error(ctx, e)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _generate_adr(spec, *, title: str | None = None, decision: str | None = None) -> str:
|
|
97
|
+
try:
|
|
98
|
+
return _llm_adr(spec, title=title, decision=decision)
|
|
99
|
+
except Exception:
|
|
100
|
+
return _deterministic_adr(spec, title=title, decision=decision)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _llm_adr(spec, *, title: str | None, decision: str | None) -> str:
|
|
104
|
+
from cloudwright.architect import Architect
|
|
105
|
+
|
|
106
|
+
arch = Architect()
|
|
107
|
+
spec_summary = spec.model_dump_json(indent=2, exclude_none=True)
|
|
108
|
+
|
|
109
|
+
decision_hint = f"\nDocument this specific decision: {decision}" if decision else ""
|
|
110
|
+
title_hint = f"\nUse this ADR title: {title}" if title else ""
|
|
111
|
+
prompt = f"Generate an ADR for this architecture:{title_hint}{decision_hint}\n\n{spec_summary}"
|
|
112
|
+
|
|
113
|
+
text, _ = arch.llm.generate([{"role": "user", "content": prompt}], _ADR_SYSTEM, max_tokens=2000)
|
|
114
|
+
if not text.strip().startswith("#"):
|
|
115
|
+
raise ValueError("LLM did not return markdown ADR")
|
|
116
|
+
return text.strip()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _deterministic_adr(spec, *, title: str | None = None, decision: str | None = None) -> str:
|
|
120
|
+
adr_title = title or spec.name
|
|
121
|
+
key_decision = decision or _infer_key_decision(spec)
|
|
122
|
+
|
|
123
|
+
lines = [
|
|
124
|
+
f"# ADR: {adr_title} — {key_decision}",
|
|
125
|
+
"",
|
|
126
|
+
"## Status",
|
|
127
|
+
"Proposed",
|
|
128
|
+
"",
|
|
129
|
+
"## Context",
|
|
130
|
+
_build_context(spec),
|
|
131
|
+
"",
|
|
132
|
+
"## Decision",
|
|
133
|
+
_build_decision(spec),
|
|
134
|
+
"",
|
|
135
|
+
"## Components",
|
|
136
|
+
"| ID | Service | Provider | Purpose |",
|
|
137
|
+
"|---|---|---|---|",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
for c in spec.components:
|
|
141
|
+
purpose = c.description or c.label
|
|
142
|
+
lines.append(f"| {c.id} | {c.service} | {c.provider} | {purpose} |")
|
|
143
|
+
|
|
144
|
+
lines += ["", "## Consequences"]
|
|
145
|
+
lines += _build_consequences(spec)
|
|
146
|
+
|
|
147
|
+
rationale = spec.metadata.get("rationale") or []
|
|
148
|
+
if rationale:
|
|
149
|
+
lines += ["", "## Alternatives Considered"]
|
|
150
|
+
for r in rationale:
|
|
151
|
+
if isinstance(r, dict):
|
|
152
|
+
lines.append(f"- **{r.get('decision', '')}**: {r.get('reason', '')}")
|
|
153
|
+
|
|
154
|
+
if spec.cost_estimate:
|
|
155
|
+
lines += [
|
|
156
|
+
"",
|
|
157
|
+
"## Cost Estimate",
|
|
158
|
+
f"Estimated monthly cost: ${spec.cost_estimate.monthly_total:,.2f} USD",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
return "\n".join(lines)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _infer_key_decision(spec) -> str:
|
|
165
|
+
rationale = spec.metadata.get("rationale") or []
|
|
166
|
+
if rationale and isinstance(rationale[0], dict):
|
|
167
|
+
return rationale[0].get("decision", f"{spec.provider.upper()} architecture")
|
|
168
|
+
return f"{spec.provider.upper()} architecture"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _build_context(spec) -> str:
|
|
172
|
+
parts = [f"This architecture, {spec.name!r}, targets the {spec.provider.upper()} platform in region {spec.region}."]
|
|
173
|
+
if spec.constraints:
|
|
174
|
+
if spec.constraints.compliance:
|
|
175
|
+
parts.append(f"Compliance requirements: {', '.join(spec.constraints.compliance)}.")
|
|
176
|
+
if spec.constraints.budget_monthly:
|
|
177
|
+
parts.append(f"Monthly budget constraint: ${spec.constraints.budget_monthly:,.0f}.")
|
|
178
|
+
parts.append(f"It consists of {len(spec.components)} components across {len(spec.connections)} connections.")
|
|
179
|
+
return " ".join(parts)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _build_decision(spec) -> str:
|
|
183
|
+
rationale = spec.metadata.get("rationale") or []
|
|
184
|
+
if rationale:
|
|
185
|
+
items = []
|
|
186
|
+
for r in rationale:
|
|
187
|
+
if isinstance(r, dict):
|
|
188
|
+
items.append(f"- **{r.get('decision', '')}**: {r.get('reason', '')}")
|
|
189
|
+
if items:
|
|
190
|
+
return "\n".join(items)
|
|
191
|
+
|
|
192
|
+
services = ", ".join(c.service for c in spec.components[:5])
|
|
193
|
+
suffix = ", ..." if len(spec.components) > 5 else ""
|
|
194
|
+
return f"Selected architecture using: {services}{suffix}."
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _build_consequences(spec) -> list[str]:
|
|
198
|
+
lines = ["### Positive"]
|
|
199
|
+
suggestions = spec.metadata.get("suggestions") or []
|
|
200
|
+
|
|
201
|
+
positives = [
|
|
202
|
+
f"Established {spec.provider.upper()} native services reduce operational overhead.",
|
|
203
|
+
f"{len(spec.components)} components provide clear separation of concerns.",
|
|
204
|
+
]
|
|
205
|
+
lines += [f"- {p}" for p in positives]
|
|
206
|
+
|
|
207
|
+
lines += ["", "### Negative"]
|
|
208
|
+
negatives = ["Vendor lock-in to selected provider and service tier."]
|
|
209
|
+
if suggestions:
|
|
210
|
+
negatives.append("Additional configuration required: " + suggestions[0].lower() + ".")
|
|
211
|
+
lines += [f"- {n}" for n in negatives]
|
|
212
|
+
|
|
213
|
+
return lines
|
{cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/analyze_cmd.py
RENAMED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
from typing import Annotated
|
|
7
6
|
|
|
8
7
|
import typer
|
|
@@ -11,6 +10,7 @@ from rich.panel import Panel
|
|
|
11
10
|
from rich.table import Table
|
|
12
11
|
from rich.tree import Tree
|
|
13
12
|
|
|
13
|
+
from cloudwright_cli.output import emit_error, emit_success, is_json_mode
|
|
14
14
|
from cloudwright_cli.utils import handle_error
|
|
15
15
|
|
|
16
16
|
console = Console()
|
|
@@ -31,15 +31,17 @@ def analyze(
|
|
|
31
31
|
if component:
|
|
32
32
|
valid_ids = {c.id for c in spec.components}
|
|
33
33
|
if component not in valid_ids:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
emit_error(
|
|
35
|
+
ctx,
|
|
36
|
+
ValueError(f"Component '{component}' not found in spec"),
|
|
37
|
+
action=f"Available: {', '.join(sorted(valid_ids))}",
|
|
38
|
+
)
|
|
37
39
|
|
|
38
40
|
analyzer = Analyzer()
|
|
39
41
|
result = analyzer.analyze(spec, component_id=component)
|
|
40
42
|
|
|
41
|
-
if ctx
|
|
42
|
-
|
|
43
|
+
if is_json_mode(ctx):
|
|
44
|
+
emit_success(ctx, {"analysis": result.to_dict()})
|
|
43
45
|
return
|
|
44
46
|
|
|
45
47
|
spof_text = ", ".join(result.spofs) if result.spofs else "None"
|
{cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/catalog_cmd.py
RENAMED
|
@@ -8,6 +8,8 @@ from cloudwright import Catalog
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
|
+
from cloudwright_cli.output import emit_success, err_console
|
|
12
|
+
|
|
11
13
|
console = Console()
|
|
12
14
|
|
|
13
15
|
catalog_app = typer.Typer(
|
|
@@ -68,9 +70,7 @@ def catalog_search(
|
|
|
68
70
|
# Resolve ctx.obj through parent chain when invoked via sub-app
|
|
69
71
|
obj = ctx.obj or (ctx.parent.obj if ctx.parent else None)
|
|
70
72
|
if obj and obj.get("json"):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
print(json.dumps({"results": results}, default=str))
|
|
73
|
+
emit_success(ctx, {"results": results})
|
|
74
74
|
return
|
|
75
75
|
|
|
76
76
|
if not results:
|
|
@@ -108,7 +108,7 @@ def catalog_compare(
|
|
|
108
108
|
) -> None:
|
|
109
109
|
"""Compare two or more cloud instances side by side."""
|
|
110
110
|
if len(instances) < 2:
|
|
111
|
-
|
|
111
|
+
err_console.print("[red]Error:[/red] Provide at least 2 instance names to compare.")
|
|
112
112
|
raise typer.Exit(1)
|
|
113
113
|
|
|
114
114
|
with console.status("Fetching instance details..."):
|
|
@@ -118,10 +118,8 @@ def catalog_compare(
|
|
|
118
118
|
# Resolve ctx.obj through parent chain when invoked via sub-app
|
|
119
119
|
obj = ctx.obj or (ctx.parent.obj if ctx.parent else None)
|
|
120
120
|
if obj and obj.get("json"):
|
|
121
|
-
import json
|
|
122
|
-
|
|
123
121
|
inst_map = {r.get("name", r.get("id", "")): r for r in results}
|
|
124
|
-
|
|
122
|
+
emit_success(ctx, {"comparison": inst_map})
|
|
125
123
|
return
|
|
126
124
|
|
|
127
125
|
if not results:
|
|
@@ -8,17 +8,20 @@ from cloudwright import Architect, ArchSpec
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
|
+
from cloudwright_cli.output import emit_success, err_console, is_json_mode
|
|
12
|
+
|
|
11
13
|
console = Console()
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def compare(
|
|
17
|
+
ctx: typer.Context,
|
|
15
18
|
spec_file: Annotated[Path, typer.Argument(help="Path to spec YAML file", exists=True)],
|
|
16
19
|
providers: Annotated[str, typer.Option(help="Comma-separated target providers")],
|
|
17
20
|
) -> None:
|
|
18
21
|
"""Compare an architecture across multiple cloud providers."""
|
|
19
22
|
target_providers = [p.strip() for p in providers.split(",") if p.strip()]
|
|
20
23
|
if not target_providers:
|
|
21
|
-
|
|
24
|
+
err_console.print("[red]Error:[/red] --providers requires at least one provider")
|
|
22
25
|
raise typer.Exit(1)
|
|
23
26
|
|
|
24
27
|
spec = ArchSpec.from_file(spec_file)
|
|
@@ -32,6 +35,26 @@ def compare(
|
|
|
32
35
|
if alt.spec:
|
|
33
36
|
alt_map[alt.provider] = alt.spec
|
|
34
37
|
|
|
38
|
+
if is_json_mode(ctx):
|
|
39
|
+
origin_total = spec.cost_estimate.monthly_total if spec.cost_estimate else 0.0
|
|
40
|
+
data = {
|
|
41
|
+
"baseline": spec.provider,
|
|
42
|
+
"providers": {
|
|
43
|
+
spec.provider: {
|
|
44
|
+
"monthly_total": origin_total,
|
|
45
|
+
"components": [c.model_dump() for c in spec.components],
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
for alt in alts:
|
|
50
|
+
data["providers"][alt.provider] = {
|
|
51
|
+
"monthly_total": alt.monthly_total,
|
|
52
|
+
"key_differences": alt.key_differences,
|
|
53
|
+
"components": [c.model_dump() for c in alt.spec.components] if alt.spec else [],
|
|
54
|
+
}
|
|
55
|
+
emit_success(ctx, {"comparison": data})
|
|
56
|
+
return
|
|
57
|
+
|
|
35
58
|
# Side-by-side service comparison
|
|
36
59
|
table = Table(title=f"Provider Comparison — {spec.name}")
|
|
37
60
|
table.add_column("Component", style="cyan")
|
|
@@ -9,6 +9,8 @@ from cloudwright.cost import CostEngine
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
|
+
from cloudwright_cli.output import emit_success, is_json_mode
|
|
13
|
+
|
|
12
14
|
console = Console()
|
|
13
15
|
|
|
14
16
|
|
|
@@ -28,10 +30,8 @@ def cost(
|
|
|
28
30
|
engine = CostEngine()
|
|
29
31
|
spec.cost_estimate = engine.estimate(spec)
|
|
30
32
|
|
|
31
|
-
if ctx
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
print(json.dumps({"estimate": spec.cost_estimate.model_dump()}, default=str))
|
|
33
|
+
if is_json_mode(ctx):
|
|
34
|
+
emit_success(ctx, {"estimate": spec.cost_estimate.model_dump(exclude_none=True)})
|
|
35
35
|
return
|
|
36
36
|
|
|
37
37
|
if compare:
|
|
@@ -13,6 +13,8 @@ from rich.panel import Panel
|
|
|
13
13
|
from rich.syntax import Syntax
|
|
14
14
|
from rich.table import Table
|
|
15
15
|
|
|
16
|
+
from cloudwright_cli.output import emit_dry_run, emit_error, emit_success, is_json_mode
|
|
17
|
+
|
|
16
18
|
console = Console()
|
|
17
19
|
|
|
18
20
|
|
|
@@ -35,24 +37,37 @@ def design(
|
|
|
35
37
|
compliance=compliance or [],
|
|
36
38
|
)
|
|
37
39
|
|
|
40
|
+
# Dry-run: show what the LLM call would look like
|
|
41
|
+
if ctx.obj and ctx.obj.get("dry_run"):
|
|
42
|
+
from cloudwright.architect import _build_constraint_prompt
|
|
43
|
+
from cloudwright.llm.anthropic import GENERATE_MODEL
|
|
44
|
+
|
|
45
|
+
system = Architect._select_system_prompt(description)
|
|
46
|
+
if constraints:
|
|
47
|
+
system += _build_constraint_prompt(constraints)
|
|
48
|
+
emit_dry_run(ctx, {
|
|
49
|
+
"model": GENERATE_MODEL,
|
|
50
|
+
"estimated_tokens": len(system + description) // 4,
|
|
51
|
+
"max_tokens": 10000,
|
|
52
|
+
"system_prompt_preview": system[:200],
|
|
53
|
+
"user_prompt_preview": description,
|
|
54
|
+
"constraints": constraints.model_dump(exclude_none=True),
|
|
55
|
+
})
|
|
56
|
+
|
|
38
57
|
try:
|
|
39
58
|
architect = Architect()
|
|
40
59
|
except RuntimeError as e:
|
|
41
|
-
|
|
42
|
-
raise typer.Exit(1) from None
|
|
60
|
+
emit_error(ctx, e, action="Set ANTHROPIC_API_KEY or OPENAI_API_KEY")
|
|
43
61
|
|
|
44
62
|
with console.status("Designing architecture..."):
|
|
45
63
|
spec = architect.design(description, constraints=constraints)
|
|
46
|
-
# Set provider/region from CLI args if not overridden by LLM
|
|
47
64
|
if spec.provider == "aws" and provider != "aws":
|
|
48
65
|
spec = spec.model_copy(update={"provider": provider})
|
|
49
66
|
if spec.region == "us-east-1" and region != "us-east-1":
|
|
50
67
|
spec = spec.model_copy(update={"region": region})
|
|
51
68
|
|
|
52
|
-
if ctx
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
print(json.dumps(spec.model_dump(), default=str))
|
|
69
|
+
if is_json_mode(ctx):
|
|
70
|
+
emit_success(ctx, {"spec": spec.model_dump(exclude_none=True), "yaml": spec.to_yaml()})
|
|
56
71
|
return
|
|
57
72
|
|
|
58
73
|
yaml_str = spec.to_yaml()
|
|
@@ -9,6 +9,8 @@ from rich.console import Console
|
|
|
9
9
|
from rich.rule import Rule
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
|
+
from cloudwright_cli.output import emit_success, is_json_mode
|
|
13
|
+
|
|
12
14
|
console = Console()
|
|
13
15
|
|
|
14
16
|
|
|
@@ -24,10 +26,8 @@ def diff(
|
|
|
24
26
|
with console.status("Computing diff..."):
|
|
25
27
|
result = Differ().diff(a, b)
|
|
26
28
|
|
|
27
|
-
if ctx
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
print(json.dumps(result.model_dump(), default=str))
|
|
29
|
+
if is_json_mode(ctx):
|
|
30
|
+
emit_success(ctx, {"diff": result.model_dump(exclude_none=True)})
|
|
31
31
|
return
|
|
32
32
|
|
|
33
33
|
console.print(Rule(f"[bold]Diff: {spec_a.name} → {spec_b.name}[/bold]"))
|
{cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/drift_cmd.py
RENAMED
|
@@ -11,6 +11,7 @@ from rich.panel import Panel
|
|
|
11
11
|
from rich.rule import Rule
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
|
+
from cloudwright_cli.output import emit_success, err_console, is_json_mode
|
|
14
15
|
from cloudwright_cli.utils import handle_error
|
|
15
16
|
|
|
16
17
|
console = Console()
|
|
@@ -23,25 +24,22 @@ def drift(
|
|
|
23
24
|
fmt: Annotated[
|
|
24
25
|
str, typer.Option("--format", "-f", help="Infrastructure format: auto, terraform, cloudformation")
|
|
25
26
|
] = "auto",
|
|
26
|
-
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
27
27
|
) -> None:
|
|
28
28
|
"""Compare design spec against deployed infrastructure to detect drift."""
|
|
29
29
|
try:
|
|
30
30
|
from cloudwright.drift import detect_drift
|
|
31
31
|
|
|
32
32
|
if not Path(spec_file).exists():
|
|
33
|
-
|
|
33
|
+
err_console.print(f"[red]Error:[/red] Design spec not found: {spec_file}")
|
|
34
34
|
raise typer.Exit(1)
|
|
35
35
|
if not Path(infra_file).exists():
|
|
36
|
-
|
|
36
|
+
err_console.print(f"[red]Error:[/red] Infrastructure file not found: {infra_file}")
|
|
37
37
|
raise typer.Exit(1)
|
|
38
38
|
|
|
39
39
|
with console.status("Detecting drift..."):
|
|
40
40
|
report = detect_drift(spec_file, infra_file, infra_format=fmt)
|
|
41
41
|
|
|
42
|
-
if
|
|
43
|
-
import json
|
|
44
|
-
|
|
42
|
+
if is_json_mode(ctx):
|
|
45
43
|
result = {
|
|
46
44
|
"drift_score": report.drift_score,
|
|
47
45
|
"drifted_components": report.drifted_components,
|
|
@@ -50,7 +48,7 @@ def drift(
|
|
|
50
48
|
"diff": report.diff.model_dump(),
|
|
51
49
|
"summary": report.summary,
|
|
52
50
|
}
|
|
53
|
-
|
|
51
|
+
emit_success(ctx, {"drift": result})
|
|
54
52
|
return
|
|
55
53
|
|
|
56
54
|
score_color = "green" if report.drift_score == 0 else "yellow" if report.drift_score < 0.3 else "red"
|
|
@@ -9,6 +9,8 @@ from cloudwright.exporter import FORMATS
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.syntax import Syntax
|
|
11
11
|
|
|
12
|
+
from cloudwright_cli.output import emit_error, emit_success, is_json_mode, validate_output_path
|
|
13
|
+
|
|
12
14
|
console = Console()
|
|
13
15
|
|
|
14
16
|
_SYNTAX_MAP = {
|
|
@@ -40,8 +42,13 @@ def export(
|
|
|
40
42
|
"""Export an architecture spec to Terraform, CloudFormation, Mermaid, SVG, PNG, SBOM, or AIBOM."""
|
|
41
43
|
fmt = format.lower().strip()
|
|
42
44
|
if fmt not in FORMATS and fmt != "cfn":
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
emit_error(ctx, ValueError(f"Unknown format {fmt!r}"), action=f"Use one of: {', '.join(FORMATS)}")
|
|
46
|
+
|
|
47
|
+
if output:
|
|
48
|
+
try:
|
|
49
|
+
validate_output_path(output)
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
emit_error(ctx, e)
|
|
45
52
|
|
|
46
53
|
spec = ArchSpec.from_file(spec_file)
|
|
47
54
|
|
|
@@ -62,10 +69,11 @@ def export(
|
|
|
62
69
|
from cloudwright.exporter.renderer import DiagramRenderer
|
|
63
70
|
|
|
64
71
|
if not DiagramRenderer.is_available():
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
emit_error(
|
|
73
|
+
ctx,
|
|
74
|
+
RuntimeError("D2 binary not found"),
|
|
75
|
+
action="Install: curl -fsSL https://d2lang.com/install.sh | sh",
|
|
67
76
|
)
|
|
68
|
-
raise typer.Exit(1)
|
|
69
77
|
|
|
70
78
|
with console.status("Rendering PNG via D2..."):
|
|
71
79
|
data = DiagramRenderer().render_png(spec)
|
|
@@ -90,10 +98,8 @@ def export(
|
|
|
90
98
|
with console.status(f"Exporting as {fmt}..."):
|
|
91
99
|
content = spec.export(fmt, output=output_str, output_dir=output_dir_str)
|
|
92
100
|
|
|
93
|
-
if ctx
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
print(json.dumps({"format": fmt, "content": content}, default=str))
|
|
101
|
+
if is_json_mode(ctx):
|
|
102
|
+
emit_success(ctx, {"format": fmt, "content": content})
|
|
97
103
|
return
|
|
98
104
|
|
|
99
105
|
if output:
|
{cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/import_cmd.py
RENAMED
|
@@ -10,6 +10,7 @@ from typing import Annotated
|
|
|
10
10
|
import typer
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
|
|
13
|
+
from cloudwright_cli.output import emit_success, is_json_mode, validate_output_path
|
|
13
14
|
from cloudwright_cli.utils import handle_error
|
|
14
15
|
|
|
15
16
|
console = Console()
|
|
@@ -45,15 +46,14 @@ def import_infra(
|
|
|
45
46
|
if name:
|
|
46
47
|
spec = spec.model_copy(update={"name": name})
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if json_mode:
|
|
51
|
-
print(json.dumps(json.loads(spec.to_json()), indent=2))
|
|
49
|
+
if is_json_mode(ctx):
|
|
50
|
+
emit_success(ctx, {"spec": json.loads(spec.to_json())})
|
|
52
51
|
return
|
|
53
52
|
|
|
54
53
|
content = spec.to_yaml()
|
|
55
54
|
|
|
56
55
|
if output:
|
|
56
|
+
validate_output_path(output)
|
|
57
57
|
Path(output).write_text(content)
|
|
58
58
|
n_comps = len(spec.components)
|
|
59
59
|
n_conns = len(spec.connections)
|
|
@@ -10,6 +10,7 @@ import yaml
|
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
|
+
from cloudwright_cli.output import validate_output_path
|
|
13
14
|
from cloudwright_cli.utils import handle_error
|
|
14
15
|
|
|
15
16
|
console = Console()
|
|
@@ -116,6 +117,7 @@ def init(
|
|
|
116
117
|
(proj_dir / "config.yaml").write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
117
118
|
else:
|
|
118
119
|
output_path = Path(output)
|
|
120
|
+
validate_output_path(output_path)
|
|
119
121
|
|
|
120
122
|
output_path.write_text(yaml.dump(spec_data, default_flow_style=False, sort_keys=False, allow_unicode=True))
|
|
121
123
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Annotated
|
|
8
7
|
|
|
@@ -11,6 +10,7 @@ from rich.console import Console
|
|
|
11
10
|
from rich.table import Table
|
|
12
11
|
from rich.text import Text
|
|
13
12
|
|
|
13
|
+
from cloudwright_cli.output import emit_stream, emit_success, is_json_mode, should_stream
|
|
14
14
|
from cloudwright_cli.utils import handle_error
|
|
15
15
|
|
|
16
16
|
console = Console()
|
|
@@ -19,7 +19,6 @@ console = Console()
|
|
|
19
19
|
def lint(
|
|
20
20
|
ctx: typer.Context,
|
|
21
21
|
spec_file: Annotated[Path, typer.Argument(help="Architecture spec YAML file", exists=True)],
|
|
22
|
-
output: Annotated[str, typer.Option(help="Output format: text, json")] = "text",
|
|
23
22
|
strict: Annotated[bool, typer.Option(help="Fail on warnings too")] = False,
|
|
24
23
|
) -> None:
|
|
25
24
|
"""Detect architecture anti-patterns in a spec file."""
|
|
@@ -30,7 +29,17 @@ def lint(
|
|
|
30
29
|
spec = ArchSpec.from_file(spec_file)
|
|
31
30
|
warnings = run_lint(spec)
|
|
32
31
|
|
|
33
|
-
if
|
|
32
|
+
if is_json_mode(ctx):
|
|
33
|
+
if should_stream(ctx):
|
|
34
|
+
for w in warnings:
|
|
35
|
+
emit_stream({
|
|
36
|
+
"rule": w.rule,
|
|
37
|
+
"severity": w.severity,
|
|
38
|
+
"component": w.component,
|
|
39
|
+
"message": w.message,
|
|
40
|
+
"recommendation": w.recommendation,
|
|
41
|
+
})
|
|
42
|
+
return
|
|
34
43
|
result = [
|
|
35
44
|
{
|
|
36
45
|
"rule": w.rule,
|
|
@@ -41,7 +50,7 @@ def lint(
|
|
|
41
50
|
}
|
|
42
51
|
for w in warnings
|
|
43
52
|
]
|
|
44
|
-
|
|
53
|
+
emit_success(ctx, {"warnings": result})
|
|
45
54
|
else:
|
|
46
55
|
if not warnings:
|
|
47
56
|
console.print(f"[green][PASS][/green] No anti-patterns detected in {spec.name}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Serve cloudwright functions as an MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def mcp_serve(
|
|
11
|
+
tools: Annotated[
|
|
12
|
+
str | None,
|
|
13
|
+
typer.Option("--tools", "-t", help="Comma-separated tool groups: design,cost,validate,analyze,export,session"),
|
|
14
|
+
] = None,
|
|
15
|
+
transport: Annotated[str, typer.Option("--transport", help="Transport: stdio or sse")] = "stdio",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Start an MCP server exposing cloudwright tools."""
|
|
18
|
+
try:
|
|
19
|
+
from cloudwright_mcp.server import create_server
|
|
20
|
+
except ImportError:
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
Console(stderr=True).print(
|
|
24
|
+
"[red]Error:[/red] cloudwright-ai-mcp not installed.\n"
|
|
25
|
+
" Install: pip install cloudwright-ai-mcp"
|
|
26
|
+
)
|
|
27
|
+
raise typer.Exit(1) from None
|
|
28
|
+
|
|
29
|
+
tool_set: set[str] | None = None
|
|
30
|
+
if tools:
|
|
31
|
+
tool_set = {t.strip() for t in tools.split(",") if t.strip()}
|
|
32
|
+
|
|
33
|
+
server = create_server(tools=tool_set)
|
|
34
|
+
server.run(transport=transport)
|