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