@praeviso/code-env-switch 0.1.7 → 0.1.9
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 +5 -9
- package/README_zh.md +5 -9
- package/bin/commands/list.js +10 -1
- package/bin/statusline/codex.js +160 -195
- package/bin/statusline/index.js +14 -5
- package/bin/usage/index.js +123 -21
- package/code-env.example.json +1 -4
- package/docs/usage.md +3 -0
- package/docs/usage_zh.md +2 -0
- package/package.json +4 -4
- package/src/commands/list.ts +11 -1
- package/src/statusline/codex.ts +198 -202
- package/src/statusline/index.ts +26 -5
- package/src/types.ts +1 -4
- package/src/usage/index.ts +135 -21
package/src/statusline/codex.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex CLI status line integration
|
|
2
|
+
* Codex CLI status line integration (official schema)
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as os from "os";
|
|
@@ -9,37 +9,27 @@ import { expandEnv, resolvePath } from "../shell/utils";
|
|
|
9
9
|
import { askConfirm, createReadline } from "../ui";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"--type",
|
|
16
|
-
"codex",
|
|
17
|
-
"--sync-usage",
|
|
18
|
-
];
|
|
19
|
-
const DEFAULT_SHOW_HINTS = false;
|
|
20
|
-
const DEFAULT_UPDATE_INTERVAL_MS = 300;
|
|
21
|
-
const DEFAULT_TIMEOUT_MS = 1000;
|
|
22
|
-
|
|
23
|
-
interface ParsedStatusLineConfig {
|
|
24
|
-
command: string | string[] | null;
|
|
25
|
-
showHints: boolean | null;
|
|
26
|
-
updateIntervalMs: number | null;
|
|
27
|
-
timeoutMs: number | null;
|
|
12
|
+
|
|
13
|
+
interface ParsedTuiConfig {
|
|
14
|
+
statusLineItems: string[] | null;
|
|
28
15
|
}
|
|
29
16
|
|
|
30
17
|
interface DesiredStatusLineConfig {
|
|
31
|
-
|
|
32
|
-
showHints: boolean;
|
|
33
|
-
updateIntervalMs: number;
|
|
34
|
-
timeoutMs: number;
|
|
18
|
+
statusLineItems: string[];
|
|
35
19
|
configPath: string;
|
|
36
20
|
}
|
|
37
21
|
|
|
38
|
-
interface
|
|
22
|
+
interface TuiSection {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
sectionText: string;
|
|
26
|
+
config: ParsedTuiConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TomlSectionRange {
|
|
39
30
|
start: number;
|
|
40
31
|
end: number;
|
|
41
32
|
sectionText: string;
|
|
42
|
-
config: ParsedStatusLineConfig;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
function parseBooleanEnv(value: string | undefined): boolean | null {
|
|
@@ -64,6 +54,25 @@ function resolveCodexConfigPath(config: Config): string {
|
|
|
64
54
|
return DEFAULT_CODEX_CONFIG_PATH;
|
|
65
55
|
}
|
|
66
56
|
|
|
57
|
+
function resolveDesiredStatusLineItems(config: Config): string[] | null {
|
|
58
|
+
const raw = config.codexStatusline?.items;
|
|
59
|
+
if (!Array.isArray(raw)) return null;
|
|
60
|
+
return raw
|
|
61
|
+
.map((entry) => String(entry).trim())
|
|
62
|
+
.filter((entry) => entry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveDesiredStatusLineConfig(
|
|
66
|
+
config: Config
|
|
67
|
+
): DesiredStatusLineConfig | null {
|
|
68
|
+
const statusLineItems = resolveDesiredStatusLineItems(config);
|
|
69
|
+
if (statusLineItems === null) return null;
|
|
70
|
+
return {
|
|
71
|
+
statusLineItems,
|
|
72
|
+
configPath: resolveCodexConfigPath(config),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
function readConfig(filePath: string): string {
|
|
68
77
|
if (!fs.existsSync(filePath)) return "";
|
|
69
78
|
try {
|
|
@@ -93,90 +102,84 @@ function stripInlineComment(value: string): string {
|
|
|
93
102
|
return value.trim();
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
function
|
|
97
|
-
const trimmed = value.trim();
|
|
98
|
-
if (
|
|
99
|
-
(trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
|
|
100
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
101
|
-
) {
|
|
102
|
-
return trimmed.slice(1, -1);
|
|
103
|
-
}
|
|
104
|
-
return trimmed;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseCommandValue(raw: string): string | string[] | null {
|
|
108
|
-
const trimmed = raw.trim();
|
|
109
|
-
if (!trimmed) return null;
|
|
110
|
-
if (trimmed.startsWith("[")) {
|
|
111
|
-
const items: string[] = [];
|
|
112
|
-
const regex = /"((?:\\.|[^"\\])*)"/g;
|
|
113
|
-
let match: RegExpExecArray | null = null;
|
|
114
|
-
while ((match = regex.exec(trimmed))) {
|
|
115
|
-
const item = match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
116
|
-
items.push(item);
|
|
117
|
-
}
|
|
118
|
-
return items.length > 0 ? items : null;
|
|
119
|
-
}
|
|
120
|
-
return unquote(trimmed);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function tokenizeCommand(command: string): string[] {
|
|
124
|
-
const tokens: string[] = [];
|
|
125
|
-
let current = "";
|
|
105
|
+
function hasUnquotedClosingBracket(value: string): boolean {
|
|
126
106
|
let inSingle = false;
|
|
127
107
|
let inDouble = false;
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
current += ch;
|
|
133
|
-
escape = false;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (ch === "\\" && !inSingle) {
|
|
137
|
-
escape = true;
|
|
108
|
+
for (let i = 0; i < value.length; i++) {
|
|
109
|
+
const ch = value[i];
|
|
110
|
+
if (ch === "\"" && !inSingle && value[i - 1] !== "\\") {
|
|
111
|
+
inDouble = !inDouble;
|
|
138
112
|
continue;
|
|
139
113
|
}
|
|
140
|
-
if (ch === "'" && !inDouble) {
|
|
114
|
+
if (ch === "'" && !inDouble && value[i - 1] !== "\\") {
|
|
141
115
|
inSingle = !inSingle;
|
|
142
116
|
continue;
|
|
143
117
|
}
|
|
144
|
-
if (ch === "
|
|
145
|
-
|
|
146
|
-
continue;
|
|
118
|
+
if (!inSingle && !inDouble && ch === "]") {
|
|
119
|
+
return true;
|
|
147
120
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseTomlStringArray(raw: string): string[] | null {
|
|
126
|
+
const trimmed = raw.trim();
|
|
127
|
+
if (!trimmed.startsWith("[") || !hasUnquotedClosingBracket(trimmed)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (/^\[\s*\]$/.test(trimmed)) return [];
|
|
131
|
+
|
|
132
|
+
const items: string[] = [];
|
|
133
|
+
const regex = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
134
|
+
let match: RegExpExecArray | null = null;
|
|
135
|
+
while ((match = regex.exec(trimmed))) {
|
|
136
|
+
if (match[0].startsWith("\"")) {
|
|
137
|
+
items.push(match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
|
|
153
138
|
continue;
|
|
154
139
|
}
|
|
155
|
-
|
|
140
|
+
items.push(match[2] || "");
|
|
156
141
|
}
|
|
157
|
-
if (current) tokens.push(current);
|
|
158
|
-
return tokens;
|
|
159
|
-
}
|
|
160
142
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (normalized === "true") return true;
|
|
164
|
-
if (normalized === "false") return false;
|
|
165
|
-
return null;
|
|
143
|
+
if (items.length === 0) return null;
|
|
144
|
+
return items;
|
|
166
145
|
}
|
|
167
146
|
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
147
|
+
function parseStatusLineItems(sectionText: string): string[] | null {
|
|
148
|
+
const lines = sectionText.split(/\r?\n/).slice(1);
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const trimmed = lines[i].trim();
|
|
152
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
|
|
153
|
+
|
|
154
|
+
const matchLine = /^status_line\s*=\s*(.*)$/.exec(trimmed);
|
|
155
|
+
if (!matchLine) continue;
|
|
156
|
+
|
|
157
|
+
const value = stripInlineComment(matchLine[1]);
|
|
158
|
+
if (!value.startsWith("[")) return null;
|
|
159
|
+
|
|
160
|
+
const parts = [value];
|
|
161
|
+
if (!hasUnquotedClosingBracket(value)) {
|
|
162
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
163
|
+
const next = stripInlineComment(lines[j]);
|
|
164
|
+
if (!next) continue;
|
|
165
|
+
parts.push(next);
|
|
166
|
+
if (hasUnquotedClosingBracket(next)) break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parseTomlStringArray(parts.join(" "));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function
|
|
177
|
-
|
|
176
|
+
function parseSectionByHeader(
|
|
177
|
+
text: string,
|
|
178
|
+
headerRegex: RegExp
|
|
179
|
+
): TomlSectionRange | null {
|
|
178
180
|
const match = headerRegex.exec(text);
|
|
179
181
|
if (!match || match.index === undefined) return null;
|
|
182
|
+
|
|
180
183
|
const start = match.index;
|
|
181
184
|
const afterHeader = start + match[0].length;
|
|
182
185
|
const rest = text.slice(afterHeader);
|
|
@@ -184,140 +187,110 @@ function parseStatusLineSection(text: string): StatusLineSection | null {
|
|
|
184
187
|
const end = nextHeaderMatch
|
|
185
188
|
? afterHeader + (nextHeaderMatch.index ?? rest.length)
|
|
186
189
|
: text.length;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
command: null,
|
|
192
|
-
showHints: null,
|
|
193
|
-
updateIntervalMs: null,
|
|
194
|
-
timeoutMs: null,
|
|
190
|
+
return {
|
|
191
|
+
start,
|
|
192
|
+
end,
|
|
193
|
+
sectionText: text.slice(start, end).trimEnd(),
|
|
195
194
|
};
|
|
195
|
+
}
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
|
|
201
|
-
const matchLine = /^([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(trimmed);
|
|
202
|
-
if (!matchLine) continue;
|
|
203
|
-
const key = matchLine[1];
|
|
204
|
-
const rawValue = stripInlineComment(matchLine[2]);
|
|
205
|
-
if (key === "command") {
|
|
206
|
-
config.command = parseCommandValue(rawValue);
|
|
207
|
-
} else if (key === "show_hints") {
|
|
208
|
-
config.showHints = parseBooleanValue(rawValue);
|
|
209
|
-
} else if (key === "update_interval_ms") {
|
|
210
|
-
config.updateIntervalMs = parseNumberValue(rawValue);
|
|
211
|
-
} else if (key === "timeout_ms") {
|
|
212
|
-
config.timeoutMs = parseNumberValue(rawValue);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
197
|
+
function parseTuiSection(text: string): TuiSection | null {
|
|
198
|
+
const section = parseSectionByHeader(text, /^\s*\[tui\]\s*$/m);
|
|
199
|
+
if (!section) return null;
|
|
215
200
|
|
|
216
201
|
return {
|
|
217
|
-
start,
|
|
218
|
-
end,
|
|
219
|
-
sectionText,
|
|
220
|
-
config
|
|
202
|
+
start: section.start,
|
|
203
|
+
end: section.end,
|
|
204
|
+
sectionText: section.sectionText,
|
|
205
|
+
config: {
|
|
206
|
+
statusLineItems: parseStatusLineItems(section.sectionText),
|
|
207
|
+
},
|
|
221
208
|
};
|
|
222
209
|
}
|
|
223
210
|
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
if (Array.isArray(value)) {
|
|
227
|
-
return value.map((entry) => String(entry));
|
|
228
|
-
}
|
|
229
|
-
const trimmed = value.trim();
|
|
230
|
-
if (!trimmed) return null;
|
|
231
|
-
return tokenizeCommand(trimmed);
|
|
211
|
+
function parseLegacyStatusLineSection(text: string): TomlSectionRange | null {
|
|
212
|
+
return parseSectionByHeader(text, /^\s*\[tui\.status_line\]\s*$/m);
|
|
232
213
|
}
|
|
233
214
|
|
|
234
|
-
function
|
|
235
|
-
existing: string
|
|
236
|
-
desired: string
|
|
215
|
+
function statusLineItemsMatch(
|
|
216
|
+
existing: string[] | null,
|
|
217
|
+
desired: string[]
|
|
237
218
|
): boolean {
|
|
238
219
|
if (!existing) return false;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (existingTokens.length !== desiredTokens.length) return false;
|
|
243
|
-
for (let i = 0; i < desiredTokens.length; i++) {
|
|
244
|
-
if (existingTokens[i] !== desiredTokens[i]) return false;
|
|
245
|
-
}
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
if (typeof existing === "string" && typeof desired === "string") {
|
|
249
|
-
return existing.trim() === desired.trim();
|
|
220
|
+
if (existing.length !== desired.length) return false;
|
|
221
|
+
for (let i = 0; i < desired.length; i++) {
|
|
222
|
+
if (existing[i] !== desired[i]) return false;
|
|
250
223
|
}
|
|
251
|
-
return
|
|
224
|
+
return true;
|
|
252
225
|
}
|
|
253
226
|
|
|
254
227
|
function configMatches(
|
|
255
|
-
config:
|
|
228
|
+
config: ParsedTuiConfig,
|
|
256
229
|
desired: DesiredStatusLineConfig
|
|
257
230
|
): boolean {
|
|
258
|
-
|
|
259
|
-
if (config.showHints !== desired.showHints) return false;
|
|
260
|
-
if (config.updateIntervalMs !== desired.updateIntervalMs) return false;
|
|
261
|
-
if (config.timeoutMs !== desired.timeoutMs) return false;
|
|
262
|
-
return true;
|
|
231
|
+
return statusLineItemsMatch(config.statusLineItems, desired.statusLineItems);
|
|
263
232
|
}
|
|
264
233
|
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const raw = config.codexStatusline?.command;
|
|
269
|
-
if (typeof raw === "string") {
|
|
270
|
-
const trimmed = raw.trim();
|
|
271
|
-
if (trimmed) return trimmed;
|
|
272
|
-
} else if (Array.isArray(raw)) {
|
|
273
|
-
const cleaned = raw
|
|
274
|
-
.map((entry) => String(entry).trim())
|
|
275
|
-
.filter((entry) => entry);
|
|
276
|
-
if (cleaned.length > 0) return cleaned;
|
|
277
|
-
}
|
|
278
|
-
return DEFAULT_STATUSLINE_COMMAND;
|
|
234
|
+
function formatStatusLineItems(items: string[]): string {
|
|
235
|
+
const parts = items.map((item) => JSON.stringify(item)).join(", ");
|
|
236
|
+
return `[${parts}]`;
|
|
279
237
|
}
|
|
280
238
|
|
|
281
|
-
function
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
239
|
+
function removeStatusLineSettingLines(lines: string[]): string[] {
|
|
240
|
+
const kept: string[] = [];
|
|
241
|
+
let inMultilineArray = false;
|
|
242
|
+
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const trimmed = line.trim();
|
|
245
|
+
|
|
246
|
+
if (inMultilineArray) {
|
|
247
|
+
const value = stripInlineComment(trimmed);
|
|
248
|
+
if (hasUnquotedClosingBracket(value)) {
|
|
249
|
+
inMultilineArray = false;
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const matchLine = /^status_line\s*=\s*(.*)$/.exec(trimmed);
|
|
255
|
+
if (!matchLine) {
|
|
256
|
+
kept.push(line);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
298
259
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
260
|
+
const value = stripInlineComment(matchLine[1]);
|
|
261
|
+
if (value.startsWith("[") && !hasUnquotedClosingBracket(value)) {
|
|
262
|
+
inMultilineArray = true;
|
|
263
|
+
}
|
|
303
264
|
}
|
|
304
|
-
|
|
265
|
+
|
|
266
|
+
return kept;
|
|
305
267
|
}
|
|
306
268
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
`
|
|
315
|
-
|
|
269
|
+
function buildTuiSection(
|
|
270
|
+
section: TuiSection | null,
|
|
271
|
+
desired: DesiredStatusLineConfig
|
|
272
|
+
): string {
|
|
273
|
+
const statusLine = `status_line = ${formatStatusLineItems(desired.statusLineItems)}`;
|
|
274
|
+
|
|
275
|
+
if (!section) {
|
|
276
|
+
return `[tui]\n${statusLine}\n`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const lines = section.sectionText.split(/\r?\n/);
|
|
280
|
+
const header = lines[0].trim() === "[tui]" ? lines[0] : "[tui]";
|
|
281
|
+
const body = removeStatusLineSettingLines(lines.slice(1));
|
|
282
|
+
|
|
283
|
+
while (body.length > 0 && body[body.length - 1].trim() === "") {
|
|
284
|
+
body.pop();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
body.push(statusLine);
|
|
288
|
+
return `${header}\n${body.join("\n")}\n`;
|
|
316
289
|
}
|
|
317
290
|
|
|
318
291
|
function upsertSection(
|
|
319
292
|
text: string,
|
|
320
|
-
section:
|
|
293
|
+
section: TuiSection | null,
|
|
321
294
|
newSection: string
|
|
322
295
|
): string {
|
|
323
296
|
if (!section) {
|
|
@@ -326,6 +299,7 @@ function upsertSection(
|
|
|
326
299
|
if (base && !base.endsWith("\n\n")) base += "\n";
|
|
327
300
|
return `${base}${newSection}`;
|
|
328
301
|
}
|
|
302
|
+
|
|
329
303
|
let prefix = text.slice(0, section.start);
|
|
330
304
|
let suffix = text.slice(section.end);
|
|
331
305
|
if (prefix && !prefix.endsWith("\n")) prefix += "\n";
|
|
@@ -333,6 +307,17 @@ function upsertSection(
|
|
|
333
307
|
return `${prefix}${newSection}${suffix}`;
|
|
334
308
|
}
|
|
335
309
|
|
|
310
|
+
function removeSection(
|
|
311
|
+
text: string,
|
|
312
|
+
section: TomlSectionRange
|
|
313
|
+
): string {
|
|
314
|
+
let prefix = text.slice(0, section.start);
|
|
315
|
+
let suffix = text.slice(section.end);
|
|
316
|
+
if (prefix && !prefix.endsWith("\n")) prefix += "\n";
|
|
317
|
+
if (suffix && !suffix.startsWith("\n")) suffix = `\n${suffix}`;
|
|
318
|
+
return `${prefix}${suffix}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
336
321
|
function writeConfig(filePath: string, text: string): boolean {
|
|
337
322
|
try {
|
|
338
323
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -352,21 +337,32 @@ export async function ensureCodexStatuslineConfig(
|
|
|
352
337
|
if (!enabled || disabled) return false;
|
|
353
338
|
|
|
354
339
|
const desired = resolveDesiredStatusLineConfig(config);
|
|
340
|
+
if (!desired) return false;
|
|
341
|
+
|
|
355
342
|
const configPath = desired.configPath;
|
|
356
343
|
const raw = readConfig(configPath);
|
|
357
|
-
const
|
|
344
|
+
const legacySection = parseLegacyStatusLineSection(raw);
|
|
345
|
+
const base = legacySection ? removeSection(raw, legacySection) : raw;
|
|
346
|
+
const section = parseTuiSection(base);
|
|
358
347
|
|
|
359
348
|
if (section && configMatches(section.config, desired)) return false;
|
|
360
349
|
|
|
361
350
|
const force =
|
|
362
351
|
parseBooleanEnv(process.env.CODE_ENV_CODEX_STATUSLINE_FORCE) === true;
|
|
352
|
+
const hasExistingStatusLine = Boolean(
|
|
353
|
+
(section && Array.isArray(section.config.statusLineItems)) || legacySection
|
|
354
|
+
);
|
|
363
355
|
|
|
364
|
-
if (
|
|
365
|
-
console.log(`codenv: existing Codex status_line config in ${configPath}:`);
|
|
366
|
-
|
|
356
|
+
if (hasExistingStatusLine && !force) {
|
|
357
|
+
console.log(`codenv: existing Codex tui.status_line config in ${configPath}:`);
|
|
358
|
+
if (section && Array.isArray(section.config.statusLineItems)) {
|
|
359
|
+
console.log(section.sectionText);
|
|
360
|
+
} else if (legacySection) {
|
|
361
|
+
console.log(legacySection.sectionText);
|
|
362
|
+
}
|
|
367
363
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
368
364
|
console.warn(
|
|
369
|
-
"codenv: no TTY available to confirm status_line overwrite."
|
|
365
|
+
"codenv: no TTY available to confirm tui.status_line overwrite."
|
|
370
366
|
);
|
|
371
367
|
return false;
|
|
372
368
|
}
|
|
@@ -374,7 +370,7 @@ export async function ensureCodexStatuslineConfig(
|
|
|
374
370
|
try {
|
|
375
371
|
const confirm = await askConfirm(
|
|
376
372
|
rl,
|
|
377
|
-
"Overwrite Codex status_line config? (y/N): "
|
|
373
|
+
"Overwrite Codex tui.status_line config? (y/N): "
|
|
378
374
|
);
|
|
379
375
|
if (!confirm) return false;
|
|
380
376
|
} finally {
|
|
@@ -382,10 +378,10 @@ export async function ensureCodexStatuslineConfig(
|
|
|
382
378
|
}
|
|
383
379
|
}
|
|
384
380
|
|
|
385
|
-
const updated = upsertSection(
|
|
381
|
+
const updated = upsertSection(base, section, buildTuiSection(section, desired));
|
|
386
382
|
if (!writeConfig(configPath, updated)) {
|
|
387
383
|
console.error(
|
|
388
|
-
"codenv: failed to write Codex config; status_line not updated."
|
|
384
|
+
"codenv: failed to write Codex config; tui.status_line not updated."
|
|
389
385
|
);
|
|
390
386
|
return false;
|
|
391
387
|
}
|
package/src/statusline/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { normalizeType, inferProfileType, getProfileDisplayName } from "../profi
|
|
|
6
6
|
import {
|
|
7
7
|
readUsageCostIndex,
|
|
8
8
|
readUsageSessionCost,
|
|
9
|
+
resolveProfileFromLog,
|
|
9
10
|
resolveUsageCostForProfile,
|
|
10
11
|
syncUsageFromStatuslineInput,
|
|
11
12
|
} from "../usage";
|
|
@@ -62,7 +63,7 @@ export function buildStatuslineResult(
|
|
|
62
63
|
let type = normalizeTypeValue(typeCandidate);
|
|
63
64
|
const envProfile = resolveEnvProfile(type);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
let profileKey = firstNonEmpty(
|
|
66
67
|
args.profileKey,
|
|
67
68
|
envProfile.key,
|
|
68
69
|
inputProfile ? inputProfile.key : null
|
|
@@ -73,6 +74,21 @@ export function buildStatuslineResult(
|
|
|
73
74
|
inputProfile ? inputProfile.name : null
|
|
74
75
|
);
|
|
75
76
|
|
|
77
|
+
const terminalTag = process.env.CODE_ENV_TERMINAL_TAG || null;
|
|
78
|
+
if (!profileKey && !profileName) {
|
|
79
|
+
const fallback = resolveProfileFromLog(
|
|
80
|
+
config,
|
|
81
|
+
configPath,
|
|
82
|
+
normalizeType(type || ""),
|
|
83
|
+
terminalTag
|
|
84
|
+
);
|
|
85
|
+
if (fallback) {
|
|
86
|
+
profileKey = fallback.profileKey;
|
|
87
|
+
profileName = fallback.profileName;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sessionId = getSessionId(stdinInput);
|
|
76
92
|
if (profileKey && !profileName && config.profiles && config.profiles[profileKey]) {
|
|
77
93
|
const profile = config.profiles[profileKey];
|
|
78
94
|
profileName = getProfileDisplayName(profileKey, profile, type || undefined);
|
|
@@ -96,9 +112,10 @@ export function buildStatuslineResult(
|
|
|
96
112
|
process.cwd()
|
|
97
113
|
)!;
|
|
98
114
|
|
|
99
|
-
const sessionId = getSessionId(stdinInput);
|
|
100
115
|
const usageType = normalizeType(type || "");
|
|
101
116
|
const stdinUsageTotals = getUsageTotalsFromInput(stdinInput, usageType);
|
|
117
|
+
const shouldSyncUsageFromSessions =
|
|
118
|
+
args.syncUsage && !stdinUsageTotals;
|
|
102
119
|
const model = firstNonEmpty(
|
|
103
120
|
args.model,
|
|
104
121
|
process.env.CODE_ENV_MODEL,
|
|
@@ -184,7 +201,7 @@ export function buildStatuslineResult(
|
|
|
184
201
|
type,
|
|
185
202
|
profileKey,
|
|
186
203
|
profileName,
|
|
187
|
-
|
|
204
|
+
shouldSyncUsageFromSessions
|
|
188
205
|
);
|
|
189
206
|
|
|
190
207
|
let finalUsage: StatuslineUsage | null = hasExplicitUsage ? usage : null;
|
|
@@ -236,14 +253,18 @@ export function buildStatuslineResult(
|
|
|
236
253
|
configPath,
|
|
237
254
|
type,
|
|
238
255
|
sessionId,
|
|
239
|
-
|
|
256
|
+
shouldSyncUsageFromSessions
|
|
240
257
|
)
|
|
241
258
|
: null;
|
|
242
259
|
sessionCost =
|
|
243
260
|
sessionCostFromRecords ??
|
|
244
261
|
(sessionUsage ? calculateUsageCost(sessionUsage, pricing) : null);
|
|
245
262
|
}
|
|
246
|
-
const costIndex = readUsageCostIndex(
|
|
263
|
+
const costIndex = readUsageCostIndex(
|
|
264
|
+
config,
|
|
265
|
+
configPath,
|
|
266
|
+
shouldSyncUsageFromSessions
|
|
267
|
+
);
|
|
247
268
|
const costTotals = costIndex
|
|
248
269
|
? resolveUsageCostForProfile(costIndex, type, profileKey, profileName)
|
|
249
270
|
: null;
|
package/src/types.ts
CHANGED