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,395 @@
|
|
|
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.initCommand = initCommand;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
function detectProject(dir, forcePython) {
|
|
44
|
+
const info = {
|
|
45
|
+
dir,
|
|
46
|
+
hasPackageJson: false,
|
|
47
|
+
hasTsConfig: false,
|
|
48
|
+
isPython: forcePython,
|
|
49
|
+
framework: null,
|
|
50
|
+
entryFile: null,
|
|
51
|
+
packageJson: null,
|
|
52
|
+
tsConfig: null,
|
|
53
|
+
};
|
|
54
|
+
// Check for package.json
|
|
55
|
+
const pkgPath = path.join(dir, "package.json");
|
|
56
|
+
if (fs.existsSync(pkgPath)) {
|
|
57
|
+
info.hasPackageJson = true;
|
|
58
|
+
try {
|
|
59
|
+
info.packageJson = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore parse errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Check for tsconfig.json
|
|
66
|
+
const tsPath = path.join(dir, "tsconfig.json");
|
|
67
|
+
if (fs.existsSync(tsPath)) {
|
|
68
|
+
info.hasTsConfig = true;
|
|
69
|
+
try {
|
|
70
|
+
// Strip comments (simple approach: remove // and /* */ comments)
|
|
71
|
+
const raw = fs.readFileSync(tsPath, "utf-8");
|
|
72
|
+
const cleaned = raw
|
|
73
|
+
.replace(/\/\/.*$/gm, "")
|
|
74
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
75
|
+
info.tsConfig = JSON.parse(cleaned);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore parse errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Check for Python project
|
|
82
|
+
if (fs.existsSync(path.join(dir, "pyproject.toml")) ||
|
|
83
|
+
fs.existsSync(path.join(dir, "setup.py")) ||
|
|
84
|
+
fs.existsSync(path.join(dir, "requirements.txt"))) {
|
|
85
|
+
info.isPython = true;
|
|
86
|
+
}
|
|
87
|
+
// Detect framework from dependencies
|
|
88
|
+
if (info.packageJson) {
|
|
89
|
+
const deps = {
|
|
90
|
+
...info.packageJson.dependencies,
|
|
91
|
+
...info.packageJson.devDependencies,
|
|
92
|
+
};
|
|
93
|
+
if (deps.express)
|
|
94
|
+
info.framework = "express";
|
|
95
|
+
}
|
|
96
|
+
if (info.isPython) {
|
|
97
|
+
// Check requirements.txt or pyproject.toml for framework
|
|
98
|
+
const reqPath = path.join(dir, "requirements.txt");
|
|
99
|
+
if (fs.existsSync(reqPath)) {
|
|
100
|
+
const reqs = fs.readFileSync(reqPath, "utf-8").toLowerCase();
|
|
101
|
+
if (reqs.includes("fastapi"))
|
|
102
|
+
info.framework = "fastapi";
|
|
103
|
+
else if (reqs.includes("flask"))
|
|
104
|
+
info.framework = "flask";
|
|
105
|
+
else if (reqs.includes("django"))
|
|
106
|
+
info.framework = "django";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Detect entry file
|
|
110
|
+
if (info.packageJson) {
|
|
111
|
+
const main = info.packageJson.main;
|
|
112
|
+
if (main && fs.existsSync(path.join(dir, main))) {
|
|
113
|
+
info.entryFile = main;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Common entry points
|
|
117
|
+
for (const candidate of ["src/index.ts", "src/index.js", "index.ts", "index.js", "app.ts", "app.js", "server.ts", "server.js"]) {
|
|
118
|
+
if (fs.existsSync(path.join(dir, candidate))) {
|
|
119
|
+
info.entryFile = candidate;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (info.isPython && !info.entryFile) {
|
|
126
|
+
for (const candidate of ["app.py", "main.py", "server.py", "wsgi.py"]) {
|
|
127
|
+
if (fs.existsSync(path.join(dir, candidate))) {
|
|
128
|
+
info.entryFile = candidate;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return info;
|
|
134
|
+
}
|
|
135
|
+
function createTrickleConfig(dir, info) {
|
|
136
|
+
const configPath = path.join(dir, ".tricklerc.json");
|
|
137
|
+
if (fs.existsSync(configPath))
|
|
138
|
+
return false;
|
|
139
|
+
// Also check for package.json "trickle" field
|
|
140
|
+
if (info.packageJson && info.packageJson.trickle) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const config = {};
|
|
144
|
+
// Suggest stubs directory based on project structure
|
|
145
|
+
if (fs.existsSync(path.join(dir, "src"))) {
|
|
146
|
+
config.stubs = "src/";
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
config.stubs = ".";
|
|
150
|
+
}
|
|
151
|
+
// Default exclude patterns
|
|
152
|
+
config.exclude = ["node_modules", "dist", "build", "__pycache__"];
|
|
153
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
function ensureTrickleDir(dir) {
|
|
157
|
+
const trickleDir = path.join(dir, ".trickle");
|
|
158
|
+
if (!fs.existsSync(trickleDir)) {
|
|
159
|
+
fs.mkdirSync(trickleDir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
return trickleDir;
|
|
162
|
+
}
|
|
163
|
+
function writeInitialTypes(trickleDir, isPython) {
|
|
164
|
+
if (isPython) {
|
|
165
|
+
const pyPath = path.join(trickleDir, "types.pyi");
|
|
166
|
+
if (!fs.existsSync(pyPath)) {
|
|
167
|
+
fs.writeFileSync(pyPath, [
|
|
168
|
+
"# Auto-generated by trickle from runtime type observations",
|
|
169
|
+
"# Run your app to populate types, then: trickle codegen --python --out .trickle/types.pyi",
|
|
170
|
+
"#",
|
|
171
|
+
"# This file will be updated automatically when using: trickle codegen --python --watch --out .trickle/types.pyi",
|
|
172
|
+
"",
|
|
173
|
+
"from typing import TypedDict",
|
|
174
|
+
"",
|
|
175
|
+
"# Types will appear here after your app processes its first requests.",
|
|
176
|
+
"",
|
|
177
|
+
].join("\n"), "utf-8");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const tsPath = path.join(trickleDir, "types.d.ts");
|
|
182
|
+
if (!fs.existsSync(tsPath)) {
|
|
183
|
+
fs.writeFileSync(tsPath, [
|
|
184
|
+
"// Auto-generated by trickle from runtime type observations",
|
|
185
|
+
"// Run your app to populate types, then: trickle codegen --out .trickle/types.d.ts",
|
|
186
|
+
"//",
|
|
187
|
+
"// This file will be updated automatically when using: trickle codegen --watch --out .trickle/types.d.ts",
|
|
188
|
+
"",
|
|
189
|
+
"// Types will appear here after your app processes its first requests.",
|
|
190
|
+
"",
|
|
191
|
+
].join("\n"), "utf-8");
|
|
192
|
+
}
|
|
193
|
+
// Also create an api-client placeholder
|
|
194
|
+
const clientPath = path.join(trickleDir, "api-client.ts");
|
|
195
|
+
if (!fs.existsSync(clientPath)) {
|
|
196
|
+
fs.writeFileSync(clientPath, [
|
|
197
|
+
"// Auto-generated typed API client by trickle",
|
|
198
|
+
"// Run your app to populate types, then: trickle codegen --client --out .trickle/api-client.ts",
|
|
199
|
+
"",
|
|
200
|
+
"// A fully-typed fetch client will appear here after your app serves its first API requests.",
|
|
201
|
+
"",
|
|
202
|
+
].join("\n"), "utf-8");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function updateTsConfig(dir, info) {
|
|
207
|
+
const tsConfigPath = path.join(dir, "tsconfig.json");
|
|
208
|
+
if (!info.hasTsConfig || !info.tsConfig) {
|
|
209
|
+
// No tsconfig — create a minimal one that includes .trickle
|
|
210
|
+
if (!info.isPython) {
|
|
211
|
+
const newConfig = {
|
|
212
|
+
compilerOptions: {
|
|
213
|
+
target: "ES2022",
|
|
214
|
+
module: "commonjs",
|
|
215
|
+
strict: true,
|
|
216
|
+
esModuleInterop: true,
|
|
217
|
+
skipLibCheck: true,
|
|
218
|
+
outDir: "./dist",
|
|
219
|
+
rootDir: "./src",
|
|
220
|
+
},
|
|
221
|
+
include: ["src", ".trickle"],
|
|
222
|
+
};
|
|
223
|
+
fs.writeFileSync(tsConfigPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
// Read the raw file to preserve formatting as much as possible
|
|
229
|
+
const raw = fs.readFileSync(tsConfigPath, "utf-8");
|
|
230
|
+
const config = info.tsConfig;
|
|
231
|
+
// Check if .trickle is already included
|
|
232
|
+
const include = config.include;
|
|
233
|
+
if (include && include.some((p) => p === ".trickle" || p.startsWith(".trickle/"))) {
|
|
234
|
+
return false; // Already configured
|
|
235
|
+
}
|
|
236
|
+
// Add .trickle to include array
|
|
237
|
+
if (include) {
|
|
238
|
+
include.push(".trickle");
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
config.include = ["src", ".trickle"];
|
|
242
|
+
}
|
|
243
|
+
fs.writeFileSync(tsConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function updatePackageJson(dir, info) {
|
|
247
|
+
if (!info.hasPackageJson || !info.packageJson) {
|
|
248
|
+
return { scriptsAdded: [] };
|
|
249
|
+
}
|
|
250
|
+
const pkgPath = path.join(dir, "package.json");
|
|
251
|
+
const pkg = info.packageJson;
|
|
252
|
+
const scripts = pkg.scripts || {};
|
|
253
|
+
const added = [];
|
|
254
|
+
// Add trickle:dev script for watch mode
|
|
255
|
+
if (!scripts["trickle:dev"]) {
|
|
256
|
+
const outFile = info.isPython ? ".trickle/types.pyi" : ".trickle/types.d.ts";
|
|
257
|
+
const langFlag = info.isPython ? " --python" : "";
|
|
258
|
+
scripts["trickle:dev"] = `trickle codegen${langFlag} --watch --out ${outFile}`;
|
|
259
|
+
added.push("trickle:dev");
|
|
260
|
+
}
|
|
261
|
+
// Add trickle:client script for API client generation
|
|
262
|
+
if (!info.isPython && !scripts["trickle:client"]) {
|
|
263
|
+
scripts["trickle:client"] = "trickle codegen --client --out .trickle/api-client.ts";
|
|
264
|
+
added.push("trickle:client");
|
|
265
|
+
}
|
|
266
|
+
// Add trickle:mock script
|
|
267
|
+
if (!scripts["trickle:mock"]) {
|
|
268
|
+
scripts["trickle:mock"] = "trickle mock";
|
|
269
|
+
added.push("trickle:mock");
|
|
270
|
+
}
|
|
271
|
+
// Detect current start script and create a trickle-wrapped version
|
|
272
|
+
const startScript = scripts.start || scripts.dev;
|
|
273
|
+
if (startScript && !scripts["trickle:start"]) {
|
|
274
|
+
// Check if it's a node command we can add -r to
|
|
275
|
+
if (startScript.match(/\bnode\s/)) {
|
|
276
|
+
scripts["trickle:start"] = startScript.replace(/\bnode\s/, "node -r trickle-observe/register ");
|
|
277
|
+
added.push("trickle:start");
|
|
278
|
+
}
|
|
279
|
+
else if (startScript.match(/\bts-node\s/)) {
|
|
280
|
+
scripts["trickle:start"] = startScript.replace(/\bts-node\s/, "ts-node -r trickle-observe/register ");
|
|
281
|
+
added.push("trickle:start");
|
|
282
|
+
}
|
|
283
|
+
else if (startScript.match(/\bnodemon\s/)) {
|
|
284
|
+
scripts["trickle:start"] = startScript.replace(/\bnodemon\s/, "nodemon -r trickle-observe/register ");
|
|
285
|
+
added.push("trickle:start");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (added.length > 0) {
|
|
289
|
+
pkg.scripts = scripts;
|
|
290
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
291
|
+
}
|
|
292
|
+
return { scriptsAdded: added };
|
|
293
|
+
}
|
|
294
|
+
function updateGitignore(dir) {
|
|
295
|
+
const giPath = path.join(dir, ".gitignore");
|
|
296
|
+
let content = "";
|
|
297
|
+
if (fs.existsSync(giPath)) {
|
|
298
|
+
content = fs.readFileSync(giPath, "utf-8");
|
|
299
|
+
if (content.includes(".trickle")) {
|
|
300
|
+
return false; // Already has it
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const addition = content.endsWith("\n") || content === ""
|
|
304
|
+
? ".trickle/\n"
|
|
305
|
+
: "\n.trickle/\n";
|
|
306
|
+
fs.writeFileSync(giPath, content + addition, "utf-8");
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
async function initCommand(opts) {
|
|
310
|
+
const dir = path.resolve(opts.dir || ".");
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log(chalk_1.default.bold(" trickle init"));
|
|
313
|
+
console.log("");
|
|
314
|
+
// Step 1: Detect project
|
|
315
|
+
const info = detectProject(dir, opts.python === true);
|
|
316
|
+
if (!info.hasPackageJson && !info.isPython) {
|
|
317
|
+
console.log(chalk_1.default.yellow(" No package.json or Python project detected."));
|
|
318
|
+
console.log(chalk_1.default.gray(" Run this command from your project root.\n"));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
const lang = info.isPython ? "Python" : "Node.js";
|
|
322
|
+
console.log(chalk_1.default.gray(` Detected: ${lang} project${info.framework ? ` (${info.framework})` : ""}`));
|
|
323
|
+
if (info.entryFile) {
|
|
324
|
+
console.log(chalk_1.default.gray(` Entry: ${info.entryFile}`));
|
|
325
|
+
}
|
|
326
|
+
console.log("");
|
|
327
|
+
// Step 2: Create .tricklerc.json
|
|
328
|
+
const configCreated = createTrickleConfig(dir, info);
|
|
329
|
+
if (configCreated) {
|
|
330
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold(".tricklerc.json")} — project config`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.log(` ${chalk_1.default.gray("-")} .tricklerc.json already exists`);
|
|
334
|
+
}
|
|
335
|
+
// Step 3: Create .trickle directory
|
|
336
|
+
const trickleDir = ensureTrickleDir(dir);
|
|
337
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold(".trickle/")} directory`);
|
|
338
|
+
// Step 3: Write initial type files
|
|
339
|
+
writeInitialTypes(trickleDir, info.isPython);
|
|
340
|
+
if (info.isPython) {
|
|
341
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold(".trickle/types.pyi")} (type stubs)`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold(".trickle/types.d.ts")} (type declarations)`);
|
|
345
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold(".trickle/api-client.ts")} (typed API client)`);
|
|
346
|
+
}
|
|
347
|
+
// Step 4: Update tsconfig.json
|
|
348
|
+
if (!info.isPython) {
|
|
349
|
+
const tsUpdated = updateTsConfig(dir, info);
|
|
350
|
+
if (tsUpdated) {
|
|
351
|
+
if (info.hasTsConfig) {
|
|
352
|
+
console.log(` ${chalk_1.default.green("~")} Updated ${chalk_1.default.bold("tsconfig.json")} — added .trickle to include`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
console.log(` ${chalk_1.default.green("+")} Created ${chalk_1.default.bold("tsconfig.json")} with .trickle included`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log(` ${chalk_1.default.gray("-")} tsconfig.json already includes .trickle`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Step 5: Update package.json scripts
|
|
363
|
+
if (info.hasPackageJson) {
|
|
364
|
+
const { scriptsAdded } = updatePackageJson(dir, info);
|
|
365
|
+
if (scriptsAdded.length > 0) {
|
|
366
|
+
for (const name of scriptsAdded) {
|
|
367
|
+
console.log(` ${chalk_1.default.green("+")} Added npm script: ${chalk_1.default.bold(name)}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Step 6: Update .gitignore
|
|
372
|
+
const giUpdated = updateGitignore(dir);
|
|
373
|
+
if (giUpdated) {
|
|
374
|
+
console.log(` ${chalk_1.default.green("~")} Updated ${chalk_1.default.bold(".gitignore")} — added .trickle/`);
|
|
375
|
+
}
|
|
376
|
+
// Step 7: Print next steps
|
|
377
|
+
console.log("");
|
|
378
|
+
console.log(chalk_1.default.bold(" Next steps:"));
|
|
379
|
+
console.log("");
|
|
380
|
+
const entryFile = info.entryFile || (info.isPython ? "app.py" : "app.js");
|
|
381
|
+
console.log(chalk_1.default.white(" 1. Run your app with trickle (one command does everything):"));
|
|
382
|
+
console.log(chalk_1.default.cyan(` trickle run ${entryFile}`));
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log(chalk_1.default.white(" 2. That's it! trickle auto-detects the runtime, observes types,"));
|
|
385
|
+
console.log(chalk_1.default.white(" and generates stubs from .tricklerc.json settings."));
|
|
386
|
+
console.log("");
|
|
387
|
+
console.log(chalk_1.default.gray(" Customize .tricklerc.json:"));
|
|
388
|
+
console.log(chalk_1.default.gray(' { "stubs": "src/", "annotate": "src/", "exclude": ["test"] }'));
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log(chalk_1.default.gray(" Other commands:"));
|
|
391
|
+
console.log(chalk_1.default.gray(" trickle functions — list observed functions"));
|
|
392
|
+
console.log(chalk_1.default.gray(" trickle types <name> — see types + sample data"));
|
|
393
|
+
console.log(chalk_1.default.gray(" trickle annotate src/ — add type annotations to source files"));
|
|
394
|
+
console.log("");
|
|
395
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
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.mockCommand = mockCommand;
|
|
40
|
+
const http = __importStar(require("http"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const api_client_1 = require("../api-client");
|
|
43
|
+
/**
|
|
44
|
+
* Convert an Express-style path like `/api/users/:id` to a regex
|
|
45
|
+
* that captures named path params.
|
|
46
|
+
*/
|
|
47
|
+
function pathToRegex(routePath) {
|
|
48
|
+
const paramNames = [];
|
|
49
|
+
const pattern = routePath.replace(/:(\w+)/g, (_match, paramName) => {
|
|
50
|
+
paramNames.push(paramName);
|
|
51
|
+
return "([^/]+)";
|
|
52
|
+
});
|
|
53
|
+
return { regex: new RegExp(`^${pattern}$`), paramNames };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a description of the mock server routes for the startup banner.
|
|
57
|
+
*/
|
|
58
|
+
function formatRouteTable(routes) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const methodColors = {
|
|
61
|
+
GET: chalk_1.default.green,
|
|
62
|
+
POST: chalk_1.default.yellow,
|
|
63
|
+
PUT: chalk_1.default.blue,
|
|
64
|
+
DELETE: chalk_1.default.red,
|
|
65
|
+
PATCH: chalk_1.default.magenta,
|
|
66
|
+
};
|
|
67
|
+
for (const route of routes) {
|
|
68
|
+
const color = methodColors[route.method] || chalk_1.default.white;
|
|
69
|
+
const method = color(route.method.padEnd(7));
|
|
70
|
+
const path = chalk_1.default.white(route.path);
|
|
71
|
+
const age = formatTimeAgo(route.observedAt);
|
|
72
|
+
lines.push(` ${method} ${path} ${chalk_1.default.gray(`(sample from ${age})`)}`);
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function formatTimeAgo(isoDate) {
|
|
77
|
+
try {
|
|
78
|
+
const date = new Date(isoDate.replace(" ", "T") + (isoDate.includes("Z") ? "" : "Z"));
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const diffSec = Math.floor((now - date.getTime()) / 1000);
|
|
81
|
+
if (diffSec < 60)
|
|
82
|
+
return `${diffSec}s ago`;
|
|
83
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
84
|
+
if (diffMin < 60)
|
|
85
|
+
return `${diffMin}m ago`;
|
|
86
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
87
|
+
if (diffHr < 24)
|
|
88
|
+
return `${diffHr}h ago`;
|
|
89
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
90
|
+
return `${diffDay}d ago`;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return isoDate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Substitute path params in sample output to match the requested values.
|
|
98
|
+
* For example, if the sample output has { id: 1 } but the request has :id = "42",
|
|
99
|
+
* replace numeric id fields with the requested value.
|
|
100
|
+
*/
|
|
101
|
+
function substituteSampleOutput(sample, paramValues) {
|
|
102
|
+
if (sample === null || sample === undefined)
|
|
103
|
+
return sample;
|
|
104
|
+
if (typeof sample !== "object")
|
|
105
|
+
return sample;
|
|
106
|
+
if (Array.isArray(sample)) {
|
|
107
|
+
return sample.map((item) => substituteSampleOutput(item, paramValues));
|
|
108
|
+
}
|
|
109
|
+
const result = {};
|
|
110
|
+
for (const [key, value] of Object.entries(sample)) {
|
|
111
|
+
// If this key matches a path param name, substitute the value
|
|
112
|
+
if (key in paramValues) {
|
|
113
|
+
const paramVal = paramValues[key];
|
|
114
|
+
// Try to preserve the original type (number vs string)
|
|
115
|
+
if (typeof value === "number") {
|
|
116
|
+
const num = Number(paramVal);
|
|
117
|
+
result[key] = isNaN(num) ? paramVal : num;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
result[key] = paramVal;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (typeof value === "object" && value !== null) {
|
|
124
|
+
result[key] = substituteSampleOutput(value, paramValues);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
result[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
async function mockCommand(opts) {
|
|
133
|
+
const port = parseInt(opts.port || "3000", 10);
|
|
134
|
+
const enableCors = opts.cors !== false;
|
|
135
|
+
// Fetch mock configuration from the backend
|
|
136
|
+
let routes;
|
|
137
|
+
try {
|
|
138
|
+
const config = await (0, api_client_1.fetchMockConfig)();
|
|
139
|
+
routes = config.routes;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof Error) {
|
|
143
|
+
console.error(chalk_1.default.red(`\n Error fetching mock config: ${err.message}\n`));
|
|
144
|
+
}
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
if (routes.length === 0) {
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(chalk_1.default.yellow(" No API routes found."));
|
|
150
|
+
console.log(chalk_1.default.gray(" Instrument your app and make some requests first."));
|
|
151
|
+
console.log("");
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
// Build route matchers
|
|
155
|
+
const matchers = routes.map((route) => {
|
|
156
|
+
const { regex, paramNames } = pathToRegex(route.path);
|
|
157
|
+
return { route, regex, paramNames };
|
|
158
|
+
});
|
|
159
|
+
// Create the mock HTTP server
|
|
160
|
+
const server = http.createServer((req, res) => {
|
|
161
|
+
const reqUrl = new URL(req.url || "/", `http://localhost:${port}`);
|
|
162
|
+
const reqMethod = (req.method || "GET").toUpperCase();
|
|
163
|
+
const reqPath = reqUrl.pathname;
|
|
164
|
+
// CORS headers
|
|
165
|
+
if (enableCors) {
|
|
166
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
167
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
168
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
169
|
+
}
|
|
170
|
+
// Handle preflight
|
|
171
|
+
if (reqMethod === "OPTIONS") {
|
|
172
|
+
res.writeHead(204);
|
|
173
|
+
res.end();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Find matching route
|
|
177
|
+
for (const { route, regex, paramNames } of matchers) {
|
|
178
|
+
if (route.method !== reqMethod)
|
|
179
|
+
continue;
|
|
180
|
+
const match = reqPath.match(regex);
|
|
181
|
+
if (!match)
|
|
182
|
+
continue;
|
|
183
|
+
// Extract path params
|
|
184
|
+
const paramValues = {};
|
|
185
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
186
|
+
paramValues[paramNames[i]] = match[i + 1];
|
|
187
|
+
}
|
|
188
|
+
// Get sample output, substituting path param values
|
|
189
|
+
let output = route.sampleOutput;
|
|
190
|
+
if (output && Object.keys(paramValues).length > 0) {
|
|
191
|
+
output = substituteSampleOutput(output, paramValues);
|
|
192
|
+
}
|
|
193
|
+
// Log the request
|
|
194
|
+
const methodColor = reqMethod === "GET" ? chalk_1.default.green :
|
|
195
|
+
reqMethod === "POST" ? chalk_1.default.yellow :
|
|
196
|
+
reqMethod === "PUT" ? chalk_1.default.blue :
|
|
197
|
+
reqMethod === "DELETE" ? chalk_1.default.red :
|
|
198
|
+
chalk_1.default.white;
|
|
199
|
+
console.log(` ${chalk_1.default.gray(new Date().toLocaleTimeString())} ${methodColor(reqMethod.padEnd(7))} ${reqPath} ${chalk_1.default.gray("→ 200")}`);
|
|
200
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
201
|
+
res.end(JSON.stringify(output ?? {}));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// No route matched
|
|
205
|
+
console.log(` ${chalk_1.default.gray(new Date().toLocaleTimeString())} ${chalk_1.default.red(reqMethod.padEnd(7))} ${reqPath} ${chalk_1.default.red("→ 404")}`);
|
|
206
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
207
|
+
res.end(JSON.stringify({ error: "Not found", path: reqPath, method: reqMethod }));
|
|
208
|
+
});
|
|
209
|
+
server.listen(port, () => {
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log(chalk_1.default.bold(" Trickle Mock Server"));
|
|
212
|
+
console.log("");
|
|
213
|
+
console.log(chalk_1.default.gray(" Routes (from runtime observations):"));
|
|
214
|
+
console.log(formatRouteTable(routes));
|
|
215
|
+
console.log("");
|
|
216
|
+
console.log(` Listening on ${chalk_1.default.cyan(`http://localhost:${port}`)}`);
|
|
217
|
+
if (enableCors) {
|
|
218
|
+
console.log(chalk_1.default.gray(" CORS enabled (Access-Control-Allow-Origin: *)"));
|
|
219
|
+
}
|
|
220
|
+
console.log(chalk_1.default.gray(" Press Ctrl+C to stop.\n"));
|
|
221
|
+
});
|
|
222
|
+
// Handle graceful shutdown
|
|
223
|
+
process.on("SIGINT", () => {
|
|
224
|
+
console.log(chalk_1.default.gray("\n Stopping mock server...\n"));
|
|
225
|
+
server.close(() => process.exit(0));
|
|
226
|
+
});
|
|
227
|
+
process.on("SIGTERM", () => {
|
|
228
|
+
server.close(() => process.exit(0));
|
|
229
|
+
});
|
|
230
|
+
// Keep process alive
|
|
231
|
+
await new Promise(() => { });
|
|
232
|
+
}
|