@getrouter/getrouter-cli 0.1.5 → 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
CHANGED
|
@@ -1014,6 +1014,7 @@ const providerValues = () => ({
|
|
|
1014
1014
|
});
|
|
1015
1015
|
const matchHeader = (line) => line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
1016
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*(?:#.*)?$/);
|
|
1017
1018
|
const mergeCodexToml = (content, input) => {
|
|
1018
1019
|
const updated = [...content.length ? content.split(/\r?\n/) : []];
|
|
1019
1020
|
const rootValueMap = rootValues(input);
|
|
@@ -1074,6 +1075,74 @@ const mergeAuthJson = (data, apiKey) => ({
|
|
|
1074
1075
|
...data,
|
|
1075
1076
|
OPENAI_API_KEY: apiKey
|
|
1076
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
|
+
};
|
|
1077
1146
|
|
|
1078
1147
|
//#endregion
|
|
1079
1148
|
//#region src/cmd/codex.ts
|
|
@@ -1092,6 +1161,7 @@ const ensureCodexDir = () => {
|
|
|
1092
1161
|
fs.mkdirSync(dir, { recursive: true });
|
|
1093
1162
|
return dir;
|
|
1094
1163
|
};
|
|
1164
|
+
const resolveCodexDir = () => path.join(os.homedir(), CODEX_DIR);
|
|
1095
1165
|
const requireInteractive$1 = () => {
|
|
1096
1166
|
if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
|
|
1097
1167
|
};
|
|
@@ -1107,7 +1177,8 @@ const promptReasoning = async (model) => await fuzzySelect({
|
|
|
1107
1177
|
});
|
|
1108
1178
|
const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
1109
1179
|
const registerCodexCommand = (program) => {
|
|
1110
|
-
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) => {
|
|
1111
1182
|
requireInteractive$1();
|
|
1112
1183
|
const model = options.model && options.model.trim().length > 0 ? options.model.trim() : await promptModel();
|
|
1113
1184
|
if (!model) return;
|
|
@@ -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
|
|
@@ -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
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
} from "../core/interactive/codex";
|
|
12
12
|
import { fuzzySelect } from "../core/interactive/fuzzy";
|
|
13
13
|
import { selectConsumer } from "../core/interactive/keys";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
mergeAuthJson,
|
|
16
|
+
mergeCodexToml,
|
|
17
|
+
removeAuthJson,
|
|
18
|
+
removeCodexConfig,
|
|
19
|
+
} from "../core/setup/codex";
|
|
15
20
|
|
|
16
21
|
const CODEX_DIR = ".codex";
|
|
17
22
|
|
|
@@ -35,6 +40,8 @@ const ensureCodexDir = () => {
|
|
|
35
40
|
return dir;
|
|
36
41
|
};
|
|
37
42
|
|
|
43
|
+
const resolveCodexDir = () => path.join(os.homedir(), CODEX_DIR);
|
|
44
|
+
|
|
38
45
|
const requireInteractive = () => {
|
|
39
46
|
if (!process.stdin.isTTY) {
|
|
40
47
|
throw new Error("Interactive mode required for codex configuration.");
|
|
@@ -64,9 +71,9 @@ type CodexCommandOptions = {
|
|
|
64
71
|
};
|
|
65
72
|
|
|
66
73
|
export const registerCodexCommand = (program: Command) => {
|
|
67
|
-
program
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
const codex = program.command("codex").description("Configure Codex");
|
|
75
|
+
|
|
76
|
+
codex
|
|
70
77
|
.option("-m, --model <model>", "Set codex model (skips model selection)")
|
|
71
78
|
.action(async (options: CodexCommandOptions) => {
|
|
72
79
|
requireInteractive();
|
|
@@ -119,4 +126,49 @@ export const registerCodexCommand = (program: Command) => {
|
|
|
119
126
|
console.log("✅ Updated ~/.codex/config.toml");
|
|
120
127
|
console.log("✅ Updated ~/.codex/auth.json");
|
|
121
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
|
+
});
|
|
122
174
|
};
|
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
|
@@ -231,4 +231,73 @@ describe("codex command", () => {
|
|
|
231
231
|
const config = fs.readFileSync(codexConfigPath(dir), "utf8");
|
|
232
232
|
expect(config).toContain('model = "legacy-model"');
|
|
233
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
|
+
});
|
|
234
303
|
});
|