import { spawn } from 'node:child_process'; import { mkdirSync, readFileSync } from 'node:fs'; import { Database } from 'bun:sqlite'; import { createServer } from 'node:net'; import { dirname, join } from 'node:path'; import { buildLocalDevConfig, resolveSqlitePath } from './dev-env'; type DrizzleJournal = { entries: Array<{ tag: string }>; }; type ExitResult = { code: number | null; signal: NodeJS.Signals | null; }; function trim(value: string | undefined) { const candidate = value?.trim(); return candidate ? candidate : undefined; } async function isPortAvailable(port: number, host: string) { return await new Promise((resolve) => { const server = createServer(); server.once('error', () => resolve(false)); server.listen(port, host, () => { server.close(() => resolve(true)); }); }); } async function pickLocalPort(host: string) { const candidatePorts = [3000, 3001, 3002, 3100, 3200, 3300]; for (const port of candidatePorts) { if (await isPortAvailable(port, host)) { return String(port); } } throw new Error(`Unable to find an open local dev port from: ${candidatePorts.join(', ')}`); } function hasTable(database: Database, tableName: string) { const row = database .query('SELECT name FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1') .get('table', tableName) as { name: string } | null; return row !== null; } function readMigrationFiles() { const journal = JSON.parse( readFileSync(join(process.cwd(), 'drizzle', 'meta', '_journal.json'), 'utf8') ) as DrizzleJournal; return journal.entries.map((entry) => join(process.cwd(), 'drizzle', `${entry.tag}.sql`)); } function bootstrapFreshDatabase(databaseUrl: string) { const databasePath = resolveSqlitePath(databaseUrl); if (!databasePath || databasePath === ':memory:') { return false; } mkdirSync(dirname(databasePath), { recursive: true }); const database = new Database(databasePath, { create: true }); try { database.exec('PRAGMA foreign_keys = ON;'); const existingCoreTables = [ 'user', 'filing', 'watchlist_item', 'filing_statement_snapshot', 'filing_taxonomy_snapshot', 'task_run' ]; if (existingCoreTables.some((tableName) => hasTable(database, tableName))) { return false; } for (const migrationFile of readMigrationFiles()) { database.exec(readFileSync(migrationFile, 'utf8')); } return true; } finally { database.close(); } } function exitFromResult(result: ExitResult) { if (result.signal) { process.exit(result.signal === 'SIGINT' || result.signal === 'SIGTERM' ? 0 : 1); return; } process.exit(result.code ?? 0); } const explicitPort = trim(process.env.PORT) || trim(process.env.APP_PORT); const bindHost = trim(process.env.HOSTNAME) || trim(process.env.HOST) || '127.0.0.1'; const resolvedPort = explicitPort || await pickLocalPort(bindHost); const config = buildLocalDevConfig({ ...process.env, HOSTNAME: bindHost, PORT: resolvedPort }); const env = { ...config.env } as NodeJS.ProcessEnv; delete env.NO_COLOR; const databasePath = resolveSqlitePath(env.DATABASE_URL ?? ''); if (databasePath && databasePath !== ':memory:') { mkdirSync(dirname(databasePath), { recursive: true }); } mkdirSync(env.WORKFLOW_LOCAL_DATA_DIR ?? '.workflow-data', { recursive: true }); const initializedDatabase = bootstrapFreshDatabase(env.DATABASE_URL ?? ''); console.info(`[dev] local origin ${config.publicOrigin}`); console.info(`[dev] sqlite ${env.DATABASE_URL}`); console.info(`[dev] workflow ${env.WORKFLOW_TARGET_WORLD} (${env.WORKFLOW_LOCAL_DATA_DIR})`); if (!explicitPort && resolvedPort !== '3000') { console.info(`[dev] port 3000 is busy, using http://localhost:${resolvedPort} instead`); } if (initializedDatabase) { console.info('[dev] initialized the local SQLite schema from drizzle SQL files'); } if (config.overrides.authOriginChanged) { console.info('[dev] forcing Better Auth origin/trusted origins to the local origin'); } if (config.overrides.apiBaseChanged) { console.info('[dev] forcing NEXT_PUBLIC_API_URL to same-origin for local dev'); } if (config.overrides.databaseChanged) { console.info('[dev] using a local SQLite database instead of the deployment path'); } if (config.overrides.workflowChanged) { console.info('[dev] forcing Workflow to the local runtime for local dev'); } if (config.overrides.secretFallbackUsed) { console.info('[dev] using the built-in local Better Auth secret because BETTER_AUTH_SECRET is unset or still a placeholder'); } const child = spawn( 'bun', ['--bun', 'next', 'dev', '--turbopack', '--hostname', config.bindHost, '--port', config.port], { env, stdio: 'inherit' } ); function forwardSignal(signal: NodeJS.Signals) { if (!child.killed) { child.kill(signal); } } process.on('SIGINT', () => forwardSignal('SIGINT')); process.on('SIGTERM', () => forwardSignal('SIGTERM')); child.on('exit', (code, signal) => { exitFromResult({ code, signal }); });