Skip to content

Commit 9133b6b

Browse files
committed
feat: add coding plan usage
1 parent b402f3e commit 9133b6b

14 files changed

Lines changed: 693 additions & 171 deletions

File tree

packages/cli/src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
usageStats,
4747
usageSummary,
4848
usageTokenPlan,
49+
usageCodingPlan,
4950
pipelineRun,
5051
pipelineValidate,
5152
advisorRecommend,
@@ -166,6 +167,7 @@ export const commands: Record<string, AnyCommand> = {
166167
"usage stats": usageStats,
167168
"usage summary": usageSummary,
168169
"usage token-plan": usageTokenPlan,
170+
"usage coding-plan": usageCodingPlan,
169171
"pipeline run": pipelineRun,
170172
"pipeline validate": pipelineValidate,
171173
"advisor recommend": advisorRecommend,
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
2+
import { emitResult } from "bailian-cli-runtime";
3+
import { printQuotaBox, readNumber, type QuotaSection } from "./quota-box.ts";
4+
import { formatNumber } from "./shared.ts";
5+
6+
const CODING_PLAN_USAGE_API =
7+
"zeldaEasy.broadscope-bailian.codingPlan.queryCodingPlanInstanceInfoV2";
8+
9+
const COMMODITY_CODES: Record<string, string> = {
10+
domestic: "sfm_codingplan_public_cn",
11+
international: "sfm_codingplan_public_intl",
12+
};
13+
14+
interface CodingPlanWindow {
15+
usedQuota?: number;
16+
totalQuota?: number;
17+
/** Usage ratio in [0, 1]; absent when the window has no positive total or no used value. */
18+
percentage?: number;
19+
resetTime?: number;
20+
}
21+
22+
interface CodingPlanUsage {
23+
instanceType?: string;
24+
per5Hour: CodingPlanWindow;
25+
perWeek: CodingPlanWindow;
26+
perBillMonth: CodingPlanWindow;
27+
}
28+
29+
function readWindow(
30+
quotaInfo: Record<string, unknown> | undefined,
31+
fieldPrefix: string,
32+
): CodingPlanWindow {
33+
const window: CodingPlanWindow = {};
34+
if (!quotaInfo) return window;
35+
36+
const usedQuota = readNumber(quotaInfo[`${fieldPrefix}UsedQuota`]);
37+
if (usedQuota !== undefined) window.usedQuota = usedQuota;
38+
const totalQuota = readNumber(quotaInfo[`${fieldPrefix}TotalQuota`]);
39+
if (totalQuota !== undefined) window.totalQuota = totalQuota;
40+
const resetTime = readNumber(quotaInfo[`${fieldPrefix}QuotaNextRefreshTime`]);
41+
if (resetTime !== undefined) window.resetTime = resetTime;
42+
43+
// Console rule: the usage rate only exists with a positive total and a used value.
44+
if (usedQuota !== undefined && totalQuota !== undefined && totalQuota > 0) {
45+
window.percentage = usedQuota / totalQuota;
46+
}
47+
return window;
48+
}
49+
50+
/** Pick the first VALID instance's quota info, mirroring the Coding Plan console. */
51+
function readUsage(result: unknown): CodingPlanUsage | undefined {
52+
const response = unwrapResponse(result as Record<string, unknown>);
53+
const instances = Array.isArray(response.codingPlanInstanceInfos)
54+
? (response.codingPlanInstanceInfos as Record<string, unknown>[])
55+
: [];
56+
const validInstance = instances.find((instance) => instance.status === "VALID");
57+
if (!validInstance) return undefined;
58+
59+
const quotaInfo = validInstance.codingPlanQuotaInfo as Record<string, unknown> | undefined;
60+
const usage: CodingPlanUsage = {
61+
per5Hour: readWindow(quotaInfo, "per5Hour"),
62+
perWeek: readWindow(quotaInfo, "perWeek"),
63+
perBillMonth: readWindow(quotaInfo, "perBillMonth"),
64+
};
65+
if (typeof validInstance.instanceType === "string" && validInstance.instanceType) {
66+
usage.instanceType = validInstance.instanceType;
67+
}
68+
return usage;
69+
}
70+
71+
function toSection(label: string, window: CodingPlanWindow): QuotaSection {
72+
const section: QuotaSection = {
73+
label,
74+
emptyMessage: "No quota data for this window; verify in the Bailian Coding Plan console.",
75+
percentage: window.percentage,
76+
resetTime: window.resetTime,
77+
};
78+
if (window.usedQuota !== undefined && window.totalQuota !== undefined) {
79+
section.detail = `Used: ${formatNumber(window.usedQuota)} / ${formatNumber(window.totalQuota)}`;
80+
}
81+
return section;
82+
}
83+
84+
function printView(usage: CodingPlanUsage, generatedAt: number): void {
85+
const planSuffix = usage.instanceType ? ` (${usage.instanceType})` : "";
86+
printQuotaBox(
87+
`Coding Plan Usage${planSuffix}`,
88+
[
89+
toSection("5-hour quota", usage.per5Hour),
90+
toSection("1-week quota", usage.perWeek),
91+
toSection("Monthly quota", usage.perBillMonth),
92+
],
93+
generatedAt,
94+
);
95+
}
96+
97+
export default defineCommand({
98+
description: "Show Coding Plan quota usage",
99+
auth: "console",
100+
usageArgs: "[flags]",
101+
exampleArgs: ["", "--output json"],
102+
async run(ctx) {
103+
const { settings } = ctx;
104+
const format = detectOutputFormat(settings.output);
105+
const requestData = {
106+
queryCodingPlanInstanceInfoRequest: {
107+
commodityCode: COMMODITY_CODES[settings.consoleSite ?? "domestic"],
108+
onlyLatestOne: true,
109+
},
110+
};
111+
112+
if (settings.dryRun) {
113+
emitResult({ api: CODING_PLAN_USAGE_API, data: requestData }, format);
114+
return;
115+
}
116+
117+
const result = await ctx.client.console(CODING_PLAN_USAGE_API, requestData);
118+
const usage = readUsage(result);
119+
120+
if (format === "json") {
121+
emitResult(usage ?? {}, format);
122+
return;
123+
}
124+
125+
if (!usage) {
126+
process.stdout.write("No active Coding Plan subscription found.\n");
127+
return;
128+
}
129+
130+
printView(usage, Date.now());
131+
},
132+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
ansi,
3+
displayWidth,
4+
renderGauge,
5+
type GaugeCell,
6+
type TextStyle,
7+
} from "bailian-cli-runtime";
8+
import { formatDateTime } from "./shared.ts";
9+
10+
const BOX_WIDTH = 76;
11+
12+
/** One quota window rendered inside the box: a label + usage ratio + reset time. */
13+
export interface QuotaSection {
14+
label: string;
15+
/** Shown instead of the gauge when the usage ratio is absent. */
16+
emptyMessage: string;
17+
/** Usage ratio in [0, 1]; absent means no data (possibly unlimited). */
18+
percentage?: number;
19+
resetTime?: number;
20+
/** Optional dim line under the gauge, e.g. "Used: 38 / 100". */
21+
detail?: string;
22+
}
23+
24+
/** Accept only finite numbers; anything else counts as absent (possibly unlimited). */
25+
export function readNumber(value: unknown): number | undefined {
26+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
27+
}
28+
29+
/** Match the `usage free` gauge label style: 0.1% precision, no trailing zeros. */
30+
function formatPercentage(ratio: number): string {
31+
const percent = Math.round(ratio * 1000) / 10;
32+
return `${Number.isInteger(percent) ? percent : percent.toFixed(1)}%`;
33+
}
34+
35+
function formatRemainingTime(resetTime: number, now: number): string {
36+
const remainingMs = Math.max(0, resetTime - now);
37+
const totalMinutes = Math.floor(remainingMs / 60_000);
38+
if (totalMinutes === 0) return "now";
39+
40+
const days = Math.floor(totalMinutes / (24 * 60));
41+
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
42+
const minutes = totalMinutes % 60;
43+
const parts: string[] = [];
44+
if (days > 0) parts.push(`${days}d`);
45+
if (hours > 0) parts.push(`${hours}h`);
46+
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}m`);
47+
return parts.join(" ");
48+
}
49+
50+
/** Print a bordered quota box with a title line and one gauge per section. */
51+
export function printQuotaBox(title: string, sections: QuotaSection[], generatedAt: number): void {
52+
const color = ansi(process.stdout);
53+
const writeLine = (text = "", style?: TextStyle) => {
54+
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${text}`));
55+
process.stdout.write(`│ ${style ? style(text) : text}${" ".repeat(padding)}│\n`);
56+
};
57+
// Pre-colored gauge cell: pad from the plain variant so ANSI escapes never shift the border.
58+
const writeGaugeLine = (cell: GaugeCell) => {
59+
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${cell.plain}`));
60+
process.stdout.write(`│ ${cell.colored}${" ".repeat(padding)}│\n`);
61+
};
62+
const writeQuota = (section: QuotaSection) => {
63+
writeLine(section.label, color.bold);
64+
if (section.percentage === undefined) {
65+
writeLine(section.emptyMessage, color.dim);
66+
return;
67+
}
68+
69+
const gaugeLabel = `${formatPercentage(section.percentage)} used`;
70+
writeGaugeLine(renderGauge(section.percentage * 100, gaugeLabel));
71+
if (section.detail) {
72+
writeLine(section.detail, color.dim);
73+
}
74+
if (section.resetTime === undefined) {
75+
writeLine("Resets: not applicable (no usage yet)", color.dim);
76+
return;
77+
}
78+
79+
const resetText = `Resets: ${formatDateTime(section.resetTime)} (in ${formatRemainingTime(section.resetTime, generatedAt)})`;
80+
writeLine(resetText, color.dim);
81+
};
82+
83+
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);
84+
writeLine(title, color.cyan);
85+
writeLine(`Generated at: ${formatDateTime(generatedAt)} (local time)`, color.dim);
86+
for (const section of sections) {
87+
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
88+
writeQuota(section);
89+
}
90+
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
91+
}

packages/commands/src/commands/usage/token-plan.ts

Lines changed: 21 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
2-
import {
3-
ansi,
4-
displayWidth,
5-
emitResult,
6-
type AnsiStyles,
7-
type TextStyle,
8-
} from "bailian-cli-runtime";
9-
import { formatDateTime } from "./shared.ts";
2+
import { emitResult } from "bailian-cli-runtime";
3+
import { printQuotaBox, readNumber } from "./quota-box.ts";
104

115
const TOKEN_PLAN_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
12-
const BOX_WIDTH = 76;
13-
const PROGRESS_WIDTH = 32;
146

157
interface TokenPlanUsage {
168
per5HourPercentage?: number;
@@ -19,16 +11,6 @@ interface TokenPlanUsage {
1911
per1WeekResetTime?: number;
2012
}
2113

22-
interface QuotaWindow {
23-
percentage?: number;
24-
resetTime?: number;
25-
}
26-
27-
/** Accept only finite numbers; anything else counts as absent (possibly unlimited). */
28-
function readNumber(value: unknown): number | undefined {
29-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
30-
}
31-
3214
function readUsage(result: unknown): TokenPlanUsage {
3315
const response = unwrapResponse(result as Record<string, unknown>);
3416
const usage: TokenPlanUsage = {};
@@ -45,78 +27,27 @@ function readUsage(result: unknown): TokenPlanUsage {
4527
return usage;
4628
}
4729

48-
function formatPercentage(ratio: number): string {
49-
return `${(ratio * 100).toFixed(2)}%`;
50-
}
51-
52-
function formatRemainingTime(resetTime: number, now: number): string {
53-
const remainingMs = Math.max(0, resetTime - now);
54-
const totalMinutes = Math.floor(remainingMs / 60_000);
55-
if (totalMinutes === 0) return "now";
56-
57-
const days = Math.floor(totalMinutes / (24 * 60));
58-
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
59-
const minutes = totalMinutes % 60;
60-
const parts: string[] = [];
61-
if (days > 0) parts.push(`${days}d`);
62-
if (hours > 0) parts.push(`${hours}h`);
63-
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}m`);
64-
return parts.join(" ");
65-
}
66-
67-
function progressBar(ratio: number): string {
68-
const clampedRatio = Math.min(1, Math.max(0, ratio));
69-
const filled = Math.round(clampedRatio * PROGRESS_WIDTH);
70-
return `[${"█".repeat(filled)}${"░".repeat(PROGRESS_WIDTH - filled)}]`;
71-
}
72-
73-
function progressStyle(percentage: number, color: AnsiStyles): TextStyle {
74-
if (percentage >= 0.9) return color.red;
75-
if (percentage >= 0.75) return color.yellow;
76-
return color.green;
77-
}
78-
7930
function printView(usage: TokenPlanUsage, generatedAt: number): void {
80-
const color = ansi(process.stdout);
81-
const writeLine = (text = "", style?: TextStyle) => {
82-
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${text}`));
83-
process.stdout.write(`│ ${style ? style(text) : text}${" ".repeat(padding)}│\n`);
84-
};
85-
const writeQuota = (label: string, unlimitedMessage: string, window: QuotaWindow) => {
86-
writeLine(label, color.bold);
87-
if (window.percentage === undefined) {
88-
writeLine(unlimitedMessage, color.dim);
89-
return;
90-
}
91-
92-
const percentageText = formatPercentage(window.percentage);
93-
const bar = progressBar(window.percentage);
94-
writeLine(`${percentageText} used ${bar}`, progressStyle(window.percentage, color));
95-
if (window.resetTime === undefined) {
96-
writeLine("Resets: not applicable (no usage yet)", color.dim);
97-
return;
98-
}
99-
100-
const resetText = `Resets: ${formatDateTime(window.resetTime)} (in ${formatRemainingTime(window.resetTime, generatedAt)})`;
101-
writeLine(resetText, color.dim);
102-
};
103-
104-
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);
105-
writeLine("Token Plan Usage", color.cyan);
106-
writeLine(`Generated at: ${formatDateTime(generatedAt)} (local time)`, color.dim);
107-
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
108-
writeQuota(
109-
"5-hour quota",
110-
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
111-
{ percentage: usage.per5HourPercentage, resetTime: usage.per5HourResetTime },
112-
);
113-
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
114-
writeQuota(
115-
"1-week quota",
116-
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
117-
{ percentage: usage.per1WeekPercentage, resetTime: usage.per1WeekResetTime },
31+
printQuotaBox(
32+
"Token Plan Usage",
33+
[
34+
{
35+
label: "5-hour quota",
36+
emptyMessage:
37+
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
38+
percentage: usage.per5HourPercentage,
39+
resetTime: usage.per5HourResetTime,
40+
},
41+
{
42+
label: "1-week quota",
43+
emptyMessage:
44+
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
45+
percentage: usage.per1WeekPercentage,
46+
resetTime: usage.per1WeekResetTime,
47+
},
48+
],
49+
generatedAt,
11850
);
119-
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
12051
}
12152

12253
export default defineCommand({

packages/commands/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export { default as usageFreetier } from "./commands/usage/freetier.ts";
4949
export { default as usageStats } from "./commands/usage/stats.ts";
5050
export { default as usageSummary } from "./commands/usage/summary.ts";
5151
export { default as usageTokenPlan } from "./commands/usage/token-plan.ts";
52+
export { default as usageCodingPlan } from "./commands/usage/coding-plan.ts";
5253
export { default as pipelineRun } from "./commands/pipeline/run.ts";
5354
export { default as pipelineValidate } from "./commands/pipeline/validate.ts";
5455
export { default as advisorRecommend } from "./commands/advisor/recommend.ts";

0 commit comments

Comments
 (0)