-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathwizard.ts
More file actions
193 lines (178 loc) · 7.08 KB
/
Copy pathwizard.ts
File metadata and controls
193 lines (178 loc) · 7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { spawnSync } from 'node:child_process'
import { showBanner } from './banner.ts'
import { loadCheckContext, runChecks } from './checks.ts'
import { type Detection, runDetection } from './detect.ts'
import { archiveEnvFile, ROOT } from './env-files.ts'
import { runComposeMode } from './modes/compose.ts'
import { runDevMode } from './modes/dev.ts'
import { runK8sMode } from './modes/k8s.ts'
import { ensurePortsFree } from './ports.ts'
import * as p from './prompter.ts'
import { glyph, theme } from './theme.ts'
export type WizardMode = 'compose' | 'dev' | 'k8s'
export interface WizardFlags {
quick: boolean
mode?: WizardMode
}
async function handleExistingConfig(detection: Detection): Promise<'continue' | 'doctor'> {
// Root counts: a compose install writes only `.env`, so excluding it made a
// configured machine look unconfigured and silently re-run from scratch.
const present = Object.entries(detection.envFiles)
.filter(([, exists]) => exists)
.map(([target]) => (target === 'root' ? '.env' : `${target}/.env`))
if (present.length === 0) return 'continue'
const choice = await p.select({
message: `Found existing config (${present.join(', ')}) — what should we do?`,
options: [
{ value: 'keep', label: 'Keep it', hint: 'run doctor against the current setup and exit' },
{
value: 'review',
label: 'Review and update',
hint: 'walk the wizard with current values prefilled',
},
{ value: 'reset', label: 'Reset', hint: 'archive current .env files and start fresh' },
],
initialValue: 'keep',
})
if (choice === 'keep') return 'doctor'
if (choice === 'reset') {
for (const target of ['sim', 'realtime', 'db', 'root'] as const) {
const backup = archiveEnvFile(target)
if (backup) p.log.step(`Archived ${backup}`)
}
}
return 'continue'
}
const LOW_DOCKER_MEM_GB = 6
const LOW_DISK_GB = 15
async function selectMode(detection: Detection, flags: WizardFlags): Promise<WizardMode> {
if (flags.mode) return flags.mode
const { dockerMemGb } = detection.specs
const vm = dockerMemGb !== null ? ` · VM ${dockerMemGb}GB` : ''
const composeState = detection.dockerRunning ? `Docker ready${vm}` : 'Docker is NOT running'
const k8sState = detection.kubeContext
? `context: ${detection.kubeContext}${vm}`
: detection.binaries.kind
? `kind available${vm}`
: 'needs kind or Docker Desktop k8s'
return p.select({
message: 'How do you want to run Sim?',
options: [
{
value: 'compose',
label: 'Docker Compose',
hint: `Run bundled Sim, fastest way to start · ${composeState}`,
},
{
value: 'dev',
label: 'Local dev (bun run dev:full)',
hint: 'Work on Sim itself · app :3000 + realtime :3002',
},
{
value: 'k8s',
label: 'Kubernetes (helm)',
hint: `Test a production-style k8s deploy · ${k8sState}`,
},
],
// Always Compose: it is the fastest path and the one most people want. A
// stopped Docker is not a reason to steer them to a source checkout — the
// compose path offers to start Docker Desktop itself.
initialValue: 'compose',
})
}
/** Spec warnings before committing to a mode — informed choice, no silent degradation. */
function warnLowSpecs(detection: Detection, mode: WizardMode): void {
const { dockerMemGb, freeDiskGb } = detection.specs
if (
(mode === 'compose' || mode === 'k8s') &&
dockerMemGb !== null &&
dockerMemGb < LOW_DOCKER_MEM_GB
) {
p.log.warn(
`Docker's VM has only ${dockerMemGb}GB of memory${mode === 'k8s' ? ' — pods will likely sit Pending or OOM' : ' — containers may OOM'}. Raise it in Docker Desktop → Settings → Resources (8GB+ recommended).`
)
}
if ((mode === 'compose' || mode === 'k8s') && freeDiskGb !== null && freeDiskGb < LOW_DISK_GB) {
p.log.warn(`Only ${freeDiskGb}GB of free disk — image pulls need 5-10GB.`)
}
}
async function finalVerify(): Promise<void> {
const findings = await runChecks(loadCheckContext(true), ['live'])
for (const finding of findings) {
console.log(` ${glyph[finding.status]} ${finding.message}`)
}
}
export async function runWizard(flags: WizardFlags): Promise<void> {
// Detection is ~600ms of subprocess work and the banner is ~600ms of animation
// with nothing to do — overlap them so the spinner below usually resolves at once.
const detecting = runDetection()
await showBanner()
const spin = p.spinner()
spin.start('Looking at what you already have…')
const detection = await detecting
spin.stop(
`Detected: docker ${detection.dockerRunning ? '✓' : '✗'} · postgres ${detection.postgresPortOpen ? '✓' : '✗'} · ` +
`${detection.shellLlmKeys.length} shell LLM key${detection.shellLlmKeys.length === 1 ? '' : 's'}` +
(detection.kubeContext ? ` · kube: ${detection.kubeContext}` : '')
)
if ((await handleExistingConfig(detection)) === 'doctor') {
const { runDoctor } = await import('./doctor.ts')
process.exitCode = await runDoctor({ fix: false, json: false })
return
}
const quick =
flags.quick ||
(await p.select({
message: 'Setup style?',
options: [
{ value: 'quick', label: 'Quick', hint: 'sensible defaults, minimal questions' },
{
value: 'custom',
label: 'Custom',
hint: 'every option: redis, trigger.dev, image variants',
},
],
initialValue: 'quick',
})) === 'quick'
const mode = await selectMode(detection, flags)
warnLowSpecs(detection, mode)
let startDevNow = false
let devScript = 'dev:full'
if (mode === 'compose') await runComposeMode(detection, quick)
else if (mode === 'dev') {
const dev = await runDevMode(detection, quick)
startDevNow = dev.startNow
devScript = dev.script
} else await runK8sMode(detection)
if (mode !== 'k8s' && !startDevNow) await finalVerify()
const url = 'http://localhost:3000'
p.note(
[
mode === 'k8s' ? `port-forward, then open ${url}` : `open ${url}`,
'manage it: bun run sim start · stop · status · logs',
'check your setup: bun run sim doctor',
mode === 'dev' && !startDevNow ? `start Sim: bun run ${devScript}` : null,
`prefer a bare "sim"? ${theme.command('bun link')} once (needs ~/.bun/bin on PATH)`,
]
.filter(Boolean)
.join('\n'),
'Next steps'
)
p.outro(theme.accent('Sim is ready.'))
if (mode === 'compose' && process.platform === 'darwin') {
spawnSync('open', [url], { stdio: 'ignore' })
}
if (startDevNow) {
// dev:full binds 3000 (app) and 3002 (realtime) — resolve any conflict
// (e.g. another worktree's dev server) before starting, instead of spawning
// a server that fails to bind. If the user leaves the ports, skip the start.
if (await ensurePortsFree([3000, 3002])) {
const child = spawnSync('bun', ['run', devScript], { cwd: ROOT, stdio: 'inherit' })
process.exitCode = child.status ?? 0
} else {
p.log.warn(
`Ports still in use — Sim wasn't started. Free them, then run ${theme.command(`bun run ${devScript}`)}.`
)
}
}
}