#!/usr/bin/env node import { run } from "./utils"; import { showStatus } from "./utils/status"; import { executeCodeCommand, PresetConfig } from "./utils/codeCommand"; import { cleanupPidFile, isServiceRunning, getServiceInfo, } from "./utils/processCheck"; import { runModelSelector } from "./utils/modelSelector"; import { activateCommand } from "./utils/activateCommand"; import { readConfigFile } from "./utils"; import { version } from "../package.json"; import { spawn, exec } from "child_process"; import {getPresetDir, loadConfigFromManifest, PID_FILE, readPresetFile, REFERENCE_COUNT_FILE} from "@CCR/shared"; import fs, { existsSync, readFileSync } from "fs"; import { join } from "path"; import { parseStatusLineData, StatusLineInput } from "./utils/statusline"; import {handlePresetCommand} from "./utils/preset"; const command = process.argv[2]; // Define all known commands const KNOWN_COMMANDS = [ "start", "stop", "restart", "status", "statusline", "code", "model", "preset", "activate", "env", "ui", "-v", "version", "-h", "help", ]; const HELP_TEXT = ` Usage: ccr [command] [preset-name] Commands: start Start server stop Stop server restart Restart server status Show server status statusline Integrated statusline code Execute claude command model Interactive model selection and configuration preset Manage presets (export, install, list, delete) activate Output environment variables for shell integration ui Open the web UI in browser -v, version Show version information -h, help Show help information Presets: Any preset directory in ~/.claude-code-router/presets/ Examples: ccr start ccr code "Write a Hello World" ccr my-preset "Write a Hello World" # Use preset configuration ccr model ccr preset export my-config # Export current config as preset ccr preset install /path/to/preset # Install a preset from directory ccr preset list # List all presets eval "$(ccr activate)" # Set environment variables globally ccr ui `; async function waitForService( timeout = 10000, initialDelay = 1000 ): Promise { // Wait for an initial period to let the service initialize await new Promise((resolve) => setTimeout(resolve, initialDelay)); const startTime = Date.now(); while (Date.now() - startTime < timeout) { const isRunning = isServiceRunning() if (isRunning) { // Wait for an additional short period to ensure service is fully ready await new Promise((resolve) => setTimeout(resolve, 500)); return true; } await new Promise((resolve) => setTimeout(resolve, 100)); } return false; } async function main() { const isRunning = isServiceRunning() // If command is not a known command, check if it's a preset if (command && !KNOWN_COMMANDS.includes(command)) { const manifest = await readPresetFile(command); if (manifest) { // This is a preset, load its configuration const presetDir = getPresetDir(command); const config = loadConfigFromManifest(manifest, presetDir); // Execute code command const codeArgs = process.argv.slice(3); // Get remaining arguments // Check noServer configuration const shouldStartServer = config.noServer !== true; // Build environment variable overrides let envOverrides: Record = {}; // Handle provider configuration (supports both old and new formats) let provider: any = null; // Old format: config.provider is the provider name if (config.provider && typeof config.provider === 'string') { const globalConfig = await readConfigFile(); provider = globalConfig.Providers?.find((p: any) => p.name === config.provider); } // New format: config.Providers is an array of providers else if (config.Providers && config.Providers.length > 0) { provider = config.Providers[0]; } // If noServer is not true, use local server baseurl if (shouldStartServer) { const globalConfig = await readConfigFile(); const port = globalConfig.PORT || 3456; envOverrides = { ...envOverrides, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}/preset/${command}`, }; } else if (provider) { // Handle api_base_url, remove /v1/messages suffix if (provider.api_base_url) { let baseUrl = provider.api_base_url; if (baseUrl.endsWith('/v1/messages')) { baseUrl = baseUrl.slice(0, -'/v1/messages'.length); } else if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } envOverrides = { ...envOverrides, ANTHROPIC_BASE_URL: baseUrl, }; } // Handle api_key if (provider.api_key) { envOverrides = { ...envOverrides, ANTHROPIC_AUTH_TOKEN: provider.api_key, }; } } // Build PresetConfig const presetConfig: PresetConfig = { noServer: config.noServer, claudeCodeSettings: config.claudeCodeSettings, StatusLine: config.StatusLine }; if (shouldStartServer && !isRunning) { console.log("Service not running, starting service..."); const cliPath = join(__dirname, "cli.js"); const startProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", }); startProcess.on("error", (error) => { console.error("Failed to start service:", error.message); process.exit(1); }); startProcess.unref(); if (await waitForService()) { executeCodeCommand(codeArgs, presetConfig, envOverrides, command); } else { console.error( "Service startup timeout, please manually run `ccr start` to start the service" ); process.exit(1); } } else { // Service is already running or no need to start server if (shouldStartServer && !isRunning) { console.error("Service is not running. Please start it first with `ccr start`"); process.exit(1); } executeCodeCommand(codeArgs, presetConfig, envOverrides, command); } return; } else { // Not a preset nor a known command console.log(HELP_TEXT); process.exit(1); } } switch (command) { case "start": await run(); break; case "stop": try { const pid = parseInt(readFileSync(PID_FILE, "utf-8")); process.kill(pid); cleanupPidFile(); if (existsSync(REFERENCE_COUNT_FILE)) { try { fs.unlinkSync(REFERENCE_COUNT_FILE); } catch (e) { // Ignore cleanup errors } } console.log( "claude code router service has been successfully stopped." ); } catch (e) { console.log( "Failed to stop the service. It may have already been stopped." ); cleanupPidFile(); } break; case "status": await showStatus(); break; case "statusline": // Read JSON input from stdin let inputData = ""; process.stdin.setEncoding("utf-8"); process.stdin.on("readable", () => { let chunk; while ((chunk = process.stdin.read()) !== null) { inputData += chunk; } }); process.stdin.on("end", async () => { try { const input: StatusLineInput = JSON.parse(inputData); // Check if preset name is provided as argument const presetName = process.argv[3]; const statusLine = await parseStatusLineData(input, presetName); console.log(statusLine); } catch (error) { console.error("Error parsing status line data:", error); process.exit(1); } }); break; // ADD THIS CASE case "model": await runModelSelector(); break; case "preset": await handlePresetCommand(process.argv.slice(3)); break; case "activate": case "env": await activateCommand(); break; case "code": if (!isRunning) { console.log("Service not running, starting service..."); const cliPath = join(__dirname, "cli.js"); const startProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", }); startProcess.on("error", (error) => { console.error("Failed to start service:", error.message); process.exit(1); }); startProcess.unref(); if (await waitForService()) { const codeArgs = process.argv.slice(3); executeCodeCommand(codeArgs); } else { console.error( "Service startup timeout, please manually run `ccr start` to start the service" ); process.exit(1); } } else { const codeArgs = process.argv.slice(3); executeCodeCommand(codeArgs); } break; case "ui": // Check if service is running if (!isRunning) { console.log("Service not running, starting service..."); const cliPath = join(__dirname, "cli.js"); const startProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", }); startProcess.on("error", (error) => { console.error("Failed to start service:", error.message); process.exit(1); }); startProcess.unref(); if (!(await waitForService())) { // If service startup fails, try to start with default config console.log( "Service startup timeout, trying to start with default configuration..." ); const { initDir, writeConfigFile, backupConfigFile, } = require("./utils"); try { // Initialize directories await initDir(); // Backup existing config file if it exists const backupPath = await backupConfigFile(); if (backupPath) { console.log( `Backed up existing configuration file to ${backupPath}` ); } // Create a minimal default config file await writeConfigFile({ PORT: 3456, Providers: [], Router: {}, }); console.log( "Created minimal default configuration file at ~/.claude-code-router/config.json" ); console.log( "Please edit this file with your actual configuration." ); // Try starting the service again const restartProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", }); restartProcess.on("error", (error) => { console.error( "Failed to start service with default config:", error.message ); process.exit(1); }); restartProcess.unref(); if (!(await waitForService(15000))) { // Wait a bit longer for the first start console.error( "Service startup still failing. Please manually run `ccr start` to start the service and check the logs." ); process.exit(1); } } catch (error: any) { console.error( "Failed to create default configuration:", error.message ); process.exit(1); } } } // Get service info and open UI const serviceInfo = await getServiceInfo(); // Add temporary API key as URL parameter if successfully generated const uiUrl = `${serviceInfo.endpoint}/ui/`; console.log(`Opening UI at ${uiUrl}`); // Open URL in browser based on platform const platform = process.platform; let openCommand = ""; if (platform === "win32") { // Windows openCommand = `start ${uiUrl}`; } else if (platform === "darwin") { // macOS openCommand = `open ${uiUrl}`; } else if (platform === "linux") { // Linux openCommand = `xdg-open ${uiUrl}`; } else { console.error("Unsupported platform for opening browser"); process.exit(1); } exec(openCommand, (error) => { if (error) { console.error("Failed to open browser:", error.message); process.exit(1); } }); break; case "-v": case "version": console.log(`claude-code-router version: ${version}`); break; case "restart": // Stop the service if it's running try { const pid = parseInt(readFileSync(PID_FILE, "utf-8")); process.kill(pid); cleanupPidFile(); if (existsSync(REFERENCE_COUNT_FILE)) { try { fs.unlinkSync(REFERENCE_COUNT_FILE); } catch (e) { // Ignore cleanup errors } } console.log("claude code router service has been stopped."); } catch (e) { console.log("Service was not running or failed to stop."); cleanupPidFile(); } // Start the service again in the background console.log("Starting claude code router service..."); const cliPath = join(__dirname, "cli.js"); const startProcess = spawn("node", [cliPath, "start"], { detached: true, stdio: "ignore", }); startProcess.on("error", (error) => { console.error("Failed to start service:", error); process.exit(1); }); startProcess.unref(); console.log("✅ Service started successfully in the background."); break; case "-h": case "help": console.log(HELP_TEXT); break; default: console.log(HELP_TEXT); process.exit(1); } } main().catch(console.error);