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,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
+ }
@@ -0,0 +1,7 @@
1
+ export interface TypesOptions {
2
+ env?: string;
3
+ diff?: boolean;
4
+ env1?: string;
5
+ env2?: string;
6
+ }
7
+ export declare function typesCommand(functionName: string, opts: TypesOptions): Promise<void>;