-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathdev.ts
More file actions
186 lines (176 loc) · 6.65 KB
/
Copy pathdev.ts
File metadata and controls
186 lines (176 loc) · 6.65 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
import { spawnSync } from 'node:child_process'
import path from 'node:path'
import { truncate } from '@sim/utils/string'
import { resolveDatabase } from '../db.ts'
import type { Detection } from '../detect.ts'
import { ROOT, readEnvFile, writeEnvValues } from '../env-files.ts'
import { SetupError } from '../errors.ts'
import { pgProbe } from '../probes.ts'
import * as p from '../prompter.ts'
import { ensureRedis, resolveRedis } from '../redis.ts'
import {
chatFlagValues,
collectSecrets,
mothershipOverride,
promptCopilotKey,
promptEmail,
promptLlmKeys,
promptSecurity,
promptSignInProviders,
promptStorage,
promptUnlocks,
} from '../steps.ts'
import { glyph, theme } from '../theme.ts'
const APP_URL = 'http://localhost:3000'
/**
* A migrate failure on a never-migrated database means setup failed — abort.
* On a database that already has applied migrations (a live but drifted dev
* DB), the failure is surfaced and the user decides whether to continue.
*/
async function runMigrations(dsn: string): Promise<void> {
const spin = p.spinner()
spin.start('Running database migrations…')
const result = spawnSync('bun', ['run', 'db:migrate'], {
cwd: path.join(ROOT, 'packages/db'),
encoding: 'utf8',
})
if (result.status === 0) {
spin.stop('Migrations applied')
return
}
spin.stop(`${glyph.fail} migrations failed`)
const error = truncate(`${result.stdout}\n${result.stderr}`.trim(), 2000)
const probe = await pgProbe(dsn)
const applied = probe.ok ? (probe.migrations?.applied ?? 0) : 0
if (applied === 0) {
throw new SetupError(`db:migrate failed on a fresh database:\n${error}`, [
`run it by hand to see the full output: ${theme.command('cd packages/db && bun run db:migrate')}`,
'check DATABASE_URL points at the database you expect',
])
}
p.log.warn(
`db:migrate failed, but this database already has ${applied} applied migrations — it may have schema drift (e.g. built with db:push).`
)
p.log.info(theme.muted(truncate(error, 600)))
const proceed = await p.confirm({
message: 'Continue setup without migrating? (doctor will keep flagging the drift)',
initialValue: true,
})
if (!proceed) throw new Error(`aborted: db:migrate failed:\n${error}`)
}
async function promptRedis(detection: Detection, existing?: string): Promise<string | null> {
const wants = await p.confirm({
message:
'Configure Redis? (powers live Chat status and table events; storage falls back to Postgres)',
initialValue: true,
})
if (!wants) return null
return resolveRedis(detection, existing)
}
async function promptTrigger(): Promise<Record<string, string> | null> {
const wants = await p.confirm({
message: 'Enable Trigger.dev for background jobs? (off = jobs run via the DB queue)',
initialValue: false,
})
if (!wants) return null
const secretKey = await p.password({
message: 'TRIGGER_SECRET_KEY',
validate: (v) => (v ? undefined : 'required'),
})
const projectId = await p.text({
message: 'TRIGGER_PROJECT_ID',
validate: (v) => (v ? undefined : 'required'),
})
return {
TRIGGER_DEV_ENABLED: 'true',
TRIGGER_SECRET_KEY: secretKey,
TRIGGER_PROJECT_ID: projectId,
}
}
export async function runDevMode(
detection: Detection,
quick: boolean
): Promise<{ startNow: boolean; script: string }> {
const sim = readEnvFile('sim')
const dsn = await resolveDatabase(detection, sim.vars.get('DATABASE_URL'))
const secrets = collectSecrets(sim)
const shared = {
DATABASE_URL: dsn,
BETTER_AUTH_SECRET: secrets.BETTER_AUTH_SECRET,
INTERNAL_API_SECRET: secrets.INTERNAL_API_SECRET,
BETTER_AUTH_URL: APP_URL,
NEXT_PUBLIC_APP_URL: APP_URL,
}
writeEnvValues('sim', {
...shared,
ENCRYPTION_KEY: secrets.ENCRYPTION_KEY,
API_ENCRYPTION_KEY: secrets.API_ENCRYPTION_KEY,
})
writeEnvValues('realtime', shared)
writeEnvValues('db', { DATABASE_URL: dsn })
p.log.step('Wrote apps/sim/.env, apps/realtime/.env, packages/db/.env (shared subset mirrored)')
await runMigrations(dsn)
const simAfter = readEnvFile('sim')
const values: Record<string, string> = {}
// Before the key is minted: a half-set override mints against one environment
// and validates against the other, and warning afterwards is too late — the
// bad key is already stored, and the next run offers to keep it.
Object.assign(values, mothershipOverride())
const copilotKey = await promptCopilotKey(simAfter.vars.get('COPILOT_API_KEY'))
if (copilotKey) values.COPILOT_API_KEY = copilotKey
Object.assign(values, chatFlagValues(copilotKey))
Object.assign(values, await promptLlmKeys(detection, !quick))
// Redis is set up in every mode, quick included. Storage falls back to
// PostgreSQL without it, but the pub/sub channels (live Chat task-status,
// table events) have no fallback — skipping it silently produces an install
// where live updates never arrive. Quick configures it with no questions at
// all; custom keeps the opt-out and the where-should-it-live ladder.
const redisUrl = quick
? await ensureRedis(detection, simAfter.vars.get('REDIS_URL'))
: await promptRedis(detection, simAfter.vars.get('REDIS_URL'))
if (redisUrl) {
values.REDIS_URL = redisUrl
writeEnvValues('realtime', { REDIS_URL: redisUrl })
}
if (!quick) {
const trigger = await promptTrigger()
if (trigger) Object.assign(values, trigger)
const storage = await promptStorage(simAfter.vars, false)
if (storage) Object.assign(values, storage)
Object.assign(values, await promptSignInProviders(simAfter.vars, APP_URL))
Object.assign(values, await promptEmail(simAfter.vars))
const security = await promptSecurity(simAfter.vars)
Object.assign(values, security.sim)
if (Object.keys(security.mirrorToRealtime).length > 0) {
writeEnvValues('realtime', security.mirrorToRealtime)
}
Object.assign(values, await promptUnlocks(simAfter.vars))
}
if (Object.keys(values).length > 0) writeEnvValues('sim', values)
let script = 'dev:full'
if (detection.specs.hostMemGb < 16) {
script = await p.select({
message: `Low RAM detected (${detection.specs.hostMemGb}GB) — which dev server?`,
options: [
{
value: 'dev:full:capped',
label: 'Capped heap (recommended)',
hint: 'caps Node at 4GB — every integration still available',
},
{
value: 'dev:full',
label: 'Uncapped',
hint: 'lets the dev server take what it needs (~4GB typical)',
},
],
initialValue: 'dev:full:capped',
})
}
return {
startNow: await p.confirm({
message: `Start Sim now? (bun run ${script})`,
initialValue: true,
}),
script,
}
}