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.
- package/dist/api-client.d.ts +208 -0
- package/dist/api-client.js +237 -0
- package/dist/commands/annotate.d.ts +6 -0
- package/dist/commands/annotate.js +433 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/auto.d.ts +8 -0
- package/dist/commands/auto.js +268 -0
- package/dist/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +271 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +408 -0
- package/dist/commands/codegen.d.ts +21 -0
- package/dist/commands/codegen.js +129 -0
- package/dist/commands/coverage.d.ts +13 -0
- package/dist/commands/coverage.js +126 -0
- package/dist/commands/dashboard.d.ts +1 -0
- package/dist/commands/dashboard.js +83 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.js +319 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.js +79 -0
- package/dist/commands/docs.d.ts +13 -0
- package/dist/commands/docs.js +383 -0
- package/dist/commands/errors.d.ts +7 -0
- package/dist/commands/errors.js +180 -0
- package/dist/commands/export.d.ts +18 -0
- package/dist/commands/export.js +238 -0
- package/dist/commands/functions.d.ts +6 -0
- package/dist/commands/functions.js +71 -0
- package/dist/commands/infer.d.ts +14 -0
- package/dist/commands/infer.js +275 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +395 -0
- package/dist/commands/mock.d.ts +5 -0
- package/dist/commands/mock.js +232 -0
- package/dist/commands/openapi.d.ts +8 -0
- package/dist/commands/openapi.js +82 -0
- package/dist/commands/overview.d.ts +11 -0
- package/dist/commands/overview.js +266 -0
- package/dist/commands/pack.d.ts +11 -0
- package/dist/commands/pack.js +133 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +312 -0
- package/dist/commands/replay.d.ts +14 -0
- package/dist/commands/replay.js +289 -0
- package/dist/commands/run.d.ts +17 -0
- package/dist/commands/run.js +997 -0
- package/dist/commands/sample.d.ts +13 -0
- package/dist/commands/sample.js +260 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +80 -0
- package/dist/commands/stubs.d.ts +6 -0
- package/dist/commands/stubs.js +187 -0
- package/dist/commands/tail.d.ts +4 -0
- package/dist/commands/tail.js +76 -0
- package/dist/commands/test-gen.d.ts +13 -0
- package/dist/commands/test-gen.js +237 -0
- package/dist/commands/trace.d.ts +14 -0
- package/dist/commands/trace.js +417 -0
- package/dist/commands/types.d.ts +7 -0
- package/dist/commands/types.js +128 -0
- package/dist/commands/unpack.d.ts +11 -0
- package/dist/commands/unpack.js +166 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +310 -0
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.js +267 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +66 -0
- package/dist/formatters/diff-formatter.d.ts +5 -0
- package/dist/formatters/diff-formatter.js +43 -0
- package/dist/formatters/type-formatter.d.ts +22 -0
- package/dist/formatters/type-formatter.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +419 -0
- package/dist/local-codegen.d.ts +22 -0
- package/dist/local-codegen.js +762 -0
- package/dist/ui/badges.d.ts +16 -0
- package/dist/ui/badges.js +71 -0
- package/dist/ui/helpers.d.ts +13 -0
- package/dist/ui/helpers.js +85 -0
- package/package.json +23 -0
- package/src/api-client.ts +407 -0
- package/src/commands/annotate.ts +450 -0
- package/src/commands/audit.ts +103 -0
- package/src/commands/auto.ts +268 -0
- package/src/commands/capture.ts +257 -0
- package/src/commands/check.ts +437 -0
- package/src/commands/codegen.ts +128 -0
- package/src/commands/coverage.ts +170 -0
- package/src/commands/dashboard.ts +46 -0
- package/src/commands/dev.ts +323 -0
- package/src/commands/diff.ts +99 -0
- package/src/commands/docs.ts +392 -0
- package/src/commands/errors.ts +205 -0
- package/src/commands/export.ts +287 -0
- package/src/commands/functions.ts +81 -0
- package/src/commands/infer.ts +260 -0
- package/src/commands/init.ts +419 -0
- package/src/commands/mock.ts +220 -0
- package/src/commands/openapi.ts +53 -0
- package/src/commands/overview.ts +310 -0
- package/src/commands/pack.ts +139 -0
- package/src/commands/proxy.ts +314 -0
- package/src/commands/replay.ts +356 -0
- package/src/commands/run.ts +1190 -0
- package/src/commands/sample.ts +259 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/stubs.ts +211 -0
- package/src/commands/tail.ts +94 -0
- package/src/commands/test-gen.ts +236 -0
- package/src/commands/trace.ts +440 -0
- package/src/commands/types.ts +161 -0
- package/src/commands/unpack.ts +179 -0
- package/src/commands/validate.ts +368 -0
- package/src/commands/watch.ts +277 -0
- package/src/config.ts +38 -0
- package/src/formatters/diff-formatter.ts +51 -0
- package/src/formatters/type-formatter.ts +161 -0
- package/src/index.ts +454 -0
- package/src/local-codegen.ts +859 -0
- package/src/ui/badges.ts +66 -0
- package/src/ui/helpers.ts +80 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
}
|