@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.
@@ -15,6 +15,8 @@ const makeDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
15
15
  const codexConfigPath = (dir: string) =>
16
16
  path.join(dir, ".codex", "config.toml");
17
17
  const codexAuthPath = (dir: string) => path.join(dir, ".codex", "auth.json");
18
+ const codexBackupPath = (dir: string) =>
19
+ path.join(dir, ".getrouter", "codex-backup.json");
18
20
 
19
21
  const mockConsumer = { id: "c1", apiKey: "key-123" };
20
22
 
@@ -274,6 +276,104 @@ describe("codex command", () => {
274
276
  expect(auth.OPENAI_API_KEY).toBeUndefined();
275
277
  });
276
278
 
279
+ it("uninstall restores previous OPENAI_API_KEY when backup exists", async () => {
280
+ const dir = makeDir();
281
+ process.env.HOME = dir;
282
+ const codexDir = path.join(dir, ".codex");
283
+ fs.mkdirSync(codexDir, { recursive: true });
284
+ fs.mkdirSync(path.join(dir, ".getrouter"), { recursive: true });
285
+ fs.writeFileSync(
286
+ codexAuthPath(dir),
287
+ JSON.stringify(
288
+ {
289
+ OPENAI_API_KEY: "new-key",
290
+ _getrouter_codex_backup_openai_api_key: "legacy-backup",
291
+ _getrouter_codex_installed_openai_api_key: "legacy-installed",
292
+ OTHER: "keep",
293
+ },
294
+ null,
295
+ 2,
296
+ ),
297
+ );
298
+ fs.writeFileSync(
299
+ codexBackupPath(dir),
300
+ JSON.stringify(
301
+ {
302
+ version: 1,
303
+ createdAt: "2026-01-01T00:00:00Z",
304
+ updatedAt: "2026-01-01T00:00:00Z",
305
+ auth: {
306
+ previousOpenaiKey: "old-key",
307
+ installedOpenaiKey: "new-key",
308
+ },
309
+ },
310
+ null,
311
+ 2,
312
+ ),
313
+ );
314
+
315
+ const program = createProgram();
316
+ await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
317
+
318
+ const auth = JSON.parse(fs.readFileSync(codexAuthPath(dir), "utf8"));
319
+ expect(auth.OPENAI_API_KEY).toBe("old-key");
320
+ expect(auth._getrouter_codex_backup_openai_api_key).toBeUndefined();
321
+ expect(auth._getrouter_codex_installed_openai_api_key).toBeUndefined();
322
+ expect(auth.OTHER).toBe("keep");
323
+ expect(fs.existsSync(codexBackupPath(dir))).toBe(false);
324
+ });
325
+
326
+ it("uninstall restores previous model_provider and model", async () => {
327
+ setStdinTTY(true);
328
+ const dir = makeDir();
329
+ process.env.HOME = dir;
330
+ const codexDir = path.join(dir, ".codex");
331
+ fs.mkdirSync(codexDir, { recursive: true });
332
+ fs.writeFileSync(
333
+ codexConfigPath(dir),
334
+ [
335
+ 'model = "user-model"',
336
+ 'model_reasoning_effort = "low"',
337
+ 'model_provider = "openai"',
338
+ "",
339
+ "[model_providers.openai]",
340
+ 'name = "openai"',
341
+ ].join("\n"),
342
+ );
343
+
344
+ prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer]);
345
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
346
+ consumerService: {
347
+ ListConsumers: vi.fn().mockResolvedValue({
348
+ consumers: [
349
+ {
350
+ id: "c1",
351
+ name: "dev",
352
+ enabled: true,
353
+ createdAt: "2026-01-01T00:00:00Z",
354
+ },
355
+ ],
356
+ }),
357
+ GetConsumer: vi.fn().mockResolvedValue(mockConsumer),
358
+ } as unknown as ConsumerService,
359
+ });
360
+
361
+ const program = createProgram();
362
+ await program.parseAsync(["node", "getrouter", "codex"]);
363
+ expect(fs.existsSync(codexBackupPath(dir))).toBe(true);
364
+ await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
365
+ expect(fs.existsSync(codexBackupPath(dir))).toBe(false);
366
+
367
+ const config = fs.readFileSync(codexConfigPath(dir), "utf8");
368
+ expect(config).toContain('model = "user-model"');
369
+ expect(config).toContain('model_reasoning_effort = "low"');
370
+ expect(config).toContain('model_provider = "openai"');
371
+ expect(config).toContain("[model_providers.openai]");
372
+ expect(config).not.toContain("[model_providers.getrouter]");
373
+ expect(config).not.toContain("_getrouter_codex_backup");
374
+ expect(config).not.toContain("_getrouter_codex_installed");
375
+ });
376
+
277
377
  it("uninstall leaves root keys when provider is not getrouter", async () => {
278
378
  const dir = makeDir();
279
379
  process.env.HOME = dir;
@@ -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: ["new-codex-model-xyz"],
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
- choices.some((choice) => choice.value === "new-codex-model-xyz"),
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
  );
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { mergeAuthJson, mergeCodexToml } from "../../../src/core/setup/codex";
2
+ import {
3
+ mergeAuthJson,
4
+ mergeCodexToml,
5
+ removeAuthJson,
6
+ removeCodexConfig,
7
+ } from "../../../src/core/setup/codex";
3
8
 
4
9
  describe("codex setup helpers", () => {
5
10
  it("merges codex toml at root and provider table", () => {
@@ -35,4 +40,85 @@ describe("codex setup helpers", () => {
35
40
  expect(output.OPENAI_API_KEY).toBe("key-123");
36
41
  expect(output.existing).toBe("keep");
37
42
  });
43
+
44
+ it("removes getrouter provider section and restores root keys when provided", () => {
45
+ const input = [
46
+ 'model = "gpt-5.2-codex"',
47
+ 'model_reasoning_effort = "xhigh"',
48
+ 'model_provider = "getrouter"',
49
+ "",
50
+ "[model_providers.getrouter]",
51
+ 'name = "getrouter"',
52
+ "",
53
+ "[model_providers.openai]",
54
+ 'name = "openai"',
55
+ ].join("\n");
56
+
57
+ const { content, changed } = removeCodexConfig(input, {
58
+ restoreRoot: {
59
+ model: '"user-model"',
60
+ reasoning: '"medium"',
61
+ provider: '"openai"',
62
+ },
63
+ });
64
+ expect(changed).toBe(true);
65
+ expect(content).toContain('model = "user-model"');
66
+ expect(content).toContain('model_reasoning_effort = "medium"');
67
+ expect(content).toContain('model_provider = "openai"');
68
+ expect(content).toContain("[model_providers.openai]");
69
+ expect(content).not.toContain("[model_providers.getrouter]");
70
+ });
71
+
72
+ it("removes root keys when provider is getrouter and no restore is provided", () => {
73
+ const input = [
74
+ 'model = "gpt-5.2-codex"',
75
+ 'model_reasoning_effort = "xhigh"',
76
+ 'model_provider = "getrouter"',
77
+ "",
78
+ "[model_providers.getrouter]",
79
+ 'name = "getrouter"',
80
+ ].join("\n");
81
+
82
+ const { content } = removeCodexConfig(input);
83
+ expect(content).not.toContain('model = "gpt-5.2-codex"');
84
+ expect(content).not.toContain('model_reasoning_effort = "xhigh"');
85
+ expect(content).not.toContain('model_provider = "getrouter"');
86
+ expect(content).not.toContain("[model_providers.getrouter]");
87
+ });
88
+
89
+ it("restores OPENAI_API_KEY when installed key matches current", () => {
90
+ const input = {
91
+ OPENAI_API_KEY: "new-key",
92
+ OTHER: "keep",
93
+ } as Record<string, unknown>;
94
+ const { data, changed } = removeAuthJson(input, {
95
+ installed: "new-key",
96
+ restore: "old-key",
97
+ });
98
+ expect(changed).toBe(true);
99
+ expect(data.OPENAI_API_KEY).toBe("old-key");
100
+ expect(data.OTHER).toBe("keep");
101
+ });
102
+
103
+ it("removes OPENAI_API_KEY when forced and no restore is available", () => {
104
+ const input = {
105
+ OPENAI_API_KEY: "new-key",
106
+ OTHER: "keep",
107
+ } as Record<string, unknown>;
108
+ const { data, changed } = removeAuthJson(input, { force: true });
109
+ expect(changed).toBe(true);
110
+ expect(data.OPENAI_API_KEY).toBeUndefined();
111
+ expect(data.OTHER).toBe("keep");
112
+ });
113
+
114
+ it("leaves OPENAI_API_KEY when not forced and not installed", () => {
115
+ const input = {
116
+ OPENAI_API_KEY: "user-key",
117
+ OTHER: "keep",
118
+ } as Record<string, unknown>;
119
+ const { data, changed } = removeAuthJson(input);
120
+ expect(changed).toBe(false);
121
+ expect(data.OPENAI_API_KEY).toBe("user-key");
122
+ expect(data.OTHER).toBe("keep");
123
+ });
38
124
  });