@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.
@@ -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
- const DEFAULT_STATUSLINE_COMMAND = [
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;
12
+
13
+ interface ParsedTuiConfig {
14
+ statusLineItems: string[] | null;
28
15
  }
29
16
 
30
17
  interface DesiredStatusLineConfig {
31
- command: string | string[];
32
- showHints: boolean;
33
- updateIntervalMs: number;
34
- timeoutMs: number;
18
+ statusLineItems: string[];
35
19
  configPath: string;
36
20
  }
37
21
 
38
- interface StatusLineSection {
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 unquote(value: string): string {
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 escape = false;
129
- for (let i = 0; i < command.length; i++) {
130
- const ch = command[i];
131
- if (escape) {
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 === "\"" && !inSingle) {
145
- inDouble = !inDouble;
146
- continue;
118
+ if (!inSingle && !inDouble && ch === "]") {
119
+ return true;
147
120
  }
148
- if (!inSingle && !inDouble && /\s/.test(ch)) {
149
- if (current) {
150
- tokens.push(current);
151
- current = "";
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
- current += ch;
140
+ items.push(match[2] || "");
156
141
  }
157
- if (current) tokens.push(current);
158
- return tokens;
159
- }
160
142
 
161
- function parseBooleanValue(raw: string): boolean | null {
162
- const normalized = raw.trim().toLowerCase();
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 parseNumberValue(raw: string): number | null {
169
- const trimmed = raw.trim();
170
- if (!trimmed) return null;
171
- const parsed = Number(trimmed);
172
- if (!Number.isFinite(parsed)) return null;
173
- return Math.floor(parsed);
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 parseStatusLineSection(text: string): StatusLineSection | null {
177
- const headerRegex = /^\s*\[tui\.status_line\]\s*$/m;
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
- const sectionText = text.slice(start, end).trimEnd();
188
- const lines = sectionText.split(/\r?\n/).slice(1);
189
-
190
- const config: ParsedStatusLineConfig = {
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
- for (const line of lines) {
198
- const trimmed = line.trim();
199
- if (!trimmed) continue;
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 commandToArray(value: string | string[] | null): string[] | null {
225
- if (!value) return null;
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 commandMatches(
235
- existing: string | string[] | null,
236
- desired: string | string[]
215
+ function statusLineItemsMatch(
216
+ existing: string[] | null,
217
+ desired: string[]
237
218
  ): boolean {
238
219
  if (!existing) return false;
239
- const existingTokens = commandToArray(existing);
240
- const desiredTokens = commandToArray(desired);
241
- if (existingTokens && desiredTokens) {
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 false;
224
+ return true;
252
225
  }
253
226
 
254
227
  function configMatches(
255
- config: ParsedStatusLineConfig,
228
+ config: ParsedTuiConfig,
256
229
  desired: DesiredStatusLineConfig
257
230
  ): boolean {
258
- if (!commandMatches(config.command, desired.command)) return false;
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 resolveDesiredCommand(
266
- config: Config
267
- ): string | string[] {
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 resolveDesiredStatusLineConfig(config: Config): DesiredStatusLineConfig {
282
- const showHints =
283
- config.codexStatusline?.showHints ?? DEFAULT_SHOW_HINTS;
284
- const updateIntervalMs =
285
- config.codexStatusline?.updateIntervalMs ?? DEFAULT_UPDATE_INTERVAL_MS;
286
- const timeoutMs =
287
- config.codexStatusline?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
288
- const command = resolveDesiredCommand(config);
289
- const configPath = resolveCodexConfigPath(config);
290
- return {
291
- command,
292
- showHints,
293
- updateIntervalMs,
294
- timeoutMs,
295
- configPath,
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
- function formatCommandValue(command: string | string[]): string {
300
- if (Array.isArray(command)) {
301
- const parts = command.map((part) => JSON.stringify(part)).join(", ");
302
- return `[${parts}]`;
260
+ const value = stripInlineComment(matchLine[1]);
261
+ if (value.startsWith("[") && !hasUnquotedClosingBracket(value)) {
262
+ inMultilineArray = true;
263
+ }
303
264
  }
304
- return JSON.stringify(command);
265
+
266
+ return kept;
305
267
  }
306
268
 
307
- function buildStatusLineSection(desired: DesiredStatusLineConfig): string {
308
- const commandText = formatCommandValue(desired.command);
309
- return (
310
- `[tui.status_line]\n` +
311
- `command = ${commandText}\n` +
312
- `show_hints = ${desired.showHints ? "true" : "false"}\n` +
313
- `update_interval_ms = ${desired.updateIntervalMs}\n` +
314
- `timeout_ms = ${desired.timeoutMs}\n`
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: StatusLineSection | null,
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 section = parseStatusLineSection(raw);
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 (section && !force) {
365
- console.log(`codenv: existing Codex status_line config in ${configPath}:`);
366
- console.log(section.sectionText);
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(raw, section, buildStatusLineSection(desired));
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
  }
@@ -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
- const profileKey = firstNonEmpty(
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
- args.syncUsage
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
- args.syncUsage
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(config, configPath, args.syncUsage);
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
@@ -19,10 +19,7 @@ export interface Profile {
19
19
  }
20
20
 
21
21
  export interface CodexStatuslineConfig {
22
- command?: string | string[];
23
- showHints?: boolean;
24
- updateIntervalMs?: number;
25
- timeoutMs?: number;
22
+ items?: string[];
26
23
  configPath?: string;
27
24
  }
28
25