Files
ccs/src/cliproxy/executor/lifecycle-manager.ts
T
kaitranntt b149e252eb refactor(cliproxy): modularize top 4 giant files
Split 4 files exceeding 900 lines into focused modules:

- account-manager.ts (961 → 56 lines): 6 modules in accounts/
  - registry, query, bulk-ops, token-file-ops, types, index
- config-generator.ts (981 → 16 lines): 6 modules in config/
  - generator, port-manager, env-builder, thinking-config, path-resolver, index
- cliproxy-executor.ts (1,180 → 21 lines): 5 modules in executor/
  - index, lifecycle-manager, env-resolver, retry-handler, session-bridge
- cliproxy-command.ts (1,297 → 10 lines): 8 modules in commands/cliproxy/
  - index, auth, quota, variant, install, help, proxy-lifecycle subcommands

Total: 4,419 → 103 lines in original files (97.7% reduction)

Backward compatibility maintained via re-export shims.
Edge case fixes: CLI validation, thinking fallback, provider warning.

BREAKING CHANGES: None - all exports preserved via barrel re-exports.
2026-02-05 15:00:02 -05:00

161 lines
4.4 KiB
TypeScript

/**
* Lifecycle Manager - Spawn/Kill/Poll operations for CLIProxy
*
* Handles:
* - Spawning CLIProxyAPI binary
* - Waiting for proxy readiness via TCP polling
* - Killing proxy processes
*/
import { spawn, ChildProcess } from 'child_process';
import * as net from 'net';
import { ProgressIndicator } from '../../utils/progress-indicator';
import { fail } from '../../utils/ui';
import { getCliproxyWritablePath } from '../config-generator';
import { getPortCheckCommand, getCatCommand } from '../../utils/platform-commands';
import { CLIProxyBackend } from '../types';
/**
* Wait for TCP port to become available
* Uses polling since CLIProxyAPI doesn't emit PROXY_READY signal
*/
export async function waitForProxyReady(
port: number,
timeout: number = 5000,
pollInterval: number = 100
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
await new Promise<void>((resolve, reject) => {
const socket = net.createConnection({ port, host: '127.0.0.1' }, () => {
socket.destroy();
resolve();
});
socket.on('error', (err) => {
socket.destroy();
reject(err);
});
// Individual connection timeout
socket.setTimeout(1000, () => {
socket.destroy();
reject(new Error('Connection timeout'));
});
});
return; // Connection successful - proxy is ready
} catch {
// Connection failed, wait and retry
await new Promise((r) => setTimeout(r, pollInterval));
}
}
throw new Error(`CLIProxy not ready after ${timeout}ms on port ${port}`);
}
/**
* Spawn CLIProxyAPI binary with given config
*/
export function spawnProxy(binaryPath: string, configPath: string, verbose: boolean): ChildProcess {
const log = (msg: string) => {
if (verbose) {
console.error(`[cliproxy] ${msg}`);
}
};
const proxyArgs = ['--config', configPath];
log(`Spawning: ${binaryPath} ${proxyArgs.join(' ')}`);
const proxy = spawn(binaryPath, proxyArgs, {
stdio: ['ignore', 'ignore', 'ignore'],
detached: true,
env: {
...process.env,
WRITABLE_PATH: getCliproxyWritablePath(),
},
});
proxy.unref();
proxy.on('error', (error) => {
console.error(fail(`CLIProxy spawn error: ${error.message}`));
});
return proxy;
}
/**
* Wait for proxy to be ready with progress indication
*/
export async function waitForProxyReadyWithSpinner(
port: number,
timeout: number,
pollInterval: number,
backend: CLIProxyBackend,
configPath: string
): Promise<void> {
const readySpinner = new ProgressIndicator(`Waiting for CLIProxy on port ${port}`);
readySpinner.start();
try {
await waitForProxyReady(port, timeout, pollInterval);
readySpinner.succeed(`CLIProxy ready on port ${port}`);
} catch (error) {
const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy';
readySpinner.fail(`${backendLabel} startup failed`);
const err = error as Error;
console.error('');
console.error(fail(`${backendLabel} failed to start`));
console.error('');
console.error('Possible causes:');
console.error(` 1. Port ${port} already in use`);
console.error(' 2. Binary crashed on startup');
console.error(' 3. Invalid configuration');
console.error('');
console.error('Troubleshooting:');
console.error(` - Check port: ${getPortCheckCommand(port)}`);
console.error(' - Run with --verbose for detailed logs');
console.error(` - View config: ${getCatCommand(configPath)}`);
console.error(' - Try: ccs doctor --fix');
console.error('');
throw new Error(`CLIProxy startup failed: ${err.message}`);
}
}
/**
* Check if a port is available
*/
export async function isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => {
resolve(false);
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port, '127.0.0.1');
});
}
/**
* Find an available port in range
*/
export async function findAvailablePort(startPort: number, range: number = 10): Promise<number> {
for (let port = startPort; port < startPort + range; port++) {
if (await isPortAvailable(port)) {
return port;
}
}
throw new Error(`No available port found in range ${startPort}-${startPort + range - 1}`);
}