diff --git a/src/ccs.ts b/src/ccs.ts index 3846d23b..eb6f2d11 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -282,6 +282,13 @@ async function main(): Promise { return; } + // Special case: cliproxy command (manages CLIProxyAPI binary) + if (firstArg === 'cliproxy') { + const { handleCliproxyCommand } = await import('./commands/cliproxy-command'); + await handleCliproxyCommand(args.slice(1)); + return; + } + // Special case: headless delegation (-p flag) if (args.includes('-p') || args.includes('--prompt')) { const { DelegationHandler } = await import('./delegation/delegation-handler'); diff --git a/src/cliproxy/binary-manager.ts b/src/cliproxy/binary-manager.ts index b2d26bb1..81a5c8f6 100644 --- a/src/cliproxy/binary-manager.ts +++ b/src/cliproxy/binary-manager.ts @@ -63,6 +63,7 @@ const DEFAULT_CONFIG: BinaryManagerConfig = { binPath: getBinDir(), maxRetries: 3, verbose: false, + forceVersion: false, }; /** @@ -88,6 +89,12 @@ export class BinaryManager { if (fs.existsSync(binaryPath)) { this.log(`Binary exists: ${binaryPath}`); + // Skip auto-update if forceVersion is set (user requested specific version) + if (this.config.forceVersion) { + this.log(`Force version mode: skipping auto-update`); + return this.getBinaryPath(); + } + // Check for updates in background (non-blocking for UX) try { const updateResult = await this.checkForUpdates(); @@ -114,16 +121,21 @@ export class BinaryManager { // Download, verify, extract this.log('Binary not found, downloading...'); - // Check latest version before first download - try { - const latestVersion = await this.fetchLatestVersion(); - if (latestVersion && this.isNewerVersion(latestVersion, this.config.version)) { - this.log(`Using latest version: ${latestVersion} (instead of ${this.config.version})`); - this.config.version = latestVersion; + // Skip auto-upgrade to latest if forceVersion is set + if (!this.config.forceVersion) { + // Check latest version before first download + try { + const latestVersion = await this.fetchLatestVersion(); + if (latestVersion && this.isNewerVersion(latestVersion, this.config.version)) { + this.log(`Using latest version: ${latestVersion} (instead of ${this.config.version})`); + this.config.version = latestVersion; + } + } catch { + // Use pinned version if API fails + this.log(`Using pinned version: ${this.config.version}`); } - } catch { - // Use pinned version if API fails - this.log(`Using pinned version: ${this.config.version}`); + } else { + this.log(`Force version mode: using specified version ${this.config.version}`); } await this.downloadAndInstall(); @@ -916,4 +928,38 @@ export function getInstalledCliproxyVersion(): string { return CLIPROXY_FALLBACK_VERSION; } +/** + * Install a specific version of CLIProxyAPI + * Deletes existing binary and downloads the specified version + * + * @param version Version to install (e.g., "6.5.40") + * @param verbose Enable verbose logging + */ +export async function installCliproxyVersion(version: string, verbose = false): Promise { + // Use forceVersion to prevent auto-upgrade to latest + const manager = new BinaryManager({ version, verbose, forceVersion: true }); + + // Delete existing binary if present + if (manager.isBinaryInstalled()) { + const currentVersion = getInstalledCliproxyVersion(); + if (verbose) { + console.log(`[i] Removing existing CLIProxyAPI v${currentVersion}`); + } + manager.deleteBinary(); + } + + // Install specified version (forceVersion prevents auto-upgrade) + await manager.ensureBinary(); +} + +/** + * Fetch the latest CLIProxyAPI version from GitHub API + * @returns Latest version string (e.g., "6.5.40") + */ +export async function fetchLatestCliproxyVersion(): Promise { + const manager = new BinaryManager(); + const result = await manager.checkForUpdates(); + return result.latestVersion; +} + export default BinaryManager; diff --git a/src/cliproxy/index.ts b/src/cliproxy/index.ts index eeffe239..f7f855e5 100644 --- a/src/cliproxy/index.ts +++ b/src/cliproxy/index.ts @@ -41,6 +41,8 @@ export { isCLIProxyInstalled, getCLIProxyPath, getInstalledCliproxyVersion, + installCliproxyVersion, + fetchLatestCliproxyVersion, } from './binary-manager'; // Config generation diff --git a/src/cliproxy/types.ts b/src/cliproxy/types.ts index 86c20be7..a9ae85a7 100644 --- a/src/cliproxy/types.ts +++ b/src/cliproxy/types.ts @@ -46,6 +46,8 @@ export interface BinaryManagerConfig { maxRetries: number; /** Enable verbose logging */ verbose: boolean; + /** Force specific version (skip auto-upgrade to latest) */ + forceVersion?: boolean; } /** diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts new file mode 100644 index 00000000..f0252d9a --- /dev/null +++ b/src/commands/cliproxy-command.ts @@ -0,0 +1,198 @@ +/** + * CLIProxy Command Handler + * + * Manages CLIProxyAPI binary installation and version control. + * Allows users to install specific versions or update to latest. + * + * Usage: + * ccs cliproxy Show current version + * ccs cliproxy --install Install specific version + * ccs cliproxy --latest Install latest version + * ccs cliproxy --help Show help + */ + +import { + getInstalledCliproxyVersion, + installCliproxyVersion, + fetchLatestCliproxyVersion, + isCLIProxyInstalled, + getCLIProxyPath, +} from '../cliproxy'; +import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector'; +import { color, dim, initUI } from '../utils/ui'; + +/** + * Show cliproxy command help + */ +function showHelp(): void { + console.log(''); + console.log('Usage: ccs cliproxy [options]'); + console.log(''); + console.log('Manage CLIProxyAPI binary installation.'); + console.log(''); + console.log('Options:'); + console.log(' --install Install a specific version (e.g., 6.5.40)'); + console.log(' --latest Install the latest version from GitHub'); + console.log(' --verbose, -v Enable verbose output'); + console.log(' --help, -h Show this help message'); + console.log(''); + console.log('Examples:'); + console.log(' ccs cliproxy Show current installed version'); + console.log(' ccs cliproxy --install 6.5.38 Install version 6.5.38'); + console.log(' ccs cliproxy --latest Update to latest version'); + console.log(''); + console.log('Notes:'); + console.log(` Default fallback version: ${CLIPROXY_FALLBACK_VERSION}`); + console.log(' Releases: https://github.com/router-for-me/CLIProxyAPI/releases'); + console.log(''); +} + +/** + * Show current cliproxy status + */ +async function showStatus(verbose: boolean): Promise { + await initUI(); + + const installed = isCLIProxyInstalled(); + const currentVersion = getInstalledCliproxyVersion(); + const binaryPath = getCLIProxyPath(); + + console.log(''); + console.log(color('CLIProxyAPI Status', 'primary')); + console.log(''); + + if (installed) { + console.log(` Installed: ${color('Yes', 'success')}`); + console.log(` Version: ${color(`v${currentVersion}`, 'info')}`); + console.log(` Binary: ${dim(binaryPath)}`); + } else { + console.log(` Installed: ${color('No', 'error')}`); + console.log(` Fallback: ${color(`v${CLIPROXY_FALLBACK_VERSION}`, 'info')}`); + console.log(` ${dim('Run "ccs gemini" or any provider to auto-install')}`); + } + + // Try to fetch latest version + try { + console.log(''); + console.log(` ${dim('Checking for updates...')}`); + const latestVersion = await fetchLatestCliproxyVersion(); + + if (latestVersion !== currentVersion) { + console.log( + ` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(update available)')}` + ); + console.log(''); + console.log(` ${dim(`Run "ccs cliproxy --latest" to update`)}`); + } else { + console.log(` Latest: ${color(`v${latestVersion}`, 'success')} ${dim('(up to date)')}`); + } + } catch (error) { + if (verbose) { + const err = error as Error; + console.log(` Latest: ${dim(`Could not fetch (${err.message})`)}`); + } + } + + console.log(''); +} + +/** + * Install a specific version + */ +async function installVersion(version: string, verbose: boolean): Promise { + // Validate version format (basic semver check) + if (!/^\d+\.\d+\.\d+$/.test(version)) { + console.error('[X] Invalid version format. Expected format: X.Y.Z (e.g., 6.5.40)'); + process.exit(1); + } + + console.log(`[i] Installing CLIProxyAPI v${version}...`); + console.log(''); + + try { + await installCliproxyVersion(version, verbose); + console.log(''); + console.log(`[OK] CLIProxyAPI v${version} installed successfully`); + } catch (error) { + const err = error as Error; + console.error(''); + console.error(`[X] Failed to install CLIProxyAPI v${version}`); + console.error(` ${err.message}`); + console.error(''); + console.error('Possible causes:'); + console.error(' 1. Version does not exist on GitHub'); + console.error(' 2. Network connectivity issues'); + console.error(' 3. GitHub API rate limiting'); + console.error(''); + console.error('Check available versions at:'); + console.error(' https://github.com/router-for-me/CLIProxyAPI/releases'); + process.exit(1); + } +} + +/** + * Install latest version + */ +async function installLatest(verbose: boolean): Promise { + console.log('[i] Fetching latest CLIProxyAPI version...'); + + try { + const latestVersion = await fetchLatestCliproxyVersion(); + const currentVersion = getInstalledCliproxyVersion(); + + if (isCLIProxyInstalled() && latestVersion === currentVersion) { + console.log(`[OK] Already running latest version: v${latestVersion}`); + return; + } + + console.log(`[i] Latest version: v${latestVersion}`); + if (isCLIProxyInstalled()) { + console.log(`[i] Current version: v${currentVersion}`); + } + console.log(''); + + await installCliproxyVersion(latestVersion, verbose); + console.log(''); + console.log(`[OK] CLIProxyAPI updated to v${latestVersion}`); + } catch (error) { + const err = error as Error; + console.error(`[X] Failed to install latest version: ${err.message}`); + process.exit(1); + } +} + +/** + * Main cliproxy command handler + */ +export async function handleCliproxyCommand(args: string[]): Promise { + const verbose = args.includes('--verbose') || args.includes('-v'); + + // Handle --help + if (args.includes('--help') || args.includes('-h')) { + showHelp(); + return; + } + + // Handle --install + const installIdx = args.indexOf('--install'); + if (installIdx !== -1) { + const version = args[installIdx + 1]; + if (!version || version.startsWith('-')) { + console.error('[X] Missing version argument for --install'); + console.error(' Usage: ccs cliproxy --install '); + console.error(' Example: ccs cliproxy --install 6.5.40'); + process.exit(1); + } + await installVersion(version, verbose); + return; + } + + // Handle --latest + if (args.includes('--latest')) { + await installLatest(verbose); + return; + } + + // Default: show status + await showStatus(verbose); +} diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 7f17601c..70b23fd2 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -204,6 +204,13 @@ Claude Code Profile & Model Switcher`.trim(); ['Settings:', '~/.ccs/*.settings.json'], ]); + // CLI Proxy management + printSubSection('CLI Proxy Management', [ + ['ccs cliproxy', 'Show CLIProxyAPI status and version'], + ['ccs cliproxy --install ', 'Install specific version (e.g., 6.5.40)'], + ['ccs cliproxy --latest', 'Update to latest version'], + ]); + // CLI Proxy paths console.log(subheader('CLI Proxy:')); console.log(` Binary: ${color('~/.ccs/cliproxy/bin/cli-proxy-api', 'path')}`);