import { randomBytes } from 'node:crypto'; import { createServer as createHttpServer } from 'node:http'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer, type ViteDevServer } from 'vite'; import type Database from 'better-sqlite3'; import { FIXTURE_KEY, FIXTURE_VERSION, fixtureServers, refreshFixtureData, seedFixtureData } from './fixture-data.js'; import { parseFixtureOptions, reserveFixture } from './fixture-options.js'; const help = `Create an isolated synthetic UI preview database. npm run seed:fixture -- --db /tmp/ec-preview/cherry.db [--now 2026-10-07T12:00:00Z] npm run seed:fixture -- --db /tmp/ec-live-preview/cherry.db --watch The database's directory must not exist; its parent must exist. Existing files, databases, symlinks and environment DB_PATH are never reused. --watch refreshes only this new fixture's observations every 15 seconds until Ctrl+C. --now fixes all sample times for reproducible snapshots and cannot be combined with --watch. No web server, worker, game agent or network request is started by this command. `; async function main() { const args = process.argv.slice(2); if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(help); return; } const options = parseFixtureOptions(args); process.umask(0o077); const destination = await reserveFixture(options.dbPath); let vite: ViteDevServer | undefined; let closeDb: (() => void) | undefined; let complete = false; try { const paths = { DB_PATH: destination.dbPath, SERVERS_ROOT: join(destination.root, 't5'), T6_SERVERS_ROOT: join(destination.root, 't6'), BETA_ARCHIVE_DIR: join(destination.root, 'beta-archive'), GAME_FILES_ROOT: join(destination.root, 'game-files'), SQLITE_TMPDIR: join(destination.root, 'sqlite-tmp') }; for (const [key, value] of Object.entries(paths)) { process.env[key] = value; if (key !== 'DB_PATH') await mkdir(value, { mode: 0o700 }); } Object.assign(process.env, { RCON_PASSWORD: '', RCON_HOST: '127.0.0.1', EC_AUTH_ALLOW_HTTP: 'true', DEPLOY_KEEP_DAYS: '7', DEPLOYMENT_RESTART_CONCURRENCY: '1', DEPLOYMENT_CREATE_CONCURRENCY: '1', EC_SQLITE_MMAP_MB: '0', PLUTAINER_IMAGE: 'fixture.invalid/electric-cherry/preview:local' }); const repository = resolve(dirname(fileURLToPath(import.meta.url)), '..'); vite = await createServer({ root: repository, cacheDir: join(destination.root, 'vite-cache'), configFile: false, envFile: false, appType: 'custom', // Vite 5 otherwise opens its HMR WebSocket port even with hmr:false. // Attach it to an HTTP server that is deliberately never listened on. server: { middlewareMode: true, hmr: { server: createHttpServer() }, watch: null }, optimizeDeps: { noDiscovery: true }, resolve: { alias: { $lib: join(repository, 'src/lib'), $ui: join(repository, 'src/lib/ui') } }, logLevel: 'error' }); // Load the real migrations through Vite's ?raw support. Importing hooks // would start queues and agents; this script never loads the app router. const database = (await vite.ssrLoadModule('/src/lib/server/db/index.ts')) as { db: () => Database.Database; closeDb: () => void; }; closeDb = database.closeDb; const handle = database.db(); seedFixtureData(handle, options.now); const password = randomBytes(24).toString('base64url'); const { hashPassword } = await vite.ssrLoadModule('/src/lib/server/auth/passwords.js'); handle .prepare( `INSERT INTO auth_users(id,username,display_name,password_hash,role,created_at,updated_at) VALUES('fixture-admin','demo','Demo operator',?,'admin',?,?)` ) .run(await hashPassword(password), options.now, options.now); if ( (handle.pragma('foreign_key_check') as unknown[]).length || handle.pragma('integrity_check', { simple: true }) !== 'ok' ) { throw new Error('Fixture integrity validation failed.'); } const environment = { ...paths, RCON_PASSWORD: '', RCON_HOST: '127.0.0.1', EC_AUTH_ALLOW_HTTP: 'true', PLUTAINER_IMAGE: process.env.PLUTAINER_IMAGE! }; const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`; await writeFile( join(destination.root, 'preview.env'), Object.entries(environment) .map(([key, value]) => `export ${key}=${shellQuote(value)}`) .join('\n') + '\n', { flag: 'wx', mode: 0o600 } ); await writeFile( join(destination.root, 'fixture.json'), JSON.stringify( { format: FIXTURE_VERSION, marker: FIXTURE_KEY, dbPath: destination.dbPath, createdAt: new Date(options.now).toISOString(), username: 'demo', password, hosts: 3, servers: fixtureServers.map((server) => server.name), matches: 500, note: 'Synthetic local data only. Host credentials cannot authenticate; game images and library IDs are fictitious.' }, null, 2 ) + '\n', { flag: 'wx', mode: 0o600 } ); handle.pragma('wal_checkpoint(TRUNCATE)'); complete = true; console.log( `Created ${destination.dbPath}\n3 hosts · 12 servers · 500 matches · 7 days of metrics\nLocal sign-in details: ${join(destination.root, 'fixture.json')}\nPreview environment: ${join(destination.root, 'preview.env')}` ); if (options.watch) { refreshFixtureData(handle, Date.now()); console.log( 'Refreshing synthetic observations every 15 seconds. Leave this terminal open; Ctrl+C stops the feeder.' ); await new Promise((resolvePromise, reject) => { const finish = (error?: unknown) => { clearInterval(timer); process.off('SIGINT', stop); process.off('SIGTERM', stop); if (error) reject(error); else resolvePromise(); }; const stop = () => finish(); const timer = setInterval(() => { try { refreshFixtureData(handle, Date.now()); } catch (error) { finish(error); } }, 15_000); process.on('SIGINT', stop); process.on('SIGTERM', stop); }); } } finally { try { closeDb?.(); } finally { try { await vite?.close(); } finally { if (!complete) await rm(destination.root, { recursive: true, force: true }); } } } } main().catch((error) => { console.error( `Fixture creation failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); process.exitCode = 1; });