@getrouter/getrouter-cli 0.1.0 → 0.1.2
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/.serena/project.yml +84 -0
- package/CLAUDE.md +52 -0
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/README.zh-cn.md +2 -2
- package/biome.json +1 -1
- package/bun.lock +70 -98
- package/dist/bin.mjs +141 -127
- package/docs/plans/2026-01-04-remove-config-command-design.md +32 -0
- package/docs/plans/2026-01-04-remove-config-command-plan.md +129 -0
- package/package.json +6 -6
- package/src/cli.ts +2 -1
- package/src/cmd/index.ts +0 -2
- package/src/cmd/keys.ts +16 -13
- package/src/cmd/models.ts +2 -1
- package/src/core/api/pagination.ts +25 -0
- package/src/core/auth/refresh.ts +68 -0
- package/src/core/http/request.ts +71 -15
- package/src/core/http/retry.ts +68 -0
- package/src/core/interactive/keys.ts +19 -10
- package/src/core/output/usages.ts +11 -30
- package/tests/auth/refresh.test.ts +149 -0
- package/tests/cli.test.ts +20 -3
- package/tests/cmd/keys.test.ts +7 -3
- package/tests/cmd/models.test.ts +5 -2
- package/tests/cmd/usages.test.ts +5 -5
- package/tests/core/api/pagination.test.ts +87 -0
- package/tests/http/request.test.ts +157 -0
- package/tests/http/retry.test.ts +152 -0
- package/tests/output/usages.test.ts +11 -12
- package/tsconfig.json +2 -1
- package/src/cmd/config-helpers.ts +0 -16
- package/src/cmd/config.ts +0 -31
- package/tests/cmd/config-helpers.test.ts +0 -18
- package/tests/cmd/config.test.ts +0 -56
package/dist/bin.mjs
CHANGED
|
@@ -7,6 +7,10 @@ import { execSync, spawn } from "node:child_process";
|
|
|
7
7
|
import { randomInt } from "node:crypto";
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
|
+
//#region package.json
|
|
11
|
+
var version = "0.1.2";
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
10
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
11
15
|
function createSubscriptionServiceClient(handler) {
|
|
12
16
|
return { CurrentSubscription(request) {
|
|
@@ -232,7 +236,6 @@ const readConfig = () => ({
|
|
|
232
236
|
...defaultConfig(),
|
|
233
237
|
...readJsonFile(getConfigPath()) ?? {}
|
|
234
238
|
});
|
|
235
|
-
const writeConfig = (cfg) => writeJsonFile(getConfigPath(), cfg);
|
|
236
239
|
const readAuth = () => ({
|
|
237
240
|
...defaultAuthState(),
|
|
238
241
|
...readJsonFile(getAuthPath()) ?? {}
|
|
@@ -243,6 +246,39 @@ const writeAuth = (auth) => {
|
|
|
243
246
|
if (process.platform !== "win32") fs.chmodSync(authPath, 384);
|
|
244
247
|
};
|
|
245
248
|
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/core/http/url.ts
|
|
251
|
+
const getApiBase = () => {
|
|
252
|
+
return (readConfig().apiBase || "").replace(/\/+$/, "");
|
|
253
|
+
};
|
|
254
|
+
const buildApiUrl = (path$1) => {
|
|
255
|
+
const base = getApiBase();
|
|
256
|
+
const normalized = path$1.replace(/^\/+/, "");
|
|
257
|
+
return base ? `${base}/${normalized}` : `/${normalized}`;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/core/auth/refresh.ts
|
|
262
|
+
const EXPIRY_BUFFER_MS = 60 * 1e3;
|
|
263
|
+
const refreshAccessToken = async ({ fetchImpl }) => {
|
|
264
|
+
const auth = readAuth();
|
|
265
|
+
if (!auth.refreshToken) return null;
|
|
266
|
+
const res = await (fetchImpl ?? fetch)(buildApiUrl("v1/dashboard/auth/token"), {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { "Content-Type": "application/json" },
|
|
269
|
+
body: JSON.stringify({ refreshToken: auth.refreshToken })
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) return null;
|
|
272
|
+
const token = await res.json();
|
|
273
|
+
if (token.accessToken && token.refreshToken) writeAuth({
|
|
274
|
+
accessToken: token.accessToken,
|
|
275
|
+
refreshToken: token.refreshToken,
|
|
276
|
+
expiresAt: token.expiresAt ?? "",
|
|
277
|
+
tokenType: "Bearer"
|
|
278
|
+
});
|
|
279
|
+
return token;
|
|
280
|
+
};
|
|
281
|
+
|
|
246
282
|
//#endregion
|
|
247
283
|
//#region src/core/http/errors.ts
|
|
248
284
|
const createApiError = (payload, fallbackMessage, status) => {
|
|
@@ -256,33 +292,71 @@ const createApiError = (payload, fallbackMessage, status) => {
|
|
|
256
292
|
};
|
|
257
293
|
|
|
258
294
|
//#endregion
|
|
259
|
-
//#region src/core/http/
|
|
260
|
-
const
|
|
261
|
-
|
|
295
|
+
//#region src/core/http/retry.ts
|
|
296
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
297
|
+
const isRetryableError = (error) => {
|
|
298
|
+
if (error instanceof TypeError) return true;
|
|
299
|
+
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") {
|
|
300
|
+
const status = error.status;
|
|
301
|
+
return status >= 500 || status === 408 || status === 429;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
262
304
|
};
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
305
|
+
const withRetry = async (fn, options = {}) => {
|
|
306
|
+
const { maxRetries = 3, initialDelayMs = 1e3, maxDelayMs = 1e4, shouldRetry = isRetryableError, onRetry, sleep = defaultSleep } = options;
|
|
307
|
+
let lastError;
|
|
308
|
+
let delay = initialDelayMs;
|
|
309
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
310
|
+
return await fn();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
lastError = error;
|
|
313
|
+
if (attempt >= maxRetries || !shouldRetry(error, attempt)) throw error;
|
|
314
|
+
onRetry?.(error, attempt + 1, delay);
|
|
315
|
+
await sleep(delay);
|
|
316
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
317
|
+
}
|
|
318
|
+
throw lastError;
|
|
267
319
|
};
|
|
320
|
+
const isServerError = (status) => status >= 500 || status === 408 || status === 429;
|
|
268
321
|
|
|
269
322
|
//#endregion
|
|
270
323
|
//#region src/core/http/request.ts
|
|
271
324
|
const getAuthCookieName = () => process.env.GETROUTER_AUTH_COOKIE || process.env.KRATOS_AUTH_COOKIE || "access_token";
|
|
272
|
-
const
|
|
325
|
+
const buildHeaders = (accessToken) => {
|
|
273
326
|
const headers = { "Content-Type": "application/json" };
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
headers.
|
|
277
|
-
headers.Cookie = `${getAuthCookieName()}=${auth.accessToken}`;
|
|
327
|
+
if (accessToken) {
|
|
328
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
329
|
+
headers.Cookie = `${getAuthCookieName()}=${accessToken}`;
|
|
278
330
|
}
|
|
279
|
-
|
|
331
|
+
return headers;
|
|
332
|
+
};
|
|
333
|
+
const doFetch = async (url, method, headers, body, fetchImpl) => {
|
|
334
|
+
return (fetchImpl ?? fetch)(url, {
|
|
280
335
|
method,
|
|
281
336
|
headers,
|
|
282
337
|
body: body == null ? void 0 : JSON.stringify(body)
|
|
283
338
|
});
|
|
284
|
-
|
|
285
|
-
|
|
339
|
+
};
|
|
340
|
+
const shouldRetryResponse = (error) => {
|
|
341
|
+
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return isServerError(error.status);
|
|
342
|
+
return error instanceof TypeError;
|
|
343
|
+
};
|
|
344
|
+
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, _retrySleep }) => {
|
|
345
|
+
return withRetry(async () => {
|
|
346
|
+
const auth = readAuth();
|
|
347
|
+
const url = buildApiUrl(path$1);
|
|
348
|
+
let res = await doFetch(url, method, buildHeaders(auth.accessToken), body, fetchImpl);
|
|
349
|
+
if (res.status === 401 && auth.refreshToken) {
|
|
350
|
+
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
351
|
+
if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
|
|
352
|
+
}
|
|
353
|
+
if (!res.ok) throw createApiError(await res.json().catch(() => null), res.statusText, res.status);
|
|
354
|
+
return await res.json();
|
|
355
|
+
}, {
|
|
356
|
+
maxRetries,
|
|
357
|
+
shouldRetry: shouldRetryResponse,
|
|
358
|
+
sleep: _retrySleep
|
|
359
|
+
});
|
|
286
360
|
};
|
|
287
361
|
|
|
288
362
|
//#endregion
|
|
@@ -421,6 +495,28 @@ const registerAuthCommands = (program) => {
|
|
|
421
495
|
});
|
|
422
496
|
};
|
|
423
497
|
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/core/api/pagination.ts
|
|
500
|
+
/**
|
|
501
|
+
* Fetches all pages from a paginated API endpoint.
|
|
502
|
+
*
|
|
503
|
+
* @param fetchPage - Function that fetches a single page given a pageToken
|
|
504
|
+
* @param getItems - Function that extracts items from the response
|
|
505
|
+
* @param getNextToken - Function that extracts the next page token from the response
|
|
506
|
+
* @returns Array of all items across all pages
|
|
507
|
+
*/
|
|
508
|
+
const fetchAllPages = async (fetchPage, getItems, getNextToken) => {
|
|
509
|
+
const allItems = [];
|
|
510
|
+
let pageToken;
|
|
511
|
+
do {
|
|
512
|
+
const response = await fetchPage(pageToken);
|
|
513
|
+
const items = getItems(response);
|
|
514
|
+
allItems.push(...items);
|
|
515
|
+
pageToken = getNextToken(response);
|
|
516
|
+
} while (pageToken);
|
|
517
|
+
return allItems;
|
|
518
|
+
};
|
|
519
|
+
|
|
424
520
|
//#endregion
|
|
425
521
|
//#region src/core/interactive/fuzzy.ts
|
|
426
522
|
const normalize = (value) => value.toLowerCase();
|
|
@@ -505,10 +601,10 @@ const promptKeyEnabled = async (initial) => {
|
|
|
505
601
|
return typeof response.enabled === "boolean" ? response.enabled : initial;
|
|
506
602
|
};
|
|
507
603
|
const selectConsumer = async (consumerService) => {
|
|
508
|
-
const consumers =
|
|
604
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
509
605
|
pageSize: void 0,
|
|
510
|
-
pageToken
|
|
511
|
-
}))?.consumers ?? [];
|
|
606
|
+
pageToken
|
|
607
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
512
608
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
513
609
|
const sorted = sortByCreatedAtDesc(consumers);
|
|
514
610
|
const nameCounts = buildNameCounts(sorted);
|
|
@@ -522,10 +618,10 @@ const selectConsumer = async (consumerService) => {
|
|
|
522
618
|
}) ?? null;
|
|
523
619
|
};
|
|
524
620
|
const selectConsumerList = async (consumerService, message) => {
|
|
525
|
-
const consumers =
|
|
621
|
+
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
526
622
|
pageSize: void 0,
|
|
527
|
-
pageToken
|
|
528
|
-
}))?.consumers ?? [];
|
|
623
|
+
pageToken
|
|
624
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
529
625
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
530
626
|
const sorted = sortByCreatedAtDesc(consumers);
|
|
531
627
|
const nameCounts = buildNameCounts(sorted);
|
|
@@ -945,8 +1041,7 @@ const registerCodexCommand = (program) => {
|
|
|
945
1041
|
const apiKey = (await consumerService.GetConsumer({ id: selected.id }))?.apiKey ?? "";
|
|
946
1042
|
if (!apiKey) throw new Error("API key not found. Please create one or choose another.");
|
|
947
1043
|
const reasoningValue = mapReasoningValue(reasoningId);
|
|
948
|
-
const keyName = selected.name
|
|
949
|
-
const keyId = selected.id ?? "-";
|
|
1044
|
+
const keyName = selected.name?.trim() || "(unnamed)";
|
|
950
1045
|
if (!(await prompts({
|
|
951
1046
|
type: "confirm",
|
|
952
1047
|
name: "confirm",
|
|
@@ -955,7 +1050,7 @@ const registerCodexCommand = (program) => {
|
|
|
955
1050
|
`Model: ${model}`,
|
|
956
1051
|
`Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`,
|
|
957
1052
|
"Provider: getrouter",
|
|
958
|
-
`Key: ${keyName}
|
|
1053
|
+
`Key: ${keyName}`
|
|
959
1054
|
].join("\n"),
|
|
960
1055
|
initial: true
|
|
961
1056
|
})).confirm) return;
|
|
@@ -975,65 +1070,6 @@ const registerCodexCommand = (program) => {
|
|
|
975
1070
|
});
|
|
976
1071
|
};
|
|
977
1072
|
|
|
978
|
-
//#endregion
|
|
979
|
-
//#region src/cmd/config-helpers.ts
|
|
980
|
-
const normalizeApiBase = (value) => value.trim().replace(/\/+$/, "");
|
|
981
|
-
const parseConfigValue = (key, raw) => {
|
|
982
|
-
if (key === "apiBase") {
|
|
983
|
-
const normalized = normalizeApiBase(raw);
|
|
984
|
-
if (!/^https?:\/\//.test(normalized)) throw new Error("apiBase must start with http:// or https://");
|
|
985
|
-
return normalized;
|
|
986
|
-
}
|
|
987
|
-
const lowered = raw.toLowerCase();
|
|
988
|
-
if (["true", "1"].includes(lowered)) return true;
|
|
989
|
-
if (["false", "0"].includes(lowered)) return false;
|
|
990
|
-
throw new Error("json must be true/false or 1/0");
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
//#endregion
|
|
994
|
-
//#region src/cmd/config.ts
|
|
995
|
-
const VALID_KEYS = new Set(["apiBase", "json"]);
|
|
996
|
-
const registerConfigCommands = (program) => {
|
|
997
|
-
program.command("config").description("Manage CLI config").argument("[key]").argument("[value]").action((key, value) => {
|
|
998
|
-
const cfg = readConfig();
|
|
999
|
-
if (!key) {
|
|
1000
|
-
console.log(`apiBase=${cfg.apiBase}`);
|
|
1001
|
-
console.log(`json=${cfg.json}`);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (!value) throw new Error("Missing config value");
|
|
1005
|
-
if (!VALID_KEYS.has(key)) throw new Error("Unknown config key");
|
|
1006
|
-
const parsed = parseConfigValue(key, value);
|
|
1007
|
-
const next = {
|
|
1008
|
-
...cfg,
|
|
1009
|
-
[key]: parsed
|
|
1010
|
-
};
|
|
1011
|
-
writeConfig(next);
|
|
1012
|
-
console.log(`${key}=${next[key]}`);
|
|
1013
|
-
});
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
//#endregion
|
|
1017
|
-
//#region src/core/config/redact.ts
|
|
1018
|
-
const SECRET_KEYS = new Set([
|
|
1019
|
-
"accessToken",
|
|
1020
|
-
"refreshToken",
|
|
1021
|
-
"apiKey"
|
|
1022
|
-
]);
|
|
1023
|
-
const mask = (value) => {
|
|
1024
|
-
if (!value) return "";
|
|
1025
|
-
if (value.length <= 8) return "****";
|
|
1026
|
-
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
1027
|
-
};
|
|
1028
|
-
const redactSecrets = (obj) => {
|
|
1029
|
-
const out = { ...obj };
|
|
1030
|
-
for (const key of Object.keys(out)) {
|
|
1031
|
-
const value = out[key];
|
|
1032
|
-
if (SECRET_KEYS.has(key) && typeof value === "string") out[key] = mask(value);
|
|
1033
|
-
}
|
|
1034
|
-
return out;
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
1073
|
//#endregion
|
|
1038
1074
|
//#region src/core/output/table.ts
|
|
1039
1075
|
const truncate = (value, max) => {
|
|
@@ -1072,13 +1108,12 @@ const consumerRow = (consumer) => [
|
|
|
1072
1108
|
String(consumer.apiKey ?? "")
|
|
1073
1109
|
];
|
|
1074
1110
|
const outputConsumerTable = (consumer) => {
|
|
1075
|
-
console.log(renderTable(consumerHeaders, [consumerRow(consumer)]));
|
|
1111
|
+
console.log(renderTable(consumerHeaders, [consumerRow(consumer)], { maxColWidth: 64 }));
|
|
1076
1112
|
};
|
|
1077
1113
|
const outputConsumers = (consumers) => {
|
|
1078
1114
|
const rows = consumers.map(consumerRow);
|
|
1079
|
-
console.log(renderTable(consumerHeaders, rows));
|
|
1115
|
+
console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
|
|
1080
1116
|
};
|
|
1081
|
-
const redactConsumer = (consumer) => redactSecrets(consumer);
|
|
1082
1117
|
const requireInteractive = (message) => {
|
|
1083
1118
|
if (!process.stdin.isTTY) throw new Error(message);
|
|
1084
1119
|
};
|
|
@@ -1097,10 +1132,10 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
|
1097
1132
|
});
|
|
1098
1133
|
};
|
|
1099
1134
|
const listConsumers = async (consumerService) => {
|
|
1100
|
-
outputConsumers(((
|
|
1135
|
+
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1101
1136
|
pageSize: void 0,
|
|
1102
|
-
pageToken
|
|
1103
|
-
}))?.consumers ?? []
|
|
1137
|
+
pageToken
|
|
1138
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0));
|
|
1104
1139
|
};
|
|
1105
1140
|
const resolveConsumerForUpdate = async (consumerService, id) => {
|
|
1106
1141
|
if (id) return consumerService.GetConsumer({ id });
|
|
@@ -1125,7 +1160,7 @@ const updateConsumerById = async (consumerService, id) => {
|
|
|
1125
1160
|
requireInteractiveForAction("update");
|
|
1126
1161
|
const selected = await resolveConsumerForUpdate(consumerService, id);
|
|
1127
1162
|
if (!selected?.id) return;
|
|
1128
|
-
outputConsumerTable(
|
|
1163
|
+
outputConsumerTable(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true)));
|
|
1129
1164
|
};
|
|
1130
1165
|
const deleteConsumerById = async (consumerService, id) => {
|
|
1131
1166
|
requireInteractiveForAction("delete");
|
|
@@ -1133,7 +1168,7 @@ const deleteConsumerById = async (consumerService, id) => {
|
|
|
1133
1168
|
if (!selected?.id) return;
|
|
1134
1169
|
if (!await confirmDelete(selected)) return;
|
|
1135
1170
|
await consumerService.DeleteConsumer({ id: selected.id });
|
|
1136
|
-
outputConsumerTable(
|
|
1171
|
+
outputConsumerTable(selected);
|
|
1137
1172
|
};
|
|
1138
1173
|
const registerKeysCommands = (program) => {
|
|
1139
1174
|
const keys = program.command("keys").description("Manage API keys");
|
|
@@ -1163,12 +1198,14 @@ const registerKeysCommands = (program) => {
|
|
|
1163
1198
|
//#endregion
|
|
1164
1199
|
//#region src/cmd/models.ts
|
|
1165
1200
|
const modelHeaders = [
|
|
1201
|
+
"ID",
|
|
1166
1202
|
"NAME",
|
|
1167
1203
|
"AUTHOR",
|
|
1168
1204
|
"ENABLED",
|
|
1169
1205
|
"UPDATED_AT"
|
|
1170
1206
|
];
|
|
1171
1207
|
const modelRow = (model) => [
|
|
1208
|
+
String(model.id ?? ""),
|
|
1172
1209
|
String(model.name ?? ""),
|
|
1173
1210
|
String(model.author ?? ""),
|
|
1174
1211
|
String(model.enabled ?? ""),
|
|
@@ -1261,8 +1298,7 @@ const registerStatusCommand = (program) => {
|
|
|
1261
1298
|
|
|
1262
1299
|
//#endregion
|
|
1263
1300
|
//#region src/core/output/usages.ts
|
|
1264
|
-
const
|
|
1265
|
-
const OUTPUT_BLOCK = "▒";
|
|
1301
|
+
const TOTAL_BLOCK = "█";
|
|
1266
1302
|
const DEFAULT_WIDTH = 24;
|
|
1267
1303
|
const formatTokens = (value) => {
|
|
1268
1304
|
const abs = Math.abs(value);
|
|
@@ -1293,46 +1329,25 @@ const renderUsageChart = (rows, width = DEFAULT_WIDTH) => {
|
|
|
1293
1329
|
const header = "📊 Usage (last 7 days) · Tokens";
|
|
1294
1330
|
if (rows.length === 0) return `${header}\n\nNo usage data available.`;
|
|
1295
1331
|
const normalized = rows.map((row) => {
|
|
1296
|
-
const
|
|
1297
|
-
const
|
|
1298
|
-
const safeInput = Number.isFinite(input) ? input : 0;
|
|
1299
|
-
const safeOutput = Number.isFinite(output) ? output : 0;
|
|
1332
|
+
const total = Number(row.totalTokens);
|
|
1333
|
+
const safeTotal = Number.isFinite(total) ? total : 0;
|
|
1300
1334
|
return {
|
|
1301
1335
|
day: row.day,
|
|
1302
|
-
|
|
1303
|
-
output: safeOutput,
|
|
1304
|
-
total: safeInput + safeOutput
|
|
1336
|
+
total: safeTotal
|
|
1305
1337
|
};
|
|
1306
1338
|
});
|
|
1307
1339
|
const totals = normalized.map((row) => row.total);
|
|
1308
1340
|
const maxTotal = Math.max(0, ...totals);
|
|
1309
|
-
const lines = normalized.map((row) => {
|
|
1310
|
-
const total = row.total;
|
|
1311
|
-
if (maxTotal === 0 || total === 0) return `${row.day} ${"".padEnd(width, " ")} I:0 O:0`;
|
|
1312
|
-
const scaled = Math.max(1, Math.round(total / maxTotal * width));
|
|
1313
|
-
let inputBars = Math.round(row.input / total * scaled);
|
|
1314
|
-
let outputBars = Math.max(0, scaled - inputBars);
|
|
1315
|
-
if (row.input > 0 && row.output > 0) {
|
|
1316
|
-
if (inputBars === 0) {
|
|
1317
|
-
inputBars = 1;
|
|
1318
|
-
outputBars = Math.max(0, scaled - 1);
|
|
1319
|
-
} else if (outputBars === 0) {
|
|
1320
|
-
outputBars = 1;
|
|
1321
|
-
inputBars = Math.max(0, scaled - 1);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
const bar = `${INPUT_BLOCK.repeat(inputBars)}${OUTPUT_BLOCK.repeat(outputBars)}`;
|
|
1325
|
-
const inputLabel = formatTokens(row.input);
|
|
1326
|
-
const outputLabel = formatTokens(row.output);
|
|
1327
|
-
return `${row.day} ${bar.padEnd(width, " ")} I:${inputLabel} O:${outputLabel}`;
|
|
1328
|
-
});
|
|
1329
|
-
const legend = `Legend: ${INPUT_BLOCK} input ${OUTPUT_BLOCK} output`;
|
|
1330
1341
|
return [
|
|
1331
1342
|
header,
|
|
1332
1343
|
"",
|
|
1333
|
-
...
|
|
1334
|
-
|
|
1335
|
-
|
|
1344
|
+
...normalized.map((row) => {
|
|
1345
|
+
if (maxTotal === 0 || row.total === 0) return `${row.day} ${"".padEnd(width, " ")} 0`;
|
|
1346
|
+
const scaled = Math.max(1, Math.round(row.total / maxTotal * width));
|
|
1347
|
+
const bar = TOTAL_BLOCK.repeat(scaled);
|
|
1348
|
+
const totalLabel = formatTokens(row.total);
|
|
1349
|
+
return `${row.day} ${bar.padEnd(width, " ")} ${totalLabel}`;
|
|
1350
|
+
})
|
|
1336
1351
|
].join("\n");
|
|
1337
1352
|
};
|
|
1338
1353
|
|
|
@@ -1398,7 +1413,6 @@ const registerCommands = (program) => {
|
|
|
1398
1413
|
registerAuthCommands(program);
|
|
1399
1414
|
registerCodexCommand(program);
|
|
1400
1415
|
registerClaudeCommand(program);
|
|
1401
|
-
registerConfigCommands(program);
|
|
1402
1416
|
registerKeysCommands(program);
|
|
1403
1417
|
registerModelsCommands(program);
|
|
1404
1418
|
registerStatusCommand(program);
|
|
@@ -1409,7 +1423,7 @@ const registerCommands = (program) => {
|
|
|
1409
1423
|
//#region src/cli.ts
|
|
1410
1424
|
const createProgram = () => {
|
|
1411
1425
|
const program = new Command();
|
|
1412
|
-
program.name("getrouter").description("CLI for getrouter.dev").version(
|
|
1426
|
+
program.name("getrouter").description("CLI for getrouter.dev").version(version);
|
|
1413
1427
|
registerCommands(program);
|
|
1414
1428
|
return program;
|
|
1415
1429
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Remove Config Command Design
|
|
2
|
+
|
|
3
|
+
## Goals
|
|
4
|
+
- Remove the `getrouter config` command and its subcommand behavior from the CLI.
|
|
5
|
+
- Keep config file usage via `~/.getrouter/config.json` for runtime reads/writes.
|
|
6
|
+
- Update tests and docs to reflect removal.
|
|
7
|
+
|
|
8
|
+
## Non-Goals
|
|
9
|
+
- Do not change config file format or core config read/write logic.
|
|
10
|
+
- Do not add new CLI alternatives for config editing.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
- Delete `src/cmd/config.ts` and `src/cmd/config-helpers.ts`.
|
|
14
|
+
- Remove `registerConfigCommands` from command registration.
|
|
15
|
+
- Remove/replace tests for config commands and config helpers.
|
|
16
|
+
- Update CLI entrypoint file list test to match remaining commands.
|
|
17
|
+
- Update README docs to remove `getrouter config` references.
|
|
18
|
+
|
|
19
|
+
## Behavior Changes
|
|
20
|
+
- `getrouter config` should no longer be a recognized command.
|
|
21
|
+
- Users should edit `~/.getrouter/config.json` directly to change configuration.
|
|
22
|
+
|
|
23
|
+
## Testing Plan
|
|
24
|
+
- Add a test that asserts `getrouter config` is rejected by the CLI.
|
|
25
|
+
- Remove tests that exercise config subcommands or config helper parsing.
|
|
26
|
+
- Ensure remaining test suite passes unchanged.
|
|
27
|
+
|
|
28
|
+
## Files
|
|
29
|
+
- Remove: `src/cmd/config.ts`, `src/cmd/config-helpers.ts`
|
|
30
|
+
- Modify: `src/cmd/index.ts`, `tests/cli.test.ts`
|
|
31
|
+
- Add/Modify tests: `tests/cli.test.ts` (new assertion), delete `tests/cmd/config*.test.ts`
|
|
32
|
+
- Docs: `README.md`, `README.zh-cn.md`, `README.ja.md`
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Remove Config Command Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Remove the `getrouter config` CLI command and keep configuration changes handled via `~/.getrouter/config.json`.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Delete config command entrypoints and helper parsing, update CLI registration and tests, and remove documentation references.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Commander.js, Vitest.
|
|
10
|
+
|
|
11
|
+
### Task 1: Add failing tests for config command removal
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Modify: `tests/cli.test.ts`
|
|
15
|
+
|
|
16
|
+
**Step 1: Write the failing test**
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
it("rejects removed config command", async () => {
|
|
20
|
+
const program = createProgram();
|
|
21
|
+
program.exitOverride();
|
|
22
|
+
await expect(
|
|
23
|
+
program.parseAsync(["node", "getrouter", "config"]),
|
|
24
|
+
).rejects.toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Step 2: Run test to verify it fails**
|
|
29
|
+
|
|
30
|
+
Run: `bun run test -- tests/cli.test.ts`
|
|
31
|
+
Expected: FAIL (config command still registered).
|
|
32
|
+
|
|
33
|
+
**Step 3: Commit**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git add tests/cli.test.ts
|
|
37
|
+
git commit -m "test: remove config command"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Task 2: Remove config command implementation and helpers
|
|
41
|
+
|
|
42
|
+
**Files:**
|
|
43
|
+
- Delete: `src/cmd/config.ts`
|
|
44
|
+
- Delete: `src/cmd/config-helpers.ts`
|
|
45
|
+
- Modify: `src/cmd/index.ts`
|
|
46
|
+
- Modify: `tests/cli.test.ts`
|
|
47
|
+
|
|
48
|
+
**Step 1: Remove command registration**
|
|
49
|
+
|
|
50
|
+
- Delete import and registration of `registerConfigCommands`.
|
|
51
|
+
- Update the CLI file list test to remove `config.ts` and `config-helpers.ts`.
|
|
52
|
+
|
|
53
|
+
**Step 2: Run tests to verify they pass**
|
|
54
|
+
|
|
55
|
+
Run: `bun run test -- tests/cli.test.ts`
|
|
56
|
+
Expected: PASS
|
|
57
|
+
|
|
58
|
+
**Step 3: Commit**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git add src/cmd/index.ts tests/cli.test.ts
|
|
62
|
+
git rm src/cmd/config.ts src/cmd/config-helpers.ts
|
|
63
|
+
git commit -m "feat: remove config command"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Task 3: Remove config tests
|
|
67
|
+
|
|
68
|
+
**Files:**
|
|
69
|
+
- Delete: `tests/cmd/config.test.ts`
|
|
70
|
+
- Delete: `tests/cmd/config-helpers.test.ts`
|
|
71
|
+
|
|
72
|
+
**Step 1: Remove tests**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git rm tests/cmd/config.test.ts tests/cmd/config-helpers.test.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Step 2: Run test to verify it passes**
|
|
79
|
+
|
|
80
|
+
Run: `bun run test -- tests/cmd/config.test.ts`
|
|
81
|
+
Expected: FAIL (file removed) and overall suite should pass after full test run.
|
|
82
|
+
|
|
83
|
+
**Step 3: Commit**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git add -A
|
|
87
|
+
git commit -m "test: remove config command coverage"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Task 4: Update documentation
|
|
91
|
+
|
|
92
|
+
**Files:**
|
|
93
|
+
- Modify: `README.md`
|
|
94
|
+
- Modify: `README.zh-cn.md`
|
|
95
|
+
- Modify: `README.ja.md`
|
|
96
|
+
|
|
97
|
+
**Step 1: Remove config command references**
|
|
98
|
+
|
|
99
|
+
- Remove any `getrouter config` examples.
|
|
100
|
+
- Add a short note that config is edited in `~/.getrouter/config.json`.
|
|
101
|
+
|
|
102
|
+
**Step 2: Commit**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git add README.md README.zh-cn.md README.ja.md
|
|
106
|
+
git commit -m "docs: remove config command"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Task 5: Full verification
|
|
110
|
+
|
|
111
|
+
**Step 1: Run tests**
|
|
112
|
+
|
|
113
|
+
Run: `bun run test`
|
|
114
|
+
Expected: PASS
|
|
115
|
+
|
|
116
|
+
**Step 2: Run typecheck**
|
|
117
|
+
|
|
118
|
+
Run: `bun run typecheck`
|
|
119
|
+
Expected: PASS
|
|
120
|
+
|
|
121
|
+
**Step 3: Run lint**
|
|
122
|
+
|
|
123
|
+
Run: `bun run lint`
|
|
124
|
+
Expected: PASS
|
|
125
|
+
|
|
126
|
+
**Step 4: Run format**
|
|
127
|
+
|
|
128
|
+
Run: `bun run format`
|
|
129
|
+
Expected: PASS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getrouter/getrouter-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for getrouter.dev",
|
|
6
6
|
"bin": {
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
},
|
|
23
23
|
"packageManager": "bun@1.3.5",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"commander": "^
|
|
25
|
+
"commander": "^14.0.2",
|
|
26
26
|
"prompts": "^2.4.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@biomejs/biome": "^2.3.
|
|
30
|
-
"@types/node": "^
|
|
29
|
+
"@biomejs/biome": "^2.3.11",
|
|
30
|
+
"@types/node": "^25.0.3",
|
|
31
31
|
"@types/prompts": "^2.4.9",
|
|
32
|
-
"tsdown": "^0.
|
|
32
|
+
"tsdown": "^0.18.4",
|
|
33
33
|
"tsx": "^4.19.2",
|
|
34
34
|
"typescript": "^5.7.3",
|
|
35
|
-
"vitest": "^
|
|
35
|
+
"vitest": "^4.0.16"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { version } from "../package.json";
|
|
2
3
|
import { registerCommands } from "./cmd";
|
|
3
4
|
|
|
4
5
|
export const createProgram = () => {
|
|
@@ -6,7 +7,7 @@ export const createProgram = () => {
|
|
|
6
7
|
program
|
|
7
8
|
.name("getrouter")
|
|
8
9
|
.description("CLI for getrouter.dev")
|
|
9
|
-
.version(
|
|
10
|
+
.version(version);
|
|
10
11
|
registerCommands(program);
|
|
11
12
|
return program;
|
|
12
13
|
};
|
package/src/cmd/index.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { registerAuthCommands } from "./auth";
|
|
3
3
|
import { registerClaudeCommand } from "./claude";
|
|
4
4
|
import { registerCodexCommand } from "./codex";
|
|
5
|
-
import { registerConfigCommands } from "./config";
|
|
6
5
|
import { registerKeysCommands } from "./keys";
|
|
7
6
|
import { registerModelsCommands } from "./models";
|
|
8
7
|
import { registerStatusCommand } from "./status";
|
|
@@ -12,7 +11,6 @@ export const registerCommands = (program: Command) => {
|
|
|
12
11
|
registerAuthCommands(program);
|
|
13
12
|
registerCodexCommand(program);
|
|
14
13
|
registerClaudeCommand(program);
|
|
15
|
-
registerConfigCommands(program);
|
|
16
14
|
registerKeysCommands(program);
|
|
17
15
|
registerModelsCommands(program);
|
|
18
16
|
registerStatusCommand(program);
|