@praeviso/code-env-switch 0.1.8 → 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 +20 -10
- package/README_zh.md +19 -10
- package/bin/cli/help.js +1 -1
- package/bin/codex/config.js +273 -0
- package/bin/commands/launch.js +6 -21
- package/bin/commands/list.js +10 -1
- 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 +159 -210
- package/bin/statusline/index.js +10 -2
- package/bin/usage/index.js +123 -21
- package/code-env.example.json +3 -6
- package/docs/usage.md +3 -0
- package/docs/usage_zh.md +2 -0
- package/package.json +5 -5
- package/src/cli/help.ts +1 -1
- package/src/codex/config.ts +309 -0
- package/src/commands/launch.ts +10 -21
- package/src/commands/list.ts +11 -1
- 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 +196 -217
- package/src/statusline/index.ts +17 -2
- package/src/types.ts +1 -4
- package/src/usage/index.ts +135 -21
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
|
|
@@ -219,10 +233,7 @@ If nothing is found, `codenv add` writes to `~/.config/code-env/config.json`.
|
|
|
219
233
|
"claude": "default"
|
|
220
234
|
},
|
|
221
235
|
"codexStatusline": {
|
|
222
|
-
"
|
|
223
|
-
"showHints": false,
|
|
224
|
-
"updateIntervalMs": 300,
|
|
225
|
-
"timeoutMs": 1000
|
|
236
|
+
"items": ["model-with-reasoning", "context-remaining", "current-dir", "git-branch"]
|
|
226
237
|
},
|
|
227
238
|
"claudeStatusline": {
|
|
228
239
|
"command": "codenv statusline --type claude --sync-usage",
|
|
@@ -259,12 +270,11 @@ If nothing is found, `codenv add` writes to `~/.config/code-env/config.json`.
|
|
|
259
270
|
Notes:
|
|
260
271
|
- `unset`: global keys to clear. Type-specific defaults are applied only for the active type and won't clear the other type.
|
|
261
272
|
- `defaultProfiles`: optional; map of `codex`/`claude` to profile name or key used by `codenv auto`.
|
|
262
|
-
- `codexStatusline`: optional; config to inject Codex TUI status line settings when launching `codex`.
|
|
263
|
-
- `
|
|
264
|
-
- `
|
|
265
|
-
- `updateIntervalMs`: number; update interval in ms for the status line command.
|
|
266
|
-
- `timeoutMs`: number; timeout in ms for the status line command.
|
|
273
|
+
- `codexStatusline`: optional; config to inject official Codex TUI status line settings when launching `codex`.
|
|
274
|
+
- `items`: string[]; ordered item IDs written to `tui.status_line` in `~/.codex/config.toml`.
|
|
275
|
+
- Supported item IDs include: `model-name`, `model-with-reasoning`, `current-dir`, `project-root`, `git-branch`, `context-remaining`, `context-used`, `five-hour-limit`, `weekly-limit`, `codex-version`, `context-window-size`, `used-tokens`, `total-input-tokens`, `total-output-tokens`, `session-id`.
|
|
267
276
|
- `configPath`: optional; override `~/.codex/config.toml` (also supports `CODE_ENV_CODEX_CONFIG_PATH`).
|
|
277
|
+
- If `items` is unset, `codenv` leaves Codex status-line config unchanged (Codex defaults apply).
|
|
268
278
|
- `claudeStatusline`: optional; config to inject Claude Code statusLine settings when launching `claude`.
|
|
269
279
|
- `command`: string (or string[]; arrays are joined into a single command string).
|
|
270
280
|
- `type`: string; statusLine type (default: `command`).
|
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
|
|
@@ -218,10 +231,7 @@ codenv use codex primary | source
|
|
|
218
231
|
"claude": "default"
|
|
219
232
|
},
|
|
220
233
|
"codexStatusline": {
|
|
221
|
-
"
|
|
222
|
-
"showHints": false,
|
|
223
|
-
"updateIntervalMs": 300,
|
|
224
|
-
"timeoutMs": 1000
|
|
234
|
+
"items": ["model-with-reasoning", "context-remaining", "current-dir", "git-branch"]
|
|
225
235
|
},
|
|
226
236
|
"claudeStatusline": {
|
|
227
237
|
"command": "codenv statusline --type claude --sync-usage",
|
|
@@ -258,12 +268,11 @@ codenv use codex primary | source
|
|
|
258
268
|
说明:
|
|
259
269
|
- `unset`:全局需要清理的环境变量。按 type 的默认清理键只会对当前 type 生效,不会影响其他 type。
|
|
260
270
|
- `defaultProfiles`:可选;`codex`/`claude` 对应的默认 profile 名称或 key,供 `codenv auto` 使用。
|
|
261
|
-
- `codexStatusline`:可选;在启动 `codex`
|
|
262
|
-
- `
|
|
263
|
-
-
|
|
264
|
-
- `updateIntervalMs`:数字;状态栏命令的刷新间隔(毫秒)。
|
|
265
|
-
- `timeoutMs`:数字;状态栏命令超时(毫秒)。
|
|
271
|
+
- `codexStatusline`:可选;在启动 `codex` 时写入官方 Codex TUI 状态栏配置。
|
|
272
|
+
- `items`:字符串数组;按顺序写入 `~/.codex/config.toml` 的 `tui.status_line`。
|
|
273
|
+
- 支持的 item ID 包括:`model-name`、`model-with-reasoning`、`current-dir`、`project-root`、`git-branch`、`context-remaining`、`context-used`、`five-hour-limit`、`weekly-limit`、`codex-version`、`context-window-size`、`used-tokens`、`total-input-tokens`、`total-output-tokens`、`session-id`。
|
|
266
274
|
- `configPath`:可选;覆盖 `~/.codex/config.toml`(也可用 `CODE_ENV_CODEX_CONFIG_PATH`)。
|
|
275
|
+
- 若未设置 `items`,`codenv` 不会改写 Codex 状态栏配置(使用 Codex 默认值)。
|
|
267
276
|
- `claudeStatusline`:可选;在启动 `claude` 时写入 Claude Code statusLine 配置。
|
|
268
277
|
- `command`:字符串(或字符串数组;数组会被拼接成单个命令字符串)。
|
|
269
278
|
- `type`:字符串;statusLine 类型(默认 `command`)。
|
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/list.js
CHANGED
|
@@ -12,7 +12,16 @@ function printList(config, configPath) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
15
|
+
const usagePath = (0, usage_1.getUsagePath)(config, configPath);
|
|
16
|
+
if (usagePath) {
|
|
17
|
+
(0, usage_1.syncUsageFromSessions)(config, configPath, usagePath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore usage sync errors
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const usageTotals = (0, usage_1.readUsageTotalsIndex)(config, configPath, false);
|
|
16
25
|
const usageCosts = (0, usage_1.readUsageCostIndex)(config, configPath, false);
|
|
17
26
|
if (usageTotals) {
|
|
18
27
|
for (const row of rows) {
|
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) => {
|