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 { ensureFinancialIngestionSchemaHealthy, resolveFinancialSchemaRepairMode, } from "../lib/server/db/financial-ingestion-schema"; import { ensureLocalSqliteSchema } from "../lib/server/db/sqlite-schema-compat"; import { buildLocalDevConfig, resolveSqlitePath } from "./dev-env"; import { applyLocalSqliteVectorEnv } from "./sqlite-vector-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) { try { const row = database .query("SELECT name FROM sqlite_master WHERE type='table' AND name = ?") .get(tableName) as { name: string } | null; return row !== null; } catch (e) { return false; } } 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", "company_financial_bundle", ]; 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 initialEnv = { ...config.env, } as NodeJS.ProcessEnv; const { config: sqliteVectorConfig, env } = applyLocalSqliteVectorEnv(initialEnv); 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 ?? ""); if (!initializedDatabase && databasePath && databasePath !== ":memory:") { const client = new Database(databasePath, { create: true }); try { client.exec("PRAGMA foreign_keys = ON;"); ensureLocalSqliteSchema(client); const repairResult = ensureFinancialIngestionSchemaHealthy(client, { mode: resolveFinancialSchemaRepairMode(env.FINANCIAL_SCHEMA_REPAIR_MODE), }); if (repairResult.mode === "repaired") { console.info( `[dev] repaired financial ingestion schema (missing indexes: ${repairResult.repair?.missingIndexesBefore.join(", ") || "none"}; duplicate groups resolved: ${repairResult.repair?.duplicateGroupsResolved ?? 0}; bundle cache cleared: ${repairResult.repair?.bundleCacheCleared ? "yes" : "no"})`, ); } else if (repairResult.mode === "drifted") { console.warn( `[dev] financial ingestion schema drift detected (missing indexes: ${repairResult.missingIndexes.join(", ") || "none"}; duplicate groups: ${repairResult.duplicateGroups})`, ); } else if (repairResult.mode === "failed") { console.warn( `[dev] financial ingestion schema repair failed: ${repairResult.error ?? "unknown error"}`, ); } } finally { client.close(); } } 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 (sqliteVectorConfig.mode === "native") { console.info( `[dev] sqlite-vec native extension enabled (${sqliteVectorConfig.sqliteLibPath})`, ); } 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 }); });