@getrouter/getrouter-cli 0.1.0

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.
Files changed (120) hide show
  1. package/.github/workflows/ci.yml +19 -0
  2. package/AGENTS.md +78 -0
  3. package/README.ja.md +116 -0
  4. package/README.md +116 -0
  5. package/README.zh-cn.md +116 -0
  6. package/biome.json +10 -0
  7. package/bun.lock +397 -0
  8. package/dist/bin.mjs +1422 -0
  9. package/docs/plans/2026-01-01-getrouter-cli-config-command-plan.md +231 -0
  10. package/docs/plans/2026-01-01-getrouter-cli-config-core-plan.md +307 -0
  11. package/docs/plans/2026-01-01-getrouter-cli-design.md +106 -0
  12. package/docs/plans/2026-01-01-getrouter-cli-scaffold-plan.md +327 -0
  13. package/docs/plans/2026-01-02-getrouter-cli-auth-design.md +68 -0
  14. package/docs/plans/2026-01-02-getrouter-cli-auth-device-design.md +73 -0
  15. package/docs/plans/2026-01-02-getrouter-cli-auth-device-plan.md +411 -0
  16. package/docs/plans/2026-01-02-getrouter-cli-auth-plan.md +435 -0
  17. package/docs/plans/2026-01-02-getrouter-cli-http-client-plan.md +235 -0
  18. package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-design.md +24 -0
  19. package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-plan.md +141 -0
  20. package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-design.md +22 -0
  21. package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-plan.md +122 -0
  22. package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-design.md +23 -0
  23. package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-plan.md +141 -0
  24. package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-design.md +28 -0
  25. package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-plan.md +247 -0
  26. package/docs/plans/2026-01-02-getrouter-cli-keys-output-design.md +31 -0
  27. package/docs/plans/2026-01-02-getrouter-cli-keys-output-plan.md +187 -0
  28. package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-design.md +52 -0
  29. package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-plan.md +306 -0
  30. package/docs/plans/2026-01-02-getrouter-cli-setup-env-design.md +67 -0
  31. package/docs/plans/2026-01-02-getrouter-cli-setup-env-plan.md +441 -0
  32. package/docs/plans/2026-01-02-getrouter-cli-subscription-output-design.md +34 -0
  33. package/docs/plans/2026-01-02-getrouter-cli-subscription-output-plan.md +157 -0
  34. package/docs/plans/2026-01-03-bun-migration-plan.md +103 -0
  35. package/docs/plans/2026-01-03-cli-emoji-output.md +45 -0
  36. package/docs/plans/2026-01-03-cli-english-output.md +123 -0
  37. package/docs/plans/2026-01-03-cli-simplify-design.md +62 -0
  38. package/docs/plans/2026-01-03-cli-simplify-implementation.md +468 -0
  39. package/docs/plans/2026-01-03-readme-command-descriptions.md +116 -0
  40. package/docs/plans/2026-01-03-tsdown-migration-plan.md +75 -0
  41. package/docs/plans/2026-01-04-cli-docs-cleanup-design.md +49 -0
  42. package/docs/plans/2026-01-04-cli-docs-cleanup-plan.md +126 -0
  43. package/docs/plans/2026-01-04-codex-multistep-design.md +76 -0
  44. package/docs/plans/2026-01-04-codex-multistep-plan.md +240 -0
  45. package/docs/plans/2026-01-04-env-hook-design.md +48 -0
  46. package/docs/plans/2026-01-04-env-hook-plan.md +173 -0
  47. package/docs/plans/2026-01-04-models-keys-fuzzy-design.md +75 -0
  48. package/docs/plans/2026-01-04-models-keys-fuzzy-implementation.md +704 -0
  49. package/package.json +37 -0
  50. package/src/.gitkeep +0 -0
  51. package/src/bin.ts +4 -0
  52. package/src/cli.ts +12 -0
  53. package/src/cmd/auth.ts +44 -0
  54. package/src/cmd/claude.ts +10 -0
  55. package/src/cmd/codex.ts +119 -0
  56. package/src/cmd/config-helpers.ts +16 -0
  57. package/src/cmd/config.ts +31 -0
  58. package/src/cmd/env.ts +103 -0
  59. package/src/cmd/index.ts +20 -0
  60. package/src/cmd/keys.ts +207 -0
  61. package/src/cmd/models.ts +48 -0
  62. package/src/cmd/status.ts +106 -0
  63. package/src/cmd/usages.ts +29 -0
  64. package/src/core/api/client.ts +79 -0
  65. package/src/core/auth/device.ts +105 -0
  66. package/src/core/auth/index.ts +37 -0
  67. package/src/core/config/fs.ts +13 -0
  68. package/src/core/config/index.ts +37 -0
  69. package/src/core/config/paths.ts +5 -0
  70. package/src/core/config/redact.ts +18 -0
  71. package/src/core/config/types.ts +23 -0
  72. package/src/core/http/errors.ts +32 -0
  73. package/src/core/http/request.ts +41 -0
  74. package/src/core/http/url.ts +12 -0
  75. package/src/core/interactive/clipboard.ts +61 -0
  76. package/src/core/interactive/codex.ts +75 -0
  77. package/src/core/interactive/fuzzy.ts +64 -0
  78. package/src/core/interactive/keys.ts +164 -0
  79. package/src/core/output/table.ts +34 -0
  80. package/src/core/output/usages.ts +75 -0
  81. package/src/core/paths.ts +4 -0
  82. package/src/core/setup/codex.ts +129 -0
  83. package/src/core/setup/env.ts +220 -0
  84. package/src/core/usages/aggregate.ts +69 -0
  85. package/src/generated/router/dashboard/v1/index.ts +1104 -0
  86. package/src/index.ts +1 -0
  87. package/tests/.gitkeep +0 -0
  88. package/tests/auth/device.test.ts +75 -0
  89. package/tests/auth/status.test.ts +64 -0
  90. package/tests/cli.test.ts +31 -0
  91. package/tests/cmd/auth.test.ts +90 -0
  92. package/tests/cmd/claude.test.ts +132 -0
  93. package/tests/cmd/codex.test.ts +147 -0
  94. package/tests/cmd/config-helpers.test.ts +18 -0
  95. package/tests/cmd/config.test.ts +56 -0
  96. package/tests/cmd/keys.test.ts +163 -0
  97. package/tests/cmd/models.test.ts +63 -0
  98. package/tests/cmd/status.test.ts +82 -0
  99. package/tests/cmd/usages.test.ts +42 -0
  100. package/tests/config/fs.test.ts +14 -0
  101. package/tests/config/index.test.ts +63 -0
  102. package/tests/config/paths.test.ts +10 -0
  103. package/tests/config/redact.test.ts +17 -0
  104. package/tests/config/types.test.ts +10 -0
  105. package/tests/core/api/client.test.ts +92 -0
  106. package/tests/core/interactive/clipboard.test.ts +44 -0
  107. package/tests/core/interactive/codex.test.ts +17 -0
  108. package/tests/core/interactive/fuzzy.test.ts +30 -0
  109. package/tests/core/setup/codex.test.ts +38 -0
  110. package/tests/core/setup/env.test.ts +84 -0
  111. package/tests/core/usages/aggregate.test.ts +55 -0
  112. package/tests/http/errors.test.ts +15 -0
  113. package/tests/http/request.test.ts +82 -0
  114. package/tests/http/url.test.ts +17 -0
  115. package/tests/output/table.test.ts +29 -0
  116. package/tests/output/usages.test.ts +71 -0
  117. package/tests/paths.test.ts +9 -0
  118. package/tsconfig.json +13 -0
  119. package/tsdown.config.ts +5 -0
  120. package/vitest.config.ts +7 -0
package/dist/bin.mjs ADDED
@@ -0,0 +1,1422 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execSync, spawn } from "node:child_process";
7
+ import { randomInt } from "node:crypto";
8
+ import prompts from "prompts";
9
+
10
+ //#region src/generated/router/dashboard/v1/index.ts
11
+ function createSubscriptionServiceClient(handler) {
12
+ return { CurrentSubscription(request) {
13
+ const path$1 = `v1/dashboard/subscriptions/current`;
14
+ const body = null;
15
+ const queryParams = [];
16
+ let uri = path$1;
17
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
18
+ return handler({
19
+ path: uri,
20
+ method: "GET",
21
+ body
22
+ }, {
23
+ service: "SubscriptionService",
24
+ method: "CurrentSubscription"
25
+ });
26
+ } };
27
+ }
28
+ function createConsumerServiceClient(handler) {
29
+ return {
30
+ CreateConsumer(request) {
31
+ const path$1 = `v1/dashboard/consumers/create`;
32
+ const body = JSON.stringify(request);
33
+ const queryParams = [];
34
+ let uri = path$1;
35
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
36
+ return handler({
37
+ path: uri,
38
+ method: "POST",
39
+ body
40
+ }, {
41
+ service: "ConsumerService",
42
+ method: "CreateConsumer"
43
+ });
44
+ },
45
+ UpdateConsumer(request) {
46
+ const path$1 = `v1/dashboard/consumers/update`;
47
+ const body = JSON.stringify(request?.consumer ?? {});
48
+ const queryParams = [];
49
+ if (request.updateMask) queryParams.push(`updateMask=${encodeURIComponent(request.updateMask.toString())}`);
50
+ let uri = path$1;
51
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
52
+ return handler({
53
+ path: uri,
54
+ method: "PUT",
55
+ body
56
+ }, {
57
+ service: "ConsumerService",
58
+ method: "UpdateConsumer"
59
+ });
60
+ },
61
+ DeleteConsumer(request) {
62
+ if (!request.id) throw new Error("missing required field request.id");
63
+ const path$1 = `v1/dashboard/consumers/${request.id}`;
64
+ const body = null;
65
+ const queryParams = [];
66
+ let uri = path$1;
67
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
68
+ return handler({
69
+ path: uri,
70
+ method: "DELETE",
71
+ body
72
+ }, {
73
+ service: "ConsumerService",
74
+ method: "DeleteConsumer"
75
+ });
76
+ },
77
+ ListConsumers(request) {
78
+ const path$1 = `v1/dashboard/consumers`;
79
+ const body = null;
80
+ const queryParams = [];
81
+ if (request.pageSize) queryParams.push(`pageSize=${encodeURIComponent(request.pageSize.toString())}`);
82
+ if (request.pageToken) queryParams.push(`pageToken=${encodeURIComponent(request.pageToken.toString())}`);
83
+ let uri = path$1;
84
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
85
+ return handler({
86
+ path: uri,
87
+ method: "GET",
88
+ body
89
+ }, {
90
+ service: "ConsumerService",
91
+ method: "ListConsumers"
92
+ });
93
+ },
94
+ GetConsumer(request) {
95
+ if (!request.id) throw new Error("missing required field request.id");
96
+ const path$1 = `v1/dashboard/consumers/${request.id}`;
97
+ const body = null;
98
+ const queryParams = [];
99
+ let uri = path$1;
100
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
101
+ return handler({
102
+ path: uri,
103
+ method: "GET",
104
+ body
105
+ }, {
106
+ service: "ConsumerService",
107
+ method: "GetConsumer"
108
+ });
109
+ }
110
+ };
111
+ }
112
+ function createAuthServiceClient(handler) {
113
+ return {
114
+ Authorize(request) {
115
+ const path$1 = `v1/dashboard/auth/authorize`;
116
+ const body = JSON.stringify(request);
117
+ const queryParams = [];
118
+ let uri = path$1;
119
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
120
+ return handler({
121
+ path: uri,
122
+ method: "POST",
123
+ body
124
+ }, {
125
+ service: "AuthService",
126
+ method: "Authorize"
127
+ });
128
+ },
129
+ CreateAuth(request) {
130
+ const path$1 = `v1/dashboard/auth/create`;
131
+ const body = JSON.stringify(request);
132
+ const queryParams = [];
133
+ let uri = path$1;
134
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
135
+ return handler({
136
+ path: uri,
137
+ method: "POST",
138
+ body
139
+ }, {
140
+ service: "AuthService",
141
+ method: "CreateAuth"
142
+ });
143
+ },
144
+ RefreshToken(request) {
145
+ const path$1 = `v1/dashboard/auth/token`;
146
+ const body = JSON.stringify(request);
147
+ const queryParams = [];
148
+ let uri = path$1;
149
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
150
+ return handler({
151
+ path: uri,
152
+ method: "POST",
153
+ body
154
+ }, {
155
+ service: "AuthService",
156
+ method: "RefreshToken"
157
+ });
158
+ }
159
+ };
160
+ }
161
+ function createModelServiceClient(handler) {
162
+ return { ListModels(request) {
163
+ const path$1 = `v1/dashboard/models`;
164
+ const body = null;
165
+ const queryParams = [];
166
+ if (request.pageSize) queryParams.push(`pageSize=${encodeURIComponent(request.pageSize.toString())}`);
167
+ if (request.pageToken) queryParams.push(`pageToken=${encodeURIComponent(request.pageToken.toString())}`);
168
+ if (request.filter) queryParams.push(`filter=${encodeURIComponent(request.filter.toString())}`);
169
+ let uri = path$1;
170
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
171
+ return handler({
172
+ path: uri,
173
+ method: "GET",
174
+ body
175
+ }, {
176
+ service: "ModelService",
177
+ method: "ListModels"
178
+ });
179
+ } };
180
+ }
181
+ function createUsageServiceClient(handler) {
182
+ return { ListUsage(request) {
183
+ const path$1 = `v1/dashboard/usages`;
184
+ const body = null;
185
+ const queryParams = [];
186
+ if (request.pageSize) queryParams.push(`pageSize=${encodeURIComponent(request.pageSize.toString())}`);
187
+ if (request.pageToken) queryParams.push(`pageToken=${encodeURIComponent(request.pageToken.toString())}`);
188
+ let uri = path$1;
189
+ if (queryParams.length > 0) uri += `?${queryParams.join("&")}`;
190
+ return handler({
191
+ path: uri,
192
+ method: "GET",
193
+ body
194
+ }, {
195
+ service: "UsageService",
196
+ method: "ListUsage"
197
+ });
198
+ } };
199
+ }
200
+
201
+ //#endregion
202
+ //#region src/core/config/fs.ts
203
+ const readJsonFile = (filePath) => {
204
+ if (!fs.existsSync(filePath)) return null;
205
+ const raw = fs.readFileSync(filePath, "utf8");
206
+ return JSON.parse(raw);
207
+ };
208
+ const writeJsonFile = (filePath, value) => {
209
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
210
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
211
+ };
212
+
213
+ //#endregion
214
+ //#region src/core/config/types.ts
215
+ const defaultConfig = () => ({
216
+ apiBase: "https://getrouter.dev",
217
+ json: false
218
+ });
219
+ const defaultAuthState = () => ({
220
+ accessToken: "",
221
+ refreshToken: "",
222
+ expiresAt: "",
223
+ tokenType: "Bearer"
224
+ });
225
+
226
+ //#endregion
227
+ //#region src/core/config/index.ts
228
+ const resolveConfigDir$1 = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
229
+ const getConfigPath = () => path.join(resolveConfigDir$1(), "config.json");
230
+ const getAuthPath = () => path.join(resolveConfigDir$1(), "auth.json");
231
+ const readConfig = () => ({
232
+ ...defaultConfig(),
233
+ ...readJsonFile(getConfigPath()) ?? {}
234
+ });
235
+ const writeConfig = (cfg) => writeJsonFile(getConfigPath(), cfg);
236
+ const readAuth = () => ({
237
+ ...defaultAuthState(),
238
+ ...readJsonFile(getAuthPath()) ?? {}
239
+ });
240
+ const writeAuth = (auth) => {
241
+ const authPath = getAuthPath();
242
+ writeJsonFile(authPath, auth);
243
+ if (process.platform !== "win32") fs.chmodSync(authPath, 384);
244
+ };
245
+
246
+ //#endregion
247
+ //#region src/core/http/errors.ts
248
+ const createApiError = (payload, fallbackMessage, status) => {
249
+ const payloadObject = payload && typeof payload === "object" ? payload : void 0;
250
+ const message = payloadObject && typeof payloadObject.message === "string" ? payloadObject.message : fallbackMessage;
251
+ const err = new Error(message);
252
+ if (payloadObject && typeof payloadObject.code === "string") err.code = payloadObject.code;
253
+ if (payloadObject && payloadObject.details != null) err.details = payloadObject.details;
254
+ if (typeof status === "number") err.status = status;
255
+ return err;
256
+ };
257
+
258
+ //#endregion
259
+ //#region src/core/http/url.ts
260
+ const getApiBase = () => {
261
+ return (readConfig().apiBase || "").replace(/\/+$/, "");
262
+ };
263
+ const buildApiUrl = (path$1) => {
264
+ const base = getApiBase();
265
+ const normalized = path$1.replace(/^\/+/, "");
266
+ return base ? `${base}/${normalized}` : `/${normalized}`;
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/core/http/request.ts
271
+ const getAuthCookieName = () => process.env.GETROUTER_AUTH_COOKIE || process.env.KRATOS_AUTH_COOKIE || "access_token";
272
+ const requestJson = async ({ path: path$1, method, body, fetchImpl }) => {
273
+ const headers = { "Content-Type": "application/json" };
274
+ const auth = readAuth();
275
+ if (auth.accessToken) {
276
+ headers.Authorization = `Bearer ${auth.accessToken}`;
277
+ headers.Cookie = `${getAuthCookieName()}=${auth.accessToken}`;
278
+ }
279
+ const res = await (fetchImpl ?? fetch)(buildApiUrl(path$1), {
280
+ method,
281
+ headers,
282
+ body: body == null ? void 0 : JSON.stringify(body)
283
+ });
284
+ if (!res.ok) throw createApiError(await res.json().catch(() => null), res.statusText, res.status);
285
+ return await res.json();
286
+ };
287
+
288
+ //#endregion
289
+ //#region src/core/api/client.ts
290
+ const createApiClients = ({ fetchImpl, clients }) => {
291
+ const factories = clients ?? {
292
+ createConsumerServiceClient,
293
+ createAuthServiceClient,
294
+ createSubscriptionServiceClient,
295
+ createUsageServiceClient,
296
+ createModelServiceClient
297
+ };
298
+ const handler = async ({ path: path$1, method, body }) => {
299
+ return requestJson({
300
+ path: path$1,
301
+ method,
302
+ body: body ? JSON.parse(body) : void 0,
303
+ fetchImpl
304
+ });
305
+ };
306
+ return {
307
+ authService: factories.createAuthServiceClient(handler),
308
+ consumerService: factories.createConsumerServiceClient(handler),
309
+ modelService: factories.createModelServiceClient(handler),
310
+ subscriptionService: factories.createSubscriptionServiceClient(handler),
311
+ usageService: factories.createUsageServiceClient(handler)
312
+ };
313
+ };
314
+
315
+ //#endregion
316
+ //#region src/core/auth/index.ts
317
+ const isExpired = (expiresAt) => {
318
+ if (!expiresAt) return true;
319
+ const t = Date.parse(expiresAt);
320
+ if (Number.isNaN(t)) return true;
321
+ return t <= Date.now();
322
+ };
323
+ const getAuthStatus = () => {
324
+ const auth = readAuth();
325
+ if (!Boolean(auth.accessToken && auth.refreshToken) || isExpired(auth.expiresAt)) return { status: "logged_out" };
326
+ return {
327
+ status: "logged_in",
328
+ expiresAt: auth.expiresAt,
329
+ accessToken: auth.accessToken,
330
+ refreshToken: auth.refreshToken,
331
+ tokenType: auth.tokenType
332
+ };
333
+ };
334
+ const clearAuth = () => {
335
+ writeAuth(defaultAuthState());
336
+ };
337
+
338
+ //#endregion
339
+ //#region src/core/auth/device.ts
340
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
341
+ const generateAuthCode = () => {
342
+ let out = "";
343
+ for (let i = 0; i < 13; i += 1) out += alphabet[randomInt(32)];
344
+ return out;
345
+ };
346
+ const buildLoginUrl = (authCode) => `https://getrouter.dev/#/a/${authCode}`;
347
+ const openLoginUrl = async (url) => {
348
+ try {
349
+ if (process.platform === "darwin") {
350
+ spawn("open", [url], {
351
+ stdio: "ignore",
352
+ detached: true
353
+ }).unref();
354
+ return;
355
+ }
356
+ if (process.platform === "win32") {
357
+ spawn("cmd", [
358
+ "/c",
359
+ "start",
360
+ "",
361
+ url
362
+ ], {
363
+ stdio: "ignore",
364
+ detached: true
365
+ }).unref();
366
+ return;
367
+ }
368
+ spawn("xdg-open", [url], {
369
+ stdio: "ignore",
370
+ detached: true
371
+ }).unref();
372
+ } catch {}
373
+ };
374
+ const pollAuthorize = async ({ authorize, code, timeoutMs = 300 * 1e3, initialDelayMs = 1e3, maxDelayMs = 1e4, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now(), onRetry }) => {
375
+ const start = now();
376
+ let delay = initialDelayMs;
377
+ let attempt = 0;
378
+ while (true) {
379
+ try {
380
+ return await authorize({ code });
381
+ } catch (err) {
382
+ const status = typeof err === "object" && err !== null && "status" in err ? err.status : void 0;
383
+ if (status === 404) {} else if (status === 400) throw new Error("Auth code already used. Please log in again.");
384
+ else if (status === 403) throw new Error("Auth code expired. Please log in again.");
385
+ else throw err;
386
+ }
387
+ if (now() - start >= timeoutMs) throw new Error("Login timed out. Please run getrouter auth login again.");
388
+ attempt += 1;
389
+ onRetry?.(attempt, delay);
390
+ await sleep(delay);
391
+ delay = Math.min(delay * 2, maxDelayMs);
392
+ }
393
+ };
394
+
395
+ //#endregion
396
+ //#region src/cmd/auth.ts
397
+ const registerAuthCommands = (program) => {
398
+ program.command("login").description("Login with device flow").action(async () => {
399
+ const { authService } = createApiClients({});
400
+ const authCode = generateAuthCode();
401
+ const url = buildLoginUrl(authCode);
402
+ console.log("🔐 To authenticate, visit:");
403
+ console.log(url);
404
+ console.log("⏳ Waiting for confirmation...");
405
+ openLoginUrl(url);
406
+ const token = await pollAuthorize({
407
+ authorize: authService.Authorize.bind(authService),
408
+ code: authCode
409
+ });
410
+ writeAuth({
411
+ accessToken: token.accessToken ?? "",
412
+ refreshToken: token.refreshToken ?? "",
413
+ expiresAt: token.expiresAt ?? "",
414
+ tokenType: "Bearer"
415
+ });
416
+ console.log("✅ Login successful.");
417
+ });
418
+ program.command("logout").description("Clear local auth state").action(() => {
419
+ clearAuth();
420
+ console.log("Cleared local auth data.");
421
+ });
422
+ };
423
+
424
+ //#endregion
425
+ //#region src/core/interactive/fuzzy.ts
426
+ const normalize = (value) => value.toLowerCase();
427
+ const fuzzyScore = (query, target) => {
428
+ if (!query) return 0;
429
+ let score = 0;
430
+ let lastIndex = -1;
431
+ for (const ch of query) {
432
+ const index = target.indexOf(ch, lastIndex + 1);
433
+ if (index === -1) return null;
434
+ score += index;
435
+ lastIndex = index;
436
+ }
437
+ return score;
438
+ };
439
+ const toSearchText = (choice) => normalize([choice.title, ...choice.keywords ?? []].join(" ").trim());
440
+ const rankFuzzyChoices = (choices, input, limit = 50) => {
441
+ const query = normalize(input.trim());
442
+ if (!query) return choices.slice(0, limit);
443
+ const ranked = choices.map((choice) => {
444
+ const score = fuzzyScore(query, toSearchText(choice));
445
+ return score == null ? null : {
446
+ choice,
447
+ score
448
+ };
449
+ }).filter(Boolean);
450
+ ranked.sort((a, b) => a.score - b.score || a.choice.title.localeCompare(b.choice.title));
451
+ return ranked.slice(0, limit).map((entry) => entry.choice);
452
+ };
453
+ const fuzzySelect = async ({ message, choices }) => {
454
+ const response = await prompts({
455
+ type: "autocomplete",
456
+ name: "value",
457
+ message,
458
+ choices,
459
+ suggest: async (input, items) => rankFuzzyChoices(items, String(input))
460
+ });
461
+ if (response.value == null || response.value === "") return null;
462
+ return response.value;
463
+ };
464
+
465
+ //#endregion
466
+ //#region src/core/interactive/keys.ts
467
+ const sortByCreatedAtDesc = (consumers) => consumers.slice().sort((a, b) => {
468
+ const aTime = Date.parse(a.createdAt ?? "") || 0;
469
+ return (Date.parse(b.createdAt ?? "") || 0) - aTime;
470
+ });
471
+ const normalizeName = (consumer) => {
472
+ const name = consumer.name?.trim();
473
+ return name && name.length > 0 ? name : "(unnamed)";
474
+ };
475
+ const buildNameCounts = (consumers) => {
476
+ const counts = /* @__PURE__ */ new Map();
477
+ for (const consumer of consumers) {
478
+ const name = normalizeName(consumer);
479
+ counts.set(name, (counts.get(name) ?? 0) + 1);
480
+ }
481
+ return counts;
482
+ };
483
+ const formatChoice = (consumer, nameCounts) => {
484
+ const name = normalizeName(consumer);
485
+ const createdAt = consumer.createdAt ?? "-";
486
+ return (nameCounts.get(name) ?? 0) > 1 || name === "(unnamed)" ? `${name} (${createdAt})` : name;
487
+ };
488
+ const promptKeyName = async (initial) => {
489
+ const response = await prompts({
490
+ type: "text",
491
+ name: "name",
492
+ message: "Key name",
493
+ initial: initial ?? ""
494
+ });
495
+ const value = typeof response.name === "string" ? response.name.trim() : "";
496
+ return value.length > 0 ? value : void 0;
497
+ };
498
+ const promptKeyEnabled = async (initial) => {
499
+ const response = await prompts({
500
+ type: "confirm",
501
+ name: "enabled",
502
+ message: "Enable this key?",
503
+ initial
504
+ });
505
+ return typeof response.enabled === "boolean" ? response.enabled : initial;
506
+ };
507
+ const selectConsumer = async (consumerService) => {
508
+ const consumers = (await consumerService.ListConsumers({
509
+ pageSize: void 0,
510
+ pageToken: void 0
511
+ }))?.consumers ?? [];
512
+ if (consumers.length === 0) throw new Error("No available API keys");
513
+ const sorted = sortByCreatedAtDesc(consumers);
514
+ const nameCounts = buildNameCounts(sorted);
515
+ return await fuzzySelect({
516
+ message: "🔎 Search keys",
517
+ choices: sorted.map((consumer) => ({
518
+ title: formatChoice(consumer, nameCounts),
519
+ value: consumer,
520
+ keywords: [normalizeName(consumer), consumer.createdAt ?? ""].filter(Boolean)
521
+ }))
522
+ }) ?? null;
523
+ };
524
+ const selectConsumerList = async (consumerService, message) => {
525
+ const consumers = (await consumerService.ListConsumers({
526
+ pageSize: void 0,
527
+ pageToken: void 0
528
+ }))?.consumers ?? [];
529
+ if (consumers.length === 0) throw new Error("No available API keys");
530
+ const sorted = sortByCreatedAtDesc(consumers);
531
+ const nameCounts = buildNameCounts(sorted);
532
+ const response = await prompts({
533
+ type: "select",
534
+ name: "value",
535
+ message,
536
+ choices: sorted.map((consumer) => ({
537
+ title: formatChoice(consumer, nameCounts),
538
+ value: consumer
539
+ }))
540
+ });
541
+ if (response.value == null || response.value === "") return null;
542
+ return response.value;
543
+ };
544
+ const confirmDelete = async (consumer) => {
545
+ const response = await prompts({
546
+ type: "confirm",
547
+ name: "confirm",
548
+ message: `⚠️ Confirm delete ${consumer.name ?? "-"} (${consumer.id ?? "-"})?`,
549
+ initial: false
550
+ });
551
+ return Boolean(response.confirm);
552
+ };
553
+
554
+ //#endregion
555
+ //#region src/core/setup/env.ts
556
+ const renderLine = (shell, key, value) => {
557
+ if (shell === "ps1") return `$env:${key}="${value}"`;
558
+ return `export ${key}=${value}`;
559
+ };
560
+ const renderEnv = (shell, vars) => {
561
+ const lines = [];
562
+ if (vars.openaiBaseUrl) lines.push(renderLine(shell, "OPENAI_BASE_URL", vars.openaiBaseUrl));
563
+ if (vars.openaiApiKey) lines.push(renderLine(shell, "OPENAI_API_KEY", vars.openaiApiKey));
564
+ if (vars.anthropicBaseUrl) lines.push(renderLine(shell, "ANTHROPIC_BASE_URL", vars.anthropicBaseUrl));
565
+ if (vars.anthropicApiKey) lines.push(renderLine(shell, "ANTHROPIC_API_KEY", vars.anthropicApiKey));
566
+ lines.push("");
567
+ return lines.join("\n");
568
+ };
569
+ const renderHook = (shell) => {
570
+ if (shell === "pwsh") return [
571
+ "function getrouter {",
572
+ " $cmd = Get-Command getrouter -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1",
573
+ " if ($null -ne $cmd) {",
574
+ " & $cmd.Source @args",
575
+ " }",
576
+ " $exitCode = $LASTEXITCODE",
577
+ " if ($exitCode -ne 0) {",
578
+ " return $exitCode",
579
+ " }",
580
+ " if ($args.Count -gt 0 -and ($args[0] -eq \"codex\" -or $args[0] -eq \"claude\")) {",
581
+ " $configDir = if ($env:GETROUTER_CONFIG_DIR) { $env:GETROUTER_CONFIG_DIR } else { Join-Path $HOME \".getrouter\" }",
582
+ " $envPath = Join-Path $configDir \"env.ps1\"",
583
+ " if (Test-Path $envPath) {",
584
+ " . $envPath",
585
+ " }",
586
+ " }",
587
+ " return $exitCode",
588
+ "}",
589
+ ""
590
+ ].join("\n");
591
+ if (shell === "fish") return [
592
+ "function getrouter",
593
+ " command getrouter $argv",
594
+ " set -l exit_code $status",
595
+ " if test $exit_code -ne 0",
596
+ " return $exit_code",
597
+ " end",
598
+ " if test (count $argv) -gt 0",
599
+ " switch $argv[1]",
600
+ " case codex claude",
601
+ " set -l config_dir $GETROUTER_CONFIG_DIR",
602
+ " if test -z \"$config_dir\"",
603
+ " set config_dir \"$HOME/.getrouter\"",
604
+ " end",
605
+ " set -l env_path \"$config_dir/env.sh\"",
606
+ " if test -f \"$env_path\"",
607
+ " source \"$env_path\"",
608
+ " end",
609
+ " end",
610
+ " end",
611
+ " return $exit_code",
612
+ "end",
613
+ ""
614
+ ].join("\n");
615
+ return [
616
+ "getrouter() {",
617
+ " command getrouter \"$@\"",
618
+ " local exit_code=$?",
619
+ " if [ $exit_code -ne 0 ]; then",
620
+ " return $exit_code",
621
+ " fi",
622
+ " case \"$1\" in",
623
+ " codex|claude)",
624
+ ` local config_dir="\${GETROUTER_CONFIG_DIR:-$HOME/.getrouter}"`,
625
+ " local env_path=\"$config_dir/env.sh\"",
626
+ " if [ -f \"$env_path\" ]; then",
627
+ " source \"$env_path\"",
628
+ " fi",
629
+ " ;;",
630
+ " esac",
631
+ " return $exit_code",
632
+ "}",
633
+ ""
634
+ ].join("\n");
635
+ };
636
+ const getEnvFilePath = (shell, configDir) => path.join(configDir, shell === "ps1" ? "env.ps1" : "env.sh");
637
+ const getHookFilePath = (shell, configDir) => {
638
+ if (shell === "pwsh") return path.join(configDir, "hook.ps1");
639
+ if (shell === "fish") return path.join(configDir, "hook.fish");
640
+ return path.join(configDir, "hook.sh");
641
+ };
642
+ const writeEnvFile = (filePath, content) => {
643
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
644
+ fs.writeFileSync(filePath, content, "utf8");
645
+ if (process.platform !== "win32") fs.chmodSync(filePath, 384);
646
+ };
647
+ const resolveShellRcPath = (shell, homeDir) => {
648
+ if (shell === "zsh") return path.join(homeDir, ".zshrc");
649
+ if (shell === "bash") return path.join(homeDir, ".bashrc");
650
+ if (shell === "fish") return path.join(homeDir, ".config/fish/config.fish");
651
+ if (shell === "pwsh") {
652
+ if (process.platform === "win32") return path.join(homeDir, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1");
653
+ return path.join(homeDir, ".config/powershell/Microsoft.PowerShell_profile.ps1");
654
+ }
655
+ return null;
656
+ };
657
+ const resolveEnvShell = (shell) => shell === "pwsh" ? "ps1" : "sh";
658
+ const detectShell = () => {
659
+ const shellPath = process.env.SHELL;
660
+ if (shellPath) {
661
+ const name = shellPath.split("/").pop()?.toLowerCase();
662
+ if (name === "zsh" || name === "bash" || name === "fish" || name === "pwsh") return name;
663
+ }
664
+ if (process.platform === "win32") return "pwsh";
665
+ return "bash";
666
+ };
667
+ const applyEnvVars = (vars) => {
668
+ if (vars.openaiBaseUrl) process.env.OPENAI_BASE_URL = vars.openaiBaseUrl;
669
+ if (vars.openaiApiKey) process.env.OPENAI_API_KEY = vars.openaiApiKey;
670
+ if (vars.anthropicBaseUrl) process.env.ANTHROPIC_BASE_URL = vars.anthropicBaseUrl;
671
+ if (vars.anthropicApiKey) process.env.ANTHROPIC_API_KEY = vars.anthropicApiKey;
672
+ };
673
+ const formatSourceLine = (shell, envPath) => shell === "ps1" ? `. ${envPath}` : `source ${envPath}`;
674
+ const trySourceEnv = (shell, envShell, envPath) => {
675
+ try {
676
+ if (envShell === "ps1") {
677
+ execSync(`pwsh -NoProfile -Command ". '${envPath}'"`, { stdio: "ignore" });
678
+ return;
679
+ }
680
+ execSync(`${shell} -c "${shell === "fish" ? "source" : "source"} '${envPath}'"`, { stdio: "ignore" });
681
+ } catch {}
682
+ };
683
+ const appendRcIfMissing = (rcPath, line) => {
684
+ let content = "";
685
+ if (fs.existsSync(rcPath)) {
686
+ content = fs.readFileSync(rcPath, "utf8");
687
+ if (content.includes(line)) return false;
688
+ }
689
+ const prefix = content && !content.endsWith("\n") ? "\n" : "";
690
+ fs.mkdirSync(path.dirname(rcPath), { recursive: true });
691
+ fs.writeFileSync(rcPath, `${content}${prefix}${line}\n`, "utf8");
692
+ return true;
693
+ };
694
+ const resolveConfigDir = () => process.env.GETROUTER_CONFIG_DIR || path.join(os.homedir(), ".getrouter");
695
+
696
+ //#endregion
697
+ //#region src/cmd/env.ts
698
+ const CLAUDE_BASE_URL = "https://api.getrouter.dev/claude";
699
+ const registerEnvCommand = (program, config) => {
700
+ program.command(config.name).description(config.description).option("--install", "Install into shell rc").action(async (options) => {
701
+ if (!process.stdin.isTTY) throw new Error("Interactive mode required for key selection.");
702
+ const shell = detectShell();
703
+ const envShell = resolveEnvShell(shell);
704
+ const configDir = resolveConfigDir();
705
+ const { consumerService } = createApiClients({});
706
+ const selected = await selectConsumer(consumerService);
707
+ if (!selected?.id) return;
708
+ const consumer = await consumerService.GetConsumer({ id: selected.id });
709
+ if (!consumer?.apiKey) throw new Error("API key not found. Please create one or choose another.");
710
+ const vars = config.vars(consumer.apiKey);
711
+ const envPath = getEnvFilePath(envShell, configDir);
712
+ writeEnvFile(envPath, renderEnv(envShell, vars));
713
+ let installed = false;
714
+ let rcPath = null;
715
+ if (options.install) {
716
+ const hookPath = getHookFilePath(shell, configDir);
717
+ writeEnvFile(hookPath, renderHook(shell));
718
+ rcPath = resolveShellRcPath(shell, os.homedir());
719
+ if (rcPath) {
720
+ const envLine = formatSourceLine(envShell, envPath);
721
+ const hookLine = formatSourceLine(envShell, hookPath);
722
+ const envAdded = appendRcIfMissing(rcPath, envLine);
723
+ const hookAdded = appendRcIfMissing(rcPath, hookLine);
724
+ installed = envAdded || hookAdded;
725
+ }
726
+ applyEnvVars(vars);
727
+ trySourceEnv(shell, envShell, envPath);
728
+ }
729
+ const sourceLine = formatSourceLine(envShell, envPath);
730
+ if (options.install) {
731
+ if (installed && rcPath) console.log(`✅ Added to ${rcPath}`);
732
+ else if (rcPath) console.log(`ℹ️ Already configured in ${rcPath}`);
733
+ } else {
734
+ console.log("To load the environment in your shell, run:");
735
+ console.log(sourceLine);
736
+ }
737
+ });
738
+ };
739
+ const buildAnthropicEnv = (apiKey) => ({
740
+ anthropicBaseUrl: CLAUDE_BASE_URL,
741
+ anthropicApiKey: apiKey
742
+ });
743
+
744
+ //#endregion
745
+ //#region src/cmd/claude.ts
746
+ const registerClaudeCommand = (program) => {
747
+ registerEnvCommand(program, {
748
+ name: "claude",
749
+ description: "Configure Claude environment",
750
+ vars: buildAnthropicEnv
751
+ });
752
+ };
753
+
754
+ //#endregion
755
+ //#region src/core/interactive/codex.ts
756
+ const MODEL_CHOICES = [
757
+ {
758
+ title: "gpt-5.2-codex",
759
+ value: "gpt-5.2-codex",
760
+ description: "Latest frontier agentic coding model.",
761
+ keywords: ["gpt-5.2-codex", "codex"]
762
+ },
763
+ {
764
+ title: "gpt-5.1-codex-max",
765
+ value: "gpt-5.1-codex-max",
766
+ description: "Codex-optimized flagship for deep and fast reasoning.",
767
+ keywords: ["gpt-5.1-codex-max", "codex"]
768
+ },
769
+ {
770
+ title: "gpt-5.1-codex-mini",
771
+ value: "gpt-5.1-codex-mini",
772
+ description: "Optimized for codex. Cheaper, faster, but less capable.",
773
+ keywords: ["gpt-5.1-codex-mini", "codex"]
774
+ },
775
+ {
776
+ title: "gpt-5.2",
777
+ value: "gpt-5.2",
778
+ description: "Latest frontier model with improvements across knowledge, reasoning and coding.",
779
+ keywords: ["gpt-5.2"]
780
+ }
781
+ ];
782
+ const REASONING_CHOICES = [
783
+ {
784
+ id: "low",
785
+ label: "Low",
786
+ value: "low",
787
+ description: "Fast responses with lighter reasoning"
788
+ },
789
+ {
790
+ id: "medium",
791
+ label: "Medium (default)",
792
+ value: "medium",
793
+ description: "Balances speed and reasoning depth for everyday tasks"
794
+ },
795
+ {
796
+ id: "high",
797
+ label: "High",
798
+ value: "high",
799
+ description: "Greater reasoning depth for complex problems"
800
+ },
801
+ {
802
+ id: "extra_high",
803
+ label: "Extra high",
804
+ value: "xhigh",
805
+ description: "Extra high reasoning depth for complex problems. Warning: Extra high reasoning effort can quickly consume Plus plan rate limits."
806
+ }
807
+ ];
808
+ const REASONING_FUZZY_CHOICES = REASONING_CHOICES.map((choice) => ({
809
+ title: choice.label,
810
+ value: choice.id,
811
+ description: choice.description,
812
+ keywords: [choice.id, choice.value]
813
+ }));
814
+ const mapReasoningValue = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.value ?? "medium";
815
+
816
+ //#endregion
817
+ //#region src/core/setup/codex.ts
818
+ const CODEX_PROVIDER = "getrouter";
819
+ const CODEX_BASE_URL = "https://api.getrouter.dev/codex";
820
+ const ROOT_KEYS = [
821
+ "model",
822
+ "model_reasoning_effort",
823
+ "model_provider"
824
+ ];
825
+ const PROVIDER_SECTION = `model_providers.${CODEX_PROVIDER}`;
826
+ const PROVIDER_KEYS = [
827
+ "name",
828
+ "base_url",
829
+ "wire_api",
830
+ "requires_openai_auth"
831
+ ];
832
+ const rootValues = (input) => ({
833
+ model: `"${input.model}"`,
834
+ model_reasoning_effort: `"${input.reasoning}"`,
835
+ model_provider: `"${CODEX_PROVIDER}"`
836
+ });
837
+ const providerValues = () => ({
838
+ name: `"${CODEX_PROVIDER}"`,
839
+ base_url: `"${CODEX_BASE_URL}"`,
840
+ wire_api: `"responses"`,
841
+ requires_openai_auth: "true"
842
+ });
843
+ const matchHeader = (line) => line.match(/^\s*\[([^\]]+)\]\s*$/);
844
+ const matchKey = (line) => line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/);
845
+ const mergeCodexToml = (content, input) => {
846
+ const updated = [...content.length ? content.split(/\r?\n/) : []];
847
+ const rootValueMap = rootValues(input);
848
+ const providerValueMap = providerValues();
849
+ let currentSection = null;
850
+ let firstHeaderIndex = null;
851
+ const rootFound = /* @__PURE__ */ new Set();
852
+ for (let i = 0; i < updated.length; i += 1) {
853
+ const headerMatch = matchHeader(updated[i] ?? "");
854
+ if (headerMatch) {
855
+ currentSection = headerMatch[1]?.trim() ?? null;
856
+ if (firstHeaderIndex === null) firstHeaderIndex = i;
857
+ continue;
858
+ }
859
+ if (currentSection !== null) continue;
860
+ const keyMatch = matchKey(updated[i] ?? "");
861
+ if (!keyMatch) continue;
862
+ const key = keyMatch[1];
863
+ if (ROOT_KEYS.includes(key)) {
864
+ updated[i] = `${key} = ${rootValueMap[key]}`;
865
+ rootFound.add(key);
866
+ }
867
+ }
868
+ const insertIndex = firstHeaderIndex ?? updated.length;
869
+ const missingRoot = ROOT_KEYS.filter((key) => !rootFound.has(key)).map((key) => `${key} = ${rootValueMap[key]}`);
870
+ if (missingRoot.length > 0) {
871
+ const needsBlank = insertIndex < updated.length && updated[insertIndex]?.trim() !== "";
872
+ updated.splice(insertIndex, 0, ...missingRoot, ...needsBlank ? [""] : []);
873
+ }
874
+ const providerHeader = `[${PROVIDER_SECTION}]`;
875
+ const providerHeaderIndex = updated.findIndex((line) => line.trim() === providerHeader);
876
+ if (providerHeaderIndex === -1) {
877
+ if (updated.length > 0 && updated[updated.length - 1]?.trim() !== "") updated.push("");
878
+ updated.push(providerHeader);
879
+ for (const key of PROVIDER_KEYS) updated.push(`${key} = ${providerValueMap[key]}`);
880
+ return updated.join("\n");
881
+ }
882
+ let providerEnd = updated.length;
883
+ for (let i = providerHeaderIndex + 1; i < updated.length; i += 1) if (matchHeader(updated[i] ?? "")) {
884
+ providerEnd = i;
885
+ break;
886
+ }
887
+ const providerFound = /* @__PURE__ */ new Set();
888
+ for (let i = providerHeaderIndex + 1; i < providerEnd; i += 1) {
889
+ const keyMatch = matchKey(updated[i] ?? "");
890
+ if (!keyMatch) continue;
891
+ const key = keyMatch[1];
892
+ if (PROVIDER_KEYS.includes(key)) {
893
+ updated[i] = `${key} = ${providerValueMap[key]}`;
894
+ providerFound.add(key);
895
+ }
896
+ }
897
+ const missingProvider = PROVIDER_KEYS.filter((key) => !providerFound.has(key)).map((key) => `${key} = ${providerValueMap[key]}`);
898
+ if (missingProvider.length > 0) updated.splice(providerEnd, 0, ...missingProvider);
899
+ return updated.join("\n");
900
+ };
901
+ const mergeAuthJson = (data, apiKey) => ({
902
+ ...data,
903
+ OPENAI_API_KEY: apiKey
904
+ });
905
+
906
+ //#endregion
907
+ //#region src/cmd/codex.ts
908
+ const CODEX_DIR = ".codex";
909
+ const readFileIfExists = (filePath) => fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
910
+ const readAuthJson = (filePath) => {
911
+ if (!fs.existsSync(filePath)) return {};
912
+ const raw = fs.readFileSync(filePath, "utf8").trim();
913
+ if (!raw) return {};
914
+ const parsed = JSON.parse(raw);
915
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid auth.json format.");
916
+ return parsed;
917
+ };
918
+ const ensureCodexDir = () => {
919
+ const dir = path.join(os.homedir(), CODEX_DIR);
920
+ fs.mkdirSync(dir, { recursive: true });
921
+ return dir;
922
+ };
923
+ const requireInteractive$1 = () => {
924
+ if (!process.stdin.isTTY) throw new Error("Interactive mode required for codex configuration.");
925
+ };
926
+ const promptModel = async () => await fuzzySelect({
927
+ message: "Select Model and Effort\nAccess legacy models by running codex -m <model_name> or in your config.toml",
928
+ choices: MODEL_CHOICES
929
+ });
930
+ const promptReasoning = async (model) => await fuzzySelect({
931
+ message: `Select Reasoning Level for ${model}`,
932
+ choices: REASONING_FUZZY_CHOICES
933
+ });
934
+ const formatReasoningLabel = (id) => REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
935
+ const registerCodexCommand = (program) => {
936
+ program.command("codex").description("Configure Codex").action(async () => {
937
+ requireInteractive$1();
938
+ const model = await promptModel();
939
+ if (!model) return;
940
+ const reasoningId = await promptReasoning(model);
941
+ if (!reasoningId) return;
942
+ const { consumerService } = createApiClients({});
943
+ const selected = await selectConsumer(consumerService);
944
+ if (!selected?.id) return;
945
+ const apiKey = (await consumerService.GetConsumer({ id: selected.id }))?.apiKey ?? "";
946
+ if (!apiKey) throw new Error("API key not found. Please create one or choose another.");
947
+ const reasoningValue = mapReasoningValue(reasoningId);
948
+ const keyName = selected.name ?? "-";
949
+ const keyId = selected.id ?? "-";
950
+ if (!(await prompts({
951
+ type: "confirm",
952
+ name: "confirm",
953
+ message: [
954
+ "Apply Codex configuration?",
955
+ `Model: ${model}`,
956
+ `Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`,
957
+ "Provider: getrouter",
958
+ `Key: ${keyName} (${keyId})`
959
+ ].join("\n"),
960
+ initial: true
961
+ })).confirm) return;
962
+ const codexDir = ensureCodexDir();
963
+ const configPath = path.join(codexDir, "config.toml");
964
+ const authPath = path.join(codexDir, "auth.json");
965
+ const mergedConfig = mergeCodexToml(readFileIfExists(configPath), {
966
+ model,
967
+ reasoning: reasoningValue
968
+ });
969
+ fs.writeFileSync(configPath, mergedConfig, "utf8");
970
+ const mergedAuth = mergeAuthJson(readAuthJson(authPath), apiKey);
971
+ fs.writeFileSync(authPath, JSON.stringify(mergedAuth, null, 2));
972
+ if (process.platform !== "win32") fs.chmodSync(authPath, 384);
973
+ console.log("✅ Updated ~/.codex/config.toml");
974
+ console.log("✅ Updated ~/.codex/auth.json");
975
+ });
976
+ };
977
+
978
+ //#endregion
979
+ //#region src/cmd/config-helpers.ts
980
+ const normalizeApiBase = (value) => value.trim().replace(/\/+$/, "");
981
+ const parseConfigValue = (key, raw) => {
982
+ if (key === "apiBase") {
983
+ const normalized = normalizeApiBase(raw);
984
+ if (!/^https?:\/\//.test(normalized)) throw new Error("apiBase must start with http:// or https://");
985
+ return normalized;
986
+ }
987
+ const lowered = raw.toLowerCase();
988
+ if (["true", "1"].includes(lowered)) return true;
989
+ if (["false", "0"].includes(lowered)) return false;
990
+ throw new Error("json must be true/false or 1/0");
991
+ };
992
+
993
+ //#endregion
994
+ //#region src/cmd/config.ts
995
+ const VALID_KEYS = new Set(["apiBase", "json"]);
996
+ const registerConfigCommands = (program) => {
997
+ program.command("config").description("Manage CLI config").argument("[key]").argument("[value]").action((key, value) => {
998
+ const cfg = readConfig();
999
+ if (!key) {
1000
+ console.log(`apiBase=${cfg.apiBase}`);
1001
+ console.log(`json=${cfg.json}`);
1002
+ return;
1003
+ }
1004
+ if (!value) throw new Error("Missing config value");
1005
+ if (!VALID_KEYS.has(key)) throw new Error("Unknown config key");
1006
+ const parsed = parseConfigValue(key, value);
1007
+ const next = {
1008
+ ...cfg,
1009
+ [key]: parsed
1010
+ };
1011
+ writeConfig(next);
1012
+ console.log(`${key}=${next[key]}`);
1013
+ });
1014
+ };
1015
+
1016
+ //#endregion
1017
+ //#region src/core/config/redact.ts
1018
+ const SECRET_KEYS = new Set([
1019
+ "accessToken",
1020
+ "refreshToken",
1021
+ "apiKey"
1022
+ ]);
1023
+ const mask = (value) => {
1024
+ if (!value) return "";
1025
+ if (value.length <= 8) return "****";
1026
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
1027
+ };
1028
+ const redactSecrets = (obj) => {
1029
+ const out = { ...obj };
1030
+ for (const key of Object.keys(out)) {
1031
+ const value = out[key];
1032
+ if (SECRET_KEYS.has(key) && typeof value === "string") out[key] = mask(value);
1033
+ }
1034
+ return out;
1035
+ };
1036
+
1037
+ //#endregion
1038
+ //#region src/core/output/table.ts
1039
+ const truncate = (value, max) => {
1040
+ if (value.length <= max) return value;
1041
+ if (max <= 3) return value.slice(0, max);
1042
+ return `${value.slice(0, max - 3)}...`;
1043
+ };
1044
+ const renderTable = (headers, rows, options = {}) => {
1045
+ const maxColWidth = options.maxColWidth ?? 32;
1046
+ const normalized = rows.map((row) => row.map((cell) => cell && cell.length > 0 ? cell : "-"));
1047
+ const widths = headers.map((header, index) => {
1048
+ const colValues = normalized.map((row) => row[index] ?? "-");
1049
+ const maxLen = Math.max(header.length, ...colValues.map((v) => v.length));
1050
+ return Math.min(maxLen, maxColWidth);
1051
+ });
1052
+ const renderRow = (cells) => cells.map((cell, index) => {
1053
+ return truncate(cell ?? "-", widths[index]).padEnd(widths[index], " ");
1054
+ }).join(" ");
1055
+ return `${renderRow(headers)}\n${normalized.map((row) => renderRow(row)).join("\n")}`;
1056
+ };
1057
+
1058
+ //#endregion
1059
+ //#region src/cmd/keys.ts
1060
+ const consumerHeaders = [
1061
+ "NAME",
1062
+ "ENABLED",
1063
+ "LAST_ACCESS",
1064
+ "CREATED_AT",
1065
+ "API_KEY"
1066
+ ];
1067
+ const consumerRow = (consumer) => [
1068
+ String(consumer.name ?? ""),
1069
+ String(consumer.enabled ?? ""),
1070
+ String(consumer.lastAccess ?? ""),
1071
+ String(consumer.createdAt ?? ""),
1072
+ String(consumer.apiKey ?? "")
1073
+ ];
1074
+ const outputConsumerTable = (consumer) => {
1075
+ console.log(renderTable(consumerHeaders, [consumerRow(consumer)]));
1076
+ };
1077
+ const outputConsumers = (consumers) => {
1078
+ const rows = consumers.map(consumerRow);
1079
+ console.log(renderTable(consumerHeaders, rows));
1080
+ };
1081
+ const redactConsumer = (consumer) => redactSecrets(consumer);
1082
+ const requireInteractive = (message) => {
1083
+ if (!process.stdin.isTTY) throw new Error(message);
1084
+ };
1085
+ const requireInteractiveForSelection = () => requireInteractive("Interactive mode required when key id is omitted.");
1086
+ const requireInteractiveForAction = (action) => requireInteractive(`Interactive mode required for keys ${action}.`);
1087
+ const updateConsumer = async (consumerService, consumer, name, enabled) => {
1088
+ const updateMask = [name !== void 0 && name !== consumer.name ? "name" : null, enabled !== void 0 && enabled !== consumer.enabled ? "enabled" : null].filter(Boolean).join(",");
1089
+ if (!updateMask) return consumer;
1090
+ return consumerService.UpdateConsumer({
1091
+ consumer: {
1092
+ ...consumer,
1093
+ name: name ?? consumer.name,
1094
+ enabled: enabled ?? consumer.enabled
1095
+ },
1096
+ updateMask
1097
+ });
1098
+ };
1099
+ const listConsumers = async (consumerService) => {
1100
+ outputConsumers(((await consumerService.ListConsumers({
1101
+ pageSize: void 0,
1102
+ pageToken: void 0
1103
+ }))?.consumers ?? []).map(redactConsumer));
1104
+ };
1105
+ const resolveConsumerForUpdate = async (consumerService, id) => {
1106
+ if (id) return consumerService.GetConsumer({ id });
1107
+ requireInteractiveForSelection();
1108
+ return await selectConsumerList(consumerService, "Select key to update");
1109
+ };
1110
+ const resolveConsumerForDelete = async (consumerService, id) => {
1111
+ if (id) return consumerService.GetConsumer({ id });
1112
+ requireInteractiveForSelection();
1113
+ return await selectConsumerList(consumerService, "Select key to delete");
1114
+ };
1115
+ const createConsumer = async (consumerService) => {
1116
+ requireInteractiveForAction("create");
1117
+ const name = await promptKeyName();
1118
+ const enabled = await promptKeyEnabled(true);
1119
+ let consumer = await consumerService.CreateConsumer({});
1120
+ consumer = await updateConsumer(consumerService, consumer, name, enabled);
1121
+ outputConsumerTable(consumer);
1122
+ console.log("Please store this API key securely.");
1123
+ };
1124
+ const updateConsumerById = async (consumerService, id) => {
1125
+ requireInteractiveForAction("update");
1126
+ const selected = await resolveConsumerForUpdate(consumerService, id);
1127
+ if (!selected?.id) return;
1128
+ outputConsumerTable(redactConsumer(await updateConsumer(consumerService, selected, await promptKeyName(selected.name), await promptKeyEnabled(selected.enabled ?? true))));
1129
+ };
1130
+ const deleteConsumerById = async (consumerService, id) => {
1131
+ requireInteractiveForAction("delete");
1132
+ const selected = await resolveConsumerForDelete(consumerService, id);
1133
+ if (!selected?.id) return;
1134
+ if (!await confirmDelete(selected)) return;
1135
+ await consumerService.DeleteConsumer({ id: selected.id });
1136
+ outputConsumerTable(redactConsumer(selected));
1137
+ };
1138
+ const registerKeysCommands = (program) => {
1139
+ const keys = program.command("keys").description("Manage API keys");
1140
+ keys.allowExcessArguments(false);
1141
+ keys.action(async () => {
1142
+ const { consumerService } = createApiClients({});
1143
+ await listConsumers(consumerService);
1144
+ });
1145
+ keys.command("list").description("List API keys").action(async () => {
1146
+ const { consumerService } = createApiClients({});
1147
+ await listConsumers(consumerService);
1148
+ });
1149
+ keys.command("create").description("Create an API key").action(async () => {
1150
+ const { consumerService } = createApiClients({});
1151
+ await createConsumer(consumerService);
1152
+ });
1153
+ keys.command("update").description("Update an API key").argument("[id]", "Key id").action(async (id) => {
1154
+ const { consumerService } = createApiClients({});
1155
+ await updateConsumerById(consumerService, id);
1156
+ });
1157
+ keys.command("delete").description("Delete an API key").argument("[id]", "Key id").action(async (id) => {
1158
+ const { consumerService } = createApiClients({});
1159
+ await deleteConsumerById(consumerService, id);
1160
+ });
1161
+ };
1162
+
1163
+ //#endregion
1164
+ //#region src/cmd/models.ts
1165
+ const modelHeaders = [
1166
+ "NAME",
1167
+ "AUTHOR",
1168
+ "ENABLED",
1169
+ "UPDATED_AT"
1170
+ ];
1171
+ const modelRow = (model) => [
1172
+ String(model.name ?? ""),
1173
+ String(model.author ?? ""),
1174
+ String(model.enabled ?? ""),
1175
+ String(model.updatedAt ?? "")
1176
+ ];
1177
+ const outputModels = (models) => {
1178
+ console.log("🧠 Models");
1179
+ console.log(renderTable(modelHeaders, models.map(modelRow)));
1180
+ };
1181
+ const listModels = async () => {
1182
+ const { modelService } = createApiClients({});
1183
+ const models = (await modelService.ListModels({
1184
+ pageSize: void 0,
1185
+ pageToken: void 0,
1186
+ filter: void 0
1187
+ }))?.models ?? [];
1188
+ if (models.length === 0) {
1189
+ console.log("😕 No models found");
1190
+ return;
1191
+ }
1192
+ outputModels(models);
1193
+ };
1194
+ const registerModelsCommands = (program) => {
1195
+ const models = program.command("models").description("List models");
1196
+ models.action(async () => {
1197
+ await listModels();
1198
+ });
1199
+ models.command("list").description("List models").action(async () => {
1200
+ await listModels();
1201
+ });
1202
+ };
1203
+
1204
+ //#endregion
1205
+ //#region src/cmd/status.ts
1206
+ const LABEL_WIDTH = 10;
1207
+ const formatLine = (label, value) => {
1208
+ if (value == null || value === "") return null;
1209
+ return ` ${label.padEnd(LABEL_WIDTH, " ")}: ${value}`;
1210
+ };
1211
+ const formatAuthStatus = (status) => status === "logged_in" ? "✅ Logged in" : "❌ Logged out";
1212
+ const formatToken = (token) => {
1213
+ if (!token) return void 0;
1214
+ const trimmed = token.trim();
1215
+ if (trimmed.length <= 12) return trimmed;
1216
+ return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
1217
+ };
1218
+ const formatWindow = (startAt, endAt) => {
1219
+ if (!startAt && !endAt) return void 0;
1220
+ if (startAt && endAt) return `${startAt} → ${endAt}`;
1221
+ if (startAt) return `${startAt} →`;
1222
+ return `→ ${endAt}`;
1223
+ };
1224
+ const formatLimits = (requestPerMinute, tokenPerMinute) => {
1225
+ const parts = [];
1226
+ if (typeof requestPerMinute === "number") parts.push(`${requestPerMinute} req/min`);
1227
+ if (tokenPerMinute) parts.push(`${tokenPerMinute} tok/min`);
1228
+ if (parts.length === 0) return void 0;
1229
+ return parts.join(" · ");
1230
+ };
1231
+ const renderAuthSection = () => {
1232
+ const status = getAuthStatus();
1233
+ return ["🔐 Auth", ...[
1234
+ formatLine("Status", formatAuthStatus(status.status)),
1235
+ status.status === "logged_in" ? formatLine("Expires", status.expiresAt) : null,
1236
+ status.status === "logged_in" ? formatLine("TokenType", status.tokenType) : null,
1237
+ status.status === "logged_in" ? formatLine("Access", formatToken(status.accessToken)) : null,
1238
+ status.status === "logged_in" ? formatLine("Refresh", formatToken(status.refreshToken)) : null
1239
+ ].filter(Boolean)].join("\n");
1240
+ };
1241
+ const renderSubscriptionSection = (subscription) => {
1242
+ if (!subscription) return ["📦 Subscription", formatLine("Status", "No active subscription")].filter(Boolean).join("\n");
1243
+ const limits = formatLimits(subscription.plan?.requestPerMinute, subscription.plan?.tokenPerMinute);
1244
+ const windowLabel = formatWindow(subscription.startAt, subscription.endAt);
1245
+ return ["📦 Subscription", ...[
1246
+ formatLine("Plan", subscription.plan?.name),
1247
+ formatLine("Status", subscription.status),
1248
+ formatLine("Window", windowLabel),
1249
+ formatLine("Limits", limits)
1250
+ ].filter(Boolean)].join("\n");
1251
+ };
1252
+ const registerStatusCommand = (program) => {
1253
+ program.command("status").description("Show login and subscription status").action(async () => {
1254
+ const { subscriptionService } = createApiClients({});
1255
+ const subscription = await subscriptionService.CurrentSubscription({});
1256
+ console.log(renderAuthSection());
1257
+ console.log("");
1258
+ console.log(renderSubscriptionSection(subscription));
1259
+ });
1260
+ };
1261
+
1262
+ //#endregion
1263
+ //#region src/core/output/usages.ts
1264
+ const INPUT_BLOCK = "█";
1265
+ const OUTPUT_BLOCK = "▒";
1266
+ const DEFAULT_WIDTH = 24;
1267
+ const formatTokens = (value) => {
1268
+ const abs = Math.abs(value);
1269
+ if (abs < 1e3) return Math.round(value).toString();
1270
+ for (const unit of [
1271
+ {
1272
+ threshold: 1e9,
1273
+ suffix: "B"
1274
+ },
1275
+ {
1276
+ threshold: 1e6,
1277
+ suffix: "M"
1278
+ },
1279
+ {
1280
+ threshold: 1e3,
1281
+ suffix: "K"
1282
+ }
1283
+ ]) if (abs >= unit.threshold) {
1284
+ const scaled = value / unit.threshold;
1285
+ const decimals = Math.abs(scaled) < 10 ? 1 : 0;
1286
+ let output = scaled.toFixed(decimals);
1287
+ if (output.endsWith(".0")) output = output.slice(0, -2);
1288
+ return `${output}${unit.suffix}`;
1289
+ }
1290
+ return Math.round(value).toString();
1291
+ };
1292
+ const renderUsageChart = (rows, width = DEFAULT_WIDTH) => {
1293
+ const header = "📊 Usage (last 7 days) · Tokens";
1294
+ if (rows.length === 0) return `${header}\n\nNo usage data available.`;
1295
+ const normalized = rows.map((row) => {
1296
+ const input = Number(row.inputTokens);
1297
+ const output = Number(row.outputTokens);
1298
+ const safeInput = Number.isFinite(input) ? input : 0;
1299
+ const safeOutput = Number.isFinite(output) ? output : 0;
1300
+ return {
1301
+ day: row.day,
1302
+ input: safeInput,
1303
+ output: safeOutput,
1304
+ total: safeInput + safeOutput
1305
+ };
1306
+ });
1307
+ const totals = normalized.map((row) => row.total);
1308
+ const maxTotal = Math.max(0, ...totals);
1309
+ const lines = normalized.map((row) => {
1310
+ const total = row.total;
1311
+ if (maxTotal === 0 || total === 0) return `${row.day} ${"".padEnd(width, " ")} I:0 O:0`;
1312
+ const scaled = Math.max(1, Math.round(total / maxTotal * width));
1313
+ let inputBars = Math.round(row.input / total * scaled);
1314
+ let outputBars = Math.max(0, scaled - inputBars);
1315
+ if (row.input > 0 && row.output > 0) {
1316
+ if (inputBars === 0) {
1317
+ inputBars = 1;
1318
+ outputBars = Math.max(0, scaled - 1);
1319
+ } else if (outputBars === 0) {
1320
+ outputBars = 1;
1321
+ inputBars = Math.max(0, scaled - 1);
1322
+ }
1323
+ }
1324
+ const bar = `${INPUT_BLOCK.repeat(inputBars)}${OUTPUT_BLOCK.repeat(outputBars)}`;
1325
+ const inputLabel = formatTokens(row.input);
1326
+ const outputLabel = formatTokens(row.output);
1327
+ return `${row.day} ${bar.padEnd(width, " ")} I:${inputLabel} O:${outputLabel}`;
1328
+ });
1329
+ const legend = `Legend: ${INPUT_BLOCK} input ${OUTPUT_BLOCK} output`;
1330
+ return [
1331
+ header,
1332
+ "",
1333
+ ...lines,
1334
+ "",
1335
+ legend
1336
+ ].join("\n");
1337
+ };
1338
+
1339
+ //#endregion
1340
+ //#region src/core/usages/aggregate.ts
1341
+ const formatDay = (value) => {
1342
+ return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}`;
1343
+ };
1344
+ const toNumber = (value) => {
1345
+ if (typeof value === "number") return Number.isFinite(value) ? value : 0;
1346
+ if (typeof value === "string") {
1347
+ const parsed = Number(value);
1348
+ return Number.isFinite(parsed) ? parsed : 0;
1349
+ }
1350
+ return 0;
1351
+ };
1352
+ const aggregateUsages = (usages, maxDays = 7) => {
1353
+ const totals = /* @__PURE__ */ new Map();
1354
+ for (const usage of usages) {
1355
+ if (!usage.createdAt) continue;
1356
+ const parsed = new Date(usage.createdAt);
1357
+ if (Number.isNaN(parsed.getTime())) continue;
1358
+ const day = formatDay(parsed);
1359
+ const input = toNumber(usage.inputTokens);
1360
+ const output = toNumber(usage.outputTokens);
1361
+ const totalRaw = typeof usage.totalTokens === "string" || typeof usage.totalTokens === "number" ? Number(usage.totalTokens) : NaN;
1362
+ const total = Number.isFinite(totalRaw) && totalRaw > 0 ? totalRaw : input + output;
1363
+ const current = totals.get(day) ?? {
1364
+ day,
1365
+ inputTokens: 0,
1366
+ outputTokens: 0,
1367
+ totalTokens: 0,
1368
+ requests: 0
1369
+ };
1370
+ current.inputTokens += input;
1371
+ current.outputTokens += output;
1372
+ current.totalTokens += total;
1373
+ current.requests += 1;
1374
+ totals.set(day, current);
1375
+ }
1376
+ return Array.from(totals.values()).sort((a, b) => b.day.localeCompare(a.day)).slice(0, maxDays);
1377
+ };
1378
+
1379
+ //#endregion
1380
+ //#region src/cmd/usages.ts
1381
+ const collectUsages = async () => {
1382
+ const { usageService } = createApiClients({});
1383
+ return aggregateUsages((await usageService.ListUsage({
1384
+ pageSize: 7,
1385
+ pageToken: void 0
1386
+ }))?.usages ?? [], 7);
1387
+ };
1388
+ const registerUsagesCommand = (program) => {
1389
+ program.command("usages").description("Show recent usage").action(async () => {
1390
+ const aggregated = await collectUsages();
1391
+ console.log(renderUsageChart(aggregated));
1392
+ });
1393
+ };
1394
+
1395
+ //#endregion
1396
+ //#region src/cmd/index.ts
1397
+ const registerCommands = (program) => {
1398
+ registerAuthCommands(program);
1399
+ registerCodexCommand(program);
1400
+ registerClaudeCommand(program);
1401
+ registerConfigCommands(program);
1402
+ registerKeysCommands(program);
1403
+ registerModelsCommands(program);
1404
+ registerStatusCommand(program);
1405
+ registerUsagesCommand(program);
1406
+ };
1407
+
1408
+ //#endregion
1409
+ //#region src/cli.ts
1410
+ const createProgram = () => {
1411
+ const program = new Command();
1412
+ program.name("getrouter").description("CLI for getrouter.dev").version("0.1.0");
1413
+ registerCommands(program);
1414
+ return program;
1415
+ };
1416
+
1417
+ //#endregion
1418
+ //#region src/bin.ts
1419
+ createProgram().parse(process.argv);
1420
+
1421
+ //#endregion
1422
+ export { };