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,997 @@
|
|
|
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.runCommand = runCommand;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const api_client_1 = require("../api-client");
|
|
46
|
+
function loadProjectConfig() {
|
|
47
|
+
const configNames = [".tricklerc.json", ".tricklerc", "trickle.config.json"];
|
|
48
|
+
for (const name of configNames) {
|
|
49
|
+
const p = path.resolve(name);
|
|
50
|
+
if (fs.existsSync(p)) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Also check package.json "trickle" field
|
|
60
|
+
const pkgPath = path.resolve("package.json");
|
|
61
|
+
if (fs.existsSync(pkgPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
64
|
+
if (pkg.trickle && typeof pkg.trickle === "object") {
|
|
65
|
+
return pkg.trickle;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function mergeConfigWithOpts(opts, config) {
|
|
75
|
+
if (!config)
|
|
76
|
+
return opts;
|
|
77
|
+
const merged = { ...opts };
|
|
78
|
+
// CLI flags override config
|
|
79
|
+
if (!merged.stubs && config.stubs) {
|
|
80
|
+
merged.stubs = config.stubs;
|
|
81
|
+
}
|
|
82
|
+
if (!merged.annotate && config.annotate) {
|
|
83
|
+
// If array, join first item (run --annotate takes a single path)
|
|
84
|
+
merged.annotate = Array.isArray(config.annotate)
|
|
85
|
+
? config.annotate[0]
|
|
86
|
+
: config.annotate;
|
|
87
|
+
}
|
|
88
|
+
if (!merged.include && config.include) {
|
|
89
|
+
merged.include = Array.isArray(config.include)
|
|
90
|
+
? config.include.join(",")
|
|
91
|
+
: config.include;
|
|
92
|
+
}
|
|
93
|
+
if (!merged.exclude && config.exclude) {
|
|
94
|
+
merged.exclude = Array.isArray(config.exclude)
|
|
95
|
+
? config.exclude.join(",")
|
|
96
|
+
: config.exclude;
|
|
97
|
+
}
|
|
98
|
+
return merged;
|
|
99
|
+
}
|
|
100
|
+
// ── Detect if command is a single source file ──
|
|
101
|
+
function detectSingleFile(command) {
|
|
102
|
+
const trimmed = command.trim();
|
|
103
|
+
// Must be a single token (no spaces unless quoted)
|
|
104
|
+
if (/\s/.test(trimmed))
|
|
105
|
+
return null;
|
|
106
|
+
const ext = path.extname(trimmed).toLowerCase();
|
|
107
|
+
if (![".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".py"].includes(ext)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const resolved = path.resolve(trimmed);
|
|
111
|
+
if (!fs.existsSync(resolved))
|
|
112
|
+
return null;
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
// ── Auto-detect runtime from file extension ──
|
|
116
|
+
function autoDetectCommand(input) {
|
|
117
|
+
// If it already starts with a known runtime, return as-is
|
|
118
|
+
if (/^(node|ts-node|tsx|nodemon|bun|deno|python3?|python3?\.\d+|vitest|jest|mocha|npx|bunx|pytest|uvicorn|gunicorn|flask|django-admin)\b/.test(input)) {
|
|
119
|
+
return input;
|
|
120
|
+
}
|
|
121
|
+
// Check if the first token is a file path
|
|
122
|
+
const parts = input.split(/\s+/);
|
|
123
|
+
const file = parts[0];
|
|
124
|
+
const rest = parts.slice(1).join(" ");
|
|
125
|
+
const ext = path.extname(file).toLowerCase();
|
|
126
|
+
// Resolve relative to cwd
|
|
127
|
+
const resolved = path.resolve(file);
|
|
128
|
+
const fileExists = fs.existsSync(resolved);
|
|
129
|
+
if (!fileExists) {
|
|
130
|
+
// Not a file — might be a custom command, return as-is
|
|
131
|
+
return input;
|
|
132
|
+
}
|
|
133
|
+
switch (ext) {
|
|
134
|
+
case ".js":
|
|
135
|
+
case ".cjs":
|
|
136
|
+
return rest ? `node ${file} ${rest}` : `node ${file}`;
|
|
137
|
+
case ".mjs":
|
|
138
|
+
return rest ? `node ${file} ${rest}` : `node ${file}`;
|
|
139
|
+
case ".ts":
|
|
140
|
+
case ".tsx":
|
|
141
|
+
case ".mts": {
|
|
142
|
+
// Find best available TS runtime
|
|
143
|
+
const tsRunner = findTsRunner();
|
|
144
|
+
return rest ? `${tsRunner} ${file} ${rest}` : `${tsRunner} ${file}`;
|
|
145
|
+
}
|
|
146
|
+
case ".py":
|
|
147
|
+
return rest ? `python ${file} ${rest}` : `python ${file}`;
|
|
148
|
+
default:
|
|
149
|
+
return input;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function findTsRunner() {
|
|
153
|
+
// Check for tsx (fastest, most compatible)
|
|
154
|
+
try {
|
|
155
|
+
const { execSync } = require("child_process");
|
|
156
|
+
execSync("tsx --version", { stdio: "ignore" });
|
|
157
|
+
return "tsx";
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// not available
|
|
161
|
+
}
|
|
162
|
+
// Check for ts-node
|
|
163
|
+
try {
|
|
164
|
+
const { execSync } = require("child_process");
|
|
165
|
+
execSync("ts-node --version", { stdio: "ignore" });
|
|
166
|
+
return "ts-node";
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// not available
|
|
170
|
+
}
|
|
171
|
+
// Check for bun (supports TS natively)
|
|
172
|
+
try {
|
|
173
|
+
const { execSync } = require("child_process");
|
|
174
|
+
execSync("bun --version", { stdio: "ignore" });
|
|
175
|
+
return "bun";
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// not available
|
|
179
|
+
}
|
|
180
|
+
// Fallback to npx tsx
|
|
181
|
+
return "npx tsx";
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* `trickle run <command>` — Run any command with universal type observation.
|
|
185
|
+
*
|
|
186
|
+
* Auto-detects JS or Python, injects the right instrumentation, starts the
|
|
187
|
+
* backend if needed, and shows a summary of captured types after exit.
|
|
188
|
+
* With --stubs or --annotate, also generates type files automatically.
|
|
189
|
+
* Reads .tricklerc.json for project defaults.
|
|
190
|
+
*/
|
|
191
|
+
async function runCommand(command, opts) {
|
|
192
|
+
if (!command) {
|
|
193
|
+
console.error(chalk_1.default.red("\n Usage: trickle run <command>\n"));
|
|
194
|
+
console.error(chalk_1.default.gray(" Examples:"));
|
|
195
|
+
console.error(chalk_1.default.gray(' trickle run "node app.js"'));
|
|
196
|
+
console.error(chalk_1.default.gray(" trickle run app.ts # auto-detects TypeScript runtime"));
|
|
197
|
+
console.error(chalk_1.default.gray(" trickle run script.py # auto-detects Python"));
|
|
198
|
+
console.error(chalk_1.default.gray(' trickle run "node app.js" --stubs src/'));
|
|
199
|
+
console.error(chalk_1.default.gray(" trickle run app.js --watch # watch for changes and re-run"));
|
|
200
|
+
console.error("");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
// Load project config
|
|
204
|
+
const config = loadProjectConfig();
|
|
205
|
+
opts = mergeConfigWithOpts(opts, config);
|
|
206
|
+
// Detect if command is a single file — if so, auto-generate sidecar types
|
|
207
|
+
const singleFile = detectSingleFile(command);
|
|
208
|
+
// Auto-detect runtime from file extension
|
|
209
|
+
const resolvedCommand = autoDetectCommand(command);
|
|
210
|
+
const backendUrl = (0, config_1.getBackendUrl)();
|
|
211
|
+
// Auto-start backend if not running — fall back to local mode
|
|
212
|
+
let backendProc = null;
|
|
213
|
+
let localMode = false;
|
|
214
|
+
const backendRunning = await checkBackend(backendUrl);
|
|
215
|
+
if (!backendRunning) {
|
|
216
|
+
// Only try auto-start if using default URL (custom URL means user manages their own backend)
|
|
217
|
+
const isCustomUrl = !!process.env.TRICKLE_BACKEND_URL &&
|
|
218
|
+
process.env.TRICKLE_BACKEND_URL !== "http://localhost:4888";
|
|
219
|
+
if (!isCustomUrl) {
|
|
220
|
+
backendProc = await autoStartBackend();
|
|
221
|
+
}
|
|
222
|
+
if (!backendProc) {
|
|
223
|
+
// Fall back to local/offline mode instead of exiting
|
|
224
|
+
localMode = true;
|
|
225
|
+
console.log(chalk_1.default.yellow(`\n Backend not available — using local mode (offline)`));
|
|
226
|
+
console.log(chalk_1.default.gray(" Observations will be saved to .trickle/observations.jsonl"));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Detect language and inject instrumentation
|
|
230
|
+
const { instrumentedCommand, env: extraEnv } = injectObservation(resolvedCommand, backendUrl, opts);
|
|
231
|
+
// Print header
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(chalk_1.default.bold(opts.watch ? " trickle run --watch" : " trickle run"));
|
|
234
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
235
|
+
if (resolvedCommand !== command) {
|
|
236
|
+
console.log(chalk_1.default.gray(` File: ${command}`));
|
|
237
|
+
console.log(chalk_1.default.gray(` Resolved: ${resolvedCommand}`));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
console.log(chalk_1.default.gray(` Command: ${command}`));
|
|
241
|
+
}
|
|
242
|
+
if (instrumentedCommand !== resolvedCommand) {
|
|
243
|
+
console.log(chalk_1.default.gray(` Injected: ${instrumentedCommand}`));
|
|
244
|
+
}
|
|
245
|
+
if (localMode) {
|
|
246
|
+
console.log(chalk_1.default.gray(` Mode: local (offline)`));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
console.log(chalk_1.default.gray(` Backend: ${backendUrl}`));
|
|
250
|
+
}
|
|
251
|
+
if (config) {
|
|
252
|
+
console.log(chalk_1.default.gray(` Config: .tricklerc.json`));
|
|
253
|
+
}
|
|
254
|
+
if (opts.stubs) {
|
|
255
|
+
console.log(chalk_1.default.gray(` Stubs: ${opts.stubs}`));
|
|
256
|
+
}
|
|
257
|
+
if (opts.annotate) {
|
|
258
|
+
console.log(chalk_1.default.gray(` Annotate: ${opts.annotate}`));
|
|
259
|
+
}
|
|
260
|
+
if (opts.watch) {
|
|
261
|
+
console.log(chalk_1.default.gray(` Watch: enabled`));
|
|
262
|
+
}
|
|
263
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
264
|
+
console.log("");
|
|
265
|
+
// Shared env for all runs
|
|
266
|
+
const runEnv = {
|
|
267
|
+
...extraEnv,
|
|
268
|
+
TRICKLE_BACKEND_URL: backendUrl,
|
|
269
|
+
TRICKLE_DEBUG: process.env.TRICKLE_DEBUG || "",
|
|
270
|
+
};
|
|
271
|
+
// In local mode, set TRICKLE_LOCAL=1 so the client writes to JSONL
|
|
272
|
+
if (localMode) {
|
|
273
|
+
runEnv.TRICKLE_LOCAL = "1";
|
|
274
|
+
// Forward TRICKLE_LOCAL_DIR if set
|
|
275
|
+
if (process.env.TRICKLE_LOCAL_DIR) {
|
|
276
|
+
runEnv.TRICKLE_LOCAL_DIR = process.env.TRICKLE_LOCAL_DIR;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Execute the single-run flow
|
|
280
|
+
const exitCode = await executeSingleRun(instrumentedCommand, runEnv, opts, singleFile, localMode);
|
|
281
|
+
// If --watch, enter watch loop instead of exiting
|
|
282
|
+
if (opts.watch) {
|
|
283
|
+
await enterWatchLoop(command, instrumentedCommand, runEnv, opts, singleFile, backendProc, localMode);
|
|
284
|
+
// enterWatchLoop never returns (handles its own exit)
|
|
285
|
+
}
|
|
286
|
+
// Clean up
|
|
287
|
+
if (backendProc) {
|
|
288
|
+
backendProc.kill("SIGTERM");
|
|
289
|
+
await sleep(500);
|
|
290
|
+
}
|
|
291
|
+
process.exit(exitCode);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Execute a single observation run: run the command, wait for flush, show summary.
|
|
295
|
+
*/
|
|
296
|
+
async function executeSingleRun(instrumentedCommand, env, opts, singleFile, localMode) {
|
|
297
|
+
if (!localMode) {
|
|
298
|
+
// Snapshot functions before run (to compute delta)
|
|
299
|
+
let functionsBefore = [];
|
|
300
|
+
let errorsBefore = [];
|
|
301
|
+
try {
|
|
302
|
+
const fb = await (0, api_client_1.listFunctions)();
|
|
303
|
+
functionsBefore = fb.functions;
|
|
304
|
+
const eb = await (0, api_client_1.listErrors)();
|
|
305
|
+
errorsBefore = eb.errors;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Backend might not have data yet
|
|
309
|
+
}
|
|
310
|
+
// Start live type generation for backend mode
|
|
311
|
+
let liveStop = null;
|
|
312
|
+
if (singleFile && !opts.stubs) {
|
|
313
|
+
liveStop = startLiveBackendTypes(singleFile);
|
|
314
|
+
}
|
|
315
|
+
// Run the instrumented command
|
|
316
|
+
const exitCode = await runProcess(instrumentedCommand, env);
|
|
317
|
+
// Stop live watcher
|
|
318
|
+
if (liveStop)
|
|
319
|
+
liveStop();
|
|
320
|
+
// Wait for transport to flush
|
|
321
|
+
console.log(chalk_1.default.gray("\n Waiting for type data to flush..."));
|
|
322
|
+
await sleep(3000);
|
|
323
|
+
// Show summary with inline type signatures
|
|
324
|
+
await showSummary(functionsBefore, errorsBefore);
|
|
325
|
+
// Auto-generate stubs if --stubs was specified
|
|
326
|
+
if (opts.stubs) {
|
|
327
|
+
await autoGenerateStubs(opts.stubs);
|
|
328
|
+
}
|
|
329
|
+
// Auto-annotate if --annotate was specified
|
|
330
|
+
if (opts.annotate) {
|
|
331
|
+
await autoAnnotateFiles(opts.annotate);
|
|
332
|
+
}
|
|
333
|
+
// Auto-generate sidecar type file when invoked with a single file
|
|
334
|
+
// (unless --stubs was explicitly specified, which overrides this)
|
|
335
|
+
if (singleFile && !opts.stubs) {
|
|
336
|
+
await autoGenerateSidecar(singleFile);
|
|
337
|
+
}
|
|
338
|
+
return exitCode;
|
|
339
|
+
}
|
|
340
|
+
// ── Local/offline mode ──
|
|
341
|
+
const localDir = env.TRICKLE_LOCAL_DIR || process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), ".trickle");
|
|
342
|
+
const jsonlPath = path.join(localDir, "observations.jsonl");
|
|
343
|
+
const { generateLocalStubs, generateFromJsonl } = await Promise.resolve().then(() => __importStar(require("../local-codegen")));
|
|
344
|
+
// Start live type generation — types update while the process runs
|
|
345
|
+
let liveTypesStop = null;
|
|
346
|
+
if (singleFile) {
|
|
347
|
+
liveTypesStop = startLiveLocalTypes(singleFile, jsonlPath, generateLocalStubs);
|
|
348
|
+
}
|
|
349
|
+
// Run the instrumented command
|
|
350
|
+
const exitCode = await runProcess(instrumentedCommand, env);
|
|
351
|
+
// Stop live watcher
|
|
352
|
+
if (liveTypesStop)
|
|
353
|
+
liveTypesStop();
|
|
354
|
+
// Brief pause for any async file writes to complete
|
|
355
|
+
await sleep(500);
|
|
356
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
357
|
+
console.log(chalk_1.default.gray("\n No observations captured."));
|
|
358
|
+
return exitCode;
|
|
359
|
+
}
|
|
360
|
+
// Final type generation (catches any remaining observations)
|
|
361
|
+
if (singleFile) {
|
|
362
|
+
generateLocalStubs(singleFile, jsonlPath);
|
|
363
|
+
}
|
|
364
|
+
// Show local summary
|
|
365
|
+
const stubs = generateFromJsonl(jsonlPath);
|
|
366
|
+
const allModules = Object.keys(stubs);
|
|
367
|
+
let totalFunctions = 0;
|
|
368
|
+
for (const mod of allModules) {
|
|
369
|
+
const lines = stubs[mod].ts.split("\n");
|
|
370
|
+
totalFunctions += lines.filter((l) => l.startsWith("export declare function")).length;
|
|
371
|
+
}
|
|
372
|
+
console.log("");
|
|
373
|
+
console.log(chalk_1.default.bold(" Summary (local mode)"));
|
|
374
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
375
|
+
console.log(` Functions observed: ${chalk_1.default.bold(String(totalFunctions))}`);
|
|
376
|
+
console.log(` Data saved to: ${chalk_1.default.gray(jsonlPath)}`);
|
|
377
|
+
if (singleFile) {
|
|
378
|
+
const ext = path.extname(singleFile).toLowerCase();
|
|
379
|
+
const isPython = ext === ".py";
|
|
380
|
+
const baseName = path.basename(singleFile, ext);
|
|
381
|
+
const stubExt = isPython ? ".pyi" : ".d.ts";
|
|
382
|
+
const stubFile = path.join(path.dirname(singleFile), `${baseName}${stubExt}`);
|
|
383
|
+
if (fs.existsSync(stubFile)) {
|
|
384
|
+
const relPath = path.relative(process.cwd(), stubFile);
|
|
385
|
+
console.log(chalk_1.default.green(` Types written to ${chalk_1.default.bold(relPath)}`));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
389
|
+
console.log("");
|
|
390
|
+
return exitCode;
|
|
391
|
+
}
|
|
392
|
+
// ── Live type generation ──
|
|
393
|
+
/**
|
|
394
|
+
* Start a background watcher that regenerates type stubs whenever the
|
|
395
|
+
* JSONL file changes. Returns a stop function.
|
|
396
|
+
*
|
|
397
|
+
* Uses polling (fs.watchFile) because the file is being appended to by
|
|
398
|
+
* the child process and fs.watch can be unreliable with rapid appends.
|
|
399
|
+
*/
|
|
400
|
+
function startLiveLocalTypes(sourceFile, jsonlPath, generateLocalStubs) {
|
|
401
|
+
let lastSize = 0;
|
|
402
|
+
let lastFunctionCount = 0;
|
|
403
|
+
let debounceTimer = null;
|
|
404
|
+
let stopped = false;
|
|
405
|
+
const regenerate = () => {
|
|
406
|
+
if (stopped)
|
|
407
|
+
return;
|
|
408
|
+
try {
|
|
409
|
+
if (!fs.existsSync(jsonlPath))
|
|
410
|
+
return;
|
|
411
|
+
const stat = fs.statSync(jsonlPath);
|
|
412
|
+
if (stat.size === lastSize)
|
|
413
|
+
return; // no new data
|
|
414
|
+
lastSize = stat.size;
|
|
415
|
+
const { written, functionCount } = generateLocalStubs(sourceFile, jsonlPath);
|
|
416
|
+
if (written.length > 0 && functionCount > lastFunctionCount) {
|
|
417
|
+
const newCount = functionCount - lastFunctionCount;
|
|
418
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
419
|
+
const relPath = path.relative(process.cwd(), written[0]);
|
|
420
|
+
console.log(chalk_1.default.gray(` [${ts}]`) +
|
|
421
|
+
chalk_1.default.green(` +${newCount} type(s)`) +
|
|
422
|
+
chalk_1.default.gray(` → ${relPath}`) +
|
|
423
|
+
chalk_1.default.gray(` (${functionCount} total)`));
|
|
424
|
+
lastFunctionCount = functionCount;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// Never crash — this is a background helper
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
// Do an initial check after a short delay (catch fast-running scripts)
|
|
432
|
+
const initialTimer = setTimeout(regenerate, 800);
|
|
433
|
+
// Poll every 2 seconds
|
|
434
|
+
const interval = setInterval(regenerate, 2000);
|
|
435
|
+
// Also try fs.watchFile for faster response on changes
|
|
436
|
+
try {
|
|
437
|
+
fs.watchFile(jsonlPath, { interval: 1000 }, () => {
|
|
438
|
+
if (debounceTimer)
|
|
439
|
+
clearTimeout(debounceTimer);
|
|
440
|
+
debounceTimer = setTimeout(regenerate, 200);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// watchFile may fail if file doesn't exist yet — polling handles it
|
|
445
|
+
}
|
|
446
|
+
return () => {
|
|
447
|
+
stopped = true;
|
|
448
|
+
clearTimeout(initialTimer);
|
|
449
|
+
clearInterval(interval);
|
|
450
|
+
if (debounceTimer)
|
|
451
|
+
clearTimeout(debounceTimer);
|
|
452
|
+
try {
|
|
453
|
+
fs.unwatchFile(jsonlPath);
|
|
454
|
+
}
|
|
455
|
+
catch { }
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Start a background poller that fetches stubs from the backend and
|
|
460
|
+
* writes sidecar type files while the process runs. Returns a stop function.
|
|
461
|
+
*/
|
|
462
|
+
function startLiveBackendTypes(sourceFile) {
|
|
463
|
+
let lastFunctionCount = 0;
|
|
464
|
+
let stopped = false;
|
|
465
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
466
|
+
const isPython = ext === ".py";
|
|
467
|
+
const dir = path.dirname(sourceFile);
|
|
468
|
+
const baseName = path.basename(sourceFile, ext);
|
|
469
|
+
const sidecarName = isPython ? `${baseName}.pyi` : `${baseName}.d.ts`;
|
|
470
|
+
const sidecarPath = path.join(dir, sidecarName);
|
|
471
|
+
const poll = async () => {
|
|
472
|
+
if (stopped)
|
|
473
|
+
return;
|
|
474
|
+
try {
|
|
475
|
+
const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
|
|
476
|
+
await stubsCommand(dir, { silent: true });
|
|
477
|
+
if (fs.existsSync(sidecarPath)) {
|
|
478
|
+
const content = fs.readFileSync(sidecarPath, "utf-8");
|
|
479
|
+
const funcCount = (content.match(/export declare function/g) || []).length;
|
|
480
|
+
if (funcCount > lastFunctionCount) {
|
|
481
|
+
const newCount = funcCount - lastFunctionCount;
|
|
482
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
483
|
+
console.log(chalk_1.default.gray(` [${ts}]`) +
|
|
484
|
+
chalk_1.default.green(` +${newCount} type(s)`) +
|
|
485
|
+
chalk_1.default.gray(` → ${sidecarName}`) +
|
|
486
|
+
chalk_1.default.gray(` (${funcCount} total)`));
|
|
487
|
+
lastFunctionCount = funcCount;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Never crash — background helper
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
// Poll every 3 seconds (backend mode has higher overhead)
|
|
496
|
+
const interval = setInterval(poll, 3000);
|
|
497
|
+
return () => {
|
|
498
|
+
stopped = true;
|
|
499
|
+
clearInterval(interval);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// ── Auto-generate sidecar type file ──
|
|
503
|
+
async function autoGenerateSidecar(filePath) {
|
|
504
|
+
try {
|
|
505
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
506
|
+
const isPython = ext === ".py";
|
|
507
|
+
const dir = path.dirname(filePath);
|
|
508
|
+
const baseName = path.basename(filePath, ext);
|
|
509
|
+
// Determine sidecar filename
|
|
510
|
+
const sidecarName = isPython ? `${baseName}.pyi` : `${baseName}.d.ts`;
|
|
511
|
+
const sidecarPath = path.join(dir, sidecarName);
|
|
512
|
+
// Use the stubs command to generate stubs for the file's directory
|
|
513
|
+
const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
|
|
514
|
+
await stubsCommand(dir, { silent: true });
|
|
515
|
+
// Check if the sidecar was generated
|
|
516
|
+
if (fs.existsSync(sidecarPath)) {
|
|
517
|
+
const stats = fs.statSync(sidecarPath);
|
|
518
|
+
if (stats.size > 0) {
|
|
519
|
+
console.log(chalk_1.default.green(`\n Types written to ${chalk_1.default.bold(sidecarName)}`));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Don't fail the run if sidecar generation fails
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// ── Watch mode ──
|
|
528
|
+
/**
|
|
529
|
+
* Find source files to watch based on the command.
|
|
530
|
+
* Returns the directory to watch and specific file paths.
|
|
531
|
+
*/
|
|
532
|
+
function findWatchTargets(command) {
|
|
533
|
+
const parts = command.split(/\s+/);
|
|
534
|
+
// Find the first token that looks like a file path
|
|
535
|
+
for (const part of parts) {
|
|
536
|
+
const ext = path.extname(part).toLowerCase();
|
|
537
|
+
if ([".js", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".py", ".jsx"].includes(ext)) {
|
|
538
|
+
const resolved = path.resolve(part);
|
|
539
|
+
if (fs.existsSync(resolved)) {
|
|
540
|
+
return {
|
|
541
|
+
dir: path.dirname(resolved),
|
|
542
|
+
file: resolved,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return { dir: process.cwd(), file: null };
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Enter watch mode — watch source files and re-run on changes.
|
|
551
|
+
*/
|
|
552
|
+
async function enterWatchLoop(originalCommand, instrumentedCommand, env, opts, singleFile, backendProc, localMode) {
|
|
553
|
+
const { dir: watchDir, file: watchFile } = findWatchTargets(originalCommand);
|
|
554
|
+
const watchExts = new Set([".js", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".py", ".jsx"]);
|
|
555
|
+
const ignoreDirs = new Set(["node_modules", ".git", "dist", "build", "__pycache__", ".trickle"]);
|
|
556
|
+
console.log("");
|
|
557
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
558
|
+
console.log(chalk_1.default.cyan(" Watching for changes...") + chalk_1.default.gray(` (${watchDir})`));
|
|
559
|
+
console.log(chalk_1.default.gray(" Press Ctrl+C to stop."));
|
|
560
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
561
|
+
console.log("");
|
|
562
|
+
let debounceTimer = null;
|
|
563
|
+
let runCount = 1;
|
|
564
|
+
const triggerRerun = () => {
|
|
565
|
+
if (debounceTimer)
|
|
566
|
+
clearTimeout(debounceTimer);
|
|
567
|
+
debounceTimer = setTimeout(async () => {
|
|
568
|
+
runCount++;
|
|
569
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
570
|
+
console.log("");
|
|
571
|
+
console.log(chalk_1.default.cyan(` [${ts}]`) + chalk_1.default.bold(` Re-running (#${runCount})...`));
|
|
572
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
573
|
+
try {
|
|
574
|
+
await executeSingleRun(instrumentedCommand, env, opts, singleFile, localMode);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
console.log(chalk_1.default.red(" Run failed. Waiting for next change..."));
|
|
578
|
+
}
|
|
579
|
+
console.log("");
|
|
580
|
+
console.log(chalk_1.default.gray(" Watching for changes..."));
|
|
581
|
+
}, 300); // 300ms debounce
|
|
582
|
+
};
|
|
583
|
+
// Use fs.watch with recursive option (supported on macOS and Windows)
|
|
584
|
+
try {
|
|
585
|
+
const watcher = fs.watch(watchDir, { recursive: true }, (_eventType, filename) => {
|
|
586
|
+
if (!filename)
|
|
587
|
+
return;
|
|
588
|
+
// Check file extension
|
|
589
|
+
const ext = path.extname(filename).toLowerCase();
|
|
590
|
+
if (!watchExts.has(ext))
|
|
591
|
+
return;
|
|
592
|
+
// Skip ignored directories
|
|
593
|
+
const parts = filename.split(path.sep);
|
|
594
|
+
if (parts.some(p => ignoreDirs.has(p)))
|
|
595
|
+
return;
|
|
596
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
597
|
+
console.log(chalk_1.default.gray(` [${ts}] Changed: ${filename}`));
|
|
598
|
+
triggerRerun();
|
|
599
|
+
});
|
|
600
|
+
// Handle graceful shutdown
|
|
601
|
+
const cleanup = () => {
|
|
602
|
+
watcher.close();
|
|
603
|
+
if (debounceTimer)
|
|
604
|
+
clearTimeout(debounceTimer);
|
|
605
|
+
if (backendProc) {
|
|
606
|
+
backendProc.kill("SIGTERM");
|
|
607
|
+
}
|
|
608
|
+
console.log(chalk_1.default.gray("\n Watch stopped.\n"));
|
|
609
|
+
process.exit(0);
|
|
610
|
+
};
|
|
611
|
+
process.on("SIGINT", cleanup);
|
|
612
|
+
process.on("SIGTERM", cleanup);
|
|
613
|
+
// Keep the process alive
|
|
614
|
+
await new Promise(() => { });
|
|
615
|
+
}
|
|
616
|
+
catch (err) {
|
|
617
|
+
// Fallback: if recursive watch isn't supported, watch just the target file
|
|
618
|
+
if (watchFile) {
|
|
619
|
+
console.log(chalk_1.default.gray(" (Watching single file: " + path.basename(watchFile) + ")"));
|
|
620
|
+
const watcher = fs.watch(watchFile, () => {
|
|
621
|
+
const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
622
|
+
console.log(chalk_1.default.gray(` [${ts}] Changed: ${path.basename(watchFile)}`));
|
|
623
|
+
triggerRerun();
|
|
624
|
+
});
|
|
625
|
+
const cleanup = () => {
|
|
626
|
+
watcher.close();
|
|
627
|
+
if (debounceTimer)
|
|
628
|
+
clearTimeout(debounceTimer);
|
|
629
|
+
if (backendProc) {
|
|
630
|
+
backendProc.kill("SIGTERM");
|
|
631
|
+
}
|
|
632
|
+
console.log(chalk_1.default.gray("\n Watch stopped.\n"));
|
|
633
|
+
process.exit(0);
|
|
634
|
+
};
|
|
635
|
+
process.on("SIGINT", cleanup);
|
|
636
|
+
process.on("SIGTERM", cleanup);
|
|
637
|
+
await new Promise(() => { });
|
|
638
|
+
}
|
|
639
|
+
// Can't watch anything
|
|
640
|
+
console.error(chalk_1.default.red(" Could not set up file watcher."));
|
|
641
|
+
if (backendProc)
|
|
642
|
+
backendProc.kill("SIGTERM");
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// ── Auto-generate stubs ──
|
|
647
|
+
async function autoGenerateStubs(dir) {
|
|
648
|
+
try {
|
|
649
|
+
const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
|
|
650
|
+
await stubsCommand(dir, {});
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (err instanceof Error) {
|
|
654
|
+
console.error(chalk_1.default.yellow(`\n Stubs generation warning: ${err.message}`));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// ── Auto-annotate files ──
|
|
659
|
+
async function autoAnnotateFiles(fileOrDir) {
|
|
660
|
+
try {
|
|
661
|
+
const { annotateCommand } = await Promise.resolve().then(() => __importStar(require("./annotate")));
|
|
662
|
+
const resolved = path.resolve(fileOrDir);
|
|
663
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
664
|
+
// Annotate all JS/TS/Python files in the directory
|
|
665
|
+
const files = findAnnotatableFiles(resolved);
|
|
666
|
+
if (files.length === 0) {
|
|
667
|
+
console.log(chalk_1.default.gray(`\n No annotatable files found in ${fileOrDir}`));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
for (const file of files) {
|
|
671
|
+
await annotateCommand(file, {});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
// Annotate a single file
|
|
676
|
+
await annotateCommand(fileOrDir, {});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
if (err instanceof Error) {
|
|
681
|
+
console.error(chalk_1.default.yellow(`\n Annotation warning: ${err.message}`));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function findAnnotatableFiles(dir) {
|
|
686
|
+
const results = [];
|
|
687
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
const fullPath = path.join(dir, entry.name);
|
|
690
|
+
if (entry.isDirectory()) {
|
|
691
|
+
if (["node_modules", "__pycache__", ".git", "dist", "build", ".trickle"].includes(entry.name))
|
|
692
|
+
continue;
|
|
693
|
+
results.push(...findAnnotatableFiles(fullPath));
|
|
694
|
+
}
|
|
695
|
+
else if (entry.isFile()) {
|
|
696
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
697
|
+
if ([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".py"].includes(ext)) {
|
|
698
|
+
results.push(fullPath);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return results;
|
|
703
|
+
}
|
|
704
|
+
// ── Inline type signatures in summary ──
|
|
705
|
+
async function fetchTypeSignatures(newFunctions) {
|
|
706
|
+
try {
|
|
707
|
+
const { annotations } = await (0, api_client_1.fetchAnnotations)({});
|
|
708
|
+
return annotations || {};
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return {};
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function formatSignature(fnName, annotation, maxLen = 90) {
|
|
715
|
+
const params = annotation.params
|
|
716
|
+
.map((p) => `${p.name}: ${p.type}`)
|
|
717
|
+
.join(", ");
|
|
718
|
+
const sig = `${fnName}(${params}) → ${annotation.returnType}`;
|
|
719
|
+
if (sig.length > maxLen) {
|
|
720
|
+
return sig.substring(0, maxLen - 1) + "…";
|
|
721
|
+
}
|
|
722
|
+
return sig;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Detect if a script file uses ES modules.
|
|
726
|
+
*/
|
|
727
|
+
function isEsmFile(command) {
|
|
728
|
+
const parts = command.split(/\s+/);
|
|
729
|
+
for (const part of parts) {
|
|
730
|
+
if (part.endsWith(".mjs") || part.endsWith(".mts"))
|
|
731
|
+
return true;
|
|
732
|
+
if (part.endsWith(".js") ||
|
|
733
|
+
part.endsWith(".ts") ||
|
|
734
|
+
part.endsWith(".tsx") ||
|
|
735
|
+
part.endsWith(".jsx")) {
|
|
736
|
+
const filePath = path.resolve(part);
|
|
737
|
+
try {
|
|
738
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
739
|
+
if (/^\s*(import|export)\s/m.test(content))
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
// File might not exist at this path
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
let dir = path.dirname(filePath);
|
|
747
|
+
for (let i = 0; i < 10; i++) {
|
|
748
|
+
const pkgPath = path.join(dir, "package.json");
|
|
749
|
+
if (fs.existsSync(pkgPath)) {
|
|
750
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
751
|
+
if (pkg.type === "module")
|
|
752
|
+
return true;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
const parent = path.dirname(dir);
|
|
756
|
+
if (parent === dir)
|
|
757
|
+
break;
|
|
758
|
+
dir = parent;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// Ignore
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Detect the language and inject the appropriate auto-observation mechanism.
|
|
770
|
+
*/
|
|
771
|
+
function injectObservation(command, backendUrl, opts) {
|
|
772
|
+
const env = {};
|
|
773
|
+
if (command.includes("trickle-observe/observe") ||
|
|
774
|
+
command.includes("trickle-observe/register") ||
|
|
775
|
+
command.includes("trickle/observe") ||
|
|
776
|
+
command.includes("trickle/register") ||
|
|
777
|
+
command.includes("-m trickle")) {
|
|
778
|
+
return { instrumentedCommand: command, env };
|
|
779
|
+
}
|
|
780
|
+
const observePath = resolveObservePath();
|
|
781
|
+
const observeEsmPath = resolveObserveEsmPath();
|
|
782
|
+
if (opts.include)
|
|
783
|
+
env.TRICKLE_OBSERVE_INCLUDE = opts.include;
|
|
784
|
+
if (opts.exclude)
|
|
785
|
+
env.TRICKLE_OBSERVE_EXCLUDE = opts.exclude;
|
|
786
|
+
const nodeMatch = command.match(/^(node|ts-node|tsx|nodemon)\s/);
|
|
787
|
+
if (nodeMatch) {
|
|
788
|
+
const runner = nodeMatch[1];
|
|
789
|
+
const useEsm = isEsmFile(command) && observeEsmPath;
|
|
790
|
+
if (useEsm) {
|
|
791
|
+
const modified = command.replace(new RegExp(`^${runner}\\s`), `${runner} --import ${observeEsmPath} `);
|
|
792
|
+
return { instrumentedCommand: modified, env };
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
const modified = command.replace(new RegExp(`^${runner}\\s`), `${runner} -r ${observePath} `);
|
|
796
|
+
return { instrumentedCommand: modified, env };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (/^(vitest|jest|mocha|npx|bunx|bun)\b/.test(command)) {
|
|
800
|
+
const existing = process.env.NODE_OPTIONS || "";
|
|
801
|
+
if (observeEsmPath) {
|
|
802
|
+
env.NODE_OPTIONS =
|
|
803
|
+
`${existing} -r ${observePath} --import ${observeEsmPath}`.trim();
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
env.NODE_OPTIONS = `${existing} -r ${observePath}`.trim();
|
|
807
|
+
}
|
|
808
|
+
return { instrumentedCommand: command, env };
|
|
809
|
+
}
|
|
810
|
+
const pyMatch = command.match(/^(python3?|python3?\.\d+)\s/);
|
|
811
|
+
if (pyMatch) {
|
|
812
|
+
const python = pyMatch[1];
|
|
813
|
+
const rest = command.slice(pyMatch[0].length);
|
|
814
|
+
if (opts.include)
|
|
815
|
+
env.TRICKLE_OBSERVE_INCLUDE = opts.include;
|
|
816
|
+
if (opts.exclude)
|
|
817
|
+
env.TRICKLE_OBSERVE_EXCLUDE = opts.exclude;
|
|
818
|
+
return {
|
|
819
|
+
instrumentedCommand: `${python} -c "from trickle.observe_runner import main; main()" ${rest}`,
|
|
820
|
+
env,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (/^(pytest|uvicorn|gunicorn|flask|django-admin)\b/.test(command)) {
|
|
824
|
+
if (opts.include)
|
|
825
|
+
env.TRICKLE_OBSERVE_INCLUDE = opts.include;
|
|
826
|
+
if (opts.exclude)
|
|
827
|
+
env.TRICKLE_OBSERVE_EXCLUDE = opts.exclude;
|
|
828
|
+
return {
|
|
829
|
+
instrumentedCommand: `python -c "from trickle.observe_runner import main; main()" -m ${command}`,
|
|
830
|
+
env,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
console.log(chalk_1.default.yellow(" Could not detect language. Trying Node.js instrumentation..."));
|
|
834
|
+
const existing = process.env.NODE_OPTIONS || "";
|
|
835
|
+
env.NODE_OPTIONS = `${existing} -r ${observePath}`.trim();
|
|
836
|
+
return { instrumentedCommand: command, env };
|
|
837
|
+
}
|
|
838
|
+
function resolveObservePath() {
|
|
839
|
+
try {
|
|
840
|
+
return require.resolve("trickle-observe/observe");
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// Not in node_modules
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
return require.resolve("trickle/observe");
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
// Not in node_modules
|
|
850
|
+
}
|
|
851
|
+
const monorepoPath = path.resolve(__dirname, "..", "..", "..", "client-js", "observe.js");
|
|
852
|
+
if (fs.existsSync(monorepoPath))
|
|
853
|
+
return monorepoPath;
|
|
854
|
+
return "trickle-observe/observe";
|
|
855
|
+
}
|
|
856
|
+
function resolveObserveEsmPath() {
|
|
857
|
+
try {
|
|
858
|
+
return require.resolve("trickle-observe/observe-esm");
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
// Not in node_modules
|
|
862
|
+
}
|
|
863
|
+
const monorepoPath = path.resolve(__dirname, "..", "..", "..", "client-js", "observe-esm.mjs");
|
|
864
|
+
if (fs.existsSync(monorepoPath))
|
|
865
|
+
return monorepoPath;
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
function runProcess(command, env) {
|
|
869
|
+
return new Promise((resolve) => {
|
|
870
|
+
const proc = (0, child_process_1.spawn)(command, [], {
|
|
871
|
+
stdio: "inherit",
|
|
872
|
+
shell: true,
|
|
873
|
+
env: { ...process.env, ...env },
|
|
874
|
+
});
|
|
875
|
+
proc.on("error", (err) => {
|
|
876
|
+
console.error(chalk_1.default.red(`\n Failed to start: ${err.message}\n`));
|
|
877
|
+
resolve(1);
|
|
878
|
+
});
|
|
879
|
+
proc.on("exit", (code) => {
|
|
880
|
+
resolve(code ?? 1);
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Show a summary of what was captured during the run, with inline type signatures.
|
|
886
|
+
*/
|
|
887
|
+
async function showSummary(functionsBefore, errorsBefore) {
|
|
888
|
+
try {
|
|
889
|
+
const { functions } = await (0, api_client_1.listFunctions)();
|
|
890
|
+
const { errors } = await (0, api_client_1.listErrors)();
|
|
891
|
+
const beforeIds = new Set(functionsBefore.map((f) => f.id));
|
|
892
|
+
const newFunctions = functions.filter((f) => !beforeIds.has(f.id));
|
|
893
|
+
const beforeErrorIds = new Set(errorsBefore.map((e) => e.id));
|
|
894
|
+
const newErrors = errors.filter((e) => !beforeErrorIds.has(e.id));
|
|
895
|
+
// Fetch inline type signatures for the new functions
|
|
896
|
+
const annotations = await fetchTypeSignatures(newFunctions);
|
|
897
|
+
console.log("");
|
|
898
|
+
console.log(chalk_1.default.bold(" Summary"));
|
|
899
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
900
|
+
if (functions.length === 0) {
|
|
901
|
+
console.log(chalk_1.default.yellow(" No functions captured. The command may not have"));
|
|
902
|
+
console.log(chalk_1.default.yellow(" loaded any modules that could be instrumented."));
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
console.log(` Functions observed: ${chalk_1.default.bold(String(functions.length))} total, ${chalk_1.default.green(String(newFunctions.length) + " new")}`);
|
|
906
|
+
if (newFunctions.length > 0) {
|
|
907
|
+
console.log("");
|
|
908
|
+
const shown = newFunctions.slice(0, 15);
|
|
909
|
+
for (const fn of shown) {
|
|
910
|
+
const annotation = annotations[fn.function_name];
|
|
911
|
+
if (annotation) {
|
|
912
|
+
// Show full type signature
|
|
913
|
+
const sig = formatSignature(fn.function_name, annotation);
|
|
914
|
+
console.log(` ${chalk_1.default.green("+")} ${sig}`);
|
|
915
|
+
console.log(chalk_1.default.gray(` ${fn.module} module`));
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
const moduleBadge = chalk_1.default.gray(`[${fn.module}]`);
|
|
919
|
+
console.log(` ${chalk_1.default.green("+")} ${fn.function_name} ${moduleBadge}`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (newFunctions.length > 15) {
|
|
923
|
+
console.log(chalk_1.default.gray(` ... and ${newFunctions.length - 15} more`));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (newErrors.length > 0) {
|
|
927
|
+
console.log("");
|
|
928
|
+
console.log(` Errors captured: ${chalk_1.default.red(String(newErrors.length))}`);
|
|
929
|
+
const shownErrors = newErrors.slice(0, 5);
|
|
930
|
+
for (const err of shownErrors) {
|
|
931
|
+
const fn = functions.find((f) => f.id === err.function_id);
|
|
932
|
+
const fnName = fn ? fn.function_name : "unknown";
|
|
933
|
+
console.log(` ${chalk_1.default.red("!")} ${fnName}: ${chalk_1.default.gray(err.error_message.substring(0, 80))}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
console.log("");
|
|
937
|
+
console.log(chalk_1.default.gray(" Explore results:"));
|
|
938
|
+
console.log(chalk_1.default.gray(" trickle functions # list all captured functions"));
|
|
939
|
+
if (newFunctions.length > 0) {
|
|
940
|
+
const example = newFunctions[0].function_name;
|
|
941
|
+
console.log(chalk_1.default.gray(` trickle types ${example} # see types + sample data`));
|
|
942
|
+
}
|
|
943
|
+
if (newErrors.length > 0) {
|
|
944
|
+
console.log(chalk_1.default.gray(" trickle errors # see captured errors"));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
console.log(chalk_1.default.gray(" " + "─".repeat(50)));
|
|
948
|
+
console.log("");
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
console.log(chalk_1.default.gray("\n Could not fetch summary from backend.\n"));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function checkBackend(url) {
|
|
955
|
+
try {
|
|
956
|
+
const res = await fetch(`${url}/api/health`, {
|
|
957
|
+
signal: AbortSignal.timeout(2000),
|
|
958
|
+
});
|
|
959
|
+
return res.ok;
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async function autoStartBackend() {
|
|
966
|
+
const backendPaths = [
|
|
967
|
+
path.resolve("packages/backend/dist/index.js"),
|
|
968
|
+
path.resolve("node_modules/trickle-backend/dist/index.js"),
|
|
969
|
+
];
|
|
970
|
+
for (const p of backendPaths) {
|
|
971
|
+
if (fs.existsSync(p)) {
|
|
972
|
+
console.log(chalk_1.default.gray(" Auto-starting trickle backend..."));
|
|
973
|
+
const proc = (0, child_process_1.spawn)("node", [p], {
|
|
974
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
975
|
+
env: { ...process.env },
|
|
976
|
+
detached: false,
|
|
977
|
+
});
|
|
978
|
+
proc.stdout?.on("data", () => { });
|
|
979
|
+
proc.stderr?.on("data", () => { });
|
|
980
|
+
proc.unref();
|
|
981
|
+
for (let i = 0; i < 20; i++) {
|
|
982
|
+
await sleep(500);
|
|
983
|
+
const ready = await checkBackend((0, config_1.getBackendUrl)());
|
|
984
|
+
if (ready) {
|
|
985
|
+
console.log(chalk_1.default.gray(" Backend started ✓\n"));
|
|
986
|
+
return proc;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
proc.kill("SIGTERM");
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
function sleep(ms) {
|
|
996
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
997
|
+
}
|