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
This commit is contained in:
kaitranntt
2026-01-28 11:57:22 -05:00
parent 9de2682062
commit cb6c21216d
3 changed files with 368 additions and 0 deletions
+226
View File
@@ -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 <profile> <from> <to>
return {
subcommand: 'add',
profile: args[1],
from: args[2],
to: args[3],
};
}
if (rawCommand === 'remove' || rawCommand === 'rm' || rawCommand === 'delete') {
// ccs cliproxy alias remove <profile> <from>
return {
subcommand: 'remove',
profile: args[1],
from: args[2],
};
}
return { subcommand: null };
}
/**
* Show alias command help.
*/
async function showAliasHelp(): Promise<void> {
await initUI();
console.log('');
console.log(header('Model Alias Management'));
console.log('');
console.log(subheader('Usage:'));
console.log(` ${color('ccs cliproxy alias', 'command')} <command> [args]`);
console.log('');
console.log(subheader('Commands:'));
const commands: [string, string][] = [
['list [profile]', 'List all aliases (or for specific profile)'],
['add <profile> <from> <to>', 'Add model alias mapping'],
['remove <profile> <from>', '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<void> {
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 <profile> <from> <to>', '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<void> {
await initUI();
if (!profile || !from || !to) {
console.log(fail('Missing required arguments'));
console.log('');
console.log('Usage:');
console.log(` ${color('ccs cliproxy alias add <profile> <from> <to>', '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<void> {
await initUI();
if (!profile || !from) {
console.log(fail('Missing required arguments'));
console.log('');
console.log('Usage:');
console.log(` ${color('ccs cliproxy alias remove <profile> <from>', '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<void> {
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;
}
}
+25
View File
@@ -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<void> {
['remove <name>', '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 <profile> <from> <to>', 'Add model alias'],
['alias remove <profile> <from>', 'Remove model alias'],
],
],
[
'Quota Management:',
[
@@ -1004,6 +1019,16 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
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;
+117
View File
@@ -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<void> {
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('');
}