mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
* refactor(dev): remove the minimal-registry escape hatch `dev:minimal` existed because the tool registry was 71-82% of every workspace route's module graph and aliasing it away was the only way to make dev bearable. The metadata work removed that reason, so the hatch now buys almost nothing: before this stack 31.7s -> 20.0s cold (-37%) after this stack 22.9s -> 20.7s cold (-10%) A 10% cold-compile win, on the run that happens once — restarts are ~4.2s either way — is not worth what it costs. `tools/registry.minimal.ts` and `blocks/registry-maps.minimal.ts` are 283 lines of hand-curated duplicates of the real registries that **nothing keeps in sync** (no lint, no CI check, no test); they are correct today only because someone remembered. And the mode is actively misleading: it silently drops ~250 services and ~280 blocks, so anything reproduced under it may not reproduce for real. Removes both files, the `SIM_DEV_MINIMAL_REGISTRY` branch from `next.config.ts` (including the whole `webpack()` hook, which existed only for this), and the `dev:minimal` / `dev:full:minimal-registry` scripts. Verified after removal: `tsc` clean, boundary + metadata + skills + monorepo gates pass, and `next dev` starts and serves the canvas at 22.6s cold / HTTP 200. * fix(setup): stop the wizard offering the removed minimal-registry mode The setup wizard prompted for a dev server on machines under 16GB and **defaulted** to `dev:full:minimal-registry` — a script this stack deletes. Anyone running `bun run setup` on a low-RAM machine would have accepted the default and hit "Script not found", which is exactly the contributor the mode existed to help. Repointed at `dev:full:capped`, which still exists and caps Node at 4GB without dropping ~250 integrations — a strictly better answer to the same question. The hints were also stale: they warned the full registry "can use 4-5GB+ on its own", which was true when a dev server sat at 11.5GB. It now sits at ~4GB, so they say that instead. Missed by an earlier sweep because the pattern searched for `dev:minimal` and `registry.minimal`, and this string is `dev:full:minimal-registry` — the two halves reversed. Re-swept across every file type for all spellings: zero references remain. Also audited every script value the wizard can return, so the class of bug is checked, not just this instance.
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
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,
|
|
}
|
|
}
|