@getrouter/getrouter-cli 0.1.2 → 0.1.3

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import fs from "node:fs";
4
- import os from "node:os";
5
4
  import path from "node:path";
5
+ import os from "node:os";
6
6
  import { execSync, spawn } from "node:child_process";
7
7
  import { randomInt } from "node:crypto";
8
8
  import prompts from "prompts";
@@ -204,16 +204,47 @@ function createUsageServiceClient(handler) {
204
204
 
205
205
  //#endregion
206
206
  //#region src/core/config/fs.ts
207
+ const getCorruptBackupPath = (filePath) => {
208
+ const dir = path.dirname(filePath);
209
+ const ext = path.extname(filePath);
210
+ const base = path.basename(filePath, ext);
211
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
212
+ const rand = Math.random().toString(16).slice(2, 8);
213
+ return path.join(dir, `${base}.corrupt-${stamp}-${rand}${ext}`);
214
+ };
207
215
  const readJsonFile = (filePath) => {
208
216
  if (!fs.existsSync(filePath)) return null;
209
- const raw = fs.readFileSync(filePath, "utf8");
210
- return JSON.parse(raw);
217
+ let raw;
218
+ try {
219
+ raw = fs.readFileSync(filePath, "utf8");
220
+ } catch {
221
+ console.warn(`⚠️ Unable to read ${filePath}. Continuing with defaults.`);
222
+ return null;
223
+ }
224
+ try {
225
+ return JSON.parse(raw);
226
+ } catch {
227
+ const backupPath = getCorruptBackupPath(filePath);
228
+ try {
229
+ fs.renameSync(filePath, backupPath);
230
+ console.warn(`⚠️ Invalid JSON in ${filePath}. Moved to ${backupPath} and continuing with defaults.`);
231
+ } catch {
232
+ console.warn(`⚠️ Invalid JSON in ${filePath}. Please fix or delete this file, then try again.`);
233
+ }
234
+ return null;
235
+ }
211
236
  };
212
237
  const writeJsonFile = (filePath, value) => {
213
238
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
214
239
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
215
240
  };
216
241
 
242
+ //#endregion
243
+ //#region src/core/config/paths.ts
244
+ const resolveConfigDir = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
245
+ const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
246
+ const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
247
+
217
248
  //#endregion
218
249
  //#region src/core/config/types.ts
219
250
  const defaultConfig = () => ({
@@ -229,9 +260,6 @@ const defaultAuthState = () => ({
229
260
 
230
261
  //#endregion
231
262
  //#region src/core/config/index.ts
232
- const resolveConfigDir$1 = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
233
- const getConfigPath = () => path.join(resolveConfigDir$1(), "config.json");
234
- const getAuthPath = () => path.join(resolveConfigDir$1(), "auth.json");
235
263
  const readConfig = () => ({
236
264
  ...defaultConfig(),
237
265
  ...readJsonFile(getConfigPath()) ?? {}
@@ -649,9 +677,13 @@ const confirmDelete = async (consumer) => {
649
677
 
650
678
  //#endregion
651
679
  //#region src/core/setup/env.ts
680
+ const quoteEnvValue = (shell, value) => {
681
+ if (shell === "ps1") return `'${value.replaceAll("'", "''")}'`;
682
+ return `'${value.replaceAll("'", "'\\''")}'`;
683
+ };
652
684
  const renderLine = (shell, key, value) => {
653
- if (shell === "ps1") return `$env:${key}="${value}"`;
654
- return `export ${key}=${value}`;
685
+ if (shell === "ps1") return `$env:${key}=${quoteEnvValue(shell, value)}`;
686
+ return `export ${key}=${quoteEnvValue(shell, value)}`;
655
687
  };
656
688
  const renderEnv = (shell, vars) => {
657
689
  const lines = [];
@@ -787,7 +819,6 @@ const appendRcIfMissing = (rcPath, line) => {
787
819
  fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
788
820
  return true;
789
821
  };
790
- const resolveConfigDir = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
791
822
 
792
823
  //#endregion
793
824
  //#region src/cmd/env.ts
@@ -847,6 +878,29 @@ const registerClaudeCommand = (program) => {
847
878
  });
848
879
  };
849
880
 
881
+ //#endregion
882
+ //#region src/core/api/providerModels.ts
883
+ const asTrimmedString = (value) => {
884
+ if (typeof value !== "string") return null;
885
+ const trimmed = value.trim();
886
+ return trimmed.length > 0 ? trimmed : null;
887
+ };
888
+ const buildProviderModelsPath = (tag) => {
889
+ const query = new URLSearchParams();
890
+ if (tag) query.set("tag", tag);
891
+ const qs = query.toString();
892
+ return `v1/dashboard/providers/models${qs ? `?${qs}` : ""}`;
893
+ };
894
+ const listProviderModels = async ({ tag, fetchImpl }) => {
895
+ const raw = (await requestJson({
896
+ path: buildProviderModelsPath(tag),
897
+ method: "GET",
898
+ fetchImpl,
899
+ maxRetries: 0
900
+ }))?.models;
901
+ return (Array.isArray(raw) ? raw : []).map(asTrimmedString).filter(Boolean);
902
+ };
903
+
850
904
  //#endregion
851
905
  //#region src/core/interactive/codex.ts
852
906
  const MODEL_CHOICES = [
@@ -875,6 +929,20 @@ const MODEL_CHOICES = [
875
929
  keywords: ["gpt-5.2"]
876
930
  }
877
931
  ];
932
+ const getCodexModelChoices = async () => {
933
+ try {
934
+ const remoteChoices = (await listProviderModels({ tag: "codex" })).map((model) => ({
935
+ title: model,
936
+ value: model,
937
+ keywords: [model, "codex"]
938
+ }));
939
+ if (remoteChoices.length > 0) {
940
+ remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
941
+ return remoteChoices;
942
+ }
943
+ } catch {}
944
+ return MODEL_CHOICES;
945
+ };
878
946
  const REASONING_CHOICES = [
879
947
  {
880
948
  id: "low",
@@ -1019,19 +1087,21 @@ const ensureCodexDir = () => {
1019
1087
  const requireInteractive$1 = () => {
1020
1088
  if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
1021
1089
  };
1022
- const promptModel = async () => await fuzzySelect({
1023
- message: "Select Model and Effort\nAccess legacy models by running codex -m <model_name> or in your config.toml",
1024
- choices: MODEL_CHOICES
1025
- });
1090
+ const promptModel = async () => {
1091
+ return await fuzzySelect({
1092
+ message: "Select Model and Effort\nAccess legacy models by running getrouter codex -m <model_name> or in your config.toml",
1093
+ choices: await getCodexModelChoices()
1094
+ });
1095
+ };
1026
1096
  const promptReasoning = async (model) => await fuzzySelect({
1027
1097
  message: `Select Reasoning Level for ${model}`,
1028
1098
  choices: REASONING_FUZZY_CHOICES
1029
1099
  });
1030
1100
  const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
1031
1101
  const registerCodexCommand = (program) => {
1032
- program.command("codex").description("Configure Codex").action(async () => {
1102
+ program.command("codex").description("Configure Codex").option("-m, --model <model>", "Set codex model (skips model selection)").action(async (options) => {
1033
1103
  requireInteractive$1();
1034
- const model = await promptModel();
1104
+ const model = options.model && options.model.trim().length > 0 ? options.model.trim() : await promptModel();
1035
1105
  if (!model) return;
1036
1106
  const reasoningId = await promptReasoning(model);
1037
1107
  if (!reasoningId) return;
@@ -1070,6 +1140,27 @@ const registerCodexCommand = (program) => {
1070
1140
  });
1071
1141
  };
1072
1142
 
1143
+ //#endregion
1144
+ //#region src/core/config/redact.ts
1145
+ const SECRET_KEYS = new Set([
1146
+ "accessToken",
1147
+ "refreshToken",
1148
+ "apiKey"
1149
+ ]);
1150
+ const mask = (value) => {
1151
+ if (!value) return "";
1152
+ if (value.length <= 8) return "****";
1153
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
1154
+ };
1155
+ const redactSecrets = (obj) => {
1156
+ const out = { ...obj };
1157
+ for (const key of Object.keys(out)) {
1158
+ const value = out[key];
1159
+ if (SECRET_KEYS.has(key) && typeof value === "string") out[key] = mask(value);
1160
+ }
1161
+ return out;
1162
+ };
1163
+
1073
1164
  //#endregion
1074
1165
  //#region src/core/output/table.ts
1075
1166
  const truncate = (value, max) => {
@@ -1100,18 +1191,21 @@ const consumerHeaders = [
1100
1191
  "CREATED_AT",
1101
1192
  "API_KEY"
1102
1193
  ];
1103
- const consumerRow = (consumer) => [
1104
- String(consumer.name ?? ""),
1105
- String(consumer.enabled ?? ""),
1106
- String(consumer.lastAccess ?? ""),
1107
- String(consumer.createdAt ?? ""),
1108
- String(consumer.apiKey ?? "")
1109
- ];
1110
- const outputConsumerTable = (consumer) => {
1111
- console.log(renderTable(consumerHeaders, [consumerRow(consumer)], { maxColWidth: 64 }));
1112
- };
1113
- const outputConsumers = (consumers) => {
1114
- const rows = consumers.map(consumerRow);
1194
+ const consumerRow = (consumer, showApiKey) => {
1195
+ const { apiKey } = showApiKey ? consumer : redactSecrets(consumer);
1196
+ return [
1197
+ String(consumer.name ?? ""),
1198
+ String(consumer.enabled ?? ""),
1199
+ String(consumer.lastAccess ?? ""),
1200
+ String(consumer.createdAt ?? ""),
1201
+ String(apiKey ?? "")
1202
+ ];
1203
+ };
1204
+ const outputConsumerTable = (consumer, showApiKey) => {
1205
+ console.log(renderTable(consumerHeaders, [consumerRow(consumer, showApiKey)], { maxColWidth: 64 }));
1206
+ };
1207
+ const outputConsumers = (consumers, showApiKey) => {
1208
+ const rows = consumers.map((consumer) => consumerRow(consumer, showApiKey));
1115
1209
  console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
1116
1210
  };
1117
1211
  const requireInteractive = (message) => {
@@ -1131,11 +1225,11 @@ const updateConsumer = async (consumerService, consumer, name, enabled) => {
1131
1225
  updateMask
1132
1226
  });
1133
1227
  };
1134
- const listConsumers = async (consumerService) => {
1228
+ const listConsumers = async (consumerService, showApiKey) => {
1135
1229
  outputConsumers(await fetchAllPages((pageToken) => consumerService.ListConsumers({
1136
1230
  pageSize: void 0,
1137
1231
  pageToken
1138
- }), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0));
1232
+ }), (res) => res?.consumers ?? [], (res) => res?.nextPageToken || void 0), showApiKey);
1139
1233
  };
1140
1234
  const resolveConsumerForUpdate = async (consumerService, id) => {
1141
1235
  if (id) return consumerService.GetConsumer({ id });
@@ -1153,14 +1247,14 @@ const createConsumer = async (consumerService) => {
1153
1247
  const enabled = await promptKeyEnabled(true);
1154
1248
  let consumer = await consumerService.CreateConsumer({});
1155
1249
  consumer = await updateConsumer(consumerService, consumer, name, enabled);
1156
- outputConsumerTable(consumer);
1250
+ outputConsumerTable(consumer, true);
1157
1251
  console.log("Please store this API key securely.");
1158
1252
  };
1159
1253
  const updateConsumerById = async (consumerService, id) => {
1160
1254
  requireInteractiveForAction("update");
1161
1255
  const selected = await resolveConsumerForUpdate(consumerService, id);
1162
1256
  if (!selected?.id) return;
1163
- outputConsumerTable(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true)));
1257
+ outputConsumerTable(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true)), false);
1164
1258
  };
1165
1259
  const deleteConsumerById = async (consumerService, id) => {
1166
1260
  requireInteractiveForAction("delete");
@@ -1168,18 +1262,20 @@ const deleteConsumerById = async (consumerService, id) => {
1168
1262
  if (!selected?.id) return;
1169
1263
  if (!await confirmDelete(selected)) return;
1170
1264
  await consumerService.DeleteConsumer({ id: selected.id });
1171
- outputConsumerTable(selected);
1265
+ outputConsumerTable(selected, false);
1172
1266
  };
1173
1267
  const registerKeysCommands = (program) => {
1174
1268
  const keys = program.command("keys").description("Manage API keys");
1269
+ keys.option("--show", "Show full API keys");
1175
1270
  keys.allowExcessArguments(false);
1176
- keys.action(async () => {
1271
+ keys.action(async (options) => {
1177
1272
  const { consumerService } = createApiClients({});
1178
- await listConsumers(consumerService);
1273
+ await listConsumers(consumerService, Boolean(options.show));
1179
1274
  });
1180
- keys.command("list").description("List API keys").action(async () => {
1275
+ keys.command("list").description("List API keys").option("--show", "Show full API keys").action(async (options, command) => {
1181
1276
  const { consumerService } = createApiClients({});
1182
- await listConsumers(consumerService);
1277
+ const parentShow = Boolean(command.parent?.opts().show);
1278
+ await listConsumers(consumerService, Boolean(options.show) || parentShow);
1183
1279
  });
1184
1280
  keys.command("create").description("Create an API key").action(async () => {
1185
1281
  const { consumerService } = createApiClients({});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getrouter/getrouter-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "CLI for getrouter.dev",
6
6
  "bin": {
package/src/cmd/codex.ts CHANGED
@@ -5,7 +5,7 @@ import type { Command } from "commander";
5
5
  import prompts from "prompts";
6
6
  import { createApiClients } from "../core/api/client";
7
7
  import {
8
- MODEL_CHOICES,
8
+ getCodexModelChoices,
9
9
  mapReasoningValue,
10
10
  REASONING_CHOICES,
11
11
  REASONING_FUZZY_CHOICES,
@@ -42,12 +42,14 @@ const requireInteractive = () => {
42
42
  }
43
43
  };
44
44
 
45
- const promptModel = async () =>
46
- await fuzzySelect({
45
+ const promptModel = async () => {
46
+ const choices = await getCodexModelChoices();
47
+ return await fuzzySelect({
47
48
  message:
48
- "Select Model and Effort\nAccess legacy models by running codex -m <model_name> or in your config.toml",
49
- choices: MODEL_CHOICES,
49
+ "Select Model and Effort\nAccess legacy models by running getrouter codex -m <model_name> or in your config.toml",
50
+ choices,
50
51
  });
52
+ };
51
53
 
52
54
  const promptReasoning = async (model: string) =>
53
55
  await fuzzySelect({
@@ -58,13 +60,21 @@ const promptReasoning = async (model: string) =>
58
60
  const formatReasoningLabel = (id: string) =>
59
61
  REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
60
62
 
63
+ type CodexCommandOptions = {
64
+ model?: string;
65
+ };
66
+
61
67
  export const registerCodexCommand = (program: Command) => {
62
68
  program
63
69
  .command("codex")
64
70
  .description("Configure Codex")
65
- .action(async () => {
71
+ .option("-m, --model <model>", "Set codex model (skips model selection)")
72
+ .action(async (options: CodexCommandOptions) => {
66
73
  requireInteractive();
67
- const model = await promptModel();
74
+ const model =
75
+ options.model && options.model.trim().length > 0
76
+ ? options.model.trim()
77
+ : await promptModel();
68
78
  if (!model) return;
69
79
  const reasoningId = await promptReasoning(model);
70
80
  if (!reasoningId) return;
package/src/cmd/env.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import os from "node:os";
2
2
  import type { Command } from "commander";
3
3
  import { createApiClients } from "../core/api/client";
4
+ import { resolveConfigDir } from "../core/config/paths";
4
5
  import { selectConsumer } from "../core/interactive/keys";
5
6
  import {
6
7
  appendRcIfMissing,
@@ -12,7 +13,6 @@ import {
12
13
  getHookFilePath,
13
14
  renderEnv,
14
15
  renderHook,
15
- resolveConfigDir,
16
16
  resolveEnvShell,
17
17
  resolveShellRcPath,
18
18
  trySourceEnv,
package/src/cmd/keys.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
  import { createApiClients } from "../core/api/client";
3
3
  import { fetchAllPages } from "../core/api/pagination";
4
+ import { redactSecrets } from "../core/config/redact";
4
5
  import {
5
6
  confirmDelete,
6
7
  promptKeyEnabled,
@@ -23,22 +24,32 @@ const consumerHeaders = [
23
24
  "API_KEY",
24
25
  ];
25
26
 
26
- const consumerRow = (consumer: ConsumerLike) => [
27
- String(consumer.name ?? ""),
28
- String(consumer.enabled ?? ""),
29
- String(consumer.lastAccess ?? ""),
30
- String(consumer.createdAt ?? ""),
31
- String(consumer.apiKey ?? ""),
32
- ];
27
+ const consumerRow = (consumer: ConsumerLike, showApiKey: boolean) => {
28
+ const { apiKey } = showApiKey
29
+ ? consumer
30
+ : (redactSecrets(consumer as Record<string, unknown>) as ConsumerLike);
31
+ return [
32
+ String(consumer.name ?? ""),
33
+ String(consumer.enabled ?? ""),
34
+ String(consumer.lastAccess ?? ""),
35
+ String(consumer.createdAt ?? ""),
36
+ String(apiKey ?? ""),
37
+ ];
38
+ };
33
39
 
34
- const outputConsumerTable = (consumer: ConsumerLike) => {
40
+ const outputConsumerTable = (consumer: ConsumerLike, showApiKey: boolean) => {
35
41
  console.log(
36
- renderTable(consumerHeaders, [consumerRow(consumer)], { maxColWidth: 64 }),
42
+ renderTable(consumerHeaders, [consumerRow(consumer, showApiKey)], {
43
+ maxColWidth: 64,
44
+ }),
37
45
  );
38
46
  };
39
47
 
40
- const outputConsumers = (consumers: routercommonv1_Consumer[]) => {
41
- const rows = consumers.map(consumerRow);
48
+ const outputConsumers = (
49
+ consumers: routercommonv1_Consumer[],
50
+ showApiKey: boolean,
51
+ ) => {
52
+ const rows = consumers.map((consumer) => consumerRow(consumer, showApiKey));
42
53
  console.log(renderTable(consumerHeaders, rows, { maxColWidth: 64 }));
43
54
  };
44
55
 
@@ -81,6 +92,7 @@ const updateConsumer = async (
81
92
 
82
93
  const listConsumers = async (
83
94
  consumerService: Pick<ConsumerService, "ListConsumers">,
95
+ showApiKey: boolean,
84
96
  ) => {
85
97
  const consumers = await fetchAllPages(
86
98
  (pageToken) =>
@@ -91,7 +103,7 @@ const listConsumers = async (
91
103
  (res) => res?.consumers ?? [],
92
104
  (res) => res?.nextPageToken || undefined,
93
105
  );
94
- outputConsumers(consumers);
106
+ outputConsumers(consumers, showApiKey);
95
107
  };
96
108
 
97
109
  const resolveConsumerForUpdate = async (
@@ -124,7 +136,7 @@ const createConsumer = async (
124
136
  const enabled = await promptKeyEnabled(true);
125
137
  let consumer = await consumerService.CreateConsumer({});
126
138
  consumer = await updateConsumer(consumerService, consumer, name, enabled);
127
- outputConsumerTable(consumer);
139
+ outputConsumerTable(consumer, true);
128
140
  console.log("Please store this API key securely.");
129
141
  };
130
142
 
@@ -146,7 +158,7 @@ const updateConsumerById = async (
146
158
  name,
147
159
  enabled,
148
160
  );
149
- outputConsumerTable(consumer);
161
+ outputConsumerTable(consumer, false);
150
162
  };
151
163
 
152
164
  const deleteConsumerById = async (
@@ -162,24 +174,27 @@ const deleteConsumerById = async (
162
174
  const confirmed = await confirmDelete(selected);
163
175
  if (!confirmed) return;
164
176
  await consumerService.DeleteConsumer({ id: selected.id });
165
- outputConsumerTable(selected);
177
+ outputConsumerTable(selected, false);
166
178
  };
167
179
 
168
180
  export const registerKeysCommands = (program: Command) => {
169
181
  const keys = program.command("keys").description("Manage API keys");
182
+ keys.option("--show", "Show full API keys");
170
183
  keys.allowExcessArguments(false);
171
184
 
172
- keys.action(async () => {
185
+ keys.action(async (options: { show?: boolean }) => {
173
186
  const { consumerService } = createApiClients({});
174
- await listConsumers(consumerService);
187
+ await listConsumers(consumerService, Boolean(options.show));
175
188
  });
176
189
 
177
190
  keys
178
191
  .command("list")
179
192
  .description("List API keys")
180
- .action(async () => {
193
+ .option("--show", "Show full API keys")
194
+ .action(async (options: { show?: boolean }, command: Command) => {
181
195
  const { consumerService } = createApiClients({});
182
- await listConsumers(consumerService);
196
+ const parentShow = Boolean(command.parent?.opts().show);
197
+ await listConsumers(consumerService, Boolean(options.show) || parentShow);
183
198
  });
184
199
 
185
200
  keys
@@ -0,0 +1,32 @@
1
+ import { requestJson } from "../http/request";
2
+
3
+ const asTrimmedString = (value: unknown): string | null => {
4
+ if (typeof value !== "string") return null;
5
+ const trimmed = value.trim();
6
+ return trimmed.length > 0 ? trimmed : null;
7
+ };
8
+
9
+ const buildProviderModelsPath = (tag?: string) => {
10
+ const query = new URLSearchParams();
11
+ if (tag) query.set("tag", tag);
12
+ const qs = query.toString();
13
+ return `v1/dashboard/providers/models${qs ? `?${qs}` : ""}`;
14
+ };
15
+
16
+ export const listProviderModels = async ({
17
+ tag,
18
+ fetchImpl,
19
+ }: {
20
+ tag?: string;
21
+ fetchImpl?: typeof fetch;
22
+ }): Promise<string[]> => {
23
+ const res = await requestJson<{ models?: unknown }>({
24
+ path: buildProviderModelsPath(tag),
25
+ method: "GET",
26
+ fetchImpl,
27
+ maxRetries: 0,
28
+ });
29
+ const raw = res?.models;
30
+ const models = Array.isArray(raw) ? raw : [];
31
+ return models.map(asTrimmedString).filter(Boolean) as string[];
32
+ };
@@ -1,10 +1,41 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
+ const getCorruptBackupPath = (filePath: string) => {
5
+ const dir = path.dirname(filePath);
6
+ const ext = path.extname(filePath);
7
+ const base = path.basename(filePath, ext);
8
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
9
+ const rand = Math.random().toString(16).slice(2, 8);
10
+ return path.join(dir, `${base}.corrupt-${stamp}-${rand}${ext}`);
11
+ };
12
+
4
13
  export const readJsonFile = <T = unknown>(filePath: string): T | null => {
5
14
  if (!fs.existsSync(filePath)) return null;
6
- const raw = fs.readFileSync(filePath, "utf8");
7
- return JSON.parse(raw) as T;
15
+ let raw: string;
16
+ try {
17
+ raw = fs.readFileSync(filePath, "utf8");
18
+ } catch {
19
+ console.warn(`⚠️ Unable to read ${filePath}. Continuing with defaults.`);
20
+ return null;
21
+ }
22
+
23
+ try {
24
+ return JSON.parse(raw) as T;
25
+ } catch {
26
+ const backupPath = getCorruptBackupPath(filePath);
27
+ try {
28
+ fs.renameSync(filePath, backupPath);
29
+ console.warn(
30
+ `⚠️ Invalid JSON in ${filePath}. Moved to ${backupPath} and continuing with defaults.`,
31
+ );
32
+ } catch {
33
+ console.warn(
34
+ `⚠️ Invalid JSON in ${filePath}. Please fix or delete this file, then try again.`,
35
+ );
36
+ }
37
+ return null;
38
+ }
8
39
  };
9
40
 
10
41
  export const writeJsonFile = (filePath: string, value: unknown) => {
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
2
  import { readJsonFile, writeJsonFile } from "./fs";
3
+ import { getAuthPath, getConfigPath } from "./paths";
5
4
  import {
6
5
  type AuthState,
7
6
  type ConfigFile,
@@ -9,12 +8,6 @@ import {
9
8
  defaultConfig,
10
9
  } from "./types";
11
10
 
12
- const resolveConfigDir = () =>
13
- process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
14
-
15
- const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
16
- const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
17
-
18
11
  export const readConfig = (): ConfigFile => ({
19
12
  ...defaultConfig(),
20
13
  ...(readJsonFile<ConfigFile>(getConfigPath()) ?? {}),
@@ -32,6 +25,7 @@ export const writeAuth = (auth: AuthState) => {
32
25
  const authPath = getAuthPath();
33
26
  writeJsonFile(authPath, auth);
34
27
  if (process.platform !== "win32") {
28
+ // Restrict token file permissions on Unix-like systems.
35
29
  fs.chmodSync(authPath, 0o600);
36
30
  }
37
31
  };
@@ -1,5 +1,8 @@
1
+ import os from "node:os";
1
2
  import path from "node:path";
2
- import { getConfigDir } from "../paths";
3
3
 
4
- export const getConfigPath = () => path.join(getConfigDir(), "config.json");
5
- export const getAuthPath = () => path.join(getConfigDir(), "auth.json");
4
+ export const resolveConfigDir = () =>
5
+ process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
6
+
7
+ export const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
8
+ export const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
@@ -1,3 +1,4 @@
1
+ import { listProviderModels } from "../api/providerModels";
1
2
  import type { FuzzyChoice } from "./fuzzy";
2
3
 
3
4
  export type ReasoningChoice = {
@@ -35,6 +36,26 @@ export const MODEL_CHOICES: FuzzyChoice<string>[] = [
35
36
  },
36
37
  ];
37
38
 
39
+ export const getCodexModelChoices = async (): Promise<
40
+ FuzzyChoice<string>[]
41
+ > => {
42
+ try {
43
+ const remoteModels = await listProviderModels({ tag: "codex" });
44
+ const remoteChoices = remoteModels.map((model) => ({
45
+ title: model,
46
+ value: model,
47
+ keywords: [model, "codex"],
48
+ }));
49
+
50
+ if (remoteChoices.length > 0) {
51
+ remoteChoices.sort((a, b) => a.title.localeCompare(b.title));
52
+ return remoteChoices;
53
+ }
54
+ } catch {}
55
+
56
+ return MODEL_CHOICES;
57
+ };
58
+
38
59
  export const REASONING_CHOICES: ReasoningChoice[] = [
39
60
  {
40
61
  id: "low",
@@ -45,6 +45,7 @@ export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
45
45
  let firstHeaderIndex: number | null = null;
46
46
  const rootFound = new Set<string>();
47
47
 
48
+ // Update root keys that appear before any section headers.
48
49
  for (let i = 0; i < updated.length; i += 1) {
49
50
  const headerMatch = matchHeader(updated[i] ?? "");
50
51
  if (headerMatch) {
@@ -66,6 +67,7 @@ export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
66
67
  }
67
68
  }
68
69
 
70
+ // Insert missing root keys before the first section header (or at EOF).
69
71
  const insertIndex = firstHeaderIndex ?? updated.length;
70
72
  const missingRoot = ROOT_KEYS.filter((key) => !rootFound.has(key)).map(
71
73
  (key) => `${key} = ${rootValueMap[key]}`,
@@ -76,6 +78,7 @@ export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
76
78
  updated.splice(insertIndex, 0, ...missingRoot, ...(needsBlank ? [""] : []));
77
79
  }
78
80
 
81
+ // Ensure the provider section exists and keep its keys in sync.
79
82
  const providerHeader = `[${PROVIDER_SECTION}]`;
80
83
  const providerHeaderIndex = updated.findIndex(
81
84
  (line) => line.trim() === providerHeader,
@@ -91,6 +94,7 @@ export const mergeCodexToml = (content: string, input: CodexConfigInput) => {
91
94
  return updated.join("\n");
92
95
  }
93
96
 
97
+ // Find the provider section bounds for in-place updates.
94
98
  let providerEnd = updated.length;
95
99
  for (let i = providerHeaderIndex + 1; i < updated.length; i += 1) {
96
100
  if (matchHeader(updated[i] ?? "")) {
@@ -1,6 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
- import os from "node:os";
4
3
  import path from "node:path";
5
4
 
6
5
  export type EnvVars = {
@@ -14,11 +13,21 @@ export type EnvShell = "sh" | "ps1";
14
13
 
15
14
  export type RcShell = "zsh" | "bash" | "fish" | "pwsh";
16
15
 
16
+ const quoteEnvValue = (shell: EnvShell, value: string) => {
17
+ if (shell === "ps1") {
18
+ // PowerShell: single quotes are literal; escape by doubling.
19
+ return `'${value.replaceAll("'", "''")}'`;
20
+ }
21
+
22
+ // POSIX shell: use single quotes; escape embedded single quotes with: '\''.
23
+ return `'${value.replaceAll("'", "'\\''")}'`;
24
+ };
25
+
17
26
  const renderLine = (shell: EnvShell, key: string, value: string) => {
18
27
  if (shell === "ps1") {
19
- return `$env:${key}="${value}"`;
28
+ return `$env:${key}=${quoteEnvValue(shell, value)}`;
20
29
  }
21
- return `export ${key}=${value}`;
30
+ return `export ${key}=${quoteEnvValue(shell, value)}`;
22
31
  };
23
32
 
24
33
  export const renderEnv = (shell: EnvShell, vars: EnvVars) => {
@@ -39,6 +48,7 @@ export const renderEnv = (shell: EnvShell, vars: EnvVars) => {
39
48
  return lines.join("\n");
40
49
  };
41
50
 
51
+ // Wrap getrouter to source env after successful codex/claude runs.
42
52
  export const renderHook = (shell: RcShell) => {
43
53
  if (shell === "pwsh") {
44
54
  return [
@@ -126,6 +136,7 @@ export const writeEnvFile = (filePath: string, content: string) => {
126
136
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
127
137
  fs.writeFileSync(filePath, content, "utf8");
128
138
  if (process.platform !== "win32") {
139
+ // Limit env file readability since it can contain API keys.
129
140
  fs.chmodSync(filePath, 0o600);
130
141
  }
131
142
  };
@@ -215,6 +226,3 @@ export const appendRcIfMissing = (rcPath: string, line: string) => {
215
226
  fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
216
227
  return true;
217
228
  };
218
-
219
- export const resolveConfigDir = () =>
220
- process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import prompts from "prompts";
5
- import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { createProgram } from "../../src/cli";
7
7
  import { createApiClients } from "../../src/core/api/client";
8
8
  import type { ConsumerService } from "../../src/generated/router/dashboard/v1";
@@ -32,6 +32,7 @@ const originalEnv = Object.fromEntries(
32
32
  );
33
33
 
34
34
  afterEach(() => {
35
+ vi.unstubAllGlobals();
35
36
  setStdinTTY(originalIsTTY);
36
37
  prompts.inject([]);
37
38
  for (const key of ENV_KEYS) {
@@ -44,6 +45,57 @@ afterEach(() => {
44
45
  });
45
46
 
46
47
  describe("codex command", () => {
48
+ beforeEach(() => {
49
+ vi.stubGlobal(
50
+ "fetch",
51
+ vi.fn().mockResolvedValue({
52
+ ok: true,
53
+ status: 200,
54
+ json: vi.fn().mockResolvedValue({ models: [] }),
55
+ }),
56
+ );
57
+ });
58
+
59
+ it("fetches codex models from remote endpoint", async () => {
60
+ setStdinTTY(true);
61
+ const dir = makeDir();
62
+ process.env.HOME = dir;
63
+
64
+ const fetchMock = vi.fn().mockResolvedValue({
65
+ ok: true,
66
+ status: 200,
67
+ json: vi.fn().mockResolvedValue({
68
+ models: ["gpt-5.2-codex"],
69
+ }),
70
+ });
71
+ vi.stubGlobal("fetch", fetchMock);
72
+
73
+ prompts.inject(["gpt-5.2-codex", "extra_high", mockConsumer, true]);
74
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
75
+ consumerService: {
76
+ ListConsumers: vi.fn().mockResolvedValue({
77
+ consumers: [
78
+ {
79
+ id: "c1",
80
+ name: "dev",
81
+ enabled: true,
82
+ createdAt: "2026-01-01T00:00:00Z",
83
+ },
84
+ ],
85
+ }),
86
+ GetConsumer: vi.fn().mockResolvedValue(mockConsumer),
87
+ } as unknown as ConsumerService,
88
+ });
89
+
90
+ const program = createProgram();
91
+ await program.parseAsync(["node", "getrouter", "codex"]);
92
+
93
+ expect(fetchMock).toHaveBeenCalled();
94
+ expect(String(fetchMock.mock.calls[0]?.[0] ?? "")).toContain(
95
+ "/v1/dashboard/providers/models?tag=codex",
96
+ );
97
+ });
98
+
47
99
  it("writes codex config and auth after interactive flow", async () => {
48
100
  setStdinTTY(true);
49
101
  const dir = makeDir();
@@ -144,4 +196,38 @@ describe("codex command", () => {
144
196
  expect(auth.OPENAI_API_KEY).toBe("key-123");
145
197
  expect(auth.OTHER).toBe("keep");
146
198
  });
199
+
200
+ it("supports -m to set a custom model", async () => {
201
+ setStdinTTY(true);
202
+ const dir = makeDir();
203
+ process.env.HOME = dir;
204
+ prompts.inject(["extra_high", mockConsumer, true]);
205
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
206
+ consumerService: {
207
+ ListConsumers: vi.fn().mockResolvedValue({
208
+ consumers: [
209
+ {
210
+ id: "c1",
211
+ name: "dev",
212
+ enabled: true,
213
+ createdAt: "2026-01-01T00:00:00Z",
214
+ },
215
+ ],
216
+ }),
217
+ GetConsumer: vi.fn().mockResolvedValue(mockConsumer),
218
+ } as unknown as ConsumerService,
219
+ });
220
+
221
+ const program = createProgram();
222
+ await program.parseAsync([
223
+ "node",
224
+ "getrouter",
225
+ "codex",
226
+ "-m",
227
+ "legacy-model",
228
+ ]);
229
+
230
+ const config = fs.readFileSync(codexConfigPath(dir), "utf8");
231
+ expect(config).toContain('model = "legacy-model"');
232
+ });
147
233
  });
@@ -38,7 +38,7 @@ afterEach(() => {
38
38
  });
39
39
 
40
40
  describe("keys command", () => {
41
- it("lists keys with full apiKey", async () => {
41
+ it("lists keys with redacted apiKey by default", async () => {
42
42
  setStdinTTY(false);
43
43
  (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
44
44
  consumerService: {
@@ -54,6 +54,24 @@ describe("keys command", () => {
54
54
  expect(output).toContain("API_KEY");
55
55
  expect(output).toContain("NAME");
56
56
  expect(output).not.toContain("ID");
57
+ expect(output).toContain("abcd...WXYZ");
58
+ expect(output).not.toContain("abcd1234WXYZ");
59
+ log.mockRestore();
60
+ });
61
+
62
+ it("lists keys with full apiKey when --show is provided", async () => {
63
+ setStdinTTY(false);
64
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
65
+ consumerService: {
66
+ ListConsumers: vi.fn().mockResolvedValue({ consumers: [mockConsumer] }),
67
+ } as unknown as ConsumerService,
68
+ subscriptionService: emptySubscriptionService,
69
+ authService: emptyAuthService,
70
+ });
71
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
72
+ const program = createProgram();
73
+ await program.parseAsync(["node", "getrouter", "keys", "list", "--show"]);
74
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
57
75
  expect(output).toContain("abcd1234WXYZ");
58
76
  log.mockRestore();
59
77
  });
@@ -73,6 +91,24 @@ describe("keys command", () => {
73
91
  const output = log.mock.calls.map((c) => c[0]).join("\n");
74
92
  expect(output).toContain("NAME");
75
93
  expect(output).not.toContain("ID");
94
+ expect(output).toContain("abcd...WXYZ");
95
+ expect(output).not.toContain("abcd1234WXYZ");
96
+ log.mockRestore();
97
+ });
98
+
99
+ it("lists keys when no subcommand and --show is provided", async () => {
100
+ setStdinTTY(true);
101
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
102
+ consumerService: {
103
+ ListConsumers: vi.fn().mockResolvedValue({ consumers: [mockConsumer] }),
104
+ } as unknown as ConsumerService,
105
+ subscriptionService: emptySubscriptionService,
106
+ authService: emptyAuthService,
107
+ });
108
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
109
+ const program = createProgram();
110
+ await program.parseAsync(["node", "getrouter", "keys", "--show"]);
111
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
76
112
  expect(output).toContain("abcd1234WXYZ");
77
113
  log.mockRestore();
78
114
  });
@@ -81,10 +117,15 @@ describe("keys command", () => {
81
117
  setStdinTTY(false);
82
118
  const program = createProgram();
83
119
  program.exitOverride();
84
- program.configureOutput({
120
+ const silentOutput = {
85
121
  writeErr: () => {},
86
122
  writeOut: () => {},
87
- });
123
+ outputError: () => {},
124
+ };
125
+ program.configureOutput(silentOutput);
126
+ program.commands
127
+ .find((command) => command.name() === "keys")
128
+ ?.configureOutput(silentOutput);
88
129
  await expect(
89
130
  program.parseAsync(["node", "getrouter", "keys", "get", "c1"]),
90
131
  ).rejects.toBeTruthy();
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { describe, expect, it } from "vitest";
4
+ import { describe, expect, it, vi } from "vitest";
5
5
  import { readJsonFile, writeJsonFile } from "../../src/core/config/fs";
6
6
 
7
7
  describe("config fs", () => {
@@ -11,4 +11,25 @@ describe("config fs", () => {
11
11
  writeJsonFile(file, { hello: "world" });
12
12
  expect(readJsonFile(file)).toEqual({ hello: "world" });
13
13
  });
14
+
15
+ it("tolerates invalid JSON by returning null and backing up the file", () => {
16
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
17
+ const file = path.join(dir, "config.json");
18
+ fs.writeFileSync(file, "{", "utf8");
19
+
20
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
21
+ try {
22
+ expect(readJsonFile(file)).toBeNull();
23
+ } finally {
24
+ warnSpy.mockRestore();
25
+ }
26
+
27
+ expect(fs.existsSync(file)).toBe(false);
28
+ const backups = fs
29
+ .readdirSync(dir)
30
+ .filter(
31
+ (name) => name.startsWith("config.corrupt-") && name.endsWith(".json"),
32
+ );
33
+ expect(backups.length).toBe(1);
34
+ });
14
35
  });
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { describe, expect, it } from "vitest";
4
+ import { describe, expect, it, vi } from "vitest";
5
5
  import {
6
6
  readAuth,
7
7
  readConfig,
@@ -34,6 +34,21 @@ describe("config read/write", () => {
34
34
  expect(auth.expiresAt).toBe("c");
35
35
  });
36
36
 
37
+ it("falls back to defaults when config.json is invalid JSON", () => {
38
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
39
+ process.env.GETROUTER_CONFIG_DIR = dir;
40
+ fs.writeFileSync(path.join(dir, "config.json"), "{", "utf8");
41
+
42
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
43
+ try {
44
+ const cfg = readConfig();
45
+ expect(cfg.apiBase).toBe("https://getrouter.dev");
46
+ expect(cfg.json).toBe(false);
47
+ } finally {
48
+ warnSpy.mockRestore();
49
+ }
50
+ });
51
+
37
52
  it("defaults tokenType to Bearer", () => {
38
53
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
39
54
  process.env.GETROUTER_CONFIG_DIR = dir;
@@ -1,10 +1,33 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
1
4
  import { describe, expect, it } from "vitest";
2
5
  import { getAuthPath, getConfigPath } from "../../src/core/config/paths";
3
6
 
4
7
  describe("config paths", () => {
8
+ const originalConfigDir = process.env.GETROUTER_CONFIG_DIR;
9
+
10
+ const restore = () => {
11
+ if (originalConfigDir === undefined) {
12
+ delete process.env.GETROUTER_CONFIG_DIR;
13
+ return;
14
+ }
15
+ process.env.GETROUTER_CONFIG_DIR = originalConfigDir;
16
+ };
17
+
5
18
  it("returns ~/.getrouter paths", () => {
19
+ delete process.env.GETROUTER_CONFIG_DIR;
6
20
  expect(getConfigPath()).toContain(".getrouter");
7
21
  expect(getConfigPath()).toContain("config.json");
8
22
  expect(getAuthPath()).toContain("auth.json");
23
+ restore();
24
+ });
25
+
26
+ it("uses GETROUTER_CONFIG_DIR when set", () => {
27
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
28
+ process.env.GETROUTER_CONFIG_DIR = dir;
29
+ expect(getConfigPath()).toBe(path.join(dir, "config.json"));
30
+ expect(getAuthPath()).toBe(path.join(dir, "auth.json"));
31
+ restore();
9
32
  });
10
33
  });
@@ -1,11 +1,16 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import {
3
+ getCodexModelChoices,
3
4
  MODEL_CHOICES,
4
5
  mapReasoningValue,
5
6
  REASONING_CHOICES,
6
7
  } from "../../../src/core/interactive/codex";
7
8
 
8
9
  describe("codex interactive helpers", () => {
10
+ afterEach(() => {
11
+ vi.unstubAllGlobals();
12
+ });
13
+
9
14
  it("maps extra high to xhigh", () => {
10
15
  expect(mapReasoningValue("extra_high")).toBe("xhigh");
11
16
  });
@@ -14,4 +19,23 @@ describe("codex interactive helpers", () => {
14
19
  expect(MODEL_CHOICES.length).toBeGreaterThan(0);
15
20
  expect(REASONING_CHOICES.length).toBeGreaterThan(0);
16
21
  });
22
+
23
+ it("fetches codex models from ListProviderModels(tag=codex)", async () => {
24
+ const fetchMock = vi.fn().mockResolvedValue({
25
+ ok: true,
26
+ status: 200,
27
+ json: vi.fn().mockResolvedValue({
28
+ models: ["new-codex-model-xyz"],
29
+ }),
30
+ });
31
+ vi.stubGlobal("fetch", fetchMock);
32
+
33
+ const choices = await getCodexModelChoices();
34
+ expect(
35
+ choices.some((choice) => choice.value === "new-codex-model-xyz"),
36
+ ).toBe(true);
37
+ expect(String(fetchMock.mock.calls[0]?.[0] ?? "")).toContain(
38
+ "/v1/dashboard/providers/models?tag=codex",
39
+ );
40
+ });
17
41
  });
@@ -23,17 +23,31 @@ describe("setup env helpers", () => {
23
23
  it("renders sh env", () => {
24
24
  const output = renderEnv("sh", vars);
25
25
  expect(output).toContain(
26
- "export OPENAI_BASE_URL=https://api.getrouter.dev/codex",
26
+ "export OPENAI_BASE_URL='https://api.getrouter.dev/codex'",
27
27
  );
28
- expect(output).toContain("export ANTHROPIC_API_KEY=key-123");
28
+ expect(output).toContain("export ANTHROPIC_API_KEY='key-123'");
29
29
  });
30
30
 
31
31
  it("renders ps1 env", () => {
32
32
  const output = renderEnv("ps1", vars);
33
33
  expect(output).toContain(
34
- '$env:OPENAI_BASE_URL="https://api.getrouter.dev/codex"',
34
+ "$env:OPENAI_BASE_URL='https://api.getrouter.dev/codex'",
35
35
  );
36
- expect(output).toContain('$env:ANTHROPIC_API_KEY="key-123"');
36
+ expect(output).toContain("$env:ANTHROPIC_API_KEY='key-123'");
37
+ });
38
+
39
+ it("escapes values safely", () => {
40
+ expect(
41
+ renderEnv("sh", {
42
+ openaiApiKey: "a'b",
43
+ }),
44
+ ).toContain("export OPENAI_API_KEY='a'\\''b'");
45
+
46
+ expect(
47
+ renderEnv("ps1", {
48
+ openaiApiKey: "a'b",
49
+ }),
50
+ ).toContain("$env:OPENAI_API_KEY='a''b'");
37
51
  });
38
52
 
39
53
  it("writes env file", () => {
package/tsconfig.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2020",
3
+ "target": "ES2021",
4
4
  "module": "CommonJS",
5
5
  "rootDir": ".",
6
6
  "outDir": "dist",
package/src/core/paths.ts DELETED
@@ -1,4 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
-
4
- export const getConfigDir = () => path.join(os.homedir(), ".getrouter");
@@ -1,9 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getConfigDir } from "../src/core/paths";
3
-
4
- describe("paths", () => {
5
- it("returns ~/.getrouter path", () => {
6
- const dir = getConfigDir();
7
- expect(dir).toContain(".getrouter");
8
- });
9
- });