@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 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
+ [![CI](https://github.com/getrouter/getrouter-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/getrouter/getrouter-cli/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@getrouter/getrouter-cli)](https://www.npmjs.com/package/@getrouter/getrouter-cli)
5
+ [![npm downloads](https://img.shields.io/npm/dm/@getrouter/getrouter-cli)](https://www.npmjs.com/package/@getrouter/getrouter-cli)
6
+ [![node](https://img.shields.io/node/v/@getrouter/getrouter-cli)](https://www.npmjs.com/package/@getrouter/getrouter-cli)
7
+ [![bun](https://img.shields.io/badge/bun-1.3.5-000?logo=bun&logoColor=white)](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
+ [![Download History (last 30 days)](https://quickchart.io/chart/render/zf-3cc45f8d-a7de-4553-bdfa-877c4592ce59)](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.9";
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 sortByCreatedAtDesc = (consumers) => consumers.slice().sort((a, b) => {
603
- const aTime = Date.parse(a.createdAt ?? "") || 0;
604
- return (Date.parse(b.createdAt ?? "") || 0) - aTime;
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 createdAt = consumer.createdAt ?? "-";
621
- return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${createdAt})` : 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 = sortByCreatedAtDesc(consumers);
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: [normalizeName(consumer), consumer.createdAt ?? ""].filter(Boolean)
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 = sortByCreatedAtDesc(consumers);
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: "low",
968
- label: "Low",
969
- value: "low",
970
- description: "Fast responses with lighter reasoning"
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: "extra_high",
986
- label: "Extra high",
987
- value: "xhigh",
988
- description: "Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits."
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 matchProviderValue = (line) => line.match(/^\s*model_provider\s*=\s*(['"]?)([^'"]+)\1\s*(?:#.*)?$/);
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
- OPENAI_API_KEY: apiKey
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 stripRootKeys = (lines) => {
1107
- const updated = [];
1108
- let currentSection = null;
1109
- for (const line of lines) {
1110
- const headerMatch = matchHeader(line);
1111
- if (headerMatch) {
1112
- currentSection = headerMatch[1]?.trim() ?? null;
1113
- updated.push(line);
1114
- continue;
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
- return updated;
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
- let providerIsGetrouter = false;
1128
- let currentSection = null;
1129
- for (const line of lines) {
1130
- const headerMatch = matchHeader(line);
1131
- if (headerMatch) {
1132
- currentSection = headerMatch[1]?.trim() ?? null;
1133
- continue;
1134
- }
1135
- if (currentSection !== null) continue;
1136
- if ((matchProviderValue(line)?.[2]?.trim())?.toLowerCase() === CODEX_PROVIDER) providerIsGetrouter = true;
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
- let updated = stripGetrouterProviderSection(lines);
1139
- if (providerIsGetrouter) updated = stripRootKeys(updated);
1140
- const nextContent = updated.join("\n");
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
- if (!("OPENAI_API_KEY" in data)) return {
1148
- data,
1149
- changed: false
1150
- };
1151
- const { OPENAI_API_KEY: _ignored, ...rest } = data;
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: rest,
1154
- changed: true
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 mergedConfig = mergeCodexToml(readFileIfExists(configPath), {
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(readAuthJson(authPath), apiKey);
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) : null;
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrouter/getrouter-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "CLI for getrouter.dev",
6
6
  "bin": {