trickle-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +208 -0
- package/dist/api-client.js +237 -0
- package/dist/commands/annotate.d.ts +6 -0
- package/dist/commands/annotate.js +433 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/auto.d.ts +8 -0
- package/dist/commands/auto.js +268 -0
- package/dist/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +271 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +408 -0
- package/dist/commands/codegen.d.ts +21 -0
- package/dist/commands/codegen.js +129 -0
- package/dist/commands/coverage.d.ts +13 -0
- package/dist/commands/coverage.js +126 -0
- package/dist/commands/dashboard.d.ts +1 -0
- package/dist/commands/dashboard.js +83 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.js +319 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.js +79 -0
- package/dist/commands/docs.d.ts +13 -0
- package/dist/commands/docs.js +383 -0
- package/dist/commands/errors.d.ts +7 -0
- package/dist/commands/errors.js +180 -0
- package/dist/commands/export.d.ts +18 -0
- package/dist/commands/export.js +238 -0
- package/dist/commands/functions.d.ts +6 -0
- package/dist/commands/functions.js +71 -0
- package/dist/commands/infer.d.ts +14 -0
- package/dist/commands/infer.js +275 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +395 -0
- package/dist/commands/mock.d.ts +5 -0
- package/dist/commands/mock.js +232 -0
- package/dist/commands/openapi.d.ts +8 -0
- package/dist/commands/openapi.js +82 -0
- package/dist/commands/overview.d.ts +11 -0
- package/dist/commands/overview.js +266 -0
- package/dist/commands/pack.d.ts +11 -0
- package/dist/commands/pack.js +133 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +312 -0
- package/dist/commands/replay.d.ts +14 -0
- package/dist/commands/replay.js +289 -0
- package/dist/commands/run.d.ts +17 -0
- package/dist/commands/run.js +997 -0
- package/dist/commands/sample.d.ts +13 -0
- package/dist/commands/sample.js +260 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +80 -0
- package/dist/commands/stubs.d.ts +6 -0
- package/dist/commands/stubs.js +187 -0
- package/dist/commands/tail.d.ts +4 -0
- package/dist/commands/tail.js +76 -0
- package/dist/commands/test-gen.d.ts +13 -0
- package/dist/commands/test-gen.js +237 -0
- package/dist/commands/trace.d.ts +14 -0
- package/dist/commands/trace.js +417 -0
- package/dist/commands/types.d.ts +7 -0
- package/dist/commands/types.js +128 -0
- package/dist/commands/unpack.d.ts +11 -0
- package/dist/commands/unpack.js +166 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +310 -0
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.js +267 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +66 -0
- package/dist/formatters/diff-formatter.d.ts +5 -0
- package/dist/formatters/diff-formatter.js +43 -0
- package/dist/formatters/type-formatter.d.ts +22 -0
- package/dist/formatters/type-formatter.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +419 -0
- package/dist/local-codegen.d.ts +22 -0
- package/dist/local-codegen.js +762 -0
- package/dist/ui/badges.d.ts +16 -0
- package/dist/ui/badges.js +71 -0
- package/dist/ui/helpers.d.ts +13 -0
- package/dist/ui/helpers.js +85 -0
- package/package.json +23 -0
- package/src/api-client.ts +407 -0
- package/src/commands/annotate.ts +450 -0
- package/src/commands/audit.ts +103 -0
- package/src/commands/auto.ts +268 -0
- package/src/commands/capture.ts +257 -0
- package/src/commands/check.ts +437 -0
- package/src/commands/codegen.ts +128 -0
- package/src/commands/coverage.ts +170 -0
- package/src/commands/dashboard.ts +46 -0
- package/src/commands/dev.ts +323 -0
- package/src/commands/diff.ts +99 -0
- package/src/commands/docs.ts +392 -0
- package/src/commands/errors.ts +205 -0
- package/src/commands/export.ts +287 -0
- package/src/commands/functions.ts +81 -0
- package/src/commands/infer.ts +260 -0
- package/src/commands/init.ts +419 -0
- package/src/commands/mock.ts +220 -0
- package/src/commands/openapi.ts +53 -0
- package/src/commands/overview.ts +310 -0
- package/src/commands/pack.ts +139 -0
- package/src/commands/proxy.ts +314 -0
- package/src/commands/replay.ts +356 -0
- package/src/commands/run.ts +1190 -0
- package/src/commands/sample.ts +259 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/stubs.ts +211 -0
- package/src/commands/tail.ts +94 -0
- package/src/commands/test-gen.ts +236 -0
- package/src/commands/trace.ts +440 -0
- package/src/commands/types.ts +161 -0
- package/src/commands/unpack.ts +179 -0
- package/src/commands/validate.ts +368 -0
- package/src/commands/watch.ts +277 -0
- package/src/config.ts +38 -0
- package/src/formatters/diff-formatter.ts +51 -0
- package/src/formatters/type-formatter.ts +161 -0
- package/src/index.ts +454 -0
- package/src/local-codegen.ts +859 -0
- package/src/ui/badges.ts +66 -0
- package/src/ui/helpers.ts +80 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SampleOptions {
|
|
2
|
+
format?: string;
|
|
3
|
+
out?: string;
|
|
4
|
+
route?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `trickle sample` — Generate test fixtures from observed runtime data.
|
|
8
|
+
*
|
|
9
|
+
* Produces JSON, TypeScript constants, or factory functions from the
|
|
10
|
+
* actual sample inputs and outputs captured by trickle. Great for tests,
|
|
11
|
+
* seed scripts, and Storybook data.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sampleCommand(routeFilter: string | undefined, opts: SampleOptions): Promise<void>;
|
|
@@ -0,0 +1,260 @@
|
|
|
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.sampleCommand = sampleCommand;
|
|
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 sample` — Generate test fixtures from observed runtime data.
|
|
46
|
+
*
|
|
47
|
+
* Produces JSON, TypeScript constants, or factory functions from the
|
|
48
|
+
* actual sample inputs and outputs captured by trickle. Great for tests,
|
|
49
|
+
* seed scripts, and Storybook data.
|
|
50
|
+
*/
|
|
51
|
+
async function sampleCommand(routeFilter, opts) {
|
|
52
|
+
let routes;
|
|
53
|
+
try {
|
|
54
|
+
const config = await (0, api_client_1.fetchMockConfig)();
|
|
55
|
+
routes = config.routes;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
console.error(chalk_1.default.red("\n Cannot connect to trickle backend."));
|
|
59
|
+
console.error(chalk_1.default.gray(" Is the backend running?\n"));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Filter routes
|
|
63
|
+
if (routeFilter) {
|
|
64
|
+
const filter = routeFilter.toLowerCase();
|
|
65
|
+
routes = routes.filter((r) => r.functionName.toLowerCase().includes(filter) ||
|
|
66
|
+
r.path.toLowerCase().includes(filter));
|
|
67
|
+
}
|
|
68
|
+
// Only include routes with sample data
|
|
69
|
+
routes = routes.filter((r) => r.sampleOutput !== null && r.sampleOutput !== undefined);
|
|
70
|
+
if (routes.length === 0) {
|
|
71
|
+
console.error(chalk_1.default.yellow("\n No sample data found."));
|
|
72
|
+
if (routeFilter) {
|
|
73
|
+
console.error(chalk_1.default.gray(` No routes matching "${routeFilter}" with sample data.\n`));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(chalk_1.default.gray(" Instrument your app and make some requests first.\n"));
|
|
77
|
+
}
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
const format = (opts.format || "json").toLowerCase();
|
|
81
|
+
let output;
|
|
82
|
+
switch (format) {
|
|
83
|
+
case "json":
|
|
84
|
+
output = generateJson(routes);
|
|
85
|
+
break;
|
|
86
|
+
case "ts":
|
|
87
|
+
case "typescript":
|
|
88
|
+
output = generateTypeScript(routes);
|
|
89
|
+
break;
|
|
90
|
+
case "factory":
|
|
91
|
+
output = generateFactories(routes);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
console.error(chalk_1.default.red(`\n Unknown format: ${format}`));
|
|
95
|
+
console.error(chalk_1.default.gray(" Supported: json, ts, factory\n"));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
if (opts.out) {
|
|
99
|
+
const resolvedPath = path.resolve(opts.out);
|
|
100
|
+
const dir = path.dirname(resolvedPath);
|
|
101
|
+
if (!fs.existsSync(dir)) {
|
|
102
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
fs.writeFileSync(resolvedPath, output, "utf-8");
|
|
105
|
+
console.log(chalk_1.default.green(`\n Fixtures written to ${chalk_1.default.bold(opts.out)}`));
|
|
106
|
+
console.log(chalk_1.default.gray(` ${routes.length} routes, format: ${format}\n`));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log(output);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function toPascalCase(name) {
|
|
113
|
+
return name
|
|
114
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
117
|
+
.join("");
|
|
118
|
+
}
|
|
119
|
+
function toCamelCase(name) {
|
|
120
|
+
const pascal = toPascalCase(name);
|
|
121
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
122
|
+
}
|
|
123
|
+
// ── JSON format ──
|
|
124
|
+
function generateJson(routes) {
|
|
125
|
+
const samples = {};
|
|
126
|
+
for (const route of routes) {
|
|
127
|
+
const key = `${route.method} ${route.path}`;
|
|
128
|
+
const entry = {
|
|
129
|
+
response: route.sampleOutput,
|
|
130
|
+
};
|
|
131
|
+
if (route.sampleInput) {
|
|
132
|
+
const input = route.sampleInput;
|
|
133
|
+
const body = input.body || input;
|
|
134
|
+
if (body && typeof body === "object" && Object.keys(body).length > 0) {
|
|
135
|
+
entry.request = body;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
samples[key] = entry;
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(samples, null, 2) + "\n";
|
|
141
|
+
}
|
|
142
|
+
// ── TypeScript constants ──
|
|
143
|
+
function generateTypeScript(routes) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push("// Auto-generated test fixtures by trickle");
|
|
146
|
+
lines.push(`// Generated at ${new Date().toISOString()}`);
|
|
147
|
+
lines.push("// Do not edit manually — re-run `trickle sample --format ts` to update");
|
|
148
|
+
lines.push("");
|
|
149
|
+
for (const route of routes) {
|
|
150
|
+
const varName = toCamelCase(route.functionName);
|
|
151
|
+
// Response sample
|
|
152
|
+
lines.push(`/** Sample response for ${route.method} ${route.path} */`);
|
|
153
|
+
lines.push(`export const ${varName}Response = ${formatValue(route.sampleOutput, 0)} as const;`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
// Request body sample (for POST/PUT/PATCH)
|
|
156
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
157
|
+
const input = route.sampleInput;
|
|
158
|
+
const body = input.body || input;
|
|
159
|
+
if (body && typeof body === "object" && Object.keys(body).length > 0) {
|
|
160
|
+
lines.push(`/** Sample request body for ${route.method} ${route.path} */`);
|
|
161
|
+
lines.push(`export const ${varName}Request = ${formatValue(body, 0)} as const;`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
167
|
+
}
|
|
168
|
+
// ── Factory functions ──
|
|
169
|
+
function generateFactories(routes) {
|
|
170
|
+
const lines = [];
|
|
171
|
+
lines.push("// Auto-generated test fixture factories by trickle");
|
|
172
|
+
lines.push(`// Generated at ${new Date().toISOString()}`);
|
|
173
|
+
lines.push("// Do not edit manually — re-run `trickle sample --format factory` to update");
|
|
174
|
+
lines.push("");
|
|
175
|
+
// First emit the base samples as constants
|
|
176
|
+
for (const route of routes) {
|
|
177
|
+
const varName = toCamelCase(route.functionName);
|
|
178
|
+
lines.push(`const _${varName}Response = ${formatValue(route.sampleOutput, 0)};`);
|
|
179
|
+
lines.push("");
|
|
180
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
181
|
+
const input = route.sampleInput;
|
|
182
|
+
const body = input.body || input;
|
|
183
|
+
if (body && typeof body === "object" && Object.keys(body).length > 0) {
|
|
184
|
+
lines.push(`const _${varName}Request = ${formatValue(body, 0)};`);
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Then emit factory functions
|
|
190
|
+
for (const route of routes) {
|
|
191
|
+
const varName = toCamelCase(route.functionName);
|
|
192
|
+
const typeName = toPascalCase(route.functionName);
|
|
193
|
+
// Response factory
|
|
194
|
+
if (route.sampleOutput && typeof route.sampleOutput === "object" && !Array.isArray(route.sampleOutput)) {
|
|
195
|
+
lines.push(`/** Create a test fixture for ${route.method} ${route.path} response */`);
|
|
196
|
+
lines.push(`export function create${typeName}Response(overrides?: Partial<typeof _${varName}Response>): typeof _${varName}Response {`);
|
|
197
|
+
lines.push(` return { ..._${varName}Response, ...overrides };`);
|
|
198
|
+
lines.push(`}`);
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// For non-object responses (arrays, primitives), just export the constant
|
|
203
|
+
lines.push(`/** Sample response for ${route.method} ${route.path} */`);
|
|
204
|
+
lines.push(`export const ${varName}Response = _${varName}Response;`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
}
|
|
207
|
+
// Request body factory
|
|
208
|
+
if (["POST", "PUT", "PATCH"].includes(route.method) && route.sampleInput) {
|
|
209
|
+
const input = route.sampleInput;
|
|
210
|
+
const body = input.body || input;
|
|
211
|
+
if (body && typeof body === "object" && !Array.isArray(body) && Object.keys(body).length > 0) {
|
|
212
|
+
lines.push(`/** Create a test fixture for ${route.method} ${route.path} request body */`);
|
|
213
|
+
lines.push(`export function create${typeName}Request(overrides?: Partial<typeof _${varName}Request>): typeof _${varName}Request {`);
|
|
214
|
+
lines.push(` return { ..._${varName}Request, ...overrides };`);
|
|
215
|
+
lines.push(`}`);
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
221
|
+
}
|
|
222
|
+
// ── Value formatting ──
|
|
223
|
+
function formatValue(value, indent) {
|
|
224
|
+
const pad = " ".repeat(indent);
|
|
225
|
+
const innerPad = " ".repeat(indent + 1);
|
|
226
|
+
if (value === null)
|
|
227
|
+
return "null";
|
|
228
|
+
if (value === undefined)
|
|
229
|
+
return "undefined";
|
|
230
|
+
switch (typeof value) {
|
|
231
|
+
case "string":
|
|
232
|
+
return JSON.stringify(value);
|
|
233
|
+
case "number":
|
|
234
|
+
case "boolean":
|
|
235
|
+
return String(value);
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
if (value.length === 0)
|
|
239
|
+
return "[]";
|
|
240
|
+
if (value.length === 1 && typeof value[0] !== "object") {
|
|
241
|
+
return `[${formatValue(value[0], 0)}]`;
|
|
242
|
+
}
|
|
243
|
+
const items = value.map((v) => `${innerPad}${formatValue(v, indent + 1)}`);
|
|
244
|
+
return `[\n${items.join(",\n")}\n${pad}]`;
|
|
245
|
+
}
|
|
246
|
+
if (typeof value === "object") {
|
|
247
|
+
const obj = value;
|
|
248
|
+
const keys = Object.keys(obj);
|
|
249
|
+
if (keys.length === 0)
|
|
250
|
+
return "{}";
|
|
251
|
+
const entries = keys.map((key) => {
|
|
252
|
+
const formattedVal = formatValue(obj[key], indent + 1);
|
|
253
|
+
// Use identifier-safe keys without quotes, quoted otherwise
|
|
254
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
255
|
+
return `${innerPad}${safeKey}: ${formattedVal}`;
|
|
256
|
+
});
|
|
257
|
+
return `{\n${entries.join(",\n")}\n${pad}}`;
|
|
258
|
+
}
|
|
259
|
+
return JSON.stringify(value);
|
|
260
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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.searchCommand = searchCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
async function searchCommand(query, opts) {
|
|
10
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
11
|
+
const url = new URL("/api/search", backendUrl);
|
|
12
|
+
url.searchParams.set("q", query);
|
|
13
|
+
if (opts.env) {
|
|
14
|
+
url.searchParams.set("env", opts.env);
|
|
15
|
+
}
|
|
16
|
+
let data;
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(url.toString());
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.text();
|
|
21
|
+
throw new Error(`HTTP ${res.status}: ${body}`);
|
|
22
|
+
}
|
|
23
|
+
data = await res.json();
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err instanceof Error && err.message.startsWith("HTTP ")) {
|
|
27
|
+
console.error(chalk_1.default.red(`\n Error: ${err.message}\n`));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(chalk_1.default.red(`\n Cannot connect to trickle backend at ${chalk_1.default.bold(backendUrl)}.`));
|
|
31
|
+
console.error(chalk_1.default.red(" Is the backend running?\n"));
|
|
32
|
+
}
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
console.log(JSON.stringify(data, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log(chalk_1.default.bold(` Search: "${query}"`));
|
|
41
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
42
|
+
if (data.total === 0) {
|
|
43
|
+
console.log(chalk_1.default.gray(" No matches found.\n"));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log(chalk_1.default.gray(` ${data.total} function${data.total === 1 ? "" : "s"} matched\n`));
|
|
47
|
+
for (const result of data.results) {
|
|
48
|
+
// Function name with method coloring
|
|
49
|
+
const fnName = result.functionName;
|
|
50
|
+
const methodMatch = fnName.match(/^(GET|POST|PUT|PATCH|DELETE)\s/);
|
|
51
|
+
if (methodMatch) {
|
|
52
|
+
const method = methodMatch[1];
|
|
53
|
+
const rest = fnName.slice(method.length);
|
|
54
|
+
const methodColors = {
|
|
55
|
+
GET: chalk_1.default.green,
|
|
56
|
+
POST: chalk_1.default.yellow,
|
|
57
|
+
PUT: chalk_1.default.blue,
|
|
58
|
+
PATCH: chalk_1.default.cyan,
|
|
59
|
+
DELETE: chalk_1.default.red,
|
|
60
|
+
};
|
|
61
|
+
const colorFn = methodColors[method] || chalk_1.default.white;
|
|
62
|
+
console.log(` ${colorFn(chalk_1.default.bold(method))}${chalk_1.default.white(rest)}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(` ${chalk_1.default.white(chalk_1.default.bold(fnName))}`);
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk_1.default.gray(` module: ${result.module} env: ${result.environment}`));
|
|
68
|
+
// Show matching fields
|
|
69
|
+
for (const match of result.matches) {
|
|
70
|
+
const typeStr = match.typeName ? chalk_1.default.cyan(match.typeName) : chalk_1.default.gray(match.kind);
|
|
71
|
+
if (match.kind === "name") {
|
|
72
|
+
console.log(chalk_1.default.gray(" → ") + chalk_1.default.yellow("function name match"));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(chalk_1.default.gray(" → ") + chalk_1.default.white(match.path) + chalk_1.default.gray(": ") + typeStr);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log("");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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.stubsCommand = stubsCommand;
|
|
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
|
+
* Map of file extensions to their stub extension.
|
|
46
|
+
*/
|
|
47
|
+
const JS_EXTS = new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
48
|
+
const PY_EXTS = new Set([".py"]);
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a module name for matching against file stems.
|
|
51
|
+
* Module names from trickle may use dashes or underscores.
|
|
52
|
+
* File stems use whatever the OS has.
|
|
53
|
+
*/
|
|
54
|
+
function normalizeForMatch(name) {
|
|
55
|
+
return name.replace(/[-_]/g, "").toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively find all source files in a directory.
|
|
59
|
+
*/
|
|
60
|
+
function findSourceFiles(dir) {
|
|
61
|
+
const results = [];
|
|
62
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const fullPath = path.join(dir, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
// Skip node_modules, __pycache__, .git, etc.
|
|
67
|
+
if (entry.name === "node_modules" ||
|
|
68
|
+
entry.name === "__pycache__" ||
|
|
69
|
+
entry.name === ".git" ||
|
|
70
|
+
entry.name === "dist" ||
|
|
71
|
+
entry.name === "build" ||
|
|
72
|
+
entry.name === ".trickle") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
results.push(...findSourceFiles(fullPath));
|
|
76
|
+
}
|
|
77
|
+
else if (entry.isFile()) {
|
|
78
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
79
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) {
|
|
80
|
+
results.push(fullPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
async function stubsCommand(dir, opts) {
|
|
87
|
+
const targetDir = path.resolve(dir);
|
|
88
|
+
if (!fs.existsSync(targetDir)) {
|
|
89
|
+
console.error(chalk_1.default.red(`\n Directory not found: ${targetDir}\n`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (!fs.statSync(targetDir).isDirectory()) {
|
|
93
|
+
console.error(chalk_1.default.red(`\n Not a directory: ${targetDir}\n`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// Fetch per-module stubs from backend
|
|
97
|
+
const { stubs } = await (0, api_client_1.fetchStubs)({ env: opts.env });
|
|
98
|
+
if (!stubs || Object.keys(stubs).length === 0) {
|
|
99
|
+
if (!opts.silent) {
|
|
100
|
+
console.log(chalk_1.default.yellow("\n No observed types found. Run your code with trickle first.\n"));
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Find all source files in the target directory
|
|
105
|
+
const sourceFiles = findSourceFiles(targetDir);
|
|
106
|
+
if (sourceFiles.length === 0) {
|
|
107
|
+
if (!opts.silent) {
|
|
108
|
+
console.log(chalk_1.default.yellow(`\n No source files found in ${targetDir}\n`));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Build a map: normalized stem → source file path
|
|
113
|
+
const fileMap = new Map();
|
|
114
|
+
for (const filePath of sourceFiles) {
|
|
115
|
+
const ext = path.extname(filePath);
|
|
116
|
+
const stem = path.basename(filePath, ext);
|
|
117
|
+
const key = normalizeForMatch(stem);
|
|
118
|
+
if (!fileMap.has(key))
|
|
119
|
+
fileMap.set(key, []);
|
|
120
|
+
fileMap.get(key).push(filePath);
|
|
121
|
+
}
|
|
122
|
+
const written = [];
|
|
123
|
+
const writtenPaths = new Set();
|
|
124
|
+
const skipped = [];
|
|
125
|
+
for (const [moduleName, moduleStubs] of Object.entries(stubs)) {
|
|
126
|
+
const normalizedModule = normalizeForMatch(moduleName);
|
|
127
|
+
// Find matching source files
|
|
128
|
+
const matchingFiles = fileMap.get(normalizedModule);
|
|
129
|
+
if (!matchingFiles || matchingFiles.length === 0) {
|
|
130
|
+
skipped.push(moduleName);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const sourceFile of matchingFiles) {
|
|
134
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
135
|
+
const isPython = PY_EXTS.has(ext);
|
|
136
|
+
const stubContent = isPython ? moduleStubs.python : moduleStubs.ts;
|
|
137
|
+
const stubExt = isPython ? ".pyi" : ".d.ts";
|
|
138
|
+
// Generate stub file path next to source file
|
|
139
|
+
const sourceDir = path.dirname(sourceFile);
|
|
140
|
+
const sourceStem = path.basename(sourceFile, ext);
|
|
141
|
+
const stubPath = path.join(sourceDir, `${sourceStem}${stubExt}`);
|
|
142
|
+
// Skip if already written (multiple modules may normalize to same name)
|
|
143
|
+
if (writtenPaths.has(stubPath))
|
|
144
|
+
continue;
|
|
145
|
+
writtenPaths.add(stubPath);
|
|
146
|
+
if (opts.dryRun) {
|
|
147
|
+
const relPath = path.relative(process.cwd(), stubPath);
|
|
148
|
+
console.log(chalk_1.default.cyan(` Would create: ${relPath}`));
|
|
149
|
+
console.log(chalk_1.default.gray(` (from module "${moduleName}" → ${path.basename(sourceFile)})`));
|
|
150
|
+
written.push(relPath);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Write stub file
|
|
154
|
+
fs.writeFileSync(stubPath, stubContent, "utf-8");
|
|
155
|
+
const relPath = path.relative(process.cwd(), stubPath);
|
|
156
|
+
written.push(relPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Output summary
|
|
160
|
+
if (opts.silent)
|
|
161
|
+
return;
|
|
162
|
+
console.log();
|
|
163
|
+
if (opts.dryRun) {
|
|
164
|
+
console.log(chalk_1.default.cyan(" Dry run — no files written.\n"));
|
|
165
|
+
if (written.length > 0) {
|
|
166
|
+
console.log(chalk_1.default.gray(` ${written.length} stub file(s) would be created.\n`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (written.length > 0) {
|
|
170
|
+
console.log(chalk_1.default.green(` Generated ${written.length} type stub file(s):\n`));
|
|
171
|
+
for (const f of written) {
|
|
172
|
+
console.log(chalk_1.default.gray(` ${f}`));
|
|
173
|
+
}
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(chalk_1.default.gray(" Your IDE should now pick up these types automatically."));
|
|
176
|
+
console.log(chalk_1.default.gray(" Add *.d.ts / *.pyi to .gitignore if you don't want to commit them."));
|
|
177
|
+
console.log();
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log(chalk_1.default.yellow(" No matching source files found for observed modules.\n"));
|
|
181
|
+
if (skipped.length > 0) {
|
|
182
|
+
console.log(chalk_1.default.gray(` Observed modules: ${skipped.join(", ")}`));
|
|
183
|
+
console.log(chalk_1.default.gray(` Source files in: ${path.relative(process.cwd(), targetDir) || "."}`));
|
|
184
|
+
console.log(chalk_1.default.gray(" Make sure module names match file names (e.g., module 'helpers' → helpers.js)\n"));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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.tailCommand = tailCommand;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const api_client_1 = require("../api-client");
|
|
9
|
+
const badges_1 = require("../ui/badges");
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
function eventBadge(event) {
|
|
12
|
+
const lower = event.toLowerCase();
|
|
13
|
+
if (lower === "error" || lower.includes("error")) {
|
|
14
|
+
return chalk_1.default.bgRed.white.bold(" ERROR ");
|
|
15
|
+
}
|
|
16
|
+
if (lower === "new_type" || lower.includes("new")) {
|
|
17
|
+
return chalk_1.default.bgGreen.black.bold(" NEW_TYPE ");
|
|
18
|
+
}
|
|
19
|
+
if (lower === "type_changed" || lower.includes("changed")) {
|
|
20
|
+
return chalk_1.default.bgYellow.black.bold(" TYPE_CHANGED ");
|
|
21
|
+
}
|
|
22
|
+
if (lower === "ingest" || lower.includes("ingest")) {
|
|
23
|
+
return chalk_1.default.bgBlue.white.bold(" INGEST ");
|
|
24
|
+
}
|
|
25
|
+
return chalk_1.default.bgGray.white.bold(` ${event.toUpperCase()} `);
|
|
26
|
+
}
|
|
27
|
+
function formatEventDetail(event) {
|
|
28
|
+
const data = event.data;
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (data.functionName) {
|
|
31
|
+
parts.push(chalk_1.default.white.bold(String(data.functionName)));
|
|
32
|
+
}
|
|
33
|
+
if (data.module) {
|
|
34
|
+
parts.push(chalk_1.default.gray(`(${data.module})`));
|
|
35
|
+
}
|
|
36
|
+
if (data.env) {
|
|
37
|
+
parts.push((0, badges_1.envBadge)(String(data.env)));
|
|
38
|
+
}
|
|
39
|
+
if (data.error && typeof data.error === "object") {
|
|
40
|
+
const err = data.error;
|
|
41
|
+
if (err.message) {
|
|
42
|
+
parts.push(chalk_1.default.red(String(err.message)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (data.error_message) {
|
|
46
|
+
parts.push(chalk_1.default.red(String(data.error_message)));
|
|
47
|
+
}
|
|
48
|
+
return parts.join(" ");
|
|
49
|
+
}
|
|
50
|
+
async function tailCommand(opts) {
|
|
51
|
+
const url = (0, config_1.getBackendUrl)();
|
|
52
|
+
console.log(chalk_1.default.gray(`\n Connecting to trickle at ${url}...`));
|
|
53
|
+
const cleanup = (0, api_client_1.tailEvents)((event) => {
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
const timeStr = chalk_1.default.gray(now.replace(/T/, " ").replace(/\..+/, ""));
|
|
56
|
+
const badge = eventBadge(event.event);
|
|
57
|
+
const detail = formatEventDetail(event);
|
|
58
|
+
console.log(` ${timeStr} ${badge} ${detail}`);
|
|
59
|
+
}, opts.filter);
|
|
60
|
+
// Give it a moment to connect, then show the listening message
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
console.log(chalk_1.default.green(" Listening for events...") + chalk_1.default.gray(" (Ctrl+C to stop)\n"));
|
|
63
|
+
}, 500);
|
|
64
|
+
// Keep the process alive and handle graceful shutdown
|
|
65
|
+
const onSignal = () => {
|
|
66
|
+
console.log(chalk_1.default.gray("\n Disconnected.\n"));
|
|
67
|
+
cleanup();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
};
|
|
70
|
+
process.on("SIGINT", onSignal);
|
|
71
|
+
process.on("SIGTERM", onSignal);
|
|
72
|
+
// Keep the event loop alive
|
|
73
|
+
await new Promise(() => {
|
|
74
|
+
// Never resolves — the process stays alive until killed
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TestGenOptions {
|
|
2
|
+
out?: string;
|
|
3
|
+
framework?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `trickle test --generate` — Generate API test files from runtime observations.
|
|
8
|
+
*
|
|
9
|
+
* Uses real sample request/response data captured at runtime to generate
|
|
10
|
+
* ready-to-run test files with correct endpoints, request bodies, and
|
|
11
|
+
* response shape assertions.
|
|
12
|
+
*/
|
|
13
|
+
export declare function testGenCommand(opts: TestGenOptions): Promise<void>;
|