feat: add quota check and account management to auth login

- Add enabled field to AccountMetadataV3 for account enable/disable
- Add getEnabledAccounts/getTotalAccountCount methods to AccountManager
- Add quota.ts with quota checking logic via fetchAvailableModels API
- Integrate 'Check quotas' and 'Manage accounts' options into auth menu
- Display quota status for Claude, Gemini 3 Pro, and Gemini 3 Flash
- Add standalone check-quota.mjs script for manual verification
- Update tsconfig to exclude temp_research folder
This commit is contained in:
Maxim Esipov
2026-01-24 13:08:03 +03:00
parent 13774f9745
commit 9a85cb272b
8 changed files with 620 additions and 32 deletions

218
scripts/check-quota.mjs Normal file
View File

@@ -0,0 +1,218 @@
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
const CLIENT_SECRET = "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";
const CLOUD_CODE_BASE = "https://cloudcode-pa.googleapis.com";
const USER_AGENT = "antigravity/windows/amd64";
const FALLBACK_PROJECT_ID = "bamboo-precept-lgxtn";
function getDefaultAccountsPath() {
if (process.platform === "win32") {
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
return join(appData, "opencode", "antigravity-accounts.json");
}
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(xdgConfig, "opencode", "antigravity-accounts.json");
}
function parseArgs() {
const args = process.argv.slice(2);
let path = getDefaultAccountsPath();
let accountIndex = null;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "--path" && args[i + 1]) {
path = args[i + 1];
i += 1;
continue;
}
if (arg === "--account" && args[i + 1]) {
const parsed = Number.parseInt(args[i + 1], 10);
if (!Number.isNaN(parsed)) {
accountIndex = parsed - 1;
}
i += 1;
}
}
return { path, accountIndex };
}
async function postJson(url, token, body, extraHeaders = {}) {
return fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
...extraHeaders,
},
body: JSON.stringify(body),
});
}
async function refreshAccessToken(refreshToken) {
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 200)}`);
}
const payload = await response.json();
return payload.access_token;
}
async function loadProjectId(accessToken) {
const body = { metadata: { ideType: "ANTIGRAVITY" } };
const response = await postJson(`${CLOUD_CODE_BASE}/v1internal:loadCodeAssist`, accessToken, body);
if (!response.ok) {
return "";
}
const payload = await response.json();
if (typeof payload.cloudaicompanionProject === "string") {
return payload.cloudaicompanionProject;
}
if (payload.cloudaicompanionProject && typeof payload.cloudaicompanionProject.id === "string") {
return payload.cloudaicompanionProject.id;
}
return "";
}
function classifyGroup(modelName) {
const lower = modelName.toLowerCase();
if (lower.includes("claude")) return "claude";
if (!lower.includes("gemini-3")) return null;
if (lower.includes("flash")) return "gemini-flash";
return "gemini-pro";
}
function updateGroup(groups, group, remainingFraction, resetTime) {
const entry = groups[group] || { count: 0 };
entry.count += 1;
if (typeof remainingFraction === "number") {
if (entry.remaining === undefined) {
entry.remaining = remainingFraction;
} else {
entry.remaining = Math.min(entry.remaining, remainingFraction);
}
}
if (resetTime) {
const timestamp = Date.parse(resetTime);
if (Number.isFinite(timestamp)) {
if (!entry.resetTime) {
entry.resetTime = resetTime;
} else {
const existing = Date.parse(entry.resetTime);
if (!Number.isFinite(existing) || timestamp < existing) {
entry.resetTime = resetTime;
}
}
}
}
groups[group] = entry;
}
function formatDuration(targetTime) {
const delta = targetTime - Date.now();
if (delta <= 0) return "now";
const totalSeconds = Math.round(delta / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function printGroup(label, entry) {
if (!entry || entry.count === 0) return;
const remaining = typeof entry.remaining === "number" ? Math.round(entry.remaining * 100) : null;
const status = remaining === null ? "UNKNOWN" : remaining <= 0 ? "LIMITED" : "OK";
const details = [];
if (remaining !== null) details.push(`remaining ${remaining}%`);
if (entry.resetTime) {
const time = formatDuration(Date.parse(entry.resetTime));
details.push(`resets in ${time}`);
}
const suffix = details.length ? ` (${details.join(", ")})` : "";
console.log(` ${label}: ${status}${suffix}`);
}
async function run() {
const { path, accountIndex } = parseArgs();
const payload = JSON.parse(readFileSync(path, "utf8"));
const accounts = payload.accounts || [];
if (accounts.length === 0) {
console.log("No accounts found.");
return;
}
const selected = accountIndex === null
? accounts.map((account, index) => ({ account, index }))
: accounts
.map((account, index) => ({ account, index }))
.filter((item) => item.index === accountIndex);
for (const { account, index } of selected) {
const label = account.email || `Account ${index + 1}`;
const disabled = account.enabled === false ? " (disabled)" : "";
console.log(`\n${index + 1}. ${label}${disabled}`);
try {
const accessToken = await refreshAccessToken(account.refreshToken);
let projectId = await loadProjectId(accessToken);
if (!projectId) {
projectId = account.managedProjectId || account.projectId || FALLBACK_PROJECT_ID;
}
console.log(` project: ${projectId}`);
const body = projectId ? { project: projectId } : {};
const response = await postJson(
`${CLOUD_CODE_BASE}/v1internal:fetchAvailableModels`,
accessToken,
body,
);
console.log(` fetchAvailableModels: ${response.status}`);
if (!response.ok) {
const text = await response.text().catch(() => "");
console.log(` error: ${text.trim().slice(0, 200)}`);
continue;
}
const data = await response.json();
const groups = {};
const models = data.models || {};
for (const [modelName, info] of Object.entries(models)) {
const group = classifyGroup(modelName);
if (!group) continue;
if (!info || !info.quotaInfo) continue;
const remaining = info.quotaInfo.remainingFraction ?? 0;
updateGroup(groups, group, remaining, info.quotaInfo.resetTime);
}
printGroup("Claude", groups["claude"]);
printGroup("Gemini 3 Pro", groups["gemini-pro"]);
printGroup("Gemini 3 Flash", groups["gemini-flash"]);
} catch (error) {
console.log(` error: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -37,6 +37,7 @@ import { AccountManager, type ModelFamily, parseRateLimitReason, calculateBackof
import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
import { loadConfig, initRuntimeConfig, type AntigravityConfig } from "./plugin/config";
import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery";
import { checkAccountsQuota } from "./plugin/quota";
import { initDiskSignatureCache } from "./plugin/cache";
import { createProactiveRefreshQueue, type ProactiveRefreshQueue } from "./plugin/refresh-queue";
import { initLogger, createLogger } from "./plugin/logger";
@@ -306,6 +307,7 @@ async function persistAccountPool(
managedProjectId: parts.managedProjectId,
addedAt: now,
lastUsed: now,
enabled: true,
});
continue;
}
@@ -1830,39 +1832,93 @@ export const createAntigravityPlugin = (providerId: string) => async (
let refreshAccountIndex: number | undefined;
const existingStorage = await loadAccounts();
if (existingStorage && existingStorage.accounts.length > 0) {
const now = Date.now();
const existingAccounts = existingStorage.accounts.map((acc, idx) => {
let status: 'active' | 'rate-limited' | 'expired' | 'unknown' = 'unknown';
const rateLimits = acc.rateLimitResetTimes;
if (rateLimits) {
const isRateLimited = Object.values(rateLimits).some(
(resetTime) => typeof resetTime === 'number' && resetTime > now
);
if (isRateLimited) {
status = 'rate-limited';
let menuResult;
while (true) {
const now = Date.now();
const existingAccounts = existingStorage.accounts.map((acc, idx) => {
let status: 'active' | 'rate-limited' | 'expired' | 'unknown' = 'unknown';
const rateLimits = acc.rateLimitResetTimes;
if (rateLimits) {
const isRateLimited = Object.values(rateLimits).some(
(resetTime) => typeof resetTime === 'number' && resetTime > now
);
if (isRateLimited) {
status = 'rate-limited';
} else {
status = 'active';
}
} else {
status = 'active';
}
} else {
status = 'active';
if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
status = 'rate-limited';
}
return {
email: acc.email,
index: idx,
addedAt: acc.addedAt,
lastUsed: acc.lastUsed,
status,
isCurrentAccount: idx === (existingStorage.activeIndex ?? 0),
enabled: acc.enabled !== false,
};
});
menuResult = await promptLoginMode(existingAccounts);
if (menuResult.mode === "check") {
console.log("\nChecking quotas for all accounts...");
const results = await checkAccountsQuota(existingStorage.accounts, client, providerId);
for (const res of results) {
const label = res.email || `Account ${res.index + 1}`;
const disabledStr = res.disabled ? " (disabled)" : "";
console.log(`\n${res.index + 1}. ${label}${disabledStr}`);
if (res.status === "error") {
console.log(` Error: ${res.error}`);
continue;
}
if (!res.quota || Object.keys(res.quota.groups).length === 0) {
console.log(" No quota information available.");
if (res.quota?.error) console.log(` Error: ${res.quota.error}`);
continue;
}
const printGrp = (name: string, group: any) => {
if (!group) return;
const remaining = typeof group.remainingFraction === 'number'
? `${Math.round(group.remainingFraction * 100)}%`
: 'UNKNOWN';
const resetStr = group.resetTime ? `, resets in ${formatWaitTime(Date.parse(group.resetTime) - Date.now())}` : '';
console.log(` ${name}: ${remaining}${resetStr}`);
};
printGrp("Claude", res.quota.groups.claude);
printGrp("Gemini 3 Pro", res.quota.groups["gemini-pro"]);
printGrp("Gemini 3 Flash", res.quota.groups["gemini-flash"]);
if (res.updatedAccount) {
existingStorage.accounts[res.index] = res.updatedAccount;
await saveAccounts(existingStorage);
}
}
console.log("");
continue;
}
if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
status = 'rate-limited';
if (menuResult.mode === "manage") {
if (menuResult.toggleAccountIndex !== undefined) {
const acc = existingStorage.accounts[menuResult.toggleAccountIndex];
if (acc) {
acc.enabled = acc.enabled === false;
await saveAccounts(existingStorage);
console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`);
}
}
continue;
}
return {
email: acc.email,
index: idx,
addedAt: acc.addedAt,
lastUsed: acc.lastUsed,
status,
isCurrentAccount: idx === (existingStorage.activeIndex ?? 0),
};
});
const menuResult = await promptLoginMode(existingAccounts);
break;
}
if (menuResult.mode === "cancel") {
return {

View File

@@ -127,6 +127,7 @@ export interface ManagedAccount {
parts: RefreshParts;
access?: string;
expires?: number;
enabled: boolean;
rateLimitResetTimes: RateLimitStateV3;
lastSwitchReason?: "rate-limit" | "initial" | "rotation";
coolingDownUntil?: number;
@@ -277,6 +278,7 @@ export class AccountManager {
},
access: matchesFallback ? authFallback?.access : undefined,
expires: matchesFallback ? authFallback?.expires : undefined,
enabled: acc.enabled !== false,
rateLimitResetTimes: acc.rateLimitResetTimes ?? {},
lastSwitchReason: acc.lastSwitchReason,
coolingDownUntil: acc.coolingDownUntil,
@@ -319,6 +321,7 @@ export class AccountManager {
parts: authParts,
access: authFallback.access,
expires: authFallback.expires,
enabled: true,
rateLimitResetTimes: {},
touchedForQuota: {},
};
@@ -342,6 +345,7 @@ export class AccountManager {
parts,
access: authFallback.access,
expires: authFallback.expires,
enabled: true,
rateLimitResetTimes: {},
touchedForQuota: {},
},
@@ -354,9 +358,17 @@ export class AccountManager {
}
getAccountCount(): number {
return this.getEnabledAccounts().length;
}
getTotalAccountCount(): number {
return this.accounts.length;
}
getEnabledAccounts(): ManagedAccount[] {
return this.accounts.filter((account) => account.enabled);
}
getAccountsSnapshot(): ManagedAccount[] {
return this.accounts.map((a) => ({ ...a, parts: { ...a.parts }, rateLimitResetTimes: { ...a.rateLimitResetTimes } }));
}

View File

@@ -29,7 +29,7 @@ export async function promptAddAnotherAccount(currentCount: number): Promise<boo
}
}
export type LoginMode = "add" | "fresh" | "cancel";
export type LoginMode = "add" | "fresh" | "manage" | "check" | "cancel";
export interface ExistingAccountInfo {
email?: string;
@@ -38,12 +38,14 @@ export interface ExistingAccountInfo {
lastUsed?: number;
status?: AccountStatus;
isCurrentAccount?: boolean;
enabled?: boolean;
}
export interface LoginMenuResult {
mode: LoginMode;
deleteAccountIndex?: number;
refreshAccountIndex?: number;
toggleAccountIndex?: number;
deleteAll?: boolean;
}
@@ -58,7 +60,7 @@ async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]):
console.log("");
while (true) {
const answer = await rl.question("(a)dd new account(s) or (f)resh start? [a/f]: ");
const answer = await rl.question("(a)dd new, (f)resh start, (m)anage, (c)heck quotas? [a/f/m/c]: ");
const normalized = answer.trim().toLowerCase();
if (normalized === "a" || normalized === "add") {
@@ -67,8 +69,14 @@ async function promptLoginModeFallback(existingAccounts: ExistingAccountInfo[]):
if (normalized === "f" || normalized === "fresh") {
return { mode: "fresh" };
}
if (normalized === "m" || normalized === "manage") {
return { mode: "manage" };
}
if (normalized === "c" || normalized === "check") {
return { mode: "check" };
}
console.log("Please enter 'a' to add accounts or 'f' to start fresh.");
console.log("Please enter 'a', 'f', 'm', or 'c'.");
}
} finally {
rl.close();
@@ -87,6 +95,7 @@ export async function promptLoginMode(existingAccounts: ExistingAccountInfo[]):
lastUsed: acc.lastUsed,
status: acc.status,
isCurrentAccount: acc.isCurrentAccount,
enabled: acc.enabled,
}));
console.log("");
@@ -98,6 +107,12 @@ export async function promptLoginMode(existingAccounts: ExistingAccountInfo[]):
case "add":
return { mode: "add" };
case "check":
return { mode: "check" };
case "manage":
return { mode: "manage" };
case "select-account": {
const accountAction = await showAccountDetails(action.account);
if (accountAction === "delete") {
@@ -106,6 +121,9 @@ export async function promptLoginMode(existingAccounts: ExistingAccountInfo[]):
if (accountAction === "refresh") {
return { mode: "add", refreshAccountIndex: action.account.index };
}
if (accountAction === "toggle") {
return { mode: "manage", toggleAccountIndex: action.account.index };
}
continue;
}

273
src/plugin/quota.ts Normal file
View File

@@ -0,0 +1,273 @@
import {
ANTIGRAVITY_ENDPOINT_PROD,
ANTIGRAVITY_HEADERS,
ANTIGRAVITY_PROVIDER_ID,
} from "../constants";
import { accessTokenExpired, formatRefreshParts, parseRefreshParts } from "./auth";
import { ensureProjectContext } from "./project";
import { refreshAccessToken } from "./token";
import { getModelFamily } from "./transform/model-resolver";
import type { PluginClient, OAuthAuthDetails } from "./types";
import type { AccountMetadataV3 } from "./storage";
const FETCH_TIMEOUT_MS = 10000;
export type QuotaGroup = "claude" | "gemini-pro" | "gemini-flash";
export interface QuotaGroupSummary {
remainingFraction?: number;
resetTime?: string;
modelCount: number;
}
export interface QuotaSummary {
groups: Partial<Record<QuotaGroup, QuotaGroupSummary>>;
modelCount: number;
error?: string;
}
export type AccountQuotaStatus = "ok" | "disabled" | "error";
export interface AccountQuotaResult {
index: number;
email?: string;
status: AccountQuotaStatus;
error?: string;
disabled?: boolean;
quota?: QuotaSummary;
updatedAccount?: AccountMetadataV3;
}
interface FetchAvailableModelsResponse {
models?: Record<string, FetchAvailableModelEntry>;
}
interface FetchAvailableModelEntry {
quotaInfo?: {
remainingFraction?: number;
resetTime?: string;
};
displayName?: string;
modelName?: string;
}
function buildAuthFromAccount(account: AccountMetadataV3): OAuthAuthDetails {
return {
type: "oauth",
refresh: formatRefreshParts({
refreshToken: account.refreshToken,
projectId: account.projectId,
managedProjectId: account.managedProjectId,
}),
access: undefined,
expires: undefined,
};
}
function normalizeRemainingFraction(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
if (value < 0) return 0;
if (value > 1) return 1;
return value;
}
function parseResetTime(resetTime?: string): number | null {
if (!resetTime) return null;
const timestamp = Date.parse(resetTime);
if (!Number.isFinite(timestamp)) {
return null;
}
return timestamp;
}
function classifyQuotaGroup(modelName: string, displayName?: string): QuotaGroup | null {
const combined = `${modelName} ${displayName ?? ""}`.toLowerCase();
if (combined.includes("claude")) {
return "claude";
}
const isGemini3 = combined.includes("gemini-3") || combined.includes("gemini 3");
if (!isGemini3) {
return null;
}
const family = getModelFamily(modelName);
return family === "gemini-flash" ? "gemini-flash" : "gemini-pro";
}
function aggregateQuota(models?: Record<string, FetchAvailableModelEntry>): QuotaSummary {
const groups: Partial<Record<QuotaGroup, QuotaGroupSummary>> = {};
if (!models) {
return { groups, modelCount: 0 };
}
let totalCount = 0;
for (const [modelName, entry] of Object.entries(models)) {
const group = classifyQuotaGroup(modelName, entry.displayName ?? entry.modelName);
if (!group) {
continue;
}
const quotaInfo = entry.quotaInfo;
const remainingFraction = quotaInfo
? normalizeRemainingFraction(quotaInfo.remainingFraction ?? 0)
: undefined;
const resetTime = quotaInfo?.resetTime;
const resetTimestamp = parseResetTime(resetTime);
totalCount += 1;
const existing = groups[group];
const nextCount = (existing?.modelCount ?? 0) + 1;
const nextRemaining =
remainingFraction === undefined
? existing?.remainingFraction
: existing?.remainingFraction === undefined
? remainingFraction
: Math.min(existing.remainingFraction, remainingFraction);
let nextResetTime = existing?.resetTime;
if (resetTimestamp !== null) {
if (!existing?.resetTime) {
nextResetTime = resetTime;
} else {
const existingTimestamp = parseResetTime(existing.resetTime);
if (existingTimestamp === null || resetTimestamp < existingTimestamp) {
nextResetTime = resetTime;
}
}
}
groups[group] = {
remainingFraction: nextRemaining,
resetTime: nextResetTime,
modelCount: nextCount,
};
}
return { groups, modelCount: totalCount };
}
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = FETCH_TIMEOUT_MS): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
async function fetchAvailableModels(
accessToken: string,
projectId: string,
): Promise<FetchAvailableModelsResponse> {
const endpoint = ANTIGRAVITY_ENDPOINT_PROD;
const quotaUserAgent = ANTIGRAVITY_HEADERS["User-Agent"] || "antigravity/windows/amd64";
const errors: string[] = [];
const body = projectId ? { project: projectId } : {};
const response = await fetchWithTimeout(`${endpoint}/v1internal:fetchAvailableModels`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": quotaUserAgent,
},
body: JSON.stringify(body),
});
if (response.ok) {
return (await response.json()) as FetchAvailableModelsResponse;
}
const message = await response.text().catch(() => "");
const snippet = message.trim().slice(0, 200);
errors.push(
`fetchAvailableModels ${response.status} at ${endpoint}${snippet ? `: ${snippet}` : ""}`,
);
throw new Error(errors.join("; ") || "fetchAvailableModels failed");
}
function applyAccountUpdates(account: AccountMetadataV3, auth: OAuthAuthDetails): AccountMetadataV3 | undefined {
const parts = parseRefreshParts(auth.refresh);
if (!parts.refreshToken) {
return undefined;
}
const updated: AccountMetadataV3 = {
...account,
refreshToken: parts.refreshToken,
projectId: parts.projectId ?? account.projectId,
managedProjectId: parts.managedProjectId ?? account.managedProjectId,
};
const changed =
updated.refreshToken !== account.refreshToken ||
updated.projectId !== account.projectId ||
updated.managedProjectId !== account.managedProjectId;
return changed ? updated : undefined;
}
export async function checkAccountsQuota(
accounts: AccountMetadataV3[],
client: PluginClient,
providerId = ANTIGRAVITY_PROVIDER_ID,
): Promise<AccountQuotaResult[]> {
const results: AccountQuotaResult[] = [];
for (const [index, account] of accounts.entries()) {
const disabled = account.enabled === false;
let auth = buildAuthFromAccount(account);
try {
if (accessTokenExpired(auth)) {
const refreshed = await refreshAccessToken(auth, client, providerId);
if (!refreshed) {
throw new Error("Token refresh failed");
}
auth = refreshed;
}
const projectContext = await ensureProjectContext(auth);
auth = projectContext.auth;
const updatedAccount = applyAccountUpdates(account, auth);
let quotaResult: QuotaSummary;
try {
const response = await fetchAvailableModels(
auth.access ?? "",
projectContext.effectiveProjectId,
);
quotaResult = aggregateQuota(response.models);
} catch (error) {
quotaResult = {
groups: {},
modelCount: 0,
error: error instanceof Error ? error.message : String(error),
};
}
results.push({
index,
email: account.email,
status: "ok",
disabled,
quota: quotaResult,
updatedAccount,
});
} catch (error) {
results.push({
index,
email: account.email,
status: "error",
disabled,
error: error instanceof Error ? error.message : String(error),
});
}
}
return results;
}

View File

@@ -182,6 +182,7 @@ export interface AccountMetadataV3 {
managedProjectId?: string;
addedAt: number;
lastUsed: number;
enabled?: boolean;
lastSwitchReason?: "rate-limit" | "initial" | "rotation";
rateLimitResetTimes?: RateLimitStateV3;
coolingDownUntil?: number;

View File

@@ -11,15 +11,18 @@ export interface AccountInfo {
lastUsed?: number;
status?: AccountStatus;
isCurrentAccount?: boolean;
enabled?: boolean;
}
export type AuthMenuAction =
| { type: 'add' }
| { type: 'select-account'; account: AccountInfo }
| { type: 'delete-all' }
| { type: 'check' }
| { type: 'manage' }
| { type: 'cancel' };
export type AccountAction = 'back' | 'delete' | 'refresh' | 'cancel';
export type AccountAction = 'back' | 'delete' | 'refresh' | 'toggle' | 'cancel';
function formatRelativeTime(timestamp: number | undefined): string {
if (!timestamp) return 'never';
@@ -48,11 +51,14 @@ function getStatusBadge(status: AccountStatus | undefined): string {
export async function showAuthMenu(accounts: AccountInfo[]): Promise<AuthMenuAction> {
const items: MenuItem<AuthMenuAction>[] = [
{ label: 'Add new account', value: { type: 'add' } },
{ label: 'Check quotas', value: { type: 'check' } },
{ label: 'Manage accounts (enable/disable)', value: { type: 'manage' } },
...accounts.map(account => {
const badge = getStatusBadge(account.status);
const disabledBadge = account.enabled === false ? ` ${ANSI.red}[disabled]${ANSI.reset}` : '';
const label = account.email || `Account ${account.index + 1}`;
const fullLabel = badge ? `${label} ${badge}` : label;
const fullLabel = `${label}${badge ? ' ' + badge : ''}${disabledBadge}`;
return {
label: fullLabel,
@@ -84,9 +90,10 @@ export async function showAuthMenu(accounts: AccountInfo[]): Promise<AuthMenuAct
export async function showAccountDetails(account: AccountInfo): Promise<AccountAction> {
const label = account.email || `Account ${account.index + 1}`;
const badge = getStatusBadge(account.status);
const disabledBadge = account.enabled === false ? ` ${ANSI.red}[disabled]${ANSI.reset}` : '';
console.log('');
console.log(`${ANSI.bold}Account: ${label}${badge ? ' ' + badge : ''}${ANSI.reset}`);
console.log(`${ANSI.bold}Account: ${label}${badge ? ' ' + badge : ''}${disabledBadge}${ANSI.reset}`);
console.log(`${ANSI.dim}Added: ${formatDate(account.addedAt)}${ANSI.reset}`);
console.log(`${ANSI.dim}Last used: ${formatRelativeTime(account.lastUsed)}${ANSI.reset}`);
console.log('');
@@ -94,6 +101,7 @@ export async function showAccountDetails(account: AccountInfo): Promise<AccountA
while (true) {
const result = await select([
{ label: 'Back', value: 'back' as const },
{ label: account.enabled === false ? 'Enable account' : 'Disable account', value: 'toggle' as const, color: account.enabled === false ? 'green' : 'yellow' },
{ label: 'Refresh token', value: 'refresh' as const, color: 'cyan' },
{ label: 'Delete this account', value: 'delete' as const, color: 'red' },
], {

View File

@@ -1,4 +1,6 @@
{
"include": ["src/**/*", "scripts/**/*"],
"exclude": ["node_modules", "dist", "temp_research", "script"],
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM"],