@getrouter/getrouter-cli 0.1.8 → 0.1.10
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.ja.md +7 -0
- package/README.md +7 -0
- package/README.zh-cn.md +7 -0
- package/dist/bin.mjs +51 -36
- package/package.json +1 -1
- package/src/cmd/auth.ts +1 -1
- package/src/cmd/keys.ts +3 -1
- package/src/core/api/client.ts +3 -0
- package/src/core/http/request.ts +9 -3
- package/src/core/interactive/codex.ts +16 -17
- package/src/core/interactive/keys.ts +26 -10
- package/tests/cmd/keys.test.ts +34 -0
- package/tests/core/interactive/codex.test.ts +3 -4
- package/tests/http/request.test.ts +30 -0
package/README.ja.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
表示された URL をブラウザで開くと、CLI はトークンを受け取るまでポーリングします。
|
|
54
|
+
すでにログイン済みでも `getrouter login` を再実行すると、ローカルの auth.json は新しいトークンで上書きされます。
|
|
54
55
|
|
|
55
56
|
## よく使うコマンド
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ getrouter login
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
Codex の設定/認証から GetRouter の項目を削除する場合:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
書き込まれるファイル(codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml`(model + reasoning + provider 設定)
|
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Follow the printed URL in your browser, then the CLI will poll until it receives tokens.
|
|
54
|
+
Re-running `getrouter login` will overwrite the local auth state with new tokens.
|
|
54
55
|
|
|
55
56
|
## Common Commands
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ Notes:
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
To remove GetRouter entries from Codex config/auth:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
Files written (codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml` (model + reasoning + provider settings)
|
package/README.zh-cn.md
CHANGED
|
@@ -51,6 +51,7 @@ getrouter login
|
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
按提示打开浏览器完成确认,CLI 会轮询直到拿到 token。
|
|
54
|
+
即使已登录,也可再次执行 `getrouter login`,会用新 token 覆盖本地 auth.json。
|
|
54
55
|
|
|
55
56
|
## 常用命令
|
|
56
57
|
|
|
@@ -75,6 +76,12 @@ getrouter login
|
|
|
75
76
|
getrouter codex
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
如需移除 Codex 配置/认证中的 GetRouter 条目:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
getrouter codex uninstall
|
|
83
|
+
```
|
|
84
|
+
|
|
78
85
|
写入文件(codex):
|
|
79
86
|
|
|
80
87
|
- `~/.codex/config.toml`(model + reasoning + provider 设置)
|
package/dist/bin.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { randomInt } from "node:crypto";
|
|
|
8
8
|
import prompts from "prompts";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "0.1.
|
|
11
|
+
var version = "0.1.10";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -369,12 +369,15 @@ const shouldRetryResponse = (error) => {
|
|
|
369
369
|
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number") return isServerError(error.status);
|
|
370
370
|
return error instanceof TypeError;
|
|
371
371
|
};
|
|
372
|
-
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, _retrySleep }) => {
|
|
372
|
+
const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries = 3, includeAuth = true, _retrySleep }) => {
|
|
373
373
|
return withRetry(async () => {
|
|
374
|
-
const auth = readAuth()
|
|
374
|
+
const auth = includeAuth ? readAuth() : {
|
|
375
|
+
accessToken: void 0,
|
|
376
|
+
refreshToken: void 0
|
|
377
|
+
};
|
|
375
378
|
const url = buildApiUrl(path$1);
|
|
376
|
-
let res = await doFetch(url, method, buildHeaders(auth.accessToken), body, fetchImpl);
|
|
377
|
-
if (res.status === 401 && auth.refreshToken) {
|
|
379
|
+
let res = await doFetch(url, method, includeAuth ? buildHeaders(auth.accessToken) : buildHeaders(), body, fetchImpl);
|
|
380
|
+
if (includeAuth && res.status === 401 && auth.refreshToken) {
|
|
378
381
|
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
379
382
|
if (refreshed?.accessToken) res = await doFetch(url, method, buildHeaders(refreshed.accessToken), body, fetchImpl);
|
|
380
383
|
}
|
|
@@ -389,7 +392,7 @@ const requestJson = async ({ path: path$1, method, body, fetchImpl, maxRetries =
|
|
|
389
392
|
|
|
390
393
|
//#endregion
|
|
391
394
|
//#region src/core/api/client.ts
|
|
392
|
-
const createApiClients = ({ fetchImpl, clients }) => {
|
|
395
|
+
const createApiClients = ({ fetchImpl, clients, includeAuth = true }) => {
|
|
393
396
|
const factories = clients ?? {
|
|
394
397
|
createConsumerServiceClient,
|
|
395
398
|
createAuthServiceClient,
|
|
@@ -402,7 +405,8 @@ const createApiClients = ({ fetchImpl, clients }) => {
|
|
|
402
405
|
path: path$1,
|
|
403
406
|
method,
|
|
404
407
|
body: body ? JSON.parse(body) : void 0,
|
|
405
|
-
fetchImpl
|
|
408
|
+
fetchImpl,
|
|
409
|
+
includeAuth
|
|
406
410
|
});
|
|
407
411
|
};
|
|
408
412
|
return {
|
|
@@ -505,7 +509,7 @@ const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDe
|
|
|
505
509
|
//#region src/cmd/auth.ts
|
|
506
510
|
const registerAuthCommands = (program) => {
|
|
507
511
|
program.command("login").description("Login with device flow").action(async () => {
|
|
508
|
-
const { authService } = createApiClients({});
|
|
512
|
+
const { authService } = createApiClients({ includeAuth: false });
|
|
509
513
|
const authCode = generateAuthCode();
|
|
510
514
|
const url = buildLoginUrl(authCode);
|
|
511
515
|
console.log("🔐 To authenticate, visit:");
|
|
@@ -595,9 +599,19 @@ const fuzzySelect = async ({ message, choices }) => {
|
|
|
595
599
|
|
|
596
600
|
//#endregion
|
|
597
601
|
//#region src/core/interactive/keys.ts
|
|
598
|
-
const
|
|
599
|
-
const
|
|
600
|
-
return
|
|
602
|
+
const parseTimestamp = (value) => {
|
|
603
|
+
const parsed = Date.parse(value ?? "");
|
|
604
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
605
|
+
};
|
|
606
|
+
const getUpdatedAtTime = (consumer) => {
|
|
607
|
+
const updatedAt = parseTimestamp(consumer.updatedAt);
|
|
608
|
+
if (updatedAt) return updatedAt;
|
|
609
|
+
return parseTimestamp(consumer.createdAt);
|
|
610
|
+
};
|
|
611
|
+
const getDisplayTimestamp = (consumer) => consumer.updatedAt ?? consumer.createdAt ?? "-";
|
|
612
|
+
const sortConsumersByUpdatedAtDesc = (consumers) => consumers.slice().sort((a, b) => {
|
|
613
|
+
const aTime = getUpdatedAtTime(a);
|
|
614
|
+
return getUpdatedAtTime(b) - aTime;
|
|
601
615
|
});
|
|
602
616
|
const normalizeName = (consumer) => {
|
|
603
617
|
const name = consumer.name?.trim();
|
|
@@ -613,8 +627,8 @@ const buildNameCounts = (consumers) => {
|
|
|
613
627
|
};
|
|
614
628
|
const formatChoice = (consumer, nameCounts) => {
|
|
615
629
|
const name = normalizeName(consumer);
|
|
616
|
-
const
|
|
617
|
-
return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${
|
|
630
|
+
const displayTimestamp = getDisplayTimestamp(consumer);
|
|
631
|
+
return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${displayTimestamp})` : name;
|
|
618
632
|
};
|
|
619
633
|
const promptKeyName = async (initial) => {
|
|
620
634
|
const response = await prompts({
|
|
@@ -649,14 +663,18 @@ const selectConsumer = async (consumerService) => {
|
|
|
649
663
|
pageToken
|
|
650
664
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
651
665
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
652
|
-
const sorted =
|
|
666
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
653
667
|
const nameCounts = buildNameCounts(sorted);
|
|
654
668
|
return await fuzzySelect({
|
|
655
669
|
message: "🔎 Search keys",
|
|
656
670
|
choices: sorted.map((consumer) => ({
|
|
657
671
|
title: formatChoice(consumer, nameCounts),
|
|
658
672
|
value: consumer,
|
|
659
|
-
keywords: [
|
|
673
|
+
keywords: [
|
|
674
|
+
normalizeName(consumer),
|
|
675
|
+
consumer.updatedAt ?? "",
|
|
676
|
+
consumer.createdAt ?? ""
|
|
677
|
+
].filter(Boolean)
|
|
660
678
|
}))
|
|
661
679
|
}) ?? null;
|
|
662
680
|
};
|
|
@@ -666,7 +684,7 @@ const selectConsumerList = async (consumerService, message) => {
|
|
|
666
684
|
pageToken
|
|
667
685
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
668
686
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
669
|
-
const sorted =
|
|
687
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
670
688
|
const nameCounts = buildNameCounts(sorted);
|
|
671
689
|
const response = await prompts({
|
|
672
690
|
type: "select",
|
|
@@ -951,25 +969,16 @@ const getCodexModelChoices = async () => {
|
|
|
951
969
|
value: model,
|
|
952
970
|
keywords: [model, "codex"]
|
|
953
971
|
}));
|
|
954
|
-
if (remoteChoices.length > 0)
|
|
955
|
-
remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
|
|
956
|
-
return remoteChoices;
|
|
957
|
-
}
|
|
972
|
+
if (remoteChoices.length > 0) return remoteChoices.reverse();
|
|
958
973
|
} catch {}
|
|
959
974
|
return MODEL_CHOICES;
|
|
960
975
|
};
|
|
961
976
|
const REASONING_CHOICES = [
|
|
962
977
|
{
|
|
963
|
-
id: "
|
|
964
|
-
label: "
|
|
965
|
-
value: "
|
|
966
|
-
description: "
|
|
967
|
-
},
|
|
968
|
-
{
|
|
969
|
-
id: "medium",
|
|
970
|
-
label: "Medium (default)",
|
|
971
|
-
value: "medium",
|
|
972
|
-
description: "Balances speed and reasoning depth for everyday tasks"
|
|
978
|
+
id: "extra_high",
|
|
979
|
+
label: "Extra high",
|
|
980
|
+
value: "xhigh",
|
|
981
|
+
description: "Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits."
|
|
973
982
|
},
|
|
974
983
|
{
|
|
975
984
|
id: "high",
|
|
@@ -978,10 +987,16 @@ const REASONING_CHOICES = [
|
|
|
978
987
|
description: "Greater reasoning depth for complex problems"
|
|
979
988
|
},
|
|
980
989
|
{
|
|
981
|
-
id: "
|
|
982
|
-
label: "
|
|
983
|
-
value: "
|
|
984
|
-
description: "
|
|
990
|
+
id: "medium",
|
|
991
|
+
label: "Medium (default)",
|
|
992
|
+
value: "medium",
|
|
993
|
+
description: "Balances speed and reasoning depth for everyday tasks"
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
id: "low",
|
|
997
|
+
label: "Low",
|
|
998
|
+
value: "low",
|
|
999
|
+
description: "Fast responses with lighter reasoning"
|
|
985
1000
|
}
|
|
986
1001
|
];
|
|
987
1002
|
const REASONING_FUZZY_CHOICES = REASONING_CHOICES.map((choice) => ({
|
|
@@ -1326,10 +1341,10 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
|
1326
1341
|
});
|
|
1327
1342
|
};
|
|
1328
1343
|
const listConsumers = async (consumerService, showApiKey) => {
|
|
1329
|
-
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1344
|
+
outputConsumers(sortConsumersByUpdatedAtDesc(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1330
1345
|
pageSize: void 0,
|
|
1331
1346
|
pageToken
|
|
1332
|
-
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0), showApiKey);
|
|
1347
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0)), showApiKey);
|
|
1333
1348
|
};
|
|
1334
1349
|
const resolveConsumerForUpdate = async (consumerService, id) => {
|
|
1335
1350
|
if (id) return consumerService.GetConsumer({ id });
|
package/package.json
CHANGED
package/src/cmd/auth.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const registerAuthCommands = (program: Command) => {
|
|
|
14
14
|
.command("login")
|
|
15
15
|
.description("Login with device flow")
|
|
16
16
|
.action(async () => {
|
|
17
|
-
const { authService } = createApiClients({});
|
|
17
|
+
const { authService } = createApiClients({ includeAuth: false });
|
|
18
18
|
const authCode = generateAuthCode();
|
|
19
19
|
const url = buildLoginUrl(authCode);
|
|
20
20
|
console.log("🔐 To authenticate, visit:");
|
package/src/cmd/keys.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
promptKeyEnabled,
|
|
8
8
|
promptKeyName,
|
|
9
9
|
selectConsumerList,
|
|
10
|
+
sortConsumersByUpdatedAtDesc,
|
|
10
11
|
} from "../core/interactive/keys";
|
|
11
12
|
import { renderTable } from "../core/output/table";
|
|
12
13
|
import type {
|
|
@@ -103,7 +104,8 @@ const listConsumers = async (
|
|
|
103
104
|
(res) => res?.consumers ?? [],
|
|
104
105
|
(res) => res?.nextPageToken || undefined,
|
|
105
106
|
);
|
|
106
|
-
|
|
107
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
108
|
+
outputConsumers(sorted, showApiKey);
|
|
107
109
|
};
|
|
108
110
|
|
|
109
111
|
const resolveConsumerForUpdate = async (
|
package/src/core/api/client.ts
CHANGED
|
@@ -46,9 +46,11 @@ export type ApiClients = {
|
|
|
46
46
|
export const createApiClients = ({
|
|
47
47
|
fetchImpl,
|
|
48
48
|
clients,
|
|
49
|
+
includeAuth = true,
|
|
49
50
|
}: {
|
|
50
51
|
fetchImpl?: typeof fetch;
|
|
51
52
|
clients?: ClientFactories;
|
|
53
|
+
includeAuth?: boolean;
|
|
52
54
|
}): ApiClients => {
|
|
53
55
|
const factories =
|
|
54
56
|
clients ??
|
|
@@ -66,6 +68,7 @@ export const createApiClients = ({
|
|
|
66
68
|
method,
|
|
67
69
|
body: body ? JSON.parse(body) : undefined,
|
|
68
70
|
fetchImpl,
|
|
71
|
+
includeAuth,
|
|
69
72
|
});
|
|
70
73
|
};
|
|
71
74
|
|
package/src/core/http/request.ts
CHANGED
|
@@ -10,6 +10,7 @@ type RequestInput = {
|
|
|
10
10
|
body?: unknown;
|
|
11
11
|
fetchImpl?: typeof fetch;
|
|
12
12
|
maxRetries?: number;
|
|
13
|
+
includeAuth?: boolean;
|
|
13
14
|
/** For testing: override the sleep function used for retry delays */
|
|
14
15
|
_retrySleep?: (ms: number) => Promise<void>;
|
|
15
16
|
};
|
|
@@ -63,18 +64,23 @@ export const requestJson = async <T = unknown>({
|
|
|
63
64
|
body,
|
|
64
65
|
fetchImpl,
|
|
65
66
|
maxRetries = 3,
|
|
67
|
+
includeAuth = true,
|
|
66
68
|
_retrySleep,
|
|
67
69
|
}: RequestInput): Promise<T> => {
|
|
68
70
|
return withRetry(
|
|
69
71
|
async () => {
|
|
70
|
-
const auth =
|
|
72
|
+
const auth = includeAuth
|
|
73
|
+
? readAuth()
|
|
74
|
+
: { accessToken: undefined, refreshToken: undefined };
|
|
71
75
|
const url = buildApiUrl(path);
|
|
72
|
-
const headers =
|
|
76
|
+
const headers = includeAuth
|
|
77
|
+
? buildHeaders(auth.accessToken)
|
|
78
|
+
: buildHeaders();
|
|
73
79
|
|
|
74
80
|
let res = await doFetch(url, method, headers, body, fetchImpl);
|
|
75
81
|
|
|
76
82
|
// On 401, attempt token refresh and retry once
|
|
77
|
-
if (res.status === 401 && auth.refreshToken) {
|
|
83
|
+
if (includeAuth && res.status === 401 && auth.refreshToken) {
|
|
78
84
|
const refreshed = await refreshAccessToken({ fetchImpl });
|
|
79
85
|
if (refreshed?.accessToken) {
|
|
80
86
|
const newHeaders = buildHeaders(refreshed.accessToken);
|
|
@@ -48,8 +48,7 @@ export const getCodexModelChoices = async (): Promise<
|
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
50
|
if (remoteChoices.length > 0) {
|
|
51
|
-
remoteChoices.
|
|
52
|
-
return remoteChoices;
|
|
51
|
+
return remoteChoices.reverse();
|
|
53
52
|
}
|
|
54
53
|
} catch {}
|
|
55
54
|
|
|
@@ -58,16 +57,11 @@ export const getCodexModelChoices = async (): Promise<
|
|
|
58
57
|
|
|
59
58
|
export const REASONING_CHOICES: ReasoningChoice[] = [
|
|
60
59
|
{
|
|
61
|
-
id: "
|
|
62
|
-
label: "
|
|
63
|
-
value: "
|
|
64
|
-
description:
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
id: "medium",
|
|
68
|
-
label: "Medium (default)",
|
|
69
|
-
value: "medium",
|
|
70
|
-
description: "Balances speed and reasoning depth for everyday tasks",
|
|
60
|
+
id: "extra_high",
|
|
61
|
+
label: "Extra high",
|
|
62
|
+
value: "xhigh",
|
|
63
|
+
description:
|
|
64
|
+
"Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits.",
|
|
71
65
|
},
|
|
72
66
|
{
|
|
73
67
|
id: "high",
|
|
@@ -76,11 +70,16 @@ export const REASONING_CHOICES: ReasoningChoice[] = [
|
|
|
76
70
|
description: "Greater reasoning depth for complex problems",
|
|
77
71
|
},
|
|
78
72
|
{
|
|
79
|
-
id: "
|
|
80
|
-
label: "
|
|
81
|
-
value: "
|
|
82
|
-
description:
|
|
83
|
-
|
|
73
|
+
id: "medium",
|
|
74
|
+
label: "Medium (default)",
|
|
75
|
+
value: "medium",
|
|
76
|
+
description: "Balances speed and reasoning depth for everyday tasks",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "low",
|
|
80
|
+
label: "Low",
|
|
81
|
+
value: "low",
|
|
82
|
+
description: "Fast responses with lighter reasoning",
|
|
84
83
|
},
|
|
85
84
|
];
|
|
86
85
|
|
|
@@ -17,10 +17,24 @@ export type KeyMenuAction =
|
|
|
17
17
|
| "delete"
|
|
18
18
|
| "exit";
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const parseTimestamp = (value?: string) => {
|
|
21
|
+
const parsed = Date.parse(value ?? "");
|
|
22
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getUpdatedAtTime = (consumer: Consumer) => {
|
|
26
|
+
const updatedAt = parseTimestamp(consumer.updatedAt);
|
|
27
|
+
if (updatedAt) return updatedAt;
|
|
28
|
+
return parseTimestamp(consumer.createdAt);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getDisplayTimestamp = (consumer: Consumer) =>
|
|
32
|
+
consumer.updatedAt ?? consumer.createdAt ?? "-";
|
|
33
|
+
|
|
34
|
+
export const sortConsumersByUpdatedAtDesc = (consumers: Consumer[]) =>
|
|
21
35
|
consumers.slice().sort((a, b) => {
|
|
22
|
-
const aTime =
|
|
23
|
-
const bTime =
|
|
36
|
+
const aTime = getUpdatedAtTime(a);
|
|
37
|
+
const bTime = getUpdatedAtTime(b);
|
|
24
38
|
return bTime - aTime;
|
|
25
39
|
});
|
|
26
40
|
|
|
@@ -40,9 +54,9 @@ const buildNameCounts = (consumers: Consumer[]) => {
|
|
|
40
54
|
|
|
41
55
|
const formatChoice = (consumer: Consumer, nameCounts: Map<string, number>) => {
|
|
42
56
|
const name = normalizeName(consumer);
|
|
43
|
-
const
|
|
57
|
+
const displayTimestamp = getDisplayTimestamp(consumer);
|
|
44
58
|
const needsDetail = (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)";
|
|
45
|
-
return needsDetail ? `${name} (${
|
|
59
|
+
return needsDetail ? `${name} (${displayTimestamp})` : name;
|
|
46
60
|
};
|
|
47
61
|
|
|
48
62
|
export const selectKeyAction = async (): Promise<KeyMenuAction> => {
|
|
@@ -127,16 +141,18 @@ export const selectConsumer = async (
|
|
|
127
141
|
if (consumers.length === 0) {
|
|
128
142
|
throw new Error("No available API keys");
|
|
129
143
|
}
|
|
130
|
-
const sorted =
|
|
144
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
131
145
|
const nameCounts = buildNameCounts(sorted);
|
|
132
146
|
const selected = await fuzzySelect({
|
|
133
147
|
message: "🔎 Search keys",
|
|
134
148
|
choices: sorted.map((consumer) => ({
|
|
135
149
|
title: formatChoice(consumer, nameCounts),
|
|
136
150
|
value: consumer,
|
|
137
|
-
keywords: [
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
keywords: [
|
|
152
|
+
normalizeName(consumer),
|
|
153
|
+
consumer.updatedAt ?? "",
|
|
154
|
+
consumer.createdAt ?? "",
|
|
155
|
+
].filter(Boolean),
|
|
140
156
|
})),
|
|
141
157
|
});
|
|
142
158
|
return selected ?? null;
|
|
@@ -158,7 +174,7 @@ export const selectConsumerList = async (
|
|
|
158
174
|
if (consumers.length === 0) {
|
|
159
175
|
throw new Error("No available API keys");
|
|
160
176
|
}
|
|
161
|
-
const sorted =
|
|
177
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
162
178
|
const nameCounts = buildNameCounts(sorted);
|
|
163
179
|
const response = await prompts({
|
|
164
180
|
type: "select",
|
package/tests/cmd/keys.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ const mockConsumer = {
|
|
|
19
19
|
apiKey: "abcd1234WXYZ",
|
|
20
20
|
lastAccess: "2026-01-02T00:00:00Z",
|
|
21
21
|
createdAt: "2026-01-01T00:00:00Z",
|
|
22
|
+
updatedAt: "2026-01-02T00:00:00Z",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const emptyAuthService = {} as AuthService;
|
|
@@ -113,6 +114,39 @@ describe("keys command", () => {
|
|
|
113
114
|
log.mockRestore();
|
|
114
115
|
});
|
|
115
116
|
|
|
117
|
+
it("sorts keys by updatedAt desc in list output", async () => {
|
|
118
|
+
setStdinTTY(false);
|
|
119
|
+
const consumers = [
|
|
120
|
+
{
|
|
121
|
+
...mockConsumer,
|
|
122
|
+
id: "c1",
|
|
123
|
+
name: "older-key",
|
|
124
|
+
updatedAt: "2026-01-02T00:00:00Z",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
...mockConsumer,
|
|
128
|
+
id: "c2",
|
|
129
|
+
name: "newer-key",
|
|
130
|
+
updatedAt: "2026-01-03T00:00:00Z",
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
134
|
+
consumerService: {
|
|
135
|
+
ListConsumers: vi.fn().mockResolvedValue({ consumers }),
|
|
136
|
+
} as unknown as ConsumerService,
|
|
137
|
+
subscriptionService: emptySubscriptionService,
|
|
138
|
+
authService: emptyAuthService,
|
|
139
|
+
});
|
|
140
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
141
|
+
const program = createProgram();
|
|
142
|
+
await program.parseAsync(["node", "getrouter", "keys", "list"]);
|
|
143
|
+
const output = log.mock.calls.map((c) => c[0]).join("\n");
|
|
144
|
+
expect(output.indexOf("newer-key")).toBeLessThan(
|
|
145
|
+
output.indexOf("older-key"),
|
|
146
|
+
);
|
|
147
|
+
log.mockRestore();
|
|
148
|
+
});
|
|
149
|
+
|
|
116
150
|
it("rejects removed get subcommand", async () => {
|
|
117
151
|
setStdinTTY(false);
|
|
118
152
|
const program = createProgram();
|
|
@@ -25,15 +25,14 @@ describe("codex interactive helpers", () => {
|
|
|
25
25
|
ok: true,
|
|
26
26
|
status: 200,
|
|
27
27
|
json: vi.fn().mockResolvedValue({
|
|
28
|
-
models: ["
|
|
28
|
+
models: ["older-codex-model", "newer-codex-model"],
|
|
29
29
|
}),
|
|
30
30
|
});
|
|
31
31
|
vi.stubGlobal("fetch", fetchMock);
|
|
32
32
|
|
|
33
33
|
const choices = await getCodexModelChoices();
|
|
34
|
-
expect(
|
|
35
|
-
|
|
36
|
-
).toBe(true);
|
|
34
|
+
expect(choices[0]?.value).toBe("newer-codex-model");
|
|
35
|
+
expect(choices[1]?.value).toBe("older-codex-model");
|
|
37
36
|
expect(String(fetchMock.mock.calls[0]?.[0] ?? "")).toContain(
|
|
38
37
|
"/v1/dashboard/providers/models?tag=codex",
|
|
39
38
|
);
|
|
@@ -51,6 +51,36 @@ describe("requestJson", () => {
|
|
|
51
51
|
expect(headers.Cookie).toBe("access_token=t");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it("skips auth headers when includeAuth is false", async () => {
|
|
55
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
56
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(dir, "auth.json"),
|
|
59
|
+
JSON.stringify({ accessToken: "t" }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const fetchSpy = vi.fn(
|
|
63
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
64
|
+
({
|
|
65
|
+
ok: true,
|
|
66
|
+
json: async () => ({ ok: true }),
|
|
67
|
+
}) as Response,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await requestJson({
|
|
71
|
+
path: "/v1/test",
|
|
72
|
+
method: "GET",
|
|
73
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
74
|
+
includeAuth: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const call = fetchSpy.mock.calls[0] as Parameters<typeof fetch> | undefined;
|
|
78
|
+
const init = call?.[1];
|
|
79
|
+
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
80
|
+
expect(headers.Authorization).toBeUndefined();
|
|
81
|
+
expect(headers.Cookie).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
54
84
|
it("uses GETROUTER_AUTH_COOKIE when set", async () => {
|
|
55
85
|
process.env.GETROUTER_AUTH_COOKIE = "router_auth";
|
|
56
86
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|