@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.
- package/dist/bin.mjs +522 -441
- package/package.json +1 -1
- package/src/cli.ts +4 -2
- package/src/cmd/auth.ts +2 -2
- package/src/cmd/claude.ts +2 -2
- package/src/cmd/codex.ts +24 -16
- package/src/cmd/env.ts +15 -11
- package/src/cmd/index.ts +2 -2
- package/src/cmd/keys.ts +59 -35
- package/src/cmd/models.ts +24 -26
- package/src/cmd/status.ts +113 -62
- package/src/cmd/usages.ts +4 -4
- package/src/core/api/client.ts +12 -14
- package/src/core/api/pagination.ts +3 -3
- package/src/core/api/providerModels.ts +15 -14
- package/src/core/auth/device.ts +38 -23
- package/src/core/auth/index.ts +19 -11
- package/src/core/auth/refresh.ts +22 -17
- package/src/core/config/fs.ts +6 -6
- package/src/core/config/index.ts +16 -11
- package/src/core/config/paths.ts +12 -4
- package/src/core/config/redact.ts +4 -4
- package/src/core/config/types.ts +14 -10
- package/src/core/http/errors.ts +18 -10
- package/src/core/http/request.ts +29 -34
- package/src/core/http/retry.ts +37 -24
- package/src/core/http/url.ts +11 -6
- package/src/core/interactive/clipboard.ts +10 -9
- package/src/core/interactive/keys.ts +22 -26
- package/src/core/output/table.ts +5 -5
- package/src/core/output/usages.ts +34 -33
- package/src/core/setup/codex.ts +195 -142
- package/src/core/setup/env.ts +49 -42
- package/src/core/usages/aggregate.ts +3 -3
package/src/core/setup/codex.ts
CHANGED
|
@@ -39,23 +39,105 @@ const PROVIDER_KEYS = [
|
|
|
39
39
|
"requires_openai_auth",
|
|
40
40
|
] as const;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
152
|
+
function readRootValue(lines: string[], key: string): string | undefined {
|
|
71
153
|
for (const line of lines) {
|
|
72
154
|
if (matchHeader(line)) break;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return parseTomlRhsValue(
|
|
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
|
|
84
|
-
content
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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 =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 =
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
rootLines.filter((line) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
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
|
-
|
|
316
|
+
function setOrDeleteRootKey(
|
|
258
317
|
rootLines: string[],
|
|
259
318
|
key: string,
|
|
260
319
|
value: string | undefined,
|
|
261
|
-
)
|
|
262
|
-
const idx = rootLines.findIndex((line) =>
|
|
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
|
-
|
|
335
|
+
function deleteRootKey(rootLines: string[], key: string): void {
|
|
277
336
|
setOrDeleteRootKey(rootLines, key, undefined);
|
|
278
|
-
}
|
|
337
|
+
}
|
|
279
338
|
|
|
280
|
-
|
|
281
|
-
lines.some((line) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const legacyRestore =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/setup/env.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type EnvShell = "sh" | "ps1";
|
|
|
13
13
|
|
|
14
14
|
export type RcShell = "zsh" | "bash" | "fish" | "pwsh";
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
164
|
-
shell === "pwsh" ? "ps1" : "sh";
|
|
169
|
+
export function resolveEnvShell(shell: RcShell): EnvShell {
|
|
170
|
+
return shell === "pwsh" ? "ps1" : "sh";
|
|
171
|
+
}
|
|
165
172
|
|
|
166
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|