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,268 @@
|
|
|
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.autoCommand = autoCommand;
|
|
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
|
+
* Detect which codegen formats are relevant based on package.json dependencies.
|
|
46
|
+
*/
|
|
47
|
+
function detectFormats(projectDir) {
|
|
48
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
49
|
+
if (!fs.existsSync(pkgPath)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
let pkg;
|
|
53
|
+
try {
|
|
54
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const deps = {
|
|
60
|
+
...(pkg.dependencies || {}),
|
|
61
|
+
...(pkg.devDependencies || {}),
|
|
62
|
+
};
|
|
63
|
+
const formats = [];
|
|
64
|
+
// Always generate base TypeScript types
|
|
65
|
+
formats.push({
|
|
66
|
+
format: "",
|
|
67
|
+
fileName: "types.d.ts",
|
|
68
|
+
label: "TypeScript types",
|
|
69
|
+
reason: "always generated",
|
|
70
|
+
});
|
|
71
|
+
// Axios client
|
|
72
|
+
if (deps["axios"]) {
|
|
73
|
+
formats.push({
|
|
74
|
+
format: "axios",
|
|
75
|
+
fileName: "axios-client.ts",
|
|
76
|
+
label: "Axios client",
|
|
77
|
+
reason: "axios detected",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Fetch-based client (always useful as a fallback if no axios)
|
|
81
|
+
if (!deps["axios"]) {
|
|
82
|
+
formats.push({
|
|
83
|
+
format: "client",
|
|
84
|
+
fileName: "api-client.ts",
|
|
85
|
+
label: "Fetch API client",
|
|
86
|
+
reason: "default HTTP client",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// React Query / TanStack Query
|
|
90
|
+
if (deps["@tanstack/react-query"] || deps["react-query"]) {
|
|
91
|
+
formats.push({
|
|
92
|
+
format: "react-query",
|
|
93
|
+
fileName: "hooks.ts",
|
|
94
|
+
label: "React Query hooks",
|
|
95
|
+
reason: deps["@tanstack/react-query"] ? "@tanstack/react-query" : "react-query",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// SWR
|
|
99
|
+
if (deps["swr"]) {
|
|
100
|
+
formats.push({
|
|
101
|
+
format: "swr",
|
|
102
|
+
fileName: "swr-hooks.ts",
|
|
103
|
+
label: "SWR hooks",
|
|
104
|
+
reason: "swr detected",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Zod
|
|
108
|
+
if (deps["zod"]) {
|
|
109
|
+
formats.push({
|
|
110
|
+
format: "zod",
|
|
111
|
+
fileName: "schemas.ts",
|
|
112
|
+
label: "Zod schemas",
|
|
113
|
+
reason: "zod detected",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// tRPC
|
|
117
|
+
if (deps["@trpc/server"] || deps["@trpc/client"]) {
|
|
118
|
+
formats.push({
|
|
119
|
+
format: "trpc",
|
|
120
|
+
fileName: "trpc-router.ts",
|
|
121
|
+
label: "tRPC router",
|
|
122
|
+
reason: deps["@trpc/server"] ? "@trpc/server" : "@trpc/client",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// class-validator / NestJS
|
|
126
|
+
if (deps["class-validator"] || deps["@nestjs/common"]) {
|
|
127
|
+
formats.push({
|
|
128
|
+
format: "class-validator",
|
|
129
|
+
fileName: "dtos.ts",
|
|
130
|
+
label: "class-validator DTOs",
|
|
131
|
+
reason: deps["class-validator"] ? "class-validator" : "@nestjs/common",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Express handler types
|
|
135
|
+
if (deps["express"] || deps["@types/express"]) {
|
|
136
|
+
formats.push({
|
|
137
|
+
format: "handlers",
|
|
138
|
+
fileName: "handlers.d.ts",
|
|
139
|
+
label: "Express handler types",
|
|
140
|
+
reason: deps["express"] ? "express" : "@types/express",
|
|
141
|
+
});
|
|
142
|
+
formats.push({
|
|
143
|
+
format: "middleware",
|
|
144
|
+
fileName: "middleware.ts",
|
|
145
|
+
label: "Express middleware",
|
|
146
|
+
reason: deps["express"] ? "express" : "@types/express",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// MSW
|
|
150
|
+
if (deps["msw"]) {
|
|
151
|
+
formats.push({
|
|
152
|
+
format: "msw",
|
|
153
|
+
fileName: "msw-handlers.ts",
|
|
154
|
+
label: "MSW mock handlers",
|
|
155
|
+
reason: "msw detected",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Pydantic (Python projects)
|
|
159
|
+
if (fs.existsSync(path.join(projectDir, "requirements.txt")) ||
|
|
160
|
+
fs.existsSync(path.join(projectDir, "pyproject.toml"))) {
|
|
161
|
+
// Check if pydantic is in requirements
|
|
162
|
+
let hasPydantic = false;
|
|
163
|
+
try {
|
|
164
|
+
const reqPath = path.join(projectDir, "requirements.txt");
|
|
165
|
+
if (fs.existsSync(reqPath)) {
|
|
166
|
+
const reqs = fs.readFileSync(reqPath, "utf-8");
|
|
167
|
+
if (reqs.toLowerCase().includes("pydantic"))
|
|
168
|
+
hasPydantic = true;
|
|
169
|
+
}
|
|
170
|
+
const pyprojectPath = path.join(projectDir, "pyproject.toml");
|
|
171
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
172
|
+
const pyproject = fs.readFileSync(pyprojectPath, "utf-8");
|
|
173
|
+
if (pyproject.toLowerCase().includes("pydantic"))
|
|
174
|
+
hasPydantic = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
if (hasPydantic) {
|
|
179
|
+
formats.push({
|
|
180
|
+
format: "pydantic",
|
|
181
|
+
fileName: "models.py",
|
|
182
|
+
label: "Pydantic models",
|
|
183
|
+
reason: "pydantic detected",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Type guards (always useful)
|
|
188
|
+
formats.push({
|
|
189
|
+
format: "guards",
|
|
190
|
+
fileName: "guards.ts",
|
|
191
|
+
label: "Type guards",
|
|
192
|
+
reason: "runtime type checking",
|
|
193
|
+
});
|
|
194
|
+
return formats;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* `trickle auto` — Auto-detect project deps and generate only relevant type files.
|
|
198
|
+
*/
|
|
199
|
+
async function autoCommand(opts) {
|
|
200
|
+
const projectDir = process.cwd();
|
|
201
|
+
const outDir = path.resolve(opts.dir || ".trickle");
|
|
202
|
+
// Detect formats
|
|
203
|
+
const formats = detectFormats(projectDir);
|
|
204
|
+
if (formats.length === 0) {
|
|
205
|
+
console.error(chalk_1.default.red("\n No package.json found in current directory."));
|
|
206
|
+
console.error(chalk_1.default.gray(" Run this command from your project root.\n"));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
// Ensure output directory
|
|
210
|
+
if (!fs.existsSync(outDir)) {
|
|
211
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log(chalk_1.default.bold(" trickle auto"));
|
|
215
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
216
|
+
console.log(chalk_1.default.gray(` Project: ${projectDir}`));
|
|
217
|
+
console.log(chalk_1.default.gray(` Output: ${outDir}`));
|
|
218
|
+
if (opts.env) {
|
|
219
|
+
console.log(chalk_1.default.gray(` Env: ${opts.env}`));
|
|
220
|
+
}
|
|
221
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
222
|
+
console.log("");
|
|
223
|
+
// Show detected formats
|
|
224
|
+
console.log(chalk_1.default.gray(" Detected dependencies:"));
|
|
225
|
+
for (const f of formats) {
|
|
226
|
+
console.log(chalk_1.default.gray(` ${chalk_1.default.white(f.label)} ← ${f.reason}`));
|
|
227
|
+
}
|
|
228
|
+
console.log("");
|
|
229
|
+
// Generate each format
|
|
230
|
+
const queryOpts = { env: opts.env };
|
|
231
|
+
let generated = 0;
|
|
232
|
+
let skipped = 0;
|
|
233
|
+
for (const f of formats) {
|
|
234
|
+
const filePath = path.join(outDir, f.fileName);
|
|
235
|
+
try {
|
|
236
|
+
const result = await (0, api_client_1.fetchCodegen)({
|
|
237
|
+
...queryOpts,
|
|
238
|
+
format: f.format || undefined,
|
|
239
|
+
});
|
|
240
|
+
const content = result.types;
|
|
241
|
+
if (!content || content.includes("No functions found") || content.includes("No API routes found")) {
|
|
242
|
+
console.log(chalk_1.default.yellow(" ─ ") + chalk_1.default.gray(`${f.fileName} (no data)`));
|
|
243
|
+
skipped++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
247
|
+
generated++;
|
|
248
|
+
const size = content.split("\n").length;
|
|
249
|
+
console.log(chalk_1.default.green(" ✓ ") +
|
|
250
|
+
chalk_1.default.bold(f.fileName) +
|
|
251
|
+
chalk_1.default.gray(` (${size} lines)`));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
console.log(chalk_1.default.yellow(" ─ ") + chalk_1.default.gray(`${f.fileName} (error)`));
|
|
255
|
+
skipped++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
console.log("");
|
|
259
|
+
if (generated > 0) {
|
|
260
|
+
console.log(chalk_1.default.green(` ${generated} files generated`) +
|
|
261
|
+
(skipped > 0 ? chalk_1.default.gray(`, ${skipped} skipped`) : ""));
|
|
262
|
+
console.log(chalk_1.default.gray(` Output directory: ${outDir}`));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
console.log(chalk_1.default.yellow(" No files generated — instrument your app and make some requests first."));
|
|
266
|
+
}
|
|
267
|
+
console.log("");
|
|
268
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CaptureOptions {
|
|
2
|
+
header?: string[];
|
|
3
|
+
body?: string;
|
|
4
|
+
env?: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `trickle capture <method> <url>` — Capture types from a live API endpoint.
|
|
9
|
+
*
|
|
10
|
+
* Makes an HTTP request to the given URL, infers TypeNode from the response,
|
|
11
|
+
* and sends the observation to the trickle backend. Zero instrumentation needed —
|
|
12
|
+
* just point at any API and start collecting types.
|
|
13
|
+
*/
|
|
14
|
+
export declare function captureCommand(method: string, url: string, opts: CaptureOptions): Promise<void>;
|
|
@@ -0,0 +1,271 @@
|
|
|
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.captureCommand = captureCommand;
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const config_1 = require("../config");
|
|
43
|
+
/**
|
|
44
|
+
* `trickle capture <method> <url>` — Capture types from a live API endpoint.
|
|
45
|
+
*
|
|
46
|
+
* Makes an HTTP request to the given URL, infers TypeNode from the response,
|
|
47
|
+
* and sends the observation to the trickle backend. Zero instrumentation needed —
|
|
48
|
+
* just point at any API and start collecting types.
|
|
49
|
+
*/
|
|
50
|
+
async function captureCommand(method, url, opts) {
|
|
51
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
52
|
+
// Validate method
|
|
53
|
+
const httpMethod = method.toUpperCase();
|
|
54
|
+
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
55
|
+
if (!validMethods.includes(httpMethod)) {
|
|
56
|
+
console.error(chalk_1.default.red(`\n Invalid HTTP method: ${method}`));
|
|
57
|
+
console.error(chalk_1.default.gray(` Valid methods: ${validMethods.join(", ")}\n`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
// Parse URL
|
|
61
|
+
let parsedUrl;
|
|
62
|
+
try {
|
|
63
|
+
parsedUrl = new URL(url);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.error(chalk_1.default.red(`\n Invalid URL: ${url}\n`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Build request headers
|
|
70
|
+
const headers = {
|
|
71
|
+
"Accept": "application/json",
|
|
72
|
+
};
|
|
73
|
+
if (opts.header) {
|
|
74
|
+
for (const h of opts.header) {
|
|
75
|
+
const colonIdx = h.indexOf(":");
|
|
76
|
+
if (colonIdx === -1) {
|
|
77
|
+
console.error(chalk_1.default.red(`\n Invalid header format: ${h}`));
|
|
78
|
+
console.error(chalk_1.default.gray(' Use "Header-Name: value" format\n'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const key = h.slice(0, colonIdx).trim();
|
|
82
|
+
const value = h.slice(colonIdx + 1).trim();
|
|
83
|
+
headers[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Parse request body
|
|
87
|
+
let reqBody;
|
|
88
|
+
let reqJson = undefined;
|
|
89
|
+
if (opts.body) {
|
|
90
|
+
reqBody = opts.body;
|
|
91
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
92
|
+
try {
|
|
93
|
+
reqJson = JSON.parse(opts.body);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Not JSON body — that's fine
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check backend connectivity
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
|
|
102
|
+
if (!res.ok)
|
|
103
|
+
throw new Error("not ok");
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
console.error(chalk_1.default.red(`\n Cannot reach trickle backend at ${chalk_1.default.bold(backendUrl)}`));
|
|
107
|
+
console.error(chalk_1.default.gray(" Start the backend first: npx trickle-backend\n"));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(chalk_1.default.bold(" trickle capture"));
|
|
112
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
113
|
+
console.log(chalk_1.default.gray(` ${chalk_1.default.bold(httpMethod)} ${url}`));
|
|
114
|
+
// Make the request
|
|
115
|
+
let response;
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(url, {
|
|
118
|
+
method: httpMethod,
|
|
119
|
+
headers,
|
|
120
|
+
body: reqBody,
|
|
121
|
+
signal: AbortSignal.timeout(30000),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
126
|
+
console.error(chalk_1.default.red(`\n Request failed: ${msg}\n`));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const status = response.status;
|
|
130
|
+
const statusColor = status < 400 ? chalk_1.default.green : chalk_1.default.red;
|
|
131
|
+
console.log(chalk_1.default.gray(` Status: `) + statusColor(`${status} ${response.statusText}`));
|
|
132
|
+
// Read response body
|
|
133
|
+
const resText = await response.text();
|
|
134
|
+
let resJson = undefined;
|
|
135
|
+
const contentType = response.headers.get("content-type") || "";
|
|
136
|
+
if (contentType.includes("json") && resText.length > 0) {
|
|
137
|
+
try {
|
|
138
|
+
resJson = JSON.parse(resText);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
console.error(chalk_1.default.yellow("\n Response is not valid JSON — cannot capture types.\n"));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.error(chalk_1.default.yellow("\n Response is not JSON — cannot capture types."));
|
|
147
|
+
console.error(chalk_1.default.gray(` Content-Type: ${contentType}\n`));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
// Build type observations
|
|
151
|
+
const routePath = normalizePath(parsedUrl.pathname);
|
|
152
|
+
const functionName = `${httpMethod} ${routePath}`;
|
|
153
|
+
const argsProperties = {};
|
|
154
|
+
if (reqJson !== undefined && reqJson !== null) {
|
|
155
|
+
argsProperties.body = jsonToTypeNode(reqJson);
|
|
156
|
+
}
|
|
157
|
+
// Extract query params
|
|
158
|
+
if (parsedUrl.search) {
|
|
159
|
+
const queryProps = {};
|
|
160
|
+
for (const [key] of parsedUrl.searchParams) {
|
|
161
|
+
queryProps[key] = { kind: "primitive", name: "string" };
|
|
162
|
+
}
|
|
163
|
+
if (Object.keys(queryProps).length > 0) {
|
|
164
|
+
argsProperties.query = { kind: "object", properties: queryProps };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const argsType = Object.keys(argsProperties).length > 0
|
|
168
|
+
? { kind: "object", properties: argsProperties }
|
|
169
|
+
: { kind: "object", properties: {} };
|
|
170
|
+
const returnType = jsonToTypeNode(resJson);
|
|
171
|
+
const typeHash = computeTypeHash(argsType, returnType);
|
|
172
|
+
const payload = {
|
|
173
|
+
functionName,
|
|
174
|
+
module: opts.module || "capture",
|
|
175
|
+
language: "js",
|
|
176
|
+
environment: opts.env || "development",
|
|
177
|
+
typeHash,
|
|
178
|
+
argsType,
|
|
179
|
+
returnType,
|
|
180
|
+
sampleInput: Object.keys(argsProperties).length > 0 ? argsProperties : undefined,
|
|
181
|
+
sampleOutput: resJson,
|
|
182
|
+
};
|
|
183
|
+
// Send to backend
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch(`${backendUrl}/api/ingest`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify(payload),
|
|
189
|
+
signal: AbortSignal.timeout(5000),
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok)
|
|
192
|
+
throw new Error(`HTTP ${res.status}`);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
196
|
+
console.error(chalk_1.default.red(`\n Failed to send types to backend: ${msg}\n`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
console.log(chalk_1.default.gray(` Route: `) + chalk_1.default.white(functionName));
|
|
200
|
+
console.log(chalk_1.default.gray(` Backend: `) + chalk_1.default.white(backendUrl));
|
|
201
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
202
|
+
console.log(chalk_1.default.green(" Types captured successfully!"));
|
|
203
|
+
// Show a preview of what was captured
|
|
204
|
+
const fieldCount = countFields(returnType);
|
|
205
|
+
console.log(chalk_1.default.gray(` Response shape: ${fieldCount} fields observed`));
|
|
206
|
+
if (reqJson) {
|
|
207
|
+
const reqFieldCount = countFields(argsProperties.body || { kind: "object", properties: {} });
|
|
208
|
+
console.log(chalk_1.default.gray(` Request body: ${reqFieldCount} fields observed`));
|
|
209
|
+
}
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(chalk_1.default.gray(" Run ") + chalk_1.default.white(`trickle codegen`) + chalk_1.default.gray(" to generate type definitions."));
|
|
212
|
+
console.log("");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Infer a TypeNode from a JSON value.
|
|
216
|
+
*/
|
|
217
|
+
function jsonToTypeNode(value) {
|
|
218
|
+
if (value === null)
|
|
219
|
+
return { kind: "primitive", name: "null" };
|
|
220
|
+
if (value === undefined)
|
|
221
|
+
return { kind: "primitive", name: "undefined" };
|
|
222
|
+
switch (typeof value) {
|
|
223
|
+
case "string": return { kind: "primitive", name: "string" };
|
|
224
|
+
case "number": return { kind: "primitive", name: "number" };
|
|
225
|
+
case "boolean": return { kind: "primitive", name: "boolean" };
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(value)) {
|
|
228
|
+
if (value.length === 0)
|
|
229
|
+
return { kind: "array", element: { kind: "unknown" } };
|
|
230
|
+
const elementType = jsonToTypeNode(value[0]);
|
|
231
|
+
return { kind: "array", element: elementType };
|
|
232
|
+
}
|
|
233
|
+
// Object
|
|
234
|
+
const obj = value;
|
|
235
|
+
const properties = {};
|
|
236
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
237
|
+
properties[key] = jsonToTypeNode(val);
|
|
238
|
+
}
|
|
239
|
+
return { kind: "object", properties };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Normalize URL path: replace dynamic segments with :param patterns.
|
|
243
|
+
*/
|
|
244
|
+
function normalizePath(urlPath) {
|
|
245
|
+
const parts = urlPath.split("/");
|
|
246
|
+
return parts
|
|
247
|
+
.map((part, i) => {
|
|
248
|
+
if (!part)
|
|
249
|
+
return part;
|
|
250
|
+
if (/^\d+$/.test(part))
|
|
251
|
+
return ":id";
|
|
252
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(part))
|
|
253
|
+
return ":id";
|
|
254
|
+
if (/^[0-9a-f]{16,}$/i.test(part) && i > 1)
|
|
255
|
+
return ":id";
|
|
256
|
+
return part;
|
|
257
|
+
})
|
|
258
|
+
.join("/");
|
|
259
|
+
}
|
|
260
|
+
function computeTypeHash(argsType, returnType) {
|
|
261
|
+
const data = JSON.stringify({ a: argsType, r: returnType });
|
|
262
|
+
return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
|
|
263
|
+
}
|
|
264
|
+
function countFields(node) {
|
|
265
|
+
if (node.kind === "object" && node.properties) {
|
|
266
|
+
return Object.keys(node.properties).length;
|
|
267
|
+
}
|
|
268
|
+
if (node.kind === "array")
|
|
269
|
+
return 1;
|
|
270
|
+
return 1;
|
|
271
|
+
}
|