Files
XCOpenCodeWeb/web/bin/cli.js

895 lines
28 KiB
JavaScript

#!/usr/bin/env node
import path from 'path';
import fs from 'fs';
import net from 'net';
import { spawn, spawnSync } from 'child_process';
import { fileURLToPath, pathToFileURL } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_PORT = 3000;
const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
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) {
return path.join(process.env.BUN_INSTALL.trim(), 'bin', 'bun');
}
return 'bun';
}
const BUN_BIN = getBunBinary();
function importFromFilePath(filePath) {
return import(pathToFileURL(filePath).href);
}
function isBunRuntime() {
return typeof globalThis.Bun !== 'undefined';
}
function isBunInstalled() {
try {
const result = spawnSync(BUN_BIN, ['--version'], { stdio: 'ignore', env: process.env });
return result.status === 0;
} catch {
return false;
}
}
function getPreferredServerRuntime() {
return isBunInstalled() ? 'bun' : 'node';
}
function generateRandomPassword(length = 16) {
const charset = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
let password = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
password += charset[randomIndex];
}
return password;
}
function parseArgs() {
const args = process.argv.slice(2);
const envPassword = process.env.OPENCHAMBER_UI_PASSWORD || undefined;
const options = { port: DEFAULT_PORT, daemon: false, uiPassword: envPassword };
let command = 'serve';
const consumeValue = (currentIndex, inlineValue) => {
if (typeof inlineValue === 'string' && inlineValue.length > 0) {
return { value: inlineValue, nextIndex: currentIndex };
}
const candidate = args[currentIndex + 1];
if (typeof candidate === 'string' && !candidate.startsWith('-')) {
return { value: candidate, nextIndex: currentIndex + 1 };
}
return { value: undefined, nextIndex: currentIndex };
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('-')) {
let optionName;
let inlineValue;
if (arg.startsWith('--')) {
const eqIndex = arg.indexOf('=');
optionName = eqIndex >= 0 ? arg.slice(2, eqIndex) : arg.slice(2);
inlineValue = eqIndex >= 0 ? arg.slice(eqIndex + 1) : undefined;
} else {
optionName = arg.slice(1);
inlineValue = undefined;
}
switch (optionName) {
case 'port':
case 'p': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
const parsed = parseInt(value ?? '', 10);
options.port = Number.isFinite(parsed) ? parsed : DEFAULT_PORT;
break;
}
case 'daemon':
case 'd':
options.daemon = true;
break;
case 'ui-password': {
const { value, nextIndex } = consumeValue(i, inlineValue);
i = nextIndex;
options.uiPassword = typeof value === 'string' ? value : '';
break;
}
case 'help':
case 'h':
showHelp();
process.exit(0);
break;
case 'version':
case 'v':
console.log(PACKAGE_JSON.version);
process.exit(0);
break;
}
} else {
command = arg;
}
}
return { command, options };
}
function showHelp() {
console.log(`
OpenChamber - Web interface for the OpenCode AI coding agent
USAGE:
openchamber [COMMAND] [OPTIONS]
COMMANDS:
serve Start the web server (default)
stop Stop running instance(s)
restart Stop and start the server
status Show server status
OPTIONS:
-p, --port Web server port (default: ${DEFAULT_PORT})
--ui-password Protect browser UI with single password
-d, --daemon Run in background (serve command)
-h, --help Show help
-v, --version Show version
ENVIRONMENT:
OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
OPENCODE_HOST External OpenCode server base URL, e.g. http://hostname:4096 (overrides OPENCODE_PORT)
OPENCODE_PORT Port of external OpenCode server to connect to
OPENCODE_SKIP_START Skip starting OpenCode, use external server
EXAMPLES:
openchamber # Start on default port 3000 (or a free port)
openchamber --port 8080 # Start on port 8080
openchamber serve --daemon # Start in background
openchamber stop # Stop all running instances
openchamber stop --port 3000 # Stop specific instance
openchamber status # Check status
`);
}
const WINDOWS_EXTENSIONS = process.platform === 'win32'
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
.split(';')
.map((ext) => ext.trim().toLowerCase())
.filter(Boolean)
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
: [''];
function isExecutable(filePath) {
try {
const stats = fs.statSync(filePath);
if (!stats.isFile()) {
return false;
}
if (process.platform === 'win32') {
return true;
}
fs.accessSync(filePath, fs.constants.X_OK);
return true;
} catch (error) {
return false;
}
}
function resolveExplicitBinary(candidate) {
if (!candidate) {
return null;
}
if (candidate.includes(path.sep) || path.isAbsolute(candidate)) {
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
return isExecutable(resolved) ? resolved : null;
}
return null;
}
function searchPathFor(command) {
const pathValue = process.env.PATH || '';
const segments = pathValue.split(path.delimiter).filter(Boolean);
for (const dir of segments) {
for (const ext of WINDOWS_EXTENSIONS) {
const fileName = process.platform === 'win32' ? `${command}${ext}` : command;
const candidate = path.join(dir, fileName);
if (isExecutable(candidate)) {
return candidate;
}
}
}
return null;
}
async function checkOpenCodeCLI() {
if (process.env.OPENCODE_BINARY) {
const override = resolveExplicitBinary(process.env.OPENCODE_BINARY);
if (override) {
process.env.OPENCODE_BINARY = override;
return override;
}
console.warn(`Warning: OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not an executable file. Falling back to PATH lookup.`);
}
const resolvedFromPath = searchPathFor('opencode');
if (resolvedFromPath) {
process.env.OPENCODE_BINARY = resolvedFromPath;
return resolvedFromPath;
}
if (process.platform !== 'win32') {
const shellCandidates = [];
if (process.env.SHELL) {
shellCandidates.push(process.env.SHELL);
}
shellCandidates.push('/bin/bash', '/bin/zsh', '/bin/sh');
for (const shellPath of shellCandidates) {
if (!shellPath || !isExecutable(shellPath)) {
continue;
}
try {
const result = spawnSync(shellPath, ['-lic', 'command -v opencode'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.status === 0) {
const candidate = result.stdout.trim().split(/\s+/).pop();
if (candidate && isExecutable(candidate)) {
const dir = path.dirname(candidate);
const currentPath = process.env.PATH || '';
const segments = currentPath.split(path.delimiter).filter(Boolean);
if (!segments.includes(dir)) {
segments.unshift(dir);
process.env.PATH = segments.join(path.delimiter);
}
process.env.OPENCODE_BINARY = candidate;
return candidate;
}
}
} catch (error) {
}
}
} else {
try {
const result = spawnSync('where', ['opencode'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.status === 0) {
const candidate = result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
if (candidate && isExecutable(candidate)) {
process.env.OPENCODE_BINARY = candidate;
return candidate;
}
}
} catch (error) {
}
}
console.error('Error: Unable to locate the opencode CLI on PATH.');
console.error(`Current PATH: ${process.env.PATH || '<empty>'}`);
console.error('Ensure the CLI is installed and reachable, or set OPENCODE_BINARY to its full path.');
process.exit(1);
}
async function isPortAvailable(port) {
if (!Number.isFinite(port) || port <= 0) {
return false;
}
// Don't specify host here; `server.listen(port)` in the web server also doesn't,
// and on some platforms (notably macOS) IPv4/IPv6 binding differences can make
// a 127.0.0.1-only probe report "free" while the real server bind fails.
return await new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.on('error', () => resolve(false));
server.listen({ port }, () => {
server.close(() => resolve(true));
});
});
}
async function resolveAvailablePort(desiredPort) {
const startPort = Number.isFinite(desiredPort) ? Math.trunc(desiredPort) : DEFAULT_PORT;
// If user explicitly chose a port (incl 0), respect it.
if (process.argv.includes('--port') || process.argv.includes('-p')) {
return startPort;
}
// Prefer the default port for predictable URLs, but fall back to an OS-assigned
// free port when it is already in use.
if (await isPortAvailable(startPort)) {
return startPort;
}
console.warn(`Port ${startPort} in use; using a free port`);
return 0;
}
async function getPidFilePath(port) {
const os = await import('os');
const tmpDir = os.tmpdir();
return path.join(tmpDir, `openchamber-${port}.pid`);
}
async function getInstanceFilePath(port) {
const os = await import('os');
const tmpDir = os.tmpdir();
return path.join(tmpDir, `openchamber-${port}.json`);
}
function readPidFile(pidFilePath) {
try {
const content = fs.readFileSync(pidFilePath, 'utf8').trim();
const pid = parseInt(content);
if (isNaN(pid)) {
return null;
}
return pid;
} catch (error) {
return null;
}
}
function writePidFile(pidFilePath, pid) {
try {
fs.writeFileSync(pidFilePath, pid.toString());
} catch (error) {
console.warn(`Warning: Could not write PID file: ${error.message}`);
}
}
function removePidFile(pidFilePath) {
try {
if (fs.existsSync(pidFilePath)) {
fs.unlinkSync(pidFilePath);
}
} catch (error) {
console.warn(`Warning: Could not remove PID file: ${error.message}`);
}
}
/**
* Read stored instance options (port, daemon, uiPassword)
*/
function readInstanceOptions(instanceFilePath) {
try {
const content = fs.readFileSync(instanceFilePath, 'utf8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
/**
* Write instance options for restart/update to reuse
*/
function writeInstanceOptions(instanceFilePath, options) {
try {
// Only store non-sensitive restart-relevant options
const toStore = {
port: options.port,
daemon: options.daemon || false,
// Store password existence but not value - will use env var
hasUiPassword: typeof options.uiPassword === 'string',
};
// For daemon mode, we need to store the password to restart properly
if (options.daemon && typeof options.uiPassword === 'string') {
toStore.uiPassword = options.uiPassword;
}
fs.writeFileSync(instanceFilePath, JSON.stringify(toStore, null, 2));
} catch (error) {
console.warn(`Warning: Could not write instance file: ${error.message}`);
}
}
function removeInstanceFile(instanceFilePath) {
try {
if (fs.existsSync(instanceFilePath)) {
fs.unlinkSync(instanceFilePath);
}
} catch (error) {
// Ignore
}
}
function isProcessRunning(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
async function requestServerShutdown(port) {
if (!Number.isFinite(port) || port <= 0) return false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 1500);
try {
const resp = await fetch(`http://127.0.0.1:${port}/api/system/shutdown`, {
method: 'POST',
signal: controller.signal,
});
return resp.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}
const commands = {
async serve(options) {
options.port = await resolveAvailablePort(options.port);
const portWasSpecified = process.argv.includes('--port') || process.argv.includes('-p');
// When using dynamic port (port=0), don't use pid/instance files - the port is
// unknown until the server binds.
if (options.port !== 0) {
const pidFilePath = await getPidFilePath(options.port);
const instanceFilePath = await getInstanceFilePath(options.port);
const existingPid = readPidFile(pidFilePath);
if (existingPid && isProcessRunning(existingPid)) {
console.error(`Error: OpenChamber is already running on port ${options.port} (PID: ${existingPid})`);
console.error('Use "openchamber stop" to stop the existing instance');
process.exit(1);
}
// Persist for restart/update to reuse the chosen port.
writeInstanceOptions(instanceFilePath, { ...options });
} else if (portWasSpecified) {
// Explicitly requested port=0; nothing to persist.
}
const opencodeBinary = await checkOpenCodeCLI();
const serverPath = path.join(__dirname, '..', 'server', 'index.js');
const effectiveUiPassword = options.uiPassword;
const serverArgs = [serverPath, '--port', options.port.toString()];
if (typeof effectiveUiPassword === 'string') {
serverArgs.push('--ui-password', effectiveUiPassword);
}
const preferredRuntime = getPreferredServerRuntime();
const runtimeBin = preferredRuntime === 'bun' ? BUN_BIN : process.execPath;
if (options.daemon) {
const child = spawn(runtimeBin, serverArgs, {
detached: true,
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
env: {
...process.env,
OPENCHAMBER_PORT: options.port.toString(),
OPENCODE_BINARY: opencodeBinary,
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
}
});
child.unref();
const resolvedPort = await new Promise((resolve) => {
let settled = false;
const timeout = setTimeout(() => {
if (settled) return;
settled = true;
resolve(options.port);
}, 5000);
child.on('message', (msg) => {
if (settled) return;
if (msg && msg.type === 'openchamber:ready' && typeof msg.port === 'number') {
settled = true;
clearTimeout(timeout);
resolve(msg.port);
}
});
child.on('exit', () => {
if (settled) return;
settled = true;
clearTimeout(timeout);
resolve(options.port);
});
});
// Important: in daemon mode we must close the IPC channel, otherwise the CLI
// process can hang around as the parent of the detached server.
try {
child.removeAllListeners('message');
child.removeAllListeners('exit');
if (typeof child.disconnect === 'function' && child.connected) {
child.disconnect();
}
} catch {
// ignore
}
if (isProcessRunning(child.pid)) {
const pidFilePathResolved = await getPidFilePath(resolvedPort);
const instanceFilePathResolved = await getInstanceFilePath(resolvedPort);
writePidFile(pidFilePathResolved, child.pid);
writeInstanceOptions(instanceFilePathResolved, { ...options, port: resolvedPort, uiPassword: effectiveUiPassword });
console.log(`OpenChamber started in daemon mode on port ${resolvedPort}`);
console.log(`PID: ${child.pid}`);
console.log(`Visit: http://localhost:${resolvedPort}`);
if (showAutoGeneratedPassword) {
console.log(`\n🔐 Auto-generated password: \x1b[92m${effectiveUiPassword}\x1b[0m`);
console.log('⚠️ Save this password - it won\'t be shown again!\n');
}
} else {
console.error('Failed to start server in daemon mode');
process.exit(1);
}
return;
}
process.env.OPENCODE_BINARY = opencodeBinary;
if (typeof effectiveUiPassword === 'string') {
process.env.OPENCHAMBER_UI_PASSWORD = effectiveUiPassword;
}
if (process.env.OPENCODE_SKIP_START) {
process.env.OPENCHAMBER_SKIP_OPENCODE_START = process.env.OPENCODE_SKIP_START;
}
// Prefer bun when installed (much faster PTY). If CLI is running under Node,
// run the server in a child process so Node doesn't have to load bun-pty.
if (preferredRuntime === 'bun' && !isBunRuntime()) {
const child = spawn(runtimeBin, serverArgs, {
stdio: 'inherit',
env: {
...process.env,
OPENCHAMBER_PORT: options.port.toString(),
OPENCODE_BINARY: opencodeBinary,
...(typeof effectiveUiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: effectiveUiPassword } : {}),
...(process.env.OPENCODE_SKIP_START ? { OPENCHAMBER_SKIP_OPENCODE_START: process.env.OPENCODE_SKIP_START } : {}),
},
});
child.on('exit', (code) => {
process.exit(typeof code === 'number' ? code : 1);
});
return;
}
const { startWebUiServer } = await importFromFilePath(serverPath);
await startWebUiServer({
port: options.port,
attachSignals: true,
exitOnShutdown: true,
uiPassword: typeof effectiveUiPassword === 'string' ? effectiveUiPassword : null,
});
},
async stop(options) {
const os = await import('os');
const tmpDir = os.tmpdir();
let runningInstances = [];
try {
const files = fs.readdirSync(tmpDir);
const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid'));
for (const file of pidFiles) {
const port = parseInt(file.replace('openchamber-', '').replace('.pid', ''));
if (!isNaN(port)) {
const pidFilePath = path.join(tmpDir, file);
const pid = readPidFile(pidFilePath);
if (pid && isProcessRunning(pid)) {
const instanceFilePath = path.join(tmpDir, `openchamber-${port}.json`);
runningInstances.push({ port, pid, pidFilePath, instanceFilePath });
} else {
removePidFile(pidFilePath);
removeInstanceFile(path.join(tmpDir, `openchamber-${port}.json`));
}
}
}
} catch (error) {
}
if (runningInstances.length === 0) {
console.log('No running OpenChamber instances found');
return;
}
const portWasSpecified = process.argv.includes('--port') || process.argv.includes('-p');
if (portWasSpecified) {
const targetInstance = runningInstances.find(inst => inst.port === options.port);
if (!targetInstance) {
console.log(`No OpenChamber instance found running on port ${options.port}`);
return;
}
console.log(`Stopping OpenChamber (PID: ${targetInstance.pid}, Port: ${targetInstance.port})...`);
try {
await requestServerShutdown(targetInstance.port);
process.kill(targetInstance.pid, 'SIGTERM');
let attempts = 0;
const maxAttempts = 10;
const checkShutdown = setInterval(() => {
attempts++;
if (!isProcessRunning(targetInstance.pid)) {
clearInterval(checkShutdown);
removePidFile(targetInstance.pidFilePath);
removeInstanceFile(targetInstance.instanceFilePath);
console.log('OpenChamber stopped successfully');
} else if (attempts >= maxAttempts) {
clearInterval(checkShutdown);
console.log('Force killing process...');
process.kill(targetInstance.pid, 'SIGKILL');
removePidFile(targetInstance.pidFilePath);
removeInstanceFile(targetInstance.instanceFilePath);
console.log('OpenChamber force stopped');
}
}, 500);
} catch (error) {
console.error(`Error stopping process: ${error.message}`);
process.exit(1);
}
} else {
console.log(`Stopping all OpenChamber instances (${runningInstances.length} found)...`);
for (const instance of runningInstances) {
console.log(` Stopping instance on port ${instance.port} (PID: ${instance.pid})...`);
try {
await requestServerShutdown(instance.port);
process.kill(instance.pid, 'SIGTERM');
let attempts = 0;
const maxAttempts = 10;
await new Promise((resolve) => {
const checkShutdown = setInterval(() => {
attempts++;
if (!isProcessRunning(instance.pid)) {
clearInterval(checkShutdown);
removePidFile(instance.pidFilePath);
removeInstanceFile(instance.instanceFilePath);
console.log(` Port ${instance.port} stopped successfully`);
resolve(true);
} else if (attempts >= maxAttempts) {
clearInterval(checkShutdown);
console.log(` Force killing port ${instance.port}...`);
try {
process.kill(instance.pid, 'SIGKILL');
removePidFile(instance.pidFilePath);
removeInstanceFile(instance.instanceFilePath);
console.log(` Port ${instance.port} force stopped`);
} catch (e) {
}
resolve(true);
}
}, 500);
});
} catch (error) {
console.error(` Error stopping port ${instance.port}: ${error.message}`);
}
}
console.log('\nAll OpenChamber instances stopped');
}
},
async restart(options) {
const os = await import('os');
const tmpDir = os.tmpdir();
// Find running instances to get their stored options
let instancesToRestart = [];
try {
const files = fs.readdirSync(tmpDir);
const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid'));
for (const file of pidFiles) {
const port = parseInt(file.replace('openchamber-', '').replace('.pid', ''));
if (!isNaN(port)) {
const pidFilePath = path.join(tmpDir, file);
const instanceFilePath = path.join(tmpDir, `openchamber-${port}.json`);
const pid = readPidFile(pidFilePath);
if (pid && isProcessRunning(pid)) {
const storedOptions = readInstanceOptions(instanceFilePath);
instancesToRestart.push({
port,
pid,
pidFilePath,
instanceFilePath,
storedOptions: storedOptions || { port, daemon: false },
});
}
}
}
} catch (error) {
// Ignore
}
const portWasSpecified = process.argv.includes('--port') || process.argv.includes('-p');
if (instancesToRestart.length === 0) {
console.log('No running OpenChamber instances to restart');
console.log('Use "openchamber serve" to start a new instance');
return;
}
if (portWasSpecified) {
// Restart specific instance
const target = instancesToRestart.find(inst => inst.port === options.port);
if (!target) {
console.log(`No OpenChamber instance found running on port ${options.port}`);
return;
}
instancesToRestart = [target];
}
for (const instance of instancesToRestart) {
console.log(`Restarting OpenChamber on port ${instance.port}...`);
// Merge stored options with any explicitly provided options
const restartOptions = {
...instance.storedOptions,
// CLI-provided options override stored ones
...(portWasSpecified ? { port: options.port } : {}),
...(process.argv.includes('--daemon') || process.argv.includes('-d') ? { daemon: options.daemon } : {}),
...(process.argv.includes('--ui-password') ? { uiPassword: options.uiPassword } : {}),
};
// Stop the instance
try {
await requestServerShutdown(instance.port);
process.kill(instance.pid, 'SIGTERM');
// Wait for it to stop
let attempts = 0;
while (isProcessRunning(instance.pid) && attempts < 20) {
await new Promise(resolve => setTimeout(resolve, 250));
attempts++;
}
if (isProcessRunning(instance.pid)) {
process.kill(instance.pid, 'SIGKILL');
}
removePidFile(instance.pidFilePath);
} catch (error) {
console.warn(`Warning: Could not stop instance: ${error.message}`);
}
// Small delay before restart
await new Promise(resolve => setTimeout(resolve, 500));
// Start with merged options
await commands.serve(restartOptions);
}
},
async status() {
const os = await import('os');
const tmpDir = os.tmpdir();
let runningInstances = [];
let stoppedInstances = [];
try {
const files = fs.readdirSync(tmpDir);
const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid'));
for (const file of pidFiles) {
const port = parseInt(file.replace('openchamber-', '').replace('.pid', ''));
if (!isNaN(port)) {
const pidFilePath = path.join(tmpDir, file);
const pid = readPidFile(pidFilePath);
if (pid && isProcessRunning(pid)) {
runningInstances.push({ port, pid, pidFilePath });
} else {
removePidFile(pidFilePath);
stoppedInstances.push({ port });
}
}
}
} catch (error) {
}
if (runningInstances.length === 0) {
console.log('OpenChamber Status:');
console.log(' Status: Stopped');
if (stoppedInstances.length > 0) {
console.log(` Previously used ports: ${stoppedInstances.map(s => s.port).join(', ')}`);
}
return;
}
console.log('OpenChamber Status:');
for (const [index, instance] of runningInstances.entries()) {
if (runningInstances.length > 1) {
console.log(`\nInstance ${index + 1}:`);
}
console.log(' Status: Running');
console.log(` PID: ${instance.pid}`);
console.log(` Port: ${instance.port}`);
console.log(` Visit: http://localhost:${instance.port}`);
try {
const { execSync } = await import('child_process');
const startTime = execSync(`ps -o lstart= -p ${instance.pid}`, { encoding: 'utf8' }).trim();
console.log(` Start Time: ${startTime}`);
} catch (error) {
}
}
},
};
async function main() {
const { command, options } = parseArgs();
if (!commands[command]) {
console.error(`Error: Unknown command '${command}'`);
console.error('Use --help to see available commands');
process.exit(1);
}
try {
await commands[command](options);
} catch (error) {
console.error(`Error executing command '${command}': ${error.message}`);
process.exit(1);
}
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(1);
});
main();
export { commands, parseArgs, getPidFilePath };