mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
feat(cli): add pause, resume, status subcommands
- ccs cliproxy pause <provider> <account> - ccs cliproxy resume <provider> <account> - ccs cliproxy status [provider] - show quota/tier info Refs #282
This commit is contained in:
@@ -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<void> {
|
||||
['remove <name>', 'Remove a CLIProxy variant profile'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Quota Management:',
|
||||
[
|
||||
['default <account>', 'Set default account for rotation'],
|
||||
['pause <account>', 'Pause account (skip in rotation)'],
|
||||
['resume <account>', '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<void> {
|
||||
await initUI();
|
||||
const parsed = parseProfileArgs(args);
|
||||
|
||||
if (!parsed.name) {
|
||||
console.log(fail('Usage: ccs cliproxy default <account> [--provider <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<void> {
|
||||
await initUI();
|
||||
const parsed = parseProfileArgs(args);
|
||||
|
||||
if (!parsed.name) {
|
||||
console.log(fail('Usage: ccs cliproxy pause <account> [--provider <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<void> {
|
||||
await initUI();
|
||||
const parsed = parseProfileArgs(args);
|
||||
|
||||
if (!parsed.name) {
|
||||
console.log(fail('Usage: ccs cliproxy resume <account> [--provider <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<void> {
|
||||
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 <account>' 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<void> {
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user