Files
Neon-Desk/scripts/dev.ts
francy51 14a7773504 Add consolidated disclosure statement type
Create unified disclosure statement to organize footnote disclosures
separate from primary financial statements. Disclosures are now grouped
by type (tax, debt, securities, derivatives, leases, intangibles, ma,
revenue, cash_flow) in a dedicated statement type for cleaner UI
presentation.
2026-03-16 18:54:23 -04:00

214 lines
6.4 KiB
TypeScript

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';
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<boolean>((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',
'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 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 ?? '');
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 (!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 });
});