@getrouter/getrouter-cli 0.1.9 → 0.1.11
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 +8 -0
- package/README.md +16 -0
- package/README.zh-cn.md +8 -0
- package/dist/bin.mjs +216 -78
- package/package.json +1 -1
- package/src/cmd/codex.ts +112 -3
- package/src/cmd/keys.ts +3 -1
- package/src/core/interactive/codex.ts +16 -17
- package/src/core/interactive/keys.ts +26 -10
- package/src/core/setup/codex.ts +200 -47
- package/tests/cmd/codex.test.ts +100 -0
- package/tests/cmd/keys.test.ts +34 -0
- package/tests/core/interactive/codex.test.ts +3 -4
- package/tests/core/setup/codex.test.ts +87 -1
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,10 +76,17 @@ 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 設定)
|
|
81
88
|
- `~/.codex/auth.json`(OPENAI_API_KEY)
|
|
89
|
+
- `~/.getrouter/codex-backup.json`(`getrouter codex uninstall` 用のバックアップ。uninstall で削除)
|
|
82
90
|
|
|
83
91
|
`getrouter claude` は Anthropic 互換の環境変数を `~/.getrouter/env.sh`(または `env.ps1`)へ書き込みます。
|
|
84
92
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# GetRouter CLI
|
|
2
2
|
|
|
3
|
+
[](https://github.com/getrouter/getrouter-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@getrouter/getrouter-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/@getrouter/getrouter-cli)
|
|
6
|
+
[](https://www.npmjs.com/package/@getrouter/getrouter-cli)
|
|
7
|
+
[](https://bun.sh)
|
|
8
|
+
|
|
3
9
|
CLI for getrouter.dev — manage API keys, subscriptions, and configure vibecoding tools.
|
|
4
10
|
|
|
5
11
|
English | [简体中文](README.zh-cn.md) | [日本語](README.ja.md)
|
|
@@ -51,6 +57,7 @@ getrouter login
|
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
Follow the printed URL in your browser, then the CLI will poll until it receives tokens.
|
|
60
|
+
Re-running `getrouter login` will overwrite the local auth state with new tokens.
|
|
54
61
|
|
|
55
62
|
## Common Commands
|
|
56
63
|
|
|
@@ -75,10 +82,17 @@ Notes:
|
|
|
75
82
|
getrouter codex
|
|
76
83
|
```
|
|
77
84
|
|
|
85
|
+
To remove GetRouter entries from Codex config/auth:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
getrouter codex uninstall
|
|
89
|
+
```
|
|
90
|
+
|
|
78
91
|
Files written (codex):
|
|
79
92
|
|
|
80
93
|
- `~/.codex/config.toml` (model + reasoning + provider settings)
|
|
81
94
|
- `~/.codex/auth.json` (OPENAI_API_KEY)
|
|
95
|
+
- `~/.getrouter/codex-backup.json` (backup for `getrouter codex uninstall`; deleted on uninstall)
|
|
82
96
|
|
|
83
97
|
`getrouter claude` writes Anthropic-compatible env vars to `~/.getrouter/env.sh` (or `env.ps1`).
|
|
84
98
|
|
|
@@ -114,3 +128,5 @@ Edit `~/.getrouter/config.json` directly to update CLI settings.
|
|
|
114
128
|
- `bun run format` — format and lint code with Biome
|
|
115
129
|
- `bun run test` — run the test suite
|
|
116
130
|
- `bun run typecheck` — run TypeScript type checks
|
|
131
|
+
|
|
132
|
+
[](https://www.npmjs.com/package/@getrouter/getrouter-cli)
|
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,10 +76,17 @@ 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 设置)
|
|
81
88
|
- `~/.codex/auth.json`(OPENAI_API_KEY)
|
|
89
|
+
- `~/.getrouter/codex-backup.json`(用于 `getrouter codex uninstall` 的备份;卸载后会删除)
|
|
82
90
|
|
|
83
91
|
`getrouter claude` 写入 Anthropic 兼容环境变量到 `~/.getrouter/env.sh`(或 `env.ps1`)。
|
|
84
92
|
|
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
|
|
@@ -599,9 +599,19 @@ const fuzzySelect = async ({ message, choices }) => {
|
|
|
599
599
|
|
|
600
600
|
//#endregion
|
|
601
601
|
//#region src/core/interactive/keys.ts
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
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;
|
|
605
615
|
});
|
|
606
616
|
const normalizeName = (consumer) => {
|
|
607
617
|
const name = consumer.name?.trim();
|
|
@@ -617,8 +627,8 @@ const buildNameCounts = (consumers) => {
|
|
|
617
627
|
};
|
|
618
628
|
const formatChoice = (consumer, nameCounts) => {
|
|
619
629
|
const name = normalizeName(consumer);
|
|
620
|
-
const
|
|
621
|
-
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;
|
|
622
632
|
};
|
|
623
633
|
const promptKeyName = async (initial) => {
|
|
624
634
|
const response = await prompts({
|
|
@@ -653,14 +663,18 @@ const selectConsumer = async (consumerService) => {
|
|
|
653
663
|
pageToken
|
|
654
664
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
655
665
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
656
|
-
const sorted =
|
|
666
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
657
667
|
const nameCounts = buildNameCounts(sorted);
|
|
658
668
|
return await fuzzySelect({
|
|
659
669
|
message: "🔎 Search keys",
|
|
660
670
|
choices: sorted.map((consumer) => ({
|
|
661
671
|
title: formatChoice(consumer, nameCounts),
|
|
662
672
|
value: consumer,
|
|
663
|
-
keywords: [
|
|
673
|
+
keywords: [
|
|
674
|
+
normalizeName(consumer),
|
|
675
|
+
consumer.updatedAt ?? "",
|
|
676
|
+
consumer.createdAt ?? ""
|
|
677
|
+
].filter(Boolean)
|
|
664
678
|
}))
|
|
665
679
|
}) ?? null;
|
|
666
680
|
};
|
|
@@ -670,7 +684,7 @@ const selectConsumerList = async (consumerService, message) => {
|
|
|
670
684
|
pageToken
|
|
671
685
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
672
686
|
if (consumers.length === 0) throw new Error("No available API keys");
|
|
673
|
-
const sorted =
|
|
687
|
+
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
674
688
|
const nameCounts = buildNameCounts(sorted);
|
|
675
689
|
const response = await prompts({
|
|
676
690
|
type: "select",
|
|
@@ -955,25 +969,16 @@ const getCodexModelChoices = async () => {
|
|
|
955
969
|
value: model,
|
|
956
970
|
keywords: [model, "codex"]
|
|
957
971
|
}));
|
|
958
|
-
if (remoteChoices.length > 0)
|
|
959
|
-
remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
|
|
960
|
-
return remoteChoices;
|
|
961
|
-
}
|
|
972
|
+
if (remoteChoices.length > 0) return remoteChoices.reverse();
|
|
962
973
|
} catch {}
|
|
963
974
|
return MODEL_CHOICES;
|
|
964
975
|
};
|
|
965
976
|
const REASONING_CHOICES = [
|
|
966
977
|
{
|
|
967
|
-
id: "
|
|
968
|
-
label: "
|
|
969
|
-
value: "
|
|
970
|
-
description: "
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
id: "medium",
|
|
974
|
-
label: "Medium (default)",
|
|
975
|
-
value: "medium",
|
|
976
|
-
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."
|
|
977
982
|
},
|
|
978
983
|
{
|
|
979
984
|
id: "high",
|
|
@@ -982,10 +987,16 @@ const REASONING_CHOICES = [
|
|
|
982
987
|
description: "Greater reasoning depth for complex problems"
|
|
983
988
|
},
|
|
984
989
|
{
|
|
985
|
-
id: "
|
|
986
|
-
label: "
|
|
987
|
-
value: "
|
|
988
|
-
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"
|
|
989
1000
|
}
|
|
990
1001
|
];
|
|
991
1002
|
const REASONING_FUZZY_CHOICES = REASONING_CHOICES.map((choice) => ({
|
|
@@ -1000,6 +1011,15 @@ const mapReasoningValue = (id) => REASONING_CHOICES.find((choice) => choice.id =
|
|
|
1000
1011
|
//#region src/core/setup/codex.ts
|
|
1001
1012
|
const CODEX_PROVIDER = "getrouter";
|
|
1002
1013
|
const CODEX_BASE_URL = "https://api.getrouter.dev/codex";
|
|
1014
|
+
const LEGACY_TOML_ROOT_MARKERS = [
|
|
1015
|
+
"_getrouter_codex_backup_model",
|
|
1016
|
+
"_getrouter_codex_backup_model_reasoning_effort",
|
|
1017
|
+
"_getrouter_codex_backup_model_provider",
|
|
1018
|
+
"_getrouter_codex_installed_model",
|
|
1019
|
+
"_getrouter_codex_installed_model_reasoning_effort",
|
|
1020
|
+
"_getrouter_codex_installed_model_provider"
|
|
1021
|
+
];
|
|
1022
|
+
const LEGACY_AUTH_MARKERS = ["_getrouter_codex_backup_openai_api_key", "_getrouter_codex_installed_openai_api_key"];
|
|
1003
1023
|
const ROOT_KEYS = [
|
|
1004
1024
|
"model",
|
|
1005
1025
|
"model_reasoning_effort",
|
|
@@ -1025,9 +1045,56 @@ const providerValues = () => ({
|
|
|
1025
1045
|
});
|
|
1026
1046
|
const matchHeader = (line) => line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
1027
1047
|
const matchKey = (line) => line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
|
|
1028
|
-
const
|
|
1048
|
+
const parseTomlRhsValue = (rhs) => {
|
|
1049
|
+
const trimmed = rhs.trim();
|
|
1050
|
+
if (!trimmed) return "";
|
|
1051
|
+
const first = trimmed[0];
|
|
1052
|
+
if (first === "\"" || first === "'") {
|
|
1053
|
+
const end = trimmed.indexOf(first, 1);
|
|
1054
|
+
return end === -1 ? trimmed : trimmed.slice(0, end + 1);
|
|
1055
|
+
}
|
|
1056
|
+
const hashIndex = trimmed.indexOf("#");
|
|
1057
|
+
return (hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex)).trim();
|
|
1058
|
+
};
|
|
1059
|
+
const readRootValue = (lines, key) => {
|
|
1060
|
+
for (const line of lines) {
|
|
1061
|
+
if (matchHeader(line)) break;
|
|
1062
|
+
if (matchKey(line)?.[1] === key) {
|
|
1063
|
+
const parts = line.split("=");
|
|
1064
|
+
parts.shift();
|
|
1065
|
+
return parseTomlRhsValue(parts.join("="));
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
const readCodexTomlRootValues = (content) => {
|
|
1070
|
+
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
1071
|
+
return {
|
|
1072
|
+
model: readRootValue(lines, "model"),
|
|
1073
|
+
reasoning: readRootValue(lines, "model_reasoning_effort"),
|
|
1074
|
+
provider: readRootValue(lines, "model_provider")
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
const normalizeTomlString = (value) => {
|
|
1078
|
+
if (!value) return "";
|
|
1079
|
+
const trimmed = value.trim();
|
|
1080
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1).trim().toLowerCase();
|
|
1081
|
+
return trimmed.replace(/['"]/g, "").trim().toLowerCase();
|
|
1082
|
+
};
|
|
1083
|
+
const stripLegacyRootMarkers = (lines) => {
|
|
1084
|
+
const updated = [];
|
|
1085
|
+
let inRoot = true;
|
|
1086
|
+
for (const line of lines) {
|
|
1087
|
+
if (matchHeader(line)) inRoot = false;
|
|
1088
|
+
if (inRoot) {
|
|
1089
|
+
const key = matchKey(line)?.[1];
|
|
1090
|
+
if (key && LEGACY_TOML_ROOT_MARKERS.includes(key)) continue;
|
|
1091
|
+
}
|
|
1092
|
+
updated.push(line);
|
|
1093
|
+
}
|
|
1094
|
+
return updated;
|
|
1095
|
+
};
|
|
1029
1096
|
const mergeCodexToml = (content, input) => {
|
|
1030
|
-
const updated = [...content.length ? content.split(/\r?\n/) : []];
|
|
1097
|
+
const updated = [...stripLegacyRootMarkers(content.length ? content.split(/\r?\n/) : [])];
|
|
1031
1098
|
const rootValueMap = rootValues(input);
|
|
1032
1099
|
const providerValueMap = providerValues();
|
|
1033
1100
|
let currentSection = null;
|
|
@@ -1082,10 +1149,12 @@ const mergeCodexToml = (content, input) => {
|
|
|
1082
1149
|
if (missingProvider.length > 0) updated.splice(providerEnd, 0, ...missingProvider);
|
|
1083
1150
|
return updated.join("\n");
|
|
1084
1151
|
};
|
|
1085
|
-
const mergeAuthJson = (data, apiKey) =>
|
|
1086
|
-
...data
|
|
1087
|
-
|
|
1088
|
-
|
|
1152
|
+
const mergeAuthJson = (data, apiKey) => {
|
|
1153
|
+
const next = { ...data };
|
|
1154
|
+
for (const key of LEGACY_AUTH_MARKERS) if (key in next) delete next[key];
|
|
1155
|
+
next.OPENAI_API_KEY = apiKey;
|
|
1156
|
+
return next;
|
|
1157
|
+
};
|
|
1089
1158
|
const stripGetrouterProviderSection = (lines) => {
|
|
1090
1159
|
const updated = [];
|
|
1091
1160
|
let skipSection = false;
|
|
@@ -1103,61 +1172,82 @@ const stripGetrouterProviderSection = (lines) => {
|
|
|
1103
1172
|
}
|
|
1104
1173
|
return updated;
|
|
1105
1174
|
};
|
|
1106
|
-
const
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
if (currentSection === null) {
|
|
1117
|
-
if (/^\s*model\s*=/.test(line)) continue;
|
|
1118
|
-
if (/^\s*model_reasoning_effort\s*=/.test(line)) continue;
|
|
1119
|
-
if (/^\s*model_provider\s*=/.test(line)) continue;
|
|
1120
|
-
}
|
|
1121
|
-
updated.push(line);
|
|
1175
|
+
const stripLegacyMarkersFromRoot = (rootLines) => rootLines.filter((line) => {
|
|
1176
|
+
const key = matchKey(line)?.[1];
|
|
1177
|
+
return !(key && LEGACY_TOML_ROOT_MARKERS.includes(key));
|
|
1178
|
+
});
|
|
1179
|
+
const setOrDeleteRootKey = (rootLines, key, value) => {
|
|
1180
|
+
const idx = rootLines.findIndex((line) => matchKey(line)?.[1] === key);
|
|
1181
|
+
if (value === void 0) {
|
|
1182
|
+
if (idx !== -1) rootLines.splice(idx, 1);
|
|
1183
|
+
return;
|
|
1122
1184
|
}
|
|
1123
|
-
|
|
1185
|
+
if (idx !== -1) rootLines[idx] = `${key} = ${value}`;
|
|
1186
|
+
else rootLines.push(`${key} = ${value}`);
|
|
1187
|
+
};
|
|
1188
|
+
const deleteRootKey = (rootLines, key) => {
|
|
1189
|
+
setOrDeleteRootKey(rootLines, key, void 0);
|
|
1124
1190
|
};
|
|
1125
|
-
const removeCodexConfig = (content) => {
|
|
1191
|
+
const removeCodexConfig = (content, options) => {
|
|
1192
|
+
const { restoreRoot } = options ?? {};
|
|
1126
1193
|
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1194
|
+
const providerIsGetrouter = normalizeTomlString(readRootValue(lines, "model_provider")) === CODEX_PROVIDER;
|
|
1195
|
+
const stripped = stripGetrouterProviderSection(lines);
|
|
1196
|
+
const firstHeaderIndex = stripped.findIndex((line) => matchHeader(line));
|
|
1197
|
+
const rootEnd = firstHeaderIndex === -1 ? stripped.length : firstHeaderIndex;
|
|
1198
|
+
const rootLines = stripLegacyMarkersFromRoot(stripped.slice(0, rootEnd));
|
|
1199
|
+
const restLines = stripped.slice(rootEnd);
|
|
1200
|
+
if (providerIsGetrouter) if (restoreRoot) {
|
|
1201
|
+
setOrDeleteRootKey(rootLines, "model", restoreRoot.model);
|
|
1202
|
+
setOrDeleteRootKey(rootLines, "model_reasoning_effort", restoreRoot.reasoning);
|
|
1203
|
+
setOrDeleteRootKey(rootLines, "model_provider", restoreRoot.provider);
|
|
1204
|
+
} else {
|
|
1205
|
+
deleteRootKey(rootLines, "model");
|
|
1206
|
+
deleteRootKey(rootLines, "model_reasoning_effort");
|
|
1207
|
+
deleteRootKey(rootLines, "model_provider");
|
|
1137
1208
|
}
|
|
1138
|
-
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1209
|
+
const recombined = [...rootLines];
|
|
1210
|
+
if (recombined.length > 0 && restLines.length > 0 && recombined[recombined.length - 1]?.trim() !== "") recombined.push("");
|
|
1211
|
+
recombined.push(...restLines);
|
|
1212
|
+
const nextContent = recombined.join("\n");
|
|
1141
1213
|
return {
|
|
1142
1214
|
content: nextContent,
|
|
1143
1215
|
changed: nextContent !== content
|
|
1144
1216
|
};
|
|
1145
1217
|
};
|
|
1146
|
-
const removeAuthJson = (data) => {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1218
|
+
const removeAuthJson = (data, options) => {
|
|
1219
|
+
const { force = false, installed, restore } = options ?? {};
|
|
1220
|
+
const next = { ...data };
|
|
1221
|
+
let changed = false;
|
|
1222
|
+
for (const key of LEGACY_AUTH_MARKERS) if (key in next) {
|
|
1223
|
+
delete next[key];
|
|
1224
|
+
changed = true;
|
|
1225
|
+
}
|
|
1226
|
+
const current = typeof next.OPENAI_API_KEY === "string" ? next.OPENAI_API_KEY : void 0;
|
|
1227
|
+
const restoreValue = typeof restore === "string" && restore.trim().length > 0 ? restore : void 0;
|
|
1228
|
+
if (installed && current && current === installed) {
|
|
1229
|
+
if (restoreValue) next.OPENAI_API_KEY = restoreValue;
|
|
1230
|
+
else delete next.OPENAI_API_KEY;
|
|
1231
|
+
changed = true;
|
|
1232
|
+
return {
|
|
1233
|
+
data: next,
|
|
1234
|
+
changed
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (force && current) {
|
|
1238
|
+
delete next.OPENAI_API_KEY;
|
|
1239
|
+
changed = true;
|
|
1240
|
+
}
|
|
1152
1241
|
return {
|
|
1153
|
-
data:
|
|
1154
|
-
changed
|
|
1242
|
+
data: next,
|
|
1243
|
+
changed
|
|
1155
1244
|
};
|
|
1156
1245
|
};
|
|
1157
1246
|
|
|
1158
1247
|
//#endregion
|
|
1159
1248
|
//#region src/cmd/codex.ts
|
|
1160
1249
|
const CODEX_DIR = ".codex";
|
|
1250
|
+
const CODEX_BACKUP_FILE = "codex-backup.json";
|
|
1161
1251
|
const readFileIfExists = (filePath) => fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
1162
1252
|
const readAuthJson = (filePath) => {
|
|
1163
1253
|
if (!fs.existsSync(filePath)) return {};
|
|
@@ -1173,6 +1263,18 @@ const ensureCodexDir = () => {
|
|
|
1173
1263
|
return dir;
|
|
1174
1264
|
};
|
|
1175
1265
|
const resolveCodexDir = () => path.join(os.homedir(), CODEX_DIR);
|
|
1266
|
+
const resolveCodexBackupPath = () => path.join(resolveConfigDir(), CODEX_BACKUP_FILE);
|
|
1267
|
+
const readCodexBackup = () => {
|
|
1268
|
+
const raw = readJsonFile(resolveCodexBackupPath());
|
|
1269
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1270
|
+
if (raw.version !== 1) return null;
|
|
1271
|
+
return raw;
|
|
1272
|
+
};
|
|
1273
|
+
const writeCodexBackup = (backup) => {
|
|
1274
|
+
const backupPath = resolveCodexBackupPath();
|
|
1275
|
+
writeJsonFile(backupPath, backup);
|
|
1276
|
+
if (process.platform !== "win32") fs.chmodSync(backupPath, 384);
|
|
1277
|
+
};
|
|
1176
1278
|
const requireInteractive$1 = () => {
|
|
1177
1279
|
if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
|
|
1178
1280
|
};
|
|
@@ -1209,12 +1311,38 @@ const registerCodexCommand = (program) => {
|
|
|
1209
1311
|
const codexDir = ensureCodexDir();
|
|
1210
1312
|
const configPath = path.join(codexDir, "config.toml");
|
|
1211
1313
|
const authPath = path.join(codexDir, "auth.json");
|
|
1212
|
-
const
|
|
1314
|
+
const existingConfig = readFileIfExists(configPath);
|
|
1315
|
+
const existingRoot = readCodexTomlRootValues(existingConfig);
|
|
1316
|
+
const existingAuth = readAuthJson(authPath);
|
|
1317
|
+
const existingOpenaiKey = typeof existingAuth.OPENAI_API_KEY === "string" ? existingAuth.OPENAI_API_KEY : void 0;
|
|
1318
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1319
|
+
const installedRoot = {
|
|
1320
|
+
model: `"${model}"`,
|
|
1321
|
+
reasoning: `"${reasoningValue}"`,
|
|
1322
|
+
provider: `"getrouter"`
|
|
1323
|
+
};
|
|
1324
|
+
const backup = readCodexBackup() ?? {
|
|
1325
|
+
version: 1,
|
|
1326
|
+
createdAt: now,
|
|
1327
|
+
updatedAt: now
|
|
1328
|
+
};
|
|
1329
|
+
backup.updatedAt = now;
|
|
1330
|
+
backup.config ??= {};
|
|
1331
|
+
backup.config.previous ??= {};
|
|
1332
|
+
if (backup.config.previous.model === void 0 && existingRoot.model && existingRoot.model !== installedRoot.model) backup.config.previous.model = existingRoot.model;
|
|
1333
|
+
if (backup.config.previous.reasoning === void 0 && existingRoot.reasoning && existingRoot.reasoning !== installedRoot.reasoning) backup.config.previous.reasoning = existingRoot.reasoning;
|
|
1334
|
+
if (backup.config.previous.provider === void 0 && existingRoot.provider && existingRoot.provider !== installedRoot.provider) backup.config.previous.provider = existingRoot.provider;
|
|
1335
|
+
backup.config.installed = installedRoot;
|
|
1336
|
+
backup.auth ??= {};
|
|
1337
|
+
if (backup.auth.previousOpenaiKey === void 0 && existingOpenaiKey && existingOpenaiKey !== apiKey) backup.auth.previousOpenaiKey = existingOpenaiKey;
|
|
1338
|
+
backup.auth.installedOpenaiKey = apiKey;
|
|
1339
|
+
writeCodexBackup(backup);
|
|
1340
|
+
const mergedConfig = mergeCodexToml(existingConfig, {
|
|
1213
1341
|
model,
|
|
1214
1342
|
reasoning: reasoningValue
|
|
1215
1343
|
});
|
|
1216
1344
|
fs.writeFileSync(configPath, mergedConfig, "utf8");
|
|
1217
|
-
const mergedAuth = mergeAuthJson(
|
|
1345
|
+
const mergedAuth = mergeAuthJson(existingAuth, apiKey);
|
|
1218
1346
|
fs.writeFileSync(authPath, JSON.stringify(mergedAuth, null, 2));
|
|
1219
1347
|
if (process.platform !== "win32") fs.chmodSync(authPath, 384);
|
|
1220
1348
|
console.log("✅ Updated ~/.codex/config.toml");
|
|
@@ -1226,11 +1354,19 @@ const registerCodexCommand = (program) => {
|
|
|
1226
1354
|
const authPath = path.join(codexDir, "auth.json");
|
|
1227
1355
|
const configExists = fs.existsSync(configPath);
|
|
1228
1356
|
const authExists = fs.existsSync(authPath);
|
|
1357
|
+
const backup = readCodexBackup();
|
|
1358
|
+
const restoreRoot = backup?.config?.previous;
|
|
1359
|
+
const restoreOpenaiKey = backup?.auth?.previousOpenaiKey;
|
|
1360
|
+
const installedOpenaiKey = backup?.auth?.installedOpenaiKey;
|
|
1229
1361
|
const configContent = configExists ? readFileIfExists(configPath) : "";
|
|
1230
|
-
const configResult = configExists ? removeCodexConfig(configContent) : null;
|
|
1362
|
+
const configResult = configExists ? removeCodexConfig(configContent, { restoreRoot }) : null;
|
|
1231
1363
|
const authContent = authExists ? fs.readFileSync(authPath, "utf8").trim() : "";
|
|
1232
1364
|
const authData = authExists ? authContent ? JSON.parse(authContent) : {} : null;
|
|
1233
|
-
const authResult = authData ? removeAuthJson(authData
|
|
1365
|
+
const authResult = authData ? removeAuthJson(authData, {
|
|
1366
|
+
force: true,
|
|
1367
|
+
installed: installedOpenaiKey,
|
|
1368
|
+
restore: restoreOpenaiKey
|
|
1369
|
+
}) : null;
|
|
1234
1370
|
if (!configExists) console.log(`ℹ️ ${configPath} not found`);
|
|
1235
1371
|
else if (configResult?.changed) {
|
|
1236
1372
|
fs.writeFileSync(configPath, configResult.content, "utf8");
|
|
@@ -1241,6 +1377,8 @@ const registerCodexCommand = (program) => {
|
|
|
1241
1377
|
fs.writeFileSync(authPath, JSON.stringify(authResult.data, null, 2));
|
|
1242
1378
|
console.log(`✅ Removed getrouter entries from ${authPath}`);
|
|
1243
1379
|
} else console.log(`ℹ️ No getrouter entries in ${authPath}`);
|
|
1380
|
+
const backupPath = resolveCodexBackupPath();
|
|
1381
|
+
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
|
|
1244
1382
|
});
|
|
1245
1383
|
};
|
|
1246
1384
|
|
|
@@ -1330,10 +1468,10 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
|
|
|
1330
1468
|
});
|
|
1331
1469
|
};
|
|
1332
1470
|
const listConsumers = async (consumerService, showApiKey) => {
|
|
1333
|
-
outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1471
|
+
outputConsumers(sortConsumersByUpdatedAtDesc(await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
1334
1472
|
pageSize: void 0,
|
|
1335
1473
|
pageToken
|
|
1336
|
-
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0), showApiKey);
|
|
1474
|
+
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0)), showApiKey);
|
|
1337
1475
|
};
|
|
1338
1476
|
const resolveConsumerForUpdate = async (consumerService, id) => {
|
|
1339
1477
|
if (id) return consumerService.GetConsumer({ id });
|