@getrouter/getrouter-cli 0.1.11 → 0.1.13

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.
@@ -0,0 +1,38 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "package.json"
8
+
9
+ jobs:
10
+ release:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 2
18
+
19
+ - name: Check version change
20
+ id: version
21
+ run: |
22
+ CURRENT_VERSION=$(jq -r '.version' package.json)
23
+ git show HEAD~1:package.json > /tmp/prev-package.json 2>/dev/null || echo '{"version":""}' > /tmp/prev-package.json
24
+ PREV_VERSION=$(jq -r '.version' /tmp/prev-package.json)
25
+ if [ "$CURRENT_VERSION" != "$PREV_VERSION" ]; then
26
+ echo "changed=true" >> $GITHUB_OUTPUT
27
+ echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
28
+ else
29
+ echo "changed=false" >> $GITHUB_OUTPUT
30
+ fi
31
+
32
+ - name: Create Release
33
+ if: steps.version.outputs.changed == 'true'
34
+ uses: softprops/action-gh-release@v2
35
+ with:
36
+ tag_name: v${{ steps.version.outputs.version }}
37
+ name: v${{ steps.version.outputs.version }}
38
+ generate_release_notes: true
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.10";
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) throw new Error("No available API keys");
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) throw new Error("No available API keys");
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 { force = false, installed, restore } = options ?? {};
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 restore === "string" && restore.trim().length > 0 ? restore : void 0;
1228
- if (installed && current && current === installed) {
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, { restoreRoot }) : null;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrouter/getrouter-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "CLI for getrouter.dev",
6
6
  "bin": {
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, { restoreRoot })
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
- throw new Error("No available API keys");
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
- throw new Error("No available API keys");
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);
@@ -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?: { restoreRoot?: CodexTomlRootValues },
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 { force = false, installed, restore } = options ?? {};
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 restore === "string" && restore.trim().length > 0
351
- ? restore
372
+ typeof effectiveRestore === "string" && effectiveRestore.trim().length > 0
373
+ ? effectiveRestore
352
374
  : undefined;
353
375
 
354
- if (installed && current && current === installed) {
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
  };
@@ -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 forced and no restore is available", () => {
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, { force: true });
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");