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,312 @@
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.proxyCommand = proxyCommand;
40
+ const http = __importStar(require("http"));
41
+ const https = __importStar(require("https"));
42
+ const crypto = __importStar(require("crypto"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const config_1 = require("../config");
45
+ /**
46
+ * `trickle proxy` — Transparent reverse proxy that captures API types.
47
+ *
48
+ * Sits between the frontend and backend, forwarding all requests while
49
+ * observing request/response shapes and sending type observations to
50
+ * the trickle backend. Works with any backend language or framework —
51
+ * no instrumentation needed.
52
+ */
53
+ async function proxyCommand(opts) {
54
+ const targetUrl = opts.target;
55
+ if (!targetUrl) {
56
+ console.error(chalk_1.default.red("\n Missing --target flag."));
57
+ console.error(chalk_1.default.gray(" Usage: trickle proxy --target http://localhost:3000\n"));
58
+ process.exit(1);
59
+ }
60
+ let parsedTarget;
61
+ try {
62
+ parsedTarget = new URL(targetUrl);
63
+ }
64
+ catch {
65
+ console.error(chalk_1.default.red(`\n Invalid target URL: ${targetUrl}\n`));
66
+ process.exit(1);
67
+ }
68
+ const port = parseInt(opts.port || "4000", 10);
69
+ const backendUrl = (0, config_1.getBackendUrl)();
70
+ // Check backend connectivity
71
+ try {
72
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
73
+ if (!res.ok)
74
+ throw new Error("not ok");
75
+ }
76
+ catch {
77
+ console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}`));
78
+ console.error(chalk_1.default.gray(" Start the backend first.\n"));
79
+ process.exit(1);
80
+ }
81
+ let requestCount = 0;
82
+ let typesSent = 0;
83
+ const server = http.createServer(async (req, res) => {
84
+ const method = (req.method || "GET").toUpperCase();
85
+ const urlPath = req.url || "/";
86
+ // Read request body
87
+ const reqBody = await readBody(req);
88
+ let reqJson = undefined;
89
+ if (reqBody.length > 0) {
90
+ try {
91
+ reqJson = JSON.parse(reqBody.toString("utf-8"));
92
+ }
93
+ catch {
94
+ // Not JSON — skip type capture for request body
95
+ }
96
+ }
97
+ // Forward to target
98
+ const targetReqUrl = new URL(urlPath, targetUrl);
99
+ const isHttps = parsedTarget.protocol === "https:";
100
+ const mod = isHttps ? https : http;
101
+ const proxyReq = mod.request(targetReqUrl.toString(), {
102
+ method,
103
+ headers: {
104
+ ...req.headers,
105
+ host: parsedTarget.host,
106
+ },
107
+ }, (proxyRes) => {
108
+ // Read response body
109
+ const chunks = [];
110
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
111
+ proxyRes.on("end", () => {
112
+ const resBody = Buffer.concat(chunks);
113
+ // Forward response to client
114
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
115
+ res.end(resBody);
116
+ requestCount++;
117
+ // Parse response JSON for type capture
118
+ let resJson = undefined;
119
+ const contentType = proxyRes.headers["content-type"] || "";
120
+ if (contentType.includes("json") && resBody.length > 0) {
121
+ try {
122
+ resJson = JSON.parse(resBody.toString("utf-8"));
123
+ }
124
+ catch {
125
+ // Not valid JSON
126
+ }
127
+ }
128
+ // Only capture types for JSON API-like routes
129
+ if (resJson !== undefined && isApiRoute(urlPath)) {
130
+ captureTypes(method, urlPath, reqJson, resJson, backendUrl).then((sent) => {
131
+ if (sent)
132
+ typesSent++;
133
+ }).catch(() => { });
134
+ // Log
135
+ const status = proxyRes.statusCode || 200;
136
+ const statusColor = status < 400 ? chalk_1.default.green : chalk_1.default.red;
137
+ console.log(chalk_1.default.gray(` ${chalk_1.default.bold(method)} ${urlPath} → `) +
138
+ statusColor(`${status}`) +
139
+ chalk_1.default.gray(` (${typesSent} types captured)`));
140
+ }
141
+ });
142
+ });
143
+ proxyReq.on("error", (err) => {
144
+ console.error(chalk_1.default.red(` Proxy error: ${err.message}`));
145
+ res.writeHead(502);
146
+ res.end(JSON.stringify({ error: "Bad Gateway", message: err.message }));
147
+ });
148
+ if (reqBody.length > 0) {
149
+ proxyReq.write(reqBody);
150
+ }
151
+ proxyReq.end();
152
+ });
153
+ server.listen(port, () => {
154
+ console.log("");
155
+ console.log(chalk_1.default.bold(" trickle proxy"));
156
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
157
+ console.log(chalk_1.default.gray(` Proxy: http://localhost:${port}`));
158
+ console.log(chalk_1.default.gray(` Target: ${targetUrl}`));
159
+ console.log(chalk_1.default.gray(` Backend: ${backendUrl}`));
160
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
161
+ console.log(chalk_1.default.gray(" Point your frontend at the proxy URL."));
162
+ console.log(chalk_1.default.gray(" Press Ctrl+C to stop.\n"));
163
+ });
164
+ process.on("SIGINT", () => {
165
+ console.log(chalk_1.default.gray(`\n Shutting down... (${requestCount} requests, ${typesSent} types captured)`));
166
+ server.close();
167
+ process.exit(0);
168
+ });
169
+ process.on("SIGTERM", () => {
170
+ server.close();
171
+ process.exit(0);
172
+ });
173
+ }
174
+ function readBody(stream) {
175
+ return new Promise((resolve) => {
176
+ const chunks = [];
177
+ stream.on("data", (chunk) => chunks.push(chunk));
178
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
179
+ stream.on("error", () => resolve(Buffer.alloc(0)));
180
+ });
181
+ }
182
+ /**
183
+ * Heuristic: only capture types for API-like routes (not static assets).
184
+ */
185
+ function isApiRoute(urlPath) {
186
+ const path = urlPath.split("?")[0];
187
+ // Skip obvious static assets
188
+ if (/\.(js|css|html|png|jpg|gif|svg|ico|woff|woff2|ttf|eot|map)$/i.test(path)) {
189
+ return false;
190
+ }
191
+ // Include /api/ routes always
192
+ if (path.includes("/api/"))
193
+ return true;
194
+ // Include anything that doesn't look like a file
195
+ if (!path.includes("."))
196
+ return true;
197
+ return false;
198
+ }
199
+ /**
200
+ * Infer a TypeNode from a JSON value.
201
+ */
202
+ function jsonToTypeNode(value) {
203
+ if (value === null)
204
+ return { kind: "primitive", name: "null" };
205
+ if (value === undefined)
206
+ return { kind: "primitive", name: "undefined" };
207
+ switch (typeof value) {
208
+ case "string": return { kind: "primitive", name: "string" };
209
+ case "number": return { kind: "primitive", name: "number" };
210
+ case "boolean": return { kind: "primitive", name: "boolean" };
211
+ case "bigint": return { kind: "primitive", name: "bigint" };
212
+ case "symbol": return { kind: "primitive", name: "symbol" };
213
+ case "function": return { kind: "function", params: [], returnType: { kind: "unknown" } };
214
+ }
215
+ if (Array.isArray(value)) {
216
+ if (value.length === 0)
217
+ return { kind: "array", element: { kind: "unknown" } };
218
+ // Infer element type from first element
219
+ const elementType = jsonToTypeNode(value[0]);
220
+ return { kind: "array", element: elementType };
221
+ }
222
+ // Object
223
+ const obj = value;
224
+ const properties = {};
225
+ for (const [key, val] of Object.entries(obj)) {
226
+ properties[key] = jsonToTypeNode(val);
227
+ }
228
+ return { kind: "object", properties };
229
+ }
230
+ /**
231
+ * Normalize URL path: replace dynamic segments like /users/123 with :param patterns.
232
+ * Uses heuristics: numeric segments and UUID-like segments become params.
233
+ */
234
+ function normalizePath(urlPath) {
235
+ const path = urlPath.split("?")[0];
236
+ const parts = path.split("/");
237
+ return parts
238
+ .map((part, i) => {
239
+ if (!part)
240
+ return part;
241
+ // Numeric IDs
242
+ if (/^\d+$/.test(part))
243
+ return ":id";
244
+ // UUIDs
245
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part))
246
+ return ":id";
247
+ // Short hex hashes
248
+ if (/^[0-9a-f]{16,}$/i.test(part) && i > 1)
249
+ return ":id";
250
+ return part;
251
+ })
252
+ .join("/");
253
+ }
254
+ /**
255
+ * Compute a SHA-256 hash (16 hex chars) for type dedup.
256
+ */
257
+ function computeTypeHash(argsType, returnType) {
258
+ const data = JSON.stringify({ a: argsType, r: returnType });
259
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
260
+ }
261
+ /**
262
+ * Send captured types to the trickle backend.
263
+ */
264
+ async function captureTypes(method, urlPath, reqJson, resJson, backendUrl) {
265
+ const normalizedPath = normalizePath(urlPath);
266
+ const functionName = `${method} ${normalizedPath}`;
267
+ // Build argsType: { body, params, query }
268
+ const argsProperties = {};
269
+ if (reqJson !== undefined && reqJson !== null) {
270
+ argsProperties.body = jsonToTypeNode(reqJson);
271
+ }
272
+ // Extract query params
273
+ const queryStart = urlPath.indexOf("?");
274
+ if (queryStart !== -1) {
275
+ const searchParams = new URLSearchParams(urlPath.slice(queryStart + 1));
276
+ const queryProps = {};
277
+ for (const [key] of searchParams) {
278
+ queryProps[key] = { kind: "primitive", name: "string" };
279
+ }
280
+ if (Object.keys(queryProps).length > 0) {
281
+ argsProperties.query = { kind: "object", properties: queryProps };
282
+ }
283
+ }
284
+ const argsType = Object.keys(argsProperties).length > 0
285
+ ? { kind: "object", properties: argsProperties }
286
+ : { kind: "object", properties: {} };
287
+ const returnType = jsonToTypeNode(resJson);
288
+ const typeHash = computeTypeHash(argsType, returnType);
289
+ const payload = {
290
+ functionName,
291
+ module: "proxy",
292
+ language: "js",
293
+ environment: "development",
294
+ typeHash,
295
+ argsType,
296
+ returnType,
297
+ sampleInput: reqJson !== undefined ? (Object.keys(argsProperties).length > 0 ? argsProperties : undefined) : undefined,
298
+ sampleOutput: resJson,
299
+ };
300
+ try {
301
+ const res = await fetch(`${backendUrl}/api/ingest`, {
302
+ method: "POST",
303
+ headers: { "Content-Type": "application/json" },
304
+ body: JSON.stringify(payload),
305
+ signal: AbortSignal.timeout(5000),
306
+ });
307
+ return res.ok;
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ }
@@ -0,0 +1,14 @@
1
+ export interface ReplayOptions {
2
+ target?: string;
3
+ strict?: boolean;
4
+ json?: boolean;
5
+ failFast?: boolean;
6
+ }
7
+ /**
8
+ * `trickle replay` — Replay captured API requests as regression tests.
9
+ *
10
+ * Uses the sample inputs/outputs already captured by trickle to replay
11
+ * requests against a running server and verify response shapes match.
12
+ * Developers get free regression tests without writing any test code.
13
+ */
14
+ export declare function replayCommand(opts: ReplayOptions): Promise<void>;
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.replayCommand = replayCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const api_client_1 = require("../api-client");
9
+ /**
10
+ * `trickle replay` — Replay captured API requests as regression tests.
11
+ *
12
+ * Uses the sample inputs/outputs already captured by trickle to replay
13
+ * requests against a running server and verify response shapes match.
14
+ * Developers get free regression tests without writing any test code.
15
+ */
16
+ async function replayCommand(opts) {
17
+ const target = opts.target || "http://localhost:3000";
18
+ // Fetch observed routes
19
+ let routes;
20
+ try {
21
+ const config = await (0, api_client_1.fetchMockConfig)();
22
+ routes = config.routes;
23
+ }
24
+ catch {
25
+ console.error(chalk_1.default.red("\n Cannot connect to trickle backend."));
26
+ console.error(chalk_1.default.gray(" Is the backend running?\n"));
27
+ process.exit(1);
28
+ }
29
+ if (routes.length === 0) {
30
+ console.error(chalk_1.default.yellow("\n No observed routes to replay."));
31
+ console.error(chalk_1.default.gray(" Instrument your app and make some requests first.\n"));
32
+ process.exit(0);
33
+ }
34
+ if (!opts.json) {
35
+ console.log("");
36
+ console.log(chalk_1.default.bold(" trickle replay"));
37
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
38
+ console.log(chalk_1.default.gray(` Target: ${target}`));
39
+ console.log(chalk_1.default.gray(` Routes: ${routes.length}`));
40
+ console.log(chalk_1.default.gray(` Mode: ${opts.strict ? "strict (exact values)" : "shape (structural match)"}`));
41
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
42
+ console.log("");
43
+ }
44
+ const results = [];
45
+ for (const route of routes) {
46
+ const result = await replayRoute(route, target, opts.strict || false);
47
+ results.push(result);
48
+ if (!opts.json) {
49
+ const icon = result.status === "pass"
50
+ ? chalk_1.default.green("✓")
51
+ : result.status === "fail"
52
+ ? chalk_1.default.red("✗")
53
+ : chalk_1.default.yellow("!");
54
+ const statusStr = result.httpStatus ? chalk_1.default.gray(` [${result.httpStatus}]`) : "";
55
+ const timeStr = chalk_1.default.gray(` ${result.durationMs}ms`);
56
+ const msg = result.message ? chalk_1.default.gray(` — ${result.message}`) : "";
57
+ console.log(` ${icon} ${chalk_1.default.bold(route.method)} ${route.path}${statusStr}${timeStr}${msg}`);
58
+ }
59
+ if (opts.failFast && result.status !== "pass") {
60
+ break;
61
+ }
62
+ }
63
+ // Summary
64
+ const passed = results.filter((r) => r.status === "pass").length;
65
+ const failed = results.filter((r) => r.status === "fail").length;
66
+ const errors = results.filter((r) => r.status === "error").length;
67
+ if (opts.json) {
68
+ console.log(JSON.stringify({
69
+ target,
70
+ mode: opts.strict ? "strict" : "shape",
71
+ total: results.length,
72
+ passed,
73
+ failed,
74
+ errors,
75
+ results,
76
+ }, null, 2));
77
+ }
78
+ else {
79
+ console.log("");
80
+ console.log(chalk_1.default.gray(" " + "─".repeat(50)));
81
+ if (failed === 0 && errors === 0) {
82
+ console.log(chalk_1.default.green(` ${passed}/${results.length} passed`) + chalk_1.default.gray(` — all routes match`));
83
+ }
84
+ else {
85
+ const parts = [];
86
+ if (passed > 0)
87
+ parts.push(chalk_1.default.green(`${passed} passed`));
88
+ if (failed > 0)
89
+ parts.push(chalk_1.default.red(`${failed} failed`));
90
+ if (errors > 0)
91
+ parts.push(chalk_1.default.yellow(`${errors} errors`));
92
+ console.log(` ${parts.join(", ")} out of ${results.length} routes`);
93
+ }
94
+ console.log("");
95
+ }
96
+ if (failed > 0 || errors > 0) {
97
+ process.exit(1);
98
+ }
99
+ }
100
+ async function replayRoute(route, target, strict) {
101
+ const { method, path: routePath } = route;
102
+ // Build URL — replace :param patterns with sample values if available
103
+ let url = routePath;
104
+ if (route.sampleInput && typeof route.sampleInput === "object") {
105
+ const input = route.sampleInput;
106
+ const params = input.params;
107
+ if (params) {
108
+ for (const [key, value] of Object.entries(params)) {
109
+ url = url.replace(`:${key}`, String(value));
110
+ }
111
+ }
112
+ }
113
+ // Replace any remaining :params with "1" as fallback
114
+ url = url.replace(/:(\w+)/g, "1");
115
+ const fullUrl = `${target}${url}`;
116
+ const start = Date.now();
117
+ try {
118
+ // Build request
119
+ const fetchOpts = { method };
120
+ const hasBody = ["POST", "PUT", "PATCH"].includes(method);
121
+ if (hasBody && route.sampleInput) {
122
+ const input = route.sampleInput;
123
+ const body = input.body || input;
124
+ if (body && typeof body === "object" && Object.keys(body).length > 0) {
125
+ fetchOpts.headers = { "Content-Type": "application/json" };
126
+ fetchOpts.body = JSON.stringify(body);
127
+ }
128
+ }
129
+ const res = await fetch(fullUrl, {
130
+ ...fetchOpts,
131
+ signal: AbortSignal.timeout(10000),
132
+ });
133
+ const durationMs = Date.now() - start;
134
+ if (!res.ok) {
135
+ return {
136
+ method, path: routePath, status: "fail",
137
+ httpStatus: res.status,
138
+ message: `HTTP ${res.status}`,
139
+ durationMs,
140
+ };
141
+ }
142
+ // Parse response
143
+ const contentType = res.headers.get("content-type") || "";
144
+ if (!contentType.includes("json")) {
145
+ return {
146
+ method, path: routePath, status: "pass",
147
+ httpStatus: res.status,
148
+ message: "non-JSON response",
149
+ durationMs,
150
+ };
151
+ }
152
+ const actual = await res.json();
153
+ if (!route.sampleOutput) {
154
+ return {
155
+ method, path: routePath, status: "pass",
156
+ httpStatus: res.status,
157
+ durationMs,
158
+ };
159
+ }
160
+ // Compare
161
+ if (strict) {
162
+ return compareStrict(method, routePath, route.sampleOutput, actual, res.status, durationMs);
163
+ }
164
+ else {
165
+ return compareShape(method, routePath, route.sampleOutput, actual, res.status, durationMs);
166
+ }
167
+ }
168
+ catch (err) {
169
+ const durationMs = Date.now() - start;
170
+ const message = err instanceof Error ? err.message : "Unknown error";
171
+ return {
172
+ method, path: routePath, status: "error",
173
+ message: message.includes("ECONNREFUSED") ? "connection refused" : message,
174
+ durationMs,
175
+ };
176
+ }
177
+ }
178
+ /**
179
+ * Shape comparison: verify that the actual response has the same structure
180
+ * (same keys, same types) as the expected sample output.
181
+ */
182
+ function compareShape(method, path, expected, actual, httpStatus, durationMs) {
183
+ const mismatches = findShapeMismatches(expected, actual, "");
184
+ if (mismatches.length === 0) {
185
+ return { method, path, status: "pass", httpStatus, durationMs };
186
+ }
187
+ return {
188
+ method, path, status: "fail", httpStatus,
189
+ message: mismatches[0],
190
+ durationMs,
191
+ };
192
+ }
193
+ function findShapeMismatches(expected, actual, prefix) {
194
+ const mismatches = [];
195
+ if (expected === null || expected === undefined) {
196
+ return mismatches;
197
+ }
198
+ const expectedType = typeOf(expected);
199
+ const actualType = typeOf(actual);
200
+ if (expectedType !== actualType) {
201
+ mismatches.push(`${prefix || "root"}: expected ${expectedType}, got ${actualType}`);
202
+ return mismatches;
203
+ }
204
+ if (expectedType === "object") {
205
+ const expectedObj = expected;
206
+ const actualObj = actual;
207
+ const expectedKeys = Object.keys(expectedObj).sort();
208
+ const actualKeys = Object.keys(actualObj).sort();
209
+ // Check for missing keys
210
+ for (const key of expectedKeys) {
211
+ if (!(key in actualObj)) {
212
+ mismatches.push(`${prefix ? prefix + "." : ""}${key}: missing`);
213
+ }
214
+ else {
215
+ // Recurse (limit depth to avoid noise)
216
+ if (prefix.split(".").length < 3) {
217
+ mismatches.push(...findShapeMismatches(expectedObj[key], actualObj[key], `${prefix ? prefix + "." : ""}${key}`));
218
+ }
219
+ }
220
+ }
221
+ }
222
+ if (expectedType === "array") {
223
+ const expectedArr = expected;
224
+ const actualArr = actual;
225
+ if (expectedArr.length > 0 && actualArr.length > 0) {
226
+ mismatches.push(...findShapeMismatches(expectedArr[0], actualArr[0], `${prefix}[0]`));
227
+ }
228
+ }
229
+ return mismatches;
230
+ }
231
+ function typeOf(value) {
232
+ if (value === null)
233
+ return "null";
234
+ if (Array.isArray(value))
235
+ return "array";
236
+ return typeof value;
237
+ }
238
+ /**
239
+ * Strict comparison: verify exact value match.
240
+ */
241
+ function compareStrict(method, path, expected, actual, httpStatus, durationMs) {
242
+ // Deep compare, but be lenient with dynamic fields (ids, timestamps)
243
+ const mismatches = findValueMismatches(expected, actual, "");
244
+ if (mismatches.length === 0) {
245
+ return { method, path, status: "pass", httpStatus, durationMs };
246
+ }
247
+ return {
248
+ method, path, status: "fail", httpStatus,
249
+ message: mismatches[0],
250
+ durationMs,
251
+ };
252
+ }
253
+ function findValueMismatches(expected, actual, prefix) {
254
+ const mismatches = [];
255
+ if (expected === null || expected === undefined)
256
+ return mismatches;
257
+ const expectedType = typeOf(expected);
258
+ const actualType = typeOf(actual);
259
+ if (expectedType !== actualType) {
260
+ mismatches.push(`${prefix || "root"}: expected ${expectedType} got ${actualType}`);
261
+ return mismatches;
262
+ }
263
+ if (expectedType === "object") {
264
+ const expectedObj = expected;
265
+ const actualObj = actual;
266
+ for (const key of Object.keys(expectedObj)) {
267
+ if (!(key in actualObj)) {
268
+ mismatches.push(`${prefix ? prefix + "." : ""}${key}: missing`);
269
+ }
270
+ else if (prefix.split(".").length < 3) {
271
+ mismatches.push(...findValueMismatches(expectedObj[key], actualObj[key], `${prefix ? prefix + "." : ""}${key}`));
272
+ }
273
+ }
274
+ }
275
+ else if (expectedType === "array") {
276
+ const expectedArr = expected;
277
+ const actualArr = actual;
278
+ if (expectedArr.length > 0 && actualArr.length > 0) {
279
+ mismatches.push(...findValueMismatches(expectedArr[0], actualArr[0], `${prefix}[0]`));
280
+ }
281
+ }
282
+ else {
283
+ // Primitive comparison
284
+ if (expected !== actual) {
285
+ mismatches.push(`${prefix || "root"}: expected ${JSON.stringify(expected)} got ${JSON.stringify(actual)}`);
286
+ }
287
+ }
288
+ return mismatches;
289
+ }
@@ -0,0 +1,17 @@
1
+ export interface RunOptions {
2
+ module?: string;
3
+ include?: string;
4
+ exclude?: string;
5
+ stubs?: string;
6
+ annotate?: string;
7
+ watch?: boolean;
8
+ }
9
+ /**
10
+ * `trickle run <command>` — Run any command with universal type observation.
11
+ *
12
+ * Auto-detects JS or Python, injects the right instrumentation, starts the
13
+ * backend if needed, and shows a summary of captured types after exit.
14
+ * With --stubs or --annotate, also generates type files automatically.
15
+ * Reads .tricklerc.json for project defaults.
16
+ */
17
+ export declare function runCommand(command: string | undefined, opts: RunOptions): Promise<void>;