@praeviso/code-env-switch 0.1.9 → 0.1.10
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.md +15 -1
- package/README_zh.md +14 -1
- package/bin/cli/help.js +1 -1
- package/bin/codex/config.js +273 -0
- package/bin/commands/launch.js +6 -21
- package/bin/commands/unset.js +4 -3
- package/bin/commands/use.js +11 -12
- package/bin/index.js +19 -1
- package/bin/profile/display.js +15 -1
- package/bin/statusline/codex.js +2 -18
- package/code-env.example.json +2 -2
- package/package.json +2 -2
- package/src/cli/help.ts +1 -1
- package/src/codex/config.ts +309 -0
- package/src/commands/launch.ts +10 -21
- package/src/commands/unset.ts +4 -2
- package/src/commands/use.ts +12 -12
- package/src/index.ts +30 -1
- package/src/profile/display.ts +17 -1
- package/src/statusline/codex.ts +1 -18
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# code-env-switch
|
|
2
2
|
|
|
3
|
-
A tiny CLI to switch between Claude Code and Codex
|
|
3
|
+
A tiny CLI to switch between Claude Code and Codex profiles.
|
|
4
4
|
|
|
5
5
|
[中文说明](README_zh.md)
|
|
6
6
|
|
|
@@ -103,6 +103,12 @@ codenv add --type codex primary OPENAI_BASE_URL=https://api.example.com/v1 OPENA
|
|
|
103
103
|
When `--type` is set, the profile name is kept as-is and `type` is stored separately.
|
|
104
104
|
Profiles are keyed by an internal id; the human-facing name lives in `profile.name`.
|
|
105
105
|
|
|
106
|
+
Codex profiles continue to store `OPENAI_BASE_URL` and `OPENAI_API_KEY` in the
|
|
107
|
+
JSON config for backward compatibility. When a Codex profile is applied,
|
|
108
|
+
`codenv` now unsets those shell variables, writes `~/.codex/config.toml` with
|
|
109
|
+
`model_provider = "OpenAI"` plus a `[model_providers.OpenAI]` block, and writes
|
|
110
|
+
`~/.codex/auth.json` with the matching API key.
|
|
111
|
+
|
|
106
112
|
Interactive add (default):
|
|
107
113
|
|
|
108
114
|
```bash
|
|
@@ -146,6 +152,11 @@ This wrapper makes `codenv use` and `codenv unset` apply automatically in the
|
|
|
146
152
|
current shell. To print the snippet without writing to rc, use
|
|
147
153
|
`codenv init --print`.
|
|
148
154
|
|
|
155
|
+
For Codex profiles, the generated shell snippet keeps reading the legacy
|
|
156
|
+
`OPENAI_*` values from your `codenv` profile, but the applied runtime state now
|
|
157
|
+
comes from `~/.codex/config.toml` and `~/.codex/auth.json` instead of exported
|
|
158
|
+
`OPENAI_BASE_URL` / `OPENAI_API_KEY` variables.
|
|
159
|
+
|
|
149
160
|
### Auto-apply default profiles (per type)
|
|
150
161
|
|
|
151
162
|
Set a default per type (codex/claude) and re-run `codenv init`:
|
|
@@ -189,6 +200,9 @@ codenv unset
|
|
|
189
200
|
eval "$(codenv unset)"
|
|
190
201
|
```
|
|
191
202
|
|
|
203
|
+
For Codex, `codenv unset` also restores the previous `~/.codex/config.toml` and
|
|
204
|
+
`~/.codex/auth.json` state captured before the last `codenv`-managed switch.
|
|
205
|
+
|
|
192
206
|
### Fish shell
|
|
193
207
|
|
|
194
208
|
```fish
|
package/README_zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# code-env-switch
|
|
2
2
|
|
|
3
|
-
一个轻量的 CLI,用于在 Claude Code 与 Codex
|
|
3
|
+
一个轻量的 CLI,用于在 Claude Code 与 Codex profile 之间快速切换。
|
|
4
4
|
|
|
5
5
|
[English](README.md)
|
|
6
6
|
|
|
@@ -103,6 +103,12 @@ codenv add --type codex primary OPENAI_BASE_URL=https://api.example.com/v1 OPENA
|
|
|
103
103
|
当设置 `--type` 时,名称保持不变,`type` 会单独存储。
|
|
104
104
|
profiles 使用内部 key,展示名称存放在 `profile.name`。
|
|
105
105
|
|
|
106
|
+
为了兼容旧配置,Codex profile 仍然在 JSON 里保存
|
|
107
|
+
`OPENAI_BASE_URL` / `OPENAI_API_KEY`。但在实际应用 Codex profile 时,
|
|
108
|
+
`codenv` 会先清理这些 shell 变量,再把对应值写入
|
|
109
|
+
`~/.codex/config.toml` 的 `model_provider = "OpenAI"` 与
|
|
110
|
+
`[model_providers.OpenAI]`,并同步写入 `~/.codex/auth.json`。
|
|
111
|
+
|
|
106
112
|
交互式添加(默认):
|
|
107
113
|
|
|
108
114
|
```bash
|
|
@@ -145,6 +151,10 @@ codenv init --shell zsh
|
|
|
145
151
|
该包装函数会让 `codenv use` 和 `codenv unset` 在当前终端自动生效。
|
|
146
152
|
如果只想打印片段而不写入,可用 `codenv init --print`。
|
|
147
153
|
|
|
154
|
+
对于 Codex profile,新的应用方式仍然读取旧 profile 里的 `OPENAI_*`
|
|
155
|
+
字段,但真正生效的是 `~/.codex/config.toml` 与 `~/.codex/auth.json`,
|
|
156
|
+
而不是导出的 `OPENAI_BASE_URL` / `OPENAI_API_KEY` 环境变量。
|
|
157
|
+
|
|
148
158
|
### 默认 profile 自动生效(按 type)
|
|
149
159
|
|
|
150
160
|
为不同 type 设置默认 profile,并重新执行一次 `codenv init`:
|
|
@@ -188,6 +198,9 @@ codenv unset
|
|
|
188
198
|
eval "$(codenv unset)"
|
|
189
199
|
```
|
|
190
200
|
|
|
201
|
+
对于 Codex,`codenv unset` 还会恢复 `codenv` 接管前备份的
|
|
202
|
+
`~/.codex/config.toml` 与 `~/.codex/auth.json`。
|
|
203
|
+
|
|
191
204
|
### Fish shell
|
|
192
205
|
|
|
193
206
|
```fish
|
package/bin/cli/help.js
CHANGED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCodexConfigPath = resolveCodexConfigPath;
|
|
4
|
+
exports.syncCodexProfile = syncCodexProfile;
|
|
5
|
+
exports.clearManagedCodexProfile = clearManagedCodexProfile;
|
|
6
|
+
exports.resolveCodexProfileFromEnv = resolveCodexProfileFromEnv;
|
|
7
|
+
/**
|
|
8
|
+
* Codex provider configuration management
|
|
9
|
+
*/
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const constants_1 = require("../constants");
|
|
14
|
+
const type_1 = require("../profile/type");
|
|
15
|
+
const utils_1 = require("../shell/utils");
|
|
16
|
+
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
17
|
+
const CODEX_PROVIDER_NAME = "OpenAI";
|
|
18
|
+
const CODEX_PROVIDER_WIRE_API = "responses";
|
|
19
|
+
function normalizeEnvValue(value) {
|
|
20
|
+
if (value === null || value === undefined)
|
|
21
|
+
return null;
|
|
22
|
+
const normalized = String(value).trim();
|
|
23
|
+
return normalized ? normalized : null;
|
|
24
|
+
}
|
|
25
|
+
function readTextIfExists(filePath) {
|
|
26
|
+
if (!fs.existsSync(filePath))
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(filePath, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeText(filePath, text) {
|
|
36
|
+
const dir = path.dirname(filePath);
|
|
37
|
+
if (!fs.existsSync(dir)) {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
fs.writeFileSync(filePath, text, "utf8");
|
|
41
|
+
}
|
|
42
|
+
function removeFileIfExists(filePath) {
|
|
43
|
+
if (!fs.existsSync(filePath))
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(filePath);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// ignore cleanup failures
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getBackupPath(configPath) {
|
|
53
|
+
return `${configPath}.codenv-provider-backup.json`;
|
|
54
|
+
}
|
|
55
|
+
function readBackup(backupPath) {
|
|
56
|
+
const raw = readTextIfExists(backupPath);
|
|
57
|
+
if (!raw)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
return {
|
|
62
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
63
|
+
modelProviderLine: typeof parsed.modelProviderLine === "string"
|
|
64
|
+
? parsed.modelProviderLine
|
|
65
|
+
: null,
|
|
66
|
+
providerSectionText: typeof parsed.providerSectionText === "string"
|
|
67
|
+
? parsed.providerSectionText
|
|
68
|
+
: null,
|
|
69
|
+
authText: typeof parsed.authText === "string" ? parsed.authText : null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function writeBackup(backupPath, backup) {
|
|
77
|
+
writeText(backupPath, `${JSON.stringify(backup, null, 2)}\n`);
|
|
78
|
+
}
|
|
79
|
+
function parseSectionByHeader(text, headerRegex) {
|
|
80
|
+
var _a;
|
|
81
|
+
const match = headerRegex.exec(text);
|
|
82
|
+
if (!match || match.index === undefined)
|
|
83
|
+
return null;
|
|
84
|
+
const start = match.index;
|
|
85
|
+
const afterHeader = start + match[0].length;
|
|
86
|
+
const rest = text.slice(afterHeader);
|
|
87
|
+
const nextHeaderMatch = rest.match(/^\s*\[.*?\]\s*$/m);
|
|
88
|
+
const end = nextHeaderMatch
|
|
89
|
+
? afterHeader + ((_a = nextHeaderMatch.index) !== null && _a !== void 0 ? _a : rest.length)
|
|
90
|
+
: text.length;
|
|
91
|
+
return {
|
|
92
|
+
start,
|
|
93
|
+
end,
|
|
94
|
+
sectionText: text.slice(start, end).trimEnd(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function getFirstSectionIndex(text) {
|
|
98
|
+
const match = text.match(/^\s*\[.*?\]\s*$/m);
|
|
99
|
+
if (!match || match.index === undefined)
|
|
100
|
+
return text.length;
|
|
101
|
+
return match.index;
|
|
102
|
+
}
|
|
103
|
+
function getRootText(text) {
|
|
104
|
+
const index = getFirstSectionIndex(text);
|
|
105
|
+
return {
|
|
106
|
+
root: text.slice(0, index),
|
|
107
|
+
rest: text.slice(index),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function readModelProviderLine(text) {
|
|
111
|
+
const { root } = getRootText(text);
|
|
112
|
+
const match = root.match(/^\s*model_provider\s*=.*$/m);
|
|
113
|
+
return match ? match[0].trimEnd() : null;
|
|
114
|
+
}
|
|
115
|
+
function removeModelProviderLine(text) {
|
|
116
|
+
const { root, rest } = getRootText(text);
|
|
117
|
+
const updatedRoot = root.replace(/^\s*model_provider\s*=.*(?:\r?\n)?/m, "");
|
|
118
|
+
return `${updatedRoot}${rest}`;
|
|
119
|
+
}
|
|
120
|
+
function insertRootLine(text, line) {
|
|
121
|
+
const { root, rest } = getRootText(text);
|
|
122
|
+
const trimmedRoot = root.trimEnd();
|
|
123
|
+
const trimmedRest = rest.trimStart();
|
|
124
|
+
if (!trimmedRoot) {
|
|
125
|
+
if (!trimmedRest)
|
|
126
|
+
return `${line}\n`;
|
|
127
|
+
return `${line}\n\n${trimmedRest}`;
|
|
128
|
+
}
|
|
129
|
+
if (!trimmedRest) {
|
|
130
|
+
return `${trimmedRoot}\n${line}\n`;
|
|
131
|
+
}
|
|
132
|
+
return `${trimmedRoot}\n${line}\n\n${trimmedRest}`;
|
|
133
|
+
}
|
|
134
|
+
function removeSection(text, headerRegex) {
|
|
135
|
+
const range = parseSectionByHeader(text, headerRegex);
|
|
136
|
+
if (!range)
|
|
137
|
+
return text;
|
|
138
|
+
const before = text.slice(0, range.start).trimEnd();
|
|
139
|
+
const after = text.slice(range.end).trimStart();
|
|
140
|
+
if (before && after)
|
|
141
|
+
return `${before}\n\n${after}`;
|
|
142
|
+
if (before)
|
|
143
|
+
return `${before}\n`;
|
|
144
|
+
if (after)
|
|
145
|
+
return `${after}\n`;
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
function appendSection(text, sectionText) {
|
|
149
|
+
const trimmed = text.trimEnd();
|
|
150
|
+
if (!trimmed)
|
|
151
|
+
return `${sectionText}\n`;
|
|
152
|
+
return `${trimmed}\n\n${sectionText}\n`;
|
|
153
|
+
}
|
|
154
|
+
function getProviderHeaderRegex() {
|
|
155
|
+
return /^\s*\[model_providers\.OpenAI\]\s*$/m;
|
|
156
|
+
}
|
|
157
|
+
function readProviderSectionText(text) {
|
|
158
|
+
const range = parseSectionByHeader(text, getProviderHeaderRegex());
|
|
159
|
+
return range ? range.sectionText : null;
|
|
160
|
+
}
|
|
161
|
+
function renderProviderSection(baseUrl) {
|
|
162
|
+
const lines = [
|
|
163
|
+
`[model_providers.${CODEX_PROVIDER_NAME}]`,
|
|
164
|
+
`name = ${JSON.stringify(CODEX_PROVIDER_NAME)}`,
|
|
165
|
+
];
|
|
166
|
+
if (baseUrl) {
|
|
167
|
+
lines.push(`base_url = ${JSON.stringify(baseUrl)}`);
|
|
168
|
+
}
|
|
169
|
+
lines.push(`wire_api = ${JSON.stringify(CODEX_PROVIDER_WIRE_API)}`);
|
|
170
|
+
lines.push("requires_openai_auth = true");
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
function ensureBackup(configPath, currentConfigText) {
|
|
174
|
+
const backupPath = getBackupPath(configPath);
|
|
175
|
+
if (readBackup(backupPath))
|
|
176
|
+
return;
|
|
177
|
+
writeBackup(backupPath, {
|
|
178
|
+
version: 1,
|
|
179
|
+
modelProviderLine: readModelProviderLine(currentConfigText),
|
|
180
|
+
providerSectionText: readProviderSectionText(currentConfigText),
|
|
181
|
+
authText: readTextIfExists(constants_1.CODEX_AUTH_PATH),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function writeManagedConfig(configPath, baseUrl) {
|
|
185
|
+
const currentText = readTextIfExists(configPath) || "";
|
|
186
|
+
ensureBackup(configPath, currentText);
|
|
187
|
+
let updated = currentText;
|
|
188
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
189
|
+
updated = removeModelProviderLine(updated);
|
|
190
|
+
updated = insertRootLine(updated, `model_provider = ${JSON.stringify(CODEX_PROVIDER_NAME)}`);
|
|
191
|
+
updated = appendSection(updated, renderProviderSection(baseUrl));
|
|
192
|
+
writeText(configPath, updated);
|
|
193
|
+
}
|
|
194
|
+
function writeManagedAuth(apiKey) {
|
|
195
|
+
const authJson = apiKey === null
|
|
196
|
+
? "null"
|
|
197
|
+
: JSON.stringify({ OPENAI_API_KEY: apiKey });
|
|
198
|
+
writeText(constants_1.CODEX_AUTH_PATH, `${authJson}\n`);
|
|
199
|
+
}
|
|
200
|
+
function restoreAuth(authText) {
|
|
201
|
+
if (authText === null) {
|
|
202
|
+
removeFileIfExists(constants_1.CODEX_AUTH_PATH);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
writeText(constants_1.CODEX_AUTH_PATH, authText);
|
|
206
|
+
}
|
|
207
|
+
function resolveCodexConfigPath(config) {
|
|
208
|
+
var _a;
|
|
209
|
+
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
210
|
+
if (envOverride && String(envOverride).trim()) {
|
|
211
|
+
const expanded = (0, utils_1.expandEnv)(String(envOverride).trim());
|
|
212
|
+
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
213
|
+
}
|
|
214
|
+
const configOverride = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.configPath;
|
|
215
|
+
if (configOverride && String(configOverride).trim()) {
|
|
216
|
+
const expanded = (0, utils_1.expandEnv)(String(configOverride).trim());
|
|
217
|
+
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
218
|
+
}
|
|
219
|
+
return DEFAULT_CODEX_CONFIG_PATH;
|
|
220
|
+
}
|
|
221
|
+
function syncCodexProfile(config, profileName) {
|
|
222
|
+
const profile = config.profiles && config.profiles[profileName];
|
|
223
|
+
if (!profile) {
|
|
224
|
+
throw new Error(`Unknown profile: ${profileName}`);
|
|
225
|
+
}
|
|
226
|
+
const env = profile.env || {};
|
|
227
|
+
const baseUrl = normalizeEnvValue(env.OPENAI_BASE_URL);
|
|
228
|
+
const apiKey = normalizeEnvValue(env.OPENAI_API_KEY);
|
|
229
|
+
const configPath = resolveCodexConfigPath(config);
|
|
230
|
+
writeManagedConfig(configPath, baseUrl);
|
|
231
|
+
writeManagedAuth(apiKey);
|
|
232
|
+
}
|
|
233
|
+
function clearManagedCodexProfile(config) {
|
|
234
|
+
const configPath = resolveCodexConfigPath(config);
|
|
235
|
+
const backupPath = getBackupPath(configPath);
|
|
236
|
+
const backup = readBackup(backupPath);
|
|
237
|
+
if (!backup)
|
|
238
|
+
return;
|
|
239
|
+
let updated = readTextIfExists(configPath) || "";
|
|
240
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
241
|
+
updated = removeModelProviderLine(updated);
|
|
242
|
+
if (backup.modelProviderLine) {
|
|
243
|
+
updated = insertRootLine(updated, backup.modelProviderLine);
|
|
244
|
+
}
|
|
245
|
+
if (backup.providerSectionText) {
|
|
246
|
+
updated = appendSection(updated, backup.providerSectionText);
|
|
247
|
+
}
|
|
248
|
+
const trimmed = updated.trimEnd();
|
|
249
|
+
if (trimmed) {
|
|
250
|
+
writeText(configPath, `${trimmed}\n`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
removeFileIfExists(configPath);
|
|
254
|
+
}
|
|
255
|
+
restoreAuth(backup.authText);
|
|
256
|
+
removeFileIfExists(backupPath);
|
|
257
|
+
}
|
|
258
|
+
function resolveCodexProfileFromEnv(config, profileKey, profileName) {
|
|
259
|
+
const profiles = config.profiles || {};
|
|
260
|
+
if (profileKey && profiles[profileKey])
|
|
261
|
+
return profileKey;
|
|
262
|
+
if (!profileName)
|
|
263
|
+
return null;
|
|
264
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
265
|
+
if ((0, type_1.inferProfileType)(key, profile || {}, null) !== "codex")
|
|
266
|
+
continue;
|
|
267
|
+
const displayName = (0, type_1.getProfileDisplayName)(key, profile || {}, "codex");
|
|
268
|
+
if (displayName === profileName || key === profileName) {
|
|
269
|
+
return key;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
package/bin/commands/launch.js
CHANGED
|
@@ -7,11 +7,11 @@ exports.runLaunch = runLaunch;
|
|
|
7
7
|
const fs = require("fs");
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
|
-
const constants_1 = require("../constants");
|
|
11
10
|
const type_1 = require("../profile/type");
|
|
12
11
|
const usage_1 = require("../usage");
|
|
13
12
|
const claude_1 = require("../statusline/claude");
|
|
14
13
|
const codex_1 = require("../statusline/codex");
|
|
14
|
+
const config_1 = require("../codex/config");
|
|
15
15
|
const SESSION_BINDING_POLL_MS = 1000;
|
|
16
16
|
const SESSION_BINDING_START_GRACE_MS = 5000;
|
|
17
17
|
function isRecord(value) {
|
|
@@ -184,24 +184,6 @@ function getProfileEnv(type) {
|
|
|
184
184
|
const name = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`] || key;
|
|
185
185
|
return { key, name };
|
|
186
186
|
}
|
|
187
|
-
function writeCodexAuthFromEnv() {
|
|
188
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
189
|
-
try {
|
|
190
|
-
fs.mkdirSync(path.dirname(constants_1.CODEX_AUTH_PATH), { recursive: true });
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// ignore
|
|
194
|
-
}
|
|
195
|
-
const authJson = apiKey === null || apiKey === undefined || apiKey === ""
|
|
196
|
-
? "null"
|
|
197
|
-
: JSON.stringify({ OPENAI_API_KEY: String(apiKey) });
|
|
198
|
-
try {
|
|
199
|
-
fs.writeFileSync(constants_1.CODEX_AUTH_PATH, `${authJson}\n`, "utf8");
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
// ignore
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
187
|
function parseBooleanEnv(value) {
|
|
206
188
|
if (value === undefined)
|
|
207
189
|
return null;
|
|
@@ -234,10 +216,13 @@ async function runLaunch(config, configPath, target, args) {
|
|
|
234
216
|
if (!type) {
|
|
235
217
|
throw new Error(`Unknown launch target: ${target}`);
|
|
236
218
|
}
|
|
219
|
+
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
237
220
|
if (type === "codex") {
|
|
238
|
-
|
|
221
|
+
const codexProfileKey = (0, config_1.resolveCodexProfileFromEnv)(config, profileKey, profileName);
|
|
222
|
+
if (codexProfileKey) {
|
|
223
|
+
(0, config_1.syncCodexProfile)(config, codexProfileKey);
|
|
224
|
+
}
|
|
239
225
|
}
|
|
240
|
-
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
241
226
|
const terminalTag = process.env.CODE_ENV_TERMINAL_TAG || null;
|
|
242
227
|
const cwd = process.cwd();
|
|
243
228
|
const startMs = Date.now();
|
package/bin/commands/unset.js
CHANGED
|
@@ -13,8 +13,9 @@ function printUnset(config) {
|
|
|
13
13
|
for (const key of (0, defaults_1.getTypeDefaultUnsetKeys)(type))
|
|
14
14
|
keySet.add(key);
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const lines = ["command codenv __codex-clear"];
|
|
17
|
+
if (keySet.size > 0) {
|
|
18
|
+
lines.push(...Array.from(keySet, (key) => `unset ${key}`));
|
|
19
|
+
}
|
|
19
20
|
console.log(lines.join("\n"));
|
|
20
21
|
}
|
package/bin/commands/use.js
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildUseLines = buildUseLines;
|
|
4
4
|
exports.printUse = printUse;
|
|
5
|
-
/**
|
|
6
|
-
* Use command - apply profile environment
|
|
7
|
-
*/
|
|
8
|
-
const path = require("path");
|
|
9
|
-
const constants_1 = require("../constants");
|
|
10
5
|
const utils_1 = require("../shell/utils");
|
|
11
6
|
const type_1 = require("../profile/type");
|
|
12
7
|
const match_1 = require("../profile/match");
|
|
@@ -23,6 +18,9 @@ function buildUseLines(config, profileName, requestedType, includeGlobalUnset, c
|
|
|
23
18
|
const unsetKeys = new Set();
|
|
24
19
|
const activeType = (0, type_1.inferProfileType)(profileName, profile, requestedType);
|
|
25
20
|
const effectiveEnv = (0, display_1.buildEffectiveEnv)(profile, activeType);
|
|
21
|
+
const managedCodexKeys = activeType === "codex"
|
|
22
|
+
? new Set(["OPENAI_BASE_URL", "OPENAI_API_KEY"])
|
|
23
|
+
: new Set();
|
|
26
24
|
const addUnset = (key) => {
|
|
27
25
|
if (unsetKeys.has(key))
|
|
28
26
|
return;
|
|
@@ -42,6 +40,13 @@ function buildUseLines(config, profileName, requestedType, includeGlobalUnset, c
|
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
for (const key of Object.keys(effectiveEnv)) {
|
|
43
|
+
if (managedCodexKeys.has(key)) {
|
|
44
|
+
if (!unsetKeys.has(key)) {
|
|
45
|
+
unsetKeys.add(key);
|
|
46
|
+
unsetLines.push(`unset ${key}`);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
45
50
|
const value = effectiveEnv[key];
|
|
46
51
|
if (value === null || value === undefined || value === "") {
|
|
47
52
|
if (!unsetKeys.has(key)) {
|
|
@@ -63,13 +68,7 @@ function buildUseLines(config, profileName, requestedType, includeGlobalUnset, c
|
|
|
63
68
|
exportLines.push(`export CODE_ENV_CONFIG_PATH=${(0, utils_1.shellEscape)(configPath)}`);
|
|
64
69
|
}
|
|
65
70
|
if ((0, match_1.shouldRemoveCodexAuth)(profileName, profile, requestedType)) {
|
|
66
|
-
|
|
67
|
-
const authDir = path.dirname(constants_1.CODEX_AUTH_PATH);
|
|
68
|
-
const authJson = codexApiKey === null || codexApiKey === undefined || codexApiKey === ""
|
|
69
|
-
? "null"
|
|
70
|
-
: JSON.stringify({ OPENAI_API_KEY: String(codexApiKey) });
|
|
71
|
-
postLines.push(`mkdir -p ${(0, utils_1.shellEscape)(authDir)}`);
|
|
72
|
-
postLines.push(`printf '%s\\n' ${(0, utils_1.shellEscape)(authJson)} > ${(0, utils_1.shellEscape)(constants_1.CODEX_AUTH_PATH)}`);
|
|
71
|
+
postLines.push("command codenv __codex-sync");
|
|
73
72
|
}
|
|
74
73
|
if (Array.isArray(profile.removeFiles)) {
|
|
75
74
|
for (const p of profile.removeFiles) {
|
package/bin/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
/**
|
|
5
|
-
* codenv - switch Claude/Codex
|
|
5
|
+
* codenv - switch Claude/Codex profiles
|
|
6
6
|
* Main entry point
|
|
7
7
|
*/
|
|
8
8
|
const fs = require("fs");
|
|
@@ -14,6 +14,7 @@ const profile_1 = require("./profile");
|
|
|
14
14
|
const commands_1 = require("./commands");
|
|
15
15
|
const usage_1 = require("./usage");
|
|
16
16
|
const ui_1 = require("./ui");
|
|
17
|
+
const config_2 = require("./codex/config");
|
|
17
18
|
function getErrorMessage(err) {
|
|
18
19
|
return err instanceof Error ? err.message : String(err);
|
|
19
20
|
}
|
|
@@ -30,6 +31,23 @@ async function main() {
|
|
|
30
31
|
}
|
|
31
32
|
const cmd = args[0];
|
|
32
33
|
try {
|
|
34
|
+
if (cmd === "__codex-sync") {
|
|
35
|
+
const configPath = process.env.CODE_ENV_CONFIG_PATH || (0, config_1.findConfigPath)(parsed.configPath);
|
|
36
|
+
const config = (0, config_1.readConfigIfExists)(configPath);
|
|
37
|
+
const profileKey = process.env.CODE_ENV_PROFILE_KEY_CODEX || null;
|
|
38
|
+
const profileName = process.env.CODE_ENV_PROFILE_NAME_CODEX || null;
|
|
39
|
+
const resolvedProfile = (0, config_2.resolveCodexProfileFromEnv)(config, profileKey, profileName);
|
|
40
|
+
if (!resolvedProfile)
|
|
41
|
+
return;
|
|
42
|
+
(0, config_2.syncCodexProfile)(config, resolvedProfile);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (cmd === "__codex-clear") {
|
|
46
|
+
const configPath = process.env.CODE_ENV_CONFIG_PATH || (0, config_1.findConfigPath)(parsed.configPath);
|
|
47
|
+
const config = (0, config_1.readConfigIfExists)(configPath);
|
|
48
|
+
(0, config_2.clearManagedCodexProfile)(config);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
33
51
|
if (cmd === "init") {
|
|
34
52
|
const initArgs = (0, cli_1.parseInitArgs)(args.slice(1));
|
|
35
53
|
const shellName = (0, shell_1.detectShell)(initArgs.shell);
|
package/bin/profile/display.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isEnvValueUnset = isEnvValueUnset;
|
|
4
4
|
exports.buildEffectiveEnv = buildEffectiveEnv;
|
|
5
5
|
exports.envMatchesProfile = envMatchesProfile;
|
|
6
|
+
exports.markerMatchesProfile = markerMatchesProfile;
|
|
6
7
|
exports.buildListRows = buildListRows;
|
|
7
8
|
const constants_1 = require("../constants");
|
|
8
9
|
const type_1 = require("./type");
|
|
@@ -37,6 +38,18 @@ function envMatchesProfile(profile) {
|
|
|
37
38
|
}
|
|
38
39
|
return Object.keys(profile.env).length > 0;
|
|
39
40
|
}
|
|
41
|
+
function markerMatchesProfile(profileKey, profile, activeType) {
|
|
42
|
+
if (!activeType)
|
|
43
|
+
return false;
|
|
44
|
+
const suffix = activeType.toUpperCase();
|
|
45
|
+
const activeKey = process.env[`CODE_ENV_PROFILE_KEY_${suffix}`];
|
|
46
|
+
if (activeKey)
|
|
47
|
+
return activeKey === profileKey;
|
|
48
|
+
const activeName = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`];
|
|
49
|
+
if (!activeName || !profile)
|
|
50
|
+
return false;
|
|
51
|
+
return activeName === (0, type_1.getProfileDisplayName)(profileKey, profile, activeType);
|
|
52
|
+
}
|
|
40
53
|
// Forward declaration to avoid circular dependency
|
|
41
54
|
// getResolvedDefaultProfileKeys will be imported from config/defaults
|
|
42
55
|
function buildListRows(config, getResolvedDefaultProfileKeys) {
|
|
@@ -62,7 +75,8 @@ function buildListRows(config, getResolvedDefaultProfileKeys) {
|
|
|
62
75
|
if (note)
|
|
63
76
|
noteParts.push(note);
|
|
64
77
|
const noteText = noteParts.join(" | ");
|
|
65
|
-
const active =
|
|
78
|
+
const active = markerMatchesProfile(key, safeProfile, usageType) ||
|
|
79
|
+
envMatchesProfile(safeProfile);
|
|
66
80
|
return { key, name: displayName, type, note: noteText, active, usageType };
|
|
67
81
|
});
|
|
68
82
|
rows.sort((a, b) => {
|
package/bin/statusline/codex.js
CHANGED
|
@@ -5,11 +5,9 @@ exports.ensureCodexStatuslineConfig = ensureCodexStatuslineConfig;
|
|
|
5
5
|
* Codex CLI status line integration (official schema)
|
|
6
6
|
*/
|
|
7
7
|
const fs = require("fs");
|
|
8
|
-
const os = require("os");
|
|
9
8
|
const path = require("path");
|
|
10
|
-
const
|
|
9
|
+
const config_1 = require("../codex/config");
|
|
11
10
|
const ui_1 = require("../ui");
|
|
12
|
-
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
13
11
|
function parseBooleanEnv(value) {
|
|
14
12
|
if (value === undefined)
|
|
15
13
|
return null;
|
|
@@ -20,20 +18,6 @@ function parseBooleanEnv(value) {
|
|
|
20
18
|
return false;
|
|
21
19
|
return null;
|
|
22
20
|
}
|
|
23
|
-
function resolveCodexConfigPath(config) {
|
|
24
|
-
var _a;
|
|
25
|
-
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
26
|
-
if (envOverride && String(envOverride).trim()) {
|
|
27
|
-
const expanded = (0, utils_1.expandEnv)(String(envOverride).trim());
|
|
28
|
-
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
29
|
-
}
|
|
30
|
-
const configOverride = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.configPath;
|
|
31
|
-
if (configOverride && String(configOverride).trim()) {
|
|
32
|
-
const expanded = (0, utils_1.expandEnv)(String(configOverride).trim());
|
|
33
|
-
return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
34
|
-
}
|
|
35
|
-
return DEFAULT_CODEX_CONFIG_PATH;
|
|
36
|
-
}
|
|
37
21
|
function resolveDesiredStatusLineItems(config) {
|
|
38
22
|
var _a;
|
|
39
23
|
const raw = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.items;
|
|
@@ -49,7 +33,7 @@ function resolveDesiredStatusLineConfig(config) {
|
|
|
49
33
|
return null;
|
|
50
34
|
return {
|
|
51
35
|
statusLineItems,
|
|
52
|
-
configPath: resolveCodexConfigPath(config),
|
|
36
|
+
configPath: (0, config_1.resolveCodexConfigPath)(config),
|
|
53
37
|
};
|
|
54
38
|
}
|
|
55
39
|
function readConfig(filePath) {
|
package/code-env.example.json
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"p_a1b2c3": {
|
|
72
72
|
"name": "primary",
|
|
73
73
|
"type": "codex",
|
|
74
|
-
"note": "Primary endpoint",
|
|
74
|
+
"note": "Primary endpoint (applied via ~/.codex/config.toml)",
|
|
75
75
|
"env": {
|
|
76
76
|
"OPENAI_BASE_URL": "https://api.example.com/v1",
|
|
77
77
|
"OPENAI_API_KEY": "YOUR_API_KEY"
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"p_d4e5f6": {
|
|
84
84
|
"name": "secondary",
|
|
85
85
|
"type": "codex",
|
|
86
|
-
"note": "Secondary endpoint",
|
|
86
|
+
"note": "Secondary endpoint (applied via ~/.codex/config.toml)",
|
|
87
87
|
"env": {
|
|
88
88
|
"OPENAI_BASE_URL": "https://api.secondary.example.com/v1",
|
|
89
89
|
"OPENAI_API_KEY": "YOUR_API_KEY"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praeviso/code-env-switch",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Switch between Claude Code and Codex
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Switch between Claude Code and Codex profiles from a single CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codenv": "bin/index.js"
|
|
7
7
|
},
|
package/src/cli/help.ts
CHANGED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex provider configuration management
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import type { Config, EnvValue } from "../types";
|
|
8
|
+
import { CODEX_AUTH_PATH } from "../constants";
|
|
9
|
+
import { getProfileDisplayName, inferProfileType } from "../profile/type";
|
|
10
|
+
import { expandEnv, resolvePath } from "../shell/utils";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
13
|
+
const CODEX_PROVIDER_NAME = "OpenAI";
|
|
14
|
+
const CODEX_PROVIDER_WIRE_API = "responses";
|
|
15
|
+
|
|
16
|
+
interface TomlSectionRange {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
sectionText: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CodexProviderBackup {
|
|
23
|
+
version: number;
|
|
24
|
+
modelProviderLine: string | null;
|
|
25
|
+
providerSectionText: string | null;
|
|
26
|
+
authText: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeEnvValue(value: EnvValue): string | null {
|
|
30
|
+
if (value === null || value === undefined) return null;
|
|
31
|
+
const normalized = String(value).trim();
|
|
32
|
+
return normalized ? normalized : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTextIfExists(filePath: string): string | null {
|
|
36
|
+
if (!fs.existsSync(filePath)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return fs.readFileSync(filePath, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeText(filePath: string, text: string): void {
|
|
45
|
+
const dir = path.dirname(filePath);
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(filePath, text, "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeFileIfExists(filePath: string): void {
|
|
53
|
+
if (!fs.existsSync(filePath)) return;
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(filePath);
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore cleanup failures
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getBackupPath(configPath: string): string {
|
|
62
|
+
return `${configPath}.codenv-provider-backup.json`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readBackup(backupPath: string): CodexProviderBackup | null {
|
|
66
|
+
const raw = readTextIfExists(backupPath);
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw) as Partial<CodexProviderBackup>;
|
|
70
|
+
return {
|
|
71
|
+
version:
|
|
72
|
+
typeof parsed.version === "number" ? parsed.version : 1,
|
|
73
|
+
modelProviderLine:
|
|
74
|
+
typeof parsed.modelProviderLine === "string"
|
|
75
|
+
? parsed.modelProviderLine
|
|
76
|
+
: null,
|
|
77
|
+
providerSectionText:
|
|
78
|
+
typeof parsed.providerSectionText === "string"
|
|
79
|
+
? parsed.providerSectionText
|
|
80
|
+
: null,
|
|
81
|
+
authText: typeof parsed.authText === "string" ? parsed.authText : null,
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeBackup(backupPath: string, backup: CodexProviderBackup): void {
|
|
89
|
+
writeText(backupPath, `${JSON.stringify(backup, null, 2)}\n`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseSectionByHeader(
|
|
93
|
+
text: string,
|
|
94
|
+
headerRegex: RegExp
|
|
95
|
+
): TomlSectionRange | null {
|
|
96
|
+
const match = headerRegex.exec(text);
|
|
97
|
+
if (!match || match.index === undefined) return null;
|
|
98
|
+
|
|
99
|
+
const start = match.index;
|
|
100
|
+
const afterHeader = start + match[0].length;
|
|
101
|
+
const rest = text.slice(afterHeader);
|
|
102
|
+
const nextHeaderMatch = rest.match(/^\s*\[.*?\]\s*$/m);
|
|
103
|
+
const end = nextHeaderMatch
|
|
104
|
+
? afterHeader + (nextHeaderMatch.index ?? rest.length)
|
|
105
|
+
: text.length;
|
|
106
|
+
return {
|
|
107
|
+
start,
|
|
108
|
+
end,
|
|
109
|
+
sectionText: text.slice(start, end).trimEnd(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getFirstSectionIndex(text: string): number {
|
|
114
|
+
const match = text.match(/^\s*\[.*?\]\s*$/m);
|
|
115
|
+
if (!match || match.index === undefined) return text.length;
|
|
116
|
+
return match.index;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getRootText(text: string): { root: string; rest: string } {
|
|
120
|
+
const index = getFirstSectionIndex(text);
|
|
121
|
+
return {
|
|
122
|
+
root: text.slice(0, index),
|
|
123
|
+
rest: text.slice(index),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readModelProviderLine(text: string): string | null {
|
|
128
|
+
const { root } = getRootText(text);
|
|
129
|
+
const match = root.match(/^\s*model_provider\s*=.*$/m);
|
|
130
|
+
return match ? match[0].trimEnd() : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeModelProviderLine(text: string): string {
|
|
134
|
+
const { root, rest } = getRootText(text);
|
|
135
|
+
const updatedRoot = root.replace(/^\s*model_provider\s*=.*(?:\r?\n)?/m, "");
|
|
136
|
+
return `${updatedRoot}${rest}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function insertRootLine(text: string, line: string): string {
|
|
140
|
+
const { root, rest } = getRootText(text);
|
|
141
|
+
const trimmedRoot = root.trimEnd();
|
|
142
|
+
const trimmedRest = rest.trimStart();
|
|
143
|
+
|
|
144
|
+
if (!trimmedRoot) {
|
|
145
|
+
if (!trimmedRest) return `${line}\n`;
|
|
146
|
+
return `${line}\n\n${trimmedRest}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!trimmedRest) {
|
|
150
|
+
return `${trimmedRoot}\n${line}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${trimmedRoot}\n${line}\n\n${trimmedRest}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function removeSection(text: string, headerRegex: RegExp): string {
|
|
157
|
+
const range = parseSectionByHeader(text, headerRegex);
|
|
158
|
+
if (!range) return text;
|
|
159
|
+
const before = text.slice(0, range.start).trimEnd();
|
|
160
|
+
const after = text.slice(range.end).trimStart();
|
|
161
|
+
if (before && after) return `${before}\n\n${after}`;
|
|
162
|
+
if (before) return `${before}\n`;
|
|
163
|
+
if (after) return `${after}\n`;
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendSection(text: string, sectionText: string): string {
|
|
168
|
+
const trimmed = text.trimEnd();
|
|
169
|
+
if (!trimmed) return `${sectionText}\n`;
|
|
170
|
+
return `${trimmed}\n\n${sectionText}\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getProviderHeaderRegex(): RegExp {
|
|
174
|
+
return /^\s*\[model_providers\.OpenAI\]\s*$/m;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readProviderSectionText(text: string): string | null {
|
|
178
|
+
const range = parseSectionByHeader(text, getProviderHeaderRegex());
|
|
179
|
+
return range ? range.sectionText : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderProviderSection(baseUrl: string | null): string {
|
|
183
|
+
const lines = [
|
|
184
|
+
`[model_providers.${CODEX_PROVIDER_NAME}]`,
|
|
185
|
+
`name = ${JSON.stringify(CODEX_PROVIDER_NAME)}`,
|
|
186
|
+
];
|
|
187
|
+
if (baseUrl) {
|
|
188
|
+
lines.push(`base_url = ${JSON.stringify(baseUrl)}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`wire_api = ${JSON.stringify(CODEX_PROVIDER_WIRE_API)}`);
|
|
191
|
+
lines.push("requires_openai_auth = true");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureBackup(configPath: string, currentConfigText: string): void {
|
|
196
|
+
const backupPath = getBackupPath(configPath);
|
|
197
|
+
if (readBackup(backupPath)) return;
|
|
198
|
+
writeBackup(backupPath, {
|
|
199
|
+
version: 1,
|
|
200
|
+
modelProviderLine: readModelProviderLine(currentConfigText),
|
|
201
|
+
providerSectionText: readProviderSectionText(currentConfigText),
|
|
202
|
+
authText: readTextIfExists(CODEX_AUTH_PATH),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeManagedConfig(configPath: string, baseUrl: string | null): void {
|
|
207
|
+
const currentText = readTextIfExists(configPath) || "";
|
|
208
|
+
ensureBackup(configPath, currentText);
|
|
209
|
+
|
|
210
|
+
let updated = currentText;
|
|
211
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
212
|
+
updated = removeModelProviderLine(updated);
|
|
213
|
+
updated = insertRootLine(
|
|
214
|
+
updated,
|
|
215
|
+
`model_provider = ${JSON.stringify(CODEX_PROVIDER_NAME)}`
|
|
216
|
+
);
|
|
217
|
+
updated = appendSection(updated, renderProviderSection(baseUrl));
|
|
218
|
+
writeText(configPath, updated);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function writeManagedAuth(apiKey: string | null): void {
|
|
222
|
+
const authJson =
|
|
223
|
+
apiKey === null
|
|
224
|
+
? "null"
|
|
225
|
+
: JSON.stringify({ OPENAI_API_KEY: apiKey });
|
|
226
|
+
writeText(CODEX_AUTH_PATH, `${authJson}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function restoreAuth(authText: string | null): void {
|
|
230
|
+
if (authText === null) {
|
|
231
|
+
removeFileIfExists(CODEX_AUTH_PATH);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
writeText(CODEX_AUTH_PATH, authText);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolveCodexConfigPath(config: Config): string {
|
|
238
|
+
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
239
|
+
if (envOverride && String(envOverride).trim()) {
|
|
240
|
+
const expanded = expandEnv(String(envOverride).trim());
|
|
241
|
+
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
242
|
+
}
|
|
243
|
+
const configOverride = config.codexStatusline?.configPath;
|
|
244
|
+
if (configOverride && String(configOverride).trim()) {
|
|
245
|
+
const expanded = expandEnv(String(configOverride).trim());
|
|
246
|
+
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
247
|
+
}
|
|
248
|
+
return DEFAULT_CODEX_CONFIG_PATH;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function syncCodexProfile(config: Config, profileName: string): void {
|
|
252
|
+
const profile = config.profiles && config.profiles[profileName];
|
|
253
|
+
if (!profile) {
|
|
254
|
+
throw new Error(`Unknown profile: ${profileName}`);
|
|
255
|
+
}
|
|
256
|
+
const env = profile.env || {};
|
|
257
|
+
const baseUrl = normalizeEnvValue(env.OPENAI_BASE_URL);
|
|
258
|
+
const apiKey = normalizeEnvValue(env.OPENAI_API_KEY);
|
|
259
|
+
const configPath = resolveCodexConfigPath(config);
|
|
260
|
+
writeManagedConfig(configPath, baseUrl);
|
|
261
|
+
writeManagedAuth(apiKey);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function clearManagedCodexProfile(config: Config): void {
|
|
265
|
+
const configPath = resolveCodexConfigPath(config);
|
|
266
|
+
const backupPath = getBackupPath(configPath);
|
|
267
|
+
const backup = readBackup(backupPath);
|
|
268
|
+
if (!backup) return;
|
|
269
|
+
|
|
270
|
+
let updated = readTextIfExists(configPath) || "";
|
|
271
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
272
|
+
updated = removeModelProviderLine(updated);
|
|
273
|
+
if (backup.modelProviderLine) {
|
|
274
|
+
updated = insertRootLine(updated, backup.modelProviderLine);
|
|
275
|
+
}
|
|
276
|
+
if (backup.providerSectionText) {
|
|
277
|
+
updated = appendSection(updated, backup.providerSectionText);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const trimmed = updated.trimEnd();
|
|
281
|
+
if (trimmed) {
|
|
282
|
+
writeText(configPath, `${trimmed}\n`);
|
|
283
|
+
} else {
|
|
284
|
+
removeFileIfExists(configPath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
restoreAuth(backup.authText);
|
|
288
|
+
removeFileIfExists(backupPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function resolveCodexProfileFromEnv(
|
|
292
|
+
config: Config,
|
|
293
|
+
profileKey: string | null,
|
|
294
|
+
profileName: string | null
|
|
295
|
+
): string | null {
|
|
296
|
+
const profiles = config.profiles || {};
|
|
297
|
+
if (profileKey && profiles[profileKey]) return profileKey;
|
|
298
|
+
if (!profileName) return null;
|
|
299
|
+
|
|
300
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
301
|
+
if (inferProfileType(key, profile || {}, null) !== "codex") continue;
|
|
302
|
+
const displayName = getProfileDisplayName(key, profile || {}, "codex");
|
|
303
|
+
if (displayName === profileName || key === profileName) {
|
|
304
|
+
return key;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -5,7 +5,6 @@ import * as fs from "fs";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import type { Config, ProfileType } from "../types";
|
|
8
|
-
import { CODEX_AUTH_PATH } from "../constants";
|
|
9
8
|
import { normalizeType } from "../profile/type";
|
|
10
9
|
import {
|
|
11
10
|
getCodexSessionsPath,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
} from "../usage";
|
|
17
16
|
import { ensureClaudeStatusline } from "../statusline/claude";
|
|
18
17
|
import { ensureCodexStatuslineConfig } from "../statusline/codex";
|
|
18
|
+
import { resolveCodexProfileFromEnv, syncCodexProfile } from "../codex/config";
|
|
19
19
|
|
|
20
20
|
const SESSION_BINDING_POLL_MS = 1000;
|
|
21
21
|
const SESSION_BINDING_START_GRACE_MS = 5000;
|
|
@@ -203,24 +203,6 @@ function getProfileEnv(type: ProfileType): { key: string | null; name: string |
|
|
|
203
203
|
return { key, name };
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function writeCodexAuthFromEnv(): void {
|
|
207
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
208
|
-
try {
|
|
209
|
-
fs.mkdirSync(path.dirname(CODEX_AUTH_PATH), { recursive: true });
|
|
210
|
-
} catch {
|
|
211
|
-
// ignore
|
|
212
|
-
}
|
|
213
|
-
const authJson =
|
|
214
|
-
apiKey === null || apiKey === undefined || apiKey === ""
|
|
215
|
-
? "null"
|
|
216
|
-
: JSON.stringify({ OPENAI_API_KEY: String(apiKey) });
|
|
217
|
-
try {
|
|
218
|
-
fs.writeFileSync(CODEX_AUTH_PATH, `${authJson}\n`, "utf8");
|
|
219
|
-
} catch {
|
|
220
|
-
// ignore
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
206
|
function parseBooleanEnv(value: string | undefined): boolean | null {
|
|
225
207
|
if (value === undefined) return null;
|
|
226
208
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -259,11 +241,18 @@ export async function runLaunch(
|
|
|
259
241
|
if (!type) {
|
|
260
242
|
throw new Error(`Unknown launch target: ${target}`);
|
|
261
243
|
}
|
|
244
|
+
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
262
245
|
if (type === "codex") {
|
|
263
|
-
|
|
246
|
+
const codexProfileKey = resolveCodexProfileFromEnv(
|
|
247
|
+
config,
|
|
248
|
+
profileKey,
|
|
249
|
+
profileName
|
|
250
|
+
);
|
|
251
|
+
if (codexProfileKey) {
|
|
252
|
+
syncCodexProfile(config, codexProfileKey);
|
|
253
|
+
}
|
|
264
254
|
}
|
|
265
255
|
|
|
266
|
-
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
267
256
|
const terminalTag = process.env.CODE_ENV_TERMINAL_TAG || null;
|
|
268
257
|
const cwd = process.cwd();
|
|
269
258
|
const startMs = Date.now();
|
package/src/commands/unset.ts
CHANGED
|
@@ -13,7 +13,9 @@ export function printUnset(config: Config): void {
|
|
|
13
13
|
for (const type of DEFAULT_PROFILE_TYPES) {
|
|
14
14
|
for (const key of getTypeDefaultUnsetKeys(type)) keySet.add(key);
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const lines: string[] = ["command codenv __codex-clear"];
|
|
17
|
+
if (keySet.size > 0) {
|
|
18
|
+
lines.push(...Array.from(keySet, (key) => `unset ${key}`));
|
|
19
|
+
}
|
|
18
20
|
console.log(lines.join("\n"));
|
|
19
21
|
}
|
package/src/commands/use.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Use command - apply profile environment
|
|
3
3
|
*/
|
|
4
|
-
import * as path from "path";
|
|
5
4
|
import type { Config, ProfileType } from "../types";
|
|
6
|
-
import { CODEX_AUTH_PATH } from "../constants";
|
|
7
5
|
import { shellEscape, expandEnv } from "../shell/utils";
|
|
8
6
|
import { inferProfileType, getProfileDisplayName } from "../profile/type";
|
|
9
7
|
import { shouldRemoveCodexAuth } from "../profile/match";
|
|
@@ -28,6 +26,10 @@ export function buildUseLines(
|
|
|
28
26
|
const unsetKeys = new Set<string>();
|
|
29
27
|
const activeType = inferProfileType(profileName, profile, requestedType);
|
|
30
28
|
const effectiveEnv = buildEffectiveEnv(profile, activeType);
|
|
29
|
+
const managedCodexKeys =
|
|
30
|
+
activeType === "codex"
|
|
31
|
+
? new Set(["OPENAI_BASE_URL", "OPENAI_API_KEY"])
|
|
32
|
+
: new Set<string>();
|
|
31
33
|
|
|
32
34
|
const addUnset = (key: string) => {
|
|
33
35
|
if (unsetKeys.has(key)) return;
|
|
@@ -49,6 +51,13 @@ export function buildUseLines(
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
for (const key of Object.keys(effectiveEnv)) {
|
|
54
|
+
if (managedCodexKeys.has(key)) {
|
|
55
|
+
if (!unsetKeys.has(key)) {
|
|
56
|
+
unsetKeys.add(key);
|
|
57
|
+
unsetLines.push(`unset ${key}`);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
52
61
|
const value = effectiveEnv[key];
|
|
53
62
|
if (value === null || value === undefined || value === "") {
|
|
54
63
|
if (!unsetKeys.has(key)) {
|
|
@@ -75,16 +84,7 @@ export function buildUseLines(
|
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
if (shouldRemoveCodexAuth(profileName, profile, requestedType)) {
|
|
78
|
-
|
|
79
|
-
const authDir = path.dirname(CODEX_AUTH_PATH);
|
|
80
|
-
const authJson =
|
|
81
|
-
codexApiKey === null || codexApiKey === undefined || codexApiKey === ""
|
|
82
|
-
? "null"
|
|
83
|
-
: JSON.stringify({ OPENAI_API_KEY: String(codexApiKey) });
|
|
84
|
-
postLines.push(`mkdir -p ${shellEscape(authDir)}`);
|
|
85
|
-
postLines.push(
|
|
86
|
-
`printf '%s\\n' ${shellEscape(authJson)} > ${shellEscape(CODEX_AUTH_PATH)}`
|
|
87
|
-
);
|
|
87
|
+
postLines.push("command codenv __codex-sync");
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (Array.isArray(profile.removeFiles)) {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* codenv - switch Claude/Codex
|
|
3
|
+
* codenv - switch Claude/Codex profiles
|
|
4
4
|
* Main entry point
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "fs";
|
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
} from "./commands";
|
|
39
39
|
import { logProfileUse } from "./usage";
|
|
40
40
|
import { createReadline, askConfirm, runInteractiveAdd, runInteractiveUse } from "./ui";
|
|
41
|
+
import {
|
|
42
|
+
clearManagedCodexProfile,
|
|
43
|
+
resolveCodexProfileFromEnv,
|
|
44
|
+
syncCodexProfile,
|
|
45
|
+
} from "./codex/config";
|
|
41
46
|
|
|
42
47
|
function getErrorMessage(err: unknown): string {
|
|
43
48
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -59,6 +64,30 @@ async function main() {
|
|
|
59
64
|
|
|
60
65
|
const cmd = args[0];
|
|
61
66
|
try {
|
|
67
|
+
if (cmd === "__codex-sync") {
|
|
68
|
+
const configPath =
|
|
69
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
70
|
+
const config = readConfigIfExists(configPath);
|
|
71
|
+
const profileKey = process.env.CODE_ENV_PROFILE_KEY_CODEX || null;
|
|
72
|
+
const profileName = process.env.CODE_ENV_PROFILE_NAME_CODEX || null;
|
|
73
|
+
const resolvedProfile = resolveCodexProfileFromEnv(
|
|
74
|
+
config,
|
|
75
|
+
profileKey,
|
|
76
|
+
profileName
|
|
77
|
+
);
|
|
78
|
+
if (!resolvedProfile) return;
|
|
79
|
+
syncCodexProfile(config, resolvedProfile);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cmd === "__codex-clear") {
|
|
84
|
+
const configPath =
|
|
85
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
86
|
+
const config = readConfigIfExists(configPath);
|
|
87
|
+
clearManagedCodexProfile(config);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
if (cmd === "init") {
|
|
63
92
|
const initArgs = parseInitArgs(args.slice(1));
|
|
64
93
|
const shellName = detectShell(initArgs.shell);
|
package/src/profile/display.ts
CHANGED
|
@@ -36,6 +36,20 @@ export function envMatchesProfile(profile: Profile | undefined): boolean {
|
|
|
36
36
|
return Object.keys(profile.env).length > 0;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function markerMatchesProfile(
|
|
40
|
+
profileKey: string,
|
|
41
|
+
profile: Profile | undefined,
|
|
42
|
+
activeType: ProfileType | null
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!activeType) return false;
|
|
45
|
+
const suffix = activeType.toUpperCase();
|
|
46
|
+
const activeKey = process.env[`CODE_ENV_PROFILE_KEY_${suffix}`];
|
|
47
|
+
if (activeKey) return activeKey === profileKey;
|
|
48
|
+
const activeName = process.env[`CODE_ENV_PROFILE_NAME_${suffix}`];
|
|
49
|
+
if (!activeName || !profile) return false;
|
|
50
|
+
return activeName === getProfileDisplayName(profileKey, profile, activeType);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
// Forward declaration to avoid circular dependency
|
|
40
54
|
// getResolvedDefaultProfileKeys will be imported from config/defaults
|
|
41
55
|
export function buildListRows(
|
|
@@ -63,7 +77,9 @@ export function buildListRows(
|
|
|
63
77
|
if (defaultLabel) noteParts.push(defaultLabel);
|
|
64
78
|
if (note) noteParts.push(note);
|
|
65
79
|
const noteText = noteParts.join(" | ");
|
|
66
|
-
const active =
|
|
80
|
+
const active =
|
|
81
|
+
markerMatchesProfile(key, safeProfile, usageType) ||
|
|
82
|
+
envMatchesProfile(safeProfile);
|
|
67
83
|
return { key, name: displayName, type, note: noteText, active, usageType };
|
|
68
84
|
});
|
|
69
85
|
rows.sort((a, b) => {
|
package/src/statusline/codex.ts
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
* Codex CLI status line integration (official schema)
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from "fs";
|
|
5
|
-
import * as os from "os";
|
|
6
5
|
import * as path from "path";
|
|
7
6
|
import type { Config } from "../types";
|
|
8
|
-
import {
|
|
7
|
+
import { resolveCodexConfigPath } from "../codex/config";
|
|
9
8
|
import { askConfirm, createReadline } from "../ui";
|
|
10
9
|
|
|
11
|
-
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
12
|
-
|
|
13
10
|
interface ParsedTuiConfig {
|
|
14
11
|
statusLineItems: string[] | null;
|
|
15
12
|
}
|
|
@@ -40,20 +37,6 @@ function parseBooleanEnv(value: string | undefined): boolean | null {
|
|
|
40
37
|
return null;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
function resolveCodexConfigPath(config: Config): string {
|
|
44
|
-
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
45
|
-
if (envOverride && String(envOverride).trim()) {
|
|
46
|
-
const expanded = expandEnv(String(envOverride).trim());
|
|
47
|
-
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
48
|
-
}
|
|
49
|
-
const configOverride = config.codexStatusline?.configPath;
|
|
50
|
-
if (configOverride && String(configOverride).trim()) {
|
|
51
|
-
const expanded = expandEnv(String(configOverride).trim());
|
|
52
|
-
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
53
|
-
}
|
|
54
|
-
return DEFAULT_CODEX_CONFIG_PATH;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
40
|
function resolveDesiredStatusLineItems(config: Config): string[] | null {
|
|
58
41
|
const raw = config.codexStatusline?.items;
|
|
59
42
|
if (!Array.isArray(raw)) return null;
|