2026-04-12 00:40:49 +03:00
|
|
|
import 'reflect-metadata';
|
2025-12-07 19:32:53 +02:00
|
|
|
import express from 'express';
|
2026-04-17 21:26:56 +08:00
|
|
|
import compression from 'compression';
|
2025-12-07 19:32:53 +02:00
|
|
|
import path from 'path';
|
2026-01-10 10:26:58 +02:00
|
|
|
import { spawn, spawnSync } from 'child_process';
|
2025-12-07 19:32:53 +02:00
|
|
|
import fs from 'fs';
|
|
|
|
|
import http from 'http';
|
2026-02-17 18:01:57 +02:00
|
|
|
import net from 'net';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
import os from 'os';
|
2026-01-06 21:31:04 +02:00
|
|
|
import crypto from 'crypto';
|
2026-04-11 13:36:02 -06:00
|
|
|
import { createUiAuth } from './lib/ui-auth/ui-auth.js';
|
2026-02-28 04:21:46 +02:00
|
|
|
import { createTunnelAuth } from './lib/opencode/tunnel-auth.js';
|
2026-03-31 18:47:00 +03:00
|
|
|
import { createManagedTunnelConfigRuntime } from './lib/tunnels/managed-config.js';
|
2026-03-12 19:40:22 +02:00
|
|
|
import { createTunnelProviderRegistry } from './lib/tunnels/registry.js';
|
|
|
|
|
import { createCloudflareTunnelProvider } from './lib/tunnels/providers/cloudflare.js';
|
2026-05-25 18:00:03 +03:00
|
|
|
import { createNgrokTunnelProvider } from './lib/tunnels/providers/ngrok.js';
|
2026-03-31 18:47:00 +03:00
|
|
|
import { createRequestSecurityRuntime } from './lib/security/request-security.js';
|
2026-03-12 19:40:22 +02:00
|
|
|
import {
|
|
|
|
|
TUNNEL_MODE_MANAGED_LOCAL,
|
|
|
|
|
TUNNEL_MODE_MANAGED_REMOTE,
|
|
|
|
|
TUNNEL_MODE_QUICK,
|
|
|
|
|
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
|
|
|
TunnelServiceError,
|
|
|
|
|
isSupportedTunnelMode,
|
|
|
|
|
normalizeOptionalPath,
|
|
|
|
|
normalizeTunnelStartRequest,
|
|
|
|
|
normalizeTunnelMode,
|
|
|
|
|
normalizeTunnelProvider,
|
|
|
|
|
} from './lib/tunnels/types.js';
|
2026-02-22 19:05:13 -03:00
|
|
|
import { prepareNotificationLastMessage } from './lib/notifications/index.js';
|
2026-03-31 18:47:00 +03:00
|
|
|
import { registerTtsRoutes } from './lib/tts/routes.js';
|
|
|
|
|
import { detectSayTtsCapability } from './lib/tts/capability-runtime.js';
|
|
|
|
|
import { createTerminalRuntime } from './lib/terminal/runtime.js';
|
2026-04-17 16:07:26 +08:00
|
|
|
import {
|
|
|
|
|
createGlobalUiEventBroadcaster,
|
2026-04-24 11:58:16 +03:00
|
|
|
createGlobalMessageStreamHub,
|
2026-04-17 16:07:26 +08:00
|
|
|
createMessageStreamWsRuntime,
|
2026-05-01 04:57:31 -07:00
|
|
|
DEFAULT_UPSTREAM_STALL_TIMEOUT_MS,
|
|
|
|
|
UPSTREAM_STALL_TIMEOUT_CONCURRENT_MS,
|
2026-04-17 16:07:26 +08:00
|
|
|
} from './lib/event-stream/index.js';
|
2026-03-31 18:47:00 +03:00
|
|
|
import { createFsSearchRuntime as createFsSearchRuntimeFactory } from './lib/fs/search.js';
|
|
|
|
|
import { createOpenCodeLifecycleRuntime } from './lib/opencode/lifecycle.js';
|
|
|
|
|
import { createOpenCodeEnvRuntime } from './lib/opencode/env-runtime.js';
|
|
|
|
|
import { resolveOpenCodeEnvConfig } from './lib/opencode/env-config.js';
|
|
|
|
|
import { createHmrStateRuntime } from './lib/opencode/hmr-state-runtime.js';
|
|
|
|
|
import { createOpenCodeNetworkRuntime } from './lib/opencode/network-runtime.js';
|
|
|
|
|
import { createOpenCodeAuthStateRuntime } from './lib/opencode/auth-state-runtime.js';
|
|
|
|
|
import { createProjectDirectoryRuntime } from './lib/opencode/project-directory-runtime.js';
|
|
|
|
|
import { createSettingsNormalizationRuntime } from './lib/opencode/settings-normalization-runtime.js';
|
|
|
|
|
import { createSettingsHelpers } from './lib/opencode/settings-helpers.js';
|
|
|
|
|
import { createThemeRuntime } from './lib/opencode/theme-runtime.js';
|
|
|
|
|
import { createFeatureRoutesRuntime } from './lib/opencode/feature-routes-runtime.js';
|
|
|
|
|
import { parseServeCliOptions } from './lib/opencode/cli-options.js';
|
2026-02-08 15:39:22 +02:00
|
|
|
import {
|
2026-03-31 18:47:00 +03:00
|
|
|
registerAuthAndAccessRoutes,
|
|
|
|
|
registerCommonRequestMiddleware,
|
|
|
|
|
registerServerStatusRoutes,
|
|
|
|
|
} from './lib/opencode/core-routes.js';
|
|
|
|
|
import { registerOpenChamberRoutes } from './lib/opencode/openchamber-routes.js';
|
|
|
|
|
import { createServerUtilsRuntime } from './lib/opencode/server-utils-runtime.js';
|
|
|
|
|
import { createStaticRoutesRuntime } from './lib/opencode/static-routes-runtime.js';
|
|
|
|
|
import { createSettingsRuntime } from './lib/opencode/settings-runtime.js';
|
|
|
|
|
import { createOpenCodeResolutionRuntime } from './lib/opencode/opencode-resolution-runtime.js';
|
|
|
|
|
import { createBootstrapRuntime } from './lib/opencode/bootstrap-runtime.js';
|
|
|
|
|
import { createSessionRuntime } from './lib/opencode/session-runtime.js';
|
|
|
|
|
import { createOpenCodeWatcherRuntime } from './lib/opencode/watcher.js';
|
2026-04-16 15:55:08 +03:00
|
|
|
import { createScheduledTasksRuntime } from './lib/scheduled-tasks/runtime.js';
|
2026-03-31 18:47:00 +03:00
|
|
|
import { createServerStartupRuntime } from './lib/opencode/server-startup-runtime.js';
|
|
|
|
|
import { createTunnelWiringRuntime } from './lib/opencode/tunnel-wiring-runtime.js';
|
|
|
|
|
import { createStartupPipelineRuntime } from './lib/opencode/startup-pipeline-runtime.js';
|
|
|
|
|
import { runCliEntryIfMain } from './lib/opencode/cli-entry-runtime.js';
|
|
|
|
|
import { registerNotificationRoutes } from './lib/notifications/routes.js';
|
|
|
|
|
import { createNotificationEmitterRuntime } from './lib/notifications/emitter-runtime.js';
|
|
|
|
|
import { createNotificationTriggerRuntime } from './lib/notifications/runtime.js';
|
|
|
|
|
import { createPushRuntime } from './lib/notifications/push-runtime.js';
|
|
|
|
|
import { createNotificationTemplateRuntime } from './lib/notifications/template-runtime.js';
|
|
|
|
|
import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
|
2026-04-16 15:55:08 +03:00
|
|
|
import { createProjectConfigRuntime } from './lib/projects/project-config.js';
|
2026-04-29 17:03:38 -04:00
|
|
|
import { createPreviewProxyRuntime } from './lib/preview/proxy-runtime.js';
|
|
|
|
|
import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware';
|
2026-01-22 01:19:24 +02:00
|
|
|
import webPush from 'web-push';
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
|
|
|
|
|
|
const DEFAULT_PORT = 3000;
|
2026-02-05 01:59:49 +02:00
|
|
|
const DESKTOP_NOTIFY_PREFIX = '[OpenChamberDesktopNotify] ';
|
|
|
|
|
const uiNotificationClients = new Set();
|
2026-04-17 16:07:26 +08:00
|
|
|
const uiNotificationWsClients = new Set();
|
2026-04-16 15:55:08 +03:00
|
|
|
const uiOpenChamberEventClients = new Set();
|
2026-01-15 15:30:46 -05:00
|
|
|
const HEALTH_CHECK_INTERVAL = 15000;
|
2025-12-07 19:32:53 +02:00
|
|
|
const SHUTDOWN_TIMEOUT = 10000;
|
|
|
|
|
const MODELS_DEV_API_URL = 'https://models.dev/api.json';
|
|
|
|
|
const MODELS_METADATA_CACHE_TTL = 5 * 60 * 1000;
|
|
|
|
|
const CLIENT_RELOAD_DELAY_MS = 800;
|
|
|
|
|
const OPEN_CODE_READY_GRACE_MS = 12000;
|
|
|
|
|
const LONG_REQUEST_TIMEOUT_MS = 4 * 60 * 1000;
|
2026-02-28 04:21:46 +02:00
|
|
|
const TUNNEL_BOOTSTRAP_TTL_DEFAULT_MS = 30 * 60 * 1000;
|
|
|
|
|
const TUNNEL_BOOTSTRAP_TTL_MIN_MS = 60 * 1000;
|
|
|
|
|
const TUNNEL_BOOTSTRAP_TTL_MAX_MS = 24 * 60 * 60 * 1000;
|
|
|
|
|
const TUNNEL_SESSION_TTL_DEFAULT_MS = 8 * 60 * 60 * 1000;
|
|
|
|
|
const TUNNEL_SESSION_TTL_MIN_MS = 5 * 60 * 1000;
|
2026-04-11 13:33:38 -06:00
|
|
|
const TUNNEL_SESSION_TTL_MAX_MS = 30 * 24 * 60 * 60 * 1000;
|
2026-04-17 21:26:56 +08:00
|
|
|
|
|
|
|
|
function headerIncludesEventStream(value) {
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return value.toLowerCase().includes('text/event-stream');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.some((entry) => typeof entry === 'string' && entry.toLowerCase().includes('text/event-stream'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:12:20 +08:00
|
|
|
/**
|
|
|
|
|
* SSE endpoint paths that must never be compressed by the compression middleware.
|
|
|
|
|
*
|
|
|
|
|
* The compression middleware filter runs before route handlers, so
|
|
|
|
|
* `res.getHeader('Content-Type')` is still undefined at that point.
|
|
|
|
|
* This means the Accept-header check alone is not sufficient for
|
|
|
|
|
* non-standard clients (e.g. curl, fetch) that omit Accept.
|
|
|
|
|
* Path-based exclusion acts as a deterministic fallback.
|
|
|
|
|
*/
|
|
|
|
|
const SSE_PATH_PREFIXES = [
|
|
|
|
|
'/api/event',
|
|
|
|
|
'/api/global/event',
|
|
|
|
|
'/api/notifications/stream',
|
|
|
|
|
'/api/openchamber/events',
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-17 21:26:56 +08:00
|
|
|
function shouldSkipCompression(req, res) {
|
|
|
|
|
if (headerIncludesEventStream(req.headers.accept)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:12:20 +08:00
|
|
|
const pathname = req.path || req.url || '';
|
2026-05-05 18:47:00 +03:00
|
|
|
if ((pathname === '/api' || pathname.startsWith('/api/')) && shouldSkipApiCompression()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:12:20 +08:00
|
|
|
if (pathname.startsWith('/api/terminal/') && pathname.endsWith('/stream')) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
for (const prefix of SSE_PATH_PREFIXES) {
|
|
|
|
|
if (pathname === prefix) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 21:26:56 +08:00
|
|
|
return headerIncludesEventStream(res.getHeader('Content-Type'));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 20:26:12 +02:00
|
|
|
const OPENCHAMBER_VERSION = (() => {
|
|
|
|
|
try {
|
|
|
|
|
const packagePath = path.resolve(__dirname, '..', 'package.json');
|
|
|
|
|
const raw = fs.readFileSync(packagePath, 'utf8');
|
|
|
|
|
const pkg = JSON.parse(raw);
|
|
|
|
|
if (pkg && typeof pkg.version === 'string' && pkg.version.trim().length > 0) {
|
|
|
|
|
return pkg.version.trim();
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
|
|
|
|
return 'unknown';
|
|
|
|
|
})();
|
2026-04-03 13:59:10 +03:00
|
|
|
|
|
|
|
|
const isEnvFlagEnabled = (value) => {
|
|
|
|
|
if (value === true || value === 1) return true;
|
|
|
|
|
if (typeof value !== 'string') return false;
|
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
|
|
|
return normalized === '1' || normalized === 'true';
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-05 18:47:00 +03:00
|
|
|
const isEnvFlagDisabled = (value) => {
|
|
|
|
|
if (value === false || value === 0) return true;
|
|
|
|
|
if (typeof value !== 'string') return false;
|
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
|
|
|
return normalized === '0' || normalized === 'false';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const shouldSkipApiCompression = () => {
|
|
|
|
|
if (isEnvFlagEnabled(process.env.OPENCHAMBER_SKIP_API_COMPRESSION)) return true;
|
|
|
|
|
if (isEnvFlagEnabled(process.env.OPENCHAMBER_COMPRESS_API)) return false;
|
|
|
|
|
if (isEnvFlagDisabled(process.env.OPENCHAMBER_COMPRESS_API)) return true;
|
|
|
|
|
return process.env.OPENCHAMBER_RUNTIME === 'desktop';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const OPENCHAMBER_VERBOSE_REQUEST_LOGS = isEnvFlagEnabled(process.env.OPENCHAMBER_VERBOSE_REQUEST_LOGS);
|
|
|
|
|
|
2026-04-03 13:59:10 +03:00
|
|
|
const PLAN_MODE_EXPERIMENT_ENABLED =
|
|
|
|
|
isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL_PLAN_MODE)
|
|
|
|
|
|| isEnvFlagEnabled(process.env.OPENCODE_EXPERIMENTAL);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
const fsPromises = fs.promises;
|
2025-12-25 01:25:52 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const settingsNormalizationRuntime = createSettingsNormalizationRuntime({
|
|
|
|
|
os,
|
|
|
|
|
path,
|
|
|
|
|
processLike: process,
|
2026-05-24 08:46:11 -04:00
|
|
|
realpathSync: fs.realpathSync,
|
2026-03-31 18:47:00 +03:00
|
|
|
tunnelBootstrapTtlDefaultMs: TUNNEL_BOOTSTRAP_TTL_DEFAULT_MS,
|
|
|
|
|
tunnelBootstrapTtlMinMs: TUNNEL_BOOTSTRAP_TTL_MIN_MS,
|
|
|
|
|
tunnelBootstrapTtlMaxMs: TUNNEL_BOOTSTRAP_TTL_MAX_MS,
|
|
|
|
|
tunnelSessionTtlDefaultMs: TUNNEL_SESSION_TTL_DEFAULT_MS,
|
|
|
|
|
tunnelSessionTtlMinMs: TUNNEL_SESSION_TTL_MIN_MS,
|
|
|
|
|
tunnelSessionTtlMaxMs: TUNNEL_SESSION_TTL_MAX_MS,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const normalizeDirectoryPath = (...args) => settingsNormalizationRuntime.normalizeDirectoryPath(...args);
|
|
|
|
|
const normalizePathForPersistence = (...args) => settingsNormalizationRuntime.normalizePathForPersistence(...args);
|
|
|
|
|
const normalizeSettingsPaths = (...args) => settingsNormalizationRuntime.normalizeSettingsPaths(...args);
|
|
|
|
|
const normalizeTunnelBootstrapTtlMs = (...args) => settingsNormalizationRuntime.normalizeTunnelBootstrapTtlMs(...args);
|
|
|
|
|
const normalizeTunnelSessionTtlMs = (...args) => settingsNormalizationRuntime.normalizeTunnelSessionTtlMs(...args);
|
|
|
|
|
const normalizeManagedRemoteTunnelHostname = (...args) =>
|
|
|
|
|
settingsNormalizationRuntime.normalizeManagedRemoteTunnelHostname(...args);
|
|
|
|
|
const normalizeManagedRemoteTunnelPresets = (...args) =>
|
|
|
|
|
settingsNormalizationRuntime.normalizeManagedRemoteTunnelPresets(...args);
|
|
|
|
|
const normalizeManagedRemoteTunnelPresetTokens = (...args) =>
|
|
|
|
|
settingsNormalizationRuntime.normalizeManagedRemoteTunnelPresetTokens(...args);
|
|
|
|
|
const isUnsafeSkillRelativePath = (...args) => settingsNormalizationRuntime.isUnsafeSkillRelativePath(...args);
|
|
|
|
|
const sanitizeTypographySizesPartial = (...args) =>
|
|
|
|
|
settingsNormalizationRuntime.sanitizeTypographySizesPartial(...args);
|
|
|
|
|
const normalizeStringArray = (...args) => settingsNormalizationRuntime.normalizeStringArray(...args);
|
|
|
|
|
const sanitizeModelRefs = (...args) => settingsNormalizationRuntime.sanitizeModelRefs(...args);
|
|
|
|
|
const sanitizeSkillCatalogs = (...args) => settingsNormalizationRuntime.sanitizeSkillCatalogs(...args);
|
|
|
|
|
const sanitizeProjects = (...args) => settingsNormalizationRuntime.sanitizeProjects(...args);
|
2025-12-25 01:25:52 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const OPENCHAMBER_USER_CONFIG_ROOT = path.join(os.homedir(), '.config', 'openchamber');
|
|
|
|
|
const OPENCHAMBER_USER_THEMES_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'themes');
|
2026-04-16 15:55:08 +03:00
|
|
|
const OPENCHAMBER_PROJECTS_CONFIG_DIR = path.join(OPENCHAMBER_USER_CONFIG_ROOT, 'projects');
|
2025-12-25 01:25:52 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const MAX_THEME_JSON_BYTES = 512 * 1024;
|
2026-03-17 13:18:54 +02:00
|
|
|
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const themeRuntime = createThemeRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
themesDir: OPENCHAMBER_USER_THEMES_DIR,
|
|
|
|
|
maxThemeJsonBytes: MAX_THEME_JSON_BYTES,
|
|
|
|
|
logger: console,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const readCustomThemesFromDisk = (...args) => themeRuntime.readCustomThemesFromDisk(...args);
|
|
|
|
|
|
|
|
|
|
let notificationTemplateRuntime = null;
|
|
|
|
|
|
|
|
|
|
const createTimeoutSignal = (...args) => notificationTemplateRuntime.createTimeoutSignal(...args);
|
|
|
|
|
const formatProjectLabel = (...args) => notificationTemplateRuntime.formatProjectLabel(...args);
|
|
|
|
|
const resolveNotificationTemplate = (...args) => notificationTemplateRuntime.resolveNotificationTemplate(...args);
|
|
|
|
|
const shouldApplyResolvedTemplateMessage = (...args) => notificationTemplateRuntime.shouldApplyResolvedTemplateMessage(...args);
|
|
|
|
|
const fetchFreeZenModels = (...args) => notificationTemplateRuntime.fetchFreeZenModels(...args);
|
|
|
|
|
const extractTextFromParts = (...args) => notificationTemplateRuntime.extractTextFromParts(...args);
|
|
|
|
|
const extractLastMessageText = (...args) => notificationTemplateRuntime.extractLastMessageText(...args);
|
|
|
|
|
const fetchLastAssistantMessageText = (...args) => notificationTemplateRuntime.fetchLastAssistantMessageText(...args);
|
|
|
|
|
const maybeCacheSessionInfoFromEvent = (...args) => notificationTemplateRuntime.maybeCacheSessionInfoFromEvent(...args);
|
|
|
|
|
const buildTemplateVariables = (...args) => notificationTemplateRuntime.buildTemplateVariables(...args);
|
|
|
|
|
const getCachedZenModels = (...args) => notificationTemplateRuntime.getCachedZenModels(...args);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const OPENCHAMBER_DATA_DIR = process.env.OPENCHAMBER_DATA_DIR
|
|
|
|
|
? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
|
|
|
|
|
: path.join(os.homedir(), '.config', 'openchamber');
|
|
|
|
|
const SETTINGS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'settings.json');
|
|
|
|
|
const PUSH_SUBSCRIPTIONS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'push-subscriptions.json');
|
|
|
|
|
const CLOUDFLARE_MANAGED_REMOTE_TUNNELS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'cloudflare-managed-remote-tunnels.json');
|
|
|
|
|
const CLOUDFLARE_LEGACY_NAMED_TUNNELS_FILE_PATH = path.join(OPENCHAMBER_DATA_DIR, 'cloudflare-named-tunnels.json');
|
|
|
|
|
const CLOUDFLARE_MANAGED_REMOTE_TUNNELS_VERSION = 1;
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const managedTunnelConfigRuntime = createManagedTunnelConfigRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
normalizeManagedRemoteTunnelHostname,
|
|
|
|
|
normalizeManagedRemoteTunnelPresets,
|
|
|
|
|
constants: {
|
|
|
|
|
CLOUDFLARE_MANAGED_REMOTE_TUNNELS_FILE_PATH,
|
|
|
|
|
CLOUDFLARE_LEGACY_NAMED_TUNNELS_FILE_PATH,
|
|
|
|
|
CLOUDFLARE_MANAGED_REMOTE_TUNNELS_VERSION,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const readManagedRemoteTunnelConfigFromDisk = (...args) => managedTunnelConfigRuntime.readManagedRemoteTunnelConfigFromDisk(...args);
|
|
|
|
|
const syncManagedRemoteTunnelConfigWithPresets = (...args) => managedTunnelConfigRuntime.syncManagedRemoteTunnelConfigWithPresets(...args);
|
|
|
|
|
const upsertManagedRemoteTunnelToken = (...args) => managedTunnelConfigRuntime.upsertManagedRemoteTunnelToken(...args);
|
|
|
|
|
const resolveManagedRemoteTunnelToken = (...args) => managedTunnelConfigRuntime.resolveManagedRemoteTunnelToken(...args);
|
|
|
|
|
|
|
|
|
|
const settingsHelpers = createSettingsHelpers({
|
|
|
|
|
normalizePathForPersistence,
|
|
|
|
|
normalizeDirectoryPath,
|
|
|
|
|
normalizeTunnelBootstrapTtlMs,
|
|
|
|
|
normalizeTunnelSessionTtlMs,
|
|
|
|
|
normalizeTunnelProvider,
|
|
|
|
|
normalizeTunnelMode,
|
|
|
|
|
normalizeOptionalPath,
|
|
|
|
|
normalizeManagedRemoteTunnelHostname,
|
|
|
|
|
normalizeManagedRemoteTunnelPresets,
|
|
|
|
|
normalizeManagedRemoteTunnelPresetTokens,
|
|
|
|
|
sanitizeTypographySizesPartial,
|
|
|
|
|
normalizeStringArray,
|
|
|
|
|
sanitizeModelRefs,
|
|
|
|
|
sanitizeSkillCatalogs,
|
|
|
|
|
sanitizeProjects,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const normalizePwaAppName = (...args) => settingsHelpers.normalizePwaAppName(...args);
|
2026-04-22 03:17:34 +07:00
|
|
|
const normalizePwaOrientation = (...args) => settingsHelpers.normalizePwaOrientation(...args);
|
2026-03-31 18:47:00 +03:00
|
|
|
const sanitizeSettingsUpdate = (...args) => settingsHelpers.sanitizeSettingsUpdate(...args);
|
|
|
|
|
const mergePersistedSettings = (...args) => settingsHelpers.mergePersistedSettings(...args);
|
|
|
|
|
const formatSettingsResponse = (...args) => settingsHelpers.formatSettingsResponse(...args);
|
|
|
|
|
|
|
|
|
|
const projectDirectoryRuntime = createProjectDirectoryRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
normalizeDirectoryPath,
|
|
|
|
|
getReadSettingsFromDiskMigrated: () => readSettingsFromDiskMigrated,
|
|
|
|
|
sanitizeProjects,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const resolveDirectoryCandidate = (...args) => projectDirectoryRuntime.resolveDirectoryCandidate(...args);
|
|
|
|
|
const validateDirectoryPath = (...args) => projectDirectoryRuntime.validateDirectoryPath(...args);
|
|
|
|
|
const resolveProjectDirectory = (...args) => projectDirectoryRuntime.resolveProjectDirectory(...args);
|
|
|
|
|
const resolveOptionalProjectDirectory = (...args) => projectDirectoryRuntime.resolveOptionalProjectDirectory(...args);
|
|
|
|
|
|
|
|
|
|
const settingsRuntime = createSettingsRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
crypto,
|
|
|
|
|
SETTINGS_FILE_PATH,
|
|
|
|
|
sanitizeProjects,
|
|
|
|
|
sanitizeSettingsUpdate,
|
|
|
|
|
mergePersistedSettings,
|
|
|
|
|
normalizeSettingsPaths,
|
|
|
|
|
normalizeStringArray,
|
|
|
|
|
formatSettingsResponse,
|
|
|
|
|
resolveDirectoryCandidate,
|
|
|
|
|
normalizeManagedRemoteTunnelHostname,
|
|
|
|
|
normalizeManagedRemoteTunnelPresets,
|
|
|
|
|
normalizeManagedRemoteTunnelPresetTokens,
|
|
|
|
|
syncManagedRemoteTunnelConfigWithPresets,
|
|
|
|
|
upsertManagedRemoteTunnelToken,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const readSettingsFromDiskMigrated = (...args) => settingsRuntime.readSettingsFromDiskMigrated(...args);
|
|
|
|
|
const readSettingsFromDisk = (...args) => settingsRuntime.readSettingsFromDisk(...args);
|
|
|
|
|
const writeSettingsToDisk = (...args) => settingsRuntime.writeSettingsToDisk(...args);
|
|
|
|
|
const persistSettings = (...args) => settingsRuntime.persistSettings(...args);
|
|
|
|
|
|
|
|
|
|
const requestSecurityRuntime = createRequestSecurityRuntime({
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getUiSessionTokenFromRequest = (...args) => requestSecurityRuntime.getUiSessionTokenFromRequest(...args);
|
|
|
|
|
|
|
|
|
|
const pushRuntime = createPushRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
webPush,
|
|
|
|
|
PUSH_SUBSCRIPTIONS_FILE_PATH,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
writeSettingsToDisk,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getOrCreateVapidKeys = (...args) => pushRuntime.getOrCreateVapidKeys(...args);
|
|
|
|
|
const addOrUpdatePushSubscription = (...args) => pushRuntime.addOrUpdatePushSubscription(...args);
|
|
|
|
|
const removePushSubscription = (...args) => pushRuntime.removePushSubscription(...args);
|
|
|
|
|
const sendPushToAllUiSessions = (...args) => pushRuntime.sendPushToAllUiSessions(...args);
|
|
|
|
|
const updateUiVisibility = (...args) => pushRuntime.updateUiVisibility(...args);
|
|
|
|
|
const isAnyUiVisible = (...args) => pushRuntime.isAnyUiVisible(...args);
|
|
|
|
|
const isUiVisible = (...args) => pushRuntime.isUiVisible(...args);
|
|
|
|
|
const ensurePushInitialized = (...args) => pushRuntime.ensurePushInitialized(...args);
|
|
|
|
|
const setPushInitialized = (...args) => pushRuntime.setPushInitialized(...args);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW = 128;
|
|
|
|
|
const TERMINAL_INPUT_WS_REBIND_WINDOW_MS = 60 * 1000;
|
|
|
|
|
const TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS = 15 * 1000;
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const rejectWebSocketUpgrade = (...args) => requestSecurityRuntime.rejectWebSocketUpgrade(...args);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const isRequestOriginAllowed = (...args) => requestSecurityRuntime.isRequestOriginAllowed(...args);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const notificationEmitterRuntime = createNotificationEmitterRuntime({
|
|
|
|
|
process,
|
|
|
|
|
getDesktopNotifyEnabled: () => ENV_DESKTOP_NOTIFY,
|
|
|
|
|
desktopNotifyPrefix: DESKTOP_NOTIFY_PREFIX,
|
|
|
|
|
getUiNotificationClients: () => uiNotificationClients,
|
2026-04-17 16:07:26 +08:00
|
|
|
getBroadcastGlobalUiEvent: () => broadcastGlobalUiEvent,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const writeSseEvent = (...args) => notificationEmitterRuntime.writeSseEvent(...args);
|
|
|
|
|
const emitDesktopNotification = (...args) => notificationEmitterRuntime.emitDesktopNotification(...args);
|
2026-04-17 16:07:26 +08:00
|
|
|
const broadcastGlobalUiEvent = createGlobalUiEventBroadcaster({
|
|
|
|
|
sseClients: uiNotificationClients,
|
|
|
|
|
wsClients: uiNotificationWsClients,
|
|
|
|
|
writeSseEvent,
|
|
|
|
|
});
|
2026-03-31 18:47:00 +03:00
|
|
|
const broadcastUiNotification = (...args) => notificationEmitterRuntime.broadcastUiNotification(...args);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const sessionRuntime = createSessionRuntime({
|
|
|
|
|
writeSseEvent,
|
|
|
|
|
getNotificationClients: () => uiNotificationClients,
|
2026-04-17 16:07:26 +08:00
|
|
|
broadcastEvent: broadcastGlobalUiEvent,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-05-01 04:57:31 -07:00
|
|
|
const getActiveSessionCount = () => {
|
|
|
|
|
const snapshot = sessionRuntime.getSessionActivitySnapshot();
|
|
|
|
|
return Object.values(snapshot).filter((entry) => entry.type === 'busy').length;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getUpstreamStallTimeoutMs = () => (
|
|
|
|
|
getActiveSessionCount() > 1
|
|
|
|
|
? UPSTREAM_STALL_TIMEOUT_CONCURRENT_MS
|
|
|
|
|
: DEFAULT_UPSTREAM_STALL_TIMEOUT_MS
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-16 15:55:08 +03:00
|
|
|
const projectConfigRuntime = createProjectConfigRuntime({
|
|
|
|
|
fsPromises,
|
|
|
|
|
path,
|
|
|
|
|
projectsDirPath: OPENCHAMBER_PROJECTS_CONFIG_DIR,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
// HMR-persistent state via globalThis
|
|
|
|
|
// These values survive Vite HMR reloads to prevent zombie OpenCode processes
|
|
|
|
|
const hmrStateRuntime = createHmrStateRuntime({
|
|
|
|
|
globalThisLike: globalThis,
|
|
|
|
|
os,
|
|
|
|
|
processLike: process,
|
|
|
|
|
stateKey: '__openchamberHmrState',
|
|
|
|
|
});
|
|
|
|
|
const hmrState = hmrStateRuntime.getOrCreateHmrState();
|
|
|
|
|
hmrStateRuntime.ensureUserProvidedOpenCodePassword(hmrState);
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
// Non-HMR state (safe to reset on reload)
|
|
|
|
|
let healthCheckInterval = null;
|
|
|
|
|
let server = null;
|
|
|
|
|
let expressApp = null;
|
|
|
|
|
let currentRestartPromise = null;
|
|
|
|
|
let isRestartingOpenCode = false;
|
|
|
|
|
let openCodeApiPrefix = '';
|
|
|
|
|
let openCodeApiPrefixDetected = true;
|
|
|
|
|
let openCodeApiDetectionTimer = null;
|
|
|
|
|
let lastOpenCodeError = null;
|
2026-04-28 12:35:00 +03:00
|
|
|
let lastOpenCodeLaunchDiagnostics = null;
|
2026-03-31 18:47:00 +03:00
|
|
|
let isOpenCodeReady = false;
|
|
|
|
|
let openCodeNotReadySince = 0;
|
|
|
|
|
let isExternalOpenCode = false;
|
|
|
|
|
let exitOnShutdown = true;
|
|
|
|
|
let uiAuthController = null;
|
|
|
|
|
let activeTunnelController = null;
|
2026-04-05 15:36:11 +03:00
|
|
|
let globalWatcherStartPromise = null;
|
2026-03-31 18:47:00 +03:00
|
|
|
const tunnelProviderRegistry = createTunnelProviderRegistry([
|
|
|
|
|
createCloudflareTunnelProvider(),
|
2026-05-25 18:00:03 +03:00
|
|
|
createNgrokTunnelProvider(),
|
2026-03-31 18:47:00 +03:00
|
|
|
]);
|
|
|
|
|
tunnelProviderRegistry.seal();
|
|
|
|
|
const tunnelAuthController = createTunnelAuth();
|
|
|
|
|
let runtimeManagedRemoteTunnelToken = '';
|
|
|
|
|
let runtimeManagedRemoteTunnelHostname = '';
|
|
|
|
|
let terminalRuntime = null;
|
2026-04-17 16:07:26 +08:00
|
|
|
let messageStreamRuntime = null;
|
2026-03-31 18:47:00 +03:00
|
|
|
const userProvidedOpenCodePassword = hmrStateRuntime.getUserProvidedOpenCodePassword(hmrState);
|
|
|
|
|
const initialOpenCodeAuthState = hmrStateRuntime.resolveOpenCodeAuthFromState({
|
|
|
|
|
hmrState,
|
|
|
|
|
userProvidedOpenCodePassword,
|
|
|
|
|
});
|
|
|
|
|
let openCodeAuthPassword = initialOpenCodeAuthState.openCodeAuthPassword;
|
|
|
|
|
let openCodeAuthSource = initialOpenCodeAuthState.openCodeAuthSource;
|
2026-03-17 13:18:54 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
// Sync helper - call after modifying any HMR state variable
|
|
|
|
|
const syncToHmrState = () => {
|
|
|
|
|
hmrStateRuntime.syncStateFromRuntime(hmrState, {
|
|
|
|
|
openCodeProcess,
|
|
|
|
|
openCodePort,
|
|
|
|
|
openCodeBaseUrl,
|
|
|
|
|
isShuttingDown,
|
|
|
|
|
signalsAttached,
|
|
|
|
|
openCodeWorkingDirectory,
|
|
|
|
|
openCodeAuthPassword,
|
|
|
|
|
openCodeAuthSource,
|
|
|
|
|
});
|
2026-03-17 13:18:54 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
// Sync helper - call to restore state from HMR (e.g., on module reload)
|
|
|
|
|
const syncFromHmrState = () => {
|
|
|
|
|
const restored = hmrStateRuntime.restoreRuntimeFromState({
|
|
|
|
|
hmrState,
|
|
|
|
|
userProvidedOpenCodePassword,
|
|
|
|
|
});
|
|
|
|
|
openCodeProcess = restored.openCodeProcess;
|
|
|
|
|
openCodePort = restored.openCodePort;
|
|
|
|
|
openCodeBaseUrl = restored.openCodeBaseUrl;
|
|
|
|
|
isShuttingDown = restored.isShuttingDown;
|
|
|
|
|
signalsAttached = restored.signalsAttached;
|
|
|
|
|
openCodeWorkingDirectory = restored.openCodeWorkingDirectory;
|
|
|
|
|
openCodeAuthPassword = restored.openCodeAuthPassword;
|
|
|
|
|
openCodeAuthSource = restored.openCodeAuthSource;
|
2026-02-28 04:21:46 +02:00
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
// Module-level variables that shadow HMR state
|
|
|
|
|
// These are synced to/from hmrState to survive HMR reloads
|
|
|
|
|
let openCodeProcess = hmrState.openCodeProcess;
|
|
|
|
|
let openCodePort = hmrState.openCodePort;
|
|
|
|
|
let openCodeBaseUrl = hmrState.openCodeBaseUrl ?? null;
|
|
|
|
|
let isShuttingDown = hmrState.isShuttingDown;
|
|
|
|
|
let signalsAttached = hmrState.signalsAttached;
|
|
|
|
|
let openCodeWorkingDirectory = hmrState.openCodeWorkingDirectory;
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const {
|
|
|
|
|
configuredOpenCodePort: ENV_CONFIGURED_OPENCODE_PORT,
|
|
|
|
|
configuredOpenCodeHost: ENV_CONFIGURED_OPENCODE_HOST,
|
|
|
|
|
effectivePort: ENV_EFFECTIVE_PORT,
|
|
|
|
|
configuredOpenCodeHostname: ENV_CONFIGURED_OPENCODE_HOSTNAME,
|
|
|
|
|
} = resolveOpenCodeEnvConfig({
|
|
|
|
|
env: process.env,
|
|
|
|
|
logger: console,
|
|
|
|
|
});
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const ENV_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START === 'true' ||
|
|
|
|
|
process.env.OPENCHAMBER_SKIP_OPENCODE_START === 'true';
|
2026-04-06 17:36:08 +03:00
|
|
|
const ENV_DESKTOP_NOTIFY = (() => {
|
|
|
|
|
if (process.env.OPENCHAMBER_DESKTOP_NOTIFY === 'true') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (process.env.OPENCHAMBER_RUNTIME === 'desktop') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const argv0 = typeof process.argv?.[0] === 'string' ? process.argv[0] : '';
|
|
|
|
|
const argv1 = typeof process.argv?.[1] === 'string' ? process.argv[1] : '';
|
|
|
|
|
return /openchamber-server/i.test(argv0) || /openchamber-server/i.test(argv1);
|
|
|
|
|
})();
|
2026-03-31 18:47:00 +03:00
|
|
|
const ENV_CONFIGURED_OPENCODE_WSL_DISTRO =
|
|
|
|
|
typeof process.env.OPENCODE_WSL_DISTRO === 'string' && process.env.OPENCODE_WSL_DISTRO.trim().length > 0
|
|
|
|
|
? process.env.OPENCODE_WSL_DISTRO.trim()
|
|
|
|
|
: (
|
|
|
|
|
typeof process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO === 'string' &&
|
|
|
|
|
process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO.trim().length > 0
|
|
|
|
|
? process.env.OPENCHAMBER_OPENCODE_WSL_DISTRO.trim()
|
|
|
|
|
: null
|
|
|
|
|
);
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const openCodeAuthStateRuntime = createOpenCodeAuthStateRuntime({
|
|
|
|
|
crypto,
|
|
|
|
|
process,
|
|
|
|
|
getAuthPassword: () => openCodeAuthPassword,
|
|
|
|
|
setAuthPassword: (value) => {
|
|
|
|
|
openCodeAuthPassword = value;
|
|
|
|
|
},
|
|
|
|
|
getAuthSource: () => openCodeAuthSource,
|
|
|
|
|
setAuthSource: (value) => {
|
|
|
|
|
openCodeAuthSource = value;
|
|
|
|
|
},
|
|
|
|
|
getUserProvidedPassword: () => userProvidedOpenCodePassword,
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getOpenCodeAuthHeaders = (...args) => openCodeAuthStateRuntime.getOpenCodeAuthHeaders(...args);
|
|
|
|
|
const isOpenCodeConnectionSecure = (...args) => openCodeAuthStateRuntime.isOpenCodeConnectionSecure(...args);
|
|
|
|
|
const ensureLocalOpenCodeServerPassword = (...args) => openCodeAuthStateRuntime.ensureLocalOpenCodeServerPassword(...args);
|
|
|
|
|
|
|
|
|
|
const openCodeNetworkState = {};
|
|
|
|
|
Object.defineProperties(openCodeNetworkState, {
|
|
|
|
|
openCodePort: { get: () => openCodePort, set: (value) => { openCodePort = value; } },
|
|
|
|
|
openCodeBaseUrl: { get: () => openCodeBaseUrl, set: (value) => { openCodeBaseUrl = value; } },
|
|
|
|
|
openCodeApiPrefix: { get: () => openCodeApiPrefix, set: (value) => { openCodeApiPrefix = value; } },
|
|
|
|
|
openCodeApiPrefixDetected: { get: () => openCodeApiPrefixDetected, set: (value) => { openCodeApiPrefixDetected = value; } },
|
|
|
|
|
openCodeApiDetectionTimer: { get: () => openCodeApiDetectionTimer, set: (value) => { openCodeApiDetectionTimer = value; } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openCodeNetworkRuntime = createOpenCodeNetworkRuntime({
|
|
|
|
|
state: openCodeNetworkState,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const waitForReady = (...args) => openCodeNetworkRuntime.waitForReady(...args);
|
|
|
|
|
const normalizeApiPrefix = (...args) => openCodeNetworkRuntime.normalizeApiPrefix(...args);
|
|
|
|
|
const setDetectedOpenCodeApiPrefix = (...args) => openCodeNetworkRuntime.setDetectedOpenCodeApiPrefix(...args);
|
|
|
|
|
const buildOpenCodeUrl = (...args) => openCodeNetworkRuntime.buildOpenCodeUrl(...args);
|
|
|
|
|
const ensureOpenCodeApiPrefix = (...args) => openCodeNetworkRuntime.ensureOpenCodeApiPrefix(...args);
|
|
|
|
|
const scheduleOpenCodeApiDetection = (...args) => openCodeNetworkRuntime.scheduleOpenCodeApiDetection(...args);
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
|
|
|
|
|
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
|
|
|
|
);
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
if (ENV_CONFIGURED_API_PREFIX && ENV_CONFIGURED_API_PREFIX !== '') {
|
|
|
|
|
console.warn('Ignoring configured OpenCode API prefix; API runs at root.');
|
|
|
|
|
}
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
let cachedLoginShellEnvSnapshot;
|
|
|
|
|
let resolvedOpencodeBinary = null;
|
|
|
|
|
let resolvedOpencodeBinarySource = null;
|
|
|
|
|
let resolvedNodeBinary = null;
|
|
|
|
|
let resolvedBunBinary = null;
|
|
|
|
|
let resolvedGitBinary = null;
|
|
|
|
|
let useWslForOpencode = false;
|
|
|
|
|
let resolvedWslBinary = null;
|
|
|
|
|
let resolvedWslOpencodePath = null;
|
|
|
|
|
let resolvedWslDistro = null;
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const openCodeEnvState = {};
|
|
|
|
|
Object.defineProperties(openCodeEnvState, {
|
|
|
|
|
cachedLoginShellEnvSnapshot: { get: () => cachedLoginShellEnvSnapshot, set: (value) => { cachedLoginShellEnvSnapshot = value; } },
|
|
|
|
|
resolvedOpencodeBinary: { get: () => resolvedOpencodeBinary, set: (value) => { resolvedOpencodeBinary = value; } },
|
|
|
|
|
resolvedOpencodeBinarySource: { get: () => resolvedOpencodeBinarySource, set: (value) => { resolvedOpencodeBinarySource = value; } },
|
|
|
|
|
resolvedNodeBinary: { get: () => resolvedNodeBinary, set: (value) => { resolvedNodeBinary = value; } },
|
|
|
|
|
resolvedBunBinary: { get: () => resolvedBunBinary, set: (value) => { resolvedBunBinary = value; } },
|
|
|
|
|
resolvedGitBinary: { get: () => resolvedGitBinary, set: (value) => { resolvedGitBinary = value; } },
|
|
|
|
|
useWslForOpencode: { get: () => useWslForOpencode, set: (value) => { useWslForOpencode = value; } },
|
|
|
|
|
resolvedWslBinary: { get: () => resolvedWslBinary, set: (value) => { resolvedWslBinary = value; } },
|
|
|
|
|
resolvedWslOpencodePath: { get: () => resolvedWslOpencodePath, set: (value) => { resolvedWslOpencodePath = value; } },
|
|
|
|
|
resolvedWslDistro: { get: () => resolvedWslDistro, set: (value) => { resolvedWslDistro = value; } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openCodeEnvRuntime = createOpenCodeEnvRuntime({
|
|
|
|
|
state: openCodeEnvState,
|
|
|
|
|
normalizeDirectoryPath,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
ENV_CONFIGURED_OPENCODE_WSL_DISTRO,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const applyLoginShellEnvSnapshot = (...args) => openCodeEnvRuntime.applyLoginShellEnvSnapshot(...args);
|
|
|
|
|
const getLoginShellEnvSnapshot = (...args) => openCodeEnvRuntime.getLoginShellEnvSnapshot(...args);
|
|
|
|
|
const ensureOpencodeCliEnv = (...args) => openCodeEnvRuntime.ensureOpencodeCliEnv(...args);
|
|
|
|
|
const applyOpencodeBinaryFromSettings = (...args) => openCodeEnvRuntime.applyOpencodeBinaryFromSettings(...args);
|
|
|
|
|
const resolveOpencodeCliPath = (...args) => openCodeEnvRuntime.resolveOpencodeCliPath(...args);
|
|
|
|
|
const isExecutable = (...args) => openCodeEnvRuntime.isExecutable(...args);
|
|
|
|
|
const searchPathFor = (...args) => openCodeEnvRuntime.searchPathFor(...args);
|
|
|
|
|
const resolveGitBinaryForSpawn = (...args) => openCodeEnvRuntime.resolveGitBinaryForSpawn(...args);
|
|
|
|
|
const resolveWslExecutablePath = (...args) => openCodeEnvRuntime.resolveWslExecutablePath(...args);
|
|
|
|
|
const buildWslExecArgs = (...args) => openCodeEnvRuntime.buildWslExecArgs(...args);
|
2026-04-12 07:29:09 +12:00
|
|
|
const resolveManagedOpenCodeLaunchSpec = (...args) => openCodeEnvRuntime.resolveManagedOpenCodeLaunchSpec(...args);
|
2026-03-31 18:47:00 +03:00
|
|
|
const clearResolvedOpenCodeBinary = (...args) => openCodeEnvRuntime.clearResolvedOpenCodeBinary(...args);
|
|
|
|
|
const openCodeResolutionRuntime = createOpenCodeResolutionRuntime({
|
|
|
|
|
path,
|
|
|
|
|
resolveOpencodeCliPath,
|
|
|
|
|
applyOpencodeBinaryFromSettings,
|
|
|
|
|
ensureOpencodeCliEnv,
|
2026-04-12 07:29:09 +12:00
|
|
|
resolveManagedOpenCodeLaunchSpec,
|
2026-03-31 18:47:00 +03:00
|
|
|
getResolvedState: () => ({
|
|
|
|
|
resolvedOpencodeBinary,
|
|
|
|
|
resolvedOpencodeBinarySource,
|
|
|
|
|
useWslForOpencode,
|
|
|
|
|
resolvedWslBinary,
|
|
|
|
|
resolvedWslOpencodePath,
|
|
|
|
|
resolvedWslDistro,
|
|
|
|
|
resolvedNodeBinary,
|
|
|
|
|
resolvedBunBinary,
|
|
|
|
|
}),
|
|
|
|
|
setResolvedOpencodeBinarySource: (value) => {
|
|
|
|
|
resolvedOpencodeBinarySource = value;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const getOpenCodeResolutionSnapshot = (...args) =>
|
|
|
|
|
openCodeResolutionRuntime.getOpenCodeResolutionSnapshot(...args);
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
applyLoginShellEnvSnapshot();
|
2026-02-28 04:21:46 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
notificationTemplateRuntime = createNotificationTemplateRuntime({
|
|
|
|
|
readSettingsFromDisk,
|
|
|
|
|
persistSettings,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
resolveGitBinaryForSpawn,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const notificationTriggerRuntime = createNotificationTriggerRuntime({
|
|
|
|
|
readSettingsFromDisk,
|
|
|
|
|
prepareNotificationLastMessage,
|
|
|
|
|
buildTemplateVariables,
|
|
|
|
|
extractLastMessageText,
|
|
|
|
|
fetchLastAssistantMessageText,
|
|
|
|
|
resolveNotificationTemplate,
|
|
|
|
|
shouldApplyResolvedTemplateMessage,
|
|
|
|
|
emitDesktopNotification,
|
|
|
|
|
broadcastUiNotification,
|
|
|
|
|
sendPushToAllUiSessions,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const maybeSendPushForTrigger = (...args) => notificationTriggerRuntime.maybeSendPushForTrigger(...args);
|
2026-04-22 19:22:50 +03:00
|
|
|
const setAutoAcceptSession = (...args) => notificationTriggerRuntime.setAutoAcceptSession(...args);
|
2026-03-31 18:47:00 +03:00
|
|
|
|
2026-04-24 11:58:16 +03:00
|
|
|
const globalMessageStreamHub = createGlobalMessageStreamHub({
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
2026-05-01 04:57:31 -07:00
|
|
|
upstreamStallTimeoutMs: getUpstreamStallTimeoutMs,
|
2026-04-24 11:58:16 +03:00
|
|
|
});
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
|
|
|
|
|
waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
parseSseDataPayload: (...args) => parseSseDataPayload(...args),
|
2026-04-24 11:58:16 +03:00
|
|
|
globalEventHub: globalMessageStreamHub,
|
2026-03-31 18:47:00 +03:00
|
|
|
onPayload: (payload) => {
|
|
|
|
|
maybeCacheSessionInfoFromEvent(payload);
|
|
|
|
|
void maybeSendPushForTrigger(payload);
|
|
|
|
|
sessionRuntime.processOpenCodeSsePayload(payload);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-17 16:07:26 +08:00
|
|
|
const processForwardedEventPayload = (payload, emitSyntheticEvent) => {
|
|
|
|
|
if (!payload || typeof payload !== 'object' || typeof emitSyntheticEvent !== 'function') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maybeCacheSessionInfoFromEvent(payload);
|
|
|
|
|
|
|
|
|
|
if (payload.type !== 'session.status') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const properties = payload.properties && typeof payload.properties === 'object' ? payload.properties : {};
|
2026-05-06 23:45:31 +07:00
|
|
|
const statusInfo = properties.status && typeof properties.status === 'object' ? properties.status : {};
|
2026-04-17 16:07:26 +08:00
|
|
|
const info = properties.info && typeof properties.info === 'object' ? properties.info : {};
|
|
|
|
|
const sessionId = typeof properties.sessionID === 'string' ? properties.sessionID.trim() : '';
|
2026-05-06 23:45:31 +07:00
|
|
|
const status = typeof statusInfo.type === 'string'
|
|
|
|
|
? statusInfo.type.trim()
|
|
|
|
|
: (typeof info.type === 'string' ? info.type.trim() : '');
|
2026-04-17 16:07:26 +08:00
|
|
|
|
|
|
|
|
if (!sessionId || !status) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emitSyntheticEvent({
|
|
|
|
|
type: 'openchamber:session-status',
|
|
|
|
|
properties: {
|
2026-05-24 19:17:04 +03:00
|
|
|
sessionID: sessionId,
|
2026-04-17 16:07:26 +08:00
|
|
|
status,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
metadata: {
|
2026-05-06 23:45:31 +07:00
|
|
|
attempt: typeof statusInfo.attempt === 'number'
|
|
|
|
|
? statusInfo.attempt
|
|
|
|
|
: (typeof info.attempt === 'number' ? info.attempt : undefined),
|
|
|
|
|
message: typeof statusInfo.message === 'string'
|
|
|
|
|
? statusInfo.message
|
|
|
|
|
: (typeof info.message === 'string' ? info.message : undefined),
|
|
|
|
|
next: typeof statusInfo.next === 'number'
|
|
|
|
|
? statusInfo.next
|
|
|
|
|
: (typeof info.next === 'number' ? info.next : undefined),
|
2026-04-17 16:07:26 +08:00
|
|
|
},
|
|
|
|
|
needsAttention: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
emitSyntheticEvent({
|
|
|
|
|
type: 'openchamber:session-activity',
|
|
|
|
|
properties: {
|
|
|
|
|
sessionId,
|
|
|
|
|
phase: status === 'busy' || status === 'retry' ? 'busy' : 'idle',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
|
|
|
|
|
const serverUtilsRuntime = createServerUtilsRuntime({
|
|
|
|
|
fs,
|
|
|
|
|
os,
|
|
|
|
|
path,
|
|
|
|
|
process,
|
|
|
|
|
openCodeReadyGraceMs: OPEN_CODE_READY_GRACE_MS,
|
|
|
|
|
longRequestTimeoutMs: LONG_REQUEST_TIMEOUT_MS,
|
|
|
|
|
getRuntime: () => ({
|
|
|
|
|
openCodePort,
|
2026-04-07 22:48:17 +03:00
|
|
|
openCodeBaseUrl,
|
2026-03-31 18:47:00 +03:00
|
|
|
openCodeNotReadySince,
|
|
|
|
|
isOpenCodeReady,
|
|
|
|
|
isRestartingOpenCode,
|
|
|
|
|
}),
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
ensureOpenCodeApiPrefix,
|
|
|
|
|
getUiNotificationClients: () => uiNotificationClients,
|
|
|
|
|
getOpenCodePort: () => openCodePort,
|
|
|
|
|
setOpenCodePortState: (value) => {
|
|
|
|
|
openCodePort = value;
|
|
|
|
|
},
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
markOpenCodeNotReady: () => {
|
|
|
|
|
isOpenCodeReady = false;
|
|
|
|
|
},
|
|
|
|
|
setOpenCodeNotReadySince: (value) => {
|
|
|
|
|
openCodeNotReadySince = value;
|
|
|
|
|
},
|
|
|
|
|
clearLastOpenCodeError: () => {
|
|
|
|
|
lastOpenCodeError = null;
|
|
|
|
|
},
|
|
|
|
|
getLoginShellPath: () => {
|
|
|
|
|
const snapshot = getLoginShellEnvSnapshot();
|
|
|
|
|
if (!snapshot || typeof snapshot.PATH !== 'string' || snapshot.PATH.length === 0) {
|
|
|
|
|
return null;
|
2026-02-28 04:21:46 +02:00
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
return snapshot.PATH;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const setOpenCodePort = (...args) => serverUtilsRuntime.setOpenCodePort(...args);
|
|
|
|
|
const waitForOpenCodePort = (...args) => serverUtilsRuntime.waitForOpenCodePort(...args);
|
|
|
|
|
const buildAugmentedPath = (...args) => serverUtilsRuntime.buildAugmentedPath(...args);
|
2026-04-25 20:44:07 +03:00
|
|
|
const buildManagedOpenCodePath = (...args) => serverUtilsRuntime.buildManagedOpenCodePath(...args);
|
2026-03-31 18:47:00 +03:00
|
|
|
const parseSseDataPayload = (...args) => serverUtilsRuntime.parseSseDataPayload(...args);
|
|
|
|
|
const staticRoutesRuntime = createStaticRoutesRuntime({
|
|
|
|
|
fs,
|
|
|
|
|
path,
|
|
|
|
|
process,
|
|
|
|
|
__dirname,
|
|
|
|
|
express,
|
|
|
|
|
resolveProjectDirectory,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
normalizePwaAppName,
|
2026-04-22 03:17:34 +07:00
|
|
|
normalizePwaOrientation,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
|
|
|
|
const featureRoutesRuntime = createFeatureRoutesRuntime({
|
|
|
|
|
clientReloadDelayMs: CLIENT_RELOAD_DELAY_MS,
|
|
|
|
|
});
|
|
|
|
|
const bootstrapRuntime = createBootstrapRuntime({
|
|
|
|
|
createUiAuth,
|
|
|
|
|
registerServerStatusRoutes,
|
|
|
|
|
registerCommonRequestMiddleware,
|
|
|
|
|
registerAuthAndAccessRoutes,
|
|
|
|
|
registerTtsRoutes,
|
|
|
|
|
registerNotificationRoutes,
|
|
|
|
|
registerOpenChamberRoutes,
|
|
|
|
|
express,
|
|
|
|
|
});
|
|
|
|
|
const tunnelWiringRuntime = createTunnelWiringRuntime({
|
|
|
|
|
crypto,
|
|
|
|
|
URL,
|
|
|
|
|
tunnelProviderRegistry,
|
|
|
|
|
tunnelAuthController,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
readManagedRemoteTunnelConfigFromDisk,
|
|
|
|
|
normalizeTunnelProvider,
|
|
|
|
|
normalizeTunnelMode,
|
|
|
|
|
normalizeOptionalPath,
|
|
|
|
|
normalizeManagedRemoteTunnelHostname,
|
|
|
|
|
normalizeTunnelBootstrapTtlMs,
|
|
|
|
|
normalizeTunnelSessionTtlMs,
|
|
|
|
|
isSupportedTunnelMode,
|
|
|
|
|
upsertManagedRemoteTunnelToken,
|
|
|
|
|
resolveManagedRemoteTunnelToken,
|
|
|
|
|
TUNNEL_MODE_QUICK,
|
|
|
|
|
TUNNEL_MODE_MANAGED_LOCAL,
|
|
|
|
|
TUNNEL_MODE_MANAGED_REMOTE,
|
|
|
|
|
TUNNEL_PROVIDER_CLOUDFLARE,
|
|
|
|
|
TunnelServiceError,
|
|
|
|
|
getActiveTunnelController: () => activeTunnelController,
|
|
|
|
|
setActiveTunnelController: (value) => {
|
|
|
|
|
activeTunnelController = value;
|
|
|
|
|
},
|
|
|
|
|
getRuntimeManagedRemoteTunnelHostname: () => runtimeManagedRemoteTunnelHostname,
|
|
|
|
|
setRuntimeManagedRemoteTunnelHostname: (value) => {
|
|
|
|
|
runtimeManagedRemoteTunnelHostname = value;
|
|
|
|
|
},
|
|
|
|
|
getRuntimeManagedRemoteTunnelToken: () => runtimeManagedRemoteTunnelToken,
|
|
|
|
|
setRuntimeManagedRemoteTunnelToken: (value) => {
|
|
|
|
|
runtimeManagedRemoteTunnelToken = value;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const startupPipelineRuntime = createStartupPipelineRuntime({
|
|
|
|
|
createTerminalRuntime,
|
2026-04-17 16:07:26 +08:00
|
|
|
createMessageStreamWsRuntime,
|
2026-03-31 18:47:00 +03:00
|
|
|
createServerStartupRuntime,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openCodeLifecycleState = {};
|
|
|
|
|
Object.defineProperties(openCodeLifecycleState, {
|
|
|
|
|
openCodeProcess: { get: () => openCodeProcess, set: (value) => { openCodeProcess = value; } },
|
|
|
|
|
openCodePort: { get: () => openCodePort, set: (value) => { openCodePort = value; } },
|
|
|
|
|
openCodeBaseUrl: { get: () => openCodeBaseUrl, set: (value) => { openCodeBaseUrl = value; } },
|
|
|
|
|
openCodeWorkingDirectory: { get: () => openCodeWorkingDirectory, set: (value) => { openCodeWorkingDirectory = value; } },
|
|
|
|
|
currentRestartPromise: { get: () => currentRestartPromise, set: (value) => { currentRestartPromise = value; } },
|
|
|
|
|
isRestartingOpenCode: { get: () => isRestartingOpenCode, set: (value) => { isRestartingOpenCode = value; } },
|
|
|
|
|
openCodeApiPrefix: { get: () => openCodeApiPrefix, set: (value) => { openCodeApiPrefix = value; } },
|
|
|
|
|
openCodeApiPrefixDetected: { get: () => openCodeApiPrefixDetected, set: (value) => { openCodeApiPrefixDetected = value; } },
|
|
|
|
|
openCodeApiDetectionTimer: { get: () => openCodeApiDetectionTimer, set: (value) => { openCodeApiDetectionTimer = value; } },
|
|
|
|
|
lastOpenCodeError: { get: () => lastOpenCodeError, set: (value) => { lastOpenCodeError = value; } },
|
2026-04-28 12:35:00 +03:00
|
|
|
lastOpenCodeLaunchDiagnostics: { get: () => lastOpenCodeLaunchDiagnostics, set: (value) => { lastOpenCodeLaunchDiagnostics = value; } },
|
2026-03-31 18:47:00 +03:00
|
|
|
isOpenCodeReady: { get: () => isOpenCodeReady, set: (value) => { isOpenCodeReady = value; } },
|
|
|
|
|
openCodeNotReadySince: { get: () => openCodeNotReadySince, set: (value) => { openCodeNotReadySince = value; } },
|
|
|
|
|
isExternalOpenCode: { get: () => isExternalOpenCode, set: (value) => { isExternalOpenCode = value; } },
|
|
|
|
|
isShuttingDown: { get: () => isShuttingDown, set: (value) => { isShuttingDown = value; } },
|
|
|
|
|
healthCheckInterval: { get: () => healthCheckInterval, set: (value) => { healthCheckInterval = value; } },
|
|
|
|
|
expressApp: { get: () => expressApp, set: (value) => { expressApp = value; } },
|
|
|
|
|
useWslForOpencode: { get: () => useWslForOpencode, set: (value) => { useWslForOpencode = value; } },
|
|
|
|
|
resolvedWslBinary: { get: () => resolvedWslBinary, set: (value) => { resolvedWslBinary = value; } },
|
|
|
|
|
resolvedWslOpencodePath: { get: () => resolvedWslOpencodePath, set: (value) => { resolvedWslOpencodePath = value; } },
|
|
|
|
|
resolvedWslDistro: { get: () => resolvedWslDistro, set: (value) => { resolvedWslDistro = value; } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
|
|
|
|
|
state: openCodeLifecycleState,
|
|
|
|
|
env: {
|
|
|
|
|
ENV_CONFIGURED_OPENCODE_PORT,
|
|
|
|
|
ENV_CONFIGURED_OPENCODE_HOST,
|
|
|
|
|
ENV_EFFECTIVE_PORT,
|
|
|
|
|
ENV_CONFIGURED_OPENCODE_HOSTNAME,
|
|
|
|
|
ENV_SKIP_OPENCODE_START,
|
|
|
|
|
},
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
syncFromHmrState,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
waitForReady,
|
|
|
|
|
normalizeApiPrefix,
|
|
|
|
|
applyOpencodeBinaryFromSettings,
|
|
|
|
|
ensureOpencodeCliEnv,
|
|
|
|
|
ensureLocalOpenCodeServerPassword,
|
|
|
|
|
buildWslExecArgs,
|
|
|
|
|
resolveWslExecutablePath,
|
2026-04-12 07:29:09 +12:00
|
|
|
resolveManagedOpenCodeLaunchSpec,
|
2026-03-31 18:47:00 +03:00
|
|
|
setOpenCodePort,
|
|
|
|
|
setDetectedOpenCodeApiPrefix,
|
|
|
|
|
setupProxy: (...args) => setupProxy(...args),
|
|
|
|
|
ensureOpenCodeApiPrefix,
|
|
|
|
|
clearResolvedOpenCodeBinary,
|
2026-04-25 20:44:07 +03:00
|
|
|
buildAugmentedPath,
|
|
|
|
|
buildManagedOpenCodePath,
|
2026-04-28 12:35:00 +03:00
|
|
|
getManagedOpenCodeShellEnvSnapshot: getLoginShellEnvSnapshot,
|
2026-05-01 04:57:31 -07:00
|
|
|
getActiveSessionCount,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args);
|
|
|
|
|
const waitForOpenCodeReady = (...args) => openCodeLifecycleRuntime.waitForOpenCodeReady(...args);
|
|
|
|
|
const waitForAgentPresence = (...args) => openCodeLifecycleRuntime.waitForAgentPresence(...args);
|
|
|
|
|
const refreshOpenCodeAfterConfigChange = (...args) => openCodeLifecycleRuntime.refreshOpenCodeAfterConfigChange(...args);
|
|
|
|
|
const startHealthMonitoring = () => openCodeLifecycleRuntime.startHealthMonitoring(HEALTH_CHECK_INTERVAL);
|
2026-04-17 23:48:39 +08:00
|
|
|
const triggerHealthCheck = () => openCodeLifecycleRuntime.triggerHealthCheck();
|
2026-04-16 15:55:08 +03:00
|
|
|
const scheduledTasksRuntime = createScheduledTasksRuntime({
|
|
|
|
|
projectConfigRuntime,
|
|
|
|
|
listProjects: async () => {
|
|
|
|
|
const settings = await readSettingsFromDiskMigrated();
|
|
|
|
|
return sanitizeProjects(settings?.projects || []);
|
|
|
|
|
},
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
waitForOpenCodeReady,
|
|
|
|
|
emitTaskRunEvent: (event) => {
|
|
|
|
|
for (const client of uiOpenChamberEventClients) {
|
|
|
|
|
try {
|
|
|
|
|
writeSseEvent(client, {
|
|
|
|
|
type: 'openchamber:scheduled-task-ran',
|
|
|
|
|
properties: {
|
|
|
|
|
projectId: event.projectID,
|
|
|
|
|
taskId: event.taskID,
|
|
|
|
|
ranAt: event.ranAt,
|
|
|
|
|
status: event.status,
|
|
|
|
|
...(event.sessionID ? { sessionId: event.sessionID } : {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
uiOpenChamberEventClients.delete(client);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
logger: console,
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-05 15:36:11 +03:00
|
|
|
const ensureGlobalWatcherStarted = async () => {
|
|
|
|
|
if (globalWatcherStartPromise) {
|
|
|
|
|
return globalWatcherStartPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
globalWatcherStartPromise = openCodeWatcherRuntime.start().catch((error) => {
|
|
|
|
|
globalWatcherStartPromise = null;
|
|
|
|
|
throw error;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return globalWatcherStartPromise;
|
|
|
|
|
};
|
2026-03-31 18:47:00 +03:00
|
|
|
const bootstrapOpenCodeAtStartup = async (...args) => {
|
|
|
|
|
await openCodeLifecycleRuntime.bootstrapOpenCodeAtStartup(...args);
|
|
|
|
|
scheduleOpenCodeApiDetection();
|
2026-04-05 15:36:11 +03:00
|
|
|
if (openCodeLifecycleState.openCodeProcess && !openCodeLifecycleState.isExternalOpenCode) {
|
|
|
|
|
startHealthMonitoring();
|
|
|
|
|
}
|
2026-04-06 17:36:08 +03:00
|
|
|
if (ENV_DESKTOP_NOTIFY) {
|
|
|
|
|
void ensureGlobalWatcherStarted().catch((error) => {
|
|
|
|
|
console.warn(`Global event watcher startup failed: ${error?.message || error}`);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
};
|
|
|
|
|
const killProcessOnPort = (...args) => openCodeLifecycleRuntime.killProcessOnPort(...args);
|
2026-04-12 07:29:09 +12:00
|
|
|
const waitForPortRelease = (...args) => openCodeLifecycleRuntime.waitForPortRelease(...args);
|
2026-03-31 18:47:00 +03:00
|
|
|
|
|
|
|
|
const fetchAgentsSnapshot = (...args) => serverUtilsRuntime.fetchAgentsSnapshot(...args);
|
|
|
|
|
const fetchProvidersSnapshot = (...args) => serverUtilsRuntime.fetchProvidersSnapshot(...args);
|
|
|
|
|
const fetchModelsSnapshot = (...args) => serverUtilsRuntime.fetchModelsSnapshot(...args);
|
|
|
|
|
const setupProxy = (...args) => serverUtilsRuntime.setupProxy(...args);
|
|
|
|
|
const gracefulShutdownRuntime = createGracefulShutdownRuntime({
|
|
|
|
|
process,
|
|
|
|
|
shutdownTimeoutMs: SHUTDOWN_TIMEOUT,
|
|
|
|
|
getExitOnShutdown: () => exitOnShutdown,
|
|
|
|
|
getIsShuttingDown: () => isShuttingDown,
|
|
|
|
|
setIsShuttingDown: (value) => {
|
|
|
|
|
isShuttingDown = value;
|
|
|
|
|
},
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
openCodeWatcherRuntime,
|
|
|
|
|
sessionRuntime,
|
|
|
|
|
getHealthCheckInterval: () => healthCheckInterval,
|
|
|
|
|
clearHealthCheckInterval: (value) => clearInterval(value),
|
|
|
|
|
getTerminalRuntime: () => terminalRuntime,
|
|
|
|
|
setTerminalRuntime: (value) => {
|
|
|
|
|
terminalRuntime = value;
|
|
|
|
|
},
|
2026-04-17 16:07:26 +08:00
|
|
|
getMessageStreamRuntime: () => messageStreamRuntime,
|
|
|
|
|
setMessageStreamRuntime: (value) => {
|
|
|
|
|
messageStreamRuntime = value;
|
|
|
|
|
},
|
2026-03-31 18:47:00 +03:00
|
|
|
shouldSkipOpenCodeStop: () => ENV_SKIP_OPENCODE_START || isExternalOpenCode,
|
|
|
|
|
getOpenCodePort: () => openCodePort,
|
|
|
|
|
getOpenCodeProcess: () => openCodeProcess,
|
|
|
|
|
setOpenCodeProcess: (value) => {
|
|
|
|
|
openCodeProcess = value;
|
|
|
|
|
},
|
|
|
|
|
killProcessOnPort,
|
2026-04-12 07:29:09 +12:00
|
|
|
waitForPortRelease,
|
2026-03-31 18:47:00 +03:00
|
|
|
getServer: () => server,
|
|
|
|
|
getUiAuthController: () => uiAuthController,
|
|
|
|
|
setUiAuthController: (value) => {
|
|
|
|
|
uiAuthController = value;
|
|
|
|
|
},
|
|
|
|
|
getActiveTunnelController: () => activeTunnelController,
|
|
|
|
|
setActiveTunnelController: (value) => {
|
|
|
|
|
activeTunnelController = value;
|
|
|
|
|
},
|
|
|
|
|
tunnelAuthController,
|
2026-04-16 15:55:08 +03:00
|
|
|
scheduledTasksRuntime,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const gracefulShutdown = (...args) => gracefulShutdownRuntime.gracefulShutdown(...args);
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
async function main(options = {}) {
|
|
|
|
|
const port = Number.isFinite(options.port) && options.port >= 0 ? Math.trunc(options.port) : DEFAULT_PORT;
|
|
|
|
|
const host = typeof options.host === 'string' && options.host.length > 0 ? options.host : undefined;
|
|
|
|
|
const tryCfTunnel = options.tryCfTunnel === true;
|
|
|
|
|
const shouldUseCanonicalTunnelConfig = typeof options.tunnelMode === 'string'
|
|
|
|
|
|| typeof options.tunnelProvider === 'string'
|
|
|
|
|
|| options.tunnelConfigPath === null
|
|
|
|
|
|| typeof options.tunnelConfigPath === 'string'
|
|
|
|
|
|| typeof options.tunnelToken === 'string'
|
|
|
|
|
|| typeof options.tunnelHostname === 'string';
|
|
|
|
|
const startupTunnelRequest = shouldUseCanonicalTunnelConfig
|
|
|
|
|
? normalizeTunnelStartRequest({
|
|
|
|
|
provider: normalizeTunnelProvider(options.tunnelProvider),
|
|
|
|
|
mode: options.tunnelMode,
|
|
|
|
|
configPath: normalizeOptionalPath(options.tunnelConfigPath),
|
|
|
|
|
token: typeof options.tunnelToken === 'string' ? options.tunnelToken.trim() : '',
|
|
|
|
|
hostname: normalizeManagedRemoteTunnelHostname(options.tunnelHostname),
|
|
|
|
|
})
|
|
|
|
|
: (tryCfTunnel
|
|
|
|
|
? {
|
|
|
|
|
provider: TUNNEL_PROVIDER_CLOUDFLARE,
|
|
|
|
|
mode: TUNNEL_MODE_QUICK,
|
|
|
|
|
configPath: undefined,
|
|
|
|
|
token: '',
|
|
|
|
|
hostname: undefined,
|
|
|
|
|
}
|
|
|
|
|
: null);
|
|
|
|
|
const attachSignals = options.attachSignals !== false;
|
|
|
|
|
const onTunnelReady = typeof options.onTunnelReady === 'function' ? options.onTunnelReady : null;
|
|
|
|
|
if (typeof options.exitOnShutdown === 'boolean') {
|
|
|
|
|
exitOnShutdown = options.exitOnShutdown;
|
2026-02-01 18:29:34 +02:00
|
|
|
}
|
2026-04-20 15:41:15 +03:00
|
|
|
if (typeof options.onDesktopNotification === 'function') {
|
|
|
|
|
notificationEmitterRuntime.setOnDesktopNotification(options.onDesktopNotification);
|
|
|
|
|
}
|
2026-05-13 17:43:08 +03:00
|
|
|
if (typeof options.getIsWindowFocused === 'function') {
|
|
|
|
|
notificationTriggerRuntime.setGetIsWindowFocused(options.getIsWindowFocused);
|
|
|
|
|
}
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
console.log(`Starting OpenChamber on port ${port === 0 ? 'auto' : port}`);
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const sayTTSCapability = await detectSayTtsCapability(process);
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const app = express();
|
|
|
|
|
const serverStartedAt = new Date().toISOString();
|
|
|
|
|
app.set('trust proxy', true);
|
2026-04-17 21:26:56 +08:00
|
|
|
app.use(compression({
|
|
|
|
|
filter: (req, res) => {
|
|
|
|
|
if (shouldSkipCompression(req, res)) return false;
|
|
|
|
|
return compression.filter(req, res);
|
|
|
|
|
},
|
|
|
|
|
threshold: 1024,
|
|
|
|
|
}));
|
2026-03-31 18:47:00 +03:00
|
|
|
expressApp = app;
|
|
|
|
|
server = http.createServer(app);
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const uiPassword = typeof options.uiPassword === 'string' ? options.uiPassword : null;
|
|
|
|
|
const bootstrapResult = bootstrapRuntime.setupBaseRoutes(app, {
|
|
|
|
|
process,
|
|
|
|
|
openchamberVersion: OPENCHAMBER_VERSION,
|
|
|
|
|
runtimeName: process.env.OPENCHAMBER_RUNTIME || 'web',
|
|
|
|
|
serverStartedAt,
|
|
|
|
|
gracefulShutdown,
|
2026-04-12 07:29:09 +12:00
|
|
|
getHealthSnapshot: () => {
|
|
|
|
|
const launchSpec = resolvedOpencodeBinary && !useWslForOpencode
|
|
|
|
|
? resolveManagedOpenCodeLaunchSpec(resolvedOpencodeBinary)
|
|
|
|
|
: null;
|
|
|
|
|
return {
|
|
|
|
|
openCodePort,
|
|
|
|
|
openCodeRunning: Boolean(openCodePort && isOpenCodeReady && !isRestartingOpenCode),
|
|
|
|
|
openCodeSecureConnection: isOpenCodeConnectionSecure(),
|
|
|
|
|
openCodeAuthSource: openCodeAuthSource || null,
|
|
|
|
|
openCodeApiPrefix: '',
|
|
|
|
|
openCodeApiPrefixDetected: true,
|
|
|
|
|
isOpenCodeReady,
|
|
|
|
|
lastOpenCodeError,
|
2026-04-28 12:35:00 +03:00
|
|
|
lastOpenCodeLaunchDiagnostics,
|
2026-04-12 07:29:09 +12:00
|
|
|
opencodeBinaryResolved: resolvedOpencodeBinary || null,
|
|
|
|
|
opencodeBinarySource: resolvedOpencodeBinarySource || null,
|
|
|
|
|
opencodeLaunchBinary: launchSpec?.binary || null,
|
|
|
|
|
opencodeLaunchArgs: launchSpec?.args || [],
|
|
|
|
|
opencodeLaunchWrapperType: launchSpec?.wrapperType || null,
|
|
|
|
|
opencodeViaWsl: useWslForOpencode,
|
|
|
|
|
opencodeWslBinary: resolvedWslBinary || null,
|
|
|
|
|
opencodeWslPath: resolvedWslOpencodePath || null,
|
|
|
|
|
opencodeWslDistro: resolvedWslDistro || null,
|
|
|
|
|
nodeBinaryResolved: resolvedNodeBinary || null,
|
|
|
|
|
bunBinaryResolved: resolvedBunBinary || null,
|
|
|
|
|
desktopNotifyEnabled: ENV_DESKTOP_NOTIFY,
|
|
|
|
|
planModeExperimentalEnabled: PLAN_MODE_EXPERIMENT_ENABLED,
|
|
|
|
|
};
|
|
|
|
|
},
|
2026-05-05 18:47:00 +03:00
|
|
|
verboseRequestLogs: OPENCHAMBER_VERBOSE_REQUEST_LOGS,
|
2026-03-31 18:47:00 +03:00
|
|
|
uiPassword,
|
|
|
|
|
tunnelAuthController,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
normalizeTunnelSessionTtlMs,
|
|
|
|
|
sayTTSCapability,
|
|
|
|
|
ensurePushInitialized,
|
2026-04-05 15:36:11 +03:00
|
|
|
ensureGlobalWatcherStarted,
|
2026-03-31 18:47:00 +03:00
|
|
|
getOrCreateVapidKeys,
|
|
|
|
|
getUiSessionTokenFromRequest,
|
|
|
|
|
writeSettingsToDisk,
|
|
|
|
|
addOrUpdatePushSubscription,
|
|
|
|
|
removePushSubscription,
|
|
|
|
|
updateUiVisibility,
|
|
|
|
|
isUiVisible,
|
2026-04-06 17:36:08 +03:00
|
|
|
getUiNotificationClients: () => uiNotificationClients,
|
|
|
|
|
writeSseEvent,
|
2026-03-31 18:47:00 +03:00
|
|
|
sessionRuntime,
|
|
|
|
|
setPushInitialized,
|
|
|
|
|
fs,
|
|
|
|
|
os,
|
|
|
|
|
path,
|
|
|
|
|
server,
|
|
|
|
|
__dirname,
|
|
|
|
|
openchamberDataDir: OPENCHAMBER_DATA_DIR,
|
|
|
|
|
modelsDevApiUrl: MODELS_DEV_API_URL,
|
|
|
|
|
modelsMetadataCacheTtl: MODELS_METADATA_CACHE_TTL,
|
|
|
|
|
fetchFreeZenModels,
|
|
|
|
|
getCachedZenModels,
|
2026-04-22 19:22:50 +03:00
|
|
|
setAutoAcceptSession,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
|
|
|
|
uiAuthController = bootstrapResult.uiAuthController;
|
|
|
|
|
|
|
|
|
|
const tunnelRuntimeContext = tunnelWiringRuntime.initialize(app, port);
|
|
|
|
|
const { tunnelService, startTunnelWithNormalizedRequest } = tunnelRuntimeContext;
|
|
|
|
|
|
|
|
|
|
await featureRoutesRuntime.registerRoutes(app, {
|
|
|
|
|
crypto,
|
|
|
|
|
fs,
|
|
|
|
|
os,
|
|
|
|
|
path,
|
|
|
|
|
fsPromises,
|
|
|
|
|
spawn,
|
|
|
|
|
resolveGitBinaryForSpawn,
|
|
|
|
|
createFsSearchRuntime: createFsSearchRuntimeFactory,
|
|
|
|
|
openchamberDataDir: OPENCHAMBER_DATA_DIR,
|
|
|
|
|
openchamberUserConfigRoot: OPENCHAMBER_USER_CONFIG_ROOT,
|
|
|
|
|
normalizeDirectoryPath,
|
|
|
|
|
resolveProjectDirectory,
|
|
|
|
|
resolveOptionalProjectDirectory,
|
|
|
|
|
validateDirectoryPath,
|
|
|
|
|
readCustomThemesFromDisk,
|
|
|
|
|
refreshOpenCodeAfterConfigChange,
|
|
|
|
|
getOpenCodeResolutionSnapshot,
|
|
|
|
|
formatSettingsResponse,
|
|
|
|
|
readSettingsFromDisk,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
persistSettings,
|
|
|
|
|
sanitizeProjects,
|
|
|
|
|
sanitizeSkillCatalogs,
|
|
|
|
|
isUnsafeSkillRelativePath,
|
|
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
|
|
|
|
getOpenCodePort: () => openCodePort,
|
|
|
|
|
buildAugmentedPath,
|
2026-04-16 15:55:08 +03:00
|
|
|
projectConfigRuntime,
|
|
|
|
|
scheduledTasksRuntime,
|
|
|
|
|
getOpenChamberEventClients: () => uiOpenChamberEventClients,
|
|
|
|
|
writeSseEvent,
|
2026-03-31 18:47:00 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-29 17:03:38 -04:00
|
|
|
const previewProxyRuntime = createPreviewProxyRuntime({
|
|
|
|
|
crypto,
|
|
|
|
|
URL,
|
|
|
|
|
createProxyMiddleware,
|
|
|
|
|
responseInterceptor,
|
|
|
|
|
});
|
|
|
|
|
previewProxyRuntime.attach(app, {
|
|
|
|
|
server,
|
|
|
|
|
express,
|
|
|
|
|
uiAuthController,
|
|
|
|
|
isRequestOriginAllowed,
|
|
|
|
|
rejectWebSocketUpgrade,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const startupPipelineResult = await startupPipelineRuntime.run({
|
|
|
|
|
app,
|
|
|
|
|
server,
|
|
|
|
|
express,
|
|
|
|
|
fs,
|
|
|
|
|
path,
|
|
|
|
|
uiAuthController,
|
|
|
|
|
buildAugmentedPath,
|
|
|
|
|
searchPathFor,
|
|
|
|
|
isExecutable,
|
|
|
|
|
isRequestOriginAllowed,
|
|
|
|
|
rejectWebSocketUpgrade,
|
2026-04-17 16:07:26 +08:00
|
|
|
buildOpenCodeUrl,
|
|
|
|
|
getOpenCodeAuthHeaders,
|
2026-04-24 11:58:16 +03:00
|
|
|
globalEventHub: globalMessageStreamHub,
|
2026-04-17 16:07:26 +08:00
|
|
|
processForwardedEventPayload,
|
|
|
|
|
messageStreamWsClients: uiNotificationWsClients,
|
2026-05-01 04:57:31 -07:00
|
|
|
upstreamStallTimeoutMs: getUpstreamStallTimeoutMs,
|
2026-03-31 18:47:00 +03:00
|
|
|
terminalHeartbeatIntervalMs: TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS,
|
|
|
|
|
terminalRebindWindowMs: TERMINAL_INPUT_WS_REBIND_WINDOW_MS,
|
|
|
|
|
terminalMaxRebindsPerWindow: TERMINAL_INPUT_WS_MAX_REBINDS_PER_WINDOW,
|
|
|
|
|
setupProxy,
|
|
|
|
|
scheduleOpenCodeApiDetection,
|
|
|
|
|
bootstrapOpenCodeAtStartup,
|
2026-04-17 23:48:39 +08:00
|
|
|
triggerHealthCheck,
|
2026-03-31 18:47:00 +03:00
|
|
|
staticRoutesRuntime,
|
|
|
|
|
process,
|
|
|
|
|
crypto,
|
|
|
|
|
normalizeTunnelBootstrapTtlMs,
|
|
|
|
|
readSettingsFromDiskMigrated,
|
|
|
|
|
tunnelAuthController,
|
|
|
|
|
startTunnelWithNormalizedRequest,
|
|
|
|
|
gracefulShutdown,
|
|
|
|
|
getSignalsAttached: () => signalsAttached,
|
|
|
|
|
setSignalsAttached: (value) => {
|
|
|
|
|
signalsAttached = value;
|
|
|
|
|
},
|
|
|
|
|
syncToHmrState,
|
|
|
|
|
TUNNEL_MODE_QUICK,
|
|
|
|
|
TUNNEL_MODE_MANAGED_LOCAL,
|
|
|
|
|
TUNNEL_MODE_MANAGED_REMOTE,
|
|
|
|
|
host,
|
|
|
|
|
port,
|
|
|
|
|
startupTunnelRequest,
|
|
|
|
|
onTunnelReady,
|
|
|
|
|
tunnelRuntimeContext,
|
|
|
|
|
attachSignals,
|
|
|
|
|
});
|
|
|
|
|
terminalRuntime = startupPipelineResult.terminalRuntime;
|
2026-04-17 16:07:26 +08:00
|
|
|
messageStreamRuntime = startupPipelineResult.messageStreamRuntime;
|
2026-02-01 18:29:34 +02:00
|
|
|
|
2026-04-16 15:55:08 +03:00
|
|
|
try {
|
|
|
|
|
await scheduledTasksRuntime.start();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('[ScheduledTasks] Failed to start runtime:', error?.message || error);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 18:29:34 +02:00
|
|
|
return {
|
2026-03-31 18:47:00 +03:00
|
|
|
expressApp: app,
|
|
|
|
|
httpServer: server,
|
|
|
|
|
getPort: () => tunnelRuntimeContext.getActivePort(),
|
|
|
|
|
getOpenCodePort: () => openCodePort,
|
|
|
|
|
getTunnelUrl: () => tunnelService.getPublicUrl(),
|
2026-04-30 13:54:58 +03:00
|
|
|
getQuitRiskStatus: () => ({
|
|
|
|
|
tunnel: {
|
|
|
|
|
active: Boolean(tunnelService.getPublicUrl()),
|
|
|
|
|
},
|
|
|
|
|
scheduledTasks: scheduledTasksRuntime.getStatus(),
|
|
|
|
|
}),
|
2026-03-31 18:47:00 +03:00
|
|
|
isReady: () => isOpenCodeReady,
|
|
|
|
|
restartOpenCode: () => restartOpenCode(),
|
|
|
|
|
stop: (shutdownOptions = {}) =>
|
|
|
|
|
gracefulShutdown({ exitProcess: shutdownOptions.exitProcess ?? false })
|
2026-02-01 18:29:34 +02:00
|
|
|
};
|
2026-03-31 18:47:00 +03:00
|
|
|
}
|
2026-01-27 19:59:48 +02:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
runCliEntryIfMain({
|
|
|
|
|
process,
|
|
|
|
|
currentFilename: __filename,
|
|
|
|
|
parseServeCliOptions,
|
|
|
|
|
defaultPort: DEFAULT_PORT,
|
|
|
|
|
cloudflareProvider: TUNNEL_PROVIDER_CLOUDFLARE,
|
|
|
|
|
managedLocalMode: TUNNEL_MODE_MANAGED_LOCAL,
|
|
|
|
|
setExitOnShutdown: (value) => {
|
|
|
|
|
exitOnShutdown = value;
|
|
|
|
|
},
|
|
|
|
|
startServer: main,
|
|
|
|
|
});
|
2026-01-22 22:24:11 +06:00
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
export {
|
|
|
|
|
gracefulShutdown,
|
|
|
|
|
setupProxy,
|
|
|
|
|
restartOpenCode,
|
|
|
|
|
main as startWebUiServer,
|
|
|
|
|
parseServeCliOptions as parseArgs,
|
2026-01-22 22:24:11 +06:00
|
|
|
};
|