@getrouter/getrouter-cli 0.1.13 → 0.1.14

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.
@@ -39,23 +39,105 @@ const PROVIDER_KEYS = [
39
39
  "requires_openai_auth",
40
40
  ] as const;
41
41
 
42
- const rootValues = (input: CodexConfigInput) => ({
43
- model: `"${input.model}"`,
44
- model_reasoning_effort: `"${input.reasoning}"`,
45
- model_provider: `"${CODEX_PROVIDER}"`,
46
- });
47
-
48
- const providerValues = () => ({
49
- name: `"${CODEX_PROVIDER}"`,
50
- base_url: `"${CODEX_BASE_URL}"`,
51
- wire_api: `"responses"`,
52
- requires_openai_auth: "true",
53
- });
54
-
55
- const matchHeader = (line: string) => line.match(/^\s*\[([^\]]+)\]\s*$/);
56
- const matchKey = (line: string) => line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
57
-
58
- const parseTomlRhsValue = (rhs: string) => {
42
+ function splitLines(content: string): string[] {
43
+ if (content.length === 0) return [];
44
+ return content.split(/\r?\n/);
45
+ }
46
+
47
+ function isLegacyTomlRootMarker(key: string): boolean {
48
+ return (LEGACY_TOML_ROOT_MARKERS as readonly string[]).includes(key);
49
+ }
50
+
51
+ function rootValues(
52
+ input: CodexConfigInput,
53
+ ): Record<(typeof ROOT_KEYS)[number], string> {
54
+ return {
55
+ model: `"${input.model}"`,
56
+ model_reasoning_effort: `"${input.reasoning}"`,
57
+ model_provider: `"${CODEX_PROVIDER}"`,
58
+ };
59
+ }
60
+
61
+ function providerValues(): Record<(typeof PROVIDER_KEYS)[number], string> {
62
+ return {
63
+ name: `"${CODEX_PROVIDER}"`,
64
+ base_url: `"${CODEX_BASE_URL}"`,
65
+ wire_api: `"responses"`,
66
+ requires_openai_auth: "true",
67
+ };
68
+ }
69
+
70
+ const HEADER_RE = /^\s*\[([^\]]+)\]\s*$/;
71
+ const KEY_RE = /^\s*([A-Za-z0-9_.-]+)\s*=/;
72
+
73
+ function matchHeader(line: string): RegExpMatchArray | null {
74
+ return line.match(HEADER_RE);
75
+ }
76
+
77
+ function matchKey(line: string): RegExpMatchArray | null {
78
+ return line.match(KEY_RE);
79
+ }
80
+
81
+ function readKeyFromLine(line: string): string | undefined {
82
+ const match = matchKey(line);
83
+ return match?.[1];
84
+ }
85
+
86
+ function readStringValue(
87
+ data: Record<string, unknown>,
88
+ key: string,
89
+ ): string | undefined {
90
+ const value = data[key];
91
+ return typeof value === "string" ? value : undefined;
92
+ }
93
+
94
+ function findSectionEnd(lines: string[], startIndex: number): number {
95
+ for (let i = startIndex; i < lines.length; i += 1) {
96
+ const line = lines[i];
97
+ if (line !== undefined && matchHeader(line)) {
98
+ return i;
99
+ }
100
+ }
101
+ return lines.length;
102
+ }
103
+
104
+ function upsertKeyLines<K extends string>(
105
+ lines: string[],
106
+ startIndex: number,
107
+ endIndex: number,
108
+ keys: readonly K[],
109
+ valueMap: Record<K, string>,
110
+ ): Set<K> {
111
+ const found = new Set<K>();
112
+
113
+ for (let i = startIndex; i < endIndex; i += 1) {
114
+ const line = lines[i];
115
+ if (line === undefined) continue;
116
+
117
+ const keyMatch = matchKey(line);
118
+ if (!keyMatch) continue;
119
+
120
+ const key = keyMatch[1] as K;
121
+ if (!keys.includes(key)) continue;
122
+
123
+ lines[i] = `${key} = ${valueMap[key]}`;
124
+ found.add(key);
125
+ }
126
+
127
+ return found;
128
+ }
129
+
130
+ function missingKeyLines<K extends string>(
131
+ keys: readonly K[],
132
+ found: ReadonlySet<K>,
133
+ valueMap: Record<K, string>,
134
+ ): string[] {
135
+ return keys
136
+ .filter((key) => !found.has(key))
137
+ .map((key) => `${key} = ${valueMap[key]}`);
138
+ }
139
+
140
+ function parseTomlRhsValue(rhs: string): string {
59
141
  const trimmed = rhs.trim();
60
142
  if (!trimmed) return "";
61
143
  const first = trimmed[0];
@@ -65,33 +147,31 @@ const parseTomlRhsValue = (rhs: string) => {
65
147
  }
66
148
  const hashIndex = trimmed.indexOf("#");
67
149
  return (hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex)).trim();
68
- };
150
+ }
69
151
 
70
- const readRootValue = (lines: string[], key: string) => {
152
+ function readRootValue(lines: string[], key: string): string | undefined {
71
153
  for (const line of lines) {
72
154
  if (matchHeader(line)) break;
73
- const keyMatch = matchKey(line);
74
- if (keyMatch?.[1] === key) {
75
- const parts = line.split("=");
76
- parts.shift();
77
- return parseTomlRhsValue(parts.join("="));
155
+
156
+ const lineKey = readKeyFromLine(line);
157
+ if (lineKey === key) {
158
+ const rhs = line.slice(line.indexOf("=") + 1);
159
+ return parseTomlRhsValue(rhs);
78
160
  }
79
161
  }
80
162
  return undefined;
81
- };
163
+ }
82
164
 
83
- export const readCodexTomlRootValues = (
84
- content: string,
85
- ): CodexTomlRootValues => {
86
- const lines = content.length ? content.split(/\r?\n/) : [];
165
+ export function readCodexTomlRootValues(content: string): CodexTomlRootValues {
166
+ const lines = splitLines(content);
87
167
  return {
88
168
  model: readRootValue(lines, "model"),
89
169
  reasoning: readRootValue(lines, "model_reasoning_effort"),
90
170
  provider: readRootValue(lines, "model_provider"),
91
171
  };
92
- };
172
+ }
93
173
 
94
- const normalizeTomlString = (value?: string) => {
174
+ function normalizeTomlString(value?: string): string {
95
175
  if (!value) return "";
96
176
  const trimmed = value.trim();
97
177
  if (
@@ -101,9 +181,9 @@ const normalizeTomlString = (value?: string) => {
101
181
  return trimmed.slice(1, -1).trim().toLowerCase();
102
182
  }
103
183
  return trimmed.replace(/['"]/g, "").trim().toLowerCase();
104
- };
184
+ }
105
185
 
106
- const stripLegacyRootMarkers = (lines: string[]) => {
186
+ function stripLegacyRootMarkers(lines: string[]): string[] {
107
187
  const updated: string[] = [];
108
188
  let inRoot = true;
109
189
 
@@ -112,60 +192,46 @@ const stripLegacyRootMarkers = (lines: string[]) => {
112
192
  inRoot = false;
113
193
  }
114
194
  if (inRoot) {
115
- const keyMatch = matchKey(line);
116
- const key = keyMatch?.[1];
117
- if (key && LEGACY_TOML_ROOT_MARKERS.includes(key as never)) continue;
195
+ const key = readKeyFromLine(line);
196
+ if (key !== undefined && isLegacyTomlRootMarker(key)) continue;
118
197
  }
119
198
  updated.push(line);
120
199
  }
121
200
 
122
201
  return updated;
123
- };
202
+ }
124
203
 
125
- export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
126
- const lines = content.length ? content.split(/\r?\n/) : [];
127
- const updated = [...stripLegacyRootMarkers(lines)];
204
+ export function mergeCodexToml(
205
+ content: string,
206
+ input: CodexConfigInput,
207
+ ): string {
208
+ const lines = splitLines(content);
209
+ const updated = stripLegacyRootMarkers(lines);
128
210
  const rootValueMap = rootValues(input);
129
211
  const providerValueMap = providerValues();
130
212
 
131
- let currentSection: string | null = null;
132
- let firstHeaderIndex: number | null = null;
133
- const rootFound = new Set<string>();
134
-
135
- // Update root keys that appear before any section headers.
136
- for (let i = 0; i < updated.length; i += 1) {
137
- const headerMatch = matchHeader(updated[i] ?? "");
138
- if (headerMatch) {
139
- currentSection = headerMatch[1]?.trim() ?? null;
140
- if (firstHeaderIndex === null) {
141
- firstHeaderIndex = i;
142
- }
143
- continue;
144
- }
145
- if (currentSection !== null) {
146
- continue;
147
- }
148
- const keyMatch = matchKey(updated[i] ?? "");
149
- if (!keyMatch) continue;
150
- const key = keyMatch[1] as keyof typeof rootValueMap;
151
- if (ROOT_KEYS.includes(key)) {
152
- updated[i] = `${key} = ${rootValueMap[key]}`;
153
- rootFound.add(key);
154
- }
155
- }
156
-
157
- // Insert missing root keys before the first section header (or at EOF).
158
- const insertIndex = firstHeaderIndex ?? updated.length;
159
- const missingRoot = ROOT_KEYS.filter((key) => !rootFound.has(key)).map(
160
- (key) => `${key} = ${rootValueMap[key]}`,
213
+ const firstHeaderIndex = updated.findIndex(
214
+ (line) => matchHeader(line) !== null,
215
+ );
216
+ const rootEnd = firstHeaderIndex === -1 ? updated.length : firstHeaderIndex;
217
+
218
+ const rootFound = upsertKeyLines(
219
+ updated,
220
+ 0,
221
+ rootEnd,
222
+ ROOT_KEYS,
223
+ rootValueMap,
161
224
  );
225
+
226
+ const missingRoot = missingKeyLines(ROOT_KEYS, rootFound, rootValueMap);
162
227
  if (missingRoot.length > 0) {
228
+ const insertIndex = rootEnd;
163
229
  const needsBlank =
164
230
  insertIndex < updated.length && updated[insertIndex]?.trim() !== "";
231
+
165
232
  updated.splice(insertIndex, 0, ...missingRoot, ...(needsBlank ? [""] : []));
166
233
  }
167
234
 
168
- // Ensure the provider section exists and keep its keys in sync.
169
235
  const providerHeader = `[${PROVIDER_SECTION}]`;
170
236
  const providerHeaderIndex = updated.findIndex(
171
237
  (line) => line.trim() === providerHeader,
@@ -174,47 +240,40 @@ export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
174
240
  if (updated.length > 0 && updated[updated.length - 1]?.trim() !== "") {
175
241
  updated.push("");
176
242
  }
177
- updated.push(providerHeader);
178
- for (const key of PROVIDER_KEYS) {
179
- updated.push(`${key} = ${providerValueMap[key]}`);
180
- }
243
+ updated.push(
244
+ providerHeader,
245
+ ...PROVIDER_KEYS.map((key) => `${key} = ${providerValueMap[key]}`),
246
+ );
181
247
  return updated.join("\n");
182
248
  }
183
249
 
184
- // Find the provider section bounds for in-place updates.
185
- let providerEnd = updated.length;
186
- for (let i = providerHeaderIndex + 1; i < updated.length; i += 1) {
187
- if (matchHeader(updated[i] ?? "")) {
188
- providerEnd = i;
189
- break;
190
- }
191
- }
250
+ const providerStart = providerHeaderIndex + 1;
251
+ const providerEnd = findSectionEnd(updated, providerStart);
192
252
 
193
- const providerFound = new Set<string>();
194
- for (let i = providerHeaderIndex + 1; i < providerEnd; i += 1) {
195
- const keyMatch = matchKey(updated[i] ?? "");
196
- if (!keyMatch) continue;
197
- const key = keyMatch[1] as keyof typeof providerValueMap;
198
- if (PROVIDER_KEYS.includes(key)) {
199
- updated[i] = `${key} = ${providerValueMap[key]}`;
200
- providerFound.add(key);
201
- }
202
- }
253
+ const providerFound = upsertKeyLines(
254
+ updated,
255
+ providerStart,
256
+ providerEnd,
257
+ PROVIDER_KEYS,
258
+ providerValueMap,
259
+ );
203
260
 
204
- const missingProvider = PROVIDER_KEYS.filter(
205
- (key) => !providerFound.has(key),
206
- ).map((key) => `${key} = ${providerValueMap[key]}`);
261
+ const missingProvider = missingKeyLines(
262
+ PROVIDER_KEYS,
263
+ providerFound,
264
+ providerValueMap,
265
+ );
207
266
  if (missingProvider.length > 0) {
208
267
  updated.splice(providerEnd, 0, ...missingProvider);
209
268
  }
210
269
 
211
270
  return updated.join("\n");
212
- };
271
+ }
213
272
 
214
- export const mergeAuthJson = (
273
+ export function mergeAuthJson(
215
274
  data: Record<string, unknown>,
216
275
  apiKey: string,
217
- ): Record<string, unknown> => {
276
+ ): Record<string, unknown> {
218
277
  const next: Record<string, unknown> = { ...data };
219
278
  for (const key of LEGACY_AUTH_MARKERS) {
220
279
  if (key in next) {
@@ -223,16 +282,16 @@ export const mergeAuthJson = (
223
282
  }
224
283
  next.OPENAI_API_KEY = apiKey;
225
284
  return next;
226
- };
285
+ }
227
286
 
228
- const stripGetrouterProviderSection = (lines: string[]) => {
287
+ function stripGetrouterProviderSection(lines: string[]): string[] {
229
288
  const updated: string[] = [];
230
289
  let skipSection = false;
231
290
 
232
291
  for (const line of lines) {
233
292
  const headerMatch = matchHeader(line);
234
293
  if (headerMatch) {
235
- const section = headerMatch[1]?.trim() ?? "";
294
+ const section = headerMatch[1]?.trim();
236
295
  if (section === PROVIDER_SECTION) {
237
296
  skipSection = true;
238
297
  continue;
@@ -245,21 +304,21 @@ const stripGetrouterProviderSection = (lines: string[]) => {
245
304
  }
246
305
 
247
306
  return updated;
248
- };
307
+ }
249
308
 
250
- const stripLegacyMarkersFromRoot = (rootLines: string[]) =>
251
- rootLines.filter((line) => {
252
- const keyMatch = matchKey(line);
253
- const key = keyMatch?.[1];
254
- return !(key && LEGACY_TOML_ROOT_MARKERS.includes(key as never));
309
+ function stripLegacyMarkersFromRoot(rootLines: string[]): string[] {
310
+ return rootLines.filter((line) => {
311
+ const key = readKeyFromLine(line);
312
+ return !(key !== undefined && isLegacyTomlRootMarker(key));
255
313
  });
314
+ }
256
315
 
257
- const setOrDeleteRootKey = (
316
+ function setOrDeleteRootKey(
258
317
  rootLines: string[],
259
318
  key: string,
260
319
  value: string | undefined,
261
- ) => {
262
- const idx = rootLines.findIndex((line) => matchKey(line)?.[1] === key);
320
+ ): void {
321
+ const idx = rootLines.findIndex((line) => readKeyFromLine(line) === key);
263
322
  if (value === undefined) {
264
323
  if (idx !== -1) {
265
324
  rootLines.splice(idx, 1);
@@ -271,28 +330,28 @@ const setOrDeleteRootKey = (
271
330
  } else {
272
331
  rootLines.push(`${key} = ${value}`);
273
332
  }
274
- };
333
+ }
275
334
 
276
- const deleteRootKey = (rootLines: string[], key: string) => {
335
+ function deleteRootKey(rootLines: string[], key: string): void {
277
336
  setOrDeleteRootKey(rootLines, key, undefined);
278
- };
337
+ }
279
338
 
280
- const hasLegacyRootMarkers = (lines: string[]) =>
281
- lines.some((line) => {
282
- const keyMatch = matchKey(line);
283
- const key = keyMatch?.[1];
284
- return !!(key && LEGACY_TOML_ROOT_MARKERS.includes(key as never));
339
+ function hasLegacyRootMarkers(lines: string[]): boolean {
340
+ return lines.some((line) => {
341
+ const key = readKeyFromLine(line);
342
+ return key !== undefined && isLegacyTomlRootMarker(key);
285
343
  });
344
+ }
286
345
 
287
- export const removeCodexConfig = (
346
+ export function removeCodexConfig(
288
347
  content: string,
289
348
  options?: {
290
349
  restoreRoot?: CodexTomlRootValues;
291
350
  allowRootRemoval?: boolean;
292
351
  },
293
- ) => {
352
+ ): { content: string; changed: boolean } {
294
353
  const { restoreRoot, allowRootRemoval = true } = options ?? {};
295
- const lines = content.length ? content.split(/\r?\n/) : [];
354
+ const lines = splitLines(content);
296
355
  const providerIsGetrouter =
297
356
  normalizeTomlString(readRootValue(lines, "model_provider")) ===
298
357
  CODEX_PROVIDER;
@@ -332,27 +391,27 @@ export const removeCodexConfig = (
332
391
 
333
392
  const nextContent = recombined.join("\n");
334
393
  return { content: nextContent, changed: nextContent !== content };
335
- };
394
+ }
336
395
 
337
- export const removeAuthJson = (
396
+ export function removeAuthJson(
338
397
  data: Record<string, unknown>,
339
398
  options?: {
340
399
  installed?: string;
341
400
  restore?: string;
342
401
  },
343
- ) => {
402
+ ): { data: Record<string, unknown>; changed: boolean } {
344
403
  const { installed, restore } = options ?? {};
345
404
  const next: Record<string, unknown> = { ...data };
346
405
  let changed = false;
347
406
 
348
- const legacyInstalled =
349
- typeof next._getrouter_codex_installed_openai_api_key === "string"
350
- ? (next._getrouter_codex_installed_openai_api_key as string)
351
- : undefined;
352
- const legacyRestore =
353
- typeof next._getrouter_codex_backup_openai_api_key === "string"
354
- ? (next._getrouter_codex_backup_openai_api_key as string)
355
- : undefined;
407
+ const legacyInstalled = readStringValue(
408
+ next,
409
+ "_getrouter_codex_installed_openai_api_key",
410
+ );
411
+ const legacyRestore = readStringValue(
412
+ next,
413
+ "_getrouter_codex_backup_openai_api_key",
414
+ );
356
415
 
357
416
  const effectiveInstalled = installed ?? legacyInstalled;
358
417
  const effectiveRestore = restore ?? legacyRestore;
@@ -364,14 +423,8 @@ export const removeAuthJson = (
364
423
  }
365
424
  }
366
425
 
367
- const current =
368
- typeof next.OPENAI_API_KEY === "string"
369
- ? (next.OPENAI_API_KEY as string)
370
- : undefined;
371
- const restoreValue =
372
- typeof effectiveRestore === "string" && effectiveRestore.trim().length > 0
373
- ? effectiveRestore
374
- : undefined;
426
+ const current = readStringValue(next, "OPENAI_API_KEY");
427
+ const restoreValue = effectiveRestore?.trim() ? effectiveRestore : undefined;
375
428
 
376
429
  if (effectiveInstalled && current && current === effectiveInstalled) {
377
430
  if (restoreValue) {
@@ -384,4 +437,4 @@ export const removeAuthJson = (
384
437
  }
385
438
 
386
439
  return { data: next, changed };
387
- };
440
+ }
@@ -13,7 +13,7 @@ export type EnvShell = "sh" | "ps1";
13
13
 
14
14
  export type RcShell = "zsh" | "bash" | "fish" | "pwsh";
15
15
 
16
- const quoteEnvValue = (shell: EnvShell, value: string) => {
16
+ function quoteEnvValue(shell: EnvShell, value: string): string {
17
17
  if (shell === "ps1") {
18
18
  // PowerShell: single quotes are literal; escape by doubling.
19
19
  return `'${value.replaceAll("'", "''")}'`;
@@ -21,35 +21,37 @@ const quoteEnvValue = (shell: EnvShell, value: string) => {
21
21
 
22
22
  // POSIX shell: use single quotes; escape embedded single quotes with: '\''.
23
23
  return `'${value.replaceAll("'", "'\\''")}'`;
24
- };
24
+ }
25
25
 
26
- const renderLine = (shell: EnvShell, key: string, value: string) => {
26
+ function renderLine(shell: EnvShell, key: string, value: string): string {
27
27
  if (shell === "ps1") {
28
28
  return `$env:${key}=${quoteEnvValue(shell, value)}`;
29
29
  }
30
30
  return `export ${key}=${quoteEnvValue(shell, value)}`;
31
- };
31
+ }
32
+
33
+ export function renderEnv(shell: EnvShell, vars: EnvVars): string {
34
+ const entries: Array<[keyof EnvVars, string]> = [
35
+ ["openaiBaseUrl", "OPENAI_BASE_URL"],
36
+ ["openaiApiKey", "OPENAI_API_KEY"],
37
+ ["anthropicBaseUrl", "ANTHROPIC_BASE_URL"],
38
+ ["anthropicApiKey", "ANTHROPIC_API_KEY"],
39
+ ];
32
40
 
33
- export const renderEnv = (shell: EnvShell, vars: EnvVars) => {
34
41
  const lines: string[] = [];
35
- if (vars.openaiBaseUrl) {
36
- lines.push(renderLine(shell, "OPENAI_BASE_URL", vars.openaiBaseUrl));
37
- }
38
- if (vars.openaiApiKey) {
39
- lines.push(renderLine(shell, "OPENAI_API_KEY", vars.openaiApiKey));
40
- }
41
- if (vars.anthropicBaseUrl) {
42
- lines.push(renderLine(shell, "ANTHROPIC_BASE_URL", vars.anthropicBaseUrl));
43
- }
44
- if (vars.anthropicApiKey) {
45
- lines.push(renderLine(shell, "ANTHROPIC_API_KEY", vars.anthropicApiKey));
42
+ for (const [varKey, envKey] of entries) {
43
+ const value = vars[varKey];
44
+ if (value) {
45
+ lines.push(renderLine(shell, envKey, value));
46
+ }
46
47
  }
48
+
47
49
  lines.push("");
48
50
  return lines.join("\n");
49
- };
51
+ }
50
52
 
51
53
  // Wrap getrouter to source env after successful codex/claude runs.
52
- export const renderHook = (shell: RcShell) => {
54
+ export function renderHook(shell: RcShell): string {
53
55
  if (shell === "pwsh") {
54
56
  return [
55
57
  "function getrouter {",
@@ -121,27 +123,31 @@ export const renderHook = (shell: RcShell) => {
121
123
  "}",
122
124
  "",
123
125
  ].join("\n");
124
- };
126
+ }
125
127
 
126
- export const getEnvFilePath = (shell: EnvShell, configDir: string) =>
127
- path.join(configDir, shell === "ps1" ? "env.ps1" : "env.sh");
128
+ export function getEnvFilePath(shell: EnvShell, configDir: string): string {
129
+ return path.join(configDir, shell === "ps1" ? "env.ps1" : "env.sh");
130
+ }
128
131
 
129
- export const getHookFilePath = (shell: RcShell, configDir: string) => {
132
+ export function getHookFilePath(shell: RcShell, configDir: string): string {
130
133
  if (shell === "pwsh") return path.join(configDir, "hook.ps1");
131
134
  if (shell === "fish") return path.join(configDir, "hook.fish");
132
135
  return path.join(configDir, "hook.sh");
133
- };
136
+ }
134
137
 
135
- export const writeEnvFile = (filePath: string, content: string) => {
138
+ export function writeEnvFile(filePath: string, content: string): void {
136
139
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
137
140
  fs.writeFileSync(filePath, content, "utf8");
138
141
  if (process.platform !== "win32") {
139
142
  // Limit env file readability since it can contain API keys.
140
143
  fs.chmodSync(filePath, 0o600);
141
144
  }
142
- };
145
+ }
143
146
 
144
- export const resolveShellRcPath = (shell: RcShell, homeDir: string) => {
147
+ export function resolveShellRcPath(
148
+ shell: RcShell,
149
+ homeDir: string,
150
+ ): string | null {
145
151
  if (shell === "zsh") return path.join(homeDir, ".zshrc");
146
152
  if (shell === "bash") return path.join(homeDir, ".bashrc");
147
153
  if (shell === "fish") return path.join(homeDir, ".config/fish/config.fish");
@@ -158,12 +164,13 @@ export const resolveShellRcPath = (shell: RcShell, homeDir: string) => {
158
164
  );
159
165
  }
160
166
  return null;
161
- };
167
+ }
162
168
 
163
- export const resolveEnvShell = (shell: RcShell): EnvShell =>
164
- shell === "pwsh" ? "ps1" : "sh";
169
+ export function resolveEnvShell(shell: RcShell): EnvShell {
170
+ return shell === "pwsh" ? "ps1" : "sh";
171
+ }
165
172
 
166
- export const detectShell = (): RcShell => {
173
+ export function detectShell(): RcShell {
167
174
  const shellPath = process.env.SHELL;
168
175
  if (shellPath) {
169
176
  const name = shellPath.split("/").pop()?.toLowerCase();
@@ -178,9 +185,9 @@ export const detectShell = (): RcShell => {
178
185
  }
179
186
  if (process.platform === "win32") return "pwsh";
180
187
  return "bash";
181
- };
188
+ }
182
189
 
183
- export const applyEnvVars = (vars: EnvVars) => {
190
+ export function applyEnvVars(vars: EnvVars): void {
184
191
  if (vars.openaiBaseUrl) process.env.OPENAI_BASE_URL = vars.openaiBaseUrl;
185
192
  if (vars.openaiApiKey) process.env.OPENAI_API_KEY = vars.openaiApiKey;
186
193
  if (vars.anthropicBaseUrl) {
@@ -189,16 +196,17 @@ export const applyEnvVars = (vars: EnvVars) => {
189
196
  if (vars.anthropicApiKey) {
190
197
  process.env.ANTHROPIC_API_KEY = vars.anthropicApiKey;
191
198
  }
192
- };
199
+ }
193
200
 
194
- export const formatSourceLine = (shell: EnvShell, envPath: string) =>
195
- shell === "ps1" ? `. ${envPath}` : `source ${envPath}`;
201
+ export function formatSourceLine(shell: EnvShell, envPath: string): string {
202
+ return shell === "ps1" ? `. ${envPath}` : `source ${envPath}`;
203
+ }
196
204
 
197
- export const trySourceEnv = (
205
+ export function trySourceEnv(
198
206
  shell: RcShell,
199
207
  envShell: EnvShell,
200
208
  envPath: string,
201
- ) => {
209
+ ): void {
202
210
  try {
203
211
  if (envShell === "ps1") {
204
212
  execSync(`pwsh -NoProfile -Command ". '${envPath}'"`, {
@@ -206,16 +214,15 @@ export const trySourceEnv = (
206
214
  });
207
215
  return;
208
216
  }
209
- const command = shell === "fish" ? "source" : "source";
210
- execSync(`${shell} -c "${command} '${envPath}'"`, {
217
+ execSync(`${shell} -c "source '${envPath}'"`, {
211
218
  stdio: "ignore",
212
219
  });
213
220
  } catch {
214
221
  // Best-effort: ignore failures and let the caller print instructions.
215
222
  }
216
- };
223
+ }
217
224
 
218
- export const appendRcIfMissing = (rcPath: string, line: string) => {
225
+ export function appendRcIfMissing(rcPath: string, line: string): boolean {
219
226
  let content = "";
220
227
  if (fs.existsSync(rcPath)) {
221
228
  content = fs.readFileSync(rcPath, "utf8");
@@ -225,4 +232,4 @@ export const appendRcIfMissing = (rcPath: string, line: string) => {
225
232
  fs.mkdirSync(path.dirname(rcPath), { recursive: true });
226
233
  fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
227
234
  return true;
228
- };
235
+ }
@@ -29,10 +29,10 @@ const toNumber = (value: number | string | undefined) => {
29
29
  return 0;
30
30
  };
31
31
 
32
- export const aggregateUsages = (
32
+ export function aggregateUsages(
33
33
  usages: RawUsage[],
34
34
  maxDays = 7,
35
- ): AggregatedUsage[] => {
35
+ ): AggregatedUsage[] {
36
36
  const totals = new Map<string, AggregatedUsage>();
37
37
 
38
38
  for (const usage of usages) {
@@ -66,4 +66,4 @@ export const aggregateUsages = (
66
66
  return Array.from(totals.values())
67
67
  .sort((a, b) => b.day.localeCompare(a.day))
68
68
  .slice(0, maxDays);
69
- };
69
+ }