@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # code-env-switch
2
2
 
3
- A tiny CLI to switch between Claude Code and Codex environment variables.
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
@@ -5,7 +5,7 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.printHelp = printHelp;
7
7
  function printHelp() {
8
- const msg = `codenv - switch Claude/Codex env vars
8
+ const msg = `codenv - switch Claude/Codex profiles
9
9
 
10
10
  Usage:
11
11
  codenv list
@@ -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
+ }
@@ -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
- writeCodexAuthFromEnv();
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();
@@ -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
- if (keySet.size === 0)
17
- return;
18
- const lines = Array.from(keySet, (key) => `unset ${key}`);
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
  }
@@ -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
- const codexApiKey = effectiveEnv.OPENAI_API_KEY;
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 env vars
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);
@@ -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 = envMatchesProfile(safeProfile);
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) => {
@@ -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 utils_1 = require("../shell/utils");
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) {
@@ -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.9",
4
- "description": "Switch between Claude Code and Codex environment variables from a single CLI",
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
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  export function printHelp(): void {
6
- const msg = `codenv - switch Claude/Codex env vars
6
+ const msg = `codenv - switch Claude/Codex profiles
7
7
 
8
8
  Usage:
9
9
  codenv list
@@ -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
+ }
@@ -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
- writeCodexAuthFromEnv();
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();
@@ -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
- if (keySet.size === 0) return;
17
- const lines = Array.from(keySet, (key) => `unset ${key}`);
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
  }
@@ -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
- const codexApiKey = effectiveEnv.OPENAI_API_KEY;
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 env vars
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);
@@ -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 = envMatchesProfile(safeProfile);
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) => {
@@ -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 { expandEnv, resolvePath } from "../shell/utils";
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;