Files
ccs/src/commands/cliproxy/proxy-lifecycle-subcommand.ts
T

162 lines
5.9 KiB
TypeScript

/**
* CLIProxy Lifecycle Management
*
* Handles:
* - ccs cliproxy start
* - ccs cliproxy restart
* - ccs cliproxy status
* - ccs cliproxy stop
*/
import { initUI, header, color, dim, ok, warn, info } from '../../utils/ui';
import { getProxyStatus, startProxy, stopProxy } from '../../cliproxy/services';
import { detectRunningProxy } from '../../cliproxy/proxy/proxy-detector';
import { resolveLifecyclePort } from '../../cliproxy/config/port-manager';
import { getEffectiveManagementSecret, maskToken } from '../../cliproxy/auth/auth-token-manager';
/**
* Print how to reach the local CLIProxy Control Panel (a.k.a. API Management
* Center) without exposing the management secret in routine output.
*
* The panel is served by CLIProxy at `/management.html` on the proxy port and
* its login is gated by the management secret (default `ccs`). Keep the resolved
* key masked here because `start` and `status` output is commonly shared in
* support tickets and logs. Users can explicitly reveal tokens with
* `ccs tokens --show` when they need the raw value.
*/
export function printControlPanelAccess(port: number): void {
const secret = getEffectiveManagementSecret();
console.log('');
console.log(` Control Panel: http://127.0.0.1:${port}/management.html`);
console.log(` Panel login key: ${maskToken(secret)}`);
console.log(dim(' To show the full key: ccs tokens --show'));
}
export async function handleStart(verbose = false): Promise<void> {
await initUI();
console.log(header('Start CLIProxy'));
console.log('');
const port = resolveLifecyclePort();
const result = await startProxy(port, verbose);
if (result.started) {
if (result.alreadyRunning) {
console.log(info(`CLIProxy already running on port ${result.port}`));
if (result.configRegenerated) {
console.log(warn('Config updated - restart CLIProxy to apply changes'));
}
} else {
console.log(ok(`CLIProxy started on port ${result.port}`));
}
console.log(dim('To stop: ccs cliproxy stop'));
if (result.port) {
printControlPanelAccess(result.port);
}
} else {
console.log(warn(result.error || 'Failed to start CLIProxy'));
}
console.log('');
}
export async function handleRestart(verbose = false): Promise<void> {
await initUI();
console.log(header('Restart CLIProxy'));
console.log('');
const port = resolveLifecyclePort();
const stopResult = await stopProxy(port);
if (stopResult.stopped) {
console.log(ok(`CLIProxy stopped (PID ${stopResult.pid})`));
} else if (stopResult.error === 'No active CLIProxy session found') {
console.log(info('No active CLIProxy session found, starting a new instance'));
} else {
console.log(warn(stopResult.error || 'Failed to stop existing CLIProxy'));
console.log(info('Attempting to start a fresh instance...'));
}
const startResult = await startProxy(port, verbose);
if (startResult.started) {
if (startResult.alreadyRunning) {
console.log(info(`CLIProxy already running on port ${startResult.port}`));
} else {
console.log(ok(`CLIProxy started on port ${startResult.port}`));
}
} else {
console.log(warn(startResult.error || 'Failed to restart CLIProxy'));
}
console.log('');
}
export async function handleProxyStatus(verbose = false): Promise<void> {
await initUI();
console.log(header('CLIProxy Status'));
console.log('');
const port = resolveLifecyclePort();
const status = getProxyStatus(port);
if (status.running) {
console.log(` Status: ${color('Running', 'success')}`);
console.log(` PID: ${status.pid}`);
console.log(` Port: ${status.port}`);
console.log(` Sessions: ${status.sessionCount || 0} active`);
if (status.startedAt) {
console.log(` Started: ${new Date(status.startedAt).toLocaleString()}`);
if (verbose) {
const uptimeMs = Date.now() - new Date(status.startedAt).getTime();
const uptimeSec = Math.floor(uptimeMs / 1000);
const h = Math.floor(uptimeSec / 3600);
const m = Math.floor((uptimeSec % 3600) / 60);
const s = uptimeSec % 60;
const uptimeStr = h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
console.log(` Uptime: ${uptimeStr}`);
}
}
console.log('');
console.log(dim('To stop: ccs cliproxy stop'));
printControlPanelAccess(status.port ?? port);
} else {
// Fallback: detect untracked/orphaned proxy process (e.g. detached session without lock file).
const detected = await detectRunningProxy(port);
if (detected.running && detected.verified) {
console.log(` Status: ${color('Running', 'success')}`);
console.log(` PID: ${detected.pid ?? 'unknown'}`);
console.log(` Port: ${port}`);
console.log(` Sessions: ${detected.sessionCount || 0} active`);
if (!detected.sessionCount) {
console.log(dim(' Note: Detected running proxy without local session lock'));
}
if (verbose) {
console.log(dim(' Note: Process detected via port scan; no local lock file present'));
}
console.log('');
console.log(dim('To stop: ccs cliproxy stop'));
printControlPanelAccess(port);
} else {
console.log(` Status: ${color('Not running', 'warning')}`);
console.log('');
console.log(dim('CLIProxy starts automatically when you run ccs gemini, codex, etc.'));
}
}
console.log('');
}
export async function handleStop(): Promise<void> {
await initUI();
console.log(header('Stop CLIProxy'));
console.log('');
const port = resolveLifecyclePort();
const result = await stopProxy(port);
if (result.stopped) {
console.log(ok(`CLIProxy stopped (PID ${result.pid})`));
if (result.sessionCount && result.sessionCount > 0) {
console.log(info(`${result.sessionCount} active session(s) were disconnected`));
}
} else {
console.log(warn(result.error || 'Failed to stop CLIProxy'));
}
console.log('');
}