Files
openchamber/packages/web/bin/cli.js
T
Bohdan Triapitsyn 9eb8a92169 fix(cli): probe bun.exe under BUN_INSTALL on Windows
isBunInstalled() spawned <BUN_INSTALL>/bin/bun, which does not exist on
Windows where the installer ships bun.exe. spawnSync adds no extension to
an explicit path, so the probe failed and the CLI quietly started the
server under Node even with Bun installed. The probe now names bun.exe
on win32.

Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
2026-09-05 14:46:06 +03:00

465 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
import { fileURLToPath, pathToFileURL } from 'url';
import { isModuleCliExecution } from './cli-entry.js';
import { EXIT_CODE, TunnelCliError } from './lib/cli-errors.js';
import {
resolveServeHost,
hasUiPasswordConfigured,
generateUiPassword,
resolveServeUiPassword,
assertAuthenticatedNetworkExposure,
} from './lib/cli-network.js';
import {
maskToken,
resolveToken,
redactProfileForOutput,
redactProfilesForOutput,
warnIfUnsafeFilePermissions,
ensureTunnelProfilesMigrated as ensureTunnelProfilesMigratedBase,
} from './lib/cli-tunnel-profiles.js';
import {
parseArgs,
showHelp,
showControlHelp,
showStartupHelp,
showConnectUrlHelp,
showTunnelHelp,
generateCompletionScript,
findClosestMatch,
} from './lib/cli-args.js';
import { readDesktopLocalPortFromSettings } from './lib/cli-paths.js';
import { resolveExplicitBinary, searchPathFor } from './lib/cli-executables.js';
import { startupCommand } from './lib/commands-startup.js';
import { logsCommand } from './lib/commands-logs.js';
import { statusCommand } from './lib/commands-status.js';
import { scheduleCommand } from './lib/commands-schedule.js';
import { sessionCommand } from './lib/commands-session.js';
import { modelsCommand } from './lib/commands-models.js';
import { projectsCommand } from './lib/commands-projects.js';
import { createUpdateCommand } from './lib/commands-update.js';
import { createConnectUrlCommand } from './lib/commands-connect-url.js';
import { createLifecycleCommands } from './lib/commands-lifecycle.js';
import { createServeCommand } from './lib/commands-serve.js';
import { createTunnelCommand, isValidTunnelDoctorResponse, shouldDisplayTunnelQr } from './lib/commands-tunnel.js';
import {
resolveDoctorPortStatuses,
discoverRunningInstances,
discoverOpenChamberInstanceOnPort,
discoverLifecycleInstances,
discoverUnconfirmedRegistryInstanceOnPort,
resolveTunnelProviders,
} from './lib/cli-lifecycle.js';
import {
fetchTunnelProvidersFromPort,
fetchSystemInfoFromPort,
} from './lib/cli-http.js';
import {
getPidFilePath,
getInstanceFilePath,
isProcessRunning,
isOpenchamberCmdline,
isOpenchamberProcessRunning,
getOpenchamberProcessState,
} from './lib/cli-process.js';
import {
intro as clackIntro, outro as clackOutro, cancel as clackCancel,
isJsonMode,
isQuietMode,
printJson,
logStatus,
} from './cli-output.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
let onCancelCleanup = null;
let activeCommandOptions = null;
let foregroundServerActive = false;
let foregroundShutdown = null;
function setCancelCleanup(handler) {
onCancelCleanup = typeof handler === 'function' ? handler : null;
}
function shouldWarnForTunnelProfileFile() {
if (!activeCommandOptions) return false;
return !isJsonMode(activeCommandOptions) && !isQuietMode(activeCommandOptions);
}
function ensureTunnelProfilesMigrated() {
return ensureTunnelProfilesMigratedBase({ shouldWarn: shouldWarnForTunnelProfileFile() });
}
const HAS_PLAIN_FLAG = process.argv.includes('--plain');
const STYLE_ENABLED = process.stdout.isTTY && process.env.NO_COLOR !== '1' && !HAS_PLAIN_FLAG;
const ANSI = {
bold: '\x1b[1m',
unbold: '\x1b[22m',
};
function boldText(text) {
if (!STYLE_ENABLED) return text;
return `${ANSI.bold}${text}${ANSI.unbold}`;
}
function importFromFilePath(filePath) {
return import(pathToFileURL(filePath).href);
}
function getBunBinary() {
if (typeof process.env.BUN_BINARY === 'string' && process.env.BUN_BINARY.trim().length > 0) {
return process.env.BUN_BINARY.trim();
}
if (typeof process.env.BUN_INSTALL === 'string' && process.env.BUN_INSTALL.trim().length > 0) {
// The Windows installer places bun.exe there; spawnSync does not append
// the extension to an explicit path, so without it the probe fails and
// the CLI silently falls back to Node.
return path.join(process.env.BUN_INSTALL.trim(), 'bin', process.platform === 'win32' ? 'bun.exe' : 'bun');
}
return 'bun';
}
const BUN_BIN = getBunBinary();
function isBunRuntime() {
return typeof globalThis.Bun !== 'undefined';
}
function isBunInstalled() {
try {
const result = spawnSync(BUN_BIN, ['--version'], {
stdio: 'ignore',
env: process.env,
windowsHide: true,
});
return result.status === 0;
} catch {
return false;
}
}
function getPreferredServerRuntime() {
return isBunInstalled() ? 'bun' : 'node';
}
async function checkOpenCodeCLI(onNotice) {
if (process.env.OPENCODE_BINARY) {
const override = resolveExplicitBinary(process.env.OPENCODE_BINARY);
if (override) {
process.env.OPENCODE_BINARY = override;
return override;
}
const message = `OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not an executable file. Falling back to PATH lookup.`;
if (typeof onNotice === 'function') {
onNotice({ level: 'warning', code: 'OPENCODE_BINARY_INVALID', message });
} else {
console.warn(`Warning: ${message}`);
}
}
const resolvedFromPath = searchPathFor('opencode');
if (resolvedFromPath) {
process.env.OPENCODE_BINARY = resolvedFromPath;
return resolvedFromPath;
}
throw new Error(
`Unable to locate the opencode CLI on PATH (${process.env.PATH || '<empty>'}). ` +
'Ensure the CLI is installed and reachable, or set OPENCODE_BINARY to its full path.'
);
}
const commands = {
serve: null,
'connect-url': null,
stop: null,
restart: null,
status: statusCommand,
schedule: scheduleCommand,
session: sessionCommand,
models: modelsCommand,
projects: projectsCommand,
logs: logsCommand,
startup: startupCommand,
update: null,
};
commands.serve = createServeCommand({
serverPath: path.join(__dirname, '..', 'server', 'index.js'),
bunBin: BUN_BIN,
checkOpenCodeCLI,
getPreferredServerRuntime,
setForegroundServerActive(value) { foregroundServerActive = value; },
setForegroundShutdown(handler) { foregroundShutdown = handler; },
});
{
const lifecycleCommands = createLifecycleCommands({ serveCommand: commands.serve.bind(commands) });
commands.stop = lifecycleCommands.stop;
commands.restart = lifecycleCommands.restart;
}
commands.tunnel = createTunnelCommand({
serveCommand: commands.serve.bind(commands),
stopCommand: commands.stop.bind(commands),
setCancelCleanup,
boldText,
ensureTunnelProfilesMigrated,
});
commands['connect-url'] = createConnectUrlCommand({
serveCommand: commands.serve.bind(commands),
});
commands.update = createUpdateCommand({
importFromFilePath,
packageManagerPath: path.join(__dirname, '..', 'server', 'lib', 'package-manager.js'),
serveCommand: commands.serve.bind(commands),
});
async function main() {
const parsed = parseArgs();
const { command, subcommand, tunnelAction, startupAction, scheduleAction, sessionAction, controlAction, options, removedFlagErrors, helpRequested, versionRequested } = parsed;
activeCommandOptions = options;
if (versionRequested) {
if (isJsonMode(options)) {
printJson({ version: PACKAGE_JSON.version });
} else {
console.log(PACKAGE_JSON.version);
}
return;
}
if (removedFlagErrors.length > 0) {
if (isJsonMode(options)) {
printJson({
status: 'error',
error: {
message: removedFlagErrors[0],
details: removedFlagErrors,
},
});
} else {
for (const error of removedFlagErrors) {
console.error(`Error: ${error}`);
}
}
process.exit(1);
}
if (helpRequested) {
if (command === 'tunnel') {
showTunnelHelp();
} else if (command === 'startup') {
showStartupHelp();
} else if (command === 'connect-url') {
showConnectUrlHelp();
} else if (command === 'schedule') {
await commands.schedule(options, 'help');
} else if (command === 'session') {
await commands.session(options, 'help');
} else if (command === 'models') {
await commands.models(options, 'help');
} else if (command === 'projects') {
await commands.projects(options, 'help');
} else if (command === 'control') {
showControlHelp();
} else {
showHelp();
}
return;
}
if (command === 'tunnel') {
await commands.tunnel(options, subcommand, tunnelAction);
return;
}
if (command === 'startup') {
await commands.startup(options, startupAction);
return;
}
if (command === 'schedule') {
await commands.schedule(options, scheduleAction);
return;
}
if (command === 'session') {
await commands.session(options, sessionAction);
return;
}
if (command === 'models') {
await commands.models(options, 'show');
return;
}
if (command === 'projects') {
await commands.projects(options, 'list');
return;
}
if (command === 'control') {
if (controlAction !== 'help') {
throw new TunnelCliError(`Unknown control command '${controlAction}'.`, EXIT_CODE.USAGE_ERROR);
}
showControlHelp();
return;
}
if (!commands[command]) {
const knownCommands = ['serve', 'stop', 'restart', 'status', 'schedule', 'session', 'models', 'projects', 'control', 'tunnel', 'startup', 'logs', 'update'];
const suggestion = findClosestMatch(command, knownCommands);
const hint = suggestion ? ` Did you mean '${suggestion}'?` : '';
if (isJsonMode(options)) {
printJson({
status: 'error',
error: {
message: `Unknown command '${command}'.${hint}`,
},
messages: [{ level: 'info', code: 'USAGE_HELP', message: 'Use --help to see available commands' }],
});
} else {
console.error(`Error: Unknown command '${command}'.${hint}`);
console.error('Use --help to see available commands');
}
process.exit(EXIT_CODE.USAGE_ERROR);
}
await commands[command](options);
}
const isCliExecution = isModuleCliExecution(process.argv[1], import.meta.url, fs.realpathSync, 'openchamber');
if (isCliExecution) {
let isHandlingSigint = false;
process.on('SIGINT', () => {
if (isHandlingSigint) {
return;
}
if (foregroundServerActive) {
if (typeof foregroundShutdown === 'function') {
void foregroundShutdown('SIGINT');
}
return;
}
isHandlingSigint = true;
(async () => {
clackCancel('Operation cancelled.');
if (onCancelCleanup) {
try {
await onCancelCleanup();
} catch {
} finally {
setCancelCleanup(null);
}
}
process.exit(130);
})();
});
process.on('unhandledRejection', (reason, promise) => {
if (isJsonMode(activeCommandOptions)) {
printJson({
status: 'error',
error: {
message: `Unhandled rejection: ${String(reason)}`,
},
});
} else {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
}
process.exit(1);
});
process.on('uncaughtException', (error) => {
if (isJsonMode(activeCommandOptions)) {
printJson({
status: 'error',
error: {
message: `Uncaught exception: ${error instanceof Error ? error.message : String(error)}`,
},
});
} else {
console.error('Uncaught Exception:', error);
}
process.exit(1);
});
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
if (isJsonMode(activeCommandOptions)) {
printJson({
status: 'error',
error: {
message,
},
});
} else if (process.stdout?.isTTY && !HAS_PLAIN_FLAG) {
clackIntro(boldText('Error'));
logStatus('error', message);
clackOutro('failed');
} else {
console.error(`Error: ${message}`);
}
const exitCode = error instanceof TunnelCliError ? error.exitCode : EXIT_CODE.GENERAL_ERROR;
process.exit(exitCode);
});
}
export {
main,
commands,
parseArgs,
assertAuthenticatedNetworkExposure,
resolveServeHost,
hasUiPasswordConfigured,
generateUiPassword,
resolveServeUiPassword,
shouldDisplayTunnelQr,
isValidTunnelDoctorResponse,
readDesktopLocalPortFromSettings,
getPidFilePath,
getInstanceFilePath,
isProcessRunning,
isOpenchamberProcessRunning,
isOpenchamberCmdline,
getOpenchamberProcessState,
resolveTunnelProviders,
fetchTunnelProvidersFromPort,
fetchSystemInfoFromPort,
discoverRunningInstances,
discoverOpenChamberInstanceOnPort,
discoverLifecycleInstances,
discoverUnconfirmedRegistryInstanceOnPort,
ensureTunnelProfilesMigrated,
resolveToken,
redactProfileForOutput,
redactProfilesForOutput,
maskToken,
findClosestMatch,
generateCompletionScript,
TunnelCliError,
EXIT_CODE,
warnIfUnsafeFilePermissions,
};