import { existsSync, readFileSync } from 'node:fs'; import { isAbsolute, resolve } from 'node:path'; import { spawn } from 'node:child_process'; const root = resolve(import.meta.dirname, '..'); const localEnvPath = resolve(root, '.env.local'); const parseEnvFile = (path) => { if (!existsSync(path)) return {}; const result = {}; for (const rawLine of readFileSync(path, 'utf8').split(/\r?\n/u)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const separator = line.indexOf('='); if (separator < 1) throw new Error(`Invalid environment line in ${path}: ${rawLine}`); const key = line.slice(0, separator).trim(); let value = line.slice(separator + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } result[key] = value; } return result; }; const localEnv = parseEnvFile(localEnvPath); const pathDefaults = { BOTSU_DRAWING_STORE_ROOT: '.data/drawings', BOTSU_DOCUMENT_STORE_ROOT: '.data/documents', BOTSU_COOKIE_STORE_ROOT: '.data/cookies', BOTSU_TCG_STORE_ROOT: '.data/tcg', BOTSU_MOODBOARD_STORE_ROOT: '.data/moodboard', BOTSU_RADIO_STORE_ROOT: '.data/radio', TCG_CATALOG_INDEX_PATH: '.data/tcg/catalog-index.json', EMOJI_KITCHEN_ROOT: '.data/emoji-kitchen/stickers', }; const env = { BOTSU_DEV_HOST: '127.0.0.1', BOTSU_DEV_PORT: '5173', BOTSU_DEV_ALLOWED_HOSTS: 'localhost,127.0.0.1', BOTSU_PRESENCE_PROXY_URL: 'http://127.0.0.1:8091', HOST: '127.0.0.1', PORT: '8091', PRESENCE_ALLOWED_ORIGIN: 'http://127.0.0.1:5173,http://localhost:5173', MATRIX_HOMESERVER: 'botsu.net', ...localEnv, ...process.env, }; for (const [key, value] of Object.entries(pathDefaults)) { const configured = env[key] ?? value; env[key] = isAbsolute(configured) ? configured : resolve(root, configured); } const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const children = [ spawn(npm, ['run', 'dev:api'], { cwd: root, env, stdio: 'inherit', detached: process.platform !== 'win32' }), spawn(npm, ['run', 'dev:client'], { cwd: root, env, stdio: 'inherit', detached: process.platform !== 'win32' }), ]; let stopping = false; const stop = (signal = 'SIGTERM') => { if (stopping) return; stopping = true; for (const child of children) { if (!child.pid || child.exitCode !== null) continue; try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, signal); } catch (error) { if (error.code !== 'ESRCH') throw error; } } }; for (const signal of ['SIGINT', 'SIGTERM']) { process.once(signal, () => stop(signal)); } for (const child of children) { child.once('error', (error) => { console.error(error); stop(); process.exitCode = 1; }); child.once('exit', (code, signal) => { if (!stopping) { if (code !== 0) console.error(`Local service stopped (${signal ?? `exit ${code}`}).`); stop(); process.exitCode = code ?? 1; } }); }