(opts: CommonOptions, deps: UsageDeps = {})
| 79 | * omitted (forward-compat until the backend adds the projection). |
| 80 | */ |
| 81 | export async function runUsage(opts: CommonOptions, deps: UsageDeps = {}): Promise<UsageResponse> { |
| 82 | const stderr = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`)); |
| 83 | const out = makeOutput(opts.output, deps); |
| 84 | |
| 85 | if (opts.dryRun) { |
| 86 | emitDryRunBanner(stderr); |
| 87 | stderr('[note] credit balance requires a backend update — showing dry-run sample values'); |
| 88 | out.print(DRY_RUN_USAGE_SAMPLE, data => renderUsage(data as UsageResponse)); |
| 89 | return DRY_RUN_USAGE_SAMPLE; |
| 90 | } |
| 91 | |
| 92 | const client = makeHttpClient(opts, { |
| 93 | env: deps.env, |
| 94 | credentialsPath: deps.credentialsPath, |
| 95 | fetchImpl: deps.fetchImpl, |
| 96 | stderr: deps.stderr, |
| 97 | }); |
| 98 | |
| 99 | // Environment-correct portal origin for billing/upgrade links (dev and prod |
| 100 | // portals live on different domains — never hardcode). Resolved from the |
| 101 | // same credentials the client was just built from; undefined for unknown |
| 102 | // hosts (links then render as routes only). |
| 103 | const portalBase = resolvePortalBase( |
| 104 | loadConfig({ |
| 105 | profile: opts.profile, |
| 106 | endpointUrl: opts.endpointUrl, |
| 107 | env: deps.env, |
| 108 | credentialsPath: deps.credentialsPath, |
| 109 | }).apiUrl, |
| 110 | ); |
| 111 | const billingUrl = |
| 112 | portalBase !== undefined |
| 113 | ? `${portalBase}/dashboard/settings/billing` |
| 114 | : 'the portal Billing page (/dashboard/settings/billing)'; |
| 115 | |
| 116 | // /me is the only available source of credits/plan today. |
| 117 | // When the backend adds credits/subPlan to MeResponse (or adds /usage), |
| 118 | // this single get call is sufficient — no code change needed in the CLI. |
| 119 | const me = await client.get<UsageResponse>('/me'); |
| 120 | |
| 121 | out.print(me, data => renderUsage(data as UsageResponse, portalBase)); |
| 122 | |
| 123 | // In text mode, emit a backend-gap note when credits are missing so the |
| 124 | // user knows why the balance isn't shown (instead of assuming zero or error). |
| 125 | if (opts.output === 'text' && me.credits === undefined) { |
| 126 | stderr( |
| 127 | '[note] credit balance not available — backend does not yet expose credits on /me.' + |
| 128 | ` Check ${billingUrl} for your current balance.`, |
| 129 | ); |
| 130 | } |
| 131 | |
| 132 | return me; |
| 133 | } |
| 134 | |
| 135 | function renderUsage(u: UsageResponse, portalBase?: string): string { |
| 136 | const lines: string[] = []; |
no test coverage detected