@getrouter/getrouter-cli 0.1.13 โ 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +522 -441
- package/package.json +1 -1
- package/src/cli.ts +4 -2
- package/src/cmd/auth.ts +2 -2
- package/src/cmd/claude.ts +2 -2
- package/src/cmd/codex.ts +24 -16
- package/src/cmd/env.ts +15 -11
- package/src/cmd/index.ts +2 -2
- package/src/cmd/keys.ts +59 -35
- package/src/cmd/models.ts +24 -26
- package/src/cmd/status.ts +113 -62
- package/src/cmd/usages.ts +4 -4
- package/src/core/api/client.ts +12 -14
- package/src/core/api/pagination.ts +3 -3
- package/src/core/api/providerModels.ts +15 -14
- package/src/core/auth/device.ts +38 -23
- package/src/core/auth/index.ts +19 -11
- package/src/core/auth/refresh.ts +22 -17
- package/src/core/config/fs.ts +6 -6
- package/src/core/config/index.ts +16 -11
- package/src/core/config/paths.ts +12 -4
- package/src/core/config/redact.ts +4 -4
- package/src/core/config/types.ts +14 -10
- package/src/core/http/errors.ts +18 -10
- package/src/core/http/request.ts +29 -34
- package/src/core/http/retry.ts +37 -24
- package/src/core/http/url.ts +11 -6
- package/src/core/interactive/clipboard.ts +10 -9
- package/src/core/interactive/keys.ts +22 -26
- package/src/core/output/table.ts +5 -5
- package/src/core/output/usages.ts +34 -33
- package/src/core/setup/codex.ts +195 -142
- package/src/core/setup/env.ts +49 -42
- package/src/core/usages/aggregate.ts +3 -3
package/src/core/http/retry.ts
CHANGED
|
@@ -1,38 +1,54 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RetryOptions {
|
|
2
2
|
maxRetries?: number;
|
|
3
3
|
initialDelayMs?: number;
|
|
4
4
|
maxDelayMs?: number;
|
|
5
5
|
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
6
6
|
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
7
7
|
sleep?: (ms: number) => Promise<void>;
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
11
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
export function isServerError(status: number): boolean {
|
|
15
|
+
return status >= 500 || status === 408 || status === 429;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getErrorStatus(error: unknown): number | undefined {
|
|
19
|
+
if (typeof error !== "object" || error === null) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!("status" in error)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const status = (error as { status: unknown }).status;
|
|
28
|
+
if (typeof status !== "number") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return status;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isRetryableError(error: unknown, _attempt?: number): boolean {
|
|
15
36
|
if (error instanceof TypeError) {
|
|
16
37
|
return true;
|
|
17
38
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"status" in error &&
|
|
23
|
-
typeof (error as { status: unknown }).status === "number"
|
|
24
|
-
) {
|
|
25
|
-
const status = (error as { status: number }).status;
|
|
26
|
-
// Retry on 5xx server errors, 408 timeout, 429 rate limit
|
|
27
|
-
return status >= 500 || status === 408 || status === 429;
|
|
39
|
+
|
|
40
|
+
const status = getErrorStatus(error);
|
|
41
|
+
if (status === undefined) {
|
|
42
|
+
return false;
|
|
28
43
|
}
|
|
29
|
-
return false;
|
|
30
|
-
};
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
return isServerError(status);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function withRetry<T>(
|
|
33
49
|
fn: () => Promise<T>,
|
|
34
50
|
options: RetryOptions = {},
|
|
35
|
-
): Promise<T>
|
|
51
|
+
): Promise<T> {
|
|
36
52
|
const {
|
|
37
53
|
maxRetries = 3,
|
|
38
54
|
initialDelayMs = 1000,
|
|
@@ -62,7 +78,4 @@ export const withRetry = async <T>(
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
throw lastError;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const isServerError = (status: number): boolean =>
|
|
68
|
-
status >= 500 || status === 408 || status === 429;
|
|
81
|
+
}
|
package/src/core/http/url.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readConfig } from "../config";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function getApiBase(): string {
|
|
4
4
|
const raw = readConfig().apiBase || "";
|
|
5
5
|
return raw.replace(/\/+$/, "");
|
|
6
|
-
}
|
|
6
|
+
}
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export function buildApiUrl(path: string): string {
|
|
9
9
|
const base = getApiBase();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
11
|
+
|
|
12
|
+
if (base) {
|
|
13
|
+
return `${base}/${normalizedPath}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return `/${normalizedPath}`;
|
|
17
|
+
}
|
|
@@ -19,23 +19,23 @@ type CopyOptions = {
|
|
|
19
19
|
spawnFn?: SpawnLike;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export function getClipboardCommands(
|
|
23
23
|
platform: NodeJS.Platform,
|
|
24
|
-
): ClipboardCommand[]
|
|
24
|
+
): ClipboardCommand[] {
|
|
25
25
|
if (platform === "darwin") return [{ command: "pbcopy", args: [] }];
|
|
26
26
|
if (platform === "win32") return [{ command: "clip", args: [] }];
|
|
27
27
|
return [
|
|
28
28
|
{ command: "wl-copy", args: [] },
|
|
29
29
|
{ command: "xclip", args: ["-selection", "clipboard"] },
|
|
30
30
|
];
|
|
31
|
-
}
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function runClipboardCommand(
|
|
34
34
|
text: string,
|
|
35
35
|
command: ClipboardCommand,
|
|
36
36
|
spawnFn: SpawnLike,
|
|
37
|
-
): Promise<boolean>
|
|
38
|
-
new Promise((resolve) => {
|
|
37
|
+
): Promise<boolean> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
39
|
const child = spawnFn(command.command, command.args, {
|
|
40
40
|
stdio: ["pipe", "ignore", "ignore"],
|
|
41
41
|
});
|
|
@@ -44,11 +44,12 @@ const runClipboardCommand = (
|
|
|
44
44
|
child.stdin.write(text);
|
|
45
45
|
child.stdin.end();
|
|
46
46
|
});
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
export
|
|
49
|
+
export async function copyToClipboard(
|
|
49
50
|
text: string,
|
|
50
51
|
options: CopyOptions = {},
|
|
51
|
-
)
|
|
52
|
+
): Promise<boolean> {
|
|
52
53
|
if (!text) return false;
|
|
53
54
|
const platform = options.platform ?? process.platform;
|
|
54
55
|
const spawnFn = options.spawnFn ?? spawn;
|
|
@@ -58,4 +59,4 @@ export const copyToClipboard = async (
|
|
|
58
59
|
if (ok) return true;
|
|
59
60
|
}
|
|
60
61
|
return false;
|
|
61
|
-
}
|
|
62
|
+
}
|
|
@@ -126,10 +126,8 @@ export const promptKeyEnabled = async (
|
|
|
126
126
|
};
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
): Promise<routercommonv1_Consumer | null> => {
|
|
132
|
-
const consumers = await fetchAllPages(
|
|
129
|
+
const fetchConsumers = async (consumerService: ConsumerService) =>
|
|
130
|
+
fetchAllPages(
|
|
133
131
|
(pageToken) =>
|
|
134
132
|
consumerService.ListConsumers({
|
|
135
133
|
pageSize: undefined,
|
|
@@ -138,6 +136,8 @@ export const selectConsumer = async (
|
|
|
138
136
|
(res) => res?.consumers ?? [],
|
|
139
137
|
(res) => res?.nextPageToken || undefined,
|
|
140
138
|
);
|
|
139
|
+
|
|
140
|
+
const ensureConsumers = (consumers: Consumer[]) => {
|
|
141
141
|
if (consumers.length === 0) {
|
|
142
142
|
console.log(
|
|
143
143
|
"No available API keys. Create one at https://getrouter.dev/dashboard/keys",
|
|
@@ -145,11 +145,20 @@ export const selectConsumer = async (
|
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
147
|
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
148
|
-
|
|
148
|
+
return { sorted, nameCounts: buildNameCounts(sorted) };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const selectConsumer = async (
|
|
152
|
+
consumerService: ConsumerService,
|
|
153
|
+
): Promise<routercommonv1_Consumer | null> => {
|
|
154
|
+
const consumers = await fetchConsumers(consumerService);
|
|
155
|
+
const prepared = ensureConsumers(consumers);
|
|
156
|
+
if (!prepared) return null;
|
|
157
|
+
|
|
149
158
|
const selected = await fuzzySelect({
|
|
150
159
|
message: "๐ Search keys",
|
|
151
|
-
choices: sorted.map((consumer) => ({
|
|
152
|
-
title: formatChoice(consumer, nameCounts),
|
|
160
|
+
choices: prepared.sorted.map((consumer) => ({
|
|
161
|
+
title: formatChoice(consumer, prepared.nameCounts),
|
|
153
162
|
value: consumer,
|
|
154
163
|
keywords: [
|
|
155
164
|
normalizeName(consumer),
|
|
@@ -165,29 +174,16 @@ export const selectConsumerList = async (
|
|
|
165
174
|
consumerService: ConsumerService,
|
|
166
175
|
message: string,
|
|
167
176
|
): Promise<routercommonv1_Consumer | null> => {
|
|
168
|
-
const consumers = await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
pageToken,
|
|
173
|
-
}),
|
|
174
|
-
(res) => res?.consumers ?? [],
|
|
175
|
-
(res) => res?.nextPageToken || undefined,
|
|
176
|
-
);
|
|
177
|
-
if (consumers.length === 0) {
|
|
178
|
-
console.log(
|
|
179
|
-
"No available API keys. Create one at https://getrouter.dev/dashboard/keys",
|
|
180
|
-
);
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
const sorted = sortConsumersByUpdatedAtDesc(consumers);
|
|
184
|
-
const nameCounts = buildNameCounts(sorted);
|
|
177
|
+
const consumers = await fetchConsumers(consumerService);
|
|
178
|
+
const prepared = ensureConsumers(consumers);
|
|
179
|
+
if (!prepared) return null;
|
|
180
|
+
|
|
185
181
|
const response = await prompts({
|
|
186
182
|
type: "select",
|
|
187
183
|
name: "value",
|
|
188
184
|
message,
|
|
189
|
-
choices: sorted.map((consumer) => ({
|
|
190
|
-
title: formatChoice(consumer, nameCounts),
|
|
185
|
+
choices: prepared.sorted.map((consumer) => ({
|
|
186
|
+
title: formatChoice(consumer, prepared.nameCounts),
|
|
191
187
|
value: consumer,
|
|
192
188
|
})),
|
|
193
189
|
});
|
package/src/core/output/table.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
type TableOptions = { maxColWidth?: number };
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function truncate(value: string, max: number): string {
|
|
4
4
|
if (value.length <= max) return value;
|
|
5
5
|
if (max <= 3) return value.slice(0, max);
|
|
6
6
|
return `${value.slice(0, max - 3)}...`;
|
|
7
|
-
}
|
|
7
|
+
}
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function renderTable(
|
|
10
10
|
headers: string[],
|
|
11
11
|
rows: string[][],
|
|
12
12
|
options: TableOptions = {},
|
|
13
|
-
)
|
|
13
|
+
): string {
|
|
14
14
|
const maxColWidth = options.maxColWidth ?? 32;
|
|
15
15
|
const normalized = rows.map((row) =>
|
|
16
16
|
row.map((cell) => (cell && cell.length > 0 ? cell : "-")),
|
|
@@ -31,4 +31,4 @@ export const renderTable = (
|
|
|
31
31
|
const headerRow = renderRow(headers);
|
|
32
32
|
const body = normalized.map((row) => renderRow(row)).join("\n");
|
|
33
33
|
return `${headerRow}\n${body}`;
|
|
34
|
-
}
|
|
34
|
+
}
|
|
@@ -3,54 +3,55 @@ import type { AggregatedUsage } from "../usages/aggregate";
|
|
|
3
3
|
const TOTAL_BLOCK = "โ";
|
|
4
4
|
const DEFAULT_WIDTH = 24;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function formatTokens(value: number): string {
|
|
7
7
|
const abs = Math.abs(value);
|
|
8
8
|
if (abs < 1000) return Math.round(value).toString();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (output.endsWith(".0")) {
|
|
20
|
-
output = output.slice(0, -2);
|
|
21
|
-
}
|
|
22
|
-
return `${output}${unit.suffix}`;
|
|
23
|
-
}
|
|
9
|
+
|
|
10
|
+
let threshold = 1_000;
|
|
11
|
+
let suffix = "K";
|
|
12
|
+
|
|
13
|
+
if (abs >= 1_000_000_000) {
|
|
14
|
+
threshold = 1_000_000_000;
|
|
15
|
+
suffix = "B";
|
|
16
|
+
} else if (abs >= 1_000_000) {
|
|
17
|
+
threshold = 1_000_000;
|
|
18
|
+
suffix = "M";
|
|
24
19
|
}
|
|
25
|
-
return Math.round(value).toString();
|
|
26
|
-
};
|
|
27
20
|
|
|
28
|
-
|
|
21
|
+
const scaled = value / threshold;
|
|
22
|
+
const decimals = Math.abs(scaled) < 10 ? 1 : 0;
|
|
23
|
+
const output = scaled.toFixed(decimals).replace(/\.0$/, "");
|
|
24
|
+
return `${output}${suffix}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderUsageChart(
|
|
29
28
|
rows: AggregatedUsage[],
|
|
30
29
|
width = DEFAULT_WIDTH,
|
|
31
|
-
)
|
|
30
|
+
): string {
|
|
32
31
|
const header = "๐ Usage (last 7 days) ยท Tokens";
|
|
33
32
|
if (rows.length === 0) {
|
|
34
33
|
return `${header}\n\nNo usage data available.`;
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
35
|
+
|
|
36
|
+
const data = rows.map((row) => {
|
|
37
|
+
const rawTotal = Number(row.totalTokens);
|
|
39
38
|
return {
|
|
40
39
|
day: row.day,
|
|
41
|
-
total:
|
|
40
|
+
total: Number.isFinite(rawTotal) ? rawTotal : 0,
|
|
42
41
|
};
|
|
43
42
|
});
|
|
44
|
-
|
|
45
|
-
const maxTotal = Math.max(0, ...
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
|
|
44
|
+
const maxTotal = Math.max(0, ...data.map((d) => d.total));
|
|
45
|
+
|
|
46
|
+
const lines = data.map(({ day, total }) => {
|
|
47
|
+
if (maxTotal === 0 || total === 0) {
|
|
48
|
+
return `${day} ${" ".repeat(width)} 0`;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
|
|
51
|
+
const scaled = Math.max(1, Math.round((total / maxTotal) * width));
|
|
51
52
|
const bar = TOTAL_BLOCK.repeat(scaled);
|
|
52
|
-
|
|
53
|
-
return `${row.day} ${bar.padEnd(width, " ")} ${totalLabel}`;
|
|
53
|
+
return `${day} ${bar.padEnd(width, " ")} ${formatTokens(total)}`;
|
|
54
54
|
});
|
|
55
|
+
|
|
55
56
|
return [header, "", ...lines].join("\n");
|
|
56
|
-
}
|
|
57
|
+
}
|