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.
This commit is contained in:
kaitranntt
2026-01-03 16:34:49 -05:00
parent 24847f5804
commit 944f5c0fb0
+113
View File
@@ -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<void> {
[
['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<void> {
console.log('');
}
// ============================================================================
// DOCTOR COMMAND - Quota diagnostics and shared project detection
// ============================================================================
async function handleDoctor(): Promise<void> {
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<void> {
return;
}
if (command === 'doctor' || command === 'diag') {
await handleDoctor();
return;
}
const installIdx = args.indexOf('--install');
if (installIdx !== -1) {
const version = args[installIdx + 1];