mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-22 22:23:37 +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 * as path from 'path';
|
||||||
import { getAllAuthStatus, getOAuthConfig, triggerOAuth } from '../cliproxy/auth-handler';
|
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 { fetchAllProviderQuotas } from '../cliproxy/quota-fetcher';
|
||||||
|
import { isOnCooldown } from '../cliproxy/quota-manager';
|
||||||
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
import { CLIPROXY_FALLBACK_VERSION } from '../cliproxy/platform-detector';
|
||||||
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
import { CLIPROXY_PROFILES, CLIProxyProfileName } from '../auth/profile-detector';
|
||||||
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog';
|
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../cliproxy/model-catalog';
|
||||||
@@ -544,6 +551,15 @@ async function showHelp(): Promise<void> {
|
|||||||
['remove <name>', 'Remove a CLIProxy variant profile'],
|
['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:',
|
'Proxy Lifecycle:',
|
||||||
[
|
[
|
||||||
@@ -691,6 +707,202 @@ function formatQuotaBar(percentage: number): string {
|
|||||||
return `[${filledChar.repeat(filled)}${' '.repeat(empty)}]`;
|
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
|
// MAIN ROUTER
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -734,6 +946,27 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
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');
|
const installIdx = args.indexOf('--install');
|
||||||
if (installIdx !== -1) {
|
if (installIdx !== -1) {
|
||||||
let version = args[installIdx + 1];
|
let version = args[installIdx + 1];
|
||||||
|
|||||||
Reference in New Issue
Block a user