@getrouter/getrouter-cli 0.1.4 → 0.1.6
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 +119 -20
- package/docs/plans/2026-01-06-codex-uninstall-design.md +34 -0
- package/docs/plans/2026-01-06-codex-uninstall-plan.md +252 -0
- package/package.json +1 -1
- package/src/cmd/codex.ts +63 -18
- package/src/cmd/keys.ts +16 -7
- package/src/core/interactive/keys.ts +17 -4
- package/src/core/setup/codex.ts +84 -0
- package/tests/cmd/codex.test.ts +74 -4
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.5";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/generated/router/dashboard/v1/index.ts
|
|
@@ -616,8 +616,12 @@ const promptKeyName = async (initial) => {
|
|
|
616
616
|
message: "Key name",
|
|
617
617
|
initial: initial ?? ""
|
|
618
618
|
});
|
|
619
|
+
if (!("name" in response)) return { cancelled: true };
|
|
619
620
|
const value = typeof response.name === "string" ? response.name.trim() : "";
|
|
620
|
-
return
|
|
621
|
+
return {
|
|
622
|
+
cancelled: false,
|
|
623
|
+
name: value.length > 0 ? value : void 0
|
|
624
|
+
};
|
|
621
625
|
};
|
|
622
626
|
const promptKeyEnabled = async (initial) => {
|
|
623
627
|
const response = await prompts({
|
|
@@ -626,7 +630,11 @@ const promptKeyEnabled = async (initial) => {
|
|
|
626
630
|
message: "Enable this key?",
|
|
627
631
|
initial
|
|
628
632
|
});
|
|
629
|
-
|
|
633
|
+
if (!("enabled" in response)) return { cancelled: true };
|
|
634
|
+
return {
|
|
635
|
+
cancelled: false,
|
|
636
|
+
enabled: typeof response.enabled === "boolean" ? response.enabled : initial
|
|
637
|
+
};
|
|
630
638
|
};
|
|
631
639
|
const selectConsumer = async (consumerService) => {
|
|
632
640
|
const consumers = await fetchAllPages((pageToken) => consumerService.ListConsumers({
|
|
@@ -1006,6 +1014,7 @@ const providerValues = () => ({
|
|
|
1006
1014
|
});
|
|
1007
1015
|
const matchHeader = (line) => line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
1008
1016
|
const matchKey = (line) => line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
|
|
1017
|
+
const matchProviderValue = (line) => line.match(/^\s*model_provider\s*=\s*(['"]?)([^'"]+)\1\s*(?:#.*)?$/);
|
|
1009
1018
|
const mergeCodexToml = (content, input) => {
|
|
1010
1019
|
const updated = [...content.length ? content.split(/\r?\n/) : []];
|
|
1011
1020
|
const rootValueMap = rootValues(input);
|
|
@@ -1066,6 +1075,74 @@ const mergeAuthJson = (data, apiKey) => ({
|
|
|
1066
1075
|
...data,
|
|
1067
1076
|
OPENAI_API_KEY: apiKey
|
|
1068
1077
|
});
|
|
1078
|
+
const stripGetrouterProviderSection = (lines) => {
|
|
1079
|
+
const updated = [];
|
|
1080
|
+
let skipSection = false;
|
|
1081
|
+
for (const line of lines) {
|
|
1082
|
+
const headerMatch = matchHeader(line);
|
|
1083
|
+
if (headerMatch) {
|
|
1084
|
+
if ((headerMatch[1]?.trim() ?? "") === PROVIDER_SECTION) {
|
|
1085
|
+
skipSection = true;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
skipSection = false;
|
|
1089
|
+
}
|
|
1090
|
+
if (skipSection) continue;
|
|
1091
|
+
updated.push(line);
|
|
1092
|
+
}
|
|
1093
|
+
return updated;
|
|
1094
|
+
};
|
|
1095
|
+
const stripRootKeys = (lines) => {
|
|
1096
|
+
const updated = [];
|
|
1097
|
+
let currentSection = null;
|
|
1098
|
+
for (const line of lines) {
|
|
1099
|
+
const headerMatch = matchHeader(line);
|
|
1100
|
+
if (headerMatch) {
|
|
1101
|
+
currentSection = headerMatch[1]?.trim() ?? null;
|
|
1102
|
+
updated.push(line);
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (currentSection === null) {
|
|
1106
|
+
if (/^\s*model\s*=/.test(line)) continue;
|
|
1107
|
+
if (/^\s*model_reasoning_effort\s*=/.test(line)) continue;
|
|
1108
|
+
if (/^\s*model_provider\s*=/.test(line)) continue;
|
|
1109
|
+
}
|
|
1110
|
+
updated.push(line);
|
|
1111
|
+
}
|
|
1112
|
+
return updated;
|
|
1113
|
+
};
|
|
1114
|
+
const removeCodexConfig = (content) => {
|
|
1115
|
+
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
1116
|
+
let providerIsGetrouter = false;
|
|
1117
|
+
let currentSection = null;
|
|
1118
|
+
for (const line of lines) {
|
|
1119
|
+
const headerMatch = matchHeader(line);
|
|
1120
|
+
if (headerMatch) {
|
|
1121
|
+
currentSection = headerMatch[1]?.trim() ?? null;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (currentSection !== null) continue;
|
|
1125
|
+
if ((matchProviderValue(line)?.[2]?.trim())?.toLowerCase() === CODEX_PROVIDER) providerIsGetrouter = true;
|
|
1126
|
+
}
|
|
1127
|
+
let updated = stripGetrouterProviderSection(lines);
|
|
1128
|
+
if (providerIsGetrouter) updated = stripRootKeys(updated);
|
|
1129
|
+
const nextContent = updated.join("\n");
|
|
1130
|
+
return {
|
|
1131
|
+
content: nextContent,
|
|
1132
|
+
changed: nextContent !== content
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
const removeAuthJson = (data) => {
|
|
1136
|
+
if (!("OPENAI_API_KEY" in data)) return {
|
|
1137
|
+
data,
|
|
1138
|
+
changed: false
|
|
1139
|
+
};
|
|
1140
|
+
const { OPENAI_API_KEY: _ignored, ...rest } = data;
|
|
1141
|
+
return {
|
|
1142
|
+
data: rest,
|
|
1143
|
+
changed: true
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
1069
1146
|
|
|
1070
1147
|
//#endregion
|
|
1071
1148
|
//#region src/cmd/codex.ts
|
|
@@ -1084,6 +1161,7 @@ const ensureCodexDir = () => {
|
|
|
1084
1161
|
fs.mkdirSync(dir, { recursive: true });
|
|
1085
1162
|
return dir;
|
|
1086
1163
|
};
|
|
1164
|
+
const resolveCodexDir = () => path.join(os.homedir(), CODEX_DIR);
|
|
1087
1165
|
const requireInteractive$1 = () => {
|
|
1088
1166
|
if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
|
|
1089
1167
|
};
|
|
@@ -1099,7 +1177,8 @@ const promptReasoning = async (model) => await fuzzySelect({
|
|
|
1099
1177
|
});
|
|
1100
1178
|
const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
1101
1179
|
const registerCodexCommand = (program) => {
|
|
1102
|
-
program.command("codex").description("Configure Codex")
|
|
1180
|
+
const codex = program.command("codex").description("Configure Codex");
|
|
1181
|
+
codex.option("-m, --model <model>", "Set codex model (skips model selection)").action(async (options) => {
|
|
1103
1182
|
requireInteractive$1();
|
|
1104
1183
|
const model = options.model && options.model.trim().length > 0 ? options.model.trim() : await promptModel();
|
|
1105
1184
|
if (!model) return;
|
|
@@ -1112,18 +1191,10 @@ const registerCodexCommand = (program) => {
|
|
|
1112
1191
|
if (!apiKey) throw new Error("API key not found. Please create one or choose another.");
|
|
1113
1192
|
const reasoningValue = mapReasoningValue(reasoningId);
|
|
1114
1193
|
const keyName = selected.name?.trim() || "(unnamed)";
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
"Apply Codex configuration?",
|
|
1120
|
-
`Model: ${model}`,
|
|
1121
|
-
`Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`,
|
|
1122
|
-
"Provider: getrouter",
|
|
1123
|
-
`Key: ${keyName}`
|
|
1124
|
-
].join("\n"),
|
|
1125
|
-
initial: true
|
|
1126
|
-
})).confirm) return;
|
|
1194
|
+
console.log(`Model: ${model}`);
|
|
1195
|
+
console.log(`Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`);
|
|
1196
|
+
console.log("Provider: getrouter");
|
|
1197
|
+
console.log(`Key: ${keyName}`);
|
|
1127
1198
|
const codexDir = ensureCodexDir();
|
|
1128
1199
|
const configPath = path.join(codexDir, "config.toml");
|
|
1129
1200
|
const authPath = path.join(codexDir, "auth.json");
|
|
@@ -1138,6 +1209,28 @@ const registerCodexCommand = (program) => {
|
|
|
1138
1209
|
console.log("✅ Updated ~/.codex/config.toml");
|
|
1139
1210
|
console.log("✅ Updated ~/.codex/auth.json");
|
|
1140
1211
|
});
|
|
1212
|
+
codex.command("uninstall").description("Remove getrouter Codex configuration").action(() => {
|
|
1213
|
+
const codexDir = resolveCodexDir();
|
|
1214
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
1215
|
+
const authPath = path.join(codexDir, "auth.json");
|
|
1216
|
+
const configExists = fs.existsSync(configPath);
|
|
1217
|
+
const authExists = fs.existsSync(authPath);
|
|
1218
|
+
const configContent = configExists ? readFileIfExists(configPath) : "";
|
|
1219
|
+
const configResult = configExists ? removeCodexConfig(configContent) : null;
|
|
1220
|
+
const authContent = authExists ? fs.readFileSync(authPath, "utf8").trim() : "";
|
|
1221
|
+
const authData = authExists ? authContent ? JSON.parse(authContent) : {} : null;
|
|
1222
|
+
const authResult = authData ? removeAuthJson(authData) : null;
|
|
1223
|
+
if (!configExists) console.log(`ℹ️ ${configPath} not found`);
|
|
1224
|
+
else if (configResult?.changed) {
|
|
1225
|
+
fs.writeFileSync(configPath, configResult.content, "utf8");
|
|
1226
|
+
console.log(`✅ Removed getrouter entries from ${configPath}`);
|
|
1227
|
+
} else console.log(`ℹ️ No getrouter entries in ${configPath}`);
|
|
1228
|
+
if (!authExists) console.log(`ℹ️ ${authPath} not found`);
|
|
1229
|
+
else if (authResult?.changed) {
|
|
1230
|
+
fs.writeFileSync(authPath, JSON.stringify(authResult.data, null, 2));
|
|
1231
|
+
console.log(`✅ Removed getrouter entries from ${authPath}`);
|
|
1232
|
+
} else console.log(`ℹ️ No getrouter entries in ${authPath}`);
|
|
1233
|
+
});
|
|
1141
1234
|
};
|
|
1142
1235
|
|
|
1143
1236
|
//#endregion
|
|
@@ -1243,10 +1336,12 @@ const resolveConsumerForDelete = async (consumerService, id) => {
|
|
|
1243
1336
|
};
|
|
1244
1337
|
const createConsumer = async (consumerService) => {
|
|
1245
1338
|
requireInteractiveForAction("create");
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1339
|
+
const nameResult = await promptKeyName();
|
|
1340
|
+
if (nameResult.cancelled) return;
|
|
1341
|
+
const enabledResult = await promptKeyEnabled(true);
|
|
1342
|
+
if (enabledResult.cancelled) return;
|
|
1248
1343
|
let consumer = await consumerService.CreateConsumer({});
|
|
1249
|
-
consumer = await updateConsumer(consumerService, consumer, name, enabled);
|
|
1344
|
+
consumer = await updateConsumer(consumerService, consumer, nameResult.name, enabledResult.enabled);
|
|
1250
1345
|
outputConsumerTable(consumer, true);
|
|
1251
1346
|
console.log("Please store this API key securely.");
|
|
1252
1347
|
};
|
|
@@ -1254,7 +1349,11 @@ const updateConsumerById = async (consumerService, id) => {
|
|
|
1254
1349
|
requireInteractiveForAction("update");
|
|
1255
1350
|
const selected = await resolveConsumerForUpdate(consumerService, id);
|
|
1256
1351
|
if (!selected?.id) return;
|
|
1257
|
-
|
|
1352
|
+
const nameResult = await promptKeyName(selected.name);
|
|
1353
|
+
if (nameResult.cancelled) return;
|
|
1354
|
+
const enabledResult = await promptKeyEnabled(selected.enabled ?? true);
|
|
1355
|
+
if (enabledResult.cancelled) return;
|
|
1356
|
+
outputConsumerTable(await updateConsumer(consumerService, selected, nameResult.name, enabledResult.enabled), false);
|
|
1258
1357
|
};
|
|
1259
1358
|
const deleteConsumerById = async (consumerService, id) => {
|
|
1260
1359
|
requireInteractiveForAction("delete");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Codex Uninstall Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Add a `getrouter codex uninstall` subcommand that removes only getrouter-managed Codex configuration, without touching unrelated user settings.
|
|
5
|
+
|
|
6
|
+
## Scope
|
|
7
|
+
- Remove the `[model_providers.getrouter]` section from `~/.codex/config.toml`.
|
|
8
|
+
- Remove `model`, `model_reasoning_effort`, and `model_provider` only when `model_provider` is set to `"getrouter"`.
|
|
9
|
+
- Remove `OPENAI_API_KEY` from `~/.codex/auth.json`.
|
|
10
|
+
- Preserve all other keys, providers, comments, and formatting as much as possible.
|
|
11
|
+
|
|
12
|
+
## CLI Shape
|
|
13
|
+
- Keep the existing `getrouter codex` interactive flow unchanged.
|
|
14
|
+
- Add `getrouter codex uninstall` as a non-interactive subcommand.
|
|
15
|
+
- The uninstall action reports per-file status: removed, no-op, or missing.
|
|
16
|
+
|
|
17
|
+
## Data Flow
|
|
18
|
+
1. Resolve `~/.codex` via `os.homedir()`.
|
|
19
|
+
2. For each file:
|
|
20
|
+
- If missing, log and continue.
|
|
21
|
+
- Read content, apply a removal helper, and write back only if changed.
|
|
22
|
+
3. Abort with a clear error if parsing fails; avoid partial writes.
|
|
23
|
+
|
|
24
|
+
## Implementation Notes
|
|
25
|
+
- Extend `src/core/setup/codex.ts` with:
|
|
26
|
+
- `removeCodexConfig(content)` for TOML line removal.
|
|
27
|
+
- `removeAuthJson(data)` for JSON key removal.
|
|
28
|
+
- Update `src/cmd/codex.ts` to register the uninstall subcommand.
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
- Add command tests for uninstall in `tests/cmd/codex.test.ts`:
|
|
32
|
+
- Removes getrouter entries but keeps other providers/keys.
|
|
33
|
+
- No-op when getrouter entries do not exist.
|
|
34
|
+
- Root keys only removed when `model_provider == "getrouter"`.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Codex Uninstall Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add `getrouter codex uninstall` to remove getrouter-managed Codex config entries while preserving user data.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Extend `src/core/setup/codex.ts` with removal helpers for TOML and JSON. Wire a new `uninstall` subcommand in `src/cmd/codex.ts` that reads, cleans, and conditionally writes the files with clear status output.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Commander.js, Vitest, Node fs/path/os.
|
|
10
|
+
|
|
11
|
+
### Task 1: Add uninstall command tests
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Modify: `tests/cmd/codex.test.ts`
|
|
15
|
+
|
|
16
|
+
**Step 1: Write the failing tests**
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
it("uninstall removes getrouter entries but keeps others", async () => {
|
|
20
|
+
const dir = makeDir();
|
|
21
|
+
process.env.HOME = dir;
|
|
22
|
+
const codexDir = path.join(dir, ".codex");
|
|
23
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
24
|
+
fs.writeFileSync(
|
|
25
|
+
codexConfigPath(dir),
|
|
26
|
+
[
|
|
27
|
+
'theme = "dark"',
|
|
28
|
+
'model = "keep"',
|
|
29
|
+
'model_reasoning_effort = "low"',
|
|
30
|
+
'model_provider = "getrouter"',
|
|
31
|
+
"",
|
|
32
|
+
"[model_providers.getrouter]",
|
|
33
|
+
'name = "getrouter"',
|
|
34
|
+
'base_url = "https://api.getrouter.dev/codex"',
|
|
35
|
+
"",
|
|
36
|
+
"[model_providers.other]",
|
|
37
|
+
'name = "other"',
|
|
38
|
+
].join("\n"),
|
|
39
|
+
);
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
codexAuthPath(dir),
|
|
42
|
+
JSON.stringify({ OTHER: "keep", OPENAI_API_KEY: "old" }, null, 2),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const program = createProgram();
|
|
46
|
+
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
47
|
+
|
|
48
|
+
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
49
|
+
expect(config).toContain('theme = "dark"');
|
|
50
|
+
expect(config).toContain('[model_providers.other]');
|
|
51
|
+
expect(config).not.toContain('[model_providers.getrouter]');
|
|
52
|
+
expect(config).not.toContain('model_provider = "getrouter"');
|
|
53
|
+
|
|
54
|
+
const auth = JSON.parse(fs.readFileSync(codexAuthPath(dir), "utf8"));
|
|
55
|
+
expect(auth.OTHER).toBe("keep");
|
|
56
|
+
expect(auth.OPENAI_API_KEY).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("uninstall leaves root keys when provider is not getrouter", async () => {
|
|
60
|
+
const dir = makeDir();
|
|
61
|
+
process.env.HOME = dir;
|
|
62
|
+
const codexDir = path.join(dir, ".codex");
|
|
63
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
codexConfigPath(dir),
|
|
66
|
+
[
|
|
67
|
+
'model = "keep"',
|
|
68
|
+
'model_reasoning_effort = "low"',
|
|
69
|
+
'model_provider = "other"',
|
|
70
|
+
"",
|
|
71
|
+
"[model_providers.getrouter]",
|
|
72
|
+
'name = "getrouter"',
|
|
73
|
+
].join("\n"),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const program = createProgram();
|
|
77
|
+
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
78
|
+
|
|
79
|
+
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
80
|
+
expect(config).toContain('model = "keep"');
|
|
81
|
+
expect(config).toContain('model_provider = "other"');
|
|
82
|
+
expect(config).not.toContain('[model_providers.getrouter]');
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Step 2: Run test to verify it fails**
|
|
87
|
+
|
|
88
|
+
Run: `bun run test -- tests/cmd/codex.test.ts`
|
|
89
|
+
Expected: FAIL with an error like "unknown command 'uninstall'" or missing behavior.
|
|
90
|
+
|
|
91
|
+
**Step 3: Commit**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git add tests/cmd/codex.test.ts
|
|
95
|
+
git commit -m "test: cover codex uninstall"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Task 2: Implement removal helpers
|
|
99
|
+
|
|
100
|
+
**Files:**
|
|
101
|
+
- Modify: `src/core/setup/codex.ts`
|
|
102
|
+
|
|
103
|
+
**Step 1: Write the minimal implementation**
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
export const removeCodexConfig = (content: string) => {
|
|
107
|
+
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
108
|
+
const updated: string[] = [];
|
|
109
|
+
let inGetrouterSection = false;
|
|
110
|
+
let providerIsGetrouter = false;
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
113
|
+
const line = lines[i] ?? "";
|
|
114
|
+
const headerMatch = matchHeader(line);
|
|
115
|
+
if (headerMatch) {
|
|
116
|
+
const section = headerMatch[1]?.trim() ?? "";
|
|
117
|
+
inGetrouterSection = section === PROVIDER_SECTION;
|
|
118
|
+
if (inGetrouterSection) continue;
|
|
119
|
+
}
|
|
120
|
+
if (inGetrouterSection) continue;
|
|
121
|
+
|
|
122
|
+
const keyMatch = matchKey(line);
|
|
123
|
+
if (keyMatch && keyMatch[1] === "model_provider") {
|
|
124
|
+
providerIsGetrouter = /getrouter/i.test(line);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
updated.push(line);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (providerIsGetrouter) {
|
|
131
|
+
return {
|
|
132
|
+
content: updated
|
|
133
|
+
.filter(
|
|
134
|
+
(line) =>
|
|
135
|
+
!/^\s*model\s*=/.test(line) &&
|
|
136
|
+
!/^\s*model_reasoning_effort\s*=/.test(line) &&
|
|
137
|
+
!/^\s*model_provider\s*=/.test(line),
|
|
138
|
+
)
|
|
139
|
+
.join("\n"),
|
|
140
|
+
changed: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { content: updated.join("\n"), changed: updated.join("\n") !== content };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const removeAuthJson = (data: Record<string, unknown>) => {
|
|
148
|
+
if (!("OPENAI_API_KEY" in data)) return { data, changed: false };
|
|
149
|
+
const { OPENAI_API_KEY: _ignored, ...rest } = data;
|
|
150
|
+
return { data: rest, changed: true };
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Step 2: Run test to verify it still fails**
|
|
155
|
+
|
|
156
|
+
Run: `bun run test -- tests/cmd/codex.test.ts`
|
|
157
|
+
Expected: FAIL until CLI wiring is added.
|
|
158
|
+
|
|
159
|
+
**Step 3: Commit**
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git add src/core/setup/codex.ts
|
|
163
|
+
git commit -m "feat: add codex uninstall helpers"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Task 3: Wire the uninstall subcommand
|
|
167
|
+
|
|
168
|
+
**Files:**
|
|
169
|
+
- Modify: `src/cmd/codex.ts`
|
|
170
|
+
|
|
171
|
+
**Step 1: Add the uninstall command**
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { removeAuthJson, removeCodexConfig } from "../core/setup/codex";
|
|
175
|
+
|
|
176
|
+
const logMissing = (filePath: string) =>
|
|
177
|
+
console.log(`ℹ️ ${filePath} not found`);
|
|
178
|
+
|
|
179
|
+
const logNoop = (filePath: string) =>
|
|
180
|
+
console.log(`ℹ️ No getrouter entries in ${filePath}`);
|
|
181
|
+
|
|
182
|
+
const logRemoved = (filePath: string) =>
|
|
183
|
+
console.log(`✅ Removed getrouter entries from ${filePath}`);
|
|
184
|
+
|
|
185
|
+
// in registerCodexCommand
|
|
186
|
+
const codex = program.command("codex").description("Configure Codex");
|
|
187
|
+
|
|
188
|
+
codex
|
|
189
|
+
.command("uninstall")
|
|
190
|
+
.description("Remove getrouter Codex configuration")
|
|
191
|
+
.action(() => {
|
|
192
|
+
const codexDir = ensureCodexDir();
|
|
193
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
194
|
+
const authPath = path.join(codexDir, "auth.json");
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(configPath)) {
|
|
197
|
+
logMissing(configPath);
|
|
198
|
+
} else {
|
|
199
|
+
const content = readFileIfExists(configPath);
|
|
200
|
+
const removed = removeCodexConfig(content);
|
|
201
|
+
if (removed.changed) {
|
|
202
|
+
fs.writeFileSync(configPath, removed.content, "utf8");
|
|
203
|
+
logRemoved(configPath);
|
|
204
|
+
} else {
|
|
205
|
+
logNoop(configPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!fs.existsSync(authPath)) {
|
|
210
|
+
logMissing(authPath);
|
|
211
|
+
} else {
|
|
212
|
+
const raw = fs.readFileSync(authPath, "utf8").trim();
|
|
213
|
+
const data = raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
|
|
214
|
+
const removed = removeAuthJson(data);
|
|
215
|
+
if (removed.changed) {
|
|
216
|
+
fs.writeFileSync(authPath, JSON.stringify(removed.data, null, 2));
|
|
217
|
+
logRemoved(authPath);
|
|
218
|
+
} else {
|
|
219
|
+
logNoop(authPath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Step 2: Run tests to verify they pass**
|
|
226
|
+
|
|
227
|
+
Run: `bun run test -- tests/cmd/codex.test.ts`
|
|
228
|
+
Expected: PASS
|
|
229
|
+
|
|
230
|
+
**Step 3: Commit**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git add src/cmd/codex.ts tests/cmd/codex.test.ts
|
|
234
|
+
git commit -m "feat: add codex uninstall command"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Task 4: Final verification
|
|
238
|
+
|
|
239
|
+
**Files:**
|
|
240
|
+
- Verify: `src/cmd/codex.ts`, `src/core/setup/codex.ts`, `tests/cmd/codex.test.ts`
|
|
241
|
+
|
|
242
|
+
**Step 1: Run full checks**
|
|
243
|
+
|
|
244
|
+
Run: `bun run test && bun run lint && bun run format`
|
|
245
|
+
Expected: PASS
|
|
246
|
+
|
|
247
|
+
**Step 2: Commit formatting (if needed)**
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
git add src/cmd/codex.ts src/core/setup/codex.ts tests/cmd/codex.test.ts
|
|
251
|
+
git commit -m "chore: format codex uninstall"
|
|
252
|
+
```
|
package/package.json
CHANGED
package/src/cmd/codex.ts
CHANGED
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { Command } from "commander";
|
|
5
|
-
import prompts from "prompts";
|
|
6
5
|
import { createApiClients } from "../core/api/client";
|
|
7
6
|
import {
|
|
8
7
|
getCodexModelChoices,
|
|
@@ -12,7 +11,12 @@ import {
|
|
|
12
11
|
} from "../core/interactive/codex";
|
|
13
12
|
import { fuzzySelect } from "../core/interactive/fuzzy";
|
|
14
13
|
import { selectConsumer } from "../core/interactive/keys";
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
mergeAuthJson,
|
|
16
|
+
mergeCodexToml,
|
|
17
|
+
removeAuthJson,
|
|
18
|
+
removeCodexConfig,
|
|
19
|
+
} from "../core/setup/codex";
|
|
16
20
|
|
|
17
21
|
const CODEX_DIR = ".codex";
|
|
18
22
|
|
|
@@ -36,6 +40,8 @@ const ensureCodexDir = () => {
|
|
|
36
40
|
return dir;
|
|
37
41
|
};
|
|
38
42
|
|
|
43
|
+
const resolveCodexDir = () => path.join(os.homedir(), CODEX_DIR);
|
|
44
|
+
|
|
39
45
|
const requireInteractive = () => {
|
|
40
46
|
if (!process.stdin.isTTY) {
|
|
41
47
|
throw new Error("Interactive mode required for codex configuration.");
|
|
@@ -65,9 +71,9 @@ type CodexCommandOptions = {
|
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
export const registerCodexCommand = (program: Command) => {
|
|
68
|
-
program
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
const codex = program.command("codex").description("Configure Codex");
|
|
75
|
+
|
|
76
|
+
codex
|
|
71
77
|
.option("-m, --model <model>", "Set codex model (skips model selection)")
|
|
72
78
|
.action(async (options: CodexCommandOptions) => {
|
|
73
79
|
requireInteractive();
|
|
@@ -91,19 +97,13 @@ export const registerCodexCommand = (program: Command) => {
|
|
|
91
97
|
|
|
92
98
|
const reasoningValue = mapReasoningValue(reasoningId);
|
|
93
99
|
const keyName = selected.name?.trim() || "(unnamed)";
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"Provider: getrouter",
|
|
102
|
-
`Key: ${keyName}`,
|
|
103
|
-
].join("\n"),
|
|
104
|
-
initial: true,
|
|
105
|
-
});
|
|
106
|
-
if (!confirm.confirm) return;
|
|
100
|
+
|
|
101
|
+
console.log(`Model: ${model}`);
|
|
102
|
+
console.log(
|
|
103
|
+
`Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`,
|
|
104
|
+
);
|
|
105
|
+
console.log("Provider: getrouter");
|
|
106
|
+
console.log(`Key: ${keyName}`);
|
|
107
107
|
|
|
108
108
|
const codexDir = ensureCodexDir();
|
|
109
109
|
const configPath = path.join(codexDir, "config.toml");
|
|
@@ -126,4 +126,49 @@ export const registerCodexCommand = (program: Command) => {
|
|
|
126
126
|
console.log("✅ Updated ~/.codex/config.toml");
|
|
127
127
|
console.log("✅ Updated ~/.codex/auth.json");
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
codex
|
|
131
|
+
.command("uninstall")
|
|
132
|
+
.description("Remove getrouter Codex configuration")
|
|
133
|
+
.action(() => {
|
|
134
|
+
const codexDir = resolveCodexDir();
|
|
135
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
136
|
+
const authPath = path.join(codexDir, "auth.json");
|
|
137
|
+
|
|
138
|
+
const configExists = fs.existsSync(configPath);
|
|
139
|
+
const authExists = fs.existsSync(authPath);
|
|
140
|
+
|
|
141
|
+
const configContent = configExists ? readFileIfExists(configPath) : "";
|
|
142
|
+
const configResult = configExists
|
|
143
|
+
? removeCodexConfig(configContent)
|
|
144
|
+
: null;
|
|
145
|
+
|
|
146
|
+
const authContent = authExists
|
|
147
|
+
? fs.readFileSync(authPath, "utf8").trim()
|
|
148
|
+
: "";
|
|
149
|
+
const authData = authExists
|
|
150
|
+
? authContent
|
|
151
|
+
? (JSON.parse(authContent) as Record<string, unknown>)
|
|
152
|
+
: {}
|
|
153
|
+
: null;
|
|
154
|
+
const authResult = authData ? removeAuthJson(authData) : null;
|
|
155
|
+
|
|
156
|
+
if (!configExists) {
|
|
157
|
+
console.log(`ℹ️ ${configPath} not found`);
|
|
158
|
+
} else if (configResult?.changed) {
|
|
159
|
+
fs.writeFileSync(configPath, configResult.content, "utf8");
|
|
160
|
+
console.log(`✅ Removed getrouter entries from ${configPath}`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`ℹ️ No getrouter entries in ${configPath}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!authExists) {
|
|
166
|
+
console.log(`ℹ️ ${authPath} not found`);
|
|
167
|
+
} else if (authResult?.changed) {
|
|
168
|
+
fs.writeFileSync(authPath, JSON.stringify(authResult.data, null, 2));
|
|
169
|
+
console.log(`✅ Removed getrouter entries from ${authPath}`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`ℹ️ No getrouter entries in ${authPath}`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
129
174
|
};
|
package/src/cmd/keys.ts
CHANGED
|
@@ -132,10 +132,17 @@ const createConsumer = async (
|
|
|
132
132
|
consumerService: Pick<ConsumerService, "CreateConsumer" | "UpdateConsumer">,
|
|
133
133
|
) => {
|
|
134
134
|
requireInteractiveForAction("create");
|
|
135
|
-
const
|
|
136
|
-
|
|
135
|
+
const nameResult = await promptKeyName();
|
|
136
|
+
if (nameResult.cancelled) return;
|
|
137
|
+
const enabledResult = await promptKeyEnabled(true);
|
|
138
|
+
if (enabledResult.cancelled) return;
|
|
137
139
|
let consumer = await consumerService.CreateConsumer({});
|
|
138
|
-
consumer = await updateConsumer(
|
|
140
|
+
consumer = await updateConsumer(
|
|
141
|
+
consumerService,
|
|
142
|
+
consumer,
|
|
143
|
+
nameResult.name,
|
|
144
|
+
enabledResult.enabled,
|
|
145
|
+
);
|
|
139
146
|
outputConsumerTable(consumer, true);
|
|
140
147
|
console.log("Please store this API key securely.");
|
|
141
148
|
};
|
|
@@ -150,13 +157,15 @@ const updateConsumerById = async (
|
|
|
150
157
|
requireInteractiveForAction("update");
|
|
151
158
|
const selected = await resolveConsumerForUpdate(consumerService, id);
|
|
152
159
|
if (!selected?.id) return;
|
|
153
|
-
const
|
|
154
|
-
|
|
160
|
+
const nameResult = await promptKeyName(selected.name);
|
|
161
|
+
if (nameResult.cancelled) return;
|
|
162
|
+
const enabledResult = await promptKeyEnabled(selected.enabled ?? true);
|
|
163
|
+
if (enabledResult.cancelled) return;
|
|
155
164
|
const consumer = await updateConsumer(
|
|
156
165
|
consumerService,
|
|
157
166
|
selected,
|
|
158
|
-
name,
|
|
159
|
-
enabled,
|
|
167
|
+
nameResult.name,
|
|
168
|
+
enabledResult.enabled,
|
|
160
169
|
);
|
|
161
170
|
outputConsumerTable(consumer, false);
|
|
162
171
|
};
|
|
@@ -78,25 +78,38 @@ export const selectKeyAction = async (): Promise<KeyMenuAction> => {
|
|
|
78
78
|
|
|
79
79
|
export const promptKeyName = async (
|
|
80
80
|
initial?: string,
|
|
81
|
-
): Promise<
|
|
81
|
+
): Promise<
|
|
82
|
+
{ cancelled: true } | { cancelled: false; name: string | undefined }
|
|
83
|
+
> => {
|
|
82
84
|
const response = await prompts({
|
|
83
85
|
type: "text",
|
|
84
86
|
name: "name",
|
|
85
87
|
message: "Key name",
|
|
86
88
|
initial: initial ?? "",
|
|
87
89
|
});
|
|
90
|
+
if (!("name" in response)) {
|
|
91
|
+
return { cancelled: true };
|
|
92
|
+
}
|
|
88
93
|
const value = typeof response.name === "string" ? response.name.trim() : "";
|
|
89
|
-
return value.length > 0 ? value : undefined;
|
|
94
|
+
return { cancelled: false, name: value.length > 0 ? value : undefined };
|
|
90
95
|
};
|
|
91
96
|
|
|
92
|
-
export const promptKeyEnabled = async (
|
|
97
|
+
export const promptKeyEnabled = async (
|
|
98
|
+
initial: boolean,
|
|
99
|
+
): Promise<{ cancelled: true } | { cancelled: false; enabled: boolean }> => {
|
|
93
100
|
const response = await prompts({
|
|
94
101
|
type: "confirm",
|
|
95
102
|
name: "enabled",
|
|
96
103
|
message: "Enable this key?",
|
|
97
104
|
initial,
|
|
98
105
|
});
|
|
99
|
-
|
|
106
|
+
if (!("enabled" in response)) {
|
|
107
|
+
return { cancelled: true };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
cancelled: false,
|
|
111
|
+
enabled: typeof response.enabled === "boolean" ? response.enabled : initial,
|
|
112
|
+
};
|
|
100
113
|
};
|
|
101
114
|
|
|
102
115
|
export const selectConsumer = async (
|
package/src/core/setup/codex.ts
CHANGED
|
@@ -34,6 +34,8 @@ const providerValues = () => ({
|
|
|
34
34
|
|
|
35
35
|
const matchHeader = (line: string) => line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
36
36
|
const matchKey = (line: string) => line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
|
|
37
|
+
const matchProviderValue = (line: string) =>
|
|
38
|
+
line.match(/^\s*model_provider\s*=\s*(['"]?)([^'"]+)\1\s*(?:#.*)?$/);
|
|
37
39
|
|
|
38
40
|
export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
|
|
39
41
|
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
@@ -131,3 +133,85 @@ export const mergeAuthJson = (
|
|
|
131
133
|
...data,
|
|
132
134
|
OPENAI_API_KEY: apiKey,
|
|
133
135
|
});
|
|
136
|
+
|
|
137
|
+
const stripGetrouterProviderSection = (lines: string[]) => {
|
|
138
|
+
const updated: string[] = [];
|
|
139
|
+
let skipSection = false;
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const headerMatch = matchHeader(line);
|
|
143
|
+
if (headerMatch) {
|
|
144
|
+
const section = headerMatch[1]?.trim() ?? "";
|
|
145
|
+
if (section === PROVIDER_SECTION) {
|
|
146
|
+
skipSection = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
skipSection = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (skipSection) continue;
|
|
153
|
+
updated.push(line);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return updated;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const stripRootKeys = (lines: string[]) => {
|
|
160
|
+
const updated: string[] = [];
|
|
161
|
+
let currentSection: string | null = null;
|
|
162
|
+
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
const headerMatch = matchHeader(line);
|
|
165
|
+
if (headerMatch) {
|
|
166
|
+
currentSection = headerMatch[1]?.trim() ?? null;
|
|
167
|
+
updated.push(line);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (currentSection === null) {
|
|
172
|
+
if (/^\s*model\s*=/.test(line)) continue;
|
|
173
|
+
if (/^\s*model_reasoning_effort\s*=/.test(line)) continue;
|
|
174
|
+
if (/^\s*model_provider\s*=/.test(line)) continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
updated.push(line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return updated;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const removeCodexConfig = (content: string) => {
|
|
184
|
+
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
185
|
+
let providerIsGetrouter = false;
|
|
186
|
+
let currentSection: string | null = null;
|
|
187
|
+
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const headerMatch = matchHeader(line);
|
|
190
|
+
if (headerMatch) {
|
|
191
|
+
currentSection = headerMatch[1]?.trim() ?? null;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (currentSection !== null) continue;
|
|
196
|
+
|
|
197
|
+
const providerMatch = matchProviderValue(line);
|
|
198
|
+
const providerValue = providerMatch?.[2]?.trim();
|
|
199
|
+
if (providerValue?.toLowerCase() === CODEX_PROVIDER) {
|
|
200
|
+
providerIsGetrouter = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let updated = stripGetrouterProviderSection(lines);
|
|
205
|
+
if (providerIsGetrouter) {
|
|
206
|
+
updated = stripRootKeys(updated);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const nextContent = updated.join("\n");
|
|
210
|
+
return { content: nextContent, changed: nextContent !== content };
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const removeAuthJson = (data: Record<string, unknown>) => {
|
|
214
|
+
if (!("OPENAI_API_KEY" in data)) return { data, changed: false };
|
|
215
|
+
const { OPENAI_API_KEY: _ignored, ...rest } = data;
|
|
216
|
+
return { data: rest, changed: true };
|
|
217
|
+
};
|
package/tests/cmd/codex.test.ts
CHANGED
|
@@ -33,6 +33,7 @@ const originalEnv = Object.fromEntries(
|
|
|
33
33
|
|
|
34
34
|
afterEach(() => {
|
|
35
35
|
vi.unstubAllGlobals();
|
|
36
|
+
vi.clearAllMocks();
|
|
36
37
|
setStdinTTY(originalIsTTY);
|
|
37
38
|
prompts.inject([]);
|
|
38
39
|
for (const key of ENV_KEYS) {
|
|
@@ -70,7 +71,7 @@ describe("codex command", () => {
|
|
|
70
71
|
});
|
|
71
72
|
vi.stubGlobal("fetch", fetchMock);
|
|
72
73
|
|
|
73
|
-
prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer
|
|
74
|
+
prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer]);
|
|
74
75
|
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
75
76
|
consumerService: {
|
|
76
77
|
ListConsumers: vi.fn().mockResolvedValue({
|
|
@@ -100,7 +101,7 @@ describe("codex command", () => {
|
|
|
100
101
|
setStdinTTY(true);
|
|
101
102
|
const dir = makeDir();
|
|
102
103
|
process.env.HOME = dir;
|
|
103
|
-
prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer
|
|
104
|
+
prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer]);
|
|
104
105
|
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
105
106
|
consumerService: {
|
|
106
107
|
ListConsumers: vi.fn().mockResolvedValue({
|
|
@@ -162,7 +163,7 @@ describe("codex command", () => {
|
|
|
162
163
|
codexAuthPath(dir),
|
|
163
164
|
JSON.stringify({ OTHER: "keep", OPENAI_API_KEY: "old" }, null, 2),
|
|
164
165
|
);
|
|
165
|
-
prompts.inject(["gpt-5.2", "low", mockConsumer
|
|
166
|
+
prompts.inject(["gpt-5.2", "low", mockConsumer]);
|
|
166
167
|
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
167
168
|
consumerService: {
|
|
168
169
|
ListConsumers: vi.fn().mockResolvedValue({
|
|
@@ -201,7 +202,7 @@ describe("codex command", () => {
|
|
|
201
202
|
setStdinTTY(true);
|
|
202
203
|
const dir = makeDir();
|
|
203
204
|
process.env.HOME = dir;
|
|
204
|
-
prompts.inject(["extra_high", mockConsumer
|
|
205
|
+
prompts.inject(["extra_high", mockConsumer]);
|
|
205
206
|
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
206
207
|
consumerService: {
|
|
207
208
|
ListConsumers: vi.fn().mockResolvedValue({
|
|
@@ -230,4 +231,73 @@ describe("codex command", () => {
|
|
|
230
231
|
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
231
232
|
expect(config).toContain('model = "legacy-model"');
|
|
232
233
|
});
|
|
234
|
+
|
|
235
|
+
it("uninstall removes getrouter entries but keeps others", async () => {
|
|
236
|
+
const dir = makeDir();
|
|
237
|
+
process.env.HOME = dir;
|
|
238
|
+
const codexDir = path.join(dir, ".codex");
|
|
239
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
codexConfigPath(dir),
|
|
242
|
+
[
|
|
243
|
+
'theme = "dark"',
|
|
244
|
+
'model = "keep"',
|
|
245
|
+
'model_reasoning_effort = "low"',
|
|
246
|
+
'model_provider = "getrouter"',
|
|
247
|
+
"",
|
|
248
|
+
"[model_providers.getrouter]",
|
|
249
|
+
'name = "getrouter"',
|
|
250
|
+
'base_url = "https://api.getrouter.dev/codex"',
|
|
251
|
+
"",
|
|
252
|
+
"[model_providers.other]",
|
|
253
|
+
'name = "other"',
|
|
254
|
+
].join("\n"),
|
|
255
|
+
);
|
|
256
|
+
fs.writeFileSync(
|
|
257
|
+
codexAuthPath(dir),
|
|
258
|
+
JSON.stringify({ OTHER: "keep", OPENAI_API_KEY: "old" }, null, 2),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const program = createProgram();
|
|
262
|
+
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
263
|
+
|
|
264
|
+
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
265
|
+
expect(config).toContain('theme = "dark"');
|
|
266
|
+
expect(config).toContain("[model_providers.other]");
|
|
267
|
+
expect(config).not.toContain("[model_providers.getrouter]");
|
|
268
|
+
expect(config).not.toContain('model_provider = "getrouter"');
|
|
269
|
+
expect(config).not.toContain('model_reasoning_effort = "low"');
|
|
270
|
+
expect(config).not.toContain('model = "keep"');
|
|
271
|
+
|
|
272
|
+
const auth = JSON.parse(fs.readFileSync(codexAuthPath(dir), "utf8"));
|
|
273
|
+
expect(auth.OTHER).toBe("keep");
|
|
274
|
+
expect(auth.OPENAI_API_KEY).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("uninstall leaves root keys when provider is not getrouter", async () => {
|
|
278
|
+
const dir = makeDir();
|
|
279
|
+
process.env.HOME = dir;
|
|
280
|
+
const codexDir = path.join(dir, ".codex");
|
|
281
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
codexConfigPath(dir),
|
|
284
|
+
[
|
|
285
|
+
'model = "keep"',
|
|
286
|
+
'model_reasoning_effort = "low"',
|
|
287
|
+
'model_provider = "other"',
|
|
288
|
+
"",
|
|
289
|
+
"[model_providers.getrouter]",
|
|
290
|
+
'name = "getrouter"',
|
|
291
|
+
].join("\n"),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const program = createProgram();
|
|
295
|
+
await program.parseAsync(["node", "getrouter", "codex", "uninstall"]);
|
|
296
|
+
|
|
297
|
+
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
298
|
+
expect(config).toContain('model = "keep"');
|
|
299
|
+
expect(config).toContain('model_provider = "other"');
|
|
300
|
+
expect(config).toContain('model_reasoning_effort = "low"');
|
|
301
|
+
expect(config).not.toContain("[model_providers.getrouter]");
|
|
302
|
+
});
|
|
233
303
|
});
|