import * as plugins from './plugins.js'; import { commitinfo } from './00_commitinfo_data.js'; import { controllerPackageName, controllerProtocolVersion, controllerUpgradeManagementVersion, type IControllerStatus, } from '../ts_interfaces/index.js'; import { defaultControllerPort, } from './classes.config.js'; import { resolveAGLHomePaths } from './classes.aglhome.js'; import { resolveOptionalAuthoritySocketPaths } from './classes.authorityclient.js'; import { ensureControllerDataRoot, preflightAGLHomeMigration, } from './functions.controllerdataroot.js'; import type { IControllerRuntimeOverrides } from './interfaces.config.js'; import type { IControllerReadyIpcMessage, IControllerStartIpcMessage, IControllerStartResult, TControllerIpcMessage, } from './interfaces.lifecycle.js'; import { assertProvidedSetupCode, SmartDataAuthStore } from './classes.authstore.js'; import { OpenCodeController } from './classes.controller.js'; import { assertControllerStatus, isLoopbackPortListening, queryControllerStatus as queryManagedControllerStatus, requestControllerUpgradeLaunch, stopVerifiedController, tryQueryControllerStatus as tryQueryManagedControllerStatus, waitUntilControllerExited, } from './classes.controllermanagement.js'; import { inspectControllerProcess, listControllerProcessesForCli, signalVerifiedControllerProcess, } from './classes.processinspection.js'; import { createUpgradeToken, normalizeUpgradeRegistryUrl, parseUpgradeWorkerPayload, UpgradeCoordinator, upgradePackageTransitionSource, upgradePackageTransitionTarget, upgradeTransactionTargetVersion, type UpgradeStartLease, } from './classes.upgradecoordinator.js'; import { upgradeTokenEnvironmentVariable } from './constants.upgradeenvironment.js'; import { adoptOrphanedUpgradeUnderLock, createUpgradeWorkerPayload, handoffLaunchedUpgradeWorkerUnderLock, handoffStalledUpgradeRecoveryUnderLock, type IUpgradeWorkerLaunchCandidate, inspectOrphanedUpgradeForAdoption, launchDetachedUpgradeWorker, resolveCurrentCliPath, resolveCurrentPnpmGlobalInstallation, runUpgradeWorker, terminalizeUpgradeWorkerHandoffFailure, tryResolveCurrentPnpmGlobalInstallation, UpgradeWorkerCandidateDrainageError, UpgradeWorkerHandoffError, } from './classes.upgradetransaction.js'; import { followUpgradeTransaction } from './functions.upgradefollower.js'; import { ControllerMcpClient, ControllerMcpClientError } from './classes.mcpclient.js'; type TParsedArguments = Record & { _: Array }; const currentCliName = commitinfo.name === upgradePackageTransitionSource.packageName ? upgradePackageTransitionSource.cliName : upgradePackageTransitionTarget.cliName; const currentRuntimeName = currentCliName === upgradePackageTransitionTarget.cliName ? 'AGL' : currentCliName; const upgradeRegistryUsage = currentRuntimeName === 'AGL' ? ' [--registry ]' : ''; const upgradeRegistryHelp = currentRuntimeName === 'AGL' ? ' --registry Override pnpm\'s configured registry for this upgrade\n' : ''; const detachedControllerStartupTimeoutMs = 3 * 60 * 1000; const helpText = ` ${currentRuntimeName === 'AGL' ? 'AGL - Agent Gateway Layer' : currentRuntimeName} Usage: ${currentCliName} start [options] ${currentCliName} status [--port 4097] [--json] ${currentCliName} settings [--port 4097] [--browser-video-backend chromium|native] [--add-standard-dir ] [--remove-standard-dir ] [--json] ${currentCliName} stop [--port 4097] ${currentCliName} upgrade [--port 4097]${upgradeRegistryUsage} [--grace-period-seconds 300] [--continue-sessions] [--json] ${currentCliName} mcp [--port 4097] ${currentCliName} foreground [options] ${currentCliName} temp-password [--port 4097] [--ttl-hours 24] Commands: start Start ${currentRuntimeName} as a detached process (Linux and macOS) status Read live ${currentRuntimeName} and harness status over loopback settings Read settings, select browser capture (restart required), or edit the standard project directories the workspace offers as project locations stop Verify and stop the live ${currentRuntimeName} process upgrade Upgrade the pnpm-global package and preserve running state mcp Run the unified stdio MCP server authswitch Coordinate authswitch account changes with the controller foreground Run ${currentRuntimeName} attached to this terminal temp-password Mint a temporary browser login password (max 24 hours) help Show this help Startup options: --port Controller port (default: 4097) --opencode-port Local-only OpenCode port (default: 4098) --directory Transiently register this existing directory on this start (also valid later; omitted: do not create a project) --projects-root Default base for relative project paths and suggestions (default: user home; explicitly typed absolute projects may be outside it; immutable after initialization) --public-origin Browser origin used for WebAuthn --rp-id WebAuthn relying-party ID --behind-tls-proxy Trust an explicitly configured TLS reverse proxy --setup-code Operator-chosen setup code (4-256 printable ASCII characters; short codes weaken brute-force resistance) Upgrade options: ${upgradeRegistryHelp} --grace-period-seconds Maximum graceful pause time (default: 300) --continue-sessions Continue sessions paused for the upgrade Settings options: --browser-video-backend Browser capture backend: chromium or native --add-standard-dir Add an absolute existing directory to the standard project directories; its immediate subdirectories become search and creation candidates for conversations --remove-standard-dir Remove a standard project directory (registered projects and their conversations are untouched) Temp password options: --ttl-hours Temporary password lifetime in hours (max 24, default 24) Upgrade requirements: The command requires the active pnpm-global installation and currently runs on Linux and macOS. Normal progress mode prints the detached worker log before stopping anything; --json emits it in the terminal document. ${currentRuntimeName} owns one private AGL_HOME (default $XDG_CONFIG_HOME/agl, normally ~/.config/agl; repository checkouts use ./.nogit/agl). The embedded database lives under $AGL_HOME/database; override the database directory with HARNESS_CONTROLLER_DB_DIR. Set HARNESS_CONTROLLER_MONGO_URL / HARNESS_CONTROLLER_MONGO_DB to use an external MongoDB-compatible server instead. temp-password writes to the controller database directly, so run it with the same database environment as the running controller; in embedded mode it attaches to the running controller's engine socket. The password authenticates the browser UI without a passkey until it expires. The first successful start prints a one-time setup code. Runtime configuration is immutable after initialization; use the same --port for later management. --directory is transient and may be supplied on any stopped start. Detached starts append ${currentRuntimeName} errors to $AGL_HOME/logs/controller-.log. Upgrade workers write a separate private log in $AGL_HOME/logs and print its path before work begins. In --json mode the path is retained before work begins and emitted at completion. `.trim(); const publicCliCommands = new Set([ 'start', 'status', 'settings', 'stop', 'upgrade', 'mcp', 'authswitch', 'foreground', 'temp-password', 'help', ]); export const formatCliError = (errorArg: unknown): string => { const messages: string[] = []; const seen = new Set(); const visit = (valueArg: unknown, depthArg: number): void => { if (depthArg > 8 || seen.has(valueArg)) return; if ((typeof valueArg === 'object' && valueArg !== null) || typeof valueArg === 'function') { seen.add(valueArg); } if (valueArg instanceof Error) { if (valueArg.message && !messages.includes(valueArg.message)) messages.push(valueArg.message); if (valueArg instanceof AggregateError) { for (const nestedError of valueArg.errors) visit(nestedError, depthArg + 1); } if (valueArg.cause !== undefined) visit(valueArg.cause, depthArg + 1); return; } const message = String(valueArg); if (message && !messages.includes(message)) messages.push(message); }; visit(errorArg, 0); const formatted = messages.join(' Cause: ') || 'An unknown error occurred.'; return Buffer.byteLength(formatted, 'utf8') <= 8 * 1024 ? formatted : `${Buffer.from(formatted, 'utf8').subarray(0, (8 * 1024) - 16).toString('utf8')} [truncated]`; }; const readOption = ( argvArg: TParsedArguments, keyArg: string, alternateKeyArg?: string, ): unknown => argvArg[keyArg] ?? (alternateKeyArg ? argvArg[alternateKeyArg] : undefined); const readStringOption = ( argvArg: TParsedArguments, keyArg: string, alternateKeyArg?: string, ): string | undefined => { const value = readOption(argvArg, keyArg, alternateKeyArg); if (value === undefined) return undefined; if (typeof value !== 'string' || !value) throw new Error(`--${alternateKeyArg ?? keyArg} requires a value.`); return value; }; const readBooleanOption = ( argvArg: TParsedArguments, keyArg: string, alternateKeyArg?: string, ): boolean => { const value = readOption(argvArg, keyArg, alternateKeyArg); if (value === undefined) return false; if (typeof value !== 'boolean') throw new Error(`--${alternateKeyArg ?? keyArg} must be a boolean flag.`); return value; }; // The parsed argv mangles numeric-looking values (12345 → number, 0123 → 123), // so the setup code is read from the raw runtime arguments instead. let activeRuntimeArguments: string[] = []; const readProvidedSetupCode = (): string | undefined => { for (let index = 0; index < activeRuntimeArguments.length; index++) { const token = activeRuntimeArguments[index]; if (token === '--setup-code') { const value = activeRuntimeArguments[index + 1]; if (value === undefined || value.startsWith('--')) { throw new Error('--setup-code requires a value (use --setup-code= for values starting with dashes).'); } return assertProvidedSetupCode(value); } if (token.startsWith('--setup-code=')) { return assertProvidedSetupCode(token.slice('--setup-code='.length)); } } return undefined; }; const readNumberOption = ( argvArg: TParsedArguments, keyArg: string, alternateKeyArg: string, fallbackArg: number | undefined, minimumArg: number, maximumArg: number, ): number | undefined => { const value = readOption(argvArg, keyArg, alternateKeyArg); if (value === undefined) return fallbackArg; const normalized = typeof value === 'number' ? value : Number(value); if (!Number.isSafeInteger(normalized) || normalized < minimumArg || normalized > maximumArg) { throw new Error( `--${alternateKeyArg} must be an integer between ${minimumArg} and ${maximumArg}.`, ); } return normalized; }; export const readUpgradeGracePeriodMs = (argvArg: TParsedArguments): number => ( readNumberOption( argvArg, 'gracePeriodSeconds', 'grace-period-seconds', 300, 1, 3_600, )! * 1_000 ); const readPortOption = ( argvArg: TParsedArguments, keyArg: string, alternateKeyArg: string, fallbackArg?: number, ): number | undefined => readNumberOption( argvArg, keyArg, alternateKeyArg, fallbackArg, 1, 65_535, ); export const parseRuntimeOverrides = ( argvArg: TParsedArguments, ): IControllerRuntimeOverrides => { const initialProjectDirectory = readStringOption(argvArg, 'directory', 'directory'); const projectsRoot = readStringOption(argvArg, 'projectsRoot', 'projects-root'); return { controllerPort: readPortOption(argvArg, 'port', 'port', defaultControllerPort)!, publicOrigin: readStringOption(argvArg, 'publicOrigin', 'public-origin'), rpId: readStringOption(argvArg, 'rpId', 'rp-id'), initialProjectDirectory: initialProjectDirectory === undefined ? undefined : plugins.path.resolve(initialProjectDirectory), projectsRoot: projectsRoot === undefined ? undefined : plugins.path.resolve(projectsRoot), opencodePort: readPortOption(argvArg, 'opencodePort', 'opencode-port'), behindTlsProxy: readOption(argvArg, 'behindTlsProxy', 'behind-tls-proxy') === undefined ? undefined : readBooleanOption(argvArg, 'behindTlsProxy', 'behind-tls-proxy'), }; }; const currentStatusExpectation = { packageName: controllerPackageName, packageVersion: commitinfo.version, protocolVersion: controllerProtocolVersion, upgradeManagementVersion: controllerUpgradeManagementVersion, }; const assertStatus = (statusArg: unknown): IControllerStatus => assertControllerStatus( statusArg, currentStatusExpectation, ); export const queryControllerStatus = async ( portArg: number, timeoutMsArg = 2_500, ): Promise => queryManagedControllerStatus( portArg, currentStatusExpectation, timeoutMsArg, ); const tryQueryControllerStatus = async (portArg: number): Promise => { return tryQueryManagedControllerStatus(portArg, currentStatusExpectation); }; const statusJson = (statusArg: IControllerStatus): string => JSON.stringify(statusArg, null, 2); export const formatControllerStatus = (statusArg: IControllerStatus): string => { const formatHarness = (harnessIdArg: 'opencode' | 'flex' | 'codex', labelArg: string): string => { const harness = statusArg.harnesses.find((entryArg) => entryArg.harnessId === harnessIdArg); if (!harness) throw new Error(`Controller status is missing the ${labelArg} harness.`); const pid = harness.pid ? ` (PID ${harness.pid})` : ''; return `${labelArg}: ${harness.state}${pid}`; }; return [ `${currentRuntimeName}: ${statusArg.lifecycleState} (PID ${statusArg.controllerPid}, ${statusArg.processMode})`, formatHarness('opencode', 'OpenCode'), formatHarness('flex', 'Flex'), ...(statusArg.harnesses.some((entry) => entry.harnessId === 'codex') ? [formatHarness('codex', 'Codex')] : []), `Setup: ${statusArg.setupRequired ? 'required' : 'complete'}`, ].join('\n'); }; const printStatus = (statusArg: IControllerStatus): void => { process.stdout.write(formatControllerStatus(statusArg) + '\n'); }; const printStartResult = ( resultArg: IControllerStartResult, jsonArg: boolean, extrasArg: { logFilePath?: string } = {}, ): void => { if (jsonArg) { process.stdout.write(JSON.stringify({ ...resultArg, ...extrasArg }, null, 2) + '\n'); return; } process.stdout.write(`${currentRuntimeName} ready: ${resultArg.publicUrl}\n`); printStatus(resultArg.status); if (extrasArg.logFilePath) { process.stdout.write(`Logs: ${extrasArg.logFilePath}\n`); } if (resultArg.setupCode) { process.stdout.write(`Setup code: ${resultArg.setupCode}\n`); process.stdout.write('Keep this code private; it expires after 30 minutes and is accepted only for passkey enrollment.\n'); } }; const detachedLogRotateBytes = 1024 * 1024; class RetainedControllerStartupError extends Error {} const resolveDetachedLogFilePath = (portArg: number): string => { return plugins.path.join(resolveAGLHomePaths().logs, `controller-${portArg}.log`); }; const openDetachedLogFile = async ( logFilePathArg: string, ): Promise => { await plugins.fs.promises.mkdir( plugins.path.dirname(logFilePathArg), { recursive: true, mode: 0o700 }, ); try { const existing = await plugins.fs.promises.lstat(logFilePathArg); if (existing.isFile() && existing.size > detachedLogRotateBytes) { await plugins.fs.promises.rename(logFilePathArg, `${logFilePathArg}.old`); } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } // O_NOFOLLOW plus the checks below keep a pre-planted symlink or foreign file // from silently receiving controller diagnostics. const handle = await plugins.fs.promises.open( logFilePathArg, plugins.fs.constants.O_WRONLY | plugins.fs.constants.O_APPEND | plugins.fs.constants.O_CREAT | plugins.fs.constants.O_NOFOLLOW, 0o600, ); try { const stats = await handle.stat(); if (!stats.isFile()) { throw new Error(`The controller log path is not a regular file: ${logFilePathArg}`); } if (typeof process.getuid === 'function' && stats.uid !== process.getuid()) { throw new Error(`The controller log file is not owned by the current user: ${logFilePathArg}`); } if ((stats.mode & 0o077) !== 0) { throw new Error(`The controller log file must not be group or world accessible: ${logFilePathArg}`); } } catch (error) { await handle.close(); throw error; } return handle; }; const resolveCliPath = async (): Promise => { return await resolveCurrentCliPath(); }; const acquireStartAdmission = async ( commandArg: 'start' | 'foreground' | 'temp-password', ): Promise => { if (process.platform === 'win32') return undefined; const installation = await tryResolveCurrentPnpmGlobalInstallation(); if (!installation) return undefined; const upgradeToken = process.env[upgradeTokenEnvironmentVariable]; const coordinator = new UpgradeCoordinator(installation.globalRoot); return await coordinator.acquireStartLease({ token: createUpgradeToken(), cliPath: installation.cliPath, command: commandArg, upgradeToken, }); }; const serializeRuntimeOptions = (overridesArg: IControllerRuntimeOverrides): string[] => { const args = ['--port', String(overridesArg.controllerPort)]; if (overridesArg.publicOrigin !== undefined) args.push('--public-origin', overridesArg.publicOrigin); if (overridesArg.rpId !== undefined) args.push('--rp-id', overridesArg.rpId); if (overridesArg.initialProjectDirectory !== undefined) { args.push('--directory', overridesArg.initialProjectDirectory); } if (overridesArg.projectsRoot !== undefined) args.push('--projects-root', overridesArg.projectsRoot); if (overridesArg.opencodePort !== undefined) args.push('--opencode-port', String(overridesArg.opencodePort)); if (overridesArg.behindTlsProxy === true) args.push('--behind-tls-proxy'); if (overridesArg.behindTlsProxy === false) args.push('--no-behind-tls-proxy'); return args; }; const stopJustSpawnedProcess = async ( childArg: plugins.childProcess.ChildProcess, cliPathArg: string, portArg: number, fingerprintArg: string, ): Promise => { if (!childArg.pid) return; const releaseParentIpc = (): void => { if (childArg.connected) childArg.disconnect(); childArg.unref(); }; const processIdentity = { pid: childArg.pid, cliPath: cliPathArg, port: portArg, expectedProcessGroupId: childArg.pid, expectedFingerprint: fingerprintArg, command: '__serve' as const, }; if (!await inspectControllerProcess(processIdentity)) { releaseParentIpc(); return; } try { await signalVerifiedControllerProcess({ ...processIdentity, signal: 'SIGTERM', signalProcessGroup: false, }); } catch (error) { if (!await inspectControllerProcess(processIdentity)) return; throw error; } finally { // Once cleanup has been requested, the parent must release the IPC channel. // Otherwise both processes can remain alive solely waiting on each other. releaseParentIpc(); } if (await waitUntilControllerExited(processIdentity, 30_000)) return; await signalVerifiedControllerProcess({ ...processIdentity, signal: 'SIGTERM', signalProcessGroup: false, }); if (!await waitUntilControllerExited(processIdentity, 15_000)) { throw new Error( 'The detached controller did not complete cooperative cleanup; it remains available for an exact stop retry.', ); } }; const sendIpcMessage = async ( childArg: plugins.childProcess.ChildProcess, messageArg: TControllerIpcMessage, ): Promise => { if (!childArg.connected) throw new Error('Detached controller IPC channel is unavailable.'); await new Promise((resolve, reject) => { childArg.send(messageArg, (errorArg) => errorArg ? reject(errorArg) : resolve()); }); }; const waitForSpawnedChildExit = async ( childArg: plugins.childProcess.ChildProcess, timeoutMsArg: number, ): Promise => { if (childArg.exitCode !== null || childArg.signalCode !== null) return true; return await new Promise((resolve) => { let settled = false; const finish = (exitedArg: boolean) => { if (settled) return; settled = true; clearTimeout(timeout); childArg.removeListener('exit', onExit); resolve(exitedArg); }; const onExit = () => finish(true); const timeout = setTimeout(() => finish(false), timeoutMsArg); childArg.once('exit', onExit); }); }; const waitForStartGate = async (): Promise => { if (!process.connected) { throw new Error('The detached controller start gate requires parent IPC.'); } return await new Promise((resolve, reject) => { let settled = false; const finish = (errorArg?: Error, messageArg?: IControllerStartIpcMessage) => { if (settled) return; settled = true; clearTimeout(timeout); process.removeListener('message', onMessage); process.removeListener('disconnect', onDisconnect); if (errorArg) reject(errorArg); else resolve(messageArg!); }; const onMessage = (messageArg: unknown) => { const message = messageArg as Partial; if (message?.type === 'start') { finish(undefined, message as IControllerStartIpcMessage); } }; const onDisconnect = () => finish(new Error('Parent disconnected before authorizing startup.')); const timeout = setTimeout(() => { finish(new Error('Parent did not authorize detached startup within 5 seconds.')); }, 5_000); process.on('message', onMessage); process.once('disconnect', onDisconnect); }); }; const waitForDetachedReady = async ( childArg: plugins.childProcess.ChildProcess, ): Promise => { return await new Promise((resolve, reject) => { let settled = false; const finish = (errorArg?: Error, messageArg?: IControllerReadyIpcMessage) => { if (settled) return; settled = true; clearTimeout(timeout); childArg.removeListener('message', onMessage); childArg.removeListener('error', onError); childArg.removeListener('exit', onExit); if (errorArg) reject(errorArg); else resolve(messageArg!); }; const onMessage = (messageArg: unknown) => { const message = messageArg as Partial; if (message?.type === 'startupError' && typeof message.message === 'string') { finish(message.retained === true ? new RetainedControllerStartupError(message.message) : new Error(message.message)); return; } if ( message?.type === 'ready' && typeof message.publicUrl === 'string' && typeof message.status === 'object' && message.status !== null && (message.setupCode === undefined || typeof message.setupCode === 'string') ) { try { finish(undefined, { type: 'ready', publicUrl: message.publicUrl, status: assertStatus(message.status), ...(message.setupCode ? { setupCode: message.setupCode } : {}), }); } catch (error) { finish(error instanceof Error ? error : new Error(String(error))); } } }; const onError = (errorArg: Error) => finish(errorArg); const onExit = (codeArg: number | null, signalArg: NodeJS.Signals | null) => { finish(new Error(`Detached controller exited during startup (${signalArg ?? codeArg ?? 'unknown'}).`)); }; const timeout = setTimeout(() => { finish(new Error('Detached controller did not become ready within 3 minutes.')); }, detachedControllerStartupTimeoutMs); childArg.on('message', onMessage); childArg.once('error', onError); childArg.once('exit', onExit); }); }; const startDetachedWithoutAdmission = async (argvArg: TParsedArguments): Promise => { if (process.platform === 'win32') { throw new Error('Detached process management is currently supported on Linux and macOS only.'); } const overrides = parseRuntimeOverrides(argvArg); const providedSetupCode = readProvidedSetupCode(); const existing = await tryQueryControllerStatus(overrides.controllerPort); const json = readBooleanOption(argvArg, 'json'); if (existing) { if (providedSetupCode !== undefined && !json) { process.stdout.write(`${currentRuntimeName} is already running; the provided --setup-code was not used.\n`); } if (json) process.stdout.write(statusJson(existing) + '\n'); else printStatus(existing); return; } const cliPath = await resolveCliPath(); const existingProcesses = await listControllerProcessesForCli(cliPath); const existingProcess = existingProcesses.find((candidateArg) => ( candidateArg.port === overrides.controllerPort )); if (existingProcess) { throw new Error( `An ${currentRuntimeName} process is already starting or running (PID ${existingProcess.identity.pid}).`, ); } await ensureControllerDataRoot(); const upgradeToken = process.env[upgradeTokenEnvironmentVariable]; let upgradeGlobalRoot: string | undefined; if (upgradeToken) { upgradeGlobalRoot = (await resolveCurrentPnpmGlobalInstallation()).globalRoot; } const logFilePath = resolveDetachedLogFilePath(overrides.controllerPort); const logFileHandle = await openDetachedLogFile(logFilePath); let child: plugins.childProcess.ChildProcess | undefined; let spawnError: unknown; try { const childEnvironment = { ...process.env }; delete childEnvironment[upgradeTokenEnvironmentVariable]; child = plugins.childProcess.spawn( process.execPath, [cliPath, '__serve', ...serializeRuntimeOptions(overrides)], { cwd: plugins.os.homedir(), detached: true, env: childEnvironment, shell: false, // stdout stays discarded: the transport logs every connection there, // which would grow the file without bound. stderr carries diagnostics. stdio: ['ignore', 'ignore', logFileHandle.fd, 'ipc'], }, ); } catch (errorArg) { spawnError = errorArg; } let logCloseError: unknown; try { await logFileHandle.close(); } catch (errorArg) { try { await logFileHandle.close(); } catch (retryErrorArg) { logCloseError = new AggregateError( [errorArg, retryErrorArg], 'The detached controller log handle could not be closed.', ); } } if (spawnError !== undefined || logCloseError !== undefined) { const errors: unknown[] = [spawnError, logCloseError] .filter((errorArg) => errorArg !== undefined); if (child) { try { if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); if (!await waitForSpawnedChildExit(child, 2_000)) { throw new Error('The gated detached controller survived startup-handle cleanup.'); } } catch (errorArg) { errors.push(errorArg); } finally { if (child.connected) child.disconnect(); child.unref(); } } if (errors.length === 1) throw errors[0]; throw new AggregateError( errors, 'Detached controller spawn or log-handle cleanup failed.', ); } if (!child?.pid) throw new Error('Detached controller did not provide a process ID.'); let spawnedIdentity: Awaited> = null; try { for (let attempt = 0; attempt < 20; attempt++) { spawnedIdentity = await inspectControllerProcess({ pid: child.pid, cliPath, port: overrides.controllerPort, expectedProcessGroupId: child.pid, command: '__serve', }); if (spawnedIdentity) break; await new Promise((resolve) => setTimeout(resolve, 10)); } if (!spawnedIdentity) { throw new Error('Detached controller process identity could not be established.'); } } catch (error) { // The child is blocked on its IPC start gate here and owns no server, // database, or OpenCode resources yet. Killing the exact spawned PID cannot // orphan a descendant process. if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); if (!await waitForSpawnedChildExit(child, 2_000)) { throw new AggregateError( [error], 'Detached controller identity failed and the gated child did not exit after SIGKILL.', ); } throw error; } try { const readyPromise = waitForDetachedReady( child, ); try { await sendIpcMessage(child, { type: 'start', ...(providedSetupCode !== undefined ? { setupCode: providedSetupCode } : {}), ...(upgradeToken === undefined ? {} : { upgradeToken }), ...(upgradeGlobalRoot === undefined ? {} : { upgradeGlobalRoot }), }); } catch (error) { void readyPromise.catch(() => undefined); throw error; } const readyMessage = await readyPromise; const status = await queryControllerStatus(overrides.controllerPort); child.disconnect(); child.unref(); printStartResult({ status, publicUrl: readyMessage.publicUrl, ...(readyMessage.setupCode ? { setupCode: readyMessage.setupCode } : {}), }, json, { logFilePath }); if (providedSetupCode !== undefined && !readyMessage.setupCode && !json) { process.stdout.write('Setup is already complete; the provided --setup-code was not used.\n'); } } catch (error) { if (error instanceof RetainedControllerStartupError) { if (child.connected) child.disconnect(); child.unref(); process.stderr.write(`${error.message}\n`); return; } try { await stopJustSpawnedProcess( child, cliPath, overrides.controllerPort, spawnedIdentity.fingerprint, ); } catch (cleanupErrorArg) { throw new AggregateError( [error, cleanupErrorArg], `Detached controller startup failed (${formatCliError(error)}) and cleanup was incomplete (${formatCliError(cleanupErrorArg)}). Logs: ${logFilePath}`, ); } throw new Error(`${formatCliError(error)} Logs: ${logFilePath}`, { cause: error }); } }; const startDetached = async (argvArg: TParsedArguments): Promise => { await preflightAGLHomeMigration(); const lease = await acquireStartAdmission('start'); try { await startDetachedWithoutAdmission(argvArg); } finally { await lease?.release(); } }; const runForegroundWithAdmission = async (argvArg: TParsedArguments): Promise => { await preflightAGLHomeMigration(); const lease = await acquireStartAdmission('foreground'); delete process.env[upgradeTokenEnvironmentVariable]; try { await runControllerProcess(argvArg, false); } finally { await lease?.release(); } }; const runControllerProcess = async ( argvArg: TParsedArguments, detachedArg: boolean, ): Promise => { if (detachedArg && process.platform === 'win32') { throw new Error('Detached process management is currently supported on Linux and macOS only.'); } if (detachedArg && !process.connected) { throw new Error('The internal detached server command requires an attached parent IPC channel.'); } let providedSetupCode: string | undefined; let upgradeToken: string | undefined; let upgradeGlobalRoot: string | undefined; let startupRetained = false; if (detachedArg) { const gateMessage = await waitForStartGate(); // The gate payload crosses a process boundary; validate rather than trust. if (gateMessage.setupCode !== undefined) { providedSetupCode = assertProvidedSetupCode(gateMessage.setupCode); } upgradeToken = gateMessage.upgradeToken; upgradeGlobalRoot = gateMessage.upgradeGlobalRoot; } else { providedSetupCode = readProvidedSetupCode(); } if (upgradeToken !== undefined) { process.env[upgradeTokenEnvironmentVariable] = upgradeToken; } // The one place the shared per-user account authority is resolved. A host without a private // runtime directory simply has none, which the controller reports as an absent authority. const accountAuthority = resolveOptionalAuthoritySocketPaths(); const controller = new OpenCodeController({ processMode: detachedArg ? 'detached' : 'foreground', runtimeOverrides: parseRuntimeOverrides(argvArg), ...(accountAuthority === undefined ? {} : { accountAuthority }), ...(providedSetupCode !== undefined ? { setupCode: providedSetupCode } : {}), ...(upgradeToken === undefined ? {} : { upgradeToken }), ...(upgradeGlobalRoot === undefined ? {} : { upgradeGlobalRoot }), ...(upgradeToken === undefined ? {} : { onStartupBlocked: async (errorArg: Error) => { startupRetained = true; if (!detachedArg || !process.connected) return; await new Promise((resolve) => { process.send!({ type: 'startupError', message: `${errorArg.message} The target controller remains fail-closed and will retry.`, retained: true, }, () => resolve()); }); }, }), }); let ready = false; let stopping: Promise | undefined; const stop = (): Promise => { if (stopping) return stopping; let tracked: Promise; tracked = controller.stop().then(() => { if (detachedArg) { process.removeListener('SIGINT', onSignal); process.removeListener('SIGTERM', onSignal); process.removeListener('disconnect', onDisconnect); if (process.connected && typeof process.disconnect === 'function') process.disconnect(); } process.exitCode = 0; }, (errorArg) => { process.exitCode = 1; throw errorArg; }).finally(() => { if (stopping === tracked) stopping = undefined; }); stopping = tracked; return tracked; }; const onSignal = () => { void stop().catch((errorArg) => { process.exitCode = 1; process.stderr.write(`${currentCliName}: ${formatCliError(errorArg)}\n`); }); }; const onDisconnect = () => { if (!ready && !startupRetained) { void stop().catch((errorArg) => { process.exitCode = 1; process.stderr.write(`${currentCliName}: ${formatCliError(errorArg)}\n`); }); } }; process.on('SIGINT', onSignal); process.on('SIGTERM', onSignal); if (detachedArg) process.once('disconnect', onDisconnect); try { const result = await controller.start(); if (detachedArg) { if (process.connected) { await new Promise((resolve, reject) => { const message: IControllerReadyIpcMessage = { type: 'ready', status: result.status, publicUrl: result.publicUrl, ...(result.setupCode ? { setupCode: result.setupCode } : {}), }; process.send!(message, (errorArg) => errorArg ? reject(errorArg) : resolve()); }); } else if (!startupRetained) { throw new Error('Detached controller startup lost its parent IPC channel.'); } ready = true; } else { ready = true; const jsonOutput = readBooleanOption(argvArg, 'json'); printStartResult(result, jsonOutput); if (providedSetupCode !== undefined && !result.setupCode && !jsonOutput) { process.stdout.write('Setup is already complete; the provided --setup-code was not used.\n'); } } } catch (error) { const normalized = error instanceof Error ? error : new Error(String(error)); let ipcError: unknown; if (detachedArg && process.connected) { try { await new Promise((resolve, reject) => { process.send!({ type: 'startupError', message: formatCliError(normalized) }, (errorArg) => ( errorArg ? reject(errorArg) : resolve() )); }); } catch (errorArg) { ipcError = errorArg; } } let cleanupError: unknown; try { await stop(); } catch (errorArg) { cleanupError = errorArg; } const errors = [normalized, ipcError, cleanupError].filter((valueArg) => valueArg !== undefined); if (errors.length > 1) { throw new AggregateError( errors, `Detached controller startup failed: ${formatCliError(normalized)}`, ); } throw normalized; } }; const showStatus = async (argvArg: TParsedArguments): Promise => { const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!; const status = await queryControllerStatus(port).catch(() => { throw new Error(`No compatible ${currentRuntimeName} instance is responding on 127.0.0.1:${port}.`); }); if (readBooleanOption(argvArg, 'json')) process.stdout.write(statusJson(status) + '\n'); else printStatus(status); }; /** * A standard project directory named on the command line. The controller resolves and validates * it (it must exist and be a directory); the CLI only rejects what it can decide locally, so a * relative path is refused here rather than resolved against the caller's unrelated cwd. */ const assertStandardDirectoryArgument = (valueArg: string, optionArg: string): string => { if (!valueArg.startsWith('/')) { throw new Error(`--${optionArg} requires an absolute path.`); } return valueArg; }; const manageSettings = async (argvArg: TParsedArguments): Promise => { const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!; const backend = readStringOption(argvArg, 'browserVideoBackend', 'browser-video-backend'); if (backend !== undefined && backend !== 'chromium' && backend !== 'native') { throw new Error('--browser-video-backend must be chromium or native.'); } const addedDirectoryOption = readStringOption(argvArg, 'addStandardDir', 'add-standard-dir'); const removedDirectoryOption = readStringOption(argvArg, 'removeStandardDir', 'remove-standard-dir'); if (addedDirectoryOption !== undefined && removedDirectoryOption !== undefined) { throw new Error('--add-standard-dir and --remove-standard-dir cannot be combined.'); } // Arguments are decided before the controller is contacted, so a bad path fails without I/O. const addedDirectory = addedDirectoryOption === undefined ? undefined : assertStandardDirectoryArgument(addedDirectoryOption, 'add-standard-dir'); const removedDirectory = removedDirectoryOption === undefined ? undefined : assertStandardDirectoryArgument(removedDirectoryOption, 'remove-standard-dir'); const client = new ControllerMcpClient({ controllerPort: port }); let settings = (await client.getSettings()).settings; let standardProjectDirectories: string[] | undefined; if (addedDirectory !== undefined) { standardProjectDirectories = settings.standardProjectDirectories.includes(addedDirectory) ? undefined : [...settings.standardProjectDirectories, addedDirectory]; } if (removedDirectory !== undefined) { if (!settings.standardProjectDirectories.includes(removedDirectory)) { throw new Error('That directory is not a standard project directory.'); } standardProjectDirectories = settings.standardProjectDirectories.filter( (entry) => entry !== removedDirectory, ); } if (backend !== undefined || standardProjectDirectories !== undefined) { const patch: { browserVideoBackend?: 'chromium' | 'native'; standardProjectDirectories?: string[]; } = {}; if (backend !== undefined) patch.browserVideoBackend = backend; if (standardProjectDirectories !== undefined) { patch.standardProjectDirectories = standardProjectDirectories; } try { settings = (await client.updateSettings(patch)).settings; } catch (error) { if (!(error instanceof ControllerMcpClientError) || error.code !== 'OUTCOME_UNKNOWN') throw error; // Reconcile an uncertain write once; never issue the mutation twice. settings = (await client.getSettings()).settings; if (backend !== undefined && (settings.browserVideoBackend ?? 'chromium') !== backend) throw error; if ( standardProjectDirectories !== undefined && JSON.stringify(settings.standardProjectDirectories) !== JSON.stringify(standardProjectDirectories) ) throw error; } } const desired = settings.browserVideoBackend ?? 'chromium'; const active = settings.activeBrowserVideoBackend ?? 'chromium'; const restartRequired = desired !== active; if (readBooleanOption(argvArg, 'json')) process.stdout.write(JSON.stringify({ settings, restartRequired }) + '\n'); else { process.stdout.write(`Browser video: ${desired}. Active: ${active}.${restartRequired ? ' Restart AGL to apply.' : ''}\n`); process.stdout.write(settings.standardProjectDirectories.length === 0 ? 'Standard project directories: none configured.\n' : `Standard project directories:\n${settings.standardProjectDirectories.map( (directory) => ` ${directory}\n`, ).join('')}`); } }; const stopController = async (argvArg: TParsedArguments): Promise => { if (process.platform !== 'linux' && process.platform !== 'darwin') { throw new Error('CLI process stopping is currently supported on Linux and macOS only.'); } const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!; const status = await tryQueryControllerStatus(port); if (!status) { process.stdout.write(`${currentRuntimeName} is not running.\n`); return; } const cliPath = await resolveCliPath(); await stopVerifiedController({ status, cliPath, port }); process.stdout.write(`${currentRuntimeName} stopped.\n`); }; export interface ICreateCurrentPackageUpgradeTransactionOptions { token: string; port: number; sourceVersion: string; registryUrl?: string; controllerWasRunning: boolean; continueSessions: boolean; gracePeriodMs: number; } export const createCurrentPackageUpgradeTransaction = async ( coordinatorArg: UpgradeCoordinator, optionsArg: ICreateCurrentPackageUpgradeTransactionOptions, ) => { if ( commitinfo.name === upgradePackageTransitionSource.packageName && optionsArg.sourceVersion === upgradePackageTransitionSource.version ) { if (optionsArg.registryUrl !== undefined) { throw new Error( 'The legacy hcon package transition uses pnpm registry configuration and cannot retain --registry.', ); } return await coordinatorArg.createPackageTransitionTransaction(optionsArg); } return await coordinatorArg.createTransaction(optionsArg); }; const upgradeController = async (argvArg: TParsedArguments): Promise => { if (process.platform !== 'linux' && process.platform !== 'darwin') { throw new Error(`${currentCliName} upgrade is currently supported on Linux and macOS only.`); } const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!; const registryValue = readStringOption(argvArg, 'registry', 'registry'); const registryUrl = registryValue === undefined ? undefined : normalizeUpgradeRegistryUrl(registryValue); const gracePeriodMs = readUpgradeGracePeriodMs(argvArg); const continueSessions = readBooleanOption(argvArg, 'continueSessions', 'continue-sessions'); const installation = await resolveCurrentPnpmGlobalInstallation(); const coordinator = new UpgradeCoordinator(installation.globalRoot); await coordinator.initializeExistingCanonicalState(); const orphaned = await inspectOrphanedUpgradeForAdoption({ coordinator, installation, port, ...(registryUrl === undefined ? {} : { registryUrl }), }); if (!orphaned) await preflightAGLHomeMigration(); const status = await tryQueryControllerStatus(port); if (!status && await isLoopbackPortListening(port)) { throw new Error( `Port ${port} is occupied by an incompatible service or controller version; refusing to upgrade.`, ); } if (orphaned && status) { throw new Error('An orphaned upgrade cannot be adopted while a controller is running.'); } if (!status && !orphaned) await ensureControllerDataRoot(); const retainedTransaction = orphaned?.transaction; const payload = createUpgradeWorkerPayload({ port, gracePeriodMs: retainedTransaction?.gracePeriodMs ?? gracePeriodMs, continueSessions: retainedTransaction?.continueSessions ?? continueSessions, ...(status ? { expectedController: { pid: status.controllerPid, processGroupId: status.processGroupId, processFingerprint: status.processFingerprint, }, } : {}), }); const invocationToken = createUpgradeToken(); const invocationLease = await coordinator.acquireUpgradeLock({ token: invocationToken, cliPath: installation.cliPath, command: 'upgrade', }); let invocationLeaseHandled = false; let invocationError: unknown; try { let worker: IUpgradeWorkerLaunchCandidate; let controllerWasRunning = status !== undefined; if (orphaned) { const adopted = await adoptOrphanedUpgradeUnderLock({ coordinator, lock: invocationLease, token: payload.token, expected: orphaned, installation, port, ...(registryUrl === undefined ? {} : { registryUrl }), }); controllerWasRunning = adopted.controllerWasRunning; let launched: Awaited>; try { launched = await handoffStalledUpgradeRecoveryUnderLock({ coordinator, token: payload.token, installation, command: 'upgrade', lock: invocationLease, }); } finally { invocationLeaseHandled = true; } if (!launched) throw new Error('The adopted upgrade became terminal before recovery launch.'); worker = launched; } else { const racedOrphan = await inspectOrphanedUpgradeForAdoption({ coordinator, installation, port, ...(registryUrl === undefined ? {} : { registryUrl }), }); if (racedOrphan) { throw new Error('An orphaned upgrade appeared after normal upgrade preflight.'); } await createCurrentPackageUpgradeTransaction(coordinator, { token: payload.token, port, sourceVersion: installation.packageVersion, ...(registryUrl === undefined ? {} : { registryUrl }), controllerWasRunning, continueSessions, gracePeriodMs: payload.gracePeriodMs, }); try { if (status) { await coordinator.createLaunchGrant({ token: payload.token, port, packageName: controllerPackageName, packageVersion: commitinfo.version, cliPath: installation.cliPath, controller: payload.expectedController!, }); try { const launched = await requestControllerUpgradeLaunch(port, payload.token); worker = { pid: launched.workerPid, cliPath: installation.cliPath, logFilePath: launched.logFilePath, }; } finally { await coordinator.removeLaunchGrant(payload.token); } } else { const launched = await launchDetachedUpgradeWorker({ installation, payload }); worker = launched; } } catch (errorArg) { if (errorArg instanceof UpgradeWorkerCandidateDrainageError || status) { invocationLeaseHandled = true; throw errorArg instanceof UpgradeWorkerCandidateDrainageError ? errorArg : new UpgradeWorkerCandidateDrainageError( 'The controller upgrade launch failed and candidate drainage cannot be proven by the CLI.', errorArg, ); } const transaction = await coordinator.readTransaction(payload.token); if (transaction.worker?.logFilePath) { worker = { pid: transaction.worker.pid, processGroupId: transaction.worker.processGroupId, fingerprint: transaction.worker.fingerprint, cliPath: transaction.worker.cliPath, logFilePath: transaction.worker.logFilePath, }; } else { await coordinator.finishTransaction( payload.token, false, 'The upgrade worker could not be launched.', errorArg instanceof Error ? errorArg.message : String(errorArg), ); throw errorArg; } } try { try { worker = await handoffLaunchedUpgradeWorkerUnderLock({ coordinator, token: payload.token, installation, command: 'upgrade', lock: invocationLease, candidate: worker, retainLockOnFailure: true, }); } catch (errorArg) { if (!(errorArg instanceof UpgradeWorkerHandoffError)) throw errorArg; await terminalizeUpgradeWorkerHandoffFailure({ coordinator, token: payload.token, error: errorArg, }); } } finally { invocationLeaseHandled = true; } } const json = readBooleanOption(argvArg, 'json'); if (!json) { process.stdout.write([ `Upgrade worker started (PID ${worker.pid}).`, `Logs: ${worker.logFilePath}`, controllerWasRunning ? `Controller on port ${port} will restart after a successful upgrade.` : 'No running controller was found; it will remain stopped.', ].join('\n') + '\n'); } let finalTransaction: Awaited>; let followError: unknown; try { finalTransaction = await followUpgradeTransaction( coordinator, payload.token, worker.logFilePath, !json, installation, ); } catch (errorArg) { followError = errorArg; finalTransaction = await coordinator.readTransaction(payload.token).catch(() => ({ version: 2 as const, tokenHash: '', revision: 0, port, sourceVersion: retainedTransaction?.sourceVersion ?? installation.packageVersion, ...(registryUrl === undefined ? {} : { registryUrl }), controllerWasRunning, continueSessions: payload.continueSessions, gracePeriodMs: payload.gracePeriodMs, phase: 'failed' as const, message: errorArg instanceof Error ? errorArg.message : String(errorArg), createdAt: Date.now(), phaseStartedAt: Date.now(), updatedAt: Date.now(), sessions: retainedTransaction?.sessions ?? [], terminal: { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), }, })); } if (json) { const reportedWorker = finalTransaction.worker?.logFilePath ? { workerPid: finalTransaction.worker.pid, logFilePath: finalTransaction.worker.logFilePath, } : { workerPid: worker.pid, logFilePath: worker.logFilePath }; const sessionSummary = finalTransaction.sessions.reduce((summary, session) => { summary.pause[session.pauseState] = (summary.pause[session.pauseState] ?? 0) + 1; if (session.continueState) { summary.continue[session.continueState] = (summary.continue[session.continueState] ?? 0) + 1; } return summary; }, { pause: {} as Record, continue: {} as Record, }); process.stdout.write(JSON.stringify({ accepted: true, ...reportedWorker, controllerWasRunning, fromVersion: finalTransaction.sourceVersion, toVersion: upgradeTransactionTargetVersion(finalTransaction), phase: finalTransaction.phase, success: finalTransaction.terminal?.success === true && followError === undefined, error: finalTransaction.terminal?.error ?? (followError instanceof Error ? followError.message : followError === undefined ? undefined : String(followError)), sessions: sessionSummary, }, null, 2) + '\n'); } if (followError) throw followError; if (!finalTransaction.terminal?.success) { const terminalError = new Error( finalTransaction.terminal?.error ?? `${currentCliName} upgrade failed.`, ); throw terminalError; } } catch (errorArg) { invocationError = errorArg; } let releaseError: unknown; if (!invocationLeaseHandled) { try { await invocationLease.release(); } catch (errorArg) { releaseError = errorArg; } } if (invocationError !== undefined && releaseError !== undefined) { throw new AggregateError( [invocationError, releaseError], `The ${currentCliName} upgrade invocation failed and its ownership could not be released.`, ); } if (invocationError !== undefined) throw invocationError; if (releaseError !== undefined) throw releaseError; }; const runHiddenUpgradeWorker = async (): Promise => { if (activeRuntimeArguments.length !== 2 && activeRuntimeArguments.length !== 3) { throw new Error(`The internal ${currentCliName} upgrade worker command requires a payload and optional recovery context.`); } const payload = parseUpgradeWorkerPayload( activeRuntimeArguments[1], process.env[upgradeTokenEnvironmentVariable], ); if (activeRuntimeArguments.length === 2) { await runUpgradeWorker(payload); return; } const runWithRecoveryContext = runUpgradeWorker as unknown as ( payloadArg: typeof payload, optionsArg: { recoveryContext: string }, ) => Promise; await runWithRecoveryContext(payload, { recoveryContext: activeRuntimeArguments[2] }); }; const mintTempPasswordWithoutAdmission = async (argvArg: TParsedArguments): Promise => { const port = readPortOption(argvArg, 'port', 'port', defaultControllerPort)!; const ttlValue = readOption(argvArg, 'ttlHours', 'ttl-hours'); let ttlHours = 24; if (ttlValue !== undefined) { const normalized = typeof ttlValue === 'number' ? ttlValue : Number(ttlValue); if (!Number.isFinite(normalized) || normalized <= 0 || normalized > 24) { throw new Error('--ttl-hours must be a number above 0 and at most 24.'); } ttlHours = normalized; } const controllerDataRoot = await ensureControllerDataRoot(); const authStore = new SmartDataAuthStore(controllerDataRoot.databaseConfig); let operationError: unknown; try { await authStore.initForController(controllerDataRoot.directoryPath); const runtimeConfig = await authStore.getRuntimeConfig(port); if (!runtimeConfig) { throw new Error( `No controller configuration exists for port ${port}. Start the controller first, and run ` + 'temp-password with the same HARNESS_CONTROLLER_MONGO_URL / HARNESS_CONTROLLER_MONGO_DB ' + 'environment as the controller.', ); } const minted = await authStore.createTempPassword(Math.round(ttlHours * 60 * 60 * 1000)); await authStore.recordAuditEvent({ type: 'temppassword.create', outcome: 'succeeded', credentialId: minted.credentialId, }); process.stdout.write([ `Temporary password: ${minted.password}`, `Expires: ${minted.expiresAt.toISOString()}`, `Sign in at ${runtimeConfig.publicOrigin} using "CLI temporary password".`, ].join('\n') + '\n'); } catch (errorArg) { operationError = errorArg; throw errorArg; } finally { try { await authStore.close(); } catch (cleanupErrorArg) { if (operationError !== undefined) { throw new AggregateError( [operationError, cleanupErrorArg], 'Temporary password operation failed and SmartData cleanup was incomplete.', { cause: operationError }, ); } throw cleanupErrorArg; } } }; const mintTempPassword = async (argvArg: TParsedArguments): Promise => { await preflightAGLHomeMigration(); const lease = await acquireStartAdmission('temp-password'); try { await mintTempPasswordWithoutAdmission(argvArg); } finally { await lease?.release(); } }; export const runCli = async (testArgvArg?: string[]): Promise => { process.env.AGL_HOME = resolveAGLHomePaths().root; activeRuntimeArguments = (testArgvArg ?? process.argv).slice(2); if ( activeRuntimeArguments.some((argumentArg) => argumentArg === '--help' || argumentArg === '-h') && ( activeRuntimeArguments[0] === '--help' || activeRuntimeArguments[0] === '-h' || publicCliCommands.has(activeRuntimeArguments[0] ?? '') ) ) { process.stdout.write(helpText + '\n'); return; } if (activeRuntimeArguments[0] === 'mcp') { const { runMcpAglCli } = await import('./mcp.js'); await runMcpAglCli(activeRuntimeArguments.slice(1)); return; } if (activeRuntimeArguments[0] === 'authswitch') { const { runAuthSwitchCli } = await import('./functions.authswitchcli.js'); await runAuthSwitchCli(activeRuntimeArguments.slice(1)); return; } const cli = new plugins.smartcli.Smartcli(); cli.addVersion(commitinfo.version); let commandPromise: Promise = Promise.resolve(); let commandDispatched = false; const register = (commandArg: string, handlerArg: (argvArg: TParsedArguments) => Promise) => { cli.addCommand(commandArg).subscribe((argvArg: TParsedArguments) => { commandDispatched = true; commandPromise = handlerArg(argvArg); }); }; register('start', startDetached); register('status', showStatus); register('settings', manageSettings); register('stop', stopController); register('upgrade', upgradeController); register('foreground', runForegroundWithAdmission); register('temp-password', mintTempPassword); register('__serve', (argvArg) => runControllerProcess(argvArg, true)); register('__upgrade-worker', runHiddenUpgradeWorker); cli.addCommand('help').subscribe(() => { commandDispatched = true; process.stdout.write(helpText + '\n'); }); cli.standardCommand().subscribe(() => { commandDispatched = true; process.stdout.write(helpText + '\n'); }); cli.startParse(testArgvArg); const runtimeArguments = activeRuntimeArguments; const versionOnly = runtimeArguments.length > 0 && runtimeArguments.every((argumentArg) => argumentArg === '-v' || argumentArg === '--version'); if (!commandDispatched && !versionOnly) { throw new Error(`Unknown command. Run ${currentCliName} help for usage.`); } await commandPromise; };