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,287 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import chalk from "chalk";
4
+ import { fetchCodegen, fetchOpenApiSpec, fetchMockConfig } from "../api-client";
5
+ import { testGenCommand } from "./test-gen";
6
+
7
+ export interface ExportOptions {
8
+ dir?: string;
9
+ env?: string;
10
+ }
11
+
12
+ interface ExportResult {
13
+ file: string;
14
+ label: string;
15
+ ok: boolean;
16
+ count?: string;
17
+ }
18
+
19
+ /**
20
+ * `trickle export` — Generate all output formats into a directory at once.
21
+ *
22
+ * Creates a complete `.trickle/` directory with:
23
+ * - types.d.ts — TypeScript type declarations
24
+ * - api-client.ts — Typed fetch-based API client
25
+ * - handlers.d.ts — Express handler type aliases
26
+ * - schemas.ts — Zod validation schemas
27
+ * - hooks.ts — TanStack React Query hooks
28
+ * - guards.ts — Runtime type guard functions
29
+ * - openapi.json — OpenAPI 3.0 specification
30
+ * - api.test.ts — Generated API test scaffolds
31
+ */
32
+ export async function exportCommand(opts: ExportOptions): Promise<void> {
33
+ const outDir = path.resolve(opts.dir || ".trickle");
34
+
35
+ // Ensure directory exists
36
+ if (!fs.existsSync(outDir)) {
37
+ fs.mkdirSync(outDir, { recursive: true });
38
+ }
39
+
40
+ console.log("");
41
+ console.log(chalk.bold(" trickle export"));
42
+ console.log(chalk.gray(" " + "─".repeat(50)));
43
+ console.log(chalk.gray(` Output directory: ${outDir}`));
44
+ if (opts.env) {
45
+ console.log(chalk.gray(` Environment: ${opts.env}`));
46
+ }
47
+ console.log(chalk.gray(" " + "─".repeat(50)));
48
+ console.log("");
49
+
50
+ const results: ExportResult[] = [];
51
+ const queryOpts = { env: opts.env };
52
+
53
+ // 1. TypeScript types
54
+ results.push(await generateFile(
55
+ path.join(outDir, "types.d.ts"),
56
+ "TypeScript types",
57
+ () => fetchCodegen({ ...queryOpts }).then((r) => r.types),
58
+ countInterfaces,
59
+ ));
60
+
61
+ // 2. API client
62
+ results.push(await generateFile(
63
+ path.join(outDir, "api-client.ts"),
64
+ "Typed API client",
65
+ () => fetchCodegen({ ...queryOpts, format: "client" }).then((r) => r.types),
66
+ countFunctions,
67
+ ));
68
+
69
+ // 3. Express handler types
70
+ results.push(await generateFile(
71
+ path.join(outDir, "handlers.d.ts"),
72
+ "Express handler types",
73
+ () => fetchCodegen({ ...queryOpts, format: "handlers" }).then((r) => r.types),
74
+ countHandlers,
75
+ ));
76
+
77
+ // 4. Zod schemas
78
+ results.push(await generateFile(
79
+ path.join(outDir, "schemas.ts"),
80
+ "Zod schemas",
81
+ () => fetchCodegen({ ...queryOpts, format: "zod" }).then((r) => r.types),
82
+ countSchemas,
83
+ ));
84
+
85
+ // 5. React Query hooks
86
+ results.push(await generateFile(
87
+ path.join(outDir, "hooks.ts"),
88
+ "React Query hooks",
89
+ () => fetchCodegen({ ...queryOpts, format: "react-query" }).then((r) => r.types),
90
+ countHooks,
91
+ ));
92
+
93
+ // 6. Type guards
94
+ results.push(await generateFile(
95
+ path.join(outDir, "guards.ts"),
96
+ "Type guards",
97
+ () => fetchCodegen({ ...queryOpts, format: "guards" }).then((r) => r.types),
98
+ countGuards,
99
+ ));
100
+
101
+ // OpenAPI spec
102
+ results.push(await generateFile(
103
+ path.join(outDir, "openapi.json"),
104
+ "OpenAPI 3.0 spec",
105
+ async () => {
106
+ const spec = await fetchOpenApiSpec({ env: opts.env });
107
+ return JSON.stringify(spec, null, 2);
108
+ },
109
+ countPaths,
110
+ ));
111
+
112
+ // API tests
113
+ results.push(await generateFile(
114
+ path.join(outDir, "api.test.ts"),
115
+ "API test scaffolds",
116
+ async () => {
117
+ const { routes } = await fetchMockConfig();
118
+ if (routes.length === 0) return null;
119
+ // Use the testGenCommand's internal logic by fetching via codegen format?
120
+ // Actually, let's generate a simple version directly
121
+ return generateTestContent(routes);
122
+ },
123
+ countTests,
124
+ ));
125
+
126
+ // Summary
127
+ console.log("");
128
+ const successCount = results.filter((r) => r.ok).length;
129
+ const skipCount = results.filter((r) => !r.ok).length;
130
+
131
+ for (const r of results) {
132
+ if (r.ok) {
133
+ const countStr = r.count ? chalk.gray(` (${r.count})`) : "";
134
+ console.log(chalk.green(" ✓ ") + chalk.bold(r.file) + countStr);
135
+ } else {
136
+ console.log(chalk.yellow(" ─ ") + chalk.gray(r.file) + chalk.gray(" (skipped — no data)"));
137
+ }
138
+ }
139
+
140
+ console.log("");
141
+ if (successCount > 0) {
142
+ console.log(chalk.green(` ${successCount} files generated`) + (skipCount > 0 ? chalk.gray(`, ${skipCount} skipped`) : ""));
143
+ } else {
144
+ console.log(chalk.yellow(" No files generated — instrument your app and make some requests first."));
145
+ }
146
+ console.log("");
147
+ }
148
+
149
+ async function generateFile(
150
+ filePath: string,
151
+ label: string,
152
+ generator: () => Promise<string | null>,
153
+ counter: (content: string) => string | undefined,
154
+ ): Promise<ExportResult> {
155
+ const fileName = path.basename(filePath);
156
+ try {
157
+ const content = await generator();
158
+ if (!content || content.includes("No functions found") || content.includes("No API routes found") || content.includes("No observations")) {
159
+ return { file: fileName, label, ok: false };
160
+ }
161
+
162
+ fs.writeFileSync(filePath, content, "utf-8");
163
+ const count = counter(content);
164
+ return { file: fileName, label, ok: true, count };
165
+ } catch {
166
+ return { file: fileName, label, ok: false };
167
+ }
168
+ }
169
+
170
+ function countInterfaces(content: string): string | undefined {
171
+ const count = (content.match(/export (interface|type) /g) || []).length;
172
+ return count > 0 ? `${count} types` : undefined;
173
+ }
174
+
175
+ function countFunctions(content: string): string | undefined {
176
+ if (content.includes("createTrickleClient")) return "client factory";
177
+ return undefined;
178
+ }
179
+
180
+ function countHandlers(content: string): string | undefined {
181
+ const count = (content.match(/export type \w+Handler/g) || []).length;
182
+ return count > 0 ? `${count} handlers` : undefined;
183
+ }
184
+
185
+ function countSchemas(content: string): string | undefined {
186
+ const count = (content.match(/Schema = /g) || []).length;
187
+ return count > 0 ? `${count} schemas` : undefined;
188
+ }
189
+
190
+ function countHooks(content: string): string | undefined {
191
+ const count = (content.match(/export function use\w+/g) || []).length;
192
+ return count > 0 ? `${count} hooks` : undefined;
193
+ }
194
+
195
+ function countPaths(content: string): string | undefined {
196
+ try {
197
+ const spec = JSON.parse(content);
198
+ const paths = Object.keys(spec.paths || {}).length;
199
+ return paths > 0 ? `${paths} paths` : undefined;
200
+ } catch {
201
+ return undefined;
202
+ }
203
+ }
204
+
205
+ function countTests(content: string): string | undefined {
206
+ const count = (content.match(/it\("/g) || []).length;
207
+ return count > 0 ? `${count} tests` : undefined;
208
+ }
209
+
210
+ function countGuards(content: string): string | undefined {
211
+ const count = (content.match(/export function is\w+/g) || []).length;
212
+ return count > 0 ? `${count} guards` : undefined;
213
+ }
214
+
215
+ // Simplified test generation (reuses the same logic as test-gen but inline)
216
+ interface MockRoute {
217
+ method: string;
218
+ path: string;
219
+ functionName: string;
220
+ sampleInput: unknown;
221
+ sampleOutput: unknown;
222
+ }
223
+
224
+ function generateTestContent(routes: MockRoute[]): string {
225
+ const lines: string[] = [];
226
+ lines.push("// Auto-generated API tests by trickle");
227
+ lines.push(`// Generated at ${new Date().toISOString()}`);
228
+ lines.push("// Do not edit manually — re-run `trickle export` to update");
229
+ lines.push("");
230
+ lines.push('import { describe, it, expect } from "vitest";');
231
+ lines.push("");
232
+ lines.push('const BASE_URL = process.env.TEST_API_URL || "http://localhost:3000";');
233
+ lines.push("");
234
+
235
+ // Group by resource
236
+ const groups: Record<string, MockRoute[]> = {};
237
+ for (const r of routes) {
238
+ const parts = r.path.split("/").filter(Boolean);
239
+ const resource = parts[0] === "api" && parts.length >= 2 ? `/api/${parts[1]}` : `/${parts[0] || "root"}`;
240
+ if (!groups[resource]) groups[resource] = [];
241
+ groups[resource].push(r);
242
+ }
243
+
244
+ for (const [resource, resourceRoutes] of Object.entries(groups)) {
245
+ lines.push(`describe("${resource}", () => {`);
246
+ for (const route of resourceRoutes) {
247
+ const hasBody = ["POST", "PUT", "PATCH"].includes(route.method);
248
+ const fetchPath = route.path.replace(/:(\w+)/g, "test-$1");
249
+
250
+ lines.push(` it("${route.method} ${route.path} — returns expected shape", async () => {`);
251
+ lines.push(` const res = await fetch(\`\${BASE_URL}${fetchPath}\`, {`);
252
+ lines.push(` method: "${route.method}",`);
253
+ if (hasBody && route.sampleInput) {
254
+ const body = typeof route.sampleInput === "object" && route.sampleInput !== null
255
+ ? (route.sampleInput as Record<string, unknown>).body || route.sampleInput
256
+ : route.sampleInput;
257
+ if (body && typeof body === "object" && Object.keys(body as object).length > 0) {
258
+ lines.push(` headers: { "Content-Type": "application/json" },`);
259
+ lines.push(` body: JSON.stringify(${JSON.stringify(body)}),`);
260
+ }
261
+ }
262
+ lines.push(" });");
263
+ lines.push(" expect(res.ok).toBe(true);");
264
+
265
+ if (route.sampleOutput && typeof route.sampleOutput === "object") {
266
+ lines.push(" const body = await res.json();");
267
+ for (const [key, value] of Object.entries(route.sampleOutput as Record<string, unknown>)) {
268
+ if (Array.isArray(value)) {
269
+ lines.push(` expect(Array.isArray(body.${key})).toBe(true);`);
270
+ } else if (typeof value === "string") {
271
+ lines.push(` expect(typeof body.${key}).toBe("string");`);
272
+ } else if (typeof value === "number") {
273
+ lines.push(` expect(typeof body.${key}).toBe("number");`);
274
+ } else if (typeof value === "boolean") {
275
+ lines.push(` expect(typeof body.${key}).toBe("boolean");`);
276
+ }
277
+ }
278
+ }
279
+ lines.push(" });");
280
+ lines.push("");
281
+ }
282
+ lines.push("});");
283
+ lines.push("");
284
+ }
285
+
286
+ return lines.join("\n").trimEnd() + "\n";
287
+ }
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { listFunctions } from "../api-client";
4
+ import { envBadge, langBadge, timeBadge } from "../ui/badges";
5
+ import { relativeTime } from "../ui/helpers";
6
+
7
+ export interface FunctionsOptions {
8
+ env?: string;
9
+ lang?: string;
10
+ search?: string;
11
+ }
12
+
13
+ export async function functionsCommand(opts: FunctionsOptions): Promise<void> {
14
+ const result = await listFunctions({
15
+ env: opts.env,
16
+ language: opts.lang,
17
+ search: opts.search,
18
+ });
19
+
20
+ const { functions } = result;
21
+
22
+ if (functions.length === 0) {
23
+ console.log(chalk.yellow("\n No functions found.\n"));
24
+ if (opts.env || opts.lang || opts.search) {
25
+ console.log(chalk.gray(" Try adjusting your filters.\n"));
26
+ }
27
+ return;
28
+ }
29
+
30
+ console.log("");
31
+
32
+ const table = new Table({
33
+ head: [
34
+ chalk.cyan.bold("Name"),
35
+ chalk.cyan.bold("Module"),
36
+ chalk.cyan.bold("Language"),
37
+ chalk.cyan.bold("Environment"),
38
+ chalk.cyan.bold("Last Seen"),
39
+ ],
40
+ style: {
41
+ head: [],
42
+ border: ["gray"],
43
+ },
44
+ chars: {
45
+ top: "─",
46
+ "top-mid": "┬",
47
+ "top-left": "┌",
48
+ "top-right": "┐",
49
+ bottom: "─",
50
+ "bottom-mid": "┴",
51
+ "bottom-left": "└",
52
+ "bottom-right": "┘",
53
+ left: "│",
54
+ "left-mid": "├",
55
+ mid: "─",
56
+ "mid-mid": "┼",
57
+ right: "│",
58
+ "right-mid": "┤",
59
+ middle: "│",
60
+ },
61
+ });
62
+
63
+ for (const fn of functions) {
64
+ table.push([
65
+ chalk.white.bold(fn.function_name),
66
+ chalk.gray(fn.module),
67
+ langBadge(fn.language),
68
+ envBadge(fn.environment),
69
+ timeBadge(fn.last_seen_at),
70
+ ]);
71
+ }
72
+
73
+ console.log(table.toString());
74
+ console.log(
75
+ chalk.gray(`\n Showing ${chalk.white.bold(String(functions.length))} functions`) +
76
+ (result.total > functions.length
77
+ ? chalk.gray(` of ${result.total} total`)
78
+ : "") +
79
+ "\n"
80
+ );
81
+ }
@@ -0,0 +1,260 @@
1
+ import * as fs from "fs";
2
+ import * as crypto from "crypto";
3
+ import chalk from "chalk";
4
+ import { getBackendUrl } from "../config";
5
+
6
+ export interface InferOptions {
7
+ name: string;
8
+ env?: string;
9
+ module?: string;
10
+ requestBody?: string;
11
+ }
12
+
13
+ interface TypeNode {
14
+ kind: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ /**
19
+ * `trickle infer <file>` — Infer types from a JSON file or stdin and store them.
20
+ *
21
+ * Reads JSON from a file (or stdin if file is "-" or omitted with piped input),
22
+ * infers TypeNode from the data, and sends the observation to the trickle backend.
23
+ * Works offline with saved API responses, test fixtures, or piped command output.
24
+ */
25
+ export async function inferCommand(
26
+ file: string | undefined,
27
+ opts: InferOptions,
28
+ ): Promise<void> {
29
+ const backendUrl = getBackendUrl();
30
+
31
+ // Check backend connectivity
32
+ try {
33
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
34
+ if (!res.ok) throw new Error("not ok");
35
+ } catch {
36
+ console.error(chalk.red(`\n Cannot reach trickle backend at ${chalk.bold(backendUrl)}\n`));
37
+ process.exit(1);
38
+ }
39
+
40
+ // Read JSON input
41
+ let jsonText: string;
42
+ let sourceName: string;
43
+
44
+ if (!file || file === "-") {
45
+ // Read from stdin
46
+ if (process.stdin.isTTY) {
47
+ console.error(chalk.red("\n No input provided."));
48
+ console.error(chalk.gray(" Pipe JSON via stdin or provide a file path:\n"));
49
+ console.error(chalk.gray(' echo \'{"key":"value"}\' | trickle infer --name "GET /api/data"'));
50
+ console.error(chalk.gray(' trickle infer response.json --name "GET /api/data"\n'));
51
+ process.exit(1);
52
+ }
53
+ jsonText = await readStdin();
54
+ sourceName = "stdin";
55
+ } else {
56
+ // Read from file
57
+ if (!fs.existsSync(file)) {
58
+ console.error(chalk.red(`\n File not found: ${file}\n`));
59
+ process.exit(1);
60
+ }
61
+ jsonText = fs.readFileSync(file, "utf-8");
62
+ sourceName = file;
63
+ }
64
+
65
+ // Parse JSON
66
+ let jsonData: unknown;
67
+ try {
68
+ jsonData = JSON.parse(jsonText.trim());
69
+ } catch {
70
+ console.error(chalk.red("\n Input is not valid JSON.\n"));
71
+ process.exit(1);
72
+ }
73
+
74
+ // Build type observations
75
+ const functionName = opts.name;
76
+ const returnType = jsonToTypeNode(jsonData);
77
+
78
+ const argsProperties: Record<string, TypeNode> = {};
79
+
80
+ // Parse request body example if provided
81
+ if (opts.requestBody) {
82
+ try {
83
+ const reqJson = JSON.parse(opts.requestBody);
84
+ argsProperties.body = jsonToTypeNode(reqJson);
85
+ } catch {
86
+ console.error(chalk.red("\n --request-body is not valid JSON.\n"));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ const argsType: TypeNode = Object.keys(argsProperties).length > 0
92
+ ? { kind: "object", properties: argsProperties }
93
+ : { kind: "object", properties: {} };
94
+
95
+ const typeHash = computeTypeHash(argsType, returnType);
96
+
97
+ const payload = {
98
+ functionName,
99
+ module: opts.module || "infer",
100
+ language: "js",
101
+ environment: opts.env || "development",
102
+ typeHash,
103
+ argsType,
104
+ returnType,
105
+ sampleInput: Object.keys(argsProperties).length > 0 ? argsProperties : undefined,
106
+ sampleOutput: jsonData,
107
+ };
108
+
109
+ // Send to backend
110
+ try {
111
+ const res = await fetch(`${backendUrl}/api/ingest`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(payload),
115
+ signal: AbortSignal.timeout(5000),
116
+ });
117
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
118
+ } catch (err: unknown) {
119
+ const msg = err instanceof Error ? err.message : "Unknown error";
120
+ console.error(chalk.red(`\n Failed to send types to backend: ${msg}\n`));
121
+ process.exit(1);
122
+ }
123
+
124
+ console.log("");
125
+ console.log(chalk.bold(" trickle infer"));
126
+ console.log(chalk.gray(" " + "─".repeat(50)));
127
+ console.log(chalk.gray(` Source: `) + chalk.white(sourceName));
128
+ console.log(chalk.gray(` Name: `) + chalk.white(functionName));
129
+ console.log(chalk.gray(` Backend: `) + chalk.white(backendUrl));
130
+ console.log(chalk.gray(" " + "─".repeat(50)));
131
+ console.log(chalk.green(" Types inferred and stored successfully!"));
132
+
133
+ // Show a preview of the inferred shape
134
+ const shape = describeShape(returnType, 1);
135
+ console.log("");
136
+ console.log(chalk.gray(" Inferred shape:"));
137
+ for (const line of shape) {
138
+ console.log(chalk.gray(" ") + line);
139
+ }
140
+
141
+ if (opts.requestBody) {
142
+ const reqShape = describeShape(argsProperties.body, 1);
143
+ console.log("");
144
+ console.log(chalk.gray(" Request body shape:"));
145
+ for (const line of reqShape) {
146
+ console.log(chalk.gray(" ") + line);
147
+ }
148
+ }
149
+
150
+ console.log("");
151
+ console.log(chalk.gray(" Run ") + chalk.white("trickle codegen") + chalk.gray(" to generate type definitions."));
152
+ console.log("");
153
+ }
154
+
155
+ function readStdin(): Promise<string> {
156
+ return new Promise((resolve, reject) => {
157
+ let data = "";
158
+ process.stdin.setEncoding("utf-8");
159
+ process.stdin.on("data", (chunk) => { data += chunk; });
160
+ process.stdin.on("end", () => resolve(data));
161
+ process.stdin.on("error", reject);
162
+ // Timeout after 10 seconds
163
+ setTimeout(() => {
164
+ if (data.length === 0) {
165
+ reject(new Error("No input received from stdin"));
166
+ } else {
167
+ resolve(data);
168
+ }
169
+ }, 10000);
170
+ });
171
+ }
172
+
173
+ function jsonToTypeNode(value: unknown): TypeNode {
174
+ if (value === null) return { kind: "primitive", name: "null" };
175
+ if (value === undefined) return { kind: "primitive", name: "undefined" };
176
+
177
+ switch (typeof value) {
178
+ case "string": return { kind: "primitive", name: "string" };
179
+ case "number": return { kind: "primitive", name: "number" };
180
+ case "boolean": return { kind: "primitive", name: "boolean" };
181
+ }
182
+
183
+ if (Array.isArray(value)) {
184
+ if (value.length === 0) return { kind: "array", element: { kind: "unknown" } };
185
+ return { kind: "array", element: jsonToTypeNode(value[0]) };
186
+ }
187
+
188
+ const obj = value as Record<string, unknown>;
189
+ const properties: Record<string, TypeNode> = {};
190
+ for (const [key, val] of Object.entries(obj)) {
191
+ properties[key] = jsonToTypeNode(val);
192
+ }
193
+ return { kind: "object", properties };
194
+ }
195
+
196
+ function computeTypeHash(argsType: TypeNode, returnType: TypeNode): string {
197
+ const data = JSON.stringify({ a: argsType, r: returnType });
198
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
199
+ }
200
+
201
+ /**
202
+ * Describe a TypeNode shape as human-readable lines.
203
+ */
204
+ function describeShape(node: TypeNode, maxDepth: number, depth: number = 0): string[] {
205
+ const indent = " ".repeat(depth);
206
+ const lines: string[] = [];
207
+
208
+ if (node.kind === "primitive") {
209
+ lines.push(indent + chalk.cyan(node.name as string));
210
+ } else if (node.kind === "array") {
211
+ const element = node.element as TypeNode;
212
+ if (element.kind === "object" && depth < maxDepth) {
213
+ lines.push(indent + chalk.yellow("Array<{"));
214
+ const subLines = describeShape(element, maxDepth, depth + 1);
215
+ lines.push(...subLines);
216
+ lines.push(indent + chalk.yellow("}>"));
217
+ } else {
218
+ lines.push(indent + chalk.yellow(`${describeTypeCompact(element)}[]`));
219
+ }
220
+ } else if (node.kind === "object") {
221
+ const props = node.properties as Record<string, TypeNode>;
222
+ const keys = Object.keys(props);
223
+ if (keys.length === 0) {
224
+ lines.push(indent + chalk.gray("{}"));
225
+ } else {
226
+ for (const key of keys) {
227
+ const propType = props[key];
228
+ const typeStr = describeTypeCompact(propType);
229
+ lines.push(indent + chalk.white(key) + chalk.gray(": ") + chalk.cyan(typeStr));
230
+ }
231
+ }
232
+ } else {
233
+ lines.push(indent + chalk.gray(node.kind));
234
+ }
235
+
236
+ return lines;
237
+ }
238
+
239
+ function describeTypeCompact(node: TypeNode): string {
240
+ switch (node.kind) {
241
+ case "primitive":
242
+ return node.name as string;
243
+ case "array": {
244
+ const element = node.element as TypeNode;
245
+ return `${describeTypeCompact(element)}[]`;
246
+ }
247
+ case "object": {
248
+ const props = node.properties as Record<string, TypeNode>;
249
+ const keys = Object.keys(props);
250
+ if (keys.length === 0) return "{}";
251
+ if (keys.length <= 3) {
252
+ const inner = keys.map((k) => `${k}: ${describeTypeCompact(props[k])}`).join(", ");
253
+ return `{ ${inner} }`;
254
+ }
255
+ return `{ ${keys.slice(0, 2).map((k) => `${k}: ${describeTypeCompact(props[k])}`).join(", ")}, ... }`;
256
+ }
257
+ default:
258
+ return node.kind;
259
+ }
260
+ }