@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/tests/cmd/codex.test.ts
CHANGED
|
@@ -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;
|
package/tests/cmd/keys.test.ts
CHANGED
|
@@ -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: ["
|
|
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
|
-
|
|
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 {
|
|
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
|
});
|