@getrouter/getrouter-cli 0.1.11 → 0.1.12
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/dist/bin.mjs +28 -14
- package/package.json +1 -1
- package/src/cmd/codex.ts +5 -2
- package/src/core/interactive/keys.ts +8 -2
- package/src/core/setup/codex.ts +30 -13
- package/tests/cmd/codex.test.ts +56 -0
- package/tests/core/setup/codex.test.ts +2 -2
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.12";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -662,7 +662,10 @@ const selectConsumer = async (consumerService) => {
|
|
|
662
662
|
pageSize: void 0,
|
|
663
663
|
pageToken
|
|
664
664
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
665
|
-
if (consumers.length === 0)
|
|
665
|
+
if (consumers.length === 0) {
|
|
666
|
+
console.log("No available API keys. Create one at https://getrouter.dev/dashboard/keys");
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
666
669
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
667
670
|
const nameCounts = buildNameCounts(sorted);
|
|
668
671
|
return await fuzzySelect({
|
|
@@ -683,7 +686,10 @@ const selectConsumerList = async (consumerService, message) => {
|
|
|
683
686
|
pageSize: void 0,
|
|
684
687
|
pageToken
|
|
685
688
|
}), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0);
|
|
686
|
-
if (consumers.length === 0)
|
|
689
|
+
if (consumers.length === 0) {
|
|
690
|
+
console.log("No available API keys. Create one at https://getrouter.dev/dashboard/keys");
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
687
693
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
688
694
|
const nameCounts = buildNameCounts(sorted);
|
|
689
695
|
const response = await prompts({
|
|
@@ -1188,16 +1194,21 @@ const setOrDeleteRootKey = (rootLines, key, value) => {
|
|
|
1188
1194
|
const deleteRootKey = (rootLines, key) => {
|
|
1189
1195
|
setOrDeleteRootKey(rootLines, key, void 0);
|
|
1190
1196
|
};
|
|
1197
|
+
const hasLegacyRootMarkers = (lines) => lines.some((line) => {
|
|
1198
|
+
const key = matchKey(line)?.[1];
|
|
1199
|
+
return !!(key && LEGACY_TOML_ROOT_MARKERS.includes(key));
|
|
1200
|
+
});
|
|
1191
1201
|
const removeCodexConfig = (content, options) => {
|
|
1192
|
-
const { restoreRoot } = options ?? {};
|
|
1202
|
+
const { restoreRoot, allowRootRemoval = true } = options ?? {};
|
|
1193
1203
|
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
1194
1204
|
const providerIsGetrouter = normalizeTomlString(readRootValue(lines, "model_provider")) === CODEX_PROVIDER;
|
|
1205
|
+
const canRemoveRoot = allowRootRemoval || hasLegacyRootMarkers(lines);
|
|
1195
1206
|
const stripped = stripGetrouterProviderSection(lines);
|
|
1196
1207
|
const firstHeaderIndex = stripped.findIndex((line) => matchHeader(line));
|
|
1197
1208
|
const rootEnd = firstHeaderIndex === -1 ? stripped.length : firstHeaderIndex;
|
|
1198
1209
|
const rootLines = stripLegacyMarkersFromRoot(stripped.slice(0, rootEnd));
|
|
1199
1210
|
const restLines = stripped.slice(rootEnd);
|
|
1200
|
-
if (providerIsGetrouter) if (restoreRoot) {
|
|
1211
|
+
if (providerIsGetrouter && canRemoveRoot) if (restoreRoot) {
|
|
1201
1212
|
setOrDeleteRootKey(rootLines, "model", restoreRoot.model);
|
|
1202
1213
|
setOrDeleteRootKey(rootLines, "model_reasoning_effort", restoreRoot.reasoning);
|
|
1203
1214
|
setOrDeleteRootKey(rootLines, "model_provider", restoreRoot.provider);
|
|
@@ -1216,16 +1227,20 @@ const removeCodexConfig = (content, options) => {
|
|
|
1216
1227
|
};
|
|
1217
1228
|
};
|
|
1218
1229
|
const removeAuthJson = (data, options) => {
|
|
1219
|
-
const {
|
|
1230
|
+
const { installed, restore } = options ?? {};
|
|
1220
1231
|
const next = { ...data };
|
|
1221
1232
|
let changed = false;
|
|
1233
|
+
const legacyInstalled = typeof next._getrouter_codex_installed_openai_api_key === "string" ? next._getrouter_codex_installed_openai_api_key : void 0;
|
|
1234
|
+
const legacyRestore = typeof next._getrouter_codex_backup_openai_api_key === "string" ? next._getrouter_codex_backup_openai_api_key : void 0;
|
|
1235
|
+
const effectiveInstalled = installed ?? legacyInstalled;
|
|
1236
|
+
const effectiveRestore = restore ?? legacyRestore;
|
|
1222
1237
|
for (const key of LEGACY_AUTH_MARKERS) if (key in next) {
|
|
1223
1238
|
delete next[key];
|
|
1224
1239
|
changed = true;
|
|
1225
1240
|
}
|
|
1226
1241
|
const current = typeof next.OPENAI_API_KEY === "string" ? next.OPENAI_API_KEY : void 0;
|
|
1227
|
-
const restoreValue = typeof
|
|
1228
|
-
if (
|
|
1242
|
+
const restoreValue = typeof effectiveRestore === "string" && effectiveRestore.trim().length > 0 ? effectiveRestore : void 0;
|
|
1243
|
+
if (effectiveInstalled && current && current === effectiveInstalled) {
|
|
1229
1244
|
if (restoreValue) next.OPENAI_API_KEY = restoreValue;
|
|
1230
1245
|
else delete next.OPENAI_API_KEY;
|
|
1231
1246
|
changed = true;
|
|
@@ -1234,10 +1249,6 @@ const removeAuthJson = (data, options) => {
|
|
|
1234
1249
|
changed
|
|
1235
1250
|
};
|
|
1236
1251
|
}
|
|
1237
|
-
if (force && current) {
|
|
1238
|
-
delete next.OPENAI_API_KEY;
|
|
1239
|
-
changed = true;
|
|
1240
|
-
}
|
|
1241
1252
|
return {
|
|
1242
1253
|
data: next,
|
|
1243
1254
|
changed
|
|
@@ -1358,12 +1369,15 @@ const registerCodexCommand = (program) => {
|
|
|
1358
1369
|
const restoreRoot = backup?.config?.previous;
|
|
1359
1370
|
const restoreOpenaiKey = backup?.auth?.previousOpenaiKey;
|
|
1360
1371
|
const installedOpenaiKey = backup?.auth?.installedOpenaiKey;
|
|
1372
|
+
const hasBackup = Boolean(backup);
|
|
1361
1373
|
const configContent = configExists ? readFileIfExists(configPath) : "";
|
|
1362
|
-
const configResult = configExists ? removeCodexConfig(configContent, {
|
|
1374
|
+
const configResult = configExists ? removeCodexConfig(configContent, {
|
|
1375
|
+
restoreRoot,
|
|
1376
|
+
allowRootRemoval: hasBackup
|
|
1377
|
+
}) : null;
|
|
1363
1378
|
const authContent = authExists ? fs.readFileSync(authPath, "utf8").trim() : "";
|
|
1364
1379
|
const authData = authExists ? authContent ? JSON.parse(authContent) : {} : null;
|
|
1365
1380
|
const authResult = authData ? removeAuthJson(authData, {
|
|
1366
|
-
force: true,
|
|
1367
1381
|
installed: installedOpenaiKey,
|
|
1368
1382
|
restore: restoreOpenaiKey
|
|
1369
1383
|
}) : null;
|
package/package.json
CHANGED
package/src/cmd/codex.ts
CHANGED
|
@@ -235,10 +235,14 @@ export const registerCodexCommand = (program: Command) => {
|
|
|
235
235
|
const restoreRoot = backup?.config?.previous;
|
|
236
236
|
const restoreOpenaiKey = backup?.auth?.previousOpenaiKey;
|
|
237
237
|
const installedOpenaiKey = backup?.auth?.installedOpenaiKey;
|
|
238
|
+
const hasBackup = Boolean(backup);
|
|
238
239
|
|
|
239
240
|
const configContent = configExists ? readFileIfExists(configPath) : "";
|
|
240
241
|
const configResult = configExists
|
|
241
|
-
? removeCodexConfig(configContent, {
|
|
242
|
+
? removeCodexConfig(configContent, {
|
|
243
|
+
restoreRoot,
|
|
244
|
+
allowRootRemoval: hasBackup,
|
|
245
|
+
})
|
|
242
246
|
: null;
|
|
243
247
|
|
|
244
248
|
const authContent = authExists
|
|
@@ -251,7 +255,6 @@ export const registerCodexCommand = (program: Command) => {
|
|
|
251
255
|
: null;
|
|
252
256
|
const authResult = authData
|
|
253
257
|
? removeAuthJson(authData, {
|
|
254
|
-
force: true,
|
|
255
258
|
installed: installedOpenaiKey,
|
|
256
259
|
restore: restoreOpenaiKey,
|
|
257
260
|
})
|
|
@@ -139,7 +139,10 @@ export const selectConsumer = async (
|
|
|
139
139
|
(res) => res?.nextPageToken || undefined,
|
|
140
140
|
);
|
|
141
141
|
if (consumers.length === 0) {
|
|
142
|
-
|
|
142
|
+
console.log(
|
|
143
|
+
"No available API keys. Create one at https://getrouter.dev/dashboard/keys",
|
|
144
|
+
);
|
|
145
|
+
return null;
|
|
143
146
|
}
|
|
144
147
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
145
148
|
const nameCounts = buildNameCounts(sorted);
|
|
@@ -172,7 +175,10 @@ export const selectConsumerList = async (
|
|
|
172
175
|
(res) => res?.nextPageToken || undefined,
|
|
173
176
|
);
|
|
174
177
|
if (consumers.length === 0) {
|
|
175
|
-
|
|
178
|
+
console.log(
|
|
179
|
+
"No available API keys. Create one at https://getrouter.dev/dashboard/keys",
|
|
180
|
+
);
|
|
181
|
+
return null;
|
|
176
182
|
}
|
|
177
183
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
178
184
|
const nameCounts = buildNameCounts(sorted);
|
package/src/core/setup/codex.ts
CHANGED
|
@@ -277,15 +277,26 @@ const deleteRootKey = (rootLines: string[], key: string) => {
|
|
|
277
277
|
setOrDeleteRootKey(rootLines, key, undefined);
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
+
const hasLegacyRootMarkers = (lines: string[]) =>
|
|
281
|
+
lines.some((line) => {
|
|
282
|
+
const keyMatch = matchKey(line);
|
|
283
|
+
const key = keyMatch?.[1];
|
|
284
|
+
return !!(key && LEGACY_TOML_ROOT_MARKERS.includes(key as never));
|
|
285
|
+
});
|
|
286
|
+
|
|
280
287
|
export const removeCodexConfig = (
|
|
281
288
|
content: string,
|
|
282
|
-
options?: {
|
|
289
|
+
options?: {
|
|
290
|
+
restoreRoot?: CodexTomlRootValues;
|
|
291
|
+
allowRootRemoval?: boolean;
|
|
292
|
+
},
|
|
283
293
|
) => {
|
|
284
|
-
const { restoreRoot } = options ?? {};
|
|
294
|
+
const { restoreRoot, allowRootRemoval = true } = options ?? {};
|
|
285
295
|
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
286
296
|
const providerIsGetrouter =
|
|
287
297
|
normalizeTomlString(readRootValue(lines, "model_provider")) ===
|
|
288
298
|
CODEX_PROVIDER;
|
|
299
|
+
const canRemoveRoot = allowRootRemoval || hasLegacyRootMarkers(lines);
|
|
289
300
|
|
|
290
301
|
const stripped = stripGetrouterProviderSection(lines);
|
|
291
302
|
const firstHeaderIndex = stripped.findIndex((line) => matchHeader(line));
|
|
@@ -293,7 +304,7 @@ export const removeCodexConfig = (
|
|
|
293
304
|
const rootLines = stripLegacyMarkersFromRoot(stripped.slice(0, rootEnd));
|
|
294
305
|
const restLines = stripped.slice(rootEnd);
|
|
295
306
|
|
|
296
|
-
if (providerIsGetrouter) {
|
|
307
|
+
if (providerIsGetrouter && canRemoveRoot) {
|
|
297
308
|
if (restoreRoot) {
|
|
298
309
|
setOrDeleteRootKey(rootLines, "model", restoreRoot.model);
|
|
299
310
|
setOrDeleteRootKey(
|
|
@@ -326,15 +337,26 @@ export const removeCodexConfig = (
|
|
|
326
337
|
export const removeAuthJson = (
|
|
327
338
|
data: Record<string, unknown>,
|
|
328
339
|
options?: {
|
|
329
|
-
force?: boolean;
|
|
330
340
|
installed?: string;
|
|
331
341
|
restore?: string;
|
|
332
342
|
},
|
|
333
343
|
) => {
|
|
334
|
-
const {
|
|
344
|
+
const { installed, restore } = options ?? {};
|
|
335
345
|
const next: Record<string, unknown> = { ...data };
|
|
336
346
|
let changed = false;
|
|
337
347
|
|
|
348
|
+
const legacyInstalled =
|
|
349
|
+
typeof next._getrouter_codex_installed_openai_api_key === "string"
|
|
350
|
+
? (next._getrouter_codex_installed_openai_api_key as string)
|
|
351
|
+
: undefined;
|
|
352
|
+
const legacyRestore =
|
|
353
|
+
typeof next._getrouter_codex_backup_openai_api_key === "string"
|
|
354
|
+
? (next._getrouter_codex_backup_openai_api_key as string)
|
|
355
|
+
: undefined;
|
|
356
|
+
|
|
357
|
+
const effectiveInstalled = installed ?? legacyInstalled;
|
|
358
|
+
const effectiveRestore = restore ?? legacyRestore;
|
|
359
|
+
|
|
338
360
|
for (const key of LEGACY_AUTH_MARKERS) {
|
|
339
361
|
if (key in next) {
|
|
340
362
|
delete next[key];
|
|
@@ -347,11 +369,11 @@ export const removeAuthJson = (
|
|
|
347
369
|
? (next.OPENAI_API_KEY as string)
|
|
348
370
|
: undefined;
|
|
349
371
|
const restoreValue =
|
|
350
|
-
typeof
|
|
351
|
-
?
|
|
372
|
+
typeof effectiveRestore === "string" && effectiveRestore.trim().length > 0
|
|
373
|
+
? effectiveRestore
|
|
352
374
|
: undefined;
|
|
353
375
|
|
|
354
|
-
if (
|
|
376
|
+
if (effectiveInstalled && current && current === effectiveInstalled) {
|
|
355
377
|
if (restoreValue) {
|
|
356
378
|
next.OPENAI_API_KEY = restoreValue;
|
|
357
379
|
} else {
|
|
@@ -361,10 +383,5 @@ export const removeAuthJson = (
|
|
|
361
383
|
return { data: next, changed };
|
|
362
384
|
}
|
|
363
385
|
|
|
364
|
-
if (force && current) {
|
|
365
|
-
delete next.OPENAI_API_KEY;
|
|
366
|
-
changed = true;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
386
|
return { data: next, changed };
|
|
370
387
|
};
|
package/tests/cmd/codex.test.ts
CHANGED
|
@@ -239,6 +239,7 @@ describe("codex command", () => {
|
|
|
239
239
|
process.env.HOME = dir;
|
|
240
240
|
const codexDir = path.join(dir, ".codex");
|
|
241
241
|
fs.mkdirSync(codexDir, { recursive: true });
|
|
242
|
+
fs.mkdirSync(path.join(dir, ".getrouter"), { recursive: true });
|
|
242
243
|
fs.writeFileSync(
|
|
243
244
|
codexConfigPath(dir),
|
|
244
245
|
[
|
|
@@ -259,6 +260,21 @@ describe("codex command", () => {
|
|
|
259
260
|
codexAuthPath(dir),
|
|
260
261
|
JSON.stringify({ OTHER: "keep", OPENAI_API_KEY: "old" }, null, 2),
|
|
261
262
|
);
|
|
263
|
+
fs.writeFileSync(
|
|
264
|
+
codexBackupPath(dir),
|
|
265
|
+
JSON.stringify(
|
|
266
|
+
{
|
|
267
|
+
version: 1,
|
|
268
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
269
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
270
|
+
auth: {
|
|
271
|
+
installedOpenaiKey: "old",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
null,
|
|
275
|
+
2,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
262
278
|
|
|
263
279
|
const program = createProgram();
|
|
264
280
|
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
@@ -276,6 +292,46 @@ describe("codex command", () => {
|
|
|
276
292
|
expect(auth.OPENAI_API_KEY).toBeUndefined();
|
|
277
293
|
});
|
|
278
294
|
|
|
295
|
+
it("uninstall preserves existing keys when backup is missing", async () => {
|
|
296
|
+
const dir = makeDir();
|
|
297
|
+
process.env.HOME = dir;
|
|
298
|
+
const codexDir = path.join(dir, ".codex");
|
|
299
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
300
|
+
fs.writeFileSync(
|
|
301
|
+
codexConfigPath(dir),
|
|
302
|
+
[
|
|
303
|
+
'theme = "dark"',
|
|
304
|
+
'model = "keep"',
|
|
305
|
+
'model_reasoning_effort = "low"',
|
|
306
|
+
'model_provider = "getrouter"',
|
|
307
|
+
"",
|
|
308
|
+
"[model_providers.getrouter]",
|
|
309
|
+
'name = "getrouter"',
|
|
310
|
+
'base_url = "https://api.getrouter.dev/codex"',
|
|
311
|
+
"",
|
|
312
|
+
"[model_providers.other]",
|
|
313
|
+
'name = "other"',
|
|
314
|
+
].join("\n"),
|
|
315
|
+
);
|
|
316
|
+
fs.writeFileSync(
|
|
317
|
+
codexAuthPath(dir),
|
|
318
|
+
JSON.stringify({ OTHER: "keep", OPENAI_API_KEY: "old" }, null, 2),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const program = createProgram();
|
|
322
|
+
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
323
|
+
|
|
324
|
+
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
325
|
+
expect(config).toContain('theme = "dark"');
|
|
326
|
+
expect(config).toContain('model = "keep"');
|
|
327
|
+
expect(config).toContain('model_provider = "getrouter"');
|
|
328
|
+
expect(config).toContain('model_reasoning_effort = "low"');
|
|
329
|
+
|
|
330
|
+
const auth = JSON.parse(fs.readFileSync(codexAuthPath(dir), "utf8"));
|
|
331
|
+
expect(auth.OTHER).toBe("keep");
|
|
332
|
+
expect(auth.OPENAI_API_KEY).toBe("old");
|
|
333
|
+
});
|
|
334
|
+
|
|
279
335
|
it("uninstall restores previous OPENAI_API_KEY when backup exists", async () => {
|
|
280
336
|
const dir = makeDir();
|
|
281
337
|
process.env.HOME = dir;
|
|
@@ -100,12 +100,12 @@ describe("codex setup helpers", () => {
|
|
|
100
100
|
expect(data.OTHER).toBe("keep");
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
it("removes OPENAI_API_KEY when
|
|
103
|
+
it("removes OPENAI_API_KEY when installed key matches and no restore is available", () => {
|
|
104
104
|
const input = {
|
|
105
105
|
OPENAI_API_KEY: "new-key",
|
|
106
106
|
OTHER: "keep",
|
|
107
107
|
} as Record<string, unknown>;
|
|
108
|
-
const { data, changed } = removeAuthJson(input, {
|
|
108
|
+
const { data, changed } = removeAuthJson(input, { installed: "new-key" });
|
|
109
109
|
expect(changed).toBe(true);
|
|
110
110
|
expect(data.OPENAI_API_KEY).toBeUndefined();
|
|
111
111
|
expect(data.OTHER).toBe("keep");
|