cloudwright-ai-cli 0.3.0__tar.gz → 0.3.2__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.0 → cloudwright_ai_cli-0.3.2}/PKG-INFO +1 -1
- cloudwright_ai_cli-0.3.2/cloudwright_cli/__init__.py +1 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/adr.py +23 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/analyze_cmd.py +8 -6
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/catalog_cmd.py +5 -7
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/compare.py +24 -1
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/cost.py +24 -5
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/design.py +25 -7
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/diff.py +4 -4
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/drift_cmd.py +5 -7
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/export.py +15 -9
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/import_cmd.py +4 -4
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/init_cmd.py +2 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/lint_cmd.py +15 -4
- cloudwright_ai_cli-0.3.2/cloudwright_cli/commands/mcp_cmd.py +33 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/modify_cmd.py +28 -12
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/policy.py +7 -7
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/refresh_cmd.py +3 -5
- cloudwright_ai_cli-0.3.2/cloudwright_cli/commands/schema_cmd.py +176 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/score_cmd.py +3 -3
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/security_cmd.py +28 -16
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/validate.py +18 -5
- cloudwright_ai_cli-0.3.2/cloudwright_cli/completions.py +59 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/main.py +8 -0
- cloudwright_ai_cli-0.3.2/cloudwright_cli/output.py +147 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/project.py +28 -0
- cloudwright_ai_cli-0.3.2/cloudwright_cli/utils.py +26 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/tests/test_cli.py +17 -11
- cloudwright_ai_cli-0.3.0/cloudwright_cli/__init__.py +0 -1
- cloudwright_ai_cli-0.3.0/cloudwright_cli/utils.py +0 -49
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/.gitignore +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/README.md +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/__main__.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/__init__.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/chat.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/databricks_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/py.typed +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/pyproject.toml +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/tests/__init__.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/tests/test_drift_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/tests/test_init.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/tests/test_modify_cmd.py +0 -0
- {cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/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.
|
|
3
|
+
Version: 0.3.2
|
|
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.2"
|
|
@@ -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,29 @@ 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(
|
|
75
|
+
ctx,
|
|
76
|
+
{
|
|
77
|
+
"model": GENERATE_MODEL,
|
|
78
|
+
"estimated_tokens": len(spec_json + _ADR_SYSTEM) // 4,
|
|
79
|
+
"max_tokens": 2000,
|
|
80
|
+
"system_prompt_preview": _ADR_SYSTEM,
|
|
81
|
+
"user_prompt_preview": f"Generate ADR for: {spec.name}",
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
62
85
|
text = _generate_adr(spec, title=title, decision=decision)
|
|
63
86
|
|
|
64
87
|
if output:
|
{cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/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.3.0 → cloudwright_ai_cli-0.3.2}/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
|
|
|
@@ -19,19 +21,36 @@ def cost(
|
|
|
19
21
|
pricing_tier: Annotated[
|
|
20
22
|
str | None, typer.Option(help="Pricing tier (on_demand, reserved_1yr, reserved_3yr, spot)")
|
|
21
23
|
] = None,
|
|
24
|
+
workload_profile: Annotated[
|
|
25
|
+
str | None,
|
|
26
|
+
typer.Option(
|
|
27
|
+
"--workload-profile",
|
|
28
|
+
"-w",
|
|
29
|
+
help="Workload sizing profile (small, medium, large, enterprise). "
|
|
30
|
+
"Sets realistic defaults for request volumes, storage, node counts, and data transfer.",
|
|
31
|
+
),
|
|
32
|
+
] = None,
|
|
22
33
|
) -> None:
|
|
23
34
|
"""Show cost breakdown for an architecture spec."""
|
|
35
|
+
if workload_profile:
|
|
36
|
+
from cloudwright.cost import VALID_WORKLOAD_PROFILES
|
|
37
|
+
|
|
38
|
+
if workload_profile not in VALID_WORKLOAD_PROFILES:
|
|
39
|
+
console.print(
|
|
40
|
+
f"[red]Invalid workload profile:[/red] {workload_profile!r}. "
|
|
41
|
+
f"Choose from: {', '.join(sorted(VALID_WORKLOAD_PROFILES))}"
|
|
42
|
+
)
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
24
45
|
spec = ArchSpec.from_file(spec_file)
|
|
25
46
|
|
|
26
47
|
# Compute cost estimate if not present
|
|
27
48
|
if not spec.cost_estimate:
|
|
28
49
|
engine = CostEngine()
|
|
29
|
-
spec.cost_estimate = engine.estimate(spec)
|
|
30
|
-
|
|
31
|
-
if ctx.obj and ctx.obj.get("json"):
|
|
32
|
-
import json
|
|
50
|
+
spec.cost_estimate = engine.estimate(spec, workload_profile=workload_profile)
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
if is_json_mode(ctx):
|
|
53
|
+
emit_success(ctx, {"estimate": spec.cost_estimate.model_dump(exclude_none=True)})
|
|
35
54
|
return
|
|
36
55
|
|
|
37
56
|
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,40 @@ 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(
|
|
49
|
+
ctx,
|
|
50
|
+
{
|
|
51
|
+
"model": GENERATE_MODEL,
|
|
52
|
+
"estimated_tokens": len(system + description) // 4,
|
|
53
|
+
"max_tokens": 10000,
|
|
54
|
+
"system_prompt_preview": system[:200],
|
|
55
|
+
"user_prompt_preview": description,
|
|
56
|
+
"constraints": constraints.model_dump(exclude_none=True),
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
38
60
|
try:
|
|
39
61
|
architect = Architect()
|
|
40
62
|
except RuntimeError as e:
|
|
41
|
-
|
|
42
|
-
raise typer.Exit(1) from None
|
|
63
|
+
emit_error(ctx, e, action="Set ANTHROPIC_API_KEY or OPENAI_API_KEY")
|
|
43
64
|
|
|
44
65
|
with console.status("Designing architecture..."):
|
|
45
66
|
spec = architect.design(description, constraints=constraints)
|
|
46
|
-
# Set provider/region from CLI args if not overridden by LLM
|
|
47
67
|
if spec.provider == "aws" and provider != "aws":
|
|
48
68
|
spec = spec.model_copy(update={"provider": provider})
|
|
49
69
|
if spec.region == "us-east-1" and region != "us-east-1":
|
|
50
70
|
spec = spec.model_copy(update={"region": region})
|
|
51
71
|
|
|
52
|
-
if ctx
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
print(json.dumps(spec.model_dump(), default=str))
|
|
72
|
+
if is_json_mode(ctx):
|
|
73
|
+
emit_success(ctx, {"spec": spec.model_dump(exclude_none=True), "yaml": spec.to_yaml()})
|
|
56
74
|
return
|
|
57
75
|
|
|
58
76
|
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]"))
|
|
@@ -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.3.0 → cloudwright_ai_cli-0.3.2}/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,19 @@ 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
|
+
{
|
|
37
|
+
"rule": w.rule,
|
|
38
|
+
"severity": w.severity,
|
|
39
|
+
"component": w.component,
|
|
40
|
+
"message": w.message,
|
|
41
|
+
"recommendation": w.recommendation,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return
|
|
34
45
|
result = [
|
|
35
46
|
{
|
|
36
47
|
"rule": w.rule,
|
|
@@ -41,7 +52,7 @@ def lint(
|
|
|
41
52
|
}
|
|
42
53
|
for w in warnings
|
|
43
54
|
]
|
|
44
|
-
|
|
55
|
+
emit_success(ctx, {"warnings": result})
|
|
45
56
|
else:
|
|
46
57
|
if not warnings:
|
|
47
58
|
console.print(f"[green][PASS][/green] No anti-patterns detected in {spec.name}")
|
|
@@ -0,0 +1,33 @@
|
|
|
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 Install: pip install cloudwright-ai-mcp"
|
|
25
|
+
)
|
|
26
|
+
raise typer.Exit(1) from None
|
|
27
|
+
|
|
28
|
+
tool_set: set[str] | None = None
|
|
29
|
+
if tools:
|
|
30
|
+
tool_set = {t.strip() for t in tools.split(",") if t.strip()}
|
|
31
|
+
|
|
32
|
+
server = create_server(tools=tool_set)
|
|
33
|
+
server.run(transport=transport)
|
{cloudwright_ai_cli-0.3.0 → cloudwright_ai_cli-0.3.2}/cloudwright_cli/commands/modify_cmd.py
RENAMED
|
@@ -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
|
-
|
|
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,24 @@ def modify(
|
|
|
43
43
|
try:
|
|
44
44
|
architect = Architect()
|
|
45
45
|
except RuntimeError as e:
|
|
46
|
-
|
|
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(
|
|
54
|
+
ctx,
|
|
55
|
+
{
|
|
56
|
+
"model": GENERATE_MODEL,
|
|
57
|
+
"estimated_tokens": len(spec_text + instruction) // 4,
|
|
58
|
+
"max_tokens": 8000,
|
|
59
|
+
"user_prompt_preview": f"Modify: {instruction}",
|
|
60
|
+
"constraints": {"spec_file": spec_file, "instruction": instruction},
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
49
64
|
with console.status("Applying modification..."):
|
|
50
65
|
modified = architect.modify(original, instruction)
|
|
51
66
|
|
|
@@ -60,15 +75,15 @@ def modify(
|
|
|
60
75
|
|
|
61
76
|
diff_result = Differ().diff(original_costed, modified_costed)
|
|
62
77
|
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
if is_json_mode(ctx):
|
|
79
|
+
emit_success(
|
|
80
|
+
ctx,
|
|
81
|
+
{
|
|
82
|
+
"original": original.model_dump(),
|
|
83
|
+
"modified": modified.model_dump(),
|
|
84
|
+
"diff": diff_result.model_dump(),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
72
87
|
return
|
|
73
88
|
|
|
74
89
|
console.print(Rule("[bold]Changes[/bold]"))
|
|
@@ -119,6 +134,7 @@ def modify(
|
|
|
119
134
|
|
|
120
135
|
if not dry_run:
|
|
121
136
|
out_path = Path(output) if output else spec_path
|
|
137
|
+
validate_output_path(out_path)
|
|
122
138
|
out_path.write_text(modified.to_yaml())
|
|
123
139
|
console.print(f"\n[green]Written to {out_path}[/green]")
|
|
124
140
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
86
86
|
if verbose:
|
|
87
|
-
|
|
87
|
+
err_console.print_exception()
|
|
88
88
|
raise typer.Exit(1)
|