mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
Reference in New Issue
Block a user