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,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.testGenCommand = testGenCommand;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const api_client_1 = require("../api-client");
|
|
44
|
+
/**
|
|
45
|
+
* `trickle test --generate` — Generate API test files from runtime observations.
|
|
46
|
+
*
|
|
47
|
+
* Uses real sample request/response data captured at runtime to generate
|
|
48
|
+
* ready-to-run test files with correct endpoints, request bodies, and
|
|
49
|
+
* response shape assertions.
|
|
50
|
+
*/
|
|
51
|
+
async function testGenCommand(opts) {
|
|
52
|
+
const framework = opts.framework || "vitest";
|
|
53
|
+
const baseUrl = opts.baseUrl || "http://localhost:3000";
|
|
54
|
+
if (framework !== "vitest" && framework !== "jest") {
|
|
55
|
+
console.error(chalk_1.default.red(`\n Unsupported framework: ${framework}`));
|
|
56
|
+
console.error(chalk_1.default.gray(" Supported: vitest, jest\n"));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { routes } = await (0, api_client_1.fetchMockConfig)();
|
|
61
|
+
if (routes.length === 0) {
|
|
62
|
+
console.error(chalk_1.default.yellow("\n No API routes observed yet."));
|
|
63
|
+
console.error(chalk_1.default.gray(" Instrument your app and make some requests first.\n"));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const testCode = generateTestFile(routes, framework, baseUrl);
|
|
67
|
+
if (opts.out) {
|
|
68
|
+
const resolvedPath = path.resolve(opts.out);
|
|
69
|
+
const dir = path.dirname(resolvedPath);
|
|
70
|
+
if (!fs.existsSync(dir)) {
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(resolvedPath, testCode, "utf-8");
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log(chalk_1.default.green(` Tests written to ${chalk_1.default.bold(opts.out)}`));
|
|
76
|
+
console.log(chalk_1.default.gray(` ${routes.length} route tests generated (${framework})`));
|
|
77
|
+
console.log(chalk_1.default.gray(` Run with: npx ${framework === "vitest" ? "vitest run" : "jest"} ${opts.out}`));
|
|
78
|
+
console.log("");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(testCode);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err instanceof Error) {
|
|
87
|
+
console.error(chalk_1.default.red(`\n Error: ${err.message}\n`));
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function generateTestFile(routes, framework, baseUrl) {
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push("// Auto-generated API tests by trickle");
|
|
95
|
+
lines.push(`// Generated at ${new Date().toISOString()}`);
|
|
96
|
+
lines.push("// Do not edit manually — re-run `trickle test --generate` to update");
|
|
97
|
+
lines.push("");
|
|
98
|
+
// Import block
|
|
99
|
+
if (framework === "vitest") {
|
|
100
|
+
lines.push('import { describe, it, expect } from "vitest";');
|
|
101
|
+
}
|
|
102
|
+
// jest needs no import — globals are available
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(`const BASE_URL = process.env.TEST_API_URL || "${baseUrl}";`);
|
|
105
|
+
lines.push("");
|
|
106
|
+
// Group routes by resource path prefix
|
|
107
|
+
const groups = groupByResource(routes);
|
|
108
|
+
for (const [resource, resourceRoutes] of Object.entries(groups)) {
|
|
109
|
+
lines.push(`describe("${resource}", () => {`);
|
|
110
|
+
for (const route of resourceRoutes) {
|
|
111
|
+
const testName = `${route.method} ${route.path}`;
|
|
112
|
+
const hasBody = ["POST", "PUT", "PATCH"].includes(route.method);
|
|
113
|
+
lines.push(` it("${testName} — returns expected shape", async () => {`);
|
|
114
|
+
// Build fetch call
|
|
115
|
+
const fetchPath = route.path.replace(/:(\w+)/g, (_, param) => {
|
|
116
|
+
// Use sample data to get a real param value if available
|
|
117
|
+
const sampleValue = extractParamFromSample(route.sampleInput, param);
|
|
118
|
+
return sampleValue || `test-${param}`;
|
|
119
|
+
});
|
|
120
|
+
lines.push(` const res = await fetch(\`\${BASE_URL}${fetchPath}\`, {`);
|
|
121
|
+
lines.push(` method: "${route.method}",`);
|
|
122
|
+
if (hasBody && route.sampleInput) {
|
|
123
|
+
const bodyData = extractBodyFromSample(route.sampleInput);
|
|
124
|
+
if (bodyData && Object.keys(bodyData).length > 0) {
|
|
125
|
+
lines.push(` headers: { "Content-Type": "application/json" },`);
|
|
126
|
+
lines.push(` body: JSON.stringify(${JSON.stringify(bodyData, null, 6).replace(/\n/g, "\n ")}),`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
lines.push(" });");
|
|
130
|
+
lines.push("");
|
|
131
|
+
// Status assertion
|
|
132
|
+
lines.push(" expect(res.ok).toBe(true);");
|
|
133
|
+
lines.push(` expect(res.status).toBe(200);`);
|
|
134
|
+
lines.push("");
|
|
135
|
+
// Response body assertions
|
|
136
|
+
lines.push(" const body = await res.json();");
|
|
137
|
+
if (route.sampleOutput && typeof route.sampleOutput === "object") {
|
|
138
|
+
const assertions = generateAssertions(route.sampleOutput, "body");
|
|
139
|
+
for (const assertion of assertions) {
|
|
140
|
+
lines.push(` ${assertion}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push(" });");
|
|
144
|
+
lines.push("");
|
|
145
|
+
}
|
|
146
|
+
lines.push("});");
|
|
147
|
+
lines.push("");
|
|
148
|
+
}
|
|
149
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Group routes by their first meaningful path segment.
|
|
153
|
+
*/
|
|
154
|
+
function groupByResource(routes) {
|
|
155
|
+
const groups = {};
|
|
156
|
+
for (const route of routes) {
|
|
157
|
+
const parts = route.path.split("/").filter(Boolean);
|
|
158
|
+
// /api/users → "api/users", /users → "users"
|
|
159
|
+
let resource;
|
|
160
|
+
if (parts[0] === "api" && parts.length >= 2) {
|
|
161
|
+
resource = `/api/${parts[1]}`;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
resource = `/${parts[0] || "root"}`;
|
|
165
|
+
}
|
|
166
|
+
if (!groups[resource])
|
|
167
|
+
groups[resource] = [];
|
|
168
|
+
groups[resource].push(route);
|
|
169
|
+
}
|
|
170
|
+
return groups;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Try to extract a path param value from sample input.
|
|
174
|
+
*/
|
|
175
|
+
function extractParamFromSample(sampleInput, param) {
|
|
176
|
+
if (!sampleInput || typeof sampleInput !== "object")
|
|
177
|
+
return null;
|
|
178
|
+
const input = sampleInput;
|
|
179
|
+
// Check params object
|
|
180
|
+
if (input.params && typeof input.params === "object") {
|
|
181
|
+
const params = input.params;
|
|
182
|
+
if (params[param] !== undefined)
|
|
183
|
+
return String(params[param]);
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Extract request body from sample input.
|
|
189
|
+
*/
|
|
190
|
+
function extractBodyFromSample(sampleInput) {
|
|
191
|
+
if (!sampleInput || typeof sampleInput !== "object")
|
|
192
|
+
return null;
|
|
193
|
+
const input = sampleInput;
|
|
194
|
+
if (input.body && typeof input.body === "object") {
|
|
195
|
+
return input.body;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate expect() assertions for a sample response object.
|
|
201
|
+
* Checks structure (property existence and types), not exact values.
|
|
202
|
+
*/
|
|
203
|
+
function generateAssertions(obj, path, depth = 0) {
|
|
204
|
+
if (depth > 3)
|
|
205
|
+
return []; // Prevent deeply nested assertions
|
|
206
|
+
const assertions = [];
|
|
207
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
208
|
+
const propPath = `${path}.${key}`;
|
|
209
|
+
if (value === null) {
|
|
210
|
+
assertions.push(`expect(${propPath}).toBeNull();`);
|
|
211
|
+
}
|
|
212
|
+
else if (Array.isArray(value)) {
|
|
213
|
+
assertions.push(`expect(Array.isArray(${propPath})).toBe(true);`);
|
|
214
|
+
// If array has items, assert shape of first element
|
|
215
|
+
if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
|
|
216
|
+
assertions.push(`expect(${propPath}.length).toBeGreaterThan(0);`);
|
|
217
|
+
const itemAssertions = generateAssertions(value[0], `${propPath}[0]`, depth + 1);
|
|
218
|
+
assertions.push(...itemAssertions);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (typeof value === "object") {
|
|
222
|
+
assertions.push(`expect(typeof ${propPath}).toBe("object");`);
|
|
223
|
+
const nestedAssertions = generateAssertions(value, propPath, depth + 1);
|
|
224
|
+
assertions.push(...nestedAssertions);
|
|
225
|
+
}
|
|
226
|
+
else if (typeof value === "string") {
|
|
227
|
+
assertions.push(`expect(typeof ${propPath}).toBe("string");`);
|
|
228
|
+
}
|
|
229
|
+
else if (typeof value === "number") {
|
|
230
|
+
assertions.push(`expect(typeof ${propPath}).toBe("number");`);
|
|
231
|
+
}
|
|
232
|
+
else if (typeof value === "boolean") {
|
|
233
|
+
assertions.push(`expect(typeof ${propPath}).toBe("boolean");`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return assertions;
|
|
237
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TraceOptions {
|
|
2
|
+
header?: string[];
|
|
3
|
+
body?: string;
|
|
4
|
+
save?: boolean;
|
|
5
|
+
env?: string;
|
|
6
|
+
module?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `trickle trace <method> <url>` — Make an HTTP request and show the response
|
|
10
|
+
* with inline type annotations. Like curl but type-aware.
|
|
11
|
+
*
|
|
12
|
+
* Optionally saves the types to the backend with --save.
|
|
13
|
+
*/
|
|
14
|
+
export declare function traceCommand(method: string, url: string, opts: TraceOptions): Promise<void>;
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.traceCommand = traceCommand;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const config_1 = require("../config");
|
|
43
|
+
/**
|
|
44
|
+
* `trickle trace <method> <url>` — Make an HTTP request and show the response
|
|
45
|
+
* with inline type annotations. Like curl but type-aware.
|
|
46
|
+
*
|
|
47
|
+
* Optionally saves the types to the backend with --save.
|
|
48
|
+
*/
|
|
49
|
+
async function traceCommand(method, url, opts) {
|
|
50
|
+
const httpMethod = method.toUpperCase();
|
|
51
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
52
|
+
if (!validMethods.includes(httpMethod)) {
|
|
53
|
+
console.error(chalk_1.default.red(`\n Invalid HTTP method: ${method}\n`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
let parsedUrl;
|
|
57
|
+
try {
|
|
58
|
+
parsedUrl = new URL(url);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
console.error(chalk_1.default.red(`\n Invalid URL: ${url}\n`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
// Build request headers
|
|
65
|
+
const headers = { Accept: "application/json" };
|
|
66
|
+
if (opts.header) {
|
|
67
|
+
for (const h of opts.header) {
|
|
68
|
+
const colonIdx = h.indexOf(":");
|
|
69
|
+
if (colonIdx === -1) {
|
|
70
|
+
console.error(chalk_1.default.red(`\n Invalid header: ${h}\n`));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let reqBody;
|
|
77
|
+
let reqJson = undefined;
|
|
78
|
+
if (opts.body) {
|
|
79
|
+
reqBody = opts.body;
|
|
80
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
81
|
+
try {
|
|
82
|
+
reqJson = JSON.parse(opts.body);
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(chalk_1.default.bold(" trickle trace"));
|
|
88
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
89
|
+
// Make the request
|
|
90
|
+
const startTime = Date.now();
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await fetch(url, {
|
|
94
|
+
method: httpMethod,
|
|
95
|
+
headers,
|
|
96
|
+
body: reqBody,
|
|
97
|
+
signal: AbortSignal.timeout(30000),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
102
|
+
console.error(chalk_1.default.red(`\n Request failed: ${msg}\n`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const elapsed = Date.now() - startTime;
|
|
106
|
+
// Status line
|
|
107
|
+
const status = response.status;
|
|
108
|
+
const statusColor = status < 300 ? chalk_1.default.green : status < 400 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
109
|
+
console.log(chalk_1.default.gray(` ${chalk_1.default.bold(httpMethod)} ${url}`));
|
|
110
|
+
console.log(chalk_1.default.gray(` Status: `) +
|
|
111
|
+
statusColor(`${status} ${response.statusText}`) +
|
|
112
|
+
chalk_1.default.gray(` (${elapsed}ms)`));
|
|
113
|
+
// Show response headers summary
|
|
114
|
+
const contentType = response.headers.get("content-type") || "";
|
|
115
|
+
const contentLength = response.headers.get("content-length");
|
|
116
|
+
console.log(chalk_1.default.gray(` Type: ${contentType}`));
|
|
117
|
+
if (contentLength) {
|
|
118
|
+
console.log(chalk_1.default.gray(` Size: ${formatBytes(parseInt(contentLength, 10))}`));
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
121
|
+
// Read and parse body
|
|
122
|
+
const bodyText = await response.text();
|
|
123
|
+
if (!contentType.includes("json") || bodyText.length === 0) {
|
|
124
|
+
console.log("");
|
|
125
|
+
if (bodyText.length > 0) {
|
|
126
|
+
console.log(chalk_1.default.gray(" (non-JSON response)"));
|
|
127
|
+
console.log(chalk_1.default.gray(" " + bodyText.slice(0, 500)));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(chalk_1.default.gray(" (empty response)"));
|
|
131
|
+
}
|
|
132
|
+
console.log("");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let jsonData;
|
|
136
|
+
try {
|
|
137
|
+
jsonData = JSON.parse(bodyText);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
console.error(chalk_1.default.red("\n Response is not valid JSON.\n"));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
// Render annotated JSON
|
|
144
|
+
console.log("");
|
|
145
|
+
const lines = renderAnnotatedJson(jsonData, 2);
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
console.log(line);
|
|
148
|
+
}
|
|
149
|
+
// Summary
|
|
150
|
+
const typeNode = jsonToTypeNode(jsonData);
|
|
151
|
+
const stats = countTypeStats(typeNode);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
154
|
+
console.log(chalk_1.default.gray(` ${stats.fields} fields, ${stats.uniqueTypes} unique types, ${stats.depth} depth`));
|
|
155
|
+
// Optionally save to backend
|
|
156
|
+
if (opts.save) {
|
|
157
|
+
await saveTypes(parsedUrl, httpMethod, jsonData, reqJson, opts);
|
|
158
|
+
}
|
|
159
|
+
console.log("");
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Render JSON with inline type annotations.
|
|
163
|
+
*/
|
|
164
|
+
function renderAnnotatedJson(value, baseIndent) {
|
|
165
|
+
const lines = [];
|
|
166
|
+
renderValue(value, baseIndent, 0, lines, false, true);
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
function renderValue(value, baseIndent, depth, lines, trailingComma, isLast) {
|
|
170
|
+
const indent = " ".repeat(baseIndent + depth * 2);
|
|
171
|
+
const comma = trailingComma ? "," : "";
|
|
172
|
+
const annotationGap = 2;
|
|
173
|
+
if (value === null) {
|
|
174
|
+
lines.push(indent + chalk_1.default.gray("null") + comma + typeAnnotation("null", indent.length + 4, annotationGap));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (value === undefined) {
|
|
178
|
+
lines.push(indent + chalk_1.default.gray("undefined") + comma);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
switch (typeof value) {
|
|
182
|
+
case "string": {
|
|
183
|
+
const display = value.length > 60 ? `"${value.slice(0, 57)}..."` : `"${value}"`;
|
|
184
|
+
lines.push(indent + chalk_1.default.green(display) + comma + typeAnnotation("string", indent.length + display.length, annotationGap));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
case "number":
|
|
188
|
+
lines.push(indent + chalk_1.default.yellow(String(value)) + comma + typeAnnotation("number", indent.length + String(value).length, annotationGap));
|
|
189
|
+
return;
|
|
190
|
+
case "boolean":
|
|
191
|
+
lines.push(indent + chalk_1.default.blue(String(value)) + comma + typeAnnotation("boolean", indent.length + String(value).length, annotationGap));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
if (value.length === 0) {
|
|
196
|
+
lines.push(indent + "[]" + comma + typeAnnotation("unknown[]", indent.length + 2, annotationGap));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Show type annotation on the opening bracket
|
|
200
|
+
const elemType = compactType(jsonToTypeNode(value[0]));
|
|
201
|
+
lines.push(indent + "[" + typeAnnotation(`${elemType}[]`, indent.length + 1, annotationGap));
|
|
202
|
+
// Show first few elements, collapse if too many
|
|
203
|
+
const maxShow = Math.min(value.length, 3);
|
|
204
|
+
for (let i = 0; i < maxShow; i++) {
|
|
205
|
+
renderValue(value[i], baseIndent, depth + 1, lines, i < value.length - 1, i === maxShow - 1);
|
|
206
|
+
}
|
|
207
|
+
if (value.length > maxShow) {
|
|
208
|
+
const innerIndent = " ".repeat(baseIndent + (depth + 1) * 2);
|
|
209
|
+
lines.push(innerIndent + chalk_1.default.gray(`// ... +${value.length - maxShow} more items`));
|
|
210
|
+
}
|
|
211
|
+
lines.push(indent + "]" + comma);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Object
|
|
215
|
+
const obj = value;
|
|
216
|
+
const keys = Object.keys(obj);
|
|
217
|
+
if (keys.length === 0) {
|
|
218
|
+
lines.push(indent + "{}" + comma + typeAnnotation("{}", indent.length + 2, annotationGap));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
lines.push(indent + "{");
|
|
222
|
+
for (let i = 0; i < keys.length; i++) {
|
|
223
|
+
const key = keys[i];
|
|
224
|
+
const val = obj[key];
|
|
225
|
+
const isLastKey = i === keys.length - 1;
|
|
226
|
+
const keyComma = isLastKey ? "" : ",";
|
|
227
|
+
const innerIndent = " ".repeat(baseIndent + (depth + 1) * 2);
|
|
228
|
+
// For simple values, render key: value on one line
|
|
229
|
+
if (val === null || typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
|
|
230
|
+
const valStr = formatSimpleValue(val);
|
|
231
|
+
const valType = val === null ? "null" : typeof val;
|
|
232
|
+
const lineContent = `${innerIndent}${chalk_1.default.white(`"${key}"`)}: ${valStr}${keyComma}`;
|
|
233
|
+
const rawLen = innerIndent.length + `"${key}": `.length + rawValueLen(val) + keyComma.length;
|
|
234
|
+
lines.push(lineContent + typeAnnotation(valType, rawLen, annotationGap));
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Complex value — render key then value
|
|
238
|
+
lines.push(`${innerIndent}${chalk_1.default.white(`"${key}"`)}:`);
|
|
239
|
+
renderValue(val, baseIndent, depth + 1, lines, !isLastKey, isLastKey);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
lines.push(indent + "}" + comma);
|
|
243
|
+
}
|
|
244
|
+
function formatSimpleValue(val) {
|
|
245
|
+
if (val === null)
|
|
246
|
+
return chalk_1.default.gray("null");
|
|
247
|
+
if (typeof val === "string") {
|
|
248
|
+
const display = val.length > 50 ? `"${val.slice(0, 47)}..."` : `"${val}"`;
|
|
249
|
+
return chalk_1.default.green(display);
|
|
250
|
+
}
|
|
251
|
+
if (typeof val === "number")
|
|
252
|
+
return chalk_1.default.yellow(String(val));
|
|
253
|
+
if (typeof val === "boolean")
|
|
254
|
+
return chalk_1.default.blue(String(val));
|
|
255
|
+
return String(val);
|
|
256
|
+
}
|
|
257
|
+
function rawValueLen(val) {
|
|
258
|
+
if (val === null)
|
|
259
|
+
return 4;
|
|
260
|
+
if (typeof val === "string") {
|
|
261
|
+
return Math.min(val.length + 2, 53);
|
|
262
|
+
}
|
|
263
|
+
return String(val).length;
|
|
264
|
+
}
|
|
265
|
+
function typeAnnotation(type, currentCol, gap) {
|
|
266
|
+
const targetCol = 45;
|
|
267
|
+
const padding = Math.max(gap, targetCol - currentCol);
|
|
268
|
+
return " ".repeat(padding) + chalk_1.default.gray(`// ${type}`);
|
|
269
|
+
}
|
|
270
|
+
function jsonToTypeNode(value) {
|
|
271
|
+
if (value === null)
|
|
272
|
+
return { kind: "primitive", name: "null" };
|
|
273
|
+
if (value === undefined)
|
|
274
|
+
return { kind: "primitive", name: "undefined" };
|
|
275
|
+
switch (typeof value) {
|
|
276
|
+
case "string": return { kind: "primitive", name: "string" };
|
|
277
|
+
case "number": return { kind: "primitive", name: "number" };
|
|
278
|
+
case "boolean": return { kind: "primitive", name: "boolean" };
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
if (value.length === 0)
|
|
282
|
+
return { kind: "array", element: { kind: "unknown" } };
|
|
283
|
+
return { kind: "array", element: jsonToTypeNode(value[0]) };
|
|
284
|
+
}
|
|
285
|
+
const obj = value;
|
|
286
|
+
const properties = {};
|
|
287
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
288
|
+
properties[key] = jsonToTypeNode(val);
|
|
289
|
+
}
|
|
290
|
+
return { kind: "object", properties };
|
|
291
|
+
}
|
|
292
|
+
function compactType(node) {
|
|
293
|
+
switch (node.kind) {
|
|
294
|
+
case "primitive":
|
|
295
|
+
return node.name;
|
|
296
|
+
case "object": {
|
|
297
|
+
const props = node.properties;
|
|
298
|
+
const keys = Object.keys(props);
|
|
299
|
+
if (keys.length === 0)
|
|
300
|
+
return "{}";
|
|
301
|
+
if (keys.length <= 4) {
|
|
302
|
+
return `{${keys.join(", ")}}`;
|
|
303
|
+
}
|
|
304
|
+
return `{${keys.slice(0, 3).join(", ")}, …+${keys.length - 3}}`;
|
|
305
|
+
}
|
|
306
|
+
case "array":
|
|
307
|
+
return `${compactType(node.element)}[]`;
|
|
308
|
+
default:
|
|
309
|
+
return node.kind;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function countTypeStats(node) {
|
|
313
|
+
const types = new Set();
|
|
314
|
+
let fields = 0;
|
|
315
|
+
let maxDepth = 0;
|
|
316
|
+
function walk(n, depth) {
|
|
317
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
318
|
+
if (n.kind === "primitive") {
|
|
319
|
+
types.add(n.name);
|
|
320
|
+
}
|
|
321
|
+
else if (n.kind === "object") {
|
|
322
|
+
const props = n.properties;
|
|
323
|
+
for (const val of Object.values(props)) {
|
|
324
|
+
fields++;
|
|
325
|
+
walk(val, depth + 1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (n.kind === "array") {
|
|
329
|
+
types.add("array");
|
|
330
|
+
walk(n.element, depth + 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
walk(node, 0);
|
|
334
|
+
return { fields, uniqueTypes: types.size, depth: maxDepth };
|
|
335
|
+
}
|
|
336
|
+
function formatBytes(bytes) {
|
|
337
|
+
if (bytes < 1024)
|
|
338
|
+
return `${bytes}B`;
|
|
339
|
+
if (bytes < 1024 * 1024)
|
|
340
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
341
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
342
|
+
}
|
|
343
|
+
function normalizePath(urlPath) {
|
|
344
|
+
return urlPath.split("/").map((part, i) => {
|
|
345
|
+
if (!part)
|
|
346
|
+
return part;
|
|
347
|
+
if (/^\d+$/.test(part))
|
|
348
|
+
return ":id";
|
|
349
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part))
|
|
350
|
+
return ":id";
|
|
351
|
+
if (/^[0-9a-f]{16,}$/i.test(part) && i > 1)
|
|
352
|
+
return ":id";
|
|
353
|
+
return part;
|
|
354
|
+
}).join("/");
|
|
355
|
+
}
|
|
356
|
+
function computeTypeHash(argsType, returnType) {
|
|
357
|
+
const data = JSON.stringify({ a: argsType, r: returnType });
|
|
358
|
+
return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
|
|
359
|
+
}
|
|
360
|
+
async function saveTypes(parsedUrl, httpMethod, resJson, reqJson, opts) {
|
|
361
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
362
|
+
try {
|
|
363
|
+
const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
|
|
364
|
+
if (!res.ok)
|
|
365
|
+
throw new Error("not ok");
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
console.log(chalk_1.default.yellow(" Could not save types — backend not reachable."));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const routePath = normalizePath(parsedUrl.pathname);
|
|
372
|
+
const functionName = `${httpMethod} ${routePath}`;
|
|
373
|
+
const argsProperties = {};
|
|
374
|
+
if (reqJson !== undefined && reqJson !== null) {
|
|
375
|
+
argsProperties.body = jsonToTypeNode(reqJson);
|
|
376
|
+
}
|
|
377
|
+
if (parsedUrl.search) {
|
|
378
|
+
const queryProps = {};
|
|
379
|
+
for (const [key] of parsedUrl.searchParams) {
|
|
380
|
+
queryProps[key] = { kind: "primitive", name: "string" };
|
|
381
|
+
}
|
|
382
|
+
if (Object.keys(queryProps).length > 0) {
|
|
383
|
+
argsProperties.query = { kind: "object", properties: queryProps };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const argsType = Object.keys(argsProperties).length > 0
|
|
387
|
+
? { kind: "object", properties: argsProperties }
|
|
388
|
+
: { kind: "object", properties: {} };
|
|
389
|
+
const returnType = jsonToTypeNode(resJson);
|
|
390
|
+
const typeHash = computeTypeHash(argsType, returnType);
|
|
391
|
+
const payload = {
|
|
392
|
+
functionName,
|
|
393
|
+
module: opts.module || "trace",
|
|
394
|
+
language: "js",
|
|
395
|
+
environment: opts.env || "development",
|
|
396
|
+
typeHash,
|
|
397
|
+
argsType,
|
|
398
|
+
returnType,
|
|
399
|
+
sampleOutput: resJson,
|
|
400
|
+
sampleInput: Object.keys(argsProperties).length > 0 ? argsProperties : undefined,
|
|
401
|
+
};
|
|
402
|
+
try {
|
|
403
|
+
const ingestRes = await fetch(`${backendUrl}/api/ingest`, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
body: JSON.stringify(payload),
|
|
407
|
+
signal: AbortSignal.timeout(5000),
|
|
408
|
+
});
|
|
409
|
+
if (!ingestRes.ok)
|
|
410
|
+
throw new Error(`HTTP ${ingestRes.status}`);
|
|
411
|
+
console.log(chalk_1.default.green(` Types saved as "${functionName}"`));
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
415
|
+
console.log(chalk_1.default.yellow(` Could not save types: ${msg}`));
|
|
416
|
+
}
|
|
417
|
+
}
|