cloudwright-ai-cli 0.2.27__tar.gz → 0.3.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.
Files changed (38) hide show
  1. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/PKG-INFO +1 -1
  2. cloudwright_ai_cli-0.3.0/cloudwright_cli/__init__.py +1 -0
  3. cloudwright_ai_cli-0.3.0/cloudwright_cli/commands/adr.py +193 -0
  4. cloudwright_ai_cli-0.3.0/cloudwright_cli/commands/security_cmd.py +98 -0
  5. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/main.py +4 -0
  6. cloudwright_ai_cli-0.2.27/cloudwright_cli/__init__.py +0 -1
  7. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/.gitignore +0 -0
  8. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/README.md +0 -0
  9. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/__main__.py +0 -0
  10. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/__init__.py +0 -0
  11. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/analyze_cmd.py +0 -0
  12. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/catalog_cmd.py +0 -0
  13. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/chat.py +0 -0
  14. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/compare.py +0 -0
  15. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/cost.py +0 -0
  16. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/databricks_cmd.py +0 -0
  17. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/design.py +0 -0
  18. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/diff.py +0 -0
  19. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/drift_cmd.py +0 -0
  20. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/export.py +0 -0
  21. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/import_cmd.py +0 -0
  22. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/init_cmd.py +0 -0
  23. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/lint_cmd.py +0 -0
  24. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/modify_cmd.py +0 -0
  25. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/policy.py +0 -0
  26. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/refresh_cmd.py +0 -0
  27. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/score_cmd.py +0 -0
  28. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/commands/validate.py +0 -0
  29. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/project.py +0 -0
  30. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/py.typed +0 -0
  31. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/cloudwright_cli/utils.py +0 -0
  32. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/pyproject.toml +0 -0
  33. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/tests/__init__.py +0 -0
  34. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/tests/test_cli.py +0 -0
  35. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/tests/test_drift_cmd.py +0 -0
  36. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/tests/test_init.py +0 -0
  37. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.0}/tests/test_modify_cmd.py +0 -0
  38. {cloudwright_ai_cli-0.2.27 → cloudwright_ai_cli-0.3.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.2.27
3
+ Version: 0.3.0
4
4
  Summary: CLI for Cloudwright architecture intelligence
5
5
  Project-URL: Homepage, https://github.com/xmpuspus/cloudwright
6
6
  Project-URL: Repository, https://github.com/xmpuspus/cloudwright
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,193 @@
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.utils import handle_error
10
+
11
+ console = Console()
12
+
13
+ _ADR_SYSTEM = """You generate Architecture Decision Records (ADRs) in MADR format.
14
+
15
+ Given an architecture spec as JSON, produce a markdown ADR with this exact structure:
16
+
17
+ # ADR: {name} — {key decision}
18
+
19
+ ## Status
20
+ Proposed
21
+
22
+ ## Context
23
+ {problem and why a decision is needed}
24
+
25
+ ## Decision
26
+ {the chosen architecture and key choices}
27
+
28
+ ## Components
29
+ | ID | Service | Provider | Purpose |
30
+ |---|---|---|---|
31
+ {component rows}
32
+
33
+ ## Consequences
34
+ ### Positive
35
+ - {benefits}
36
+
37
+ ### Negative
38
+ - {trade-offs and risks}
39
+
40
+ ## Alternatives Considered
41
+ {alternatives if any, otherwise note none documented}
42
+
43
+ ## Cost Estimate
44
+ {monthly cost if available, otherwise omit}
45
+
46
+ Be concise. Focus on the WHY, not just what the architecture contains.
47
+ Respond with ONLY the markdown — no explanation, no code fences."""
48
+
49
+
50
+ def adr(
51
+ ctx: typer.Context,
52
+ spec_file: Annotated[Path, typer.Argument(help="Path to ArchSpec YAML file", exists=True)],
53
+ output: Annotated[str | None, typer.Option("--output", "-o", help="Write ADR to this file")] = None,
54
+ title: Annotated[str | None, typer.Option("--title", help="ADR title (default: auto-generated)")] = None,
55
+ decision: Annotated[str | None, typer.Option("--decision", help="Specific decision to document")] = None,
56
+ ) -> None:
57
+ """Generate an Architecture Decision Record from an ArchSpec."""
58
+ try:
59
+ from cloudwright import ArchSpec
60
+
61
+ spec = ArchSpec.from_file(spec_file)
62
+ text = _generate_adr(spec, title=title, decision=decision)
63
+
64
+ if output:
65
+ Path(output).write_text(text)
66
+ console.print(f"[green]ADR written to {output}[/green]")
67
+ else:
68
+ print(text)
69
+
70
+ except typer.Exit:
71
+ raise
72
+ except Exception as e:
73
+ handle_error(ctx, e)
74
+
75
+
76
+ def _generate_adr(spec, *, title: str | None = None, decision: str | None = None) -> str:
77
+ try:
78
+ return _llm_adr(spec, title=title, decision=decision)
79
+ except Exception:
80
+ return _deterministic_adr(spec, title=title, decision=decision)
81
+
82
+
83
+ def _llm_adr(spec, *, title: str | None, decision: str | None) -> str:
84
+ from cloudwright.architect import Architect
85
+
86
+ arch = Architect()
87
+ spec_summary = spec.model_dump_json(indent=2, exclude_none=True)
88
+
89
+ decision_hint = f"\nDocument this specific decision: {decision}" if decision else ""
90
+ title_hint = f"\nUse this ADR title: {title}" if title else ""
91
+ prompt = f"Generate an ADR for this architecture:{title_hint}{decision_hint}\n\n{spec_summary}"
92
+
93
+ text, _ = arch.llm.generate([{"role": "user", "content": prompt}], _ADR_SYSTEM, max_tokens=2000)
94
+ if not text.strip().startswith("#"):
95
+ raise ValueError("LLM did not return markdown ADR")
96
+ return text.strip()
97
+
98
+
99
+ def _deterministic_adr(spec, *, title: str | None = None, decision: str | None = None) -> str:
100
+ adr_title = title or spec.name
101
+ key_decision = decision or _infer_key_decision(spec)
102
+
103
+ lines = [
104
+ f"# ADR: {adr_title} — {key_decision}",
105
+ "",
106
+ "## Status",
107
+ "Proposed",
108
+ "",
109
+ "## Context",
110
+ _build_context(spec),
111
+ "",
112
+ "## Decision",
113
+ _build_decision(spec),
114
+ "",
115
+ "## Components",
116
+ "| ID | Service | Provider | Purpose |",
117
+ "|---|---|---|---|",
118
+ ]
119
+
120
+ for c in spec.components:
121
+ purpose = c.description or c.label
122
+ lines.append(f"| {c.id} | {c.service} | {c.provider} | {purpose} |")
123
+
124
+ lines += ["", "## Consequences"]
125
+ lines += _build_consequences(spec)
126
+
127
+ rationale = spec.metadata.get("rationale") or []
128
+ if rationale:
129
+ lines += ["", "## Alternatives Considered"]
130
+ for r in rationale:
131
+ if isinstance(r, dict):
132
+ lines.append(f"- **{r.get('decision', '')}**: {r.get('reason', '')}")
133
+
134
+ if spec.cost_estimate:
135
+ lines += [
136
+ "",
137
+ "## Cost Estimate",
138
+ f"Estimated monthly cost: ${spec.cost_estimate.monthly_total:,.2f} USD",
139
+ ]
140
+
141
+ return "\n".join(lines)
142
+
143
+
144
+ def _infer_key_decision(spec) -> str:
145
+ rationale = spec.metadata.get("rationale") or []
146
+ if rationale and isinstance(rationale[0], dict):
147
+ return rationale[0].get("decision", f"{spec.provider.upper()} architecture")
148
+ return f"{spec.provider.upper()} architecture"
149
+
150
+
151
+ def _build_context(spec) -> str:
152
+ parts = [f"This architecture, {spec.name!r}, targets the {spec.provider.upper()} platform in region {spec.region}."]
153
+ if spec.constraints:
154
+ if spec.constraints.compliance:
155
+ parts.append(f"Compliance requirements: {', '.join(spec.constraints.compliance)}.")
156
+ if spec.constraints.budget_monthly:
157
+ parts.append(f"Monthly budget constraint: ${spec.constraints.budget_monthly:,.0f}.")
158
+ parts.append(f"It consists of {len(spec.components)} components across {len(spec.connections)} connections.")
159
+ return " ".join(parts)
160
+
161
+
162
+ def _build_decision(spec) -> str:
163
+ rationale = spec.metadata.get("rationale") or []
164
+ if rationale:
165
+ items = []
166
+ for r in rationale:
167
+ if isinstance(r, dict):
168
+ items.append(f"- **{r.get('decision', '')}**: {r.get('reason', '')}")
169
+ if items:
170
+ return "\n".join(items)
171
+
172
+ services = ", ".join(c.service for c in spec.components[:5])
173
+ suffix = ", ..." if len(spec.components) > 5 else ""
174
+ return f"Selected architecture using: {services}{suffix}."
175
+
176
+
177
+ def _build_consequences(spec) -> list[str]:
178
+ lines = ["### Positive"]
179
+ suggestions = spec.metadata.get("suggestions") or []
180
+
181
+ positives = [
182
+ f"Established {spec.provider.upper()} native services reduce operational overhead.",
183
+ f"{len(spec.components)} components provide clear separation of concerns.",
184
+ ]
185
+ lines += [f"- {p}" for p in positives]
186
+
187
+ lines += ["", "### Negative"]
188
+ negatives = ["Vendor lock-in to selected provider and service tier."]
189
+ if suggestions:
190
+ negatives.append("Additional configuration required: " + suggestions[0].lower() + ".")
191
+ lines += [f"- {n}" for n in negatives]
192
+
193
+ return lines
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.text import Text
10
+
11
+ from cloudwright_cli.utils import handle_error
12
+
13
+ console = Console()
14
+
15
+ _SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "none": 4}
16
+
17
+
18
+ def security_scan(
19
+ ctx: typer.Context,
20
+ spec_file: Annotated[Path, typer.Argument(help="Path to ArchSpec YAML file", exists=True)],
21
+ fail_on: Annotated[str, typer.Option("--fail-on", help="Fail on: critical, high, medium, none")] = "high",
22
+ output: Annotated[str | None, typer.Option("--output", "-o")] = None,
23
+ ) -> None:
24
+ """Scan an ArchSpec for security anti-patterns and misconfigurations."""
25
+ try:
26
+ from cloudwright import ArchSpec
27
+ from cloudwright.security import SecurityScanner
28
+
29
+ spec = ArchSpec.from_file(spec_file)
30
+ report = SecurityScanner().scan(spec)
31
+
32
+ if ctx.obj and ctx.obj.get("json"):
33
+ result = {
34
+ "passed": report.passed,
35
+ "findings": [
36
+ {
37
+ "severity": f.severity,
38
+ "rule": f.rule,
39
+ "component_id": f.component_id,
40
+ "message": f.message,
41
+ "remediation": f.remediation,
42
+ }
43
+ for f in report.findings
44
+ ],
45
+ }
46
+ print(json.dumps(result, indent=2))
47
+ _maybe_exit(report, fail_on)
48
+ return
49
+
50
+ console.print(f"\nSecurity Scan: {spec_file.name}\n")
51
+
52
+ if not report.findings:
53
+ console.print("[green][PASS][/green] No security findings detected.")
54
+ else:
55
+ for f in report.findings:
56
+ sev_upper = f.severity.upper()
57
+ if f.severity == "critical":
58
+ sev_text = Text(f"[{sev_upper}]", style="bold red")
59
+ elif f.severity == "high":
60
+ sev_text = Text(f"[{sev_upper}]", style="red")
61
+ elif f.severity == "medium":
62
+ sev_text = Text(f"[{sev_upper}]", style="yellow")
63
+ else:
64
+ sev_text = Text(f"[{sev_upper}]", style="dim")
65
+
66
+ line = Text(" ")
67
+ line.append_text(sev_text)
68
+ line.append(f" {f.message}")
69
+ console.print(line)
70
+ console.print(f" Remediation: {f.remediation}", style="dim")
71
+ console.print()
72
+
73
+ total = len(report.findings)
74
+ crit = report.critical_count
75
+ high = report.high_count
76
+ med = sum(1 for f in report.findings if f.severity == "medium")
77
+
78
+ console.print(f"{total} finding(s) ({crit} critical, {high} high, {med} medium)")
79
+
80
+ threshold = _SEVERITY_ORDER.get(fail_on, 1)
81
+ worst = min((_SEVERITY_ORDER.get(f.severity, 4) for f in report.findings), default=4)
82
+ status = "PASSED" if worst > threshold else "FAILED"
83
+ style = "green" if status == "PASSED" else "red"
84
+ console.print(f"Status: [{style}]{status}[/{style}] (fail-on={fail_on})")
85
+
86
+ _maybe_exit(report, fail_on)
87
+
88
+ except typer.Exit:
89
+ raise
90
+ except Exception as e:
91
+ handle_error(ctx, e)
92
+
93
+
94
+ def _maybe_exit(report, fail_on: str) -> None:
95
+ threshold = _SEVERITY_ORDER.get(fail_on, 1)
96
+ for f in report.findings:
97
+ if _SEVERITY_ORDER.get(f.severity, 4) <= threshold:
98
+ raise typer.Exit(1)
@@ -1,6 +1,7 @@
1
1
  import typer
2
2
 
3
3
  from cloudwright_cli import __version__
4
+ from cloudwright_cli.commands.adr import adr
4
5
  from cloudwright_cli.commands.analyze_cmd import analyze
5
6
  from cloudwright_cli.commands.catalog_cmd import catalog_app
6
7
  from cloudwright_cli.commands.chat import chat
@@ -18,6 +19,7 @@ from cloudwright_cli.commands.modify_cmd import modify
18
19
  from cloudwright_cli.commands.policy import policy
19
20
  from cloudwright_cli.commands.refresh_cmd import refresh
20
21
  from cloudwright_cli.commands.score_cmd import score
22
+ from cloudwright_cli.commands.security_cmd import security_scan
21
23
  from cloudwright_cli.commands.validate import validate
22
24
 
23
25
 
@@ -65,4 +67,6 @@ app.command()(analyze)
65
67
  app.command()(refresh)
66
68
  app.command()(lint)
67
69
  app.command()(databricks_validate)
70
+ app.command(name="security")(security_scan)
71
+ app.command(name="adr")(adr)
68
72
  app.add_typer(catalog_app, name="catalog")
@@ -1 +0,0 @@
1
- __version__ = "0.2.27"