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,310 @@
1
+ import chalk from "chalk";
2
+ import { listFunctions, listTypes, FunctionRow, TypeSnapshot } from "../api-client";
3
+ import { getBackendUrl } from "../config";
4
+ import { relativeTime } from "../ui/helpers";
5
+
6
+ export interface OverviewOptions {
7
+ env?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ interface TypeNode {
12
+ kind: string;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ interface RouteInfo {
17
+ name: string;
18
+ method: string;
19
+ path: string;
20
+ module: string;
21
+ environment: string;
22
+ lastSeen: string;
23
+ argsSignature: string;
24
+ returnSignature: string;
25
+ fieldCount: number;
26
+ }
27
+
28
+ /**
29
+ * `trickle overview` — Compact API overview with inline type signatures.
30
+ *
31
+ * Shows all observed routes with their return type shapes, making it easy to
32
+ * understand your entire API surface at a glance. Like `git log --oneline` for APIs.
33
+ */
34
+ export async function overviewCommand(opts: OverviewOptions): Promise<void> {
35
+ const backendUrl = getBackendUrl();
36
+
37
+ // Check backend
38
+ try {
39
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
40
+ if (!res.ok) throw new Error("not ok");
41
+ } catch {
42
+ console.error(chalk.red(`\n Cannot reach trickle backend at ${chalk.bold(backendUrl)}\n`));
43
+ process.exit(1);
44
+ }
45
+
46
+ // Fetch all functions
47
+ const result = await listFunctions({ env: opts.env, limit: 500 });
48
+ const { functions } = result;
49
+
50
+ if (functions.length === 0) {
51
+ console.log(chalk.yellow("\n No observed routes yet."));
52
+ console.log(chalk.gray(" Run ") + chalk.white("trickle capture") + chalk.gray(" or ") + chalk.white("trickle dev") + chalk.gray(" to start observing.\n"));
53
+ return;
54
+ }
55
+
56
+ // Fetch latest type snapshot for each function
57
+ const routes: RouteInfo[] = [];
58
+ for (const fn of functions) {
59
+ try {
60
+ const typesResult = await listTypes(fn.id, { env: opts.env, limit: 1 });
61
+ const snapshot = typesResult.snapshots[0];
62
+
63
+ const returnType = snapshot
64
+ ? (typeof snapshot.return_type === "string"
65
+ ? JSON.parse(snapshot.return_type)
66
+ : snapshot.return_type) as TypeNode
67
+ : null;
68
+
69
+ const argsType = snapshot
70
+ ? (typeof snapshot.args_type === "string"
71
+ ? JSON.parse(snapshot.args_type)
72
+ : snapshot.args_type) as TypeNode
73
+ : null;
74
+
75
+ const { method, path: routePath } = parseRoute(fn.function_name);
76
+
77
+ routes.push({
78
+ name: fn.function_name,
79
+ method,
80
+ path: routePath,
81
+ module: fn.module,
82
+ environment: fn.environment,
83
+ lastSeen: fn.last_seen_at,
84
+ argsSignature: argsType ? compactSignature(argsType, 60) : "",
85
+ returnSignature: returnType ? compactSignature(returnType, 60) : "unknown",
86
+ fieldCount: returnType ? countFields(returnType) : 0,
87
+ });
88
+ } catch {
89
+ // Skip functions with errors
90
+ const { method, path: routePath } = parseRoute(fn.function_name);
91
+ routes.push({
92
+ name: fn.function_name,
93
+ method,
94
+ path: routePath,
95
+ module: fn.module,
96
+ environment: fn.environment,
97
+ lastSeen: fn.last_seen_at,
98
+ argsSignature: "",
99
+ returnSignature: "?",
100
+ fieldCount: 0,
101
+ });
102
+ }
103
+ }
104
+
105
+ // JSON output
106
+ if (opts.json) {
107
+ console.log(JSON.stringify({ routes, total: routes.length }, null, 2));
108
+ return;
109
+ }
110
+
111
+ // Group by module
112
+ const byModule = new Map<string, RouteInfo[]>();
113
+ for (const r of routes) {
114
+ const mod = r.module || "default";
115
+ if (!byModule.has(mod)) byModule.set(mod, []);
116
+ byModule.get(mod)!.push(r);
117
+ }
118
+
119
+ console.log("");
120
+ console.log(chalk.bold(" trickle overview"));
121
+ console.log(chalk.gray(" " + "─".repeat(60)));
122
+ console.log(chalk.gray(` ${routes.length} route${routes.length === 1 ? "" : "s"} observed`));
123
+ if (opts.env) {
124
+ console.log(chalk.gray(` Environment: ${opts.env}`));
125
+ }
126
+ console.log(chalk.gray(" " + "─".repeat(60)));
127
+
128
+ // Find the longest method for alignment
129
+ const maxMethodLen = Math.max(...routes.map((r) => r.method.length));
130
+ const maxPathLen = Math.min(30, Math.max(...routes.map((r) => r.path.length)));
131
+
132
+ for (const [mod, modRoutes] of byModule) {
133
+ console.log("");
134
+ if (byModule.size > 1) {
135
+ console.log(chalk.gray(` ┌─ ${mod}`));
136
+ }
137
+
138
+ // Sort routes: GET before POST before PUT before DELETE, then by path
139
+ const methodOrder: Record<string, number> = { GET: 0, POST: 1, PUT: 2, PATCH: 3, DELETE: 4 };
140
+ modRoutes.sort((a, b) => {
141
+ const orderA = methodOrder[a.method] ?? 5;
142
+ const orderB = methodOrder[b.method] ?? 5;
143
+ if (orderA !== orderB) return orderA - orderB;
144
+ return a.path.localeCompare(b.path);
145
+ });
146
+
147
+ for (const route of modRoutes) {
148
+ const methodColor = getMethodColor(route.method);
149
+ const methodStr = route.method.padEnd(maxMethodLen);
150
+ const pathStr = route.path.padEnd(maxPathLen);
151
+ const age = relativeTime(route.lastSeen);
152
+
153
+ // Build the line
154
+ const prefix = byModule.size > 1 ? " │ " : " ";
155
+ const line =
156
+ prefix +
157
+ methodColor(methodStr) +
158
+ " " +
159
+ chalk.white(pathStr) +
160
+ chalk.gray(" → ") +
161
+ chalk.cyan(route.returnSignature) +
162
+ chalk.gray(` ${age}`);
163
+
164
+ console.log(line);
165
+
166
+ // Show request body if present and non-empty
167
+ if (route.argsSignature && route.argsSignature !== "{ }") {
168
+ const argsLine =
169
+ prefix +
170
+ " ".repeat(maxMethodLen) +
171
+ " " +
172
+ " ".repeat(maxPathLen) +
173
+ chalk.gray(" ← ") +
174
+ chalk.yellow(route.argsSignature);
175
+ console.log(argsLine);
176
+ }
177
+ }
178
+
179
+ if (byModule.size > 1) {
180
+ console.log(chalk.gray(" └─"));
181
+ }
182
+ }
183
+
184
+ console.log("");
185
+ const totalFields = routes.reduce((sum, r) => sum + r.fieldCount, 0);
186
+ console.log(
187
+ chalk.gray(` ${routes.length} routes, ${totalFields} fields observed`) +
188
+ chalk.gray(` · ${backendUrl}`),
189
+ );
190
+ console.log("");
191
+ }
192
+
193
+ function parseRoute(functionName: string): { method: string; path: string } {
194
+ const spaceIdx = functionName.indexOf(" ");
195
+ if (spaceIdx > 0) {
196
+ return {
197
+ method: functionName.slice(0, spaceIdx),
198
+ path: functionName.slice(spaceIdx + 1),
199
+ };
200
+ }
201
+ return { method: "", path: functionName };
202
+ }
203
+
204
+ function getMethodColor(method: string): (s: string) => string {
205
+ switch (method) {
206
+ case "GET": return chalk.green;
207
+ case "POST": return chalk.yellow;
208
+ case "PUT": return chalk.blue;
209
+ case "PATCH": return chalk.magenta;
210
+ case "DELETE": return chalk.red;
211
+ default: return chalk.white;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Render a compact type signature from a TypeNode.
217
+ * Truncates to maxLen characters.
218
+ */
219
+ function compactSignature(node: TypeNode, maxLen: number): string {
220
+ const sig = renderCompact(node);
221
+ if (sig.length <= maxLen) return sig;
222
+ return sig.slice(0, maxLen - 1) + "…";
223
+ }
224
+
225
+ function renderCompact(node: TypeNode): string {
226
+ switch (node.kind) {
227
+ case "primitive":
228
+ return node.name as string;
229
+
230
+ case "object": {
231
+ const props = node.properties as Record<string, TypeNode>;
232
+ const keys = Object.keys(props);
233
+ if (keys.length === 0) return "{ }";
234
+
235
+ const parts: string[] = [];
236
+ for (const key of keys) {
237
+ const val = props[key];
238
+ const valStr = renderCompactShort(val);
239
+ parts.push(`${key}: ${valStr}`);
240
+ }
241
+
242
+ const full = `{ ${parts.join(", ")} }`;
243
+ if (full.length <= 60) return full;
244
+
245
+ // Truncate: show first few fields
246
+ let result = "{ ";
247
+ for (let i = 0; i < parts.length; i++) {
248
+ if (i > 0) result += ", ";
249
+ if (result.length + parts[i].length > 55 && i > 0) {
250
+ result += `…+${parts.length - i}`;
251
+ break;
252
+ }
253
+ result += parts[i];
254
+ }
255
+ return result + " }";
256
+ }
257
+
258
+ case "array": {
259
+ const element = node.element as TypeNode;
260
+ return `${renderCompact(element)}[]`;
261
+ }
262
+
263
+ case "union": {
264
+ const members = node.members as TypeNode[];
265
+ return members.map(renderCompactShort).join(" | ");
266
+ }
267
+
268
+ default:
269
+ return node.kind;
270
+ }
271
+ }
272
+
273
+ function renderCompactShort(node: TypeNode): string {
274
+ switch (node.kind) {
275
+ case "primitive":
276
+ return node.name as string;
277
+ case "object": {
278
+ const props = node.properties as Record<string, TypeNode>;
279
+ const keys = Object.keys(props);
280
+ if (keys.length === 0) return "{}";
281
+ if (keys.length <= 3) {
282
+ return `{${keys.join(", ")}}`;
283
+ }
284
+ return `{${keys.slice(0, 2).join(", ")}, …+${keys.length - 2}}`;
285
+ }
286
+ case "array":
287
+ return `${renderCompactShort(node.element as TypeNode)}[]`;
288
+ case "union": {
289
+ const members = node.members as TypeNode[];
290
+ return members.map(renderCompactShort).join(" | ");
291
+ }
292
+ default:
293
+ return node.kind;
294
+ }
295
+ }
296
+
297
+ function countFields(node: TypeNode): number {
298
+ if (node.kind === "object") {
299
+ const props = node.properties as Record<string, TypeNode>;
300
+ let count = Object.keys(props).length;
301
+ for (const val of Object.values(props)) {
302
+ count += countFields(val);
303
+ }
304
+ return count;
305
+ }
306
+ if (node.kind === "array") {
307
+ return countFields(node.element as TypeNode);
308
+ }
309
+ return 0;
310
+ }
@@ -0,0 +1,139 @@
1
+ import * as fs from "fs";
2
+ import chalk from "chalk";
3
+ import { listFunctions, listTypes } from "../api-client";
4
+ import { getBackendUrl } from "../config";
5
+
6
+ export interface PackOptions {
7
+ out?: string;
8
+ env?: string;
9
+ }
10
+
11
+ interface PackedFunction {
12
+ functionName: string;
13
+ module: string;
14
+ language: string;
15
+ environment: string;
16
+ snapshots: PackedSnapshot[];
17
+ }
18
+
19
+ interface PackedSnapshot {
20
+ typeHash: string;
21
+ env: string;
22
+ argsType: unknown;
23
+ returnType: unknown;
24
+ sampleInput?: unknown;
25
+ sampleOutput?: unknown;
26
+ observedAt: string;
27
+ }
28
+
29
+ interface PackBundle {
30
+ version: 1;
31
+ createdAt: string;
32
+ source: string;
33
+ functions: PackedFunction[];
34
+ stats: {
35
+ totalFunctions: number;
36
+ totalSnapshots: number;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * `trickle pack` — Export all observed types as a portable bundle.
42
+ *
43
+ * Creates a JSON file containing all functions and their type snapshots
44
+ * that can be shared, committed to version control, or imported elsewhere.
45
+ */
46
+ export async function packCommand(opts: PackOptions): Promise<void> {
47
+ const backendUrl = getBackendUrl();
48
+
49
+ // Check backend
50
+ try {
51
+ const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
52
+ if (!res.ok) throw new Error("not ok");
53
+ } catch {
54
+ console.error(chalk.red(`\n Cannot reach trickle backend at ${chalk.bold(backendUrl)}\n`));
55
+ process.exit(1);
56
+ }
57
+
58
+ // Use stderr for status when writing JSON to stdout
59
+ const log = opts.out ? console.log : (...args: unknown[]) => process.stderr.write(args.join(" ") + "\n");
60
+
61
+ log("");
62
+ log(chalk.bold(" trickle pack"));
63
+ log(chalk.gray(" " + "─".repeat(50)));
64
+
65
+ // Fetch all functions
66
+ const result = await listFunctions({ env: opts.env, limit: 10000 });
67
+ const { functions } = result;
68
+
69
+ if (functions.length === 0) {
70
+ log(chalk.yellow(" No observed types to pack."));
71
+ log(chalk.gray(" Run ") + chalk.white("trickle capture") + chalk.gray(" or ") + chalk.white("trickle dev") + chalk.gray(" first.\n"));
72
+ process.exit(1);
73
+ }
74
+
75
+ log(chalk.gray(` Packing ${functions.length} functions...`));
76
+
77
+ // Fetch snapshots for each function
78
+ const packedFunctions: PackedFunction[] = [];
79
+ let totalSnapshots = 0;
80
+
81
+ for (const fn of functions) {
82
+ const typesResult = await listTypes(fn.id, { env: opts.env, limit: 100 });
83
+ const snapshots: PackedSnapshot[] = [];
84
+
85
+ for (const snap of typesResult.snapshots) {
86
+ snapshots.push({
87
+ typeHash: snap.type_hash,
88
+ env: snap.env,
89
+ argsType: snap.args_type,
90
+ returnType: snap.return_type,
91
+ sampleInput: snap.sample_input || undefined,
92
+ sampleOutput: snap.sample_output || undefined,
93
+ observedAt: snap.observed_at,
94
+ });
95
+ }
96
+
97
+ if (snapshots.length > 0) {
98
+ packedFunctions.push({
99
+ functionName: fn.function_name,
100
+ module: fn.module,
101
+ language: fn.language,
102
+ environment: fn.environment,
103
+ snapshots,
104
+ });
105
+ totalSnapshots += snapshots.length;
106
+ }
107
+ }
108
+
109
+ const bundle: PackBundle = {
110
+ version: 1,
111
+ createdAt: new Date().toISOString(),
112
+ source: backendUrl,
113
+ functions: packedFunctions,
114
+ stats: {
115
+ totalFunctions: packedFunctions.length,
116
+ totalSnapshots,
117
+ },
118
+ };
119
+
120
+ const json = JSON.stringify(bundle, null, 2);
121
+
122
+ if (opts.out) {
123
+ fs.writeFileSync(opts.out, json, "utf-8");
124
+ log(chalk.green(` Packed ${packedFunctions.length} functions (${totalSnapshots} snapshots)`));
125
+ log(chalk.gray(` Written to ${opts.out}`));
126
+ const sizeKb = (Buffer.byteLength(json, "utf-8") / 1024).toFixed(1);
127
+ log(chalk.gray(` Size: ${sizeKb}KB`));
128
+ log("");
129
+ log(chalk.gray(" Share this file or import it with:"));
130
+ log(chalk.white(` trickle unpack ${opts.out}`));
131
+ log("");
132
+ } else {
133
+ // Write JSON to stdout for piping
134
+ process.stdout.write(json + "\n");
135
+ // Summary to stderr
136
+ log(chalk.green(` Packed ${packedFunctions.length} functions (${totalSnapshots} snapshots)`));
137
+ log("");
138
+ }
139
+ }