mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 08:17:22 +00:00
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:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user