| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- import { loadRuntimeConfig } from './config-loader.mjs';
- import {
- assertRuntimeAvailable,
- clearRuntimeStopRequest,
- isProcessAlive,
- readRuntimeStopRequest,
- readRuntimeState,
- runtimeStatePath,
- stopRuntimeProcess,
- writeRuntimeState,
- } from './runtime-state.mjs';
- let activeRuntime = null;
- function mergeComponents(current = {}, patch = {}) {
- const output = { ...current };
- for (const [name, value] of Object.entries(patch)) {
- output[name] = { ...(current[name] || {}), ...value, updatedAt: new Date().toISOString() };
- }
- return output;
- }
- export async function startRuntime(options = {}) {
- if (activeRuntime) return activeRuntime;
- const loaded = await loadRuntimeConfig(options);
- const statePath = runtimeStatePath(options.statePath);
- assertRuntimeAvailable(statePath);
- clearRuntimeStopRequest(statePath);
- let state = writeRuntimeState({
- pid: process.pid,
- status: 'starting',
- mode: loaded.mode,
- transport: loaded.mode === 'enterprise' ? 'server_relay' : 'local_polling',
- workspaceRoot: loaded.workspaceRoot,
- configPath: loaded.configPath,
- startedAt: new Date().toISOString(),
- components: {},
- }, statePath);
- const updateComponents = patch => {
- state = writeRuntimeState({
- ...state,
- components: mergeComponents(state.components, patch),
- }, statePath);
- };
- let dashboard = null;
- if (options.dashboard !== false && loaded.config.dashboard.enabled) {
- const { startDashboard } = await import('./processor-bridge.mjs');
- dashboard = await startDashboard(loaded.config.dashboard.port);
- updateComponents({ dashboard: { status: 'running', port: loaded.config.dashboard.port } });
- }
- let controller;
- let portraitQueueWorker = null;
- if (loaded.mode === 'enterprise') {
- const { EnterpriseRelayRuntime } = await import('./enterprise-relay-client.mjs');
- const { PortraitQueueWorker } = await import('./portrait-queue-worker.mjs');
- controller = new EnterpriseRelayRuntime({
- config: loaded.config.enterprise.relay,
- guid: options.guid || '',
- onState: updateComponents,
- });
- portraitQueueWorker = new PortraitQueueWorker({ onState: updateComponents });
- } else {
- const { PersonalRuntime } = await import('./personal-runtime.mjs');
- controller = new PersonalRuntime({
- config: loaded.config.personal.polling,
- workspaceRoot: loaded.workspaceRoot,
- onState: updateComponents,
- });
- }
- if (options.dryRun !== true) {
- controller.start();
- portraitQueueWorker?.start();
- }
- state = writeRuntimeState({ ...state, status: options.dryRun ? 'ready' : 'running' }, statePath);
- let stopping = false;
- let stopWatcher = null;
- const stop = async reason => {
- if (stopping) return;
- stopping = true;
- if (stopWatcher) clearInterval(stopWatcher);
- clearRuntimeStopRequest(statePath);
- state = writeRuntimeState({ ...state, status: 'stopping', stopReason: reason || 'requested' }, statePath);
- await portraitQueueWorker?.stop();
- await controller.stop();
- if (dashboard?.server) {
- await new Promise(resolve => dashboard.server.close(resolve));
- }
- state = writeRuntimeState({
- ...state,
- status: 'stopped',
- stoppedAt: new Date().toISOString(),
- }, statePath);
- activeRuntime = null;
- };
- stopWatcher = setInterval(() => {
- const request = readRuntimeStopRequest(statePath);
- if (!request.requestedAt) return;
- if (request.targetPid && Number(request.targetPid) !== process.pid) return;
- void stop('external-stop');
- }, 500);
- activeRuntime = {
- mode: loaded.mode,
- transport: state.transport,
- config: loaded.config,
- configPath: loaded.configPath,
- workspaceRoot: loaded.workspaceRoot,
- statePath,
- controller,
- portraitQueueWorker,
- dashboard,
- stop,
- };
- return activeRuntime;
- }
- function parseArgs(argv) {
- const command = argv.find(arg => !arg.startsWith('-')) || 'start';
- const modeArg = argv.find(arg => arg.startsWith('--mode='));
- const guidArg = argv.find(arg => arg.startsWith('--guid='));
- return {
- command,
- forceMode: modeArg ? modeArg.slice('--mode='.length) : '',
- guid: guidArg ? guidArg.slice('--guid='.length) : '',
- dashboard: !argv.includes('--no-dashboard'),
- dryRun: argv.includes('--dry-run'),
- };
- }
- export async function runCli(argv = process.argv.slice(2)) {
- const options = parseArgs(argv);
- if (options.command === 'status') {
- const state = readRuntimeState();
- process.stdout.write(`${JSON.stringify({ ...state, alive: isProcessAlive(state.pid) }, null, 2)}\n`);
- return;
- }
- if (options.command === 'stop') {
- const result = stopRuntimeProcess();
- let state = readRuntimeState();
- for (let attempt = 0; result.requested && attempt < 50; attempt += 1) {
- await new Promise(resolve => setTimeout(resolve, 100));
- state = readRuntimeState();
- if (state.status === 'stopped' || !isProcessAlive(state.pid)) break;
- }
- process.stdout.write(`${JSON.stringify({ ...result, status: state.status || 'unknown' }, null, 2)}\n`);
- return;
- }
- if (options.command !== 'start') throw new Error('Use start, status, or stop.');
- if (!options.dryRun) {
- const current = readRuntimeState();
- if (['starting', 'running'].includes(current.status) && isProcessAlive(current.pid)) {
- process.stdout.write(`${JSON.stringify({ ...current, alive: true, alreadyRunning: true }, null, 2)}\n`);
- return;
- }
- }
- const runtime = await startRuntime(options);
- process.stdout.write(`${JSON.stringify({
- status: options.dryRun ? 'ready' : 'running',
- pid: process.pid,
- mode: runtime.mode,
- transport: runtime.transport,
- dashboard: runtime.dashboard?.url || null,
- statePath: runtime.statePath,
- }, null, 2)}\n`);
- if (options.dryRun) await runtime.stop('dry-run');
- const shutdown = signal => runtime.stop(signal).finally(() => process.exit(0));
- process.once('SIGINT', () => shutdown('SIGINT'));
- process.once('SIGTERM', () => shutdown('SIGTERM'));
- }
- export { readRuntimeState };
|