cloudwright-ai-cli 0.3.0__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.
Files changed (43) hide show
  1. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/PKG-INFO +1 -1
  2. cloudwright_ai_cli-0.3.1/cloudwright_cli/__init__.py +1 -0
  3. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/adr.py +20 -0
  4. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/analyze_cmd.py +8 -6
  5. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/catalog_cmd.py +5 -7
  6. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/compare.py +24 -1
  7. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/cost.py +4 -4
  8. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/design.py +22 -7
  9. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/diff.py +4 -4
  10. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/drift_cmd.py +5 -7
  11. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/export.py +15 -9
  12. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/import_cmd.py +4 -4
  13. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/init_cmd.py +2 -0
  14. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/lint_cmd.py +13 -4
  15. cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/mcp_cmd.py +34 -0
  16. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/modify_cmd.py +19 -9
  17. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/policy.py +7 -7
  18. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/refresh_cmd.py +3 -5
  19. cloudwright_ai_cli-0.3.1/cloudwright_cli/commands/schema_cmd.py +178 -0
  20. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/score_cmd.py +3 -3
  21. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/security_cmd.py +21 -11
  22. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/validate.py +16 -5
  23. cloudwright_ai_cli-0.3.1/cloudwright_cli/completions.py +39 -0
  24. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/main.py +8 -0
  25. cloudwright_ai_cli-0.3.1/cloudwright_cli/output.py +147 -0
  26. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/project.py +28 -0
  27. cloudwright_ai_cli-0.3.1/cloudwright_cli/utils.py +26 -0
  28. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/tests/test_cli.py +17 -11
  29. cloudwright_ai_cli-0.3.0/cloudwright_cli/__init__.py +0 -1
  30. cloudwright_ai_cli-0.3.0/cloudwright_cli/utils.py +0 -49
  31. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/.gitignore +0 -0
  32. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/README.md +0 -0
  33. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/__main__.py +0 -0
  34. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/__init__.py +0 -0
  35. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/chat.py +0 -0
  36. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/commands/databricks_cmd.py +0 -0
  37. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/cloudwright_cli/py.typed +0 -0
  38. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/pyproject.toml +0 -0
  39. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/tests/__init__.py +0 -0
  40. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/tests/test_drift_cmd.py +0 -0
  41. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/tests/test_init.py +0 -0
  42. {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.1}/tests/test_modify_cmd.py +0 -0
  43. {cloudwright_ai_cli-0.3.0 → 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.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"
@@ -6,6 +6,7 @@ from typing import Annotated
6
6
  import typer
7
7
  from rich.console import Console
8
8
 
9
+ from cloudwright_cli.output import emit_dry_run, emit_error, validate_output_path
9
10
  from cloudwright_cli.utils import handle_error
10
11
 
11
12
  console = Console()
@@ -58,7 +59,26 @@ def adr(
58
59
  try:
59
60
  from cloudwright import ArchSpec
60
61
 
62
+ if output:
63
+ try:
64
+ validate_output_path(output)
65
+ except ValueError as e:
66
+ emit_error(ctx, e)
67
+
61
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
+
62
82
  text = _generate_adr(spec, title=title, decision=decision)
63
83
 
64
84
  if output:
@@ -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
- console.print(f"[red]Error:[/red] Component '{component}' not found in spec.")
35
- console.print(f"[dim]Available components: {', '.join(sorted(valid_ids))}[/dim]")
36
- raise typer.Exit(1)
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.obj and ctx.obj.get("json"):
42
- print(json.dumps(result.to_dict(), indent=2))
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"
@@ -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
- import json
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
- console.print("[red]Error:[/red] Provide at least 2 instance names to compare.")
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
- print(json.dumps({"comparison": inst_map}, default=str))
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
- console.print("[red]Error:[/red] --providers requires at least one provider")
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.obj and ctx.obj.get("json"):
32
- import json
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
- console.print(f"[red]Error:[/red] {e}")
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.obj and ctx.obj.get("json"):
53
- import json
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.obj and ctx.obj.get("json"):
28
- import json
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]"))
@@ -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
- console.print(f"[red]Error:[/red] Design spec not found: {spec_file}")
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
- console.print(f"[red]Error:[/red] Infrastructure file not found: {infra_file}")
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 json_output:
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
- console.print_json(json.dumps(result, default=str))
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
- console.print(f"[red]Error:[/red] Unknown format {fmt!r}. Supported: {', '.join(FORMATS)}")
44
- raise typer.Exit(1)
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
- console.print(
66
- "[red]Error:[/red] D2 binary not found. Install: curl -fsSL https://d2lang.com/install.sh | sh"
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.obj and ctx.obj.get("json"):
94
- import json
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:
@@ -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
- json_mode = ctx.obj and ctx.obj.get("json")
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 output == "json":
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
- print(json.dumps(result, indent=2))
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)
@@ -12,6 +12,7 @@ from rich.rule import Rule
12
12
  from rich.syntax import Syntax
13
13
  from rich.table import Table
14
14
 
15
+ from cloudwright_cli.output import emit_dry_run, emit_success, err_console, is_json_mode, validate_output_path
15
16
  from cloudwright_cli.utils import handle_error
16
17
 
17
18
  console = Console()
@@ -23,7 +24,6 @@ def modify(
23
24
  instruction: Annotated[str, typer.Argument(help="Natural language modification instruction")],
24
25
  output: Annotated[str | None, typer.Option("--output", "-o", help="Output file (default: overwrite input)")] = None,
25
26
  dry_run: Annotated[bool, typer.Option("--dry-run", help="Show changes without writing")] = False,
26
- json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
27
27
  ) -> None:
28
28
  """Modify an ArchSpec with natural language and show the diff."""
29
29
  try:
@@ -33,7 +33,7 @@ def modify(
33
33
 
34
34
  spec_path = Path(spec_file)
35
35
  if not spec_path.exists():
36
- console.print(f"[red]Error:[/red] Spec file not found: {spec_file}")
36
+ err_console.print(f"[red]Error:[/red] Spec file not found: {spec_file}")
37
37
  raise typer.Exit(1)
38
38
 
39
39
  original = ArchSpec.from_file(spec_path)
@@ -43,9 +43,21 @@ def modify(
43
43
  try:
44
44
  architect = Architect()
45
45
  except RuntimeError as e:
46
- console.print(f"[red]Error:[/red] {e}")
46
+ err_console.print(f"[red]Error:[/red] {e}")
47
47
  raise typer.Exit(1) from None
48
48
 
49
+ if ctx.obj and ctx.obj.get("dry_run"):
50
+ from cloudwright.llm.anthropic import GENERATE_MODEL
51
+
52
+ spec_text = original.to_yaml()
53
+ emit_dry_run(ctx, {
54
+ "model": GENERATE_MODEL,
55
+ "estimated_tokens": len(spec_text + instruction) // 4,
56
+ "max_tokens": 8000,
57
+ "user_prompt_preview": f"Modify: {instruction}",
58
+ "constraints": {"spec_file": spec_file, "instruction": instruction},
59
+ })
60
+
49
61
  with console.status("Applying modification..."):
50
62
  modified = architect.modify(original, instruction)
51
63
 
@@ -60,15 +72,12 @@ def modify(
60
72
 
61
73
  diff_result = Differ().diff(original_costed, modified_costed)
62
74
 
63
- if json_output:
64
- import json
65
-
66
- result = {
75
+ if is_json_mode(ctx):
76
+ emit_success(ctx, {
67
77
  "original": original.model_dump(),
68
78
  "modified": modified.model_dump(),
69
79
  "diff": diff_result.model_dump(),
70
- }
71
- console.print_json(json.dumps(result, default=str))
80
+ })
72
81
  return
73
82
 
74
83
  console.print(Rule("[bold]Changes[/bold]"))
@@ -119,6 +128,7 @@ def modify(
119
128
 
120
129
  if not dry_run:
121
130
  out_path = Path(output) if output else spec_path
131
+ validate_output_path(out_path)
122
132
  out_path.write_text(modified.to_yaml())
123
133
  console.print(f"\n[green]Written to {out_path}[/green]")
124
134
  else:
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from pathlib import Path
5
4
  from typing import Annotated
6
5
 
@@ -11,6 +10,8 @@ from cloudwright.policy import PolicyEngine
11
10
  from rich.console import Console
12
11
  from rich.table import Table
13
12
 
13
+ from cloudwright_cli.output import emit_success, err_console, is_json_mode
14
+
14
15
  console = Console()
15
16
 
16
17
 
@@ -31,9 +32,8 @@ def policy(
31
32
 
32
33
  result = engine.evaluate_from_file(spec, rules, cost_estimate=cost_estimate)
33
34
 
34
- json_mode = ctx.obj.get("json", False) if ctx.obj else False
35
- if json_mode:
36
- print(json.dumps(result.model_dump(), default=str))
35
+ if is_json_mode(ctx):
36
+ emit_success(ctx, {"evaluation": result.model_dump(exclude_none=True)})
37
37
  if not result.passed:
38
38
  raise typer.Exit(1)
39
39
  return
@@ -78,11 +78,11 @@ def policy(
78
78
  except typer.Exit:
79
79
  raise
80
80
  except FileNotFoundError as e:
81
- console.print(f"[red]Error:[/red] File not found: {e}")
81
+ err_console.print(f"[red]Error:[/red] File not found: {e}")
82
82
  raise typer.Exit(1)
83
83
  except Exception as e:
84
84
  verbose = ctx.obj.get("verbose", False) if ctx.obj else False
85
- console.print(f"[red]Error:[/red] {e}")
85
+ err_console.print(f"[red]Error:[/red] {e}")
86
86
  if verbose:
87
- console.print_exception()
87
+ err_console.print_exception()
88
88
  raise typer.Exit(1)
@@ -2,13 +2,13 @@
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
9
8
  from rich.console import Console
10
9
  from rich.table import Table
11
10
 
11
+ from cloudwright_cli.output import emit_success, is_json_mode
12
12
  from cloudwright_cli.utils import handle_error
13
13
 
14
14
  console = Console()
@@ -46,9 +46,7 @@ def refresh(
46
46
  dry_run=dry_run,
47
47
  )
48
48
 
49
- json_mode = ctx.obj and ctx.obj.get("json")
50
-
51
- if json_mode:
49
+ if is_json_mode(ctx):
52
50
  data = {
53
51
  "total_fetched": summary.total_fetched,
54
52
  "total_errors": summary.total_errors,
@@ -64,7 +62,7 @@ def refresh(
64
62
  for r in summary.results
65
63
  ],
66
64
  }
67
- print(json.dumps(data, indent=2))
65
+ emit_success(ctx, {"refresh": data})
68
66
  return
69
67
 
70
68
  table = Table(show_header=True, header_style="bold")