trickle-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/api-client.d.ts +208 -0
  2. package/dist/api-client.js +237 -0
  3. package/dist/commands/annotate.d.ts +6 -0
  4. package/dist/commands/annotate.js +433 -0
  5. package/dist/commands/audit.d.ts +7 -0
  6. package/dist/commands/audit.js +82 -0
  7. package/dist/commands/auto.d.ts +8 -0
  8. package/dist/commands/auto.js +268 -0
  9. package/dist/commands/capture.d.ts +14 -0
  10. package/dist/commands/capture.js +271 -0
  11. package/dist/commands/check.d.ts +6 -0
  12. package/dist/commands/check.js +408 -0
  13. package/dist/commands/codegen.d.ts +21 -0
  14. package/dist/commands/codegen.js +129 -0
  15. package/dist/commands/coverage.d.ts +13 -0
  16. package/dist/commands/coverage.js +126 -0
  17. package/dist/commands/dashboard.d.ts +1 -0
  18. package/dist/commands/dashboard.js +83 -0
  19. package/dist/commands/dev.d.ts +14 -0
  20. package/dist/commands/dev.js +319 -0
  21. package/dist/commands/diff.d.ts +7 -0
  22. package/dist/commands/diff.js +79 -0
  23. package/dist/commands/docs.d.ts +13 -0
  24. package/dist/commands/docs.js +383 -0
  25. package/dist/commands/errors.d.ts +7 -0
  26. package/dist/commands/errors.js +180 -0
  27. package/dist/commands/export.d.ts +18 -0
  28. package/dist/commands/export.js +238 -0
  29. package/dist/commands/functions.d.ts +6 -0
  30. package/dist/commands/functions.js +71 -0
  31. package/dist/commands/infer.d.ts +14 -0
  32. package/dist/commands/infer.js +275 -0
  33. package/dist/commands/init.d.ts +5 -0
  34. package/dist/commands/init.js +395 -0
  35. package/dist/commands/mock.d.ts +5 -0
  36. package/dist/commands/mock.js +232 -0
  37. package/dist/commands/openapi.d.ts +8 -0
  38. package/dist/commands/openapi.js +82 -0
  39. package/dist/commands/overview.d.ts +11 -0
  40. package/dist/commands/overview.js +266 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.js +133 -0
  43. package/dist/commands/proxy.d.ts +13 -0
  44. package/dist/commands/proxy.js +312 -0
  45. package/dist/commands/replay.d.ts +14 -0
  46. package/dist/commands/replay.js +289 -0
  47. package/dist/commands/run.d.ts +17 -0
  48. package/dist/commands/run.js +997 -0
  49. package/dist/commands/sample.d.ts +13 -0
  50. package/dist/commands/sample.js +260 -0
  51. package/dist/commands/search.d.ts +5 -0
  52. package/dist/commands/search.js +80 -0
  53. package/dist/commands/stubs.d.ts +6 -0
  54. package/dist/commands/stubs.js +187 -0
  55. package/dist/commands/tail.d.ts +4 -0
  56. package/dist/commands/tail.js +76 -0
  57. package/dist/commands/test-gen.d.ts +13 -0
  58. package/dist/commands/test-gen.js +237 -0
  59. package/dist/commands/trace.d.ts +14 -0
  60. package/dist/commands/trace.js +417 -0
  61. package/dist/commands/types.d.ts +7 -0
  62. package/dist/commands/types.js +128 -0
  63. package/dist/commands/unpack.d.ts +11 -0
  64. package/dist/commands/unpack.js +166 -0
  65. package/dist/commands/validate.d.ts +13 -0
  66. package/dist/commands/validate.js +310 -0
  67. package/dist/commands/watch.d.ts +9 -0
  68. package/dist/commands/watch.js +267 -0
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.js +66 -0
  71. package/dist/formatters/diff-formatter.d.ts +5 -0
  72. package/dist/formatters/diff-formatter.js +43 -0
  73. package/dist/formatters/type-formatter.d.ts +22 -0
  74. package/dist/formatters/type-formatter.js +135 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.js +419 -0
  77. package/dist/local-codegen.d.ts +22 -0
  78. package/dist/local-codegen.js +762 -0
  79. package/dist/ui/badges.d.ts +16 -0
  80. package/dist/ui/badges.js +71 -0
  81. package/dist/ui/helpers.d.ts +13 -0
  82. package/dist/ui/helpers.js +85 -0
  83. package/package.json +23 -0
  84. package/src/api-client.ts +407 -0
  85. package/src/commands/annotate.ts +450 -0
  86. package/src/commands/audit.ts +103 -0
  87. package/src/commands/auto.ts +268 -0
  88. package/src/commands/capture.ts +257 -0
  89. package/src/commands/check.ts +437 -0
  90. package/src/commands/codegen.ts +128 -0
  91. package/src/commands/coverage.ts +170 -0
  92. package/src/commands/dashboard.ts +46 -0
  93. package/src/commands/dev.ts +323 -0
  94. package/src/commands/diff.ts +99 -0
  95. package/src/commands/docs.ts +392 -0
  96. package/src/commands/errors.ts +205 -0
  97. package/src/commands/export.ts +287 -0
  98. package/src/commands/functions.ts +81 -0
  99. package/src/commands/infer.ts +260 -0
  100. package/src/commands/init.ts +419 -0
  101. package/src/commands/mock.ts +220 -0
  102. package/src/commands/openapi.ts +53 -0
  103. package/src/commands/overview.ts +310 -0
  104. package/src/commands/pack.ts +139 -0
  105. package/src/commands/proxy.ts +314 -0
  106. package/src/commands/replay.ts +356 -0
  107. package/src/commands/run.ts +1190 -0
  108. package/src/commands/sample.ts +259 -0
  109. package/src/commands/search.ts +107 -0
  110. package/src/commands/stubs.ts +211 -0
  111. package/src/commands/tail.ts +94 -0
  112. package/src/commands/test-gen.ts +236 -0
  113. package/src/commands/trace.ts +440 -0
  114. package/src/commands/types.ts +161 -0
  115. package/src/commands/unpack.ts +179 -0
  116. package/src/commands/validate.ts +368 -0
  117. package/src/commands/watch.ts +277 -0
  118. package/src/config.ts +38 -0
  119. package/src/formatters/diff-formatter.ts +51 -0
  120. package/src/formatters/type-formatter.ts +161 -0
  121. package/src/index.ts +454 -0
  122. package/src/local-codegen.ts +859 -0
  123. package/src/ui/badges.ts +66 -0
  124. package/src/ui/helpers.ts +80 -0
  125. package/tsconfig.json +8 -0
@@ -0,0 +1,392 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import chalk from "chalk";
4
+ import { fetchMockConfig, fetchCodegen, listFunctions, MockRoute } from "../api-client";
5
+
6
+ export interface DocsOptions {
7
+ out?: string;
8
+ html?: boolean;
9
+ env?: string;
10
+ title?: string;
11
+ }
12
+
13
+ /**
14
+ * `trickle docs` — Generate API documentation from observed runtime types.
15
+ *
16
+ * Produces clean Markdown (or self-contained HTML) documenting every
17
+ * observed API route with request/response types and example payloads.
18
+ */
19
+ export async function docsCommand(opts: DocsOptions): Promise<void> {
20
+ const title = opts.title || "API Documentation";
21
+
22
+ // Fetch data
23
+ let routes: MockRoute[];
24
+ let typesContent: string;
25
+ let totalFunctions: number;
26
+ try {
27
+ const [mockConfig, codegen, funcList] = await Promise.all([
28
+ fetchMockConfig(),
29
+ fetchCodegen({ env: opts.env }),
30
+ listFunctions({ env: opts.env, limit: 500 }),
31
+ ]);
32
+ routes = mockConfig.routes;
33
+ typesContent = codegen.types;
34
+ totalFunctions = funcList.total;
35
+ } catch {
36
+ console.error(chalk.red("\n Cannot connect to trickle backend."));
37
+ console.error(chalk.gray(" Is the backend running?\n"));
38
+ process.exit(1);
39
+ }
40
+
41
+ if (routes.length === 0) {
42
+ console.error(chalk.yellow("\n No observed API routes to document."));
43
+ console.error(chalk.gray(" Instrument your app and make some requests first.\n"));
44
+ process.exit(0);
45
+ }
46
+
47
+ // Parse type definitions to map route names to their types
48
+ const typeMap = buildTypeMap(typesContent);
49
+
50
+ // Group routes by resource
51
+ const groups = groupRoutesByResource(routes);
52
+
53
+ // Generate markdown
54
+ const markdown = generateMarkdown(title, groups, typeMap, totalFunctions);
55
+
56
+ if (opts.html) {
57
+ const html = wrapInHtml(title, markdown);
58
+ if (opts.out) {
59
+ fs.writeFileSync(opts.out, html, "utf-8");
60
+ console.log(chalk.green(`\n API docs written to ${chalk.bold(opts.out)}`));
61
+ console.log(chalk.gray(` ${routes.length} routes documented\n`));
62
+ } else {
63
+ console.log(html);
64
+ }
65
+ } else {
66
+ if (opts.out) {
67
+ fs.writeFileSync(opts.out, markdown, "utf-8");
68
+ console.log(chalk.green(`\n API docs written to ${chalk.bold(opts.out)}`));
69
+ console.log(chalk.gray(` ${routes.length} routes documented\n`));
70
+ } else {
71
+ console.log(markdown);
72
+ }
73
+ }
74
+ }
75
+
76
+ interface RouteGroup {
77
+ resource: string;
78
+ routes: MockRoute[];
79
+ }
80
+
81
+ function groupRoutesByResource(routes: MockRoute[]): RouteGroup[] {
82
+ const groups: Record<string, MockRoute[]> = {};
83
+
84
+ for (const route of routes) {
85
+ const parts = route.path.split("/").filter(Boolean);
86
+ let resource: string;
87
+ if (parts[0] === "api" && parts.length >= 2) {
88
+ resource = `/${parts[0]}/${parts[1]}`;
89
+ } else {
90
+ resource = `/${parts[0] || "root"}`;
91
+ }
92
+
93
+ if (!groups[resource]) groups[resource] = [];
94
+ groups[resource].push(route);
95
+ }
96
+
97
+ // Sort groups alphabetically, routes by method order
98
+ const methodOrder: Record<string, number> = { GET: 0, POST: 1, PUT: 2, PATCH: 3, DELETE: 4 };
99
+ const result: RouteGroup[] = [];
100
+
101
+ for (const [resource, resourceRoutes] of Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0]))) {
102
+ resourceRoutes.sort((a, b) => {
103
+ const ma = methodOrder[a.method] ?? 5;
104
+ const mb = methodOrder[b.method] ?? 5;
105
+ if (ma !== mb) return ma - mb;
106
+ return a.path.localeCompare(b.path);
107
+ });
108
+ result.push({ resource, routes: resourceRoutes });
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Extract type definitions from generated TypeScript code and map them
116
+ * to route function names.
117
+ */
118
+ function buildTypeMap(typesContent: string): Map<string, string> {
119
+ const map = new Map<string, string>();
120
+ if (!typesContent) return map;
121
+
122
+ // Find interface/type blocks and their associated route comments
123
+ const blocks = typesContent.split(/(?=\/\*\*|export interface|export type)/);
124
+
125
+ let currentComment = "";
126
+ for (const block of blocks) {
127
+ if (block.startsWith("/**")) {
128
+ currentComment = block;
129
+ continue;
130
+ }
131
+
132
+ const match = block.match(/export (?:interface|type) (\w+)/);
133
+ if (match) {
134
+ const typeName = match[1];
135
+ // Clean up the block
136
+ const cleanBlock = block.trim();
137
+ if (cleanBlock) {
138
+ map.set(typeName, cleanBlock);
139
+ }
140
+ }
141
+ currentComment = "";
142
+ }
143
+
144
+ return map;
145
+ }
146
+
147
+ function toPascalCase(name: string): string {
148
+ return name
149
+ .replace(/[^a-zA-Z0-9]+/g, " ")
150
+ .split(/\s+/)
151
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
152
+ .join("");
153
+ }
154
+
155
+ function generateMarkdown(
156
+ title: string,
157
+ groups: RouteGroup[],
158
+ typeMap: Map<string, string>,
159
+ totalFunctions: number,
160
+ ): string {
161
+ const lines: string[] = [];
162
+ const now = new Date().toISOString().split("T")[0];
163
+
164
+ lines.push(`# ${title}`);
165
+ lines.push("");
166
+ lines.push(`> Auto-generated by [trickle](https://github.com/yiheinchai/trickle) from runtime-observed types.`);
167
+ lines.push(`> Generated on ${now} — ${totalFunctions} functions observed.`);
168
+ lines.push("");
169
+
170
+ // Table of contents
171
+ lines.push("## Table of Contents");
172
+ lines.push("");
173
+ for (const group of groups) {
174
+ const anchor = group.resource.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
175
+ lines.push(`- [${group.resource}](#${anchor})`);
176
+ }
177
+ lines.push("");
178
+ lines.push("---");
179
+ lines.push("");
180
+
181
+ // Route groups
182
+ for (const group of groups) {
183
+ lines.push(`## ${group.resource}`);
184
+ lines.push("");
185
+
186
+ for (const route of group.routes) {
187
+ const methodBadge = `\`${route.method}\``;
188
+ lines.push(`### ${methodBadge} ${route.path}`);
189
+ lines.push("");
190
+
191
+ // Metadata
192
+ const observed = route.observedAt ? new Date(route.observedAt).toISOString().split("T")[0] : "unknown";
193
+ lines.push(`*Last observed: ${observed}*`);
194
+ lines.push("");
195
+
196
+ // Request body
197
+ const hasBody = ["POST", "PUT", "PATCH"].includes(route.method);
198
+ if (hasBody && route.sampleInput) {
199
+ const input = route.sampleInput as Record<string, unknown>;
200
+ const body = input.body || input;
201
+ if (body && typeof body === "object" && Object.keys(body as object).length > 0) {
202
+ lines.push("**Request Body**");
203
+ lines.push("");
204
+ lines.push("```typescript");
205
+ lines.push(formatTypeFromSample(body));
206
+ lines.push("```");
207
+ lines.push("");
208
+ lines.push("<details>");
209
+ lines.push("<summary>Example</summary>");
210
+ lines.push("");
211
+ lines.push("```json");
212
+ lines.push(JSON.stringify(body, null, 2));
213
+ lines.push("```");
214
+ lines.push("</details>");
215
+ lines.push("");
216
+ }
217
+ }
218
+
219
+ // Response
220
+ if (route.sampleOutput) {
221
+ // Try to find the TypeScript type
222
+ const typeName = toPascalCase(route.functionName);
223
+ const responseTypeName = typeName + "Response";
224
+ const typeBlock = typeMap.get(responseTypeName);
225
+
226
+ lines.push("**Response**");
227
+ lines.push("");
228
+
229
+ if (typeBlock) {
230
+ lines.push("```typescript");
231
+ lines.push(typeBlock);
232
+ lines.push("```");
233
+ } else {
234
+ lines.push("```typescript");
235
+ lines.push(formatTypeFromSample(route.sampleOutput));
236
+ lines.push("```");
237
+ }
238
+ lines.push("");
239
+
240
+ lines.push("<details>");
241
+ lines.push("<summary>Example Response</summary>");
242
+ lines.push("");
243
+ lines.push("```json");
244
+ lines.push(JSON.stringify(route.sampleOutput, null, 2));
245
+ lines.push("```");
246
+ lines.push("</details>");
247
+ lines.push("");
248
+ }
249
+
250
+ lines.push("---");
251
+ lines.push("");
252
+ }
253
+ }
254
+
255
+ // Footer
256
+ lines.push("*Generated by trickle — runtime type observability for JavaScript and Python.*");
257
+ lines.push("");
258
+
259
+ return lines.join("\n");
260
+ }
261
+
262
+ /**
263
+ * Infer a TypeScript type string from a sample JSON value.
264
+ */
265
+ function formatTypeFromSample(value: unknown, indent: number = 0): string {
266
+ const pad = " ".repeat(indent);
267
+
268
+ if (value === null) return `${pad}null`;
269
+ if (value === undefined) return `${pad}undefined`;
270
+
271
+ switch (typeof value) {
272
+ case "string": return `${pad}string`;
273
+ case "number": return `${pad}number`;
274
+ case "boolean": return `${pad}boolean`;
275
+ }
276
+
277
+ if (Array.isArray(value)) {
278
+ if (value.length === 0) return `${pad}unknown[]`;
279
+ const elementType = formatTypeFromSample(value[0], 0);
280
+ if (elementType.includes("\n")) {
281
+ // Multi-line element
282
+ return `${pad}Array<${formatTypeFromSample(value[0], indent).trimStart()}>`;
283
+ }
284
+ return `${pad}${elementType.trim()}[]`;
285
+ }
286
+
287
+ if (typeof value === "object") {
288
+ const obj = value as Record<string, unknown>;
289
+ const keys = Object.keys(obj);
290
+ if (keys.length === 0) return `${pad}{}`;
291
+
292
+ const lines: string[] = [];
293
+ lines.push(`${pad}{`);
294
+ for (const key of keys) {
295
+ const val = obj[key];
296
+ const valType = formatTypeFromSample(val, indent + 1).trimStart();
297
+ lines.push(`${pad} ${key}: ${valType};`);
298
+ }
299
+ lines.push(`${pad}}`);
300
+ return lines.join("\n");
301
+ }
302
+
303
+ return `${pad}unknown`;
304
+ }
305
+
306
+ /**
307
+ * Wrap markdown in a self-contained HTML document with a simple renderer.
308
+ */
309
+ function wrapInHtml(title: string, markdown: string): string {
310
+ // Escape for embedding in JS
311
+ const escapedMd = markdown
312
+ .replace(/\\/g, "\\\\")
313
+ .replace(/`/g, "\\`")
314
+ .replace(/\$/g, "\\$");
315
+
316
+ return `<!DOCTYPE html>
317
+ <html lang="en">
318
+ <head>
319
+ <meta charset="UTF-8">
320
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
321
+ <title>${escapeHtml(title)}</title>
322
+ <style>
323
+ * { margin: 0; padding: 0; box-sizing: border-box; }
324
+ body {
325
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
326
+ line-height: 1.6; color: #1a1a2e; background: #fafafa;
327
+ max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem;
328
+ }
329
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }
330
+ h2 { font-size: 1.5rem; margin-top: 2rem; margin-bottom: 0.75rem; color: #2d3748; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3rem; }
331
+ h3 { font-size: 1.1rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
332
+ p { margin-bottom: 0.75rem; }
333
+ blockquote { border-left: 3px solid #cbd5e0; padding-left: 1rem; color: #718096; margin-bottom: 1rem; }
334
+ code { background: #f0f0f0; padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.9em; }
335
+ pre { background: #1a1a2e; color: #e2e8f0; padding: 1rem; border-radius: 6px; overflow-x: auto; margin-bottom: 1rem; }
336
+ pre code { background: none; padding: 0; color: inherit; }
337
+ hr { border: none; border-top: 1px solid #e2e8f0; margin: 1.5rem 0; }
338
+ em { color: #718096; }
339
+ strong { color: #2d3748; }
340
+ ul { padding-left: 1.5rem; margin-bottom: 0.75rem; }
341
+ li { margin-bottom: 0.25rem; }
342
+ a { color: #3182ce; text-decoration: none; }
343
+ a:hover { text-decoration: underline; }
344
+ details { margin-bottom: 1rem; }
345
+ summary { cursor: pointer; color: #3182ce; font-weight: 500; margin-bottom: 0.5rem; }
346
+ summary:hover { text-decoration: underline; }
347
+ /* Method badges */
348
+ code:first-child { font-weight: bold; }
349
+ </style>
350
+ </head>
351
+ <body>
352
+ <div id="content"></div>
353
+ <script>
354
+ // Simple markdown renderer
355
+ function renderMd(md) {
356
+ let html = md;
357
+ // Code blocks (fenced)
358
+ html = html.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g, '<pre><code class="lang-$1">$2</code></pre>');
359
+ // Inline code
360
+ html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
361
+ // Headers
362
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
363
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
364
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
365
+ // Blockquotes
366
+ html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
367
+ // Bold
368
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
369
+ // Italic
370
+ html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
371
+ // Links
372
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
373
+ // Unordered lists
374
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
375
+ html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
376
+ // HR
377
+ html = html.replace(/^---$/gm, '<hr>');
378
+ // Details/summary (passthrough)
379
+ // Paragraphs
380
+ html = html.replace(/^(?!<[hupbold]|<li|<ul|<hr|<details|<summary|<\\/|<pre|<blockquote)(.+)$/gm, '<p>$1</p>');
381
+ return html;
382
+ }
383
+ const md = \`${escapedMd}\`;
384
+ document.getElementById('content').innerHTML = renderMd(md);
385
+ </script>
386
+ </body>
387
+ </html>`;
388
+ }
389
+
390
+ function escapeHtml(str: string): string {
391
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
392
+ }
@@ -0,0 +1,205 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { listErrors, getError, listFunctions, listTypes } from "../api-client";
4
+ import { envBadge, errorTypeBadge, timeBadge } from "../ui/badges";
5
+ import { parseSince, truncate, relativeTime } from "../ui/helpers";
6
+ import { formatType } from "../formatters/type-formatter";
7
+
8
+ export interface ErrorsListOptions {
9
+ env?: string;
10
+ since?: string;
11
+ function?: string;
12
+ limit?: string;
13
+ }
14
+
15
+ export async function errorsCommand(idOrUndefined: string | undefined, opts: ErrorsListOptions): Promise<void> {
16
+ // If an ID was provided, show detail mode
17
+ if (idOrUndefined !== undefined) {
18
+ const id = parseInt(idOrUndefined, 10);
19
+ if (isNaN(id)) {
20
+ console.error(chalk.red(`\n Invalid error ID: "${idOrUndefined}"\n`));
21
+ process.exit(1);
22
+ }
23
+ await showErrorDetail(id);
24
+ return;
25
+ }
26
+
27
+ // List mode
28
+ let sinceIso: string | undefined;
29
+ if (opts.since) {
30
+ try {
31
+ sinceIso = parseSince(opts.since);
32
+ } catch (err: unknown) {
33
+ if (err instanceof Error) {
34
+ console.error(chalk.red(`\n ${err.message}\n`));
35
+ }
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ const limit = opts.limit ? parseInt(opts.limit, 10) : undefined;
41
+
42
+ const result = await listErrors({
43
+ env: opts.env,
44
+ functionName: opts.function,
45
+ since: sinceIso,
46
+ limit,
47
+ });
48
+
49
+ const { errors } = result;
50
+
51
+ if (errors.length === 0) {
52
+ console.log(chalk.yellow("\n No errors found.\n"));
53
+ if (opts.env || opts.since || opts.function) {
54
+ console.log(chalk.gray(" Try adjusting your filters.\n"));
55
+ }
56
+ return;
57
+ }
58
+
59
+ console.log("");
60
+
61
+ const table = new Table({
62
+ head: [
63
+ chalk.cyan.bold("ID"),
64
+ chalk.cyan.bold("Function"),
65
+ chalk.cyan.bold("Error Type"),
66
+ chalk.cyan.bold("Message"),
67
+ chalk.cyan.bold("Env"),
68
+ chalk.cyan.bold("Time"),
69
+ ],
70
+ style: {
71
+ head: [],
72
+ border: ["gray"],
73
+ },
74
+ colWidths: [8, 22, 18, 36, 12, 12],
75
+ wordWrap: true,
76
+ chars: {
77
+ top: "─",
78
+ "top-mid": "┬",
79
+ "top-left": "┌",
80
+ "top-right": "┐",
81
+ bottom: "─",
82
+ "bottom-mid": "┴",
83
+ "bottom-left": "└",
84
+ "bottom-right": "┘",
85
+ left: "│",
86
+ "left-mid": "├",
87
+ mid: "─",
88
+ "mid-mid": "┼",
89
+ right: "│",
90
+ "right-mid": "┤",
91
+ middle: "│",
92
+ },
93
+ });
94
+
95
+ for (const err of errors) {
96
+ table.push([
97
+ chalk.gray(String(err.id)),
98
+ chalk.white(truncate(err.function_name || "", 20)),
99
+ errorTypeBadge(err.error_type),
100
+ truncate(err.error_message, 34),
101
+ envBadge(err.env),
102
+ timeBadge(err.occurred_at),
103
+ ]);
104
+ }
105
+
106
+ console.log(table.toString());
107
+ console.log(
108
+ chalk.gray(`\n Showing ${chalk.white.bold(String(errors.length))} errors`) +
109
+ (result.total > errors.length
110
+ ? chalk.gray(` of ${result.total} total`)
111
+ : "") +
112
+ "\n"
113
+ );
114
+ }
115
+
116
+ async function showErrorDetail(id: number): Promise<void> {
117
+ const result = await getError(id);
118
+ const { error: err, snapshot } = result;
119
+
120
+ console.log("");
121
+ console.log(chalk.red.bold(" ━━━ Error Detail ━━━"));
122
+ console.log("");
123
+
124
+ // Error header
125
+ console.log(` ${errorTypeBadge(err.error_type)} ${envBadge(err.env)}`);
126
+ console.log("");
127
+ console.log(chalk.white.bold(` ${err.error_message}`));
128
+ console.log(chalk.gray(` ${relativeTime(err.occurred_at)} (${err.occurred_at})`));
129
+
130
+ // Stack trace
131
+ if (err.stack_trace) {
132
+ console.log("");
133
+ console.log(chalk.gray(" ── Stack Trace ──"));
134
+ console.log("");
135
+ const lines = err.stack_trace.split("\n");
136
+ for (const line of lines) {
137
+ if (line.trim().startsWith("at ")) {
138
+ console.log(chalk.gray(` ${line.trim()}`));
139
+ } else {
140
+ console.log(chalk.red(` ${line.trim()}`));
141
+ }
142
+ }
143
+ }
144
+
145
+ // Type context
146
+ console.log("");
147
+ console.log(chalk.cyan.bold(" ── Type Context at Point of Failure ──"));
148
+ console.log("");
149
+
150
+ console.log(chalk.gray(" Function: ") + chalk.white.bold(err.function_name || `id:${err.function_id}`));
151
+ console.log(chalk.gray(" Module: ") + chalk.white(err.module || "unknown"));
152
+
153
+ if (err.args_type) {
154
+ console.log("");
155
+ console.log(chalk.gray(" Input types:"));
156
+ console.log(` ${formatType(err.args_type, 4)}`);
157
+ }
158
+
159
+ if (err.return_type) {
160
+ console.log("");
161
+ console.log(chalk.gray(" Return type:"));
162
+ console.log(` ${formatType(err.return_type, 4)}`);
163
+ }
164
+
165
+ if (err.args_snapshot !== undefined && err.args_snapshot !== null) {
166
+ console.log("");
167
+ console.log(chalk.gray(" Sample data:"));
168
+ const json = JSON.stringify(err.args_snapshot, null, 2);
169
+ if (json) {
170
+ const lines = json.split("\n");
171
+ for (const line of lines) {
172
+ const colored = line
173
+ .replace(/"([^"]+)":/g, (_, key) => `${chalk.white(`"${key}"`)}:`)
174
+ .replace(/: "([^"]*)"/g, (_, val) => `: ${chalk.green(`"${val}"`)}`)
175
+ .replace(/: (\d+\.?\d*)/g, (_, val) => `: ${chalk.yellow(val)}`)
176
+ .replace(/: (true|false)/g, (_, val) => `: ${chalk.blue(val)}`)
177
+ .replace(/: (null)/g, (_, val) => `: ${chalk.gray(val)}`);
178
+ console.log(` ${colored}`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Expected types (from the latest non-error snapshot)
184
+ if (snapshot) {
185
+ console.log("");
186
+ console.log(chalk.green.bold(" ── Expected Types (Happy Path) ──"));
187
+ console.log("");
188
+
189
+ console.log(chalk.gray(" Last successful snapshot:") + ` ${envBadge(snapshot.env as string)} ${timeBadge(snapshot.observed_at as string)}`);
190
+
191
+ if (snapshot.args_type) {
192
+ console.log("");
193
+ console.log(chalk.gray(" Expected input types:"));
194
+ console.log(` ${formatType(snapshot.args_type, 4)}`);
195
+ }
196
+
197
+ if (snapshot.return_type) {
198
+ console.log("");
199
+ console.log(chalk.gray(" Expected return type:"));
200
+ console.log(` ${formatType(snapshot.return_type, 4)}`);
201
+ }
202
+ }
203
+
204
+ console.log("");
205
+ }