route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
314 lines ⢠14.1 kB
JavaScript
;
/**
* Code command implementation (demo1 style)
* Stable process management with reference counting
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeCodeCommand = executeCodeCommand;
const chalk_1 = __importDefault(require("chalk"));
const fs_1 = require("fs");
const utils_1 = require("./utils");
/**
* Load configuration from file
*/
function loadConfig(configPath) {
try {
if (!(0, fs_1.existsSync)(configPath)) {
console.log(chalk_1.default.yellow(`ā ļø Config file not found at ${configPath}`));
process.exit(1);
}
const configData = (0, fs_1.readFileSync)(configPath, 'utf8');
const config = JSON.parse(configData);
// No defaults - user must provide complete configuration
return config;
}
catch (error) {
console.log(chalk_1.default.red('ā Failed to load configuration:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
async function executeCodeCommand(args, options) {
try {
// Import required modules
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
// Import fetch for Node.js compatibility
const fetch = globalThis.fetch;
let config = null;
let port;
let host;
// If port is specified, skip config loading and use direct connection
if (options.port) {
port = options.port;
host = options.host || '127.0.0.1';
console.log(chalk_1.default.blue(`šÆ Direct connection mode: targeting ${host}:${port}`));
}
else {
// Load configuration only when not using --port
const configPath = (0, utils_1.resolvePath)(options.config);
config = loadConfig(configPath);
// Override with CLI options
if (options.host)
config.server.host = options.host;
if (options.debug) {
config.debug.enabled = true;
config.debug.traceRequests = true;
}
if (options.logLevel)
config.debug.logLevel = options.logLevel;
port = config.server.port;
host = config.server.host;
}
// Reference counting for service management
const refCountFile = path.join(os.tmpdir(), 'ccr-reference-count.txt');
const configPaths = (0, utils_1.getConfigPaths)();
const pidFile = path.join(configPaths.configDir, '.ccr.pid');
function incrementReferenceCount() {
let count = 0;
if (fs.existsSync(refCountFile)) {
try {
count = parseInt(fs.readFileSync(refCountFile, 'utf8').trim()) || 0;
}
catch { }
}
fs.writeFileSync(refCountFile, (count + 1).toString());
}
function decrementReferenceCount() {
let count = 0;
if (fs.existsSync(refCountFile)) {
try {
count = parseInt(fs.readFileSync(refCountFile, 'utf8').trim()) || 0;
}
catch { }
}
const newCount = Math.max(0, count - 1);
if (newCount === 0) {
try {
fs.unlinkSync(refCountFile);
}
catch { }
}
else {
fs.writeFileSync(refCountFile, newCount.toString());
}
return newCount;
}
function isServiceRunning() {
if (!fs.existsSync(pidFile)) {
return false;
}
try {
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
process.kill(pid, 0); // Check if process exists
return true;
}
catch {
// Process doesn't exist, clean up stale PID file
try {
fs.unlinkSync(pidFile);
}
catch { }
return false;
}
}
async function waitForService(timeout = 15000) {
const startTime = Date.now();
await new Promise(resolve => setTimeout(resolve, 1000)); // Initial delay
while (Date.now() - startTime < timeout) {
try {
const response = await fetch(`http://${host}:${port}/status`);
if (response.ok) {
await new Promise(resolve => setTimeout(resolve, 500)); // Extra stability delay
return true;
}
}
catch { }
await new Promise(resolve => setTimeout(resolve, 100));
}
return false;
}
async function checkServiceOnPort(checkPort, checkHost = host) {
return new Promise((resolve) => {
const http = require('http');
const url = `http://${checkHost}:${checkPort}/status`;
console.log(chalk_1.default.gray(` Testing: ${url}`));
const req = http.get(url, (res) => {
const isOk = res.statusCode >= 200 && res.statusCode < 300;
console.log(chalk_1.default.gray(` Result: ${isOk ? 'ā
' : 'ā'} (${res.statusCode})`));
resolve({ success: isOk });
res.resume(); // Consume response to free up memory
});
req.on('error', (error) => {
console.log(chalk_1.default.gray(` Error: ${error.message}`));
resolve({ success: false, error: error.code || error.message });
});
req.setTimeout(5000, () => {
console.log(chalk_1.default.gray(` Error: Timeout`));
req.destroy();
resolve({ success: false, error: 'ETIMEDOUT' });
});
});
}
function startBackgroundService() {
console.log(chalk_1.default.blue('š Starting Claude Code Router service...'));
// Use the built dist/cli.js for starting service
const cliPath = path.join(__dirname, 'cli.js');
const startArgs = ['start'];
if (options.debug)
startArgs.push('--debug');
if (options.config)
startArgs.push('--config', options.config);
if (options.port)
startArgs.push('--port', options.port.toString());
const startProcess = spawn('node', [cliPath, ...startArgs], {
detached: true, // Detach from parent process
stdio: ['ignore', 'pipe', 'pipe'], // Don't inherit stdio to avoid TTY issues
env: process.env
});
startProcess.unref(); // Allow parent to exit independently
// Don't wait for background process
startProcess.stdout?.on('data', () => { }); // Consume output to prevent hanging
startProcess.stderr?.on('data', () => { });
}
async function executeClaudeCode(claudeArgs = [], overrideHost) {
incrementReferenceCount();
// Set environment variables for claude subprocess
const hostForUrl = overrideHost || (host === '0.0.0.0' ? '127.0.0.1' : host);
const env = {
...process.env,
ANTHROPIC_BASE_URL: `http://${hostForUrl}:${port}`,
ANTHROPIC_API_KEY: 'any-string-is-ok',
API_TIMEOUT_MS: '600000' // 10 minutes
};
console.log(chalk_1.default.green('š§ Environment configured for Claude Code routing'));
console.log(chalk_1.default.blue(`š¤ Launching Claude Code${claudeArgs.length ? ` with args: ${Array.isArray(claudeArgs) ? claudeArgs.join(' ') : claudeArgs}` : ''}...`));
console.log(chalk_1.default.cyan('===============================================\n'));
const claudePath = process.env.CLAUDE_PATH || 'claude';
// Ensure claudeArgs is an array
const argsArray = Array.isArray(claudeArgs) ? claudeArgs : (claudeArgs ? [claudeArgs] : []);
const claudeProcess = spawn(claudePath, argsArray, {
env,
stdio: 'inherit', // Inherit stdio for interactive Claude Code
shell: true // Use shell for better compatibility
});
// Handle normal exit
claudeProcess.on('close', (code) => {
console.log(chalk_1.default.cyan(`\nš Claude Code session ended (exit code: ${code})`));
const refCount = decrementReferenceCount();
if (refCount === 0) {
console.log(chalk_1.default.green('ā
No more Claude Code instances, router will stay running'));
}
process.exit(code || 0);
});
// Handle errors
claudeProcess.on('error', (error) => {
decrementReferenceCount();
if (error.message.includes('ENOENT')) {
console.log(chalk_1.default.red('ā Claude Code not found'));
console.log(chalk_1.default.yellow('š” Install Claude Code: npm install -g @anthropic-ai/claude-code'));
}
else {
console.log(chalk_1.default.red('ā Failed to launch Claude Code:'), error.message);
}
process.exit(1);
});
// Graceful cleanup on signals
const cleanup = () => {
decrementReferenceCount();
if (claudeProcess && !claudeProcess.killed) {
claudeProcess.kill('SIGTERM');
}
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
}
// Main logic implementation
if (options.port) {
// Mode 1: Port specified - check connection, start service if not running
console.log(chalk_1.default.blue(`š Checking for service on specified port ${port}...`));
const hostsToTry = options.host ? [host] : ['127.0.0.1', '0.0.0.0'];
let serviceFound = false;
let workingHost = '';
const connectionErrors = [];
for (const hostToTry of hostsToTry) {
const result = await checkServiceOnPort(port, hostToTry);
if (result.success) {
serviceFound = true;
workingHost = hostToTry;
break;
}
else if (result.error) {
connectionErrors.push(`${hostToTry}: ${result.error}`);
}
}
if (serviceFound) {
console.log(chalk_1.default.green(`ā
Router service found on ${workingHost}:${port}!`));
await executeClaudeCode(args, workingHost);
}
else {
console.log(chalk_1.default.yellow(`ā ļø No service found on port ${port}, starting a new service...`));
// Start service on the specified port
startBackgroundService();
if (await waitForService()) {
console.log(chalk_1.default.green('ā
Router service is ready!'));
// Use the first host that works
const connectHost = options.host || '127.0.0.1';
await executeClaudeCode(args, connectHost);
}
else {
console.log(chalk_1.default.red('ā Failed to start router service'));
process.exit(1);
}
}
}
else if (config && options.config && options.config !== configPaths.configFile) {
// Mode 2: Config specified - start service for this specific config
console.log(chalk_1.default.blue(`šÆ Starting service with specified config: ${options.config}`));
if (!isServiceRunning()) {
startBackgroundService();
if (await waitForService()) {
console.log(chalk_1.default.green('ā
Router service is ready!'));
await executeClaudeCode(args);
}
else {
console.log(chalk_1.default.red('ā Failed to start router service'));
process.exit(1);
}
}
else {
console.log(chalk_1.default.green('ā
Router service already running'));
await executeClaudeCode(args);
}
}
else {
// Mode 3: Default mode - intelligent config detection and service startup
console.log(chalk_1.default.blue('šÆ Default mode: Intelligent configuration detection'));
if (!isServiceRunning()) {
startBackgroundService();
if (await waitForService()) {
console.log(chalk_1.default.green('ā
Router service is ready!'));
await executeClaudeCode(args);
}
else {
console.log(chalk_1.default.red('ā Failed to start router service'));
process.exit(1);
}
}
else {
console.log(chalk_1.default.green('ā
Router service already running'));
await executeClaudeCode(args);
}
}
}
catch (error) {
console.log(chalk_1.default.red('ā Failed to execute code command:'), error instanceof Error ? error.message : error);
process.exit(1);
}
}
//# sourceMappingURL=code-command.js.map