opencode-enhancer 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +85 -5
- package/dist/cli.js.map +1 -1
- package/dist/providers/auth.d.ts +1 -1
- package/dist/providers/auth.d.ts.map +1 -1
- package/dist/providers/auth.js +142 -48
- package/dist/providers/auth.js.map +1 -1
- package/dist/providers/chutes.d.ts +1 -1
- package/dist/providers/chutes.d.ts.map +1 -1
- package/dist/providers/chutes.js +19 -16
- package/dist/providers/chutes.js.map +1 -1
- package/dist/providers/codex.d.ts +1 -1
- package/dist/providers/codex.d.ts.map +1 -1
- package/dist/providers/codex.js +60 -35
- package/dist/providers/codex.js.map +1 -1
- package/dist/providers/copilot.d.ts +1 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +96 -60
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +23 -0
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/opencode.d.ts.map +1 -1
- package/dist/providers/opencode.js +23 -10
- package/dist/providers/opencode.js.map +1 -1
- package/dist/providers/types.d.ts +10 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/zai.d.ts +1 -1
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +41 -14
- package/dist/providers/zai.js.map +1 -1
- package/dist/rotation.d.ts.map +1 -1
- package/dist/rotation.js +13 -3
- package/dist/rotation.js.map +1 -1
- package/dist/usage-command.d.ts.map +1 -1
- package/dist/usage-command.js +589 -85
- package/dist/usage-command.js.map +1 -1
- package/package.json +1 -1
package/dist/usage-command.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
// CLI usage command: formatted table output for provider usage
|
|
2
|
-
import { fetchAllUsage, allProviders } from
|
|
3
|
-
import {
|
|
2
|
+
import { fetchAllUsage, allProviders } from "./providers/index.js";
|
|
3
|
+
import { decodeJwtPayload } from "./jwt.js";
|
|
4
|
+
import { loadStore } from "./store.js";
|
|
5
|
+
import { readUsageCache, writeUsageCache } from "./usage-cache.js";
|
|
4
6
|
// ── ANSI helpers ──────────────────────────────────────────────
|
|
5
|
-
const isColorSupported = process.env.FORCE_COLOR !==
|
|
7
|
+
const isColorSupported = process.env.FORCE_COLOR !== "0" &&
|
|
6
8
|
process.env.NO_COLOR === undefined &&
|
|
7
9
|
(process.stdout.isTTY || process.env.FORCE_COLOR !== undefined);
|
|
8
10
|
const c = {
|
|
9
|
-
reset: isColorSupported ?
|
|
10
|
-
bold: isColorSupported ?
|
|
11
|
-
dim: isColorSupported ?
|
|
12
|
-
red: isColorSupported ?
|
|
13
|
-
green: isColorSupported ?
|
|
14
|
-
yellow: isColorSupported ?
|
|
15
|
-
blue: isColorSupported ?
|
|
16
|
-
cyan: isColorSupported ?
|
|
17
|
-
gray: isColorSupported ?
|
|
18
|
-
white: isColorSupported ?
|
|
11
|
+
reset: isColorSupported ? "\x1b[0m" : "",
|
|
12
|
+
bold: isColorSupported ? "\x1b[1m" : "",
|
|
13
|
+
dim: isColorSupported ? "\x1b[2m" : "",
|
|
14
|
+
red: isColorSupported ? "\x1b[31m" : "",
|
|
15
|
+
green: isColorSupported ? "\x1b[32m" : "",
|
|
16
|
+
yellow: isColorSupported ? "\x1b[33m" : "",
|
|
17
|
+
blue: isColorSupported ? "\x1b[34m" : "",
|
|
18
|
+
cyan: isColorSupported ? "\x1b[36m" : "",
|
|
19
|
+
gray: isColorSupported ? "\x1b[90m" : "",
|
|
20
|
+
white: isColorSupported ? "\x1b[37m" : "",
|
|
19
21
|
};
|
|
20
22
|
// ── Formatting helpers ────────────────────────────────────────
|
|
21
23
|
function formatDuration(ms) {
|
|
22
24
|
if (ms <= 0)
|
|
23
|
-
return
|
|
25
|
+
return "now";
|
|
24
26
|
const totalSeconds = Math.floor(ms / 1000);
|
|
25
27
|
const days = Math.floor(totalSeconds / 86400);
|
|
26
28
|
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
@@ -50,90 +52,528 @@ function buildBar(pct, width = 8) {
|
|
|
50
52
|
const filled = Math.round((pct / 100) * width);
|
|
51
53
|
const empty = width - filled;
|
|
52
54
|
const color = utilizationColor(pct);
|
|
53
|
-
return `${color}${
|
|
55
|
+
return `${color}${"█".repeat(filled)}${c.dim}${"░".repeat(empty)}${c.reset}`;
|
|
56
|
+
}
|
|
57
|
+
function stripAnsi(str) {
|
|
58
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
59
|
+
}
|
|
60
|
+
function visibleLength(str) {
|
|
61
|
+
return stripAnsi(str).length;
|
|
62
|
+
}
|
|
63
|
+
function clamp(value, min, max) {
|
|
64
|
+
return Math.min(Math.max(value, min), max);
|
|
65
|
+
}
|
|
66
|
+
function truncateText(str, maxLen) {
|
|
67
|
+
if (str.length <= maxLen)
|
|
68
|
+
return str;
|
|
69
|
+
if (maxLen <= 1)
|
|
70
|
+
return "…";
|
|
71
|
+
return `${str.slice(0, maxLen - 1)}…`;
|
|
54
72
|
}
|
|
55
73
|
function padRight(str, len) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const pad = Math.max(0, len - stripped.length);
|
|
59
|
-
return str + ' '.repeat(pad);
|
|
74
|
+
const pad = Math.max(0, len - visibleLength(str));
|
|
75
|
+
return str + " ".repeat(pad);
|
|
60
76
|
}
|
|
61
77
|
function padLeft(str, len) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
const pad = Math.max(0, len - visibleLength(str));
|
|
79
|
+
return " ".repeat(pad) + str;
|
|
80
|
+
}
|
|
81
|
+
const TABLE_INDENT = " ";
|
|
82
|
+
const COLUMN_GAP = " ";
|
|
83
|
+
const MINI_BAR_WIDTH = 4;
|
|
84
|
+
function normalizeWindowLabel(label) {
|
|
85
|
+
return label.trim().toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
function abbreviateWindowLabel(label) {
|
|
88
|
+
const normalized = normalizeWindowLabel(label);
|
|
89
|
+
if (normalized === "weekly")
|
|
90
|
+
return "wk";
|
|
91
|
+
if (normalized === "monthly")
|
|
92
|
+
return "mo";
|
|
93
|
+
return truncateText(label, 14);
|
|
94
|
+
}
|
|
95
|
+
function formatCount(value) {
|
|
96
|
+
if (!Number.isFinite(value))
|
|
97
|
+
return "0";
|
|
98
|
+
if (Math.abs(value) >= 1000) {
|
|
99
|
+
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(value);
|
|
100
|
+
}
|
|
101
|
+
if (Number.isInteger(value))
|
|
102
|
+
return String(value);
|
|
103
|
+
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(value);
|
|
104
|
+
}
|
|
105
|
+
function getUsageWindows(usage) {
|
|
106
|
+
if (usage.type !== "quotaBased")
|
|
107
|
+
return [];
|
|
108
|
+
return usage.windows.filter((window) => window.label !== "balance" && !window.label.startsWith("$"));
|
|
109
|
+
}
|
|
110
|
+
function formatMiniChart(utilization) {
|
|
111
|
+
return buildBar(utilization, MINI_BAR_WIDTH);
|
|
112
|
+
}
|
|
113
|
+
function getUsageTotals(usage) {
|
|
114
|
+
if (usage.type === "payAsYouGo") {
|
|
115
|
+
return {
|
|
116
|
+
usedLabel: `$${usage.used.toFixed(2)}`,
|
|
117
|
+
totalLabel: `$${usage.total.toFixed(2)}`,
|
|
118
|
+
remainingLabel: `$${usage.remaining.toFixed(2)}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (typeof usage.entitlement === "number" &&
|
|
122
|
+
typeof usage.remaining === "number" &&
|
|
123
|
+
usage.entitlement > 0) {
|
|
124
|
+
const remaining = Math.max(0, usage.remaining);
|
|
125
|
+
const used = usage.remaining < 0
|
|
126
|
+
? usage.entitlement + Math.abs(usage.remaining)
|
|
127
|
+
: usage.entitlement - remaining;
|
|
128
|
+
return {
|
|
129
|
+
usedLabel: formatCount(used),
|
|
130
|
+
totalLabel: formatCount(usage.entitlement),
|
|
131
|
+
remainingLabel: formatCount(remaining),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const usedPct = Math.round(usage.utilization);
|
|
135
|
+
const remainingPct = Math.max(0, 100 - usedPct);
|
|
136
|
+
return {
|
|
137
|
+
usedLabel: `${usedPct}%`,
|
|
138
|
+
totalLabel: "100%",
|
|
139
|
+
remainingLabel: `${remainingPct}%`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function hasAbsoluteQuotaTotals(usage) {
|
|
143
|
+
return (usage.type === "quotaBased" &&
|
|
144
|
+
typeof usage.entitlement === "number" &&
|
|
145
|
+
typeof usage.remaining === "number" &&
|
|
146
|
+
usage.entitlement > 0);
|
|
147
|
+
}
|
|
148
|
+
function formatUsageSummary(usage) {
|
|
149
|
+
const totals = getUsageTotals(usage);
|
|
150
|
+
const pctColor = utilizationColor(usage.utilization);
|
|
151
|
+
const parts = [
|
|
152
|
+
`${formatMiniChart(usage.utilization)} ${pctColor}${Math.round(usage.utilization)}%${c.reset}`,
|
|
153
|
+
`${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`,
|
|
154
|
+
`${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`,
|
|
155
|
+
`${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`,
|
|
156
|
+
];
|
|
157
|
+
return parts.join(` ${c.dim}·${c.reset} `);
|
|
158
|
+
}
|
|
159
|
+
function formatQuotaWindowDetails(window, usage, index) {
|
|
160
|
+
const used = Math.round(window.utilization);
|
|
161
|
+
const left = Math.max(0, 100 - used);
|
|
162
|
+
const totals = index === 0 && hasAbsoluteQuotaTotals(usage) ? getUsageTotals(usage) : undefined;
|
|
163
|
+
const parts = [
|
|
164
|
+
`${c.bold}${window.label}${c.reset}`,
|
|
165
|
+
`${formatMiniChart(window.utilization)} ${utilizationColor(window.utilization)}${used}%${c.reset} ${c.dim}used${c.reset}`,
|
|
166
|
+
];
|
|
167
|
+
if (totals) {
|
|
168
|
+
parts.push(`${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`, `${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`, `${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
parts.push(`${utilizationColor(window.utilization)}${left}%${c.reset} ${c.dim}left${c.reset}`);
|
|
172
|
+
}
|
|
173
|
+
parts.push(`${c.dim}resets ${formatResetTime(window.resetsAt)}${c.reset}`);
|
|
174
|
+
return parts.join(` ${c.dim}·${c.reset} `);
|
|
175
|
+
}
|
|
176
|
+
function formatUsageDetailLines(usage) {
|
|
177
|
+
if (usage.type === "payAsYouGo") {
|
|
178
|
+
return [
|
|
179
|
+
`${c.dim}credits${c.reset} ${c.red}$${usage.used.toFixed(2)}${c.reset} ${c.dim}used${c.reset} ${c.dim}·${c.reset} ${c.cyan}$${usage.total.toFixed(2)}${c.reset} ${c.dim}total${c.reset} ${c.dim}·${c.reset} ${c.green}$${usage.remaining.toFixed(2)}${c.reset} ${c.dim}left${c.reset}`,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
return getUsageWindows(usage).map((window, index) => formatQuotaWindowDetails(window, usage, index));
|
|
183
|
+
}
|
|
184
|
+
function getStatusLabel(status) {
|
|
185
|
+
if (status === "auth_expired")
|
|
186
|
+
return "auth expired";
|
|
187
|
+
if (status === "not_configured")
|
|
188
|
+
return "not configured";
|
|
189
|
+
return status;
|
|
190
|
+
}
|
|
191
|
+
function formatStatusCell(status, width) {
|
|
192
|
+
const label = getStatusLabel(status);
|
|
193
|
+
const color = status === "ok"
|
|
194
|
+
? c.green
|
|
195
|
+
: status === "not_configured"
|
|
196
|
+
? c.dim
|
|
197
|
+
: status === "auth_expired"
|
|
198
|
+
? c.yellow
|
|
199
|
+
: c.red;
|
|
200
|
+
return padRight(`${color}${label}${c.reset}`, width);
|
|
201
|
+
}
|
|
202
|
+
function formatMetricCell(value, width, color) {
|
|
203
|
+
return padLeft(`${color}${value}${c.reset}`, width);
|
|
204
|
+
}
|
|
205
|
+
function getPrimaryResetLabel(usage) {
|
|
206
|
+
if (!usage || usage.type !== "quotaBased")
|
|
207
|
+
return "—";
|
|
208
|
+
const resets = usage.windows
|
|
209
|
+
.filter((window) => window.resetsAt && window.resetsAt > Date.now())
|
|
210
|
+
.sort((a, b) => (a.resetsAt || 0) - (b.resetsAt || 0));
|
|
211
|
+
return resets.length > 0 ? stripAnsi(formatResetTime(resets[0].resetsAt)) : "—";
|
|
212
|
+
}
|
|
213
|
+
function getWindowTotals(window) {
|
|
214
|
+
if (typeof window.entitlement !== "number" ||
|
|
215
|
+
typeof window.remaining !== "number" ||
|
|
216
|
+
window.entitlement <= 0) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const remaining = Math.max(0, window.remaining);
|
|
220
|
+
const used = window.remaining < 0
|
|
221
|
+
? window.entitlement + Math.abs(window.remaining)
|
|
222
|
+
: window.entitlement - remaining;
|
|
223
|
+
return {
|
|
224
|
+
usedLabel: formatCount(used),
|
|
225
|
+
totalLabel: formatCount(window.entitlement),
|
|
226
|
+
remainingLabel: formatCount(remaining),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function formatWindowDetail(window) {
|
|
230
|
+
const pctUsed = Math.round(window.utilization);
|
|
231
|
+
const pctLeft = Math.max(0, 100 - pctUsed);
|
|
232
|
+
const totals = getWindowTotals(window);
|
|
233
|
+
const parts = totals
|
|
234
|
+
? [
|
|
235
|
+
`${c.red}${totals.usedLabel}${c.reset} ${c.dim}used${c.reset}`,
|
|
236
|
+
`${c.cyan}${totals.totalLabel}${c.reset} ${c.dim}total${c.reset}`,
|
|
237
|
+
`${c.green}${totals.remainingLabel}${c.reset} ${c.dim}left${c.reset}`,
|
|
238
|
+
`${utilizationColor(window.utilization)}${pctUsed}%${c.reset}`,
|
|
239
|
+
]
|
|
240
|
+
: [
|
|
241
|
+
`${utilizationColor(window.utilization)}${pctUsed}%${c.reset} ${c.dim}used${c.reset}`,
|
|
242
|
+
`${c.green}${pctLeft}%${c.reset} ${c.dim}left${c.reset}`,
|
|
243
|
+
];
|
|
244
|
+
parts.push(`${c.dim}resets ${stripAnsi(formatResetTime(window.resetsAt))}${c.reset}`);
|
|
245
|
+
return `${c.bold}${window.label}${c.reset}: ${parts.join(` ${c.dim}·${c.reset} `)}`;
|
|
246
|
+
}
|
|
247
|
+
function getResultDetailLines(result, verbose) {
|
|
248
|
+
if (result.status === "error" || result.status === "auth_expired") {
|
|
249
|
+
return result.error ? [result.error] : [];
|
|
250
|
+
}
|
|
251
|
+
if (result.status !== "ok" || !result.usage)
|
|
252
|
+
return [];
|
|
253
|
+
if (result.usage.type === "payAsYouGo") {
|
|
254
|
+
return verbose ? formatUsageDetailLines(result.usage) : [];
|
|
255
|
+
}
|
|
256
|
+
const windows = getUsageWindows(result.usage);
|
|
257
|
+
if (windows.length === 0)
|
|
258
|
+
return [];
|
|
259
|
+
if (!verbose && windows.length === 1)
|
|
260
|
+
return [];
|
|
261
|
+
return windows.map((window) => formatWindowDetail(window));
|
|
262
|
+
}
|
|
263
|
+
function buildSummaryRow(result, context, verbose, labels) {
|
|
264
|
+
const rawLabel = labels?.rawLabel;
|
|
265
|
+
const provider = labels?.displayLabel || getDisplayLabel(result, rawLabel, context);
|
|
266
|
+
const plan = getPlanLabel(result, result.usage, rawLabel, context, result.plan) || "—";
|
|
267
|
+
if (result.status !== "ok" || !result.usage) {
|
|
268
|
+
return {
|
|
269
|
+
provider,
|
|
270
|
+
status: result.status,
|
|
271
|
+
plan,
|
|
272
|
+
used: "—",
|
|
273
|
+
total: "—",
|
|
274
|
+
left: "—",
|
|
275
|
+
reset: "—",
|
|
276
|
+
details: getResultDetailLines(result, verbose),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const totals = getUsageTotals(result.usage);
|
|
280
|
+
return {
|
|
281
|
+
provider,
|
|
282
|
+
status: result.status,
|
|
283
|
+
plan,
|
|
284
|
+
used: totals.usedLabel,
|
|
285
|
+
total: totals.totalLabel,
|
|
286
|
+
left: totals.remainingLabel,
|
|
287
|
+
reset: getPrimaryResetLabel(result.usage),
|
|
288
|
+
usage: result.usage,
|
|
289
|
+
details: getResultDetailLines(result, verbose),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function getSummaryTableLayout(rows) {
|
|
293
|
+
return {
|
|
294
|
+
providerWidth: Math.max(18, ...rows.map((row) => visibleLength(row.provider)), visibleLength("Provider / Account")),
|
|
295
|
+
statusWidth: Math.max(14, ...rows.map((row) => visibleLength(getStatusLabel(row.status))), visibleLength("Status")),
|
|
296
|
+
planWidth: Math.max(10, ...rows.map((row) => visibleLength(row.plan)), visibleLength("Plan")),
|
|
297
|
+
usedWidth: Math.max(8, ...rows.map((row) => visibleLength(row.used)), visibleLength("Used")),
|
|
298
|
+
totalWidth: Math.max(8, ...rows.map((row) => visibleLength(row.total)), visibleLength("Total")),
|
|
299
|
+
leftWidth: Math.max(8, ...rows.map((row) => visibleLength(row.left)), visibleLength("Left")),
|
|
300
|
+
resetWidth: Math.max(8, ...rows.map((row) => visibleLength(row.reset)), visibleLength("Reset")),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function formatSummaryRow(row, layout) {
|
|
304
|
+
const providerCell = padRight(row.provider, layout.providerWidth);
|
|
305
|
+
const planCell = padRight(row.plan === "—" ? `${c.dim}—${c.reset}` : `${c.cyan}${row.plan}${c.reset}`, layout.planWidth);
|
|
306
|
+
const usedCell = row.status === "ok"
|
|
307
|
+
? formatMetricCell(row.used, layout.usedWidth, c.red)
|
|
308
|
+
: padLeft(`${c.dim}${row.used}${c.reset}`, layout.usedWidth);
|
|
309
|
+
const totalCell = row.status === "ok"
|
|
310
|
+
? formatMetricCell(row.total, layout.totalWidth, c.cyan)
|
|
311
|
+
: padLeft(`${c.dim}${row.total}${c.reset}`, layout.totalWidth);
|
|
312
|
+
const leftCell = row.status === "ok"
|
|
313
|
+
? formatMetricCell(row.left, layout.leftWidth, c.green)
|
|
314
|
+
: padLeft(`${c.dim}${row.left}${c.reset}`, layout.leftWidth);
|
|
315
|
+
const resetCell = padLeft(row.reset === "—" ? `${c.dim}—${c.reset}` : row.reset, layout.resetWidth);
|
|
316
|
+
return `${TABLE_INDENT}${providerCell}${COLUMN_GAP}${formatStatusCell(row.status, layout.statusWidth)}${COLUMN_GAP}${planCell}${COLUMN_GAP}${usedCell}${COLUMN_GAP}${totalCell}${COLUMN_GAP}${leftCell}${COLUMN_GAP}${resetCell}`;
|
|
317
|
+
}
|
|
318
|
+
function formatDetailLine(detail) {
|
|
319
|
+
return `${TABLE_INDENT} ${c.dim}↳${c.reset} ${detail}`;
|
|
320
|
+
}
|
|
321
|
+
function formatQuotaWindow(window, usage) {
|
|
322
|
+
const used = Math.round(window.utilization);
|
|
323
|
+
const left = Math.max(0, 100 - used);
|
|
324
|
+
const color = utilizationColor(used);
|
|
325
|
+
const label = `${c.dim}${abbreviateWindowLabel(window.label)}${c.reset}`;
|
|
326
|
+
const chart = formatMiniChart(window.utilization);
|
|
327
|
+
if (usage.type === "quotaBased" &&
|
|
328
|
+
typeof usage.remaining === "number" &&
|
|
329
|
+
typeof usage.entitlement === "number" &&
|
|
330
|
+
usage.entitlement > 0 &&
|
|
331
|
+
getUsageWindows(usage)[0] === window) {
|
|
332
|
+
return `${label} ${chart} ${c.green}${formatCount(usage.remaining)}${c.reset}${c.dim}/${c.reset}${c.cyan}${formatCount(usage.entitlement)}${c.reset}`;
|
|
333
|
+
}
|
|
334
|
+
return `${label} ${chart} ${color}${used}${c.reset}${c.dim}/${c.reset}${color}${left}${c.reset}`;
|
|
335
|
+
}
|
|
336
|
+
function formatUsageDetails(usage) {
|
|
337
|
+
return formatUsageSummary(usage);
|
|
338
|
+
}
|
|
339
|
+
function formatPlanValue(rawPlan) {
|
|
340
|
+
if (!rawPlan)
|
|
341
|
+
return undefined;
|
|
342
|
+
const trimmed = rawPlan.trim();
|
|
343
|
+
if (!trimmed)
|
|
344
|
+
return undefined;
|
|
345
|
+
const normalized = trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
346
|
+
const knownPlans = {
|
|
347
|
+
plus: "Plus",
|
|
348
|
+
pro: "Pro",
|
|
349
|
+
team: "Team",
|
|
350
|
+
business: "Business",
|
|
351
|
+
enterprise: "Enterprise",
|
|
352
|
+
free: "Free",
|
|
353
|
+
individual_pro: "Individual Pro",
|
|
354
|
+
individual_free: "Individual Free",
|
|
355
|
+
chatgpt_plus: "Plus",
|
|
356
|
+
chatgptplus: "Plus",
|
|
357
|
+
chatgpt_pro: "Pro",
|
|
358
|
+
chatgptpro: "Pro",
|
|
359
|
+
chatgpt_team: "Team",
|
|
360
|
+
chatgptteam: "Team",
|
|
361
|
+
};
|
|
362
|
+
const known = knownPlans[normalized];
|
|
363
|
+
if (known)
|
|
364
|
+
return known;
|
|
365
|
+
return trimmed
|
|
366
|
+
.split(/[_\s-]+/)
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
369
|
+
.join(" ");
|
|
370
|
+
}
|
|
371
|
+
function extractPlanFromUsage(usage) {
|
|
372
|
+
if (!usage || usage.type !== "quotaBased")
|
|
373
|
+
return undefined;
|
|
374
|
+
for (const window of usage.windows) {
|
|
375
|
+
const match = window.label.match(/\(([^()]+)\)/);
|
|
376
|
+
if (match?.[1])
|
|
377
|
+
return formatPlanValue(match[1]);
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
function extractPlanFromProviderName(result) {
|
|
382
|
+
if (result.providerId !== "copilot")
|
|
383
|
+
return undefined;
|
|
384
|
+
const match = result.providerName.match(/\(([^()]+)\)\s*$/);
|
|
385
|
+
return match?.[1] ? formatPlanValue(match[1]) : undefined;
|
|
386
|
+
}
|
|
387
|
+
function getProviderDisplayName(result) {
|
|
388
|
+
if (result.providerId === "copilot") {
|
|
389
|
+
return result.providerName.replace(/\s*\([^()]+\)\s*$/, "");
|
|
390
|
+
}
|
|
391
|
+
return result.providerName;
|
|
392
|
+
}
|
|
393
|
+
function getPlanTypeFromClaims(claims) {
|
|
394
|
+
if (!claims)
|
|
395
|
+
return undefined;
|
|
396
|
+
const auth = claims["https://api.openai.com/auth"];
|
|
397
|
+
return typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type : undefined;
|
|
398
|
+
}
|
|
399
|
+
function getCodexPlanFromAccount(account) {
|
|
400
|
+
const explicitPlan = formatPlanValue(account.planType);
|
|
401
|
+
if (explicitPlan)
|
|
402
|
+
return explicitPlan;
|
|
403
|
+
const accessTokenPlan = formatPlanValue(getPlanTypeFromClaims(decodeJwtPayload(account.accessToken)));
|
|
404
|
+
if (accessTokenPlan)
|
|
405
|
+
return accessTokenPlan;
|
|
406
|
+
const idTokenPlan = formatPlanValue(getPlanTypeFromClaims(decodeJwtPayload(account.idToken || "")));
|
|
407
|
+
if (idTokenPlan)
|
|
408
|
+
return idTokenPlan;
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
function getRenderContext() {
|
|
412
|
+
const store = loadStore();
|
|
413
|
+
const codexPlansByAlias = new Map();
|
|
414
|
+
for (const account of Object.values(store.accounts)) {
|
|
415
|
+
const plan = getCodexPlanFromAccount(account);
|
|
416
|
+
if (plan) {
|
|
417
|
+
codexPlansByAlias.set(account.alias, plan);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
activeAlias: store.activeAlias,
|
|
422
|
+
codexPlansByAlias,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function hasAccountRows(result) {
|
|
426
|
+
return Array.isArray(result.accounts) && result.accounts.length > 0;
|
|
427
|
+
}
|
|
428
|
+
function getDisplayLabel(result, rawLabel, context) {
|
|
429
|
+
const label = rawLabel || getProviderDisplayName(result);
|
|
430
|
+
if (result.providerId === "codex" && rawLabel && rawLabel === context.activeAlias) {
|
|
431
|
+
return `${rawLabel} ${c.cyan}(active)${c.reset}`;
|
|
432
|
+
}
|
|
433
|
+
return label;
|
|
434
|
+
}
|
|
435
|
+
function normalizePlanLookupLabel(label) {
|
|
436
|
+
if (!label)
|
|
437
|
+
return undefined;
|
|
438
|
+
const plain = stripAnsi(label)
|
|
439
|
+
.replace(/\s+\(active\)\s*$/, "")
|
|
440
|
+
.trim();
|
|
441
|
+
return plain || undefined;
|
|
442
|
+
}
|
|
443
|
+
function getPlanLabel(result, usage, rawLabel, context, explicitPlan) {
|
|
444
|
+
const directPlan = formatPlanValue(explicitPlan || result.plan);
|
|
445
|
+
if (directPlan)
|
|
446
|
+
return directPlan;
|
|
447
|
+
if (result.providerId === "codex") {
|
|
448
|
+
const lookupLabel = normalizePlanLookupLabel(rawLabel);
|
|
449
|
+
if (lookupLabel) {
|
|
450
|
+
return context.codexPlansByAlias.get(lookupLabel);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return extractPlanFromUsage(usage) || extractPlanFromProviderName(result);
|
|
454
|
+
}
|
|
455
|
+
function formatPlanCell(plan, width) {
|
|
456
|
+
if (!plan) {
|
|
457
|
+
return padRight(`${c.dim}—${c.reset}`, width);
|
|
458
|
+
}
|
|
459
|
+
return padRight(`${c.cyan}${plan}${c.reset}`, width);
|
|
460
|
+
}
|
|
461
|
+
function getTableLayout(results, context) {
|
|
462
|
+
const labels = results.flatMap((result) => [
|
|
463
|
+
getProviderDisplayName(result),
|
|
464
|
+
...(result.accounts?.map((acc) => ` ${getDisplayLabel(result, acc.label, context)}`) ?? []),
|
|
465
|
+
]);
|
|
466
|
+
const providerWidth = clamp(Math.max(18, ...labels.map((label) => visibleLength(label))), 18, Number.MAX_SAFE_INTEGER);
|
|
467
|
+
const planLengths = results.flatMap((result) => {
|
|
468
|
+
const plans = [];
|
|
469
|
+
if (!hasAccountRows(result)) {
|
|
470
|
+
plans.push(getPlanLabel(result, result.usage, undefined, context, result.plan));
|
|
471
|
+
}
|
|
472
|
+
for (const account of result.accounts ?? []) {
|
|
473
|
+
plans.push(getPlanLabel(result, account.usage, account.label, context, account.plan));
|
|
474
|
+
}
|
|
475
|
+
return plans.map((plan) => visibleLength(plan ?? "—"));
|
|
476
|
+
});
|
|
477
|
+
const planWidth = clamp(Math.max(10, ...planLengths), 10, Number.MAX_SAFE_INTEGER);
|
|
478
|
+
return {
|
|
479
|
+
providerWidth,
|
|
480
|
+
usageWidth: clamp(Math.max(18, ...results.flatMap((result) => {
|
|
481
|
+
const values = [];
|
|
482
|
+
if (result.usage)
|
|
483
|
+
values.push(formatUsageDetails(result.usage));
|
|
484
|
+
for (const account of result.accounts ?? []) {
|
|
485
|
+
values.push(formatUsageDetails(account.usage));
|
|
486
|
+
}
|
|
487
|
+
return values.map((value) => visibleLength(value));
|
|
488
|
+
})), 18, Number.MAX_SAFE_INTEGER),
|
|
489
|
+
planWidth,
|
|
490
|
+
};
|
|
65
491
|
}
|
|
66
492
|
// ── Format a single result row ────────────────────────────────
|
|
67
|
-
function formatResultRow(result,
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
493
|
+
function formatResultRow(result, layout, context, labels) {
|
|
494
|
+
const rawLabel = labels?.rawLabel;
|
|
495
|
+
const displayLabel = labels?.displayLabel || getDisplayLabel(result, rawLabel, context);
|
|
496
|
+
const name = padRight(displayLabel, layout.providerWidth);
|
|
497
|
+
const tablePrefix = `${TABLE_INDENT}${name}${COLUMN_GAP}`;
|
|
498
|
+
if (result.status === "not_configured") {
|
|
499
|
+
return `${TABLE_INDENT}${c.dim}${name}${COLUMN_GAP}${padRight("not configured", layout.usageWidth + layout.planWidth + COLUMN_GAP.length + 6)}${c.reset}`;
|
|
71
500
|
}
|
|
72
|
-
if (result.status ===
|
|
73
|
-
return
|
|
501
|
+
if (result.status === "auth_expired") {
|
|
502
|
+
return `${tablePrefix}${c.red}${padRight("auth expired", layout.usageWidth)}${c.reset}${COLUMN_GAP}${formatPlanCell(undefined, layout.planWidth)}${COLUMN_GAP}${c.dim}${result.error || ""}${c.reset}`;
|
|
74
503
|
}
|
|
75
|
-
if (result.status ===
|
|
76
|
-
return
|
|
504
|
+
if (result.status === "error") {
|
|
505
|
+
return `${tablePrefix}${c.red}${padRight("error", layout.usageWidth)}${c.reset}${COLUMN_GAP}${formatPlanCell(undefined, layout.planWidth)}${COLUMN_GAP}${c.dim}${result.error || ""}${c.reset}`;
|
|
77
506
|
}
|
|
78
507
|
if (!result.usage) {
|
|
79
|
-
return
|
|
508
|
+
return `${tablePrefix}${c.dim}no data${c.reset}`;
|
|
80
509
|
}
|
|
81
510
|
const usage = result.usage;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
// quotaBased
|
|
88
|
-
const bar = buildBar(usage.utilization);
|
|
89
|
-
const pctColor = utilizationColor(usage.utilization);
|
|
90
|
-
// Build usage string from windows
|
|
91
|
-
let usageStr = '';
|
|
92
|
-
if (usage.windows.length > 0) {
|
|
93
|
-
const parts = usage.windows
|
|
94
|
-
.filter(w => w.label !== 'balance' && !w.label.startsWith('$'))
|
|
95
|
-
.slice(0, 3) // Show up to 3 windows
|
|
96
|
-
.map((w) => {
|
|
97
|
-
const wColor = utilizationColor(w.utilization);
|
|
98
|
-
return `${wColor}${Math.round(w.utilization)}%${c.reset} ${c.dim}(${w.label})${c.reset}`;
|
|
99
|
-
});
|
|
100
|
-
usageStr = parts.join(' ');
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
usageStr = `${pctColor}${Math.round(usage.utilization)}%${c.reset}`;
|
|
511
|
+
const usageCell = padRight(formatUsageDetails(usage), layout.usageWidth);
|
|
512
|
+
const planCell = formatPlanCell(getPlanLabel(result, usage, rawLabel, context, result.plan), layout.planWidth);
|
|
513
|
+
if (usage.type === "payAsYouGo") {
|
|
514
|
+
return `${tablePrefix}${usageCell}${COLUMN_GAP}${planCell}${COLUMN_GAP}${c.dim}—${c.reset}`;
|
|
104
515
|
}
|
|
105
516
|
// Reset time: pick earliest reset
|
|
106
517
|
const resets = usage.windows
|
|
107
518
|
.filter((w) => w.resetsAt && w.resetsAt > Date.now())
|
|
108
519
|
.sort((a, b) => (a.resetsAt || 0) - (b.resetsAt || 0));
|
|
109
520
|
const resetStr = resets.length > 0 ? formatResetTime(resets[0].resetsAt) : `${c.dim}—${c.reset}`;
|
|
110
|
-
return
|
|
521
|
+
return `${tablePrefix}${usageCell}${COLUMN_GAP}${planCell}${COLUMN_GAP}${resetStr}`;
|
|
522
|
+
}
|
|
523
|
+
function formatProviderHeaderRow(result, layout) {
|
|
524
|
+
const name = padRight(getProviderDisplayName(result), layout.providerWidth);
|
|
525
|
+
return `${TABLE_INDENT}${c.bold}${name}${c.reset}${COLUMN_GAP}${" ".repeat(layout.usageWidth)}${COLUMN_GAP}${" ".repeat(layout.planWidth)}`;
|
|
526
|
+
}
|
|
527
|
+
function formatDetailRow(layout, detail) {
|
|
528
|
+
const emptyProvider = " ".repeat(layout.providerWidth);
|
|
529
|
+
const emptyPlan = " ".repeat(layout.planWidth);
|
|
530
|
+
return `${TABLE_INDENT}${emptyProvider}${COLUMN_GAP}${c.dim}↳${c.reset} ${detail}${COLUMN_GAP}${emptyPlan}`;
|
|
531
|
+
}
|
|
532
|
+
function formatResultDetailRows(result, layout) {
|
|
533
|
+
if (result.status !== "ok" || !result.usage)
|
|
534
|
+
return [];
|
|
535
|
+
return formatUsageDetailLines(result.usage).map((detail) => formatDetailRow(layout, detail));
|
|
111
536
|
}
|
|
112
537
|
// ── Format account sub-rows ───────────────────────────────────
|
|
113
|
-
function formatAccountRows(result) {
|
|
114
|
-
if (!result.accounts || result.accounts.length
|
|
538
|
+
function formatAccountRows(result, layout, context) {
|
|
539
|
+
if (!result.accounts || result.accounts.length === 0)
|
|
115
540
|
return [];
|
|
116
541
|
return result.accounts.map((acc) => {
|
|
117
|
-
const subLabel =
|
|
542
|
+
const subLabel = `${TABLE_INDENT}${getDisplayLabel(result, acc.label, context)}`;
|
|
118
543
|
const subResult = {
|
|
119
544
|
providerId: result.providerId,
|
|
120
|
-
providerName:
|
|
545
|
+
providerName: acc.label,
|
|
121
546
|
billingType: result.billingType,
|
|
547
|
+
plan: acc.plan,
|
|
122
548
|
status: acc.status,
|
|
123
549
|
usage: acc.usage,
|
|
124
550
|
error: acc.error,
|
|
125
551
|
fetchedAt: result.fetchedAt,
|
|
126
552
|
};
|
|
127
|
-
return formatResultRow(subResult,
|
|
553
|
+
return formatResultRow(subResult, layout, context, {
|
|
554
|
+
rawLabel: acc.label,
|
|
555
|
+
displayLabel: subLabel,
|
|
556
|
+
});
|
|
128
557
|
});
|
|
129
558
|
}
|
|
559
|
+
function isFullUsageCache(results) {
|
|
560
|
+
const expectedProviderIds = new Set(allProviders.map((provider) => provider.id));
|
|
561
|
+
if (results.length !== expectedProviderIds.size)
|
|
562
|
+
return false;
|
|
563
|
+
for (const result of results) {
|
|
564
|
+
if (!expectedProviderIds.has(result.providerId)) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
130
570
|
export async function runUsageCommand(opts) {
|
|
131
571
|
const providerIds = opts.provider ? [opts.provider] : undefined;
|
|
132
572
|
// Validate provider name
|
|
133
573
|
if (opts.provider) {
|
|
134
574
|
const valid = allProviders.map((p) => p.id);
|
|
135
575
|
if (!valid.includes(opts.provider)) {
|
|
136
|
-
console.error(`Unknown provider: ${opts.provider}\nAvailable: ${valid.join(
|
|
576
|
+
console.error(`Unknown provider: ${opts.provider}\nAvailable: ${valid.join(", ")}`);
|
|
137
577
|
process.exit(1);
|
|
138
578
|
}
|
|
139
579
|
}
|
|
@@ -141,11 +581,18 @@ export async function runUsageCommand(opts) {
|
|
|
141
581
|
if (!opts.noCache && !opts.json) {
|
|
142
582
|
const cached = readUsageCache();
|
|
143
583
|
if (cached) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
584
|
+
if (!providerIds && !isFullUsageCache(cached.results)) {
|
|
585
|
+
// Ignore stale partial caches left by older provider-scoped fetches.
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
const filtered = providerIds
|
|
589
|
+
? cached.results.filter((r) => providerIds.includes(r.providerId))
|
|
590
|
+
: cached.results;
|
|
591
|
+
if (providerIds ? filtered.length > 0 : true) {
|
|
592
|
+
renderTable(filtered, opts.verbose);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
149
596
|
}
|
|
150
597
|
}
|
|
151
598
|
// Show spinner
|
|
@@ -154,10 +601,12 @@ export async function runUsageCommand(opts) {
|
|
|
154
601
|
}
|
|
155
602
|
const results = await fetchAllUsage({ providerIds });
|
|
156
603
|
// Cache results
|
|
157
|
-
|
|
604
|
+
if (!providerIds) {
|
|
605
|
+
writeUsageCache(results);
|
|
606
|
+
}
|
|
158
607
|
// Clear spinner line
|
|
159
608
|
if (!opts.json) {
|
|
160
|
-
process.stdout.write(
|
|
609
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
161
610
|
}
|
|
162
611
|
// JSON output
|
|
163
612
|
if (opts.json) {
|
|
@@ -167,32 +616,87 @@ export async function runUsageCommand(opts) {
|
|
|
167
616
|
renderTable(results, opts.verbose);
|
|
168
617
|
}
|
|
169
618
|
function renderTable(results, verbose = false) {
|
|
170
|
-
|
|
171
|
-
const configured = results.filter((r) => r.status !==
|
|
172
|
-
const notConfigured = results.filter((r) => r.status ===
|
|
619
|
+
const context = getRenderContext();
|
|
620
|
+
const configured = results.filter((r) => r.status !== "not_configured");
|
|
621
|
+
const notConfigured = results.filter((r) => r.status === "not_configured");
|
|
622
|
+
const summaryRows = [];
|
|
623
|
+
for (const result of results) {
|
|
624
|
+
if (hasAccountRows(result)) {
|
|
625
|
+
for (const account of result.accounts ?? []) {
|
|
626
|
+
const subResult = {
|
|
627
|
+
providerId: result.providerId,
|
|
628
|
+
providerName: account.label,
|
|
629
|
+
billingType: result.billingType,
|
|
630
|
+
plan: account.plan,
|
|
631
|
+
status: account.status,
|
|
632
|
+
usage: account.usage,
|
|
633
|
+
error: account.error,
|
|
634
|
+
fetchedAt: result.fetchedAt,
|
|
635
|
+
};
|
|
636
|
+
summaryRows.push(buildSummaryRow(subResult, context, verbose, {
|
|
637
|
+
rawLabel: account.label,
|
|
638
|
+
displayLabel: ` ${getDisplayLabel(result, account.label, context)}`,
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
summaryRows.push(buildSummaryRow(result, context, verbose));
|
|
644
|
+
}
|
|
645
|
+
const layout = getSummaryTableLayout(summaryRows);
|
|
173
646
|
console.log();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
for (const
|
|
182
|
-
|
|
647
|
+
const header = `${TABLE_INDENT}${c.bold}${padRight("Provider / Account", layout.providerWidth)}${COLUMN_GAP}${padRight("Status", layout.statusWidth)}${COLUMN_GAP}${padRight("Plan", layout.planWidth)}${COLUMN_GAP}${padLeft("Used", layout.usedWidth)}${COLUMN_GAP}${padLeft("Total", layout.totalWidth)}${COLUMN_GAP}${padLeft("Left", layout.leftWidth)}${COLUMN_GAP}${padLeft("Reset", layout.resetWidth)}${c.reset}`;
|
|
648
|
+
const divider = `${TABLE_INDENT}${c.dim}${"─".repeat(visibleLength(header) - visibleLength(TABLE_INDENT))}${c.reset}`;
|
|
649
|
+
console.log(header);
|
|
650
|
+
console.log(divider);
|
|
651
|
+
for (const [resultIndex, result] of configured.entries()) {
|
|
652
|
+
if (hasAccountRows(result)) {
|
|
653
|
+
console.log(`${TABLE_INDENT}${c.bold}${getProviderDisplayName(result)}${c.reset}`);
|
|
654
|
+
for (const account of result.accounts ?? []) {
|
|
655
|
+
const subResult = {
|
|
656
|
+
providerId: result.providerId,
|
|
657
|
+
providerName: account.label,
|
|
658
|
+
billingType: result.billingType,
|
|
659
|
+
plan: account.plan,
|
|
660
|
+
status: account.status,
|
|
661
|
+
usage: account.usage,
|
|
662
|
+
error: account.error,
|
|
663
|
+
fetchedAt: result.fetchedAt,
|
|
664
|
+
};
|
|
665
|
+
const row = buildSummaryRow(subResult, context, verbose, {
|
|
666
|
+
rawLabel: account.label,
|
|
667
|
+
displayLabel: ` ${getDisplayLabel(result, account.label, context)}`,
|
|
668
|
+
});
|
|
669
|
+
console.log(formatSummaryRow(row, layout));
|
|
670
|
+
for (const detail of row.details) {
|
|
671
|
+
console.log(formatDetailLine(detail));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (resultIndex < configured.length - 1) {
|
|
675
|
+
console.log();
|
|
183
676
|
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const row = buildSummaryRow(result, context, verbose);
|
|
680
|
+
console.log(formatSummaryRow(row, layout));
|
|
681
|
+
for (const detail of row.details) {
|
|
682
|
+
console.log(formatDetailLine(detail));
|
|
683
|
+
}
|
|
684
|
+
if (resultIndex < configured.length - 1) {
|
|
685
|
+
console.log();
|
|
184
686
|
}
|
|
185
687
|
}
|
|
186
688
|
if (notConfigured.length > 0) {
|
|
187
|
-
|
|
689
|
+
if (configured.length > 0) {
|
|
690
|
+
console.log(divider);
|
|
691
|
+
}
|
|
188
692
|
for (const result of notConfigured) {
|
|
189
|
-
|
|
693
|
+
const row = buildSummaryRow(result, context, verbose);
|
|
694
|
+
console.log(formatSummaryRow(row, layout));
|
|
190
695
|
}
|
|
191
696
|
}
|
|
192
|
-
console.log(
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
const errCount = configured.filter((r) => r.status === 'error' || r.status === 'auth_expired').length;
|
|
697
|
+
console.log(divider);
|
|
698
|
+
const okCount = configured.filter((r) => r.status === "ok").length;
|
|
699
|
+
const errCount = configured.filter((r) => r.status === "error" || r.status === "auth_expired").length;
|
|
196
700
|
const notConfCount = notConfigured.length;
|
|
197
701
|
const parts = [];
|
|
198
702
|
if (okCount > 0)
|