@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.
@@ -2,24 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureCodexStatuslineConfig = ensureCodexStatuslineConfig;
4
4
  /**
5
- * Codex CLI status line integration
5
+ * Codex CLI status line integration (official schema)
6
6
  */
7
7
  const fs = require("fs");
8
- const os = require("os");
9
8
  const path = require("path");
10
- const utils_1 = require("../shell/utils");
9
+ const config_1 = require("../codex/config");
11
10
  const ui_1 = require("../ui");
12
- const DEFAULT_CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
13
- const DEFAULT_STATUSLINE_COMMAND = [
14
- "codenv",
15
- "statusline",
16
- "--type",
17
- "codex",
18
- "--sync-usage",
19
- ];
20
- const DEFAULT_SHOW_HINTS = false;
21
- const DEFAULT_UPDATE_INTERVAL_MS = 300;
22
- const DEFAULT_TIMEOUT_MS = 1000;
23
11
  function parseBooleanEnv(value) {
24
12
  if (value === undefined)
25
13
  return null;
@@ -30,19 +18,23 @@ function parseBooleanEnv(value) {
30
18
  return false;
31
19
  return null;
32
20
  }
33
- function resolveCodexConfigPath(config) {
21
+ function resolveDesiredStatusLineItems(config) {
34
22
  var _a;
35
- const envOverride = process.env.CODE_ENV_CODEX_CONFIG_PATH;
36
- if (envOverride && String(envOverride).trim()) {
37
- const expanded = (0, utils_1.expandEnv)(String(envOverride).trim());
38
- return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
39
- }
40
- const configOverride = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.configPath;
41
- if (configOverride && String(configOverride).trim()) {
42
- const expanded = (0, utils_1.expandEnv)(String(configOverride).trim());
43
- return (0, utils_1.resolvePath)(expanded) || DEFAULT_CODEX_CONFIG_PATH;
44
- }
45
- return DEFAULT_CODEX_CONFIG_PATH;
23
+ const raw = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.items;
24
+ if (!Array.isArray(raw))
25
+ return null;
26
+ return raw
27
+ .map((entry) => String(entry).trim())
28
+ .filter((entry) => entry);
29
+ }
30
+ function resolveDesiredStatusLineConfig(config) {
31
+ const statusLineItems = resolveDesiredStatusLineItems(config);
32
+ if (statusLineItems === null)
33
+ return null;
34
+ return {
35
+ statusLineItems,
36
+ configPath: (0, config_1.resolveCodexConfigPath)(config),
37
+ };
46
38
  }
47
39
  function readConfig(filePath) {
48
40
  if (!fs.existsSync(filePath))
@@ -73,88 +65,75 @@ function stripInlineComment(value) {
73
65
  }
74
66
  return value.trim();
75
67
  }
76
- function unquote(value) {
77
- const trimmed = value.trim();
78
- if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
79
- (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
80
- return trimmed.slice(1, -1);
81
- }
82
- return trimmed;
83
- }
84
- function parseCommandValue(raw) {
85
- const trimmed = raw.trim();
86
- if (!trimmed)
87
- return null;
88
- if (trimmed.startsWith("[")) {
89
- const items = [];
90
- const regex = /"((?:\\.|[^"\\])*)"/g;
91
- let match = null;
92
- while ((match = regex.exec(trimmed))) {
93
- const item = match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
94
- items.push(item);
95
- }
96
- return items.length > 0 ? items : null;
97
- }
98
- return unquote(trimmed);
99
- }
100
- function tokenizeCommand(command) {
101
- const tokens = [];
102
- let current = "";
68
+ function hasUnquotedClosingBracket(value) {
103
69
  let inSingle = false;
104
70
  let inDouble = false;
105
- let escape = false;
106
- for (let i = 0; i < command.length; i++) {
107
- const ch = command[i];
108
- if (escape) {
109
- current += ch;
110
- escape = false;
111
- continue;
112
- }
113
- if (ch === "\\" && !inSingle) {
114
- escape = true;
71
+ for (let i = 0; i < value.length; i++) {
72
+ const ch = value[i];
73
+ if (ch === "\"" && !inSingle && value[i - 1] !== "\\") {
74
+ inDouble = !inDouble;
115
75
  continue;
116
76
  }
117
- if (ch === "'" && !inDouble) {
77
+ if (ch === "'" && !inDouble && value[i - 1] !== "\\") {
118
78
  inSingle = !inSingle;
119
79
  continue;
120
80
  }
121
- if (ch === "\"" && !inSingle) {
122
- inDouble = !inDouble;
123
- continue;
124
- }
125
- if (!inSingle && !inDouble && /\s/.test(ch)) {
126
- if (current) {
127
- tokens.push(current);
128
- current = "";
129
- }
130
- continue;
81
+ if (!inSingle && !inDouble && ch === "]") {
82
+ return true;
131
83
  }
132
- current += ch;
133
84
  }
134
- if (current)
135
- tokens.push(current);
136
- return tokens;
137
- }
138
- function parseBooleanValue(raw) {
139
- const normalized = raw.trim().toLowerCase();
140
- if (normalized === "true")
141
- return true;
142
- if (normalized === "false")
143
- return false;
144
- return null;
85
+ return false;
145
86
  }
146
- function parseNumberValue(raw) {
87
+ function parseTomlStringArray(raw) {
147
88
  const trimmed = raw.trim();
148
- if (!trimmed)
89
+ if (!trimmed.startsWith("[") || !hasUnquotedClosingBracket(trimmed)) {
149
90
  return null;
150
- const parsed = Number(trimmed);
151
- if (!Number.isFinite(parsed))
91
+ }
92
+ if (/^\[\s*\]$/.test(trimmed))
93
+ return [];
94
+ const items = [];
95
+ const regex = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
96
+ let match = null;
97
+ while ((match = regex.exec(trimmed))) {
98
+ if (match[0].startsWith("\"")) {
99
+ items.push(match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
100
+ continue;
101
+ }
102
+ items.push(match[2] || "");
103
+ }
104
+ if (items.length === 0)
152
105
  return null;
153
- return Math.floor(parsed);
106
+ return items;
107
+ }
108
+ function parseStatusLineItems(sectionText) {
109
+ const lines = sectionText.split(/\r?\n/).slice(1);
110
+ for (let i = 0; i < lines.length; i++) {
111
+ const trimmed = lines[i].trim();
112
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
113
+ continue;
114
+ const matchLine = /^status_line\s*=\s*(.*)$/.exec(trimmed);
115
+ if (!matchLine)
116
+ continue;
117
+ const value = stripInlineComment(matchLine[1]);
118
+ if (!value.startsWith("["))
119
+ return null;
120
+ const parts = [value];
121
+ if (!hasUnquotedClosingBracket(value)) {
122
+ for (let j = i + 1; j < lines.length; j++) {
123
+ const next = stripInlineComment(lines[j]);
124
+ if (!next)
125
+ continue;
126
+ parts.push(next);
127
+ if (hasUnquotedClosingBracket(next))
128
+ break;
129
+ }
130
+ }
131
+ return parseTomlStringArray(parts.join(" "));
132
+ }
133
+ return null;
154
134
  }
155
- function parseStatusLineSection(text) {
135
+ function parseSectionByHeader(text, headerRegex) {
156
136
  var _a;
157
- const headerRegex = /^\s*\[tui\.status_line\]\s*$/m;
158
137
  const match = headerRegex.exec(text);
159
138
  if (!match || match.index === undefined)
160
139
  return null;
@@ -165,132 +144,83 @@ function parseStatusLineSection(text) {
165
144
  const end = nextHeaderMatch
166
145
  ? afterHeader + ((_a = nextHeaderMatch.index) !== null && _a !== void 0 ? _a : rest.length)
167
146
  : text.length;
168
- const sectionText = text.slice(start, end).trimEnd();
169
- const lines = sectionText.split(/\r?\n/).slice(1);
170
- const config = {
171
- command: null,
172
- showHints: null,
173
- updateIntervalMs: null,
174
- timeoutMs: null,
175
- };
176
- for (const line of lines) {
177
- const trimmed = line.trim();
178
- if (!trimmed)
179
- continue;
180
- if (trimmed.startsWith("#") || trimmed.startsWith(";"))
181
- continue;
182
- const matchLine = /^([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(trimmed);
183
- if (!matchLine)
184
- continue;
185
- const key = matchLine[1];
186
- const rawValue = stripInlineComment(matchLine[2]);
187
- if (key === "command") {
188
- config.command = parseCommandValue(rawValue);
189
- }
190
- else if (key === "show_hints") {
191
- config.showHints = parseBooleanValue(rawValue);
192
- }
193
- else if (key === "update_interval_ms") {
194
- config.updateIntervalMs = parseNumberValue(rawValue);
195
- }
196
- else if (key === "timeout_ms") {
197
- config.timeoutMs = parseNumberValue(rawValue);
198
- }
199
- }
200
147
  return {
201
148
  start,
202
149
  end,
203
- sectionText,
204
- config,
150
+ sectionText: text.slice(start, end).trimEnd(),
205
151
  };
206
152
  }
207
- function commandToArray(value) {
208
- if (!value)
153
+ function parseTuiSection(text) {
154
+ const section = parseSectionByHeader(text, /^\s*\[tui\]\s*$/m);
155
+ if (!section)
209
156
  return null;
210
- if (Array.isArray(value)) {
211
- return value.map((entry) => String(entry));
212
- }
213
- const trimmed = value.trim();
214
- if (!trimmed)
215
- return null;
216
- return tokenizeCommand(trimmed);
157
+ return {
158
+ start: section.start,
159
+ end: section.end,
160
+ sectionText: section.sectionText,
161
+ config: {
162
+ statusLineItems: parseStatusLineItems(section.sectionText),
163
+ },
164
+ };
217
165
  }
218
- function commandMatches(existing, desired) {
166
+ function parseLegacyStatusLineSection(text) {
167
+ return parseSectionByHeader(text, /^\s*\[tui\.status_line\]\s*$/m);
168
+ }
169
+ function statusLineItemsMatch(existing, desired) {
219
170
  if (!existing)
220
171
  return false;
221
- const existingTokens = commandToArray(existing);
222
- const desiredTokens = commandToArray(desired);
223
- if (existingTokens && desiredTokens) {
224
- if (existingTokens.length !== desiredTokens.length)
172
+ if (existing.length !== desired.length)
173
+ return false;
174
+ for (let i = 0; i < desired.length; i++) {
175
+ if (existing[i] !== desired[i])
225
176
  return false;
226
- for (let i = 0; i < desiredTokens.length; i++) {
227
- if (existingTokens[i] !== desiredTokens[i])
228
- return false;
229
- }
230
- return true;
231
177
  }
232
- if (typeof existing === "string" && typeof desired === "string") {
233
- return existing.trim() === desired.trim();
234
- }
235
- return false;
236
- }
237
- function configMatches(config, desired) {
238
- if (!commandMatches(config.command, desired.command))
239
- return false;
240
- if (config.showHints !== desired.showHints)
241
- return false;
242
- if (config.updateIntervalMs !== desired.updateIntervalMs)
243
- return false;
244
- if (config.timeoutMs !== desired.timeoutMs)
245
- return false;
246
178
  return true;
247
179
  }
248
- function resolveDesiredCommand(config) {
249
- var _a;
250
- const raw = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.command;
251
- if (typeof raw === "string") {
252
- const trimmed = raw.trim();
253
- if (trimmed)
254
- return trimmed;
255
- }
256
- else if (Array.isArray(raw)) {
257
- const cleaned = raw
258
- .map((entry) => String(entry).trim())
259
- .filter((entry) => entry);
260
- if (cleaned.length > 0)
261
- return cleaned;
262
- }
263
- return DEFAULT_STATUSLINE_COMMAND;
180
+ function configMatches(config, desired) {
181
+ return statusLineItemsMatch(config.statusLineItems, desired.statusLineItems);
264
182
  }
265
- function resolveDesiredStatusLineConfig(config) {
266
- var _a, _b, _c, _d, _e, _f;
267
- const showHints = (_b = (_a = config.codexStatusline) === null || _a === void 0 ? void 0 : _a.showHints) !== null && _b !== void 0 ? _b : DEFAULT_SHOW_HINTS;
268
- const updateIntervalMs = (_d = (_c = config.codexStatusline) === null || _c === void 0 ? void 0 : _c.updateIntervalMs) !== null && _d !== void 0 ? _d : DEFAULT_UPDATE_INTERVAL_MS;
269
- const timeoutMs = (_f = (_e = config.codexStatusline) === null || _e === void 0 ? void 0 : _e.timeoutMs) !== null && _f !== void 0 ? _f : DEFAULT_TIMEOUT_MS;
270
- const command = resolveDesiredCommand(config);
271
- const configPath = resolveCodexConfigPath(config);
272
- return {
273
- command,
274
- showHints,
275
- updateIntervalMs,
276
- timeoutMs,
277
- configPath,
278
- };
183
+ function formatStatusLineItems(items) {
184
+ const parts = items.map((item) => JSON.stringify(item)).join(", ");
185
+ return `[${parts}]`;
279
186
  }
280
- function formatCommandValue(command) {
281
- if (Array.isArray(command)) {
282
- const parts = command.map((part) => JSON.stringify(part)).join(", ");
283
- return `[${parts}]`;
187
+ function removeStatusLineSettingLines(lines) {
188
+ const kept = [];
189
+ let inMultilineArray = false;
190
+ for (const line of lines) {
191
+ const trimmed = line.trim();
192
+ if (inMultilineArray) {
193
+ const value = stripInlineComment(trimmed);
194
+ if (hasUnquotedClosingBracket(value)) {
195
+ inMultilineArray = false;
196
+ }
197
+ continue;
198
+ }
199
+ const matchLine = /^status_line\s*=\s*(.*)$/.exec(trimmed);
200
+ if (!matchLine) {
201
+ kept.push(line);
202
+ continue;
203
+ }
204
+ const value = stripInlineComment(matchLine[1]);
205
+ if (value.startsWith("[") && !hasUnquotedClosingBracket(value)) {
206
+ inMultilineArray = true;
207
+ }
284
208
  }
285
- return JSON.stringify(command);
209
+ return kept;
286
210
  }
287
- function buildStatusLineSection(desired) {
288
- const commandText = formatCommandValue(desired.command);
289
- return (`[tui.status_line]\n` +
290
- `command = ${commandText}\n` +
291
- `show_hints = ${desired.showHints ? "true" : "false"}\n` +
292
- `update_interval_ms = ${desired.updateIntervalMs}\n` +
293
- `timeout_ms = ${desired.timeoutMs}\n`);
211
+ function buildTuiSection(section, desired) {
212
+ const statusLine = `status_line = ${formatStatusLineItems(desired.statusLineItems)}`;
213
+ if (!section) {
214
+ return `[tui]\n${statusLine}\n`;
215
+ }
216
+ const lines = section.sectionText.split(/\r?\n/);
217
+ const header = lines[0].trim() === "[tui]" ? lines[0] : "[tui]";
218
+ const body = removeStatusLineSettingLines(lines.slice(1));
219
+ while (body.length > 0 && body[body.length - 1].trim() === "") {
220
+ body.pop();
221
+ }
222
+ body.push(statusLine);
223
+ return `${header}\n${body.join("\n")}\n`;
294
224
  }
295
225
  function upsertSection(text, section, newSection) {
296
226
  if (!section) {
@@ -309,6 +239,15 @@ function upsertSection(text, section, newSection) {
309
239
  suffix = `\n${suffix}`;
310
240
  return `${prefix}${newSection}${suffix}`;
311
241
  }
242
+ function removeSection(text, section) {
243
+ let prefix = text.slice(0, section.start);
244
+ let suffix = text.slice(section.end);
245
+ if (prefix && !prefix.endsWith("\n"))
246
+ prefix += "\n";
247
+ if (suffix && !suffix.startsWith("\n"))
248
+ suffix = `\n${suffix}`;
249
+ return `${prefix}${suffix}`;
250
+ }
312
251
  function writeConfig(filePath, text) {
313
252
  try {
314
253
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -324,22 +263,32 @@ async function ensureCodexStatuslineConfig(config, enabled) {
324
263
  if (!enabled || disabled)
325
264
  return false;
326
265
  const desired = resolveDesiredStatusLineConfig(config);
266
+ if (!desired)
267
+ return false;
327
268
  const configPath = desired.configPath;
328
269
  const raw = readConfig(configPath);
329
- const section = parseStatusLineSection(raw);
270
+ const legacySection = parseLegacyStatusLineSection(raw);
271
+ const base = legacySection ? removeSection(raw, legacySection) : raw;
272
+ const section = parseTuiSection(base);
330
273
  if (section && configMatches(section.config, desired))
331
274
  return false;
332
275
  const force = parseBooleanEnv(process.env.CODE_ENV_CODEX_STATUSLINE_FORCE) === true;
333
- if (section && !force) {
334
- console.log(`codenv: existing Codex status_line config in ${configPath}:`);
335
- console.log(section.sectionText);
276
+ const hasExistingStatusLine = Boolean((section && Array.isArray(section.config.statusLineItems)) || legacySection);
277
+ if (hasExistingStatusLine && !force) {
278
+ console.log(`codenv: existing Codex tui.status_line config in ${configPath}:`);
279
+ if (section && Array.isArray(section.config.statusLineItems)) {
280
+ console.log(section.sectionText);
281
+ }
282
+ else if (legacySection) {
283
+ console.log(legacySection.sectionText);
284
+ }
336
285
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
337
- console.warn("codenv: no TTY available to confirm status_line overwrite.");
286
+ console.warn("codenv: no TTY available to confirm tui.status_line overwrite.");
338
287
  return false;
339
288
  }
340
289
  const rl = (0, ui_1.createReadline)();
341
290
  try {
342
- const confirm = await (0, ui_1.askConfirm)(rl, "Overwrite Codex status_line config? (y/N): ");
291
+ const confirm = await (0, ui_1.askConfirm)(rl, "Overwrite Codex tui.status_line config? (y/N): ");
343
292
  if (!confirm)
344
293
  return false;
345
294
  }
@@ -347,9 +296,9 @@ async function ensureCodexStatuslineConfig(config, enabled) {
347
296
  rl.close();
348
297
  }
349
298
  }
350
- const updated = upsertSection(raw, section, buildStatusLineSection(desired));
299
+ const updated = upsertSection(base, section, buildTuiSection(section, desired));
351
300
  if (!writeConfig(configPath, updated)) {
352
- console.error("codenv: failed to write Codex config; status_line not updated.");
301
+ console.error("codenv: failed to write Codex config; tui.status_line not updated.");
353
302
  return false;
354
303
  }
355
304
  return true;
@@ -19,8 +19,17 @@ function buildStatuslineResult(args, config, configPath) {
19
19
  }
20
20
  let type = (0, input_1.normalizeTypeValue)(typeCandidate);
21
21
  const envProfile = (0, input_1.resolveEnvProfile)(type);
22
- const profileKey = (0, utils_1.firstNonEmpty)(args.profileKey, envProfile.key, inputProfile ? inputProfile.key : null);
22
+ let profileKey = (0, utils_1.firstNonEmpty)(args.profileKey, envProfile.key, inputProfile ? inputProfile.key : null);
23
23
  let profileName = (0, utils_1.firstNonEmpty)(args.profileName, envProfile.name, inputProfile ? inputProfile.name : null);
24
+ const terminalTag = process.env.CODE_ENV_TERMINAL_TAG || null;
25
+ if (!profileKey && !profileName) {
26
+ const fallback = (0, usage_1.resolveProfileFromLog)(config, configPath, (0, type_1.normalizeType)(type || ""), terminalTag);
27
+ if (fallback) {
28
+ profileKey = fallback.profileKey;
29
+ profileName = fallback.profileName;
30
+ }
31
+ }
32
+ const sessionId = (0, input_1.getSessionId)(stdinInput);
24
33
  if (profileKey && !profileName && config.profiles && config.profiles[profileKey]) {
25
34
  const profile = config.profiles[profileKey];
26
35
  profileName = (0, type_1.getProfileDisplayName)(profileKey, profile, type || undefined);
@@ -37,7 +46,6 @@ function buildStatuslineResult(args, config, configPath) {
37
46
  type = inferred;
38
47
  }
39
48
  const cwd = (0, utils_1.firstNonEmpty)(args.cwd, process.env.CODE_ENV_CWD, (0, input_1.getWorkspaceDir)(stdinInput), stdinInput ? stdinInput.cwd : null, process.cwd());
40
- const sessionId = (0, input_1.getSessionId)(stdinInput);
41
49
  const usageType = (0, type_1.normalizeType)(type || "");
42
50
  const stdinUsageTotals = (0, usage_2.getUsageTotalsFromInput)(stdinInput, usageType);
43
51
  const shouldSyncUsageFromSessions = args.syncUsage && !stdinUsageTotals;