@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/bin/usage/index.js
CHANGED
|
@@ -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
|
|
379
|
-
const
|
|
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
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/code-env.example.json
CHANGED
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
"claude": "default"
|
|
6
6
|
},
|
|
7
7
|
"codexStatusline": {
|
|
8
|
-
"
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praeviso/code-env-switch",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Switch between Claude Code and Codex
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Switch between Claude Code and Codex profiles from a single CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codenv": "bin/index.js"
|
|
7
7
|
},
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^20.11.0",
|
|
20
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
21
|
-
"@typescript-eslint/parser": "^
|
|
22
|
-
"eslint": "^
|
|
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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex provider configuration management
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import type { Config, EnvValue } from "../types";
|
|
8
|
+
import { CODEX_AUTH_PATH } from "../constants";
|
|
9
|
+
import { getProfileDisplayName, inferProfileType } from "../profile/type";
|
|
10
|
+
import { expandEnv, resolvePath } from "../shell/utils";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
13
|
+
const CODEX_PROVIDER_NAME = "OpenAI";
|
|
14
|
+
const CODEX_PROVIDER_WIRE_API = "responses";
|
|
15
|
+
|
|
16
|
+
interface TomlSectionRange {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
sectionText: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CodexProviderBackup {
|
|
23
|
+
version: number;
|
|
24
|
+
modelProviderLine: string | null;
|
|
25
|
+
providerSectionText: string | null;
|
|
26
|
+
authText: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeEnvValue(value: EnvValue): string | null {
|
|
30
|
+
if (value === null || value === undefined) return null;
|
|
31
|
+
const normalized = String(value).trim();
|
|
32
|
+
return normalized ? normalized : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTextIfExists(filePath: string): string | null {
|
|
36
|
+
if (!fs.existsSync(filePath)) return null;
|
|
37
|
+
try {
|
|
38
|
+
return fs.readFileSync(filePath, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeText(filePath: string, text: string): void {
|
|
45
|
+
const dir = path.dirname(filePath);
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(filePath, text, "utf8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeFileIfExists(filePath: string): void {
|
|
53
|
+
if (!fs.existsSync(filePath)) return;
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(filePath);
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore cleanup failures
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getBackupPath(configPath: string): string {
|
|
62
|
+
return `${configPath}.codenv-provider-backup.json`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readBackup(backupPath: string): CodexProviderBackup | null {
|
|
66
|
+
const raw = readTextIfExists(backupPath);
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw) as Partial<CodexProviderBackup>;
|
|
70
|
+
return {
|
|
71
|
+
version:
|
|
72
|
+
typeof parsed.version === "number" ? parsed.version : 1,
|
|
73
|
+
modelProviderLine:
|
|
74
|
+
typeof parsed.modelProviderLine === "string"
|
|
75
|
+
? parsed.modelProviderLine
|
|
76
|
+
: null,
|
|
77
|
+
providerSectionText:
|
|
78
|
+
typeof parsed.providerSectionText === "string"
|
|
79
|
+
? parsed.providerSectionText
|
|
80
|
+
: null,
|
|
81
|
+
authText: typeof parsed.authText === "string" ? parsed.authText : null,
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeBackup(backupPath: string, backup: CodexProviderBackup): void {
|
|
89
|
+
writeText(backupPath, `${JSON.stringify(backup, null, 2)}\n`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseSectionByHeader(
|
|
93
|
+
text: string,
|
|
94
|
+
headerRegex: RegExp
|
|
95
|
+
): TomlSectionRange | null {
|
|
96
|
+
const match = headerRegex.exec(text);
|
|
97
|
+
if (!match || match.index === undefined) return null;
|
|
98
|
+
|
|
99
|
+
const start = match.index;
|
|
100
|
+
const afterHeader = start + match[0].length;
|
|
101
|
+
const rest = text.slice(afterHeader);
|
|
102
|
+
const nextHeaderMatch = rest.match(/^\s*\[.*?\]\s*$/m);
|
|
103
|
+
const end = nextHeaderMatch
|
|
104
|
+
? afterHeader + (nextHeaderMatch.index ?? rest.length)
|
|
105
|
+
: text.length;
|
|
106
|
+
return {
|
|
107
|
+
start,
|
|
108
|
+
end,
|
|
109
|
+
sectionText: text.slice(start, end).trimEnd(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getFirstSectionIndex(text: string): number {
|
|
114
|
+
const match = text.match(/^\s*\[.*?\]\s*$/m);
|
|
115
|
+
if (!match || match.index === undefined) return text.length;
|
|
116
|
+
return match.index;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getRootText(text: string): { root: string; rest: string } {
|
|
120
|
+
const index = getFirstSectionIndex(text);
|
|
121
|
+
return {
|
|
122
|
+
root: text.slice(0, index),
|
|
123
|
+
rest: text.slice(index),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readModelProviderLine(text: string): string | null {
|
|
128
|
+
const { root } = getRootText(text);
|
|
129
|
+
const match = root.match(/^\s*model_provider\s*=.*$/m);
|
|
130
|
+
return match ? match[0].trimEnd() : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeModelProviderLine(text: string): string {
|
|
134
|
+
const { root, rest } = getRootText(text);
|
|
135
|
+
const updatedRoot = root.replace(/^\s*model_provider\s*=.*(?:\r?\n)?/m, "");
|
|
136
|
+
return `${updatedRoot}${rest}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function insertRootLine(text: string, line: string): string {
|
|
140
|
+
const { root, rest } = getRootText(text);
|
|
141
|
+
const trimmedRoot = root.trimEnd();
|
|
142
|
+
const trimmedRest = rest.trimStart();
|
|
143
|
+
|
|
144
|
+
if (!trimmedRoot) {
|
|
145
|
+
if (!trimmedRest) return `${line}\n`;
|
|
146
|
+
return `${line}\n\n${trimmedRest}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!trimmedRest) {
|
|
150
|
+
return `${trimmedRoot}\n${line}\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${trimmedRoot}\n${line}\n\n${trimmedRest}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function removeSection(text: string, headerRegex: RegExp): string {
|
|
157
|
+
const range = parseSectionByHeader(text, headerRegex);
|
|
158
|
+
if (!range) return text;
|
|
159
|
+
const before = text.slice(0, range.start).trimEnd();
|
|
160
|
+
const after = text.slice(range.end).trimStart();
|
|
161
|
+
if (before && after) return `${before}\n\n${after}`;
|
|
162
|
+
if (before) return `${before}\n`;
|
|
163
|
+
if (after) return `${after}\n`;
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function appendSection(text: string, sectionText: string): string {
|
|
168
|
+
const trimmed = text.trimEnd();
|
|
169
|
+
if (!trimmed) return `${sectionText}\n`;
|
|
170
|
+
return `${trimmed}\n\n${sectionText}\n`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getProviderHeaderRegex(): RegExp {
|
|
174
|
+
return /^\s*\[model_providers\.OpenAI\]\s*$/m;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readProviderSectionText(text: string): string | null {
|
|
178
|
+
const range = parseSectionByHeader(text, getProviderHeaderRegex());
|
|
179
|
+
return range ? range.sectionText : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderProviderSection(baseUrl: string | null): string {
|
|
183
|
+
const lines = [
|
|
184
|
+
`[model_providers.${CODEX_PROVIDER_NAME}]`,
|
|
185
|
+
`name = ${JSON.stringify(CODEX_PROVIDER_NAME)}`,
|
|
186
|
+
];
|
|
187
|
+
if (baseUrl) {
|
|
188
|
+
lines.push(`base_url = ${JSON.stringify(baseUrl)}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`wire_api = ${JSON.stringify(CODEX_PROVIDER_WIRE_API)}`);
|
|
191
|
+
lines.push("requires_openai_auth = true");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureBackup(configPath: string, currentConfigText: string): void {
|
|
196
|
+
const backupPath = getBackupPath(configPath);
|
|
197
|
+
if (readBackup(backupPath)) return;
|
|
198
|
+
writeBackup(backupPath, {
|
|
199
|
+
version: 1,
|
|
200
|
+
modelProviderLine: readModelProviderLine(currentConfigText),
|
|
201
|
+
providerSectionText: readProviderSectionText(currentConfigText),
|
|
202
|
+
authText: readTextIfExists(CODEX_AUTH_PATH),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeManagedConfig(configPath: string, baseUrl: string | null): void {
|
|
207
|
+
const currentText = readTextIfExists(configPath) || "";
|
|
208
|
+
ensureBackup(configPath, currentText);
|
|
209
|
+
|
|
210
|
+
let updated = currentText;
|
|
211
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
212
|
+
updated = removeModelProviderLine(updated);
|
|
213
|
+
updated = insertRootLine(
|
|
214
|
+
updated,
|
|
215
|
+
`model_provider = ${JSON.stringify(CODEX_PROVIDER_NAME)}`
|
|
216
|
+
);
|
|
217
|
+
updated = appendSection(updated, renderProviderSection(baseUrl));
|
|
218
|
+
writeText(configPath, updated);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function writeManagedAuth(apiKey: string | null): void {
|
|
222
|
+
const authJson =
|
|
223
|
+
apiKey === null
|
|
224
|
+
? "null"
|
|
225
|
+
: JSON.stringify({ OPENAI_API_KEY: apiKey });
|
|
226
|
+
writeText(CODEX_AUTH_PATH, `${authJson}\n`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function restoreAuth(authText: string | null): void {
|
|
230
|
+
if (authText === null) {
|
|
231
|
+
removeFileIfExists(CODEX_AUTH_PATH);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
writeText(CODEX_AUTH_PATH, authText);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolveCodexConfigPath(config: Config): string {
|
|
238
|
+
const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
|
|
239
|
+
if (envOverride && String(envOverride).trim()) {
|
|
240
|
+
const expanded = expandEnv(String(envOverride).trim());
|
|
241
|
+
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
242
|
+
}
|
|
243
|
+
const configOverride = config.codexStatusline?.configPath;
|
|
244
|
+
if (configOverride && String(configOverride).trim()) {
|
|
245
|
+
const expanded = expandEnv(String(configOverride).trim());
|
|
246
|
+
return resolvePath(expanded) || DEFAULT_CODEX_CONFIG_PATH;
|
|
247
|
+
}
|
|
248
|
+
return DEFAULT_CODEX_CONFIG_PATH;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function syncCodexProfile(config: Config, profileName: string): void {
|
|
252
|
+
const profile = config.profiles && config.profiles[profileName];
|
|
253
|
+
if (!profile) {
|
|
254
|
+
throw new Error(`Unknown profile: ${profileName}`);
|
|
255
|
+
}
|
|
256
|
+
const env = profile.env || {};
|
|
257
|
+
const baseUrl = normalizeEnvValue(env.OPENAI_BASE_URL);
|
|
258
|
+
const apiKey = normalizeEnvValue(env.OPENAI_API_KEY);
|
|
259
|
+
const configPath = resolveCodexConfigPath(config);
|
|
260
|
+
writeManagedConfig(configPath, baseUrl);
|
|
261
|
+
writeManagedAuth(apiKey);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function clearManagedCodexProfile(config: Config): void {
|
|
265
|
+
const configPath = resolveCodexConfigPath(config);
|
|
266
|
+
const backupPath = getBackupPath(configPath);
|
|
267
|
+
const backup = readBackup(backupPath);
|
|
268
|
+
if (!backup) return;
|
|
269
|
+
|
|
270
|
+
let updated = readTextIfExists(configPath) || "";
|
|
271
|
+
updated = removeSection(updated, getProviderHeaderRegex());
|
|
272
|
+
updated = removeModelProviderLine(updated);
|
|
273
|
+
if (backup.modelProviderLine) {
|
|
274
|
+
updated = insertRootLine(updated, backup.modelProviderLine);
|
|
275
|
+
}
|
|
276
|
+
if (backup.providerSectionText) {
|
|
277
|
+
updated = appendSection(updated, backup.providerSectionText);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const trimmed = updated.trimEnd();
|
|
281
|
+
if (trimmed) {
|
|
282
|
+
writeText(configPath, `${trimmed}\n`);
|
|
283
|
+
} else {
|
|
284
|
+
removeFileIfExists(configPath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
restoreAuth(backup.authText);
|
|
288
|
+
removeFileIfExists(backupPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function resolveCodexProfileFromEnv(
|
|
292
|
+
config: Config,
|
|
293
|
+
profileKey: string | null,
|
|
294
|
+
profileName: string | null
|
|
295
|
+
): string | null {
|
|
296
|
+
const profiles = config.profiles || {};
|
|
297
|
+
if (profileKey && profiles[profileKey]) return profileKey;
|
|
298
|
+
if (!profileName) return null;
|
|
299
|
+
|
|
300
|
+
for (const [key, profile] of Object.entries(profiles)) {
|
|
301
|
+
if (inferProfileType(key, profile || {}, null) !== "codex") continue;
|
|
302
|
+
const displayName = getProfileDisplayName(key, profile || {}, "codex");
|
|
303
|
+
if (displayName === profileName || key === profileName) {
|
|
304
|
+
return key;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -5,7 +5,6 @@ import * as fs from "fs";
|
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { spawn } from "child_process";
|
|
7
7
|
import type { Config, ProfileType } from "../types";
|
|
8
|
-
import { CODEX_AUTH_PATH } from "../constants";
|
|
9
8
|
import { normalizeType } from "../profile/type";
|
|
10
9
|
import {
|
|
11
10
|
getCodexSessionsPath,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
} from "../usage";
|
|
17
16
|
import { ensureClaudeStatusline } from "../statusline/claude";
|
|
18
17
|
import { ensureCodexStatuslineConfig } from "../statusline/codex";
|
|
18
|
+
import { resolveCodexProfileFromEnv, syncCodexProfile } from "../codex/config";
|
|
19
19
|
|
|
20
20
|
const SESSION_BINDING_POLL_MS = 1000;
|
|
21
21
|
const SESSION_BINDING_START_GRACE_MS = 5000;
|
|
@@ -203,24 +203,6 @@ function getProfileEnv(type: ProfileType): { key: string | null; name: string |
|
|
|
203
203
|
return { key, name };
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
function writeCodexAuthFromEnv(): void {
|
|
207
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
208
|
-
try {
|
|
209
|
-
fs.mkdirSync(path.dirname(CODEX_AUTH_PATH), { recursive: true });
|
|
210
|
-
} catch {
|
|
211
|
-
// ignore
|
|
212
|
-
}
|
|
213
|
-
const authJson =
|
|
214
|
-
apiKey === null || apiKey === undefined || apiKey === ""
|
|
215
|
-
? "null"
|
|
216
|
-
: JSON.stringify({ OPENAI_API_KEY: String(apiKey) });
|
|
217
|
-
try {
|
|
218
|
-
fs.writeFileSync(CODEX_AUTH_PATH, `${authJson}\n`, "utf8");
|
|
219
|
-
} catch {
|
|
220
|
-
// ignore
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
206
|
function parseBooleanEnv(value: string | undefined): boolean | null {
|
|
225
207
|
if (value === undefined) return null;
|
|
226
208
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -259,11 +241,18 @@ export async function runLaunch(
|
|
|
259
241
|
if (!type) {
|
|
260
242
|
throw new Error(`Unknown launch target: ${target}`);
|
|
261
243
|
}
|
|
244
|
+
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
262
245
|
if (type === "codex") {
|
|
263
|
-
|
|
246
|
+
const codexProfileKey = resolveCodexProfileFromEnv(
|
|
247
|
+
config,
|
|
248
|
+
profileKey,
|
|
249
|
+
profileName
|
|
250
|
+
);
|
|
251
|
+
if (codexProfileKey) {
|
|
252
|
+
syncCodexProfile(config, codexProfileKey);
|
|
253
|
+
}
|
|
264
254
|
}
|
|
265
255
|
|
|
266
|
-
const { key: profileKey, name: profileName } = getProfileEnv(type);
|
|
267
256
|
const terminalTag = process.env.CODE_ENV_TERMINAL_TAG || null;
|
|
268
257
|
const cwd = process.cwd();
|
|
269
258
|
const startMs = Date.now();
|
package/src/commands/list.ts
CHANGED
|
@@ -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
|
|
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) {
|