cloudwright-ai-cli 0.3.5__tar.gz → 1.0.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.3.5 → cloudwright_ai_cli-1.0.0}/PKG-INFO +2 -2
- cloudwright_ai_cli-1.0.0/cloudwright_cli/__init__.py +1 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/chat.py +23 -101
- cloudwright_ai_cli-1.0.0/cloudwright_cli/commands/chat_session.py +41 -0
- cloudwright_ai_cli-1.0.0/cloudwright_cli/commands/chat_streaming.py +51 -0
- cloudwright_ai_cli-1.0.0/cloudwright_cli/commands/chat_ui.py +74 -0
- cloudwright_ai_cli-1.0.0/cloudwright_cli/decorators.py +72 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/pyproject.toml +1 -1
- cloudwright_ai_cli-0.3.5/cloudwright_cli/__init__.py +0 -1
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/.gitignore +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/README.md +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/adr.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/compare.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/cost.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/databricks_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/design.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/diff.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/export.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/import_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/init_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/mcp_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/policy.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/schema_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/score_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/security_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/validate.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/completions.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/main.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/output.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/project.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/utils.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_chat_commands.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_chat_debug.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_chat_persistence.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_chat_streaming.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_cli.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/tests/test_modify_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.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: 1.0.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
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Topic :: System :: Systems Administration
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
|
-
Requires-Dist: cloudwright-ai<
|
|
19
|
+
Requires-Dist: cloudwright-ai<2,>=1.0.0
|
|
20
20
|
Requires-Dist: rich<15,>=13.9
|
|
21
21
|
Requires-Dist: typer<1,>=0.21
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -17,26 +17,11 @@ from rich.prompt import Prompt
|
|
|
17
17
|
from rich.rule import Rule
|
|
18
18
|
from rich.syntax import Syntax
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
from .chat_session import default_session_id, maybe_save_on_quit
|
|
21
|
+
from .chat_streaming import format_error, is_rate_limit, is_timeout
|
|
22
|
+
from .chat_ui import _HELP, print_cost_summary, print_diff, run_validate
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
Commands:
|
|
24
|
-
/save <file> Save last architecture to YAML file
|
|
25
|
-
/save-session [name] Save this conversation session
|
|
26
|
-
/load-session <name> Load a saved session
|
|
27
|
-
/sessions List saved sessions
|
|
28
|
-
/diagram Show ASCII diagram for last architecture
|
|
29
|
-
/yaml Show YAML for last architecture
|
|
30
|
-
/cost Show cost estimate for last architecture
|
|
31
|
-
/validate [fw] Run compliance check (hipaa, pci-dss, soc2, fedramp, gdpr)
|
|
32
|
-
/export <fmt> Export last architecture (terraform, mermaid, d2, cloudformation, sbom, aibom)
|
|
33
|
-
/terraform Export last architecture as Terraform
|
|
34
|
-
/new Start a new architecture from scratch
|
|
35
|
-
/help, /? Show this help
|
|
36
|
-
/quit Exit
|
|
37
|
-
|
|
38
|
-
Follow-up messages modify the current architecture. Use /new to start over.
|
|
39
|
-
"""
|
|
24
|
+
console = Console()
|
|
40
25
|
|
|
41
26
|
|
|
42
27
|
def chat(
|
|
@@ -114,7 +99,7 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
114
99
|
user_input = Prompt.ask("\n[bold cyan]>[/bold cyan]")
|
|
115
100
|
except (KeyboardInterrupt, EOFError):
|
|
116
101
|
console.print("\n[dim]Exiting.[/dim]")
|
|
117
|
-
|
|
102
|
+
maybe_save_on_quit(session, store)
|
|
118
103
|
break
|
|
119
104
|
|
|
120
105
|
text = user_input.strip()
|
|
@@ -122,7 +107,7 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
122
107
|
continue
|
|
123
108
|
|
|
124
109
|
if text.lower() in ("/quit", "/exit", "/q"):
|
|
125
|
-
|
|
110
|
+
maybe_save_on_quit(session, store)
|
|
126
111
|
console.print("[dim]Goodbye.[/dim]")
|
|
127
112
|
break
|
|
128
113
|
|
|
@@ -148,7 +133,7 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
148
133
|
|
|
149
134
|
if text.startswith("/save-session"):
|
|
150
135
|
parts = text.split(None, 1)
|
|
151
|
-
name = parts[1].strip() if len(parts) > 1 else
|
|
136
|
+
name = parts[1].strip() if len(parts) > 1 else default_session_id()
|
|
152
137
|
saved_path = store.save(name, session)
|
|
153
138
|
console.print(f"[green]Session saved: {name} ({saved_path})[/green]")
|
|
154
139
|
continue
|
|
@@ -201,7 +186,7 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
201
186
|
elif not session.current_spec.cost_estimate:
|
|
202
187
|
console.print("[yellow]No cost estimate available.[/yellow]")
|
|
203
188
|
else:
|
|
204
|
-
|
|
189
|
+
print_cost_summary(session.current_spec)
|
|
205
190
|
continue
|
|
206
191
|
|
|
207
192
|
if text.startswith("/validate"):
|
|
@@ -210,7 +195,7 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
210
195
|
else:
|
|
211
196
|
parts = text.split(None, 1)
|
|
212
197
|
framework = parts[1].strip() if len(parts) > 1 else None
|
|
213
|
-
|
|
198
|
+
run_validate(session.current_spec, framework)
|
|
214
199
|
continue
|
|
215
200
|
|
|
216
201
|
if text == "/terraform":
|
|
@@ -250,21 +235,19 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
250
235
|
live.update(Markdown("".join(chunks)))
|
|
251
236
|
except Exception as stream_err:
|
|
252
237
|
# Fallback to non-streaming if streaming fails
|
|
253
|
-
if
|
|
238
|
+
if is_rate_limit(stream_err):
|
|
254
239
|
console.print("[yellow]Rate limited, try again in a moment.[/yellow]")
|
|
255
240
|
continue
|
|
256
|
-
if
|
|
241
|
+
if is_timeout(stream_err):
|
|
257
242
|
console.print("[yellow]Request timed out, try a simpler request.[/yellow]")
|
|
258
243
|
continue
|
|
259
244
|
if isinstance(stream_err, RuntimeError) and "No LLM provider" in str(stream_err):
|
|
260
|
-
console.print(
|
|
261
|
-
"[red]No LLM provider configured.[/red] Set ANTHROPIC_API_KEY or OPENAI_API_KEY."
|
|
262
|
-
)
|
|
245
|
+
console.print("[red]No LLM provider configured.[/red] Set ANTHROPIC_API_KEY or OPENAI_API_KEY.")
|
|
263
246
|
continue
|
|
264
247
|
try:
|
|
265
248
|
_, _ = session.send(text)
|
|
266
249
|
except Exception as e:
|
|
267
|
-
console.print(
|
|
250
|
+
console.print(format_error(e))
|
|
268
251
|
continue
|
|
269
252
|
|
|
270
253
|
# Token usage (show regardless of spec)
|
|
@@ -294,11 +277,11 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
294
277
|
console.print(render_ascii(spec))
|
|
295
278
|
|
|
296
279
|
if spec.cost_estimate:
|
|
297
|
-
|
|
280
|
+
print_cost_summary(spec)
|
|
298
281
|
|
|
299
282
|
# Show spec diff when modifying
|
|
300
283
|
if had_spec and session.last_diff:
|
|
301
|
-
|
|
284
|
+
print_diff(session.last_diff)
|
|
302
285
|
|
|
303
286
|
suggestions = spec.metadata.get("suggestions", [])
|
|
304
287
|
if suggestions:
|
|
@@ -306,93 +289,32 @@ def _run_terminal_chat(resume: str | None = None, debug: bool = False) -> None:
|
|
|
306
289
|
|
|
307
290
|
|
|
308
291
|
def _default_session_id() -> str:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return datetime.now().strftime("session-%Y%m%d-%H%M%S")
|
|
292
|
+
return default_session_id()
|
|
312
293
|
|
|
313
294
|
|
|
314
295
|
def _maybe_save_on_quit(session: ConversationSession, store: SessionStore) -> None:
|
|
315
|
-
|
|
316
|
-
if turn_count == 0:
|
|
317
|
-
return
|
|
318
|
-
try:
|
|
319
|
-
answer = Prompt.ask("Save session? (y/N)", default="N")
|
|
320
|
-
except (KeyboardInterrupt, EOFError):
|
|
321
|
-
return
|
|
322
|
-
if answer.strip().lower() == "y":
|
|
323
|
-
name = _default_session_id()
|
|
324
|
-
store.save(name, session)
|
|
325
|
-
console.print(f"[green]Session saved as: {name}[/green]")
|
|
296
|
+
maybe_save_on_quit(session, store)
|
|
326
297
|
|
|
327
298
|
|
|
328
299
|
def _is_rate_limit(exc: Exception) -> bool:
|
|
329
|
-
|
|
330
|
-
return "rate limit" in msg or "rate_limit" in msg or "429" in msg
|
|
300
|
+
return is_rate_limit(exc)
|
|
331
301
|
|
|
332
302
|
|
|
333
303
|
def _is_timeout(exc: Exception) -> bool:
|
|
334
|
-
|
|
335
|
-
return "timeout" in msg or "timed out" in msg
|
|
304
|
+
return is_timeout(exc)
|
|
336
305
|
|
|
337
306
|
|
|
338
307
|
def _format_error(exc: Exception) -> str:
|
|
339
|
-
|
|
340
|
-
if isinstance(exc, RuntimeError) and "No LLM provider" in msg:
|
|
341
|
-
return "[red]No LLM provider configured.[/red] Set ANTHROPIC_API_KEY or OPENAI_API_KEY."
|
|
342
|
-
if _is_rate_limit(exc):
|
|
343
|
-
return "[yellow]Rate limited, try again in a moment.[/yellow]"
|
|
344
|
-
if _is_timeout(exc):
|
|
345
|
-
return "[yellow]Request timed out, try a simpler request.[/yellow]"
|
|
346
|
-
if isinstance(exc, ValueError):
|
|
347
|
-
return "[red]Failed to parse architecture, try rephrasing.[/red]"
|
|
348
|
-
return f"[red]Error:[/red] {exc}"
|
|
308
|
+
return format_error(exc)
|
|
349
309
|
|
|
350
310
|
|
|
351
311
|
def _print_diff(diff) -> None:
|
|
352
|
-
|
|
353
|
-
console.print(f"[green]+ Added: {', '.join(c.id for c in diff.added)}[/green]")
|
|
354
|
-
if diff.removed:
|
|
355
|
-
console.print(f"[red]- Removed: {', '.join(c.id for c in diff.removed)}[/red]")
|
|
356
|
-
if diff.changed:
|
|
357
|
-
console.print(f"[yellow]~ Changed: {', '.join(c.id for c in diff.changed)}[/yellow]")
|
|
358
|
-
if diff.cost_delta is not None and diff.cost_delta != 0:
|
|
359
|
-
sign = "+" if diff.cost_delta > 0 else ""
|
|
360
|
-
console.print(f"[dim]Cost delta: {sign}${diff.cost_delta:,.2f}/mo[/dim]")
|
|
312
|
+
print_diff(diff)
|
|
361
313
|
|
|
362
314
|
|
|
363
315
|
def _run_validate(spec: ArchSpec, framework: str | None) -> None:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if framework:
|
|
367
|
-
results = Validator().validate(spec, compliance=[framework])
|
|
368
|
-
else:
|
|
369
|
-
results = Validator().validate(spec, well_architected=True)
|
|
370
|
-
|
|
371
|
-
if not results:
|
|
372
|
-
console.print("[yellow]No validation results.[/yellow]")
|
|
373
|
-
return
|
|
374
|
-
|
|
375
|
-
for result in results:
|
|
376
|
-
passed = sum(1 for c in result.checks if c.passed)
|
|
377
|
-
total = len(result.checks)
|
|
378
|
-
status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
|
|
379
|
-
console.print(f"{result.framework}: {status} ({passed}/{total} checks passed)")
|
|
380
|
-
for check in result.checks:
|
|
381
|
-
icon = "[green]+[/green]" if check.passed else "[red]-[/red]"
|
|
382
|
-
console.print(f" {icon} {check.name}")
|
|
383
|
-
if not check.passed and check.recommendation:
|
|
384
|
-
console.print(f" [dim]{check.recommendation}[/dim]")
|
|
316
|
+
run_validate(spec, framework)
|
|
385
317
|
|
|
386
318
|
|
|
387
319
|
def _print_cost_summary(spec: ArchSpec) -> None:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
table = Table(title="Cost Estimate", show_footer=True)
|
|
391
|
-
table.add_column("Component", style="cyan")
|
|
392
|
-
table.add_column("Monthly", justify="right", footer=f"${spec.cost_estimate.monthly_total:,.2f}")
|
|
393
|
-
table.add_column("Notes", style="dim")
|
|
394
|
-
|
|
395
|
-
for item in spec.cost_estimate.breakdown:
|
|
396
|
-
table.add_row(item.component_id, f"${item.monthly:,.2f}", item.notes)
|
|
397
|
-
|
|
398
|
-
console.print(table)
|
|
320
|
+
print_cost_summary(spec)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from cloudwright import ConversationSession
|
|
6
|
+
from cloudwright.session_store import SessionStore
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_session(resume: str | None, store: SessionStore) -> ConversationSession:
|
|
14
|
+
session = ConversationSession()
|
|
15
|
+
if resume:
|
|
16
|
+
try:
|
|
17
|
+
session = store.load(resume)
|
|
18
|
+
console.print(f"[cyan]Resumed session: {resume}[/cyan]")
|
|
19
|
+
if session.current_spec:
|
|
20
|
+
console.print(f"[dim]Current architecture: {session.current_spec.name}[/dim]")
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
console.print(f"[yellow]Session {resume!r} not found. Starting fresh.[/yellow]")
|
|
23
|
+
return session
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def default_session_id() -> str:
|
|
27
|
+
return datetime.now().strftime("session-%Y%m%d-%H%M%S")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def maybe_save_on_quit(session: ConversationSession, store: SessionStore) -> None:
|
|
31
|
+
turn_count = sum(1 for m in session.history if m.get("role") == "user")
|
|
32
|
+
if turn_count == 0:
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
answer = Prompt.ask("Save session? (y/N)", default="N")
|
|
36
|
+
except (KeyboardInterrupt, EOFError):
|
|
37
|
+
return
|
|
38
|
+
if answer.strip().lower() == "y":
|
|
39
|
+
name = default_session_id()
|
|
40
|
+
store.save(name, session)
|
|
41
|
+
console.print(f"[green]Session saved as: {name}[/green]")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cloudwright import ConversationSession
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.live import Live
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def stream_response(session: ConversationSession, text: str) -> list[str]:
|
|
12
|
+
"""Stream LLM response with Live rendering. Returns collected chunks.
|
|
13
|
+
|
|
14
|
+
Falls back to session.send() if streaming fails due to a non-rate-limit,
|
|
15
|
+
non-timeout error.
|
|
16
|
+
|
|
17
|
+
Returns an empty list if the error was handled (rate limit / timeout / no provider).
|
|
18
|
+
Returns None on fallback send() error.
|
|
19
|
+
"""
|
|
20
|
+
chunks: list[str] = []
|
|
21
|
+
try:
|
|
22
|
+
with Live(Markdown(""), console=console, refresh_per_second=12) as live:
|
|
23
|
+
for chunk in session.send_stream(text):
|
|
24
|
+
chunks.append(chunk)
|
|
25
|
+
live.update(Markdown("".join(chunks)))
|
|
26
|
+
return chunks
|
|
27
|
+
except Exception as stream_err:
|
|
28
|
+
raise stream_err
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_rate_limit(exc: Exception) -> bool:
|
|
32
|
+
msg = str(exc).lower()
|
|
33
|
+
return "rate limit" in msg or "rate_limit" in msg or "429" in msg
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_timeout(exc: Exception) -> bool:
|
|
37
|
+
msg = str(exc).lower()
|
|
38
|
+
return "timeout" in msg or "timed out" in msg
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_error(exc: Exception) -> str:
|
|
42
|
+
msg = str(exc)
|
|
43
|
+
if isinstance(exc, RuntimeError) and "No LLM provider" in msg:
|
|
44
|
+
return "[red]No LLM provider configured.[/red] Set ANTHROPIC_API_KEY or OPENAI_API_KEY."
|
|
45
|
+
if is_rate_limit(exc):
|
|
46
|
+
return "[yellow]Rate limited, try again in a moment.[/yellow]"
|
|
47
|
+
if is_timeout(exc):
|
|
48
|
+
return "[yellow]Request timed out, try a simpler request.[/yellow]"
|
|
49
|
+
if isinstance(exc, ValueError):
|
|
50
|
+
return "[red]Failed to parse architecture, try rephrasing.[/red]"
|
|
51
|
+
return f"[red]Error:[/red] {exc}"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cloudwright import ArchSpec
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
_HELP = """\
|
|
10
|
+
Commands:
|
|
11
|
+
/save <file> Save last architecture to YAML file
|
|
12
|
+
/save-session [name] Save this conversation session
|
|
13
|
+
/load-session <name> Load a saved session
|
|
14
|
+
/sessions List saved sessions
|
|
15
|
+
/diagram Show ASCII diagram for last architecture
|
|
16
|
+
/yaml Show YAML for last architecture
|
|
17
|
+
/cost Show cost estimate for last architecture
|
|
18
|
+
/validate [fw] Run compliance check (hipaa, pci-dss, soc2, fedramp, gdpr)
|
|
19
|
+
/export <fmt> Export last architecture (terraform, mermaid, d2, cloudformation, sbom, aibom)
|
|
20
|
+
/terraform Export last architecture as Terraform
|
|
21
|
+
/new Start a new architecture from scratch
|
|
22
|
+
/help, /? Show this help
|
|
23
|
+
/quit Exit
|
|
24
|
+
|
|
25
|
+
Follow-up messages modify the current architecture. Use /new to start over.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def print_diff(diff) -> None:
|
|
30
|
+
if diff.added:
|
|
31
|
+
console.print(f"[green]+ Added: {', '.join(c.id for c in diff.added)}[/green]")
|
|
32
|
+
if diff.removed:
|
|
33
|
+
console.print(f"[red]- Removed: {', '.join(c.id for c in diff.removed)}[/red]")
|
|
34
|
+
if diff.changed:
|
|
35
|
+
console.print(f"[yellow]~ Changed: {', '.join(c.id for c in diff.changed)}[/yellow]")
|
|
36
|
+
if diff.cost_delta is not None and diff.cost_delta != 0:
|
|
37
|
+
sign = "+" if diff.cost_delta > 0 else ""
|
|
38
|
+
console.print(f"[dim]Cost delta: {sign}${diff.cost_delta:,.2f}/mo[/dim]")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_validate(spec: ArchSpec, framework: str | None) -> None:
|
|
42
|
+
from cloudwright.validator import Validator
|
|
43
|
+
|
|
44
|
+
if framework:
|
|
45
|
+
results = Validator().validate(spec, compliance=[framework])
|
|
46
|
+
else:
|
|
47
|
+
results = Validator().validate(spec, well_architected=True)
|
|
48
|
+
|
|
49
|
+
if not results:
|
|
50
|
+
console.print("[yellow]No validation results.[/yellow]")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
for result in results:
|
|
54
|
+
passed = sum(1 for c in result.checks if c.passed)
|
|
55
|
+
total = len(result.checks)
|
|
56
|
+
status = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
|
|
57
|
+
console.print(f"{result.framework}: {status} ({passed}/{total} checks passed)")
|
|
58
|
+
for check in result.checks:
|
|
59
|
+
icon = "[green]+[/green]" if check.passed else "[red]-[/red]"
|
|
60
|
+
console.print(f" {icon} {check.name}")
|
|
61
|
+
if not check.passed and check.recommendation:
|
|
62
|
+
console.print(f" [dim]{check.recommendation}[/dim]")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_cost_summary(spec: ArchSpec) -> None:
|
|
66
|
+
table = Table(title="Cost Estimate", show_footer=True)
|
|
67
|
+
table.add_column("Component", style="cyan")
|
|
68
|
+
table.add_column("Monthly", justify="right", footer=f"${spec.cost_estimate.monthly_total:,.2f}")
|
|
69
|
+
table.add_column("Notes", style="dim")
|
|
70
|
+
|
|
71
|
+
for item in spec.cost_estimate.breakdown:
|
|
72
|
+
table.add_row(item.component_id, f"${item.monthly:,.2f}", item.notes)
|
|
73
|
+
|
|
74
|
+
console.print(table)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
err_console = Console(stderr=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def cloudwright_command(json_output: bool = True, dry_run: bool = False) -> Callable:
|
|
14
|
+
"""Decorator that wraps command functions with standard output handling.
|
|
15
|
+
|
|
16
|
+
Handles: JSON envelope wrapping, Rich console formatting, --verbose stack
|
|
17
|
+
traces, --dry-run interception, and exit codes.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
json_output: Whether the command supports --json output mode.
|
|
21
|
+
dry_run: Whether the command supports --dry-run interception.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def decorator(fn: Callable) -> Callable:
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def wrapper(*args: Any, **kwargs: Any) -> None:
|
|
27
|
+
# Extract typer context from positional or keyword args
|
|
28
|
+
ctx = _extract_ctx(fn, args, kwargs)
|
|
29
|
+
verbose = bool(ctx and ctx.obj and ctx.obj.get("verbose"))
|
|
30
|
+
is_dry = dry_run and bool(ctx and ctx.obj and ctx.obj.get("dry_run"))
|
|
31
|
+
|
|
32
|
+
if is_dry:
|
|
33
|
+
# Commands that handle dry_run themselves will see ctx.obj["dry_run"]
|
|
34
|
+
# This outer gate lets the inner function emit_dry_run and exit.
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
fn(*args, **kwargs)
|
|
39
|
+
except typer.Exit:
|
|
40
|
+
raise
|
|
41
|
+
except SystemExit:
|
|
42
|
+
raise
|
|
43
|
+
except Exception as e:
|
|
44
|
+
if verbose:
|
|
45
|
+
err_console.print_exception()
|
|
46
|
+
else:
|
|
47
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
48
|
+
if verbose:
|
|
49
|
+
err_console.print(traceback.format_exc())
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
return wrapper
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_ctx(fn: Callable, args: tuple, kwargs: dict) -> typer.Context | None:
|
|
58
|
+
import inspect
|
|
59
|
+
|
|
60
|
+
sig = inspect.signature(fn)
|
|
61
|
+
param_names = list(sig.parameters.keys())
|
|
62
|
+
|
|
63
|
+
# Check kwargs first
|
|
64
|
+
if "ctx" in kwargs:
|
|
65
|
+
return kwargs["ctx"]
|
|
66
|
+
|
|
67
|
+
# Check positional args by parameter name
|
|
68
|
+
for i, name in enumerate(param_names):
|
|
69
|
+
if name == "ctx" and i < len(args):
|
|
70
|
+
return args[i]
|
|
71
|
+
|
|
72
|
+
return None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/analyze_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/catalog_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/databricks_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/import_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/modify_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/refresh_cmd.py
RENAMED
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.0}/cloudwright_cli/commands/schema_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudwright_ai_cli-0.3.5 → cloudwright_ai_cli-1.0.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
|