From 944f5c0fb07bcf293a164b81886595aeb8217703 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 3 Jan 2026 16:34:49 -0500 Subject: [PATCH] feat(cliproxy): add doctor subcommand for quota diagnostics Add `ccs cliproxy doctor` command that: - Fetches quota for all Antigravity accounts in parallel - Shows per-account quota status with visual bars - Detects shared GCP projects (critical failover limitation) - Warns when accounts share same quota pool Part of #252 antigravity failover enhancement. --- src/commands/cliproxy-command.ts | 113 +++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts index e0e93e35..16fc1b18 100644 --- a/src/commands/cliproxy-command.ts +++ b/src/commands/cliproxy-command.ts @@ -21,6 +21,7 @@ import * as path from 'path'; import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler'; import { getProviderAccounts } from '../cliproxy/account-manager'; +import { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher'; import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector'; import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector'; import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog'; @@ -548,6 +549,7 @@ async function showHelp(): Promise { [ ['status', 'Show running CLIProxy status'], ['stop', 'Stop running CLIProxy instance'], + ['doctor', 'Quota diagnostics and shared project detection'], ], ], [ @@ -579,6 +581,112 @@ async function showHelp(): Promise { console.log(''); } +// ============================================================================ +// DOCTOR COMMAND - Quota diagnostics and shared project detection +// ============================================================================ + +async function handleDoctor(): Promise { + await initUI(); + console.log(header('CLIProxy Quota Diagnostics')); + console.log(''); + + // Check each OAuth provider (agy is the only one with quota) + 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(subheader(`Antigravity Accounts (${accounts.length})`)); + console.log(''); + + // Fetch quota for all accounts + console.log(dim('Fetching quotas...')); + const quotaResult = await fetchAllProviderQuotas(provider); + + // Display per-account quota status + for (const { account, quota } of quotaResult.accounts) { + const accountLabel = account.email || account.id; + const defaultBadge = account.isDefault ? color(' (default)', 'info') : ''; + + if (!quota.success) { + console.log(` ${fail(accountLabel)}${defaultBadge}`); + console.log(` ${color(quota.error || 'Failed to fetch quota', 'error')}`); + if (quota.isUnprovisioned) { + console.log( + ` ${warn('Account not provisioned - open Gemini Code Assist in IDE first')}` + ); + } + console.log(''); + continue; + } + + // Calculate overall quota health + const avgQuota = quota.models.reduce((sum, m) => sum + m.percentage, 0) / quota.models.length; + const statusIcon = avgQuota > 50 ? ok('') : avgQuota > 10 ? warn('') : fail(''); + + console.log(` ${statusIcon}${accountLabel}${defaultBadge}`); + if (quota.projectId) { + console.log(` Project: ${dim(quota.projectId)}`); + } + + // Show model quotas + for (const model of quota.models) { + const bar = formatQuotaBar(model.percentage); + console.log(` ${model.name.padEnd(20)} ${bar} ${model.percentage.toFixed(0)}%`); + } + console.log(''); + } + + // Check for shared GCP projects (critical warning) + const sharedProjects = Object.entries(quotaResult.projectGroups).filter( + ([, accountIds]) => accountIds.length > 1 + ); + + if (sharedProjects.length > 0) { + console.log(''); + console.log(subheader('Shared Project Warning')); + console.log(''); + for (const [projectId, accountIds] of sharedProjects) { + console.log( + fail(`Project ${projectId.substring(0, 20)}... shared by ${accountIds.length} accounts:`) + ); + for (const accountId of accountIds) { + console.log(` - ${accountId}`); + } + console.log(''); + console.log(warn('These accounts share the same quota pool!')); + console.log(warn('Failover between them will NOT help when quota is exhausted.')); + console.log(info('Solution: Use accounts from different GCP projects.')); + } + } + + // Summary + console.log(''); + console.log(subheader('Summary')); + const healthyAccounts = quotaResult.accounts.filter( + ({ quota }) => quota.success && quota.models.some((m) => m.percentage > 5) + ); + console.log(` Accounts with quota: ${healthyAccounts.length}/${accounts.length}`); + if (sharedProjects.length > 0) { + console.log(` ${fail(`Shared projects: ${sharedProjects.length} (failover limited)`)}`); + } else if (accounts.length > 1) { + console.log(` ${ok('No shared projects (failover fully operational)')}`); + } + console.log(''); +} + +function formatQuotaBar(percentage: number): string { + const width = 20; + const filled = Math.round((percentage / 100) * width); + const empty = width - filled; + const filledChar = percentage > 50 ? '█' : percentage > 10 ? '▓' : '░'; + return `[${filledChar.repeat(filled)}${' '.repeat(empty)}]`; +} + // ============================================================================ // MAIN ROUTER // ============================================================================ @@ -617,6 +725,11 @@ export async function handleCliproxyCommand(args: string[]): Promise { return; } + if (command === 'doctor' || command === 'diag') { + await handleDoctor(); + return; + } + const installIdx = args.indexOf('--install'); if (installIdx !== -1) { const version = args[installIdx + 1];