@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.
- package/.github/workflows/ci.yml +19 -0
- package/AGENTS.md +78 -0
- package/README.ja.md +116 -0
- package/README.md +116 -0
- package/README.zh-cn.md +116 -0
- package/biome.json +10 -0
- package/bun.lock +397 -0
- package/dist/bin.mjs +1422 -0
- package/docs/plans/2026-01-01-getrouter-cli-config-command-plan.md +231 -0
- package/docs/plans/2026-01-01-getrouter-cli-config-core-plan.md +307 -0
- package/docs/plans/2026-01-01-getrouter-cli-design.md +106 -0
- package/docs/plans/2026-01-01-getrouter-cli-scaffold-plan.md +327 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-design.md +68 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-device-design.md +73 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-device-plan.md +411 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-plan.md +435 -0
- package/docs/plans/2026-01-02-getrouter-cli-http-client-plan.md +235 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-design.md +24 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-plan.md +141 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-design.md +22 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-plan.md +122 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-design.md +23 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-plan.md +141 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-design.md +28 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-plan.md +247 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-output-design.md +31 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-output-plan.md +187 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-design.md +52 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-plan.md +306 -0
- package/docs/plans/2026-01-02-getrouter-cli-setup-env-design.md +67 -0
- package/docs/plans/2026-01-02-getrouter-cli-setup-env-plan.md +441 -0
- package/docs/plans/2026-01-02-getrouter-cli-subscription-output-design.md +34 -0
- package/docs/plans/2026-01-02-getrouter-cli-subscription-output-plan.md +157 -0
- package/docs/plans/2026-01-03-bun-migration-plan.md +103 -0
- package/docs/plans/2026-01-03-cli-emoji-output.md +45 -0
- package/docs/plans/2026-01-03-cli-english-output.md +123 -0
- package/docs/plans/2026-01-03-cli-simplify-design.md +62 -0
- package/docs/plans/2026-01-03-cli-simplify-implementation.md +468 -0
- package/docs/plans/2026-01-03-readme-command-descriptions.md +116 -0
- package/docs/plans/2026-01-03-tsdown-migration-plan.md +75 -0
- package/docs/plans/2026-01-04-cli-docs-cleanup-design.md +49 -0
- package/docs/plans/2026-01-04-cli-docs-cleanup-plan.md +126 -0
- package/docs/plans/2026-01-04-codex-multistep-design.md +76 -0
- package/docs/plans/2026-01-04-codex-multistep-plan.md +240 -0
- package/docs/plans/2026-01-04-env-hook-design.md +48 -0
- package/docs/plans/2026-01-04-env-hook-plan.md +173 -0
- package/docs/plans/2026-01-04-models-keys-fuzzy-design.md +75 -0
- package/docs/plans/2026-01-04-models-keys-fuzzy-implementation.md +704 -0
- package/package.json +37 -0
- package/src/.gitkeep +0 -0
- package/src/bin.ts +4 -0
- package/src/cli.ts +12 -0
- package/src/cmd/auth.ts +44 -0
- package/src/cmd/claude.ts +10 -0
- package/src/cmd/codex.ts +119 -0
- package/src/cmd/config-helpers.ts +16 -0
- package/src/cmd/config.ts +31 -0
- package/src/cmd/env.ts +103 -0
- package/src/cmd/index.ts +20 -0
- package/src/cmd/keys.ts +207 -0
- package/src/cmd/models.ts +48 -0
- package/src/cmd/status.ts +106 -0
- package/src/cmd/usages.ts +29 -0
- package/src/core/api/client.ts +79 -0
- package/src/core/auth/device.ts +105 -0
- package/src/core/auth/index.ts +37 -0
- package/src/core/config/fs.ts +13 -0
- package/src/core/config/index.ts +37 -0
- package/src/core/config/paths.ts +5 -0
- package/src/core/config/redact.ts +18 -0
- package/src/core/config/types.ts +23 -0
- package/src/core/http/errors.ts +32 -0
- package/src/core/http/request.ts +41 -0
- package/src/core/http/url.ts +12 -0
- package/src/core/interactive/clipboard.ts +61 -0
- package/src/core/interactive/codex.ts +75 -0
- package/src/core/interactive/fuzzy.ts +64 -0
- package/src/core/interactive/keys.ts +164 -0
- package/src/core/output/table.ts +34 -0
- package/src/core/output/usages.ts +75 -0
- package/src/core/paths.ts +4 -0
- package/src/core/setup/codex.ts +129 -0
- package/src/core/setup/env.ts +220 -0
- package/src/core/usages/aggregate.ts +69 -0
- package/src/generated/router/dashboard/v1/index.ts +1104 -0
- package/src/index.ts +1 -0
- package/tests/.gitkeep +0 -0
- package/tests/auth/device.test.ts +75 -0
- package/tests/auth/status.test.ts +64 -0
- package/tests/cli.test.ts +31 -0
- package/tests/cmd/auth.test.ts +90 -0
- package/tests/cmd/claude.test.ts +132 -0
- package/tests/cmd/codex.test.ts +147 -0
- package/tests/cmd/config-helpers.test.ts +18 -0
- package/tests/cmd/config.test.ts +56 -0
- package/tests/cmd/keys.test.ts +163 -0
- package/tests/cmd/models.test.ts +63 -0
- package/tests/cmd/status.test.ts +82 -0
- package/tests/cmd/usages.test.ts +42 -0
- package/tests/config/fs.test.ts +14 -0
- package/tests/config/index.test.ts +63 -0
- package/tests/config/paths.test.ts +10 -0
- package/tests/config/redact.test.ts +17 -0
- package/tests/config/types.test.ts +10 -0
- package/tests/core/api/client.test.ts +92 -0
- package/tests/core/interactive/clipboard.test.ts +44 -0
- package/tests/core/interactive/codex.test.ts +17 -0
- package/tests/core/interactive/fuzzy.test.ts +30 -0
- package/tests/core/setup/codex.test.ts +38 -0
- package/tests/core/setup/env.test.ts +84 -0
- package/tests/core/usages/aggregate.test.ts +55 -0
- package/tests/http/errors.test.ts +15 -0
- package/tests/http/request.test.ts +82 -0
- package/tests/http/url.test.ts +17 -0
- package/tests/output/table.test.ts +29 -0
- package/tests/output/usages.test.ts +71 -0
- package/tests/paths.test.ts +9 -0
- package/tsconfig.json +13 -0
- package/tsdown.config.ts +5 -0
- 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 { };
|