/** * CLIProxy Command Dispatcher * * Routes cliproxy subcommands to their respective handlers. * This is the main entry point for all `ccs cliproxy` commands. */ import { CLIProxyBackend } from '../../cliproxy/types'; import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector'; import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import { handleSync } from '../cliproxy-sync-handler'; // Import subcommand handlers import { handleList } from './auth-subcommand'; import { handleQuotaStatus, handleDoctor, handleSetDefault, handlePauseAccount, handleResumeAccount, } from './quota-subcommand'; import { handleCreate, handleRemove, handleEdit } from './variant-subcommand'; import { handleProxyStatus, handleStart, handleStop, handleRestart, } from './proxy-lifecycle-subcommand'; import { showStatus, handleInstallVersion, handleInstallLatest } from './install-subcommand'; import { showHelp } from './help-subcommand'; import { handleCatalogStatus, handleCatalogRefresh, handleCatalogReset, } from './catalog-subcommand'; /** * Parse --backend flag from args * Returns the backend value and remaining args without --backend flag */ function parseBackendArg(args: string[]): { backend: CLIProxyBackend | undefined; remainingArgs: string[]; } { const backendIdx = args.indexOf('--backend'); if (backendIdx === -1) { // Also check for --backend=value format const backendEqualsIdx = args.findIndex((a) => a.startsWith('--backend=')); if (backendEqualsIdx !== -1) { const value = args[backendEqualsIdx].split('=')[1] as CLIProxyBackend; if (value !== 'original' && value !== 'plus') { console.warn(`Invalid backend '${value}'. Valid options: original, plus`); return { backend: undefined, remainingArgs: args }; } const remainingArgs = [...args]; remainingArgs.splice(backendEqualsIdx, 1); return { backend: value, remainingArgs }; } return { backend: undefined, remainingArgs: args }; } const value = args[backendIdx + 1]; if (value !== 'original' && value !== 'plus') { console.warn(`Invalid backend '${value}'. Valid options: original, plus`); return { backend: undefined, remainingArgs: args }; } const remainingArgs = [...args]; remainingArgs.splice(backendIdx, 2); return { backend: value, remainingArgs }; } /** * Get effective backend (CLI flag > config.yaml > default) */ function getEffectiveBackend(cliBackend?: CLIProxyBackend): CLIProxyBackend { if (cliBackend) return cliBackend; const config = loadOrCreateUnifiedConfig(); return config.cliproxy?.backend ?? DEFAULT_BACKEND; } /** * Parse --provider flag from args for quota command * Returns the provider filter value and remaining args * Accepts: agy, codex, claude, anthropic, gemini, gemini-cli, ghcp, github-copilot, all */ function parseProviderArg(args: string[]): { provider: 'agy' | 'codex' | 'claude' | 'gemini' | 'ghcp' | 'all'; remainingArgs: string[]; } { const providerIdx = args.indexOf('--provider'); if (providerIdx === -1) { // Also check for --provider=value format const providerEqualsIdx = args.findIndex((a) => a.startsWith('--provider=')); if (providerEqualsIdx !== -1) { const value = args[providerEqualsIdx].split('=')[1]?.toLowerCase() || ''; const remainingArgs = [...args]; remainingArgs.splice(providerEqualsIdx, 1); // Handle empty value if (!value) { console.error( 'Warning: --provider requires a value. Valid options: agy, codex, claude, anthropic, gemini, gemini-cli, ghcp, github-copilot, all' ); return { provider: 'all', remainingArgs }; } // Normalize aliases const normalized = value === 'gemini-cli' ? 'gemini' : value === 'github-copilot' ? 'ghcp' : value === 'anthropic' ? 'claude' : value; if ( normalized !== 'agy' && normalized !== 'codex' && normalized !== 'claude' && normalized !== 'gemini' && normalized !== 'ghcp' && normalized !== 'all' ) { console.error( `Invalid provider '${value}'. Valid options: agy, codex, claude, anthropic, gemini, gemini-cli, ghcp, github-copilot, all` ); return { provider: 'all', remainingArgs }; } return { provider: normalized as 'agy' | 'codex' | 'claude' | 'gemini' | 'ghcp' | 'all', remainingArgs, }; } return { provider: 'all', remainingArgs: args }; } const rawValue = args[providerIdx + 1]; // Warn if no value or value looks like another flag if (!rawValue || rawValue.startsWith('-')) { console.error( 'Warning: --provider requires a value. Valid options: agy, codex, claude, anthropic, gemini, gemini-cli, ghcp, github-copilot, all' ); } const value = rawValue?.toLowerCase() || 'all'; const remainingArgs = [...args]; remainingArgs.splice(providerIdx, 2); // Normalize aliases const normalized = value === 'gemini-cli' ? 'gemini' : value === 'github-copilot' ? 'ghcp' : value === 'anthropic' ? 'claude' : value; if ( normalized !== 'agy' && normalized !== 'codex' && normalized !== 'claude' && normalized !== 'gemini' && normalized !== 'ghcp' && normalized !== 'all' ) { console.error( `Invalid provider '${value}'. Valid options: agy, codex, claude, anthropic, gemini, gemini-cli, ghcp, github-copilot, all` ); return { provider: 'all', remainingArgs }; } return { provider: normalized as 'agy' | 'codex' | 'claude' | 'gemini' | 'ghcp' | 'all', remainingArgs, }; } /** * Main router for cliproxy commands */ export async function handleCliproxyCommand(args: string[]): Promise { // Parse --backend flag first (before other processing) const { backend: cliBackend, remainingArgs } = parseBackendArg(args); const effectiveBackend = getEffectiveBackend(cliBackend); const verbose = remainingArgs.includes('--verbose') || remainingArgs.includes('-v'); const command = remainingArgs[0]; if (remainingArgs.includes('--help') || remainingArgs.includes('-h')) { await showHelp(); return; } // Profile commands if (command === 'create') { await handleCreate(remainingArgs.slice(1), effectiveBackend); return; } if (command === 'edit') { await handleEdit(remainingArgs.slice(1), effectiveBackend); return; } if (command === 'list' || command === 'ls') { await handleList(); return; } if (command === 'remove' || command === 'delete' || command === 'rm') { await handleRemove(remainingArgs.slice(1)); return; } // Catalog commands if (command === 'catalog') { const subcommand = remainingArgs[1]; if (subcommand === 'refresh') { await handleCatalogRefresh(verbose); return; } if (subcommand === 'reset') { await handleCatalogReset(); return; } await handleCatalogStatus(verbose); return; } // Sync command if (command === 'sync') { await handleSync(remainingArgs.slice(1)); return; } // Proxy lifecycle commands if (command === 'start') { await handleStart(verbose); return; } if (command === 'stop') { await handleStop(); return; } if (command === 'restart') { await handleRestart(verbose); return; } if (command === 'status') { await handleProxyStatus(); return; } // Diagnostics if (command === 'doctor' || command === 'diag') { await handleDoctor(verbose); return; } // Quota management commands if (command === 'default') { await handleSetDefault(remainingArgs.slice(1)); return; } if (command === 'pause') { await handlePauseAccount(remainingArgs.slice(1)); return; } if (command === 'resume') { await handleResumeAccount(remainingArgs.slice(1)); return; } if (command === 'quota') { const { provider: providerFilter } = parseProviderArg(remainingArgs.slice(1)); await handleQuotaStatus(verbose, providerFilter); return; } // Binary installation commands const installIdx = remainingArgs.indexOf('--install'); if (installIdx !== -1) { let version = remainingArgs[installIdx + 1]; if (!version || version.startsWith('-')) { console.error('Missing version argument for --install'); console.error(' Usage: ccs cliproxy --install '); console.error(' Example: ccs cliproxy --install 6.6.80-0'); process.exit(1); } // Strip leading 'v' prefix and whitespace (user may type " v6.6.80-0 ") version = version.trim().replace(/^v/, ''); await handleInstallVersion(version, verbose, effectiveBackend); return; } if (remainingArgs.includes('--latest') || remainingArgs.includes('--update')) { await handleInstallLatest(verbose, effectiveBackend); return; } // Default: show status await showStatus(verbose, effectiveBackend); }