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