@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.
@@ -16,6 +16,7 @@ exports.syncUsageFromStatuslineInput = syncUsageFromStatuslineInput;
16
16
  exports.logProfileUse = logProfileUse;
17
17
  exports.logSessionBinding = logSessionBinding;
18
18
  exports.readSessionBindingIndex = readSessionBindingIndex;
19
+ exports.resolveProfileFromLog = resolveProfileFromLog;
19
20
  exports.clearUsageHistory = clearUsageHistory;
20
21
  exports.readUsageRecords = readUsageRecords;
21
22
  exports.syncUsageFromSessions = syncUsageFromSessions;
@@ -375,16 +376,15 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
375
376
  cacheWriteTokens;
376
377
  if (!Number.isFinite(totalTokens))
377
378
  return;
378
- const statePath = getUsageStatePath(usagePath, config);
379
- const lockPath = `${statePath}.lock`;
379
+ const sessionKey = buildSessionKey(normalizedType, sessionId);
380
+ const sessionStatePath = getUsageSessionStatePath(usagePath, sessionKey);
381
+ const legacySessionStatePath = getLegacyUsageSessionStatePath(usagePath, sessionKey);
382
+ const lockPath = `${sessionStatePath}.lock`;
380
383
  const lockFd = acquireLock(lockPath);
381
384
  if (lockFd === null)
382
385
  return;
383
386
  try {
384
- const state = readUsageState(statePath);
385
- const sessions = state.sessions || {};
386
- const key = buildSessionKey(normalizedType, sessionId);
387
- const prev = sessions[key];
387
+ const prev = readUsageSessionStateWithFallback(sessionStatePath, legacySessionStatePath);
388
388
  const prevInput = prev ? toUsageNumber(prev.inputTokens) : 0;
389
389
  const prevOutput = prev ? toUsageNumber(prev.outputTokens) : 0;
390
390
  const prevCacheRead = prev ? toUsageNumber(prev.cacheReadTokens) : 0;
@@ -396,12 +396,11 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
396
396
  let deltaCacheWrite = cacheWriteTokens - prevCacheWrite;
397
397
  let deltaTotal = totalTokens - prevTotal;
398
398
  if (deltaTotal < 0) {
399
- // Session reset: treat current totals as fresh usage.
400
- deltaInput = inputTokens;
401
- deltaOutput = outputTokens;
402
- deltaCacheRead = cacheReadTokens;
403
- deltaCacheWrite = cacheWriteTokens;
404
- deltaTotal = totalTokens;
399
+ deltaInput = 0;
400
+ deltaOutput = 0;
401
+ deltaCacheRead = 0;
402
+ deltaCacheWrite = 0;
403
+ deltaTotal = 0;
405
404
  }
406
405
  else {
407
406
  // Clamp negatives caused by reclassification (e.g. cache splits).
@@ -438,13 +437,18 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
438
437
  appendUsageRecord(usagePath, record);
439
438
  }
440
439
  const now = new Date().toISOString();
441
- sessions[key] = {
440
+ const nextInput = Math.max(prevInput, inputTokens);
441
+ const nextOutput = Math.max(prevOutput, outputTokens);
442
+ const nextCacheRead = Math.max(prevCacheRead, cacheReadTokens);
443
+ const nextCacheWrite = Math.max(prevCacheWrite, cacheWriteTokens);
444
+ const nextTotal = Math.max(prevTotal, totalTokens);
445
+ const nextSession = {
442
446
  type: normalizedType,
443
- inputTokens,
444
- outputTokens,
445
- cacheReadTokens,
446
- cacheWriteTokens,
447
- totalTokens,
447
+ inputTokens: nextInput,
448
+ outputTokens: nextOutput,
449
+ cacheReadTokens: nextCacheRead,
450
+ cacheWriteTokens: nextCacheWrite,
451
+ totalTokens: nextTotal,
448
452
  startTs: prev ? prev.startTs : now,
449
453
  endTs: now,
450
454
  cwd: cwd || (prev ? prev.cwd : null),
@@ -452,9 +456,7 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
452
456
  profileKey: profileKey || null,
453
457
  profileName: profileName || null,
454
458
  };
455
- state.sessions = sessions;
456
- updateUsageStateMetadata(state, usagePath);
457
- writeUsageState(statePath, state);
459
+ writeUsageSessionState(sessionStatePath, nextSession);
458
460
  }
459
461
  finally {
460
462
  releaseLock(lockPath, lockFd);
@@ -596,6 +598,42 @@ function resolveProfileForSession(config, logEntries, type, sessionFile, session
596
598
  }
597
599
  return { match: null, ambiguous: false };
598
600
  }
601
+ function resolveProfileFromLog(config, configPath, type, terminalTag) {
602
+ const normalizedType = (0, type_1.normalizeType)(type || "");
603
+ if (!normalizedType)
604
+ return null;
605
+ const profileLogPath = getProfileLogPath(config, configPath);
606
+ const entries = readProfileLogEntries([profileLogPath]);
607
+ if (entries.length === 0)
608
+ return null;
609
+ if (!terminalTag)
610
+ return null;
611
+ let best = null;
612
+ let bestTime = Number.NEGATIVE_INFINITY;
613
+ for (const entry of entries) {
614
+ if (entry.kind !== "use")
615
+ continue;
616
+ if (entry.profileType !== normalizedType)
617
+ continue;
618
+ if (entry.terminalTag !== terminalTag)
619
+ continue;
620
+ if (!entry.profileKey && !entry.profileName)
621
+ continue;
622
+ const ts = new Date(entry.timestamp).getTime();
623
+ if (!Number.isFinite(ts))
624
+ continue;
625
+ if (ts >= bestTime) {
626
+ bestTime = ts;
627
+ best = entry;
628
+ }
629
+ }
630
+ if (!best)
631
+ return null;
632
+ const match = normalizeProfileMatch(config, best, normalizedType);
633
+ if (!match.profileKey && !match.profileName)
634
+ return null;
635
+ return match;
636
+ }
599
637
  function readUsageState(statePath) {
600
638
  if (!statePath || !fs.existsSync(statePath)) {
601
639
  return { version: 1, files: {}, sessions: {} };
@@ -644,6 +682,54 @@ function writeUsageState(statePath, state) {
644
682
  fs.writeFileSync(statePath, payload, "utf8");
645
683
  }
646
684
  }
685
+ function getUsageSessionStateDir(usagePath) {
686
+ const dir = path.dirname(usagePath);
687
+ const base = path.basename(usagePath, path.extname(usagePath));
688
+ return path.join(dir, `${base}-sessions`);
689
+ }
690
+ function getLegacyUsageSessionStateDir(usagePath) {
691
+ return `${usagePath}.sessions`;
692
+ }
693
+ function toSafeSessionKey(value) {
694
+ return value.replace(/[^a-zA-Z0-9._-]/g, "_");
695
+ }
696
+ function getUsageSessionStatePath(usagePath, sessionKey) {
697
+ const dir = getUsageSessionStateDir(usagePath);
698
+ return path.join(dir, `${toSafeSessionKey(sessionKey)}.json`);
699
+ }
700
+ function getLegacyUsageSessionStatePath(usagePath, sessionKey) {
701
+ const dir = getLegacyUsageSessionStateDir(usagePath);
702
+ return path.join(dir, `${toSafeSessionKey(sessionKey)}.json`);
703
+ }
704
+ function readUsageSessionState(filePath) {
705
+ var _a;
706
+ if (!filePath || !fs.existsSync(filePath))
707
+ return null;
708
+ try {
709
+ const raw = fs.readFileSync(filePath, "utf8");
710
+ const parsed = JSON.parse(raw);
711
+ if (!parsed || typeof parsed !== "object")
712
+ return null;
713
+ const session = (_a = parsed.session) !== null && _a !== void 0 ? _a : parsed;
714
+ if (!session || typeof session !== "object")
715
+ return null;
716
+ return session;
717
+ }
718
+ catch {
719
+ return null;
720
+ }
721
+ }
722
+ function readUsageSessionStateWithFallback(primaryPath, legacyPath) {
723
+ return readUsageSessionState(primaryPath) || readUsageSessionState(legacyPath);
724
+ }
725
+ function writeUsageSessionState(filePath, session) {
726
+ const dir = path.dirname(filePath);
727
+ if (!fs.existsSync(dir)) {
728
+ fs.mkdirSync(dir, { recursive: true });
729
+ }
730
+ const payload = { version: 1, session };
731
+ fs.writeFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf8");
732
+ }
647
733
  function addSiblingBackupPaths(targets, filePath) {
648
734
  if (!filePath)
649
735
  return;
@@ -672,6 +758,22 @@ function clearUsageHistory(config, configPath) {
672
758
  if (usagePath) {
673
759
  targets.add(usagePath);
674
760
  addSiblingBackupPaths(targets, usagePath);
761
+ for (const sessionDir of [
762
+ getUsageSessionStateDir(usagePath),
763
+ getLegacyUsageSessionStateDir(usagePath),
764
+ ]) {
765
+ try {
766
+ const entries = fs.readdirSync(sessionDir, { withFileTypes: true });
767
+ for (const entry of entries) {
768
+ if (!entry.isFile())
769
+ continue;
770
+ targets.add(path.join(sessionDir, entry.name));
771
+ }
772
+ }
773
+ catch {
774
+ // ignore session state cleanup failures
775
+ }
776
+ }
675
777
  }
676
778
  const statePath = usagePath ? getUsageStatePath(usagePath, config) : null;
677
779
  if (statePath) {
@@ -5,10 +5,7 @@
5
5
  "claude": "default"
6
6
  },
7
7
  "codexStatusline": {
8
- "command": ["codenv", "statusline", "--type", "codex", "--sync-usage"],
9
- "showHints": false,
10
- "updateIntervalMs": 300,
11
- "timeoutMs": 1000
8
+ "items": ["model-with-reasoning", "context-remaining", "current-dir", "git-branch"]
12
9
  },
13
10
  "claudeStatusline": {
14
11
  "command": "codenv statusline --type claude --sync-usage",
@@ -74,7 +71,7 @@
74
71
  "p_a1b2c3": {
75
72
  "name": "primary",
76
73
  "type": "codex",
77
- "note": "Primary endpoint",
74
+ "note": "Primary endpoint (applied via ~/.codex/config.toml)",
78
75
  "env": {
79
76
  "OPENAI_BASE_URL": "https://api.example.com/v1",
80
77
  "OPENAI_API_KEY": "YOUR_API_KEY"
@@ -86,7 +83,7 @@
86
83
  "p_d4e5f6": {
87
84
  "name": "secondary",
88
85
  "type": "codex",
89
- "note": "Secondary endpoint",
86
+ "note": "Secondary endpoint (applied via ~/.codex/config.toml)",
90
87
  "env": {
91
88
  "OPENAI_BASE_URL": "https://api.secondary.example.com/v1",
92
89
  "OPENAI_API_KEY": "YOUR_API_KEY"
package/docs/usage.md CHANGED
@@ -56,6 +56,9 @@ Each line is a JSON object (fields may be null/omitted depending on the source):
56
56
  To see real payloads, set `CODE_ENV_STATUSLINE_DEBUG=1` and read the JSONL entries in
57
57
  `statusline-debug.jsonl` (or the path from `CODE_ENV_STATUSLINE_DEBUG_PATH`).
58
58
 
59
+ Note: with official Codex status line configuration (`tui.status_line`), Codex does not invoke
60
+ an external status line command by default.
61
+
59
62
  ### Codex (token_usage totals)
60
63
 
61
64
  ```json
package/docs/usage_zh.md CHANGED
@@ -56,6 +56,8 @@
56
56
  可通过设置 `CODE_ENV_STATUSLINE_DEBUG=1`,在 `statusline-debug.jsonl`
57
57
  (或 `CODE_ENV_STATUSLINE_DEBUG_PATH` 指定路径)看到实际 JSON。
58
58
 
59
+ 注意:使用官方 Codex 状态栏配置(`tui.status_line`)时,Codex 默认不会调用外部状态栏命令。
60
+
59
61
  ### Codex(token_usage totals)
60
62
 
61
63
  ```json
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@praeviso/code-env-switch",
3
- "version": "0.1.8",
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
  },
@@ -17,9 +17,9 @@
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^20.11.0",
20
- "@typescript-eslint/eslint-plugin": "^6.21.0",
21
- "@typescript-eslint/parser": "^6.21.0",
22
- "eslint": "^8.57.0",
20
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
21
+ "@typescript-eslint/parser": "^8.56.1",
22
+ "eslint": "^10.0.2",
23
23
  "typescript": "^5.9.3"
24
24
  }
25
25
  }
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();
@@ -6,10 +6,12 @@ import { buildListRows } from "../profile/display";
6
6
  import { getResolvedDefaultProfileKeys } from "../config/defaults";
7
7
  import {
8
8
  formatTokenCount,
9
+ getUsagePath,
9
10
  readUsageCostIndex,
10
11
  readUsageTotalsIndex,
11
12
  resolveUsageCostForProfile,
12
13
  resolveUsageTotalsForProfile,
14
+ syncUsageFromSessions,
13
15
  } from "../usage";
14
16
  import { formatUsdAmount } from "../usage/pricing";
15
17
 
@@ -20,7 +22,15 @@ export function printList(config: Config, configPath: string | null): void {
20
22
  return;
21
23
  }
22
24
  try {
23
- const usageTotals = readUsageTotalsIndex(config, configPath, true);
25
+ const usagePath = getUsagePath(config, configPath);
26
+ if (usagePath) {
27
+ syncUsageFromSessions(config, configPath, usagePath);
28
+ }
29
+ } catch {
30
+ // ignore usage sync errors
31
+ }
32
+ try {
33
+ const usageTotals = readUsageTotalsIndex(config, configPath, false);
24
34
  const usageCosts = readUsageCostIndex(config, configPath, false);
25
35
  if (usageTotals) {
26
36
  for (const row of rows) {