Refactor web CLI into focused modules (#1837)
This commit is contained in:
committed by
GitHub
parent
00821700de
commit
45df19c3b2
@@ -0,0 +1,396 @@
|
||||
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
||||
import { requestServerShutdown } from './cli-http.js';
|
||||
import { isPortAvailable } from './cli-ports.js';
|
||||
import {
|
||||
discoverLifecycleInstances,
|
||||
discoverUnconfirmedRegistryInstanceOnPort,
|
||||
} from './cli-lifecycle.js';
|
||||
import {
|
||||
readInstanceOptions,
|
||||
removePidFile,
|
||||
removeInstanceFile,
|
||||
isProcessRunning,
|
||||
stopInstanceProcess,
|
||||
} from './cli-process.js';
|
||||
import {
|
||||
intro as clackIntro,
|
||||
outro as clackOutro,
|
||||
isJsonMode,
|
||||
isQuietMode,
|
||||
shouldRenderHumanOutput,
|
||||
createSpinner,
|
||||
printJson,
|
||||
logStatus,
|
||||
} from '../cli-output.js';
|
||||
|
||||
async function stopCommand(options) {
|
||||
const showOutput = shouldRenderHumanOutput(options);
|
||||
const suppressQuietOutput = options?.suppressQuietOutput === true;
|
||||
const jsonResults = [];
|
||||
const printQuietStopResults = () => {
|
||||
if (suppressQuietOutput) return;
|
||||
if (!isQuietMode(options) || isJsonMode(options)) return;
|
||||
if (jsonResults.length === 0) {
|
||||
process.stdout.write('none\n');
|
||||
return;
|
||||
}
|
||||
for (const result of jsonResults) {
|
||||
if (result.stopped) {
|
||||
process.stdout.write(`stopped ${result.port}\n`);
|
||||
} else {
|
||||
const reason = result.reason || 'failed';
|
||||
process.stderr.write(`failed ${result.port} ${reason}\n`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const finish = (text) => {
|
||||
if (!showOutput) return;
|
||||
clackOutro(text);
|
||||
};
|
||||
|
||||
if (showOutput) {
|
||||
clackIntro('OpenChamber Stop');
|
||||
}
|
||||
|
||||
let runningInstances = await discoverLifecycleInstances(options);
|
||||
if (options.explicitPort) {
|
||||
if (runningInstances.length === 0) {
|
||||
const unconfirmedInstance = await discoverUnconfirmedRegistryInstanceOnPort(options.port, options);
|
||||
if (unconfirmedInstance) {
|
||||
runningInstances = [unconfirmedInstance];
|
||||
}
|
||||
}
|
||||
|
||||
if (runningInstances.length === 0) {
|
||||
jsonResults.push({ port: options.port, stopped: false, reason: 'not-found' });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ stoppedCount: 0, results: jsonResults });
|
||||
}
|
||||
if (showOutput) {
|
||||
logStatus('info', `no OpenChamber instance found on port ${options.port}`);
|
||||
finish('nothing to stop');
|
||||
}
|
||||
printQuietStopResults();
|
||||
return;
|
||||
}
|
||||
|
||||
const explicitInstance = runningInstances[0];
|
||||
if (explicitInstance.runtime === 'desktop') {
|
||||
jsonResults.push({ port: options.port, runtime: 'desktop', stopped: false, reason: 'desktop-managed' });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ stoppedCount: 0, results: jsonResults, messages: [{ level: 'warning', code: 'DESKTOP_MANAGED_PORT', message: `Port ${options.port} is managed by OpenChamber Desktop and cannot be stopped with this command.` }] });
|
||||
}
|
||||
if (showOutput) {
|
||||
logStatus('warning', `port ${options.port} is managed by OpenChamber Desktop`, 'cannot be stopped with this command');
|
||||
finish('no changes applied');
|
||||
}
|
||||
printQuietStopResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitInstance.source === 'probe') {
|
||||
const unmanagedStopSpin = showOutput ? createSpinner(options) : null;
|
||||
if (showOutput && !unmanagedStopSpin) {
|
||||
logStatus('info', `found unmanaged OpenChamber instance on port ${options.port}`, 'attempting shutdown');
|
||||
}
|
||||
unmanagedStopSpin?.start(`Stopping unmanaged OpenChamber on port ${options.port}...`);
|
||||
const requested = await requestServerShutdown(options.port, options.host);
|
||||
|
||||
if (Number.isFinite(explicitInstance.pid) && isProcessRunning(explicitInstance.pid)) {
|
||||
await stopInstanceProcess(explicitInstance.pid, {
|
||||
shutdownWaitMs: requested ? 5000 : 0,
|
||||
gracefulTimeoutMs: 2500,
|
||||
forceTimeoutMs: 3000,
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
const stopped = await isPortAvailable(options.port, options.host);
|
||||
if (stopped) {
|
||||
unmanagedStopSpin?.stop(`Stopped unmanaged OpenChamber on port ${options.port}`);
|
||||
jsonResults.push({ port: options.port, runtime: 'unmanaged', stopped: true });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ stoppedCount: 1, results: jsonResults });
|
||||
}
|
||||
if (showOutput && !unmanagedStopSpin) {
|
||||
logStatus('success', `stopped OpenChamber on port ${options.port}`);
|
||||
finish('stop complete');
|
||||
}
|
||||
printQuietStopResults();
|
||||
} else if (requested) {
|
||||
unmanagedStopSpin?.stop(`Shutdown requested on port ${options.port} (still occupied)`);
|
||||
jsonResults.push({ port: options.port, runtime: 'unmanaged', stopped: false, reason: 'shutdown-requested-port-busy' });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({
|
||||
status: 'warning',
|
||||
stoppedCount: 0,
|
||||
results: jsonResults,
|
||||
messages: [{ level: 'warning', code: 'SHUTDOWN_PARTIAL', message: `Shutdown was requested for port ${options.port}, but the port is still occupied.` }],
|
||||
});
|
||||
}
|
||||
if (showOutput && !unmanagedStopSpin) {
|
||||
logStatus('warning', `shutdown requested on port ${options.port}`, 'port is still occupied');
|
||||
finish('partial stop');
|
||||
}
|
||||
printQuietStopResults();
|
||||
} else {
|
||||
unmanagedStopSpin?.error(`Could not stop OpenChamber on port ${options.port}`);
|
||||
jsonResults.push({ port: options.port, runtime: 'unmanaged', stopped: false, reason: 'stop-failed' });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({
|
||||
status: 'error',
|
||||
stoppedCount: 0,
|
||||
results: jsonResults,
|
||||
messages: [{ level: 'error', code: 'STOP_FAILED', message: `Could not stop OpenChamber on port ${options.port}.` }],
|
||||
});
|
||||
}
|
||||
if (showOutput && !unmanagedStopSpin) {
|
||||
logStatus('error', `could not stop OpenChamber on port ${options.port}`);
|
||||
finish('failed');
|
||||
}
|
||||
printQuietStopResults();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitInstance.source === 'registry-unconfirmed') {
|
||||
const unconfirmedStopSpin = showOutput ? createSpinner(options) : null;
|
||||
if (showOutput && !unconfirmedStopSpin) {
|
||||
logStatus('info', `found unconfirmed OpenChamber pid ${explicitInstance.pid} on port ${options.port}`, 'HTTP shutdown endpoint is unreachable; stopping by PID');
|
||||
}
|
||||
unconfirmedStopSpin?.start(`Stopping unconfirmed OpenChamber on port ${options.port}...`);
|
||||
const stopped = await stopInstanceProcess(explicitInstance.pid, {
|
||||
shutdownWaitMs: 0,
|
||||
gracefulTimeoutMs: 2500,
|
||||
forceTimeoutMs: 3000,
|
||||
}).catch(() => false);
|
||||
|
||||
if (stopped || !isProcessRunning(explicitInstance.pid)) {
|
||||
removePidFile(explicitInstance.pidFilePath);
|
||||
removeInstanceFile(explicitInstance.instanceFilePath);
|
||||
unconfirmedStopSpin?.stop(`Stopped OpenChamber PID ${explicitInstance.pid}`);
|
||||
jsonResults.push({ port: options.port, pid: explicitInstance.pid, runtime: 'unconfirmed', stopped: true });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ stoppedCount: 1, results: jsonResults });
|
||||
}
|
||||
if (showOutput && !unconfirmedStopSpin) {
|
||||
logStatus('success', `stopped pid ${explicitInstance.pid}`);
|
||||
finish('stop complete');
|
||||
}
|
||||
printQuietStopResults();
|
||||
return;
|
||||
}
|
||||
|
||||
unconfirmedStopSpin?.error(`Could not stop OpenChamber PID ${explicitInstance.pid}`);
|
||||
jsonResults.push({ port: options.port, pid: explicitInstance.pid, runtime: 'unconfirmed', stopped: false, reason: 'stop-failed' });
|
||||
if (isJsonMode(options)) {
|
||||
printJson({
|
||||
status: 'error',
|
||||
stoppedCount: 0,
|
||||
results: jsonResults,
|
||||
messages: [{ level: 'error', code: 'STOP_FAILED', message: `Could not stop OpenChamber PID ${explicitInstance.pid}.` }],
|
||||
});
|
||||
}
|
||||
if (showOutput && !unconfirmedStopSpin) {
|
||||
logStatus('error', `could not stop pid ${explicitInstance.pid}`);
|
||||
finish('failed');
|
||||
}
|
||||
printQuietStopResults();
|
||||
return;
|
||||
}
|
||||
} else if (runningInstances.length === 0) {
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ stoppedCount: 0, results: jsonResults });
|
||||
}
|
||||
if (showOutput) {
|
||||
logStatus('info', 'No running OpenChamber instances found');
|
||||
finish('nothing to stop');
|
||||
}
|
||||
printQuietStopResults();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const instance of runningInstances) {
|
||||
const stopSpin = showOutput ? createSpinner(options) : null;
|
||||
if (showOutput && !stopSpin) {
|
||||
logStatus('info', `stopping port ${instance.port} (PID: ${instance.pid})`);
|
||||
}
|
||||
stopSpin?.start(`Stopping OpenChamber on port ${instance.port}...`);
|
||||
try {
|
||||
const requested = await requestServerShutdown(instance.port, instance.host || options.host);
|
||||
const stopped = await stopInstanceProcess(instance.pid, {
|
||||
shutdownWaitMs: requested ? 5000 : 0,
|
||||
gracefulTimeoutMs: 2500,
|
||||
forceTimeoutMs: 3000,
|
||||
});
|
||||
if (!stopped && isProcessRunning(instance.pid)) {
|
||||
throw new Error(`Timed out stopping pid ${instance.pid}`);
|
||||
}
|
||||
removePidFile(instance.pidFilePath);
|
||||
removeInstanceFile(instance.instanceFilePath);
|
||||
stopSpin?.stop(`Stopped OpenChamber on port ${instance.port}`);
|
||||
jsonResults.push({ port: instance.port, pid: instance.pid, stopped: true });
|
||||
if (showOutput && !stopSpin) {
|
||||
logStatus('success', `stopped port ${instance.port}`);
|
||||
}
|
||||
} catch (error) {
|
||||
stopSpin?.error(`Failed to stop OpenChamber on port ${instance.port}`);
|
||||
jsonResults.push({ port: instance.port, pid: instance.pid, stopped: false, reason: error instanceof Error ? error.message : String(error) });
|
||||
if (showOutput) {
|
||||
logStatus('error', `error stopping port ${instance.port}`, error.message);
|
||||
} else if (!isJsonMode(options) && !isQuietMode(options)) {
|
||||
console.error(`Error stopping port ${instance.port}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isJsonMode(options)) {
|
||||
const stoppedCount = jsonResults.filter((entry) => entry.stopped).length;
|
||||
const hasFailure = jsonResults.some((entry) => !entry.stopped);
|
||||
printJson({
|
||||
status: hasFailure ? 'warning' : 'ok',
|
||||
stoppedCount,
|
||||
results: jsonResults,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
finish(`${runningInstances.length} instance(s)`);
|
||||
printQuietStopResults();
|
||||
}
|
||||
|
||||
async function restartCommand(options, serveCommand) {
|
||||
const commandContext = this && typeof this === 'object' ? this : {};
|
||||
const runStop = typeof commandContext.stop === 'function'
|
||||
? commandContext.stop.bind(commandContext)
|
||||
: stopCommand;
|
||||
const runServe = typeof commandContext.serve === 'function'
|
||||
? commandContext.serve.bind(commandContext)
|
||||
: serveCommand;
|
||||
const showOutput = shouldRenderHumanOutput(options);
|
||||
const restarted = [];
|
||||
|
||||
if (showOutput) {
|
||||
clackIntro('OpenChamber Restart');
|
||||
}
|
||||
|
||||
let runningInstances = await discoverLifecycleInstances(options);
|
||||
if (runningInstances.length === 0) {
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ restartedCount: 0, results: restarted });
|
||||
}
|
||||
if (showOutput) {
|
||||
logStatus('info', 'No running OpenChamber instances to restart');
|
||||
clackOutro('nothing to restart');
|
||||
} else if (isQuietMode(options)) {
|
||||
process.stdout.write('restarted 0\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const instance of runningInstances) {
|
||||
if (instance.runtime === 'desktop') {
|
||||
const message = `Port ${instance.port} is managed by OpenChamber Desktop and cannot be restarted with this command.`;
|
||||
if (isJsonMode(options)) {
|
||||
printJson({
|
||||
status: 'warning',
|
||||
restartedCount: 0,
|
||||
results: [{ fromPort: instance.port, runtime: 'desktop', ok: false, reason: 'desktop-managed' }],
|
||||
messages: [{ level: 'warning', code: 'DESKTOP_MANAGED_PORT', message }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (showOutput) {
|
||||
logStatus('warning', `port ${instance.port} is managed by OpenChamber Desktop`, 'cannot be restarted with this command');
|
||||
clackOutro('no changes applied');
|
||||
} else if (isQuietMode(options)) {
|
||||
process.stdout.write('restarted 0\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const storedOptions = instance.instanceFilePath
|
||||
? (readInstanceOptions(instance.instanceFilePath) || { port: instance.port })
|
||||
: { port: instance.port };
|
||||
const instanceHost = storedOptions.host || instance.host || options.host;
|
||||
const launchMode = instance.launchMode || 'daemon';
|
||||
const isForeground = launchMode === 'foreground';
|
||||
|
||||
const restartPort = options.explicitPort ? options.port : instance.port;
|
||||
|
||||
const restartSpin = showOutput ? createSpinner(options) : null;
|
||||
if (showOutput && !restartSpin) {
|
||||
logStatus('info', `restarting port ${instance.port}`, `mode: ${launchMode}`);
|
||||
}
|
||||
restartSpin?.start(`Restarting OpenChamber on port ${instance.port}...`);
|
||||
try {
|
||||
await runStop({
|
||||
explicitPort: true,
|
||||
port: instance.port,
|
||||
host: instanceHost,
|
||||
quiet: true,
|
||||
suppressQuietOutput: true,
|
||||
});
|
||||
|
||||
// Foreground instances are managed by a process manager (systemd,
|
||||
// Docker, etc.) that will restart them automatically after stop.
|
||||
// Do not call serve() here — just record the stop as a successful
|
||||
// restart and let the process manager handle the actual restart.
|
||||
if (isForeground) {
|
||||
restarted.push({ fromPort: instance.port, toPort: restartPort, launchMode, ok: true });
|
||||
restartSpin?.stop(`Stopped foreground instance on port ${instance.port} (process manager will restart)`);
|
||||
if (showOutput && !restartSpin) {
|
||||
logStatus('success', `port ${instance.port} stopped`, 'process manager will restart');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const restartedPort = await runServe({
|
||||
port: restartPort,
|
||||
host: instanceHost,
|
||||
explicitPort: true,
|
||||
uiPassword: options.explicitUiPassword ? options.uiPassword : (storedOptions.uiPassword || options.uiPassword),
|
||||
apiOnly: storedOptions.apiOnly === true || options.apiOnly === true,
|
||||
suppressStartupSummary: true,
|
||||
quiet: true,
|
||||
suppressUiPasswordWarning: true,
|
||||
suppressQuietOutput: true,
|
||||
});
|
||||
restarted.push({ fromPort: instance.port, toPort: restartedPort, launchMode, ok: true });
|
||||
restartSpin?.stop(`Restarted OpenChamber on port ${restartedPort}`);
|
||||
if (showOutput && !restartSpin) {
|
||||
logStatus('success', `port ${restartedPort} restarted`, `mode: ${launchMode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
restartSpin?.error(`Failed to restart OpenChamber on port ${instance.port}`);
|
||||
if (showOutput && !restartSpin) {
|
||||
logStatus('error', `failed to restart port ${instance.port}`, message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (isJsonMode(options)) {
|
||||
printJson({ restartedCount: restarted.length, results: restarted.map((r) => ({ ...r, launchMode: r.launchMode })) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (showOutput) {
|
||||
clackOutro(`${runningInstances.length} instance(s) restarted`);
|
||||
} else if (isQuietMode(options)) {
|
||||
process.stdout.write(`restarted ${restarted.length}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function createLifecycleCommands({ serveCommand }) {
|
||||
return {
|
||||
stop: stopCommand,
|
||||
restart(options) {
|
||||
return restartCommand.call(this, options, serveCommand);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { createLifecycleCommands };
|
||||
Reference in New Issue
Block a user