import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawn, type ChildProcess } from "node:child_process"; let noReplyProcess: ChildProcess | null = null; const LOCK_FILE = path.join(os.tmpdir(), "dirigent-sidecar.lock"); function readLock(): { pid: number } | null { try { const raw = fs.readFileSync(LOCK_FILE, "utf8").trim(); return { pid: Number(raw) }; } catch { return null; } } function writeLock(pid: number): void { try { fs.writeFileSync(LOCK_FILE, String(pid)); } catch { /* ignore */ } } function clearLock(): void { try { fs.unlinkSync(LOCK_FILE); } catch { /* ignore */ } } function isLockHeld(): boolean { const lock = readLock(); if (!lock) return false; try { process.kill(lock.pid, 0); return true; } catch { return false; } } export function startSideCar( logger: { info: (m: string) => void; warn: (m: string) => void }, pluginDir: string, port = 8787, moderatorToken?: string, pluginApiToken?: string, gatewayPort?: number, debugMode?: boolean, ): void { logger.info(`dirigent: startSideCar called, pluginDir=${pluginDir}`); if (noReplyProcess) { logger.info("dirigent: no-reply API already running (local ref), skipping"); return; } if (isLockHeld()) { logger.info("dirigent: no-reply API already running (lock file), skipping"); return; } // services/main.mjs lives alongside the plugin directory in the distribution const serverPath = path.resolve(pluginDir, "services", "main.mjs"); logger.info(`dirigent: resolved serverPath=${serverPath}`); if (!fs.existsSync(serverPath)) { logger.warn(`dirigent: services/main.mjs not found at ${serverPath}, skipping`); return; } logger.info("dirigent: services/main.mjs found, spawning process..."); // Build plugin API URL from gateway port, or use a default const pluginApiUrl = gatewayPort ? `http://127.0.0.1:${gatewayPort}` : "http://127.0.0.1:18789"; const env: NodeJS.ProcessEnv = { ...process.env, SERVICES_PORT: String(port), PLUGIN_API_URL: pluginApiUrl, }; if (moderatorToken) { env.MODERATOR_TOKEN = moderatorToken; } if (pluginApiToken) { env.PLUGIN_API_TOKEN = pluginApiToken; } if (debugMode !== undefined) { env.DEBUG_MODE = debugMode ? "true" : "false"; } noReplyProcess = spawn(process.execPath, [serverPath], { env, stdio: ["ignore", "pipe", "pipe"], detached: false, }); if (noReplyProcess.pid) { writeLock(noReplyProcess.pid); } noReplyProcess.stdout?.on("data", (d: Buffer) => logger.info(`dirigent: services: ${d.toString().trim()}`)); noReplyProcess.stderr?.on("data", (d: Buffer) => logger.warn(`dirigent: services: ${d.toString().trim()}`)); noReplyProcess.on("exit", (code, signal) => { logger.info(`dirigent: services exited (code=${code}, signal=${signal})`); clearLock(); noReplyProcess = null; }); logger.info(`dirigent: services started (pid=${noReplyProcess.pid}, port=${port})`); } export function stopSideCar(logger: { info: (m: string) => void }): void { if (!noReplyProcess) return; logger.info("dirigent: stopping sidecar"); noReplyProcess.kill("SIGTERM"); noReplyProcess = null; clearLock(); }