From 0c6491c9d27a3bfb1ecc8c1627d1e1a70f59220a Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 25 Dec 2025 14:36:11 -0500 Subject: [PATCH] feat(cli): add tokens command for auth token management - Add ccs tokens command with subcommands: --show, --api-key, --secret, --regenerate-secret, --reset, --variant - Integrate tokens command into main CLI (ccs.ts) - Display masked tokens by default, --show for unmasked - Auto-regenerate CLIProxy config on token changes --- src/ccs.ts | 7 ++ src/commands/tokens-command.ts | 199 +++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 src/commands/tokens-command.ts diff --git a/src/ccs.ts b/src/ccs.ts index 0f075d6d..572d1a60 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -412,6 +412,13 @@ async function main(): Promise { return; } + // Special case: tokens command (auth token management) + if (firstArg === 'tokens') { + const { handleTokensCommand } = await import('./commands/tokens-command'); + const exitCode = await handleTokensCommand(args.slice(1)); + process.exit(exitCode); + } + // Special case: setup command (first-time wizard) if (firstArg === 'setup' || firstArg === '--setup') { const { handleSetupCommand } = await import('./commands/setup-command'); diff --git a/src/commands/tokens-command.ts b/src/commands/tokens-command.ts new file mode 100644 index 00000000..b595c249 --- /dev/null +++ b/src/commands/tokens-command.ts @@ -0,0 +1,199 @@ +/** + * Tokens Command + * + * Manage CLIProxyAPI auth tokens (API key and management secret). + * + * Usage: + * ccs tokens Show current tokens (masked) + * ccs tokens --show Show tokens unmasked + * ccs tokens --api-key Set global API key + * ccs tokens --secret Set management secret + * ccs tokens --regenerate-secret Auto-generate new management secret + * ccs tokens --reset Reset to defaults + * ccs tokens --variant --api-key Set per-variant API key + */ + +import { initUI, ok, info, fail, warn, color, dim, header, subheader } from '../utils/ui'; +import { + generateSecureToken, + maskToken, + setGlobalApiKey, + setGlobalManagementSecret, + setVariantApiKey, + resetAuthToDefaults, + getAuthSummary, +} from '../cliproxy'; +import { regenerateConfig } from '../cliproxy/config-generator'; + +/** + * Display current auth status + */ +async function showAuthStatus(showUnmasked: boolean): Promise { + await initUI(); + + const summary = getAuthSummary(); + + console.log(header('CLIProxy Auth Tokens')); + console.log(''); + + // API Key + const apiKeyDisplay = showUnmasked ? summary.apiKey.value : maskToken(summary.apiKey.value); + const apiKeyStatus = summary.apiKey.isCustom ? color('(custom)', 'warning') : dim('(default)'); + console.log(` API Key: ${apiKeyDisplay} ${apiKeyStatus}`); + + // Management Secret + const secretDisplay = showUnmasked + ? summary.managementSecret.value + : maskToken(summary.managementSecret.value); + const secretStatus = summary.managementSecret.isCustom + ? color('(custom)', 'warning') + : dim('(default)'); + console.log(` Management Secret: ${secretDisplay} ${secretStatus}`); + + console.log(''); + + if (showUnmasked) { + console.log(warn('Tokens shown in plain text. Do not share!')); + console.log(''); + } + + console.log(subheader('Usage')); + console.log(` ${dim('Set API key:')} ccs tokens --api-key `); + console.log(` ${dim('Set secret:')} ccs tokens --secret `); + console.log(` ${dim('Generate secret:')} ccs tokens --regenerate-secret`); + console.log(` ${dim('Reset to defaults:')} ccs tokens --reset`); + console.log(''); +} + +/** + * Handle tokens command + */ +export async function handleTokensCommand(args: string[]): Promise { + await initUI(); + + // Parse flags + const showFlag = args.includes('--show'); + const resetFlag = args.includes('--reset'); + const regenerateSecretFlag = args.includes('--regenerate-secret'); + const helpFlag = args.includes('--help') || args.includes('-h'); + + // Find --api-key value + const apiKeyIndex = args.indexOf('--api-key'); + const apiKeyValue = apiKeyIndex !== -1 ? args[apiKeyIndex + 1] : undefined; + + // Find --secret value + const secretIndex = args.indexOf('--secret'); + const secretValue = secretIndex !== -1 ? args[secretIndex + 1] : undefined; + + // Find --variant value + const variantIndex = args.indexOf('--variant'); + const variantValue = variantIndex !== -1 ? args[variantIndex + 1] : undefined; + + // Help + if (helpFlag) { + console.log(header('CCS Tokens Management')); + console.log(''); + console.log(subheader('Usage')); + console.log(` ${color('ccs tokens', 'command')} [options]`); + console.log(''); + console.log(subheader('Options')); + console.log(` ${color('(no args)', 'command')} Show masked tokens`); + console.log(` ${color('--show', 'command')} Show tokens unmasked`); + console.log(` ${color('--api-key ', 'command')} Set global API key`); + console.log(` ${color('--secret ', 'command')} Set management secret`); + console.log(` ${color('--regenerate-secret', 'command')} Generate new management secret`); + console.log( + ` ${color('--variant ', 'command')} Target specific variant (with --api-key)` + ); + console.log(` ${color('--reset', 'command')} Reset all to defaults`); + console.log(` ${color('--help, -h', 'command')} Show this help`); + console.log(''); + console.log(subheader('Examples')); + console.log(` ${dim('# View current tokens')}`); + console.log(` ccs tokens`); + console.log(''); + console.log(` ${dim('# Set custom API key')}`); + console.log(` ccs tokens --api-key my-custom-key-123`); + console.log(''); + console.log(` ${dim('# Generate secure management secret')}`); + console.log(` ccs tokens --regenerate-secret`); + console.log(''); + console.log(` ${dim('# Set per-variant API key')}`); + console.log(` ccs tokens --variant my-gemini --api-key variant-key-456`); + console.log(''); + return 0; + } + + // Reset to defaults + if (resetFlag) { + resetAuthToDefaults(); + // Regenerate CLIProxy config to apply changes + regenerateConfig(); + console.log(ok('Auth tokens reset to defaults')); + console.log(info('CLIProxy config regenerated')); + return 0; + } + + // Regenerate management secret + if (regenerateSecretFlag) { + const newSecret = generateSecureToken(32); + setGlobalManagementSecret(newSecret); + // Regenerate CLIProxy config to apply changes + regenerateConfig(); + console.log(ok('New management secret generated')); + console.log(` Secret: ${maskToken(newSecret)}`); + console.log(info('CLIProxy config regenerated')); + console.log(warn('Restart CLIProxy to apply: ccs cliproxy restart')); + return 0; + } + + // Set API key + if (apiKeyValue !== undefined) { + if (!apiKeyValue || apiKeyValue.startsWith('-')) { + console.error(fail('Missing value for --api-key')); + return 1; + } + + if (variantValue) { + // Per-variant API key + try { + setVariantApiKey(variantValue, apiKeyValue); + console.log(ok(`API key set for variant '${variantValue}'`)); + } catch (err) { + const error = err instanceof Error ? err.message : 'Unknown error'; + console.error(fail(error)); + return 1; + } + } else { + // Global API key + setGlobalApiKey(apiKeyValue); + console.log(ok('Global API key updated')); + } + + // Regenerate CLIProxy config to apply changes + regenerateConfig(); + console.log(info('CLIProxy config regenerated')); + console.log(warn('Restart CLIProxy to apply: ccs cliproxy restart')); + return 0; + } + + // Set management secret + if (secretValue !== undefined) { + if (!secretValue || secretValue.startsWith('-')) { + console.error(fail('Missing value for --secret')); + return 1; + } + + setGlobalManagementSecret(secretValue); + // Regenerate CLIProxy config to apply changes + regenerateConfig(); + console.log(ok('Management secret updated')); + console.log(info('CLIProxy config regenerated')); + console.log(warn('Restart CLIProxy to apply: ccs cliproxy restart')); + return 0; + } + + // Default: show status + await showAuthStatus(showFlag); + return 0; +}