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.
Files changed (125) hide show
  1. package/dist/api-client.d.ts +208 -0
  2. package/dist/api-client.js +237 -0
  3. package/dist/commands/annotate.d.ts +6 -0
  4. package/dist/commands/annotate.js +433 -0
  5. package/dist/commands/audit.d.ts +7 -0
  6. package/dist/commands/audit.js +82 -0
  7. package/dist/commands/auto.d.ts +8 -0
  8. package/dist/commands/auto.js +268 -0
  9. package/dist/commands/capture.d.ts +14 -0
  10. package/dist/commands/capture.js +271 -0
  11. package/dist/commands/check.d.ts +6 -0
  12. package/dist/commands/check.js +408 -0
  13. package/dist/commands/codegen.d.ts +21 -0
  14. package/dist/commands/codegen.js +129 -0
  15. package/dist/commands/coverage.d.ts +13 -0
  16. package/dist/commands/coverage.js +126 -0
  17. package/dist/commands/dashboard.d.ts +1 -0
  18. package/dist/commands/dashboard.js +83 -0
  19. package/dist/commands/dev.d.ts +14 -0
  20. package/dist/commands/dev.js +319 -0
  21. package/dist/commands/diff.d.ts +7 -0
  22. package/dist/commands/diff.js +79 -0
  23. package/dist/commands/docs.d.ts +13 -0
  24. package/dist/commands/docs.js +383 -0
  25. package/dist/commands/errors.d.ts +7 -0
  26. package/dist/commands/errors.js +180 -0
  27. package/dist/commands/export.d.ts +18 -0
  28. package/dist/commands/export.js +238 -0
  29. package/dist/commands/functions.d.ts +6 -0
  30. package/dist/commands/functions.js +71 -0
  31. package/dist/commands/infer.d.ts +14 -0
  32. package/dist/commands/infer.js +275 -0
  33. package/dist/commands/init.d.ts +5 -0
  34. package/dist/commands/init.js +395 -0
  35. package/dist/commands/mock.d.ts +5 -0
  36. package/dist/commands/mock.js +232 -0
  37. package/dist/commands/openapi.d.ts +8 -0
  38. package/dist/commands/openapi.js +82 -0
  39. package/dist/commands/overview.d.ts +11 -0
  40. package/dist/commands/overview.js +266 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.js +133 -0
  43. package/dist/commands/proxy.d.ts +13 -0
  44. package/dist/commands/proxy.js +312 -0
  45. package/dist/commands/replay.d.ts +14 -0
  46. package/dist/commands/replay.js +289 -0
  47. package/dist/commands/run.d.ts +17 -0
  48. package/dist/commands/run.js +997 -0
  49. package/dist/commands/sample.d.ts +13 -0
  50. package/dist/commands/sample.js +260 -0
  51. package/dist/commands/search.d.ts +5 -0
  52. package/dist/commands/search.js +80 -0
  53. package/dist/commands/stubs.d.ts +6 -0
  54. package/dist/commands/stubs.js +187 -0
  55. package/dist/commands/tail.d.ts +4 -0
  56. package/dist/commands/tail.js +76 -0
  57. package/dist/commands/test-gen.d.ts +13 -0
  58. package/dist/commands/test-gen.js +237 -0
  59. package/dist/commands/trace.d.ts +14 -0
  60. package/dist/commands/trace.js +417 -0
  61. package/dist/commands/types.d.ts +7 -0
  62. package/dist/commands/types.js +128 -0
  63. package/dist/commands/unpack.d.ts +11 -0
  64. package/dist/commands/unpack.js +166 -0
  65. package/dist/commands/validate.d.ts +13 -0
  66. package/dist/commands/validate.js +310 -0
  67. package/dist/commands/watch.d.ts +9 -0
  68. package/dist/commands/watch.js +267 -0
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.js +66 -0
  71. package/dist/formatters/diff-formatter.d.ts +5 -0
  72. package/dist/formatters/diff-formatter.js +43 -0
  73. package/dist/formatters/type-formatter.d.ts +22 -0
  74. package/dist/formatters/type-formatter.js +135 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.js +419 -0
  77. package/dist/local-codegen.d.ts +22 -0
  78. package/dist/local-codegen.js +762 -0
  79. package/dist/ui/badges.d.ts +16 -0
  80. package/dist/ui/badges.js +71 -0
  81. package/dist/ui/helpers.d.ts +13 -0
  82. package/dist/ui/helpers.js +85 -0
  83. package/package.json +23 -0
  84. package/src/api-client.ts +407 -0
  85. package/src/commands/annotate.ts +450 -0
  86. package/src/commands/audit.ts +103 -0
  87. package/src/commands/auto.ts +268 -0
  88. package/src/commands/capture.ts +257 -0
  89. package/src/commands/check.ts +437 -0
  90. package/src/commands/codegen.ts +128 -0
  91. package/src/commands/coverage.ts +170 -0
  92. package/src/commands/dashboard.ts +46 -0
  93. package/src/commands/dev.ts +323 -0
  94. package/src/commands/diff.ts +99 -0
  95. package/src/commands/docs.ts +392 -0
  96. package/src/commands/errors.ts +205 -0
  97. package/src/commands/export.ts +287 -0
  98. package/src/commands/functions.ts +81 -0
  99. package/src/commands/infer.ts +260 -0
  100. package/src/commands/init.ts +419 -0
  101. package/src/commands/mock.ts +220 -0
  102. package/src/commands/openapi.ts +53 -0
  103. package/src/commands/overview.ts +310 -0
  104. package/src/commands/pack.ts +139 -0
  105. package/src/commands/proxy.ts +314 -0
  106. package/src/commands/replay.ts +356 -0
  107. package/src/commands/run.ts +1190 -0
  108. package/src/commands/sample.ts +259 -0
  109. package/src/commands/search.ts +107 -0
  110. package/src/commands/stubs.ts +211 -0
  111. package/src/commands/tail.ts +94 -0
  112. package/src/commands/test-gen.ts +236 -0
  113. package/src/commands/trace.ts +440 -0
  114. package/src/commands/types.ts +161 -0
  115. package/src/commands/unpack.ts +179 -0
  116. package/src/commands/validate.ts +368 -0
  117. package/src/commands/watch.ts +277 -0
  118. package/src/config.ts +38 -0
  119. package/src/formatters/diff-formatter.ts +51 -0
  120. package/src/formatters/type-formatter.ts +161 -0
  121. package/src/index.ts +454 -0
  122. package/src/local-codegen.ts +859 -0
  123. package/src/ui/badges.ts +66 -0
  124. package/src/ui/helpers.ts +80 -0
  125. 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
+ }