@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.
@@ -1,38 +1,54 @@
1
- export type RetryOptions = {
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
- const defaultSleep = (ms: number) =>
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
- const isRetryableError = (error: unknown): boolean => {
14
- // Network errors (fetch failures)
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
- // Errors with status codes
19
- if (
20
- typeof error === "object" &&
21
- error !== null &&
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
- export const withRetry = async <T>(
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
+ }
@@ -1,12 +1,17 @@
1
1
  import { readConfig } from "../config";
2
2
 
3
- export const getApiBase = () => {
3
+ export function getApiBase(): string {
4
4
  const raw = readConfig().apiBase || "";
5
5
  return raw.replace(/\/+$/, "");
6
- };
6
+ }
7
7
 
8
- export const buildApiUrl = (path: string) => {
8
+ export function buildApiUrl(path: string): string {
9
9
  const base = getApiBase();
10
- const normalized = path.replace(/^\/+/, "");
11
- return base ? `${base}/${normalized}` : `/${normalized}`;
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 const getClipboardCommands = (
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
- const runClipboardCommand = (
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 const copyToClipboard = async (
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
- export const selectConsumer = async (
130
- consumerService: ConsumerService,
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
- const nameCounts = buildNameCounts(sorted);
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 fetchAllPages(
169
- (pageToken) =>
170
- consumerService.ListConsumers({
171
- pageSize: undefined,
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
  });
@@ -1,16 +1,16 @@
1
1
  type TableOptions = { maxColWidth?: number };
2
2
 
3
- const truncate = (value: string, max: number) => {
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 const renderTable = (
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
- const formatTokens = (value: number) => {
6
+ function formatTokens(value: number): string {
7
7
  const abs = Math.abs(value);
8
8
  if (abs < 1000) return Math.round(value).toString();
9
- const units = [
10
- { threshold: 1_000_000_000, suffix: "B" },
11
- { threshold: 1_000_000, suffix: "M" },
12
- { threshold: 1_000, suffix: "K" },
13
- ];
14
- for (const unit of units) {
15
- if (abs >= unit.threshold) {
16
- const scaled = value / unit.threshold;
17
- const decimals = Math.abs(scaled) < 10 ? 1 : 0;
18
- let output = scaled.toFixed(decimals);
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
- export const renderUsageChart = (
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
- const normalized = rows.map((row) => {
37
- const total = Number(row.totalTokens);
38
- const safeTotal = Number.isFinite(total) ? total : 0;
35
+
36
+ const data = rows.map((row) => {
37
+ const rawTotal = Number(row.totalTokens);
39
38
  return {
40
39
  day: row.day,
41
- total: safeTotal,
40
+ total: Number.isFinite(rawTotal) ? rawTotal : 0,
42
41
  };
43
42
  });
44
- const totals = normalized.map((row) => row.total);
45
- const maxTotal = Math.max(0, ...totals);
46
- const lines = normalized.map((row) => {
47
- if (maxTotal === 0 || row.total === 0) {
48
- return `${row.day} ${"".padEnd(width, " ")} 0`;
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
- const scaled = Math.max(1, Math.round((row.total / maxTotal) * width));
50
+
51
+ const scaled = Math.max(1, Math.round((total / maxTotal) * width));
51
52
  const bar = TOTAL_BLOCK.repeat(scaled);
52
- const totalLabel = formatTokens(row.total);
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
+ }