From cb6c21216dcaf0536530f110af75dc20ee7c7738 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 26 Jan 2026 02:03:18 -0500 Subject: [PATCH] feat(cliproxy): add sync and alias CLI commands - add `ccs cliproxy sync` command for manual sync - add `ccs cliproxy alias` command for model alias management - support --dry-run and --verbose flags - integrate with cliproxy subcommand router --- src/commands/cliproxy-alias-handler.ts | 226 +++++++++++++++++++++++++ src/commands/cliproxy-command.ts | 25 +++ src/commands/cliproxy-sync-handler.ts | 117 +++++++++++++ 3 files changed, 368 insertions(+) create mode 100644 src/commands/cliproxy-alias-handler.ts create mode 100644 src/commands/cliproxy-sync-handler.ts diff --git a/src/commands/cliproxy-alias-handler.ts b/src/commands/cliproxy-alias-handler.ts new file mode 100644 index 00000000..b69a0f7c --- /dev/null +++ b/src/commands/cliproxy-alias-handler.ts @@ -0,0 +1,226 @@ +/** + * CLIProxy Alias Command Handler + * + * Handles `ccs cliproxy alias` commands for managing model alias mappings. + */ + +import { addProfileAlias, removeProfileAlias, listAllAliases } from '../cliproxy/sync'; +import { initUI, header, subheader, color, dim, ok, fail, warn, info, table } from '../utils/ui'; + +interface AliasArgs { + subcommand: 'add' | 'remove' | 'list' | 'help' | null; + profile?: string; + from?: string; + to?: string; +} + +/** + * Parse alias command arguments. + */ +export function parseAliasArgs(args: string[]): AliasArgs { + const rawCommand = args[0]; + + if (!rawCommand || rawCommand === 'help' || args.includes('--help')) { + return { subcommand: 'help' }; + } + + if (rawCommand === 'list' || rawCommand === 'ls') { + return { subcommand: 'list', profile: args[1] }; + } + + if (rawCommand === 'add') { + // ccs cliproxy alias add + return { + subcommand: 'add', + profile: args[1], + from: args[2], + to: args[3], + }; + } + + if (rawCommand === 'remove' || rawCommand === 'rm' || rawCommand === 'delete') { + // ccs cliproxy alias remove + return { + subcommand: 'remove', + profile: args[1], + from: args[2], + }; + } + + return { subcommand: null }; +} + +/** + * Show alias command help. + */ +async function showAliasHelp(): Promise { + await initUI(); + console.log(''); + console.log(header('Model Alias Management')); + console.log(''); + console.log(subheader('Usage:')); + console.log(` ${color('ccs cliproxy alias', 'command')} [args]`); + console.log(''); + + console.log(subheader('Commands:')); + const commands: [string, string][] = [ + ['list [profile]', 'List all aliases (or for specific profile)'], + ['add ', 'Add model alias mapping'], + ['remove ', 'Remove model alias mapping'], + ]; + + const maxLen = Math.max(...commands.map(([cmd]) => cmd.length)); + for (const [cmd, desc] of commands) { + console.log(` ${color(cmd.padEnd(maxLen + 2), 'command')} ${desc}`); + } + + console.log(''); + console.log(subheader('Examples:')); + console.log(` ${dim('# List all aliases')}`); + console.log(` ${color('ccs cliproxy alias list', 'command')}`); + console.log(''); + console.log(` ${dim('# Add alias for GLM profile')}`); + console.log( + ` ${color('ccs cliproxy alias add glm claude-sonnet-4-20250514 glm-4.7-thinking', 'command')}` + ); + console.log(''); + console.log(` ${dim('# Remove alias')}`); + console.log(` ${color('ccs cliproxy alias remove glm claude-sonnet-4-20250514', 'command')}`); + console.log(''); +} + +/** + * Handle `ccs cliproxy alias list` command. + */ +async function handleAliasList(profile?: string): Promise { + await initUI(); + const allAliases = listAllAliases(); + + if (Object.keys(allAliases).length === 0) { + console.log(info('No model aliases configured')); + console.log(''); + console.log('Add aliases with:'); + console.log(` ${color('ccs cliproxy alias add ', 'command')}`); + console.log(''); + return; + } + + // Filter to specific profile if provided + const profiles = profile ? { [profile]: allAliases[profile] } : allAliases; + + if (profile && !allAliases[profile]) { + console.log(warn(`No aliases found for profile: ${profile}`)); + console.log(''); + return; + } + + console.log(header('Model Aliases')); + console.log(''); + + for (const [profileName, aliases] of Object.entries(profiles)) { + if (!aliases || aliases.length === 0) continue; + + console.log(subheader(profileName)); + + const rows = aliases.map((a) => [a.from, color('->', 'info'), a.to]); + console.log( + table(rows, { head: ['Claude Model', '', 'Target Model'], colWidths: [35, 4, 30] }) + ); + console.log(''); + } +} + +/** + * Handle `ccs cliproxy alias add` command. + */ +async function handleAliasAdd(profile: string, from: string, to: string): Promise { + await initUI(); + + if (!profile || !from || !to) { + console.log(fail('Missing required arguments')); + console.log(''); + console.log('Usage:'); + console.log(` ${color('ccs cliproxy alias add ', 'command')}`); + console.log(''); + console.log('Example:'); + console.log( + ` ${color('ccs cliproxy alias add glm claude-sonnet-4-20250514 glm-4.7-thinking', 'command')}` + ); + console.log(''); + process.exit(1); + } + + addProfileAlias(profile, from, to); + + console.log(ok(`Added alias for ${profile}`)); + console.log(` ${from} -> ${to}`); + console.log(''); + console.log(info('Run sync to apply changes:')); + console.log(` ${color('ccs cliproxy sync', 'command')}`); + console.log(''); +} + +/** + * Handle `ccs cliproxy alias remove` command. + */ +async function handleAliasRemove(profile: string, from: string): Promise { + await initUI(); + + if (!profile || !from) { + console.log(fail('Missing required arguments')); + console.log(''); + console.log('Usage:'); + console.log(` ${color('ccs cliproxy alias remove ', 'command')}`); + console.log(''); + process.exit(1); + } + + const removed = removeProfileAlias(profile, from); + + if (!removed) { + console.log(warn(`Alias not found: ${profile}/${from}`)); + console.log(''); + return; + } + + console.log(ok(`Removed alias from ${profile}`)); + console.log(` ${from}`); + console.log(''); + console.log(info('Run sync to apply changes:')); + console.log(` ${color('ccs cliproxy sync', 'command')}`); + console.log(''); +} + +/** + * Handle `ccs cliproxy alias` command router. + */ +export async function handleAlias(args: string[]): Promise { + const parsed = parseAliasArgs(args); + + switch (parsed.subcommand) { + case 'list': + await handleAliasList(parsed.profile); + break; + + case 'add': + if (parsed.profile && parsed.from && parsed.to) { + await handleAliasAdd(parsed.profile, parsed.from, parsed.to); + } else { + await handleAliasAdd('', '', ''); // Will show error message + } + break; + + case 'remove': + if (parsed.profile && parsed.from) { + await handleAliasRemove(parsed.profile, parsed.from); + } else { + await handleAliasRemove('', ''); // Will show error message + } + break; + + case 'help': + default: + await showAliasHelp(); + break; + } +} diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index ebb20861..fa21091f 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -64,6 +64,10 @@ import { installLatest, } from '../cliproxy/services'; +// Import sync and alias handlers +import { handleSync } from './cliproxy-sync-handler'; +import { handleAlias } from './cliproxy-alias-handler'; + // ============================================================================ // ARGUMENT PARSING // ============================================================================ @@ -613,6 +617,17 @@ async function showHelp(): Promise { ['remove ', 'Remove a CLIProxy variant profile'], ], ], + [ + 'Remote Sync:', + [ + ['sync', 'Sync API profiles to remote CLIProxy'], + ['sync --dry-run', 'Preview sync without applying'], + ['sync --force', 'Sync without confirmation prompt'], + ['alias list', 'List model alias mappings'], + ['alias add ', 'Add model alias'], + ['alias remove ', 'Remove model alias'], + ], + ], [ 'Quota Management:', [ @@ -1004,6 +1019,16 @@ export async function handleCliproxyCommand(args: string[]): Promise { return; } + if (command === 'sync') { + await handleSync(remainingArgs.slice(1)); + return; + } + + if (command === 'alias') { + await handleAlias(remainingArgs.slice(1)); + return; + } + if (command === 'stop') { await handleStop(); return; diff --git a/src/commands/cliproxy-sync-handler.ts b/src/commands/cliproxy-sync-handler.ts new file mode 100644 index 00000000..5d000049 --- /dev/null +++ b/src/commands/cliproxy-sync-handler.ts @@ -0,0 +1,117 @@ +/** + * CLIProxy Sync Command Handler + * + * Handles `ccs cliproxy sync` command for syncing API profiles to local CLIProxy config. + */ + +import { syncToLocalConfig, generateSyncPreview, getLocalSyncStatus } from '../cliproxy/sync'; +import { initUI, header, subheader, color, dim, ok, fail, warn, info, table } from '../utils/ui'; + +interface SyncArgs { + dryRun: boolean; + verbose: boolean; +} + +/** + * Parse sync command arguments. + */ +export function parseSyncArgs(args: string[]): SyncArgs { + return { + dryRun: args.includes('--dry-run') || args.includes('-n'), + verbose: args.includes('--verbose') || args.includes('-v'), + }; +} + +/** + * Handle `ccs cliproxy sync` command. + */ +export async function handleSync(args: string[]): Promise { + await initUI(); + const parsed = parseSyncArgs(args); + + console.log(header('CLIProxy Profile Sync')); + console.log(''); + + // Check status + const status = getLocalSyncStatus(); + + if (!status.configExists) { + console.log(fail('CLIProxy config not found')); + console.log(''); + console.log('Run to generate config:'); + console.log(` ${color('ccs doctor --fix', 'command')}`); + console.log(''); + process.exit(1); + } + + // Get preview + const preview = generateSyncPreview(); + + if (preview.length === 0) { + console.log(warn('No API profiles configured to sync')); + console.log(''); + console.log('Configure API profiles with:'); + console.log(` ${color('ccs api create', 'command')}`); + console.log(''); + return; + } + + // Show preview + console.log(subheader(`Profiles to Sync (${preview.length})`)); + console.log(''); + + const rows = preview.map((p) => { + const aliases = p.hasAliases ? color(`${p.aliasCount} aliases`, 'info') : dim('none'); + const url = p.baseUrl ? dim(p.baseUrl) : dim('-'); + return [p.name, url, aliases]; + }); + + console.log(table(rows, { head: ['Profile', 'Base URL', 'Aliases'], colWidths: [15, 40, 15] })); + console.log(''); + + if (parsed.verbose) { + console.log(dim(`Config path: ${status.configPath}`)); + console.log(dim(`Current keys: ${status.currentKeyCount}`)); + console.log(''); + } + + // Dry-run mode + if (parsed.dryRun) { + console.log(info('Dry-run mode - no changes will be made')); + console.log(''); + console.log(`Would sync ${preview.length} profile(s) to:`); + console.log(` ${dim(status.configPath)}`); + console.log(''); + console.log('Run without --dry-run to apply changes:'); + console.log(` ${color('ccs cliproxy sync', 'command')}`); + console.log(''); + return; + } + + // Execute sync + console.log(info('Syncing profiles to local config...')); + + const result = syncToLocalConfig(); + + if (!result.success) { + console.log(''); + console.log(fail(`Sync failed: ${result.error}`)); + console.log(''); + process.exit(1); + } + + console.log(''); + console.log(ok(`Synced ${result.syncedCount} profile(s)`)); + console.log(` ${dim(result.configPath)}`); + console.log(''); + + // Show synced profiles + for (const p of preview) { + console.log(` ${ok('')} ${p.name}`); + } + console.log(''); + + console.log(info('Restart CLIProxy to apply changes:')); + console.log(` ${color('ccs cliproxy stop && ccs gemini', 'command')}`); + console.log(''); +}