From cfd8dd974e875b858f78bb73e74e44062b72d38e Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 6 Jan 2026 12:17:48 -0500 Subject: [PATCH] feat(cli): add pause, resume, status subcommands - ccs cliproxy pause - ccs cliproxy resume - ccs cliproxy status [provider] - show quota/tier info Refs #282 --- src/commands/cliproxy-command.ts | 235 ++++++++++++++++++++++++++++++- 1 file changed, 234 insertions(+), 1 deletion(-) diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index 080d271b..c90408a6 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -20,8 +20,15 @@ import * as path from 'path'; import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler'; -import { getProviderAccounts } from '../cliproxy/account-manager'; +import { + getProviderAccounts, + setDefaultAccount, + pauseAccount, + resumeAccount, + findAccountByQuery, +} from '../cliproxy/account-manager'; import { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher'; +import { isOnCooldown } from '../cliproxy/quota-manager'; import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector'; import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector'; import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog'; @@ -544,6 +551,15 @@ async function showHelp(): Promise { ['remove ', 'Remove a CLIProxy variant profile'], ], ], + [ + 'Quota Management:', + [ + ['default ', 'Set default account for rotation'], + ['pause ', 'Pause account (skip in rotation)'], + ['resume ', 'Resume paused account'], + ['quota', 'Show quota status for all accounts'], + ], + ], [ 'Proxy Lifecycle:', [ @@ -691,6 +707,202 @@ function formatQuotaBar(percentage: number): string { return `[${filledChar.repeat(filled)}${' '.repeat(empty)}]`; } +// ============================================================================ +// QUOTA MANAGEMENT COMMANDS +// ============================================================================ + +async function handleSetDefault(args: string[]): Promise { + await initUI(); + const parsed = parseProfileArgs(args); + + if (!parsed.name) { + console.log(fail('Usage: ccs cliproxy default [--provider ]')); + console.log(''); + console.log('Examples:'); + console.log(' ccs cliproxy default ultra@gmail.com'); + console.log(' ccs cliproxy default john --provider agy'); + process.exit(1); + } + + const provider = (parsed.provider || 'agy') as CLIProxyProvider; + const account = findAccountByQuery(provider, parsed.name); + + if (!account) { + console.log(fail(`Account not found: ${parsed.name}`)); + console.log(''); + const accounts = getProviderAccounts(provider); + if (accounts.length > 0) { + console.log('Available accounts:'); + for (const acc of accounts) { + const badge = acc.isDefault ? color(' (current default)', 'info') : ''; + console.log(` - ${acc.email || acc.id}${badge}`); + } + } else { + console.log(`No accounts found for provider: ${provider}`); + console.log(`Run: ccs ${provider} --auth`); + } + process.exit(1); + } + + const success = setDefaultAccount(provider, account.id); + + if (success) { + console.log(ok(`Default account set to: ${account.email || account.id}`)); + console.log(info(`Provider: ${provider}`)); + } else { + console.log(fail('Failed to set default account')); + process.exit(1); + } +} + +async function handlePauseAccount(args: string[]): Promise { + await initUI(); + const parsed = parseProfileArgs(args); + + if (!parsed.name) { + console.log(fail('Usage: ccs cliproxy pause [--provider ]')); + console.log(''); + console.log('Pauses an account so it will be skipped in quota rotation.'); + process.exit(1); + } + + const provider = (parsed.provider || 'agy') as CLIProxyProvider; + const account = findAccountByQuery(provider, parsed.name); + + if (!account) { + console.log(fail(`Account not found: ${parsed.name}`)); + process.exit(1); + } + + if (account.paused) { + console.log(warn(`Account already paused: ${account.email || account.id}`)); + console.log(info(`Paused at: ${account.pausedAt || 'unknown'}`)); + return; + } + + const success = pauseAccount(provider, account.id); + + if (success) { + console.log(ok(`Account paused: ${account.email || account.id}`)); + console.log(info('Account will be skipped in quota rotation')); + } else { + console.log(fail('Failed to pause account')); + process.exit(1); + } +} + +async function handleResumeAccount(args: string[]): Promise { + await initUI(); + const parsed = parseProfileArgs(args); + + if (!parsed.name) { + console.log(fail('Usage: ccs cliproxy resume [--provider ]')); + console.log(''); + console.log('Resumes a paused account for quota rotation.'); + process.exit(1); + } + + const provider = (parsed.provider || 'agy') as CLIProxyProvider; + const account = findAccountByQuery(provider, parsed.name); + + if (!account) { + console.log(fail(`Account not found: ${parsed.name}`)); + process.exit(1); + } + + if (!account.paused) { + console.log(warn(`Account is not paused: ${account.email || account.id}`)); + return; + } + + const success = resumeAccount(provider, account.id); + + if (success) { + console.log(ok(`Account resumed: ${account.email || account.id}`)); + console.log(info('Account is now active in quota rotation')); + } else { + console.log(fail('Failed to resume account')); + process.exit(1); + } +} + +async function handleQuotaStatus(): Promise { + await initUI(); + console.log(header('Quota Status')); + console.log(''); + + const provider: CLIProxyProvider = 'agy'; + const accounts = getProviderAccounts(provider); + + if (accounts.length === 0) { + console.log(info('No Antigravity accounts configured')); + console.log(` Run: ${color('ccs agy --auth', 'command')} to authenticate`); + return; + } + + console.log(dim('Fetching quotas...')); + const quotaResult = await fetchAllProviderQuotas(provider); + + // Build table rows + const rows: string[][] = []; + for (const account of accounts) { + const quotaData = quotaResult.accounts.find((q) => q.account.id === account.id); + const quota = quotaData?.quota; + + // Calculate average quota + let avgQuota = 'N/A'; + if (quota?.success && quota.models.length > 0) { + const avg = Math.round( + quota.models.reduce((sum, m) => sum + m.percentage, 0) / quota.models.length + ); + avgQuota = `${avg}%`; + } + + // Build status badges + const statusParts: string[] = []; + if (account.paused) statusParts.push(color('PAUSED', 'warning')); + if (isOnCooldown(provider, account.id)) statusParts.push(color('COOLDOWN', 'warning')); + + const defaultMark = account.isDefault ? color('*', 'success') : ' '; + const tier = account.tier || 'unknown'; + const status = statusParts.join(', '); + + rows.push([ + defaultMark, + account.nickname || account.email || account.id, + tier, + avgQuota, + status, + ]); + } + + console.log(''); + console.log( + table(rows, { + head: ['', 'Account', 'Tier', 'Quota', 'Status'], + colWidths: [3, 30, 10, 10, 20], + }) + ); + console.log(''); + console.log(info(`Default account marked with ${color('*', 'success')}`)); + console.log(''); + + // Show summary of paused/cooldown accounts + const pausedCount = accounts.filter((a) => a.paused).length; + const cooldownCount = accounts.filter((a) => isOnCooldown(provider, a.id)).length; + if (pausedCount > 0) { + console.log( + warn(`${pausedCount} account(s) paused - use 'ccs cliproxy resume ' to re-enable`) + ); + } + if (cooldownCount > 0) { + console.log(info(`${cooldownCount} account(s) on cooldown (exhausted recently)`)); + } + if (pausedCount > 0 || cooldownCount > 0) { + console.log(''); + } +} + // ============================================================================ // MAIN ROUTER // ============================================================================ @@ -734,6 +946,27 @@ export async function handleCliproxyCommand(args: string[]): Promise { return; } + // Quota management commands + if (command === 'default') { + await handleSetDefault(args.slice(1)); + return; + } + + if (command === 'pause') { + await handlePauseAccount(args.slice(1)); + return; + } + + if (command === 'resume') { + await handleResumeAccount(args.slice(1)); + return; + } + + if (command === 'quota') { + await handleQuotaStatus(); + return; + } + const installIdx = args.indexOf('--install'); if (installIdx !== -1) { let version = args[installIdx + 1];