CLI tool for checking Google Cloud Code API quota usage across multiple accounts and identities (antigravity/gemini-cli). ESM TypeScript project targeting Node >= 20.
# Run all tests
bun run test # or: npx vitest run
# Run a single test file
npx vitest run src/__tests__/quota.test.ts
# Run a single test by name
npx vitest run src/__tests__/quota.test.ts -t "converts remainingFraction to percent"
# Typecheck (no emit)
bun run typecheck # tsc -p tsconfig.json --noEmit
# Build (compile to dist/)
bun run build # tsc -p tsconfig.jsonNo linter or formatter is configured. Conventions are enforced by code review and one meta-test (esm-imports.test.ts) that verifies all relative imports use .js extensions.
- 2-space indentation, double quotes, semicolons always, trailing commas on multiline
constby default;letonly when reassignment is needed; nevervar- K&R brace style: opening brace on same line
- Template literals for interpolation
- Relative imports: always use
.jsextension (enforced by test) - Node built-ins: use
node:protocol (import path from "node:path") - Type-only: use
import type { Foo }for pure-type imports; use inlinetypekeyword for mixed (import { runStatus, type OutputFormat } from "./commands/status.js") - Order: external packages, then relative modules, then types
- Named exports only. No default exports anywhere in source code.
| Category | Convention | Examples |
|---|---|---|
| Functions | camelCase | fetchQuota, parseQuotaResponse, runCli |
| Variables | camelCase | accessToken, lastError, remainingFraction |
| Constants | UPPER_SNAKE_CASE | FETCH_TIMEOUT_MS, MODEL_FILTER_PATTERN |
| Types/Interfaces | PascalCase | ModelQuota, StatusDeps, QuotaIdentity |
| Error classes | PascalCase+Error | QuotaError, OAuthTokenError, ProjectError |
| Files | kebab-case | quota.ts, token.ts, userinfo.ts |
| Test files | <name>.test.ts |
quota.test.ts, status.test.ts |
| Directories | kebab-case | oauth/, google/, commands/, output/ |
- Explicit return types on all exported functions
as conston constant arrays/objects for narrow typingtypefor unions/aliases;interfacefor object shapes and API contractsreadonlyon class properties for immutability- Minimal type assertions; only
as Twhen truly necessary
- Exported functions: single
inputobject parameter for anything with 2+ args - Internal helpers: positional parameters are fine
- Dependency injection: optional
fetchImpl?: FetchLikeon all network functions, defaulting to globalfetch - Command modules (
status.ts,login.ts): fullDepsinterface for DI with adefaultDepsconstant
// Exported function pattern
export async function fetchQuota(input: {
accessToken: string;
projectId: string;
identity?: QuotaIdentity;
fetchImpl?: FetchLike;
}): Promise<ModelQuota[]> { ... }- Custom error classes extend
Errorwithreadonly name = "ClassName"as a class property - Constructor takes a single object
inputparameter - All extra fields are
readonly(status,endpoint, etc.) - Catch blocks: use
catch { ... }(no binding) for non-critical errors - Safe error stringification:
err instanceof Error ? err.message : String(err)
export class QuotaError extends Error {
readonly name = "QuotaError";
readonly status: number;
readonly endpoint: string;
constructor(input: { message: string; status: number; endpoint: string }) {
super(input.message);
this.status = input.status;
this.endpoint = input.endpoint;
}
}- Module-level doc comment at top of every file
- JSDoc on all exported functions, types, and interfaces
- Use
@param,@returns,@throwstags for important APIs - Internal helpers may have shorter or no JSDoc
- Module-level JSDoc comment
- Imports (external, then relative, then types)
- Type/interface exports
- Constants (exported, then private)
- Private helper functions
- Exported main functions
async/awaitthroughout; no raw.then()chainsPromise.all()for parallelism- Top-level
awaitin entry point (src/index.ts)
- Framework: Vitest with explicit imports (
import { describe, it, expect, vi } from "vitest") - Location: all tests in
src/__tests__/(flat directory) - No snapshots, no external mocking libraries (
msw,nock, etc.)
- Flat
it()blocks for simple modules describe/itnesting for modules with logical groupings- Test names: descriptive, behavior-focused, present tense verb ("converts...", "handles...", "throws...")
- Network: inject
fetchImplvia DI; usecreateFakeFetch()factory returning{ fetch, calls }to capture and assert on requests - Commands: mock entire
Depsinterface withvi.fn().mockResolvedValue() - Filesystem: real temp directories via
mkdtemp(), cleaned up infinallyblocks - No real network calls in any test
- AAA pattern (Arrange/Act/Assert)
expect(...).rejects.toThrow(...)for async errorsexpect(err).toBeInstanceOf(ErrorClass)for error type checksexpect.objectContaining({...})for partial matching
- Two identities:
antigravity(IDE quota viafetchAvailableModels) andgemini-cli(CLI quota viaretrieveUserQuota) hit different API endpoints - Model names are dynamic: fetched from Google APIs at runtime, filtered by
/gemini|claude|image|imagen/iregex - Summary allowlist: hardcoded list in
src/output/table.tscontrols which models appear in the summary table; all models appear in "Full detail" - Account storage: JSON file at
~/.config/opencode/usage-google-accounts.json - Immutable data:
upsertAccountreturns new objects via spread, never mutates in place - Token caching: Access tokens are cached to the store file (
cachedAccessToken/cachedExpiresAtfields on each identity). On subsequent runs within the token's ~1 hour lifetime (with a 5-minute safety margin), the OAuth refresh round-trip is skipped. Cache persistence is fire-and-forget (non-blocking, errors swallowed). - Network timeouts: Quota fetch and project discovery use 15s timeouts; token refresh uses a 10s timeout via
AbortController. All timeouts throw on expiry rather than hanging.