feat(cli): adicionar grupo usage com 7 subcomandos (Fase 2.5)

Implementa `omniroute usage` com subcomandos:
- analytics: agregados por provedor com filtro --period/--provider
- budget list|get|set|reset: gerenciamento de budgets de custo
- quota: estado de quota por provedor com --check
- logs: call-logs com --search, --since, --api-key e --follow (tail 2s)
- utilization: métricas de uso por API key
- history: histórico de requisições
- proxy-logs: logs em nível de proxy

API keys mascaradas em outputs human. Todos os subcomandos suportam
--output json/table/csv/jsonl via emit().
This commit is contained in:
diegosouzapw
2026-05-15 00:59:21 -03:00
parent 993cd32829
commit 85489a0295
5 changed files with 632 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ import { registerChat } from "./chat.mjs";
import { registerStream } from "./stream.mjs";
import { registerSimulate } from "./simulate.mjs";
import { registerCost } from "./cost.mjs";
import { registerUsage } from "./usage.mjs";
import { registerServe } from "./serve.mjs";
import { registerStop } from "./stop.mjs";
import { registerRestart } from "./restart.mjs";
@@ -33,6 +34,7 @@ export function registerCommands(program) {
registerStream(program);
registerSimulate(program);
registerCost(program);
registerUsage(program);
registerServe(program);
registerStop(program);
registerRestart(program);

331
bin/cli/commands/usage.mjs Normal file
View File

@@ -0,0 +1,331 @@
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch } from "../api.mjs";
import { emit, maskSecret } from "../output.mjs";
import { t } from "../i18n.mjs";
const fmtTs = (v) => (v ? new Date(v).toISOString().replace("T", " ").slice(0, 19) : "-");
const maskKey = (v) => (typeof v === "string" ? maskSecret(v) : (v ?? "-"));
const fmtCost = (v) => (v ? `$${Number(v).toFixed(4)}` : "-");
const fmtTokens = (v) => {
if (!v) return "0";
if (v > 1e6) return `${(v / 1e6).toFixed(1)}M`;
if (v > 1e3) return `${(v / 1e3).toFixed(1)}K`;
return String(v);
};
const analyticsSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "requests", header: "Reqs", formatter: (v) => (v != null ? v.toLocaleString() : "0") },
{ key: "tokensIn", header: "Tokens In", formatter: fmtTokens },
{ key: "tokensOut", header: "Tokens Out", formatter: fmtTokens },
{ key: "costUsd", header: "Cost (USD)", formatter: fmtCost },
];
const budgetSchema = [
{ key: "scope", header: "Scope", width: 25 },
{ key: "period", header: "Period" },
{ key: "limit", header: "Limit (USD)", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "used", header: "Used (USD)", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "remaining", header: "Remaining", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "pct", header: "%", formatter: (v) => `${(Number(v) * 100).toFixed(1)}%` },
];
const quotaSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "limit", header: "Limit", formatter: fmtTokens },
{ key: "used", header: "Used", formatter: fmtTokens },
{ key: "remaining", header: "Remaining", formatter: fmtTokens },
{ key: "resetAt", header: "Reset At", formatter: fmtTs },
{ key: "state", header: "State" },
];
const logsSchema = [
{ key: "timestamp", header: "Time", width: 20, formatter: fmtTs },
{ key: "apiKey", header: "API Key", width: 16, formatter: maskKey },
{ key: "method", header: "Method", width: 8 },
{ key: "provider", header: "Provider", width: 14 },
{ key: "model", header: "Model", width: 25 },
{ key: "tokens", header: "Tokens", formatter: fmtTokens },
{ key: "costUsd", header: "Cost", formatter: fmtCost },
{ key: "latencyMs", header: "Latency", formatter: (v) => (v ? `${v}ms` : "-") },
{ key: "status", header: "Status" },
];
export function registerUsage(program) {
const usage = program.command("usage").description(t("usage.description"));
// analytics
usage
.command("analytics")
.description(t("usage.analytics.description"))
.option("--period <range>", t("usage.analytics.period"), "30d")
.option("--provider <id>", t("usage.analytics.provider"))
.action(runUsageAnalytics);
// budget
const budget = usage.command("budget").description(t("usage.budget.description"));
budget.command("list").action(runBudgetList);
budget.command("get [scope]").action(runBudgetGet);
budget
.command("set <amount>")
.option("--scope <s>", t("usage.budget.set.scope"), "global")
.option("--period <p>", t("usage.budget.set.period"), "monthly")
.action(runBudgetSet);
budget.command("reset [scope]").action(runBudgetReset);
// quota
usage
.command("quota")
.description(t("usage.quota.description"))
.option("--provider <id>", t("usage.quota.provider"))
.option("--check", t("usage.quota.check"))
.action(runUsageQuota);
// logs
usage
.command("logs")
.description(t("usage.logs.description"))
.option("--limit <n>", t("usage.logs.limit"), parseInt, 100)
.option("--search <q>", t("usage.logs.search"))
.option("--since <ts>", t("usage.logs.since"))
.option("--follow", t("usage.logs.follow"))
.option("--api-key <k>", t("usage.logs.api_key"))
.action(runUsageLogs);
// utilization
usage
.command("utilization")
.description(t("usage.utilization.description"))
.option("--api-key <k>", t("usage.utilization.api_key"))
.action(runUsageUtilization);
// history
usage
.command("history")
.description(t("usage.history.description"))
.option("--limit <n>", t("usage.history.limit"), parseInt, 100)
.action(runUsageHistory);
// proxy-logs
usage
.command("proxy-logs")
.description(t("usage.proxy_logs.description"))
.option("--limit <n>", t("usage.proxy_logs.limit"), parseInt, 100)
.action(runUsageProxyLogs);
}
export async function runUsageAnalytics(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ range: opts.period ?? "30d" });
if (opts.provider) p.set("provider", opts.provider);
const res = await fetchOrExit(`/api/usage/analytics?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.byProvider ?? data.providers ?? []).map((r) => ({
provider: r.provider ?? r.providerId ?? "",
requests: r.totalRequests ?? r.requests ?? 0,
tokensIn: r.totalTokensIn ?? r.tokensIn ?? 0,
tokensOut: r.totalTokensOut ?? r.tokensOut ?? 0,
costUsd: r.totalCost ?? r.cost ?? r.costUsd ?? 0,
}));
emit(rows, globalOpts, analyticsSchema);
}
export async function runBudgetList(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await fetchOrExit("/api/usage/budget", globalOpts);
const data = await res.json();
const rows = normalizeBudgetRows(data);
emit(rows, globalOpts, budgetSchema);
}
export async function runBudgetGet(scope, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (scope) p.set("scope", scope);
const res = await fetchOrExit(`/api/usage/budget?${p}`, globalOpts);
const data = await res.json();
const rows = normalizeBudgetRows(data);
emit(rows, globalOpts, budgetSchema);
}
export async function runBudgetSet(amount, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/usage/budget", {
method: "POST",
body: {
amount: Number(amount),
scope: opts.scope ?? "global",
period: opts.period ?? "monthly",
},
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
const txt = await res.text().catch(() => "");
process.stderr.write(`[error] HTTP ${res.status}: ${txt.slice(0, 200)}\n`);
process.exit(res.exitCode ?? 1);
}
if (!globalOpts.quiet)
process.stdout.write(
`Budget set: $${Number(amount).toFixed(2)} / ${opts.scope ?? "global"} / ${opts.period ?? "monthly"}\n`
);
}
export async function runBudgetReset(scope, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/usage/budget", {
method: "DELETE",
body: { scope: scope ?? "global" },
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
const txt = await res.text().catch(() => "");
process.stderr.write(`[error] HTTP ${res.status}: ${txt.slice(0, 200)}\n`);
process.exit(res.exitCode ?? 1);
}
if (!globalOpts.quiet) process.stdout.write(`Budget reset: ${scope ?? "global"}\n`);
}
export async function runUsageQuota(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (opts.provider) p.set("provider", opts.provider);
if (opts.check) p.set("check", "true");
const res = await fetchOrExit(`/api/usage/quota?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.providers ?? data.data ?? (Array.isArray(data) ? data : [])).map(
(r) => ({
provider: r.provider ?? r.providerId ?? "",
limit: r.limit ?? r.quota ?? r.maxTokens ?? null,
used: r.used ?? r.tokensUsed ?? null,
remaining: r.remaining ?? r.percentRemaining ?? null,
resetAt: r.resetAt ?? r.nextReset ?? null,
state: r.state ?? (r.percentRemaining > 0 ? "available" : "exhausted"),
})
);
emit(rows, globalOpts, quotaSchema);
}
export async function runUsageLogs(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
if (opts.follow) {
await followLogs(opts, globalOpts);
return;
}
const p = buildLogParams(opts);
const res = await fetchOrExit(`/api/usage/call-logs?${p}`, globalOpts);
const data = await res.json();
const rows = toLogRows(toArray(data.logs ?? data.items ?? data));
emit(rows, globalOpts, logsSchema);
}
export async function runUsageUtilization(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (opts.apiKey) p.set("apiKey", opts.apiKey);
const res = await fetchOrExit(`/api/usage/utilization?${p}`, globalOpts);
const data = await res.json();
const rows = Array.isArray(data) ? data : toArray(data.data ?? data.items ?? [data]);
emit(rows, globalOpts, null);
}
export async function runUsageHistory(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ limit: String(opts.limit ?? 100) });
const res = await fetchOrExit(`/api/usage/history?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.items ?? data.history ?? (Array.isArray(data) ? data : []));
emit(rows, globalOpts, null);
}
export async function runUsageProxyLogs(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ limit: String(opts?.limit ?? 100) });
const res = await fetchOrExit(`/api/usage/proxy-logs?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.logs ?? data.items ?? (Array.isArray(data) ? data : []));
emit(rows, globalOpts, null);
}
async function followLogs(opts, globalOpts) {
let lastId = null;
process.stderr.write("[following logs — press Ctrl+C to stop]\n");
const sigint = () => process.exit(0);
process.on("SIGINT", sigint);
try {
while (true) {
const p = buildLogParams({ ...opts, limit: opts.limit ?? 20 });
if (lastId) p.append("afterId", String(lastId));
const res = await apiFetch(`/api/usage/call-logs?${p}`, {
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (res.ok) {
const data = await res.json();
const rows = toLogRows(toArray(data.logs ?? data.items ?? data));
if (rows.length > 0) {
emit(rows, { ...globalOpts, quiet: true }, logsSchema);
lastId = rows[rows.length - 1]?.id ?? lastId;
}
}
await sleep(2000);
}
} finally {
process.off("SIGINT", sigint);
}
}
function buildLogParams(opts) {
const p = new URLSearchParams({ limit: String(opts.limit ?? 100) });
if (opts.search) p.set("search", opts.search);
if (opts.since) p.set("since", opts.since);
if (opts.apiKey) p.set("apiKey", opts.apiKey);
return p;
}
function toLogRows(items) {
return items.map((r) => ({
id: r.id,
timestamp: r.createdAt ?? r.timestamp ?? r.ts,
apiKey: r.apiKey ?? r.keyId ?? r.apiKeyId,
method: r.method ?? "POST",
provider: r.provider ?? r.providerId,
model: r.model ?? r.modelId,
tokens: (r.tokensIn ?? r.promptTokens ?? 0) + (r.tokensOut ?? r.completionTokens ?? 0),
costUsd: r.cost ?? r.costUsd ?? r.totalCost,
latencyMs: r.latencyMs ?? r.durationMs,
status: r.status ?? r.statusCode,
}));
}
function normalizeBudgetRows(data) {
const items = toArray(data.budgets ?? data.items ?? (Array.isArray(data) ? data : [data]));
return items.map((r) => ({
scope: r.scope ?? r.scopeId ?? "global",
period: r.period ?? "monthly",
limit: r.limit ?? r.amount ?? 0,
used: r.used ?? r.spent ?? 0,
remaining: r.remaining ?? Math.max(0, (r.limit ?? 0) - (r.used ?? 0)),
pct: r.pct ?? (r.limit > 0 ? (r.used ?? 0) / r.limit : 0),
}));
}
async function fetchOrExit(path, globalOpts) {
const res = await apiFetch(path, { timeout: globalOpts.timeout, acceptNotOk: true });
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
process.stderr.write(t("common.authRequired") + "\n");
} else {
process.stderr.write(t("common.serverOffline") + "\n");
}
process.exit(res.exitCode ?? 1);
}
return res;
}
function toArray(val) {
return Array.isArray(val) ? val : [];
}

View File

@@ -70,6 +70,46 @@
"empty_prompt": "Error: prompt is required (positional arg, --file, or --stdin)"
}
},
"usage": {
"description": "Usage analytics, budgets, quotas, and logs",
"analytics": {
"description": "Show aggregated usage analytics",
"period": "Time range: 1d|7d|30d|90d|ytd|all (default: 30d)",
"provider": "Filter by provider ID"
},
"budget": {
"description": "Manage cost budgets",
"set": {
"scope": "Budget scope (default: global)",
"period": "Budget period: daily|weekly|monthly (default: monthly)"
}
},
"quota": {
"description": "Show provider quota usage",
"provider": "Filter by provider ID",
"check": "Show whether quota is available for a new request"
},
"logs": {
"description": "Show request call logs",
"limit": "Number of log entries to return (default: 100)",
"search": "Search query to filter logs",
"since": "Return logs since this timestamp",
"follow": "Continuously tail new log entries",
"api_key": "Filter logs by API key"
},
"utilization": {
"description": "Show API key utilization metrics",
"api_key": "Filter by API key"
},
"history": {
"description": "Show request history",
"limit": "Number of history entries (default: 100)"
},
"proxy_logs": {
"description": "Show proxy-level request logs",
"limit": "Number of proxy log entries (default: 100)"
}
},
"cost": {
"description": "Show cost report with breakdown by provider, model, combo, or API key",
"period": "Time range: 1d|7d|30d|90d|ytd|all (default: 30d)",

View File

@@ -70,6 +70,46 @@
"empty_prompt": "Erro: prompt obrigatório (argumento posicional, --file ou --stdin)"
}
},
"usage": {
"description": "Analytics de uso, budgets, quotas e logs",
"analytics": {
"description": "Exibir analytics de uso agregados",
"period": "Período: 1d|7d|30d|90d|ytd|all (padrão: 30d)",
"provider": "Filtrar por ID de provedor"
},
"budget": {
"description": "Gerenciar budgets de custo",
"set": {
"scope": "Escopo do budget (padrão: global)",
"period": "Período do budget: daily|weekly|monthly (padrão: monthly)"
}
},
"quota": {
"description": "Exibir uso de quota dos provedores",
"provider": "Filtrar por ID de provedor",
"check": "Mostrar se há quota disponível para uma nova requisição"
},
"logs": {
"description": "Exibir logs de chamadas",
"limit": "Número de entradas de log a retornar (padrão: 100)",
"search": "Filtro de busca",
"since": "Retornar logs a partir deste timestamp",
"follow": "Fazer tail contínuo de novos logs",
"api_key": "Filtrar logs por chave de API"
},
"utilization": {
"description": "Exibir métricas de utilização por chave de API",
"api_key": "Filtrar por chave de API"
},
"history": {
"description": "Exibir histórico de requisições",
"limit": "Número de entradas de histórico (padrão: 100)"
},
"proxy_logs": {
"description": "Exibir logs de requisições em nível de proxy",
"limit": "Número de entradas de proxy log (padrão: 100)"
}
},
"cost": {
"description": "Exibir relatório de custos com breakdown por provedor, modelo, combo ou chave de API",
"period": "Período: 1d|7d|30d|90d|ytd|all (padrão: 30d)",

View File

@@ -0,0 +1,219 @@
import test from "node:test";
import assert from "node:assert/strict";
const ANALYTICS_DATA = {
byProvider: [
{
provider: "openai",
totalRequests: 100,
totalTokensIn: 40000,
totalTokensOut: 16000,
totalCost: 0.35,
},
{
provider: "anthropic",
totalRequests: 50,
totalTokensIn: 20000,
totalTokensOut: 8000,
totalCost: 0.15,
},
],
};
const BUDGET_DATA = {
budgets: [
{ scope: "global", period: "monthly", limit: 100, used: 42.5, remaining: 57.5, pct: 0.425 },
],
};
const QUOTA_DATA = {
providers: [
{ provider: "openai", limit: 1000000, used: 500000, remaining: 500000, state: "available" },
],
};
const LOGS_DATA = {
logs: [
{
id: "1",
createdAt: "2026-05-15T10:00:00Z",
apiKey: "sk-test-key",
provider: "openai",
model: "gpt-4o",
tokensIn: 100,
tokensOut: 50,
cost: 0.001,
latencyMs: 500,
status: 200,
},
{
id: "2",
createdAt: "2026-05-15T10:01:00Z",
apiKey: "sk-test-key",
provider: "anthropic",
model: "claude-3-5-sonnet",
tokensIn: 80,
tokensOut: 40,
cost: 0.0008,
latencyMs: 400,
status: 200,
},
],
};
const UTILIZATION_DATA = [{ apiKey: "sk-test-key", requests: 150, cost: 0.5, avgLatency: 450 }];
const HISTORY_DATA = { items: [{ id: "a", model: "gpt-4o", provider: "openai", cost: 0.01 }] };
const PROXY_LOGS_DATA = {
logs: [{ id: "p1", path: "/v1/chat/completions", method: "POST", status: 200 }],
};
function makeResp(data: unknown, status = 200) {
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
};
obj.json = obj.json.bind(obj);
obj.text = obj.text.bind(obj);
return obj;
}
function mockFetch(overrides: Record<string, unknown> = {}) {
return (url: string) => {
if (url.includes("/api/usage/analytics"))
return Promise.resolve(makeResp(overrides.analytics ?? ANALYTICS_DATA));
if (url.includes("/api/usage/budget"))
return Promise.resolve(makeResp(overrides.budget ?? BUDGET_DATA));
if (url.includes("/api/usage/quota"))
return Promise.resolve(makeResp(overrides.quota ?? QUOTA_DATA));
if (url.includes("/api/usage/call-logs"))
return Promise.resolve(makeResp(overrides.logs ?? LOGS_DATA));
if (url.includes("/api/usage/utilization"))
return Promise.resolve(makeResp(overrides.utilization ?? UTILIZATION_DATA));
if (url.includes("/api/usage/history"))
return Promise.resolve(makeResp(overrides.history ?? HISTORY_DATA));
if (url.includes("/api/usage/proxy-logs"))
return Promise.resolve(makeResp(overrides.proxyLogs ?? PROXY_LOGS_DATA));
return Promise.resolve(makeResp({}, 404));
};
}
async function captureStdout(fn: () => Promise<void>): Promise<string> {
const chunks: string[] = [];
const orig = process.stdout.write.bind(process.stdout);
process.stdout.write = (c: string | Uint8Array) => {
chunks.push(typeof c === "string" ? c : c.toString());
return true;
};
try {
await fn();
} finally {
process.stdout.write = orig;
}
return chunks.join("");
}
test("runUsageAnalytics exibe providers em json", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runUsageAnalytics } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "json", quiet: true }) };
const out = await captureStdout(() => runUsageAnalytics({ period: "30d" }, cmd as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed[0].provider, "openai");
assert.ok(parsed[0].costUsd > 0);
});
test("runBudgetList exibe budgets", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runBudgetList } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "json", quiet: true }) };
const out = await captureStdout(() => runBudgetList({}, cmd as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed[0].scope, "global");
assert.ok(parsed[0].limit > 0);
});
test("runUsageQuota exibe providers de quota", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runUsageQuota } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "json", quiet: true }) };
const out = await captureStdout(() => runUsageQuota({}, cmd as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed[0].provider, "openai");
});
test("runUsageLogs exibe logs com mascaramento de API key", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runUsageLogs } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "table", quiet: false }) };
const out = await captureStdout(() => runUsageLogs({ limit: 10 }, cmd as any));
globalThis.fetch = origFetch;
assert.ok(!out.includes("sk-test-key") || out.includes("***"));
});
test("runUsageLogs --output json retorna rows com campos esperados", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runUsageLogs } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "json", quiet: true }) };
const out = await captureStdout(() => runUsageLogs({ limit: 10 }, cmd as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed.length, 2);
assert.ok(typeof parsed[0].provider === "string");
assert.ok(typeof parsed[0].tokens === "number");
});
test("runUsageHistory exibe histórico", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = mockFetch() as any;
const { runUsageHistory } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "json", quiet: true }) };
const out = await captureStdout(() => runUsageHistory({ limit: 50 }, cmd as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.ok(parsed.length >= 1);
});
test("runBudgetSet envia POST com amount, scope e period", async () => {
let capturedBody: unknown = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, init: any) => {
if (url.includes("/api/usage/budget") && init?.method === "POST") {
capturedBody = JSON.parse(init.body);
}
return Promise.resolve(makeResp({ ok: true }));
}) as any;
const { runBudgetSet } = await import("../../bin/cli/commands/usage.mjs");
const cmd = { optsWithGlobals: () => ({ output: "table", quiet: false }) };
await captureStdout(() => runBudgetSet("50", { scope: "global", period: "monthly" }, cmd as any));
globalThis.fetch = origFetch;
assert.ok(capturedBody !== null);
assert.equal((capturedBody as any).amount, 50);
assert.equal((capturedBody as any).scope, "global");
});