BoilerWorks

boilerworks / boilerworks / wizard.py
Blame History Raw 213 lines
1
"""Interactive setup wizard — collects answers, writes boilerworks.yaml."""
2
3
from __future__ import annotations
4
5
import re
6
import sys
7
from pathlib import Path
8
9
import questionary
10
from rich.panel import Panel
11
12
from boilerworks.console import console, print_success
13
from boilerworks.manifest import BoilerworksManifest, DataConfig, ServicesConfig, TestingConfig
14
from boilerworks.registry import Registry, TemplateInfo
15
16
_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$")
17
18
_SELECTION_GUIDE = """[bold]How to pick a template size:[/bold]
19
20
[cyan]Full[/cyan] — Apps with users (login, permissions, org management)
21
[magenta]Micro[/magenta] — API-key services, microservices, workers
22
[blue]Edge[/blue] — Cloudflare Workers / Pages, globally distributed
23
24
[bold]When to choose Full vs Micro:[/bold]
25
If it has users logging in → Full
26
If it's a service with API keys → Micro
27
If it needs global edge scale → Edge
28
"""
29
30
31
def _validate_slug(text: str) -> bool | str:
32
if not text:
33
return "Project name is required"
34
if not _SLUG_RE.match(text):
35
return "Must be lowercase, start with a letter, letters/digits/hyphens only (e.g. my-project)"
36
return True
37
38
39
def _template_choices(templates: list[TemplateInfo]) -> list[questionary.Choice]:
40
choices = []
41
current_lang = None
42
for t in templates:
43
if t.language != current_lang:
44
current_lang = t.language
45
choices.append(questionary.Separator(f"── {t.language} ──"))
46
status_icon = {"done": "●", "building": "◐", "planned": "○"}.get(t.status, "?")
47
label = f"{status_icon} {t.name:<24} {t.description}"
48
choices.append(questionary.Choice(title=label, value=t.name))
49
return choices
50
51
52
def run_wizard(output_path: str | Path = "boilerworks.yaml") -> None:
53
"""Walk through the 13-step wizard and write boilerworks.yaml."""
54
registry = Registry()
55
console.print(
56
Panel(
57
"[bold cyan]Boilerworks Setup Wizard[/bold cyan]\nAnswers are saved to [bold]boilerworks.yaml[/bold]",
58
border_style="cyan",
59
)
60
)
61
62
# ── Step 1: Project name ─────────────────────────────────────────────────
63
project = questionary.text(
64
"Project name (slug format, e.g. my-app):",
65
validate=_validate_slug,
66
).ask()
67
if project is None:
68
console.print("[yellow]Cancelled.[/yellow]")
69
sys.exit(0)
70
71
# ── Step 2: Template size ─────────────────────────────────────────────────
72
console.print(Panel(_SELECTION_GUIDE, title="Template Guide", border_style="dim"))
73
size = questionary.select(
74
"Template size:",
75
choices=["full", "micro", "edge"],
76
).ask()
77
if size is None:
78
console.print("[yellow]Cancelled.[/yellow]")
79
sys.exit(0)
80
81
# ── Step 3: Template family ───────────────────────────────────────────────
82
filtered = registry.filter_by_size(size)
83
family = questionary.select(
84
"Template family:",
85
choices=_template_choices(filtered),
86
).ask()
87
if family is None:
88
console.print("[yellow]Cancelled.[/yellow]")
89
sys.exit(0)
90
91
template = registry.get_by_name(family)
92
93
# ── Step 4: Topology ──────────────────────────────────────────────────────
94
available_topologies = template.topologies if template else ["standard"]
95
if len(available_topologies) > 1:
96
topology = questionary.select(
97
"Deployment topology:",
98
choices=available_topologies,
99
).ask()
100
else:
101
topology = available_topologies[0]
102
console.print(f" [dim]Topology: {topology} (only option for this template)[/dim]")
103
if topology is None:
104
topology = "standard"
105
106
# ── Step 5: Cloud provider ────────────────────────────────────────────────
107
cloud_answer = questionary.select(
108
"Cloud provider:",
109
choices=["aws", "gcp", "azure", "none"],
110
).ask()
111
cloud = cloud_answer if cloud_answer != "none" else None
112
113
# ── Step 5b: Ops (infra-as-code) ─────────────────────────────────────────
114
ops = False
115
if cloud:
116
ops = (
117
questionary.confirm(
118
"Include infrastructure-as-code (boilerworks-opscode)?",
119
default=True,
120
).ask()
121
or False
122
)
123
124
# ── Step 6: Region ────────────────────────────────────────────────────────
125
region: str | None = None
126
if cloud:
127
region = questionary.text(
128
f"Region (e.g. {'us-east-1' if cloud == 'aws' else 'us-central1' if cloud == 'gcp' else 'eastus'}):",
129
).ask()
130
if not region:
131
region = None
132
133
# ── Step 7: Domain ────────────────────────────────────────────────────────
134
domain_answer = questionary.text("Domain (optional, e.g. myapp.com):").ask()
135
domain = domain_answer.strip() if domain_answer and domain_answer.strip() else None
136
137
# ── Step 8 & 9: Mobile / Web presence (Full only) ────────────────────────
138
mobile = False
139
web_presence = False
140
if size == "full":
141
mobile = questionary.confirm("Include mobile app template?", default=False).ask() or False
142
web_presence = questionary.confirm("Include web presence / marketing site?", default=False).ask() or False
143
144
# ── Step 10: Compliance ───────────────────────────────────────────────────
145
compliance_answer = questionary.checkbox(
146
"Compliance requirements:",
147
choices=["soc2", "hipaa", "pci-dss", "gdpr", "none"],
148
).ask()
149
compliance = [c for c in (compliance_answer or []) if c != "none"]
150
151
# ── Step 11: Email provider ───────────────────────────────────────────────
152
email_answer = questionary.select(
153
"Email provider:",
154
choices=["ses", "sendgrid", "mailgun", "none"],
155
).ask()
156
email = email_answer if email_answer != "none" else None
157
158
# ── Step 12: E2E testing ──────────────────────────────────────────────────
159
e2e_answer = questionary.select(
160
"End-to-end testing framework:",
161
choices=["playwright", "cypress", "none"],
162
).ask()
163
e2e = e2e_answer if e2e_answer != "none" else None
164
165
# ── Step 13: Summary + confirm ────────────────────────────────────────────
166
summary_lines = [
167
f" [bold]Project:[/bold] {project}",
168
f" [bold]Template:[/bold] {family} ({size})",
169
f" [bold]Topology:[/bold] {topology}",
170
f" [bold]Cloud:[/bold] {cloud or 'none'}",
171
f" [bold]Ops:[/bold] {'yes' if ops else 'no'}",
172
f" [bold]Region:[/bold] {region or '—'}",
173
f" [bold]Domain:[/bold] {domain or '—'}",
174
]
175
if size == "full":
176
summary_lines += [
177
f" [bold]Mobile:[/bold] {'yes' if mobile else 'no'}",
178
f" [bold]Web:[/bold] {'yes' if web_presence else 'no'}",
179
]
180
summary_lines += [
181
f" [bold]Compliance:[/bold] {', '.join(compliance) if compliance else 'none'}",
182
f" [bold]Email:[/bold] {email or 'none'}",
183
f" [bold]E2E:[/bold] {e2e or 'none'}",
184
]
185
console.print(Panel("\n".join(summary_lines), title="[bold]Summary[/bold]", border_style="green"))
186
187
confirmed = questionary.confirm("Write boilerworks.yaml?", default=True).ask()
188
if not confirmed:
189
console.print("[yellow]Cancelled — nothing written.[/yellow]")
190
sys.exit(0)
191
192
manifest = BoilerworksManifest(
193
project=project,
194
family=family,
195
size=size,
196
topology=topology,
197
cloud=cloud,
198
ops=ops,
199
region=region,
200
domain=domain,
201
mobile=mobile,
202
web_presence=web_presence,
203
compliance=compliance,
204
services=ServicesConfig(email=email),
205
data=DataConfig(),
206
testing=TestingConfig(e2e=e2e),
207
)
208
209
out = Path(output_path)
210
manifest.to_file(out)
211
print_success(f"Written to [bold]{out}[/bold]")
212
console.print("\n[dim]Next:[/dim] [bold]boilerworks init[/bold]")
213

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button