trickle-cli 0.1.4 → 0.1.6

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/index.js CHANGED
@@ -39,6 +39,7 @@ const unpack_1 = require("./commands/unpack");
39
39
  const run_1 = require("./commands/run");
40
40
  const annotate_1 = require("./commands/annotate");
41
41
  const stubs_1 = require("./commands/stubs");
42
+ const vars_1 = require("./commands/vars");
42
43
  const program = new commander_1.Command();
43
44
  program
44
45
  .name("trickle")
@@ -387,6 +388,17 @@ program
387
388
  .action(async (dir, opts) => {
388
389
  await (0, stubs_1.stubsCommand)(dir, opts);
389
390
  });
391
+ // trickle vars
392
+ program
393
+ .command("vars")
394
+ .description("Show captured variable types and sample values from runtime observations")
395
+ .option("-f, --file <file>", "Filter by file path or module name")
396
+ .option("-m, --module <module>", "Filter by module name")
397
+ .option("--json", "Output raw JSON")
398
+ .option("--tensors", "Show only tensor/ndarray variables")
399
+ .action(async (opts) => {
400
+ await (0, vars_1.varsCommand)(opts);
401
+ });
390
402
  // trickle annotate <file>
391
403
  program
392
404
  .command("annotate <file>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for trickle runtime type observability",
5
5
  "bin": {
6
6
  "trickle": "dist/index.js"
@@ -46,11 +46,13 @@ function detectProject(dir: string, forcePython: boolean): ProjectInfo {
46
46
  if (fs.existsSync(tsPath)) {
47
47
  info.hasTsConfig = true;
48
48
  try {
49
- // Strip comments (simple approach: remove // and /* */ comments)
49
+ // Strip comments but preserve glob patterns like /**/*.ts
50
50
  const raw = fs.readFileSync(tsPath, "utf-8");
51
+ // Only strip // comments that are NOT inside strings
52
+ // And /* */ comments that are NOT inside strings (careful with globs)
51
53
  const cleaned = raw
52
54
  .replace(/\/\/.*$/gm, "")
53
- .replace(/\/\*[\s\S]*?\*\//g, "");
55
+ .replace(/("[^"]*")|\/\*[\s\S]*?\*\//g, (match, str) => str || "");
54
56
  info.tsConfig = JSON.parse(cleaned);
55
57
  } catch {
56
58
  // ignore parse errors
@@ -206,7 +208,7 @@ function writeInitialTypes(trickleDir: string, isPython: boolean): void {
206
208
  function updateTsConfig(dir: string, info: ProjectInfo): boolean {
207
209
  const tsConfigPath = path.join(dir, "tsconfig.json");
208
210
 
209
- if (!info.hasTsConfig || !info.tsConfig) {
211
+ if (!info.hasTsConfig) {
210
212
  // No tsconfig — create a minimal one that includes .trickle
211
213
  if (!info.isPython) {
212
214
  const newConfig = {
@@ -227,25 +229,52 @@ function updateTsConfig(dir: string, info: ProjectInfo): boolean {
227
229
  return false;
228
230
  }
229
231
 
230
- // Read the raw file to preserve formatting as much as possible
232
+ // Use text-based insertion to preserve formatting and glob patterns
233
+ // (JSON.parse + stringify corrupts /**/ globs by treating them as comments)
231
234
  const raw = fs.readFileSync(tsConfigPath, "utf-8");
232
- const config = info.tsConfig;
233
235
 
234
236
  // Check if .trickle is already included
235
- const include = config.include as string[] | undefined;
236
- if (include && include.some((p: string) => p === ".trickle" || p.startsWith(".trickle/"))) {
237
- return false; // Already configured
237
+ if (raw.includes(".trickle")) return false;
238
+
239
+ // Find the "include" array and insert ".trickle" at the end
240
+ const includeMatch = raw.match(/"include"\s*:\s*\[([^\]]*)\]/);
241
+ if (includeMatch) {
242
+ const bracket = includeMatch.index! + includeMatch[0].length - 1; // position of ]
243
+ const inside = includeMatch[1];
244
+ // Detect if it's single-line or multi-line
245
+ if (inside.includes("\n")) {
246
+ // Multi-line — add before the closing bracket with same indentation
247
+ const lastEntry = inside.match(/.*\S[^\n]*/g);
248
+ const indent = lastEntry ? lastEntry[lastEntry.length - 1].match(/^(\s*)/)?.[1] || " " : " ";
249
+ const updated = raw.slice(0, bracket) + `,\n${indent}".trickle"` + raw.slice(bracket);
250
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
251
+ } else {
252
+ // Single-line: ["src/**/*.ts", "tmp/**/*.ts"] → add ".trickle"
253
+ const updated = raw.slice(0, bracket) + ', ".trickle"' + raw.slice(bracket);
254
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
255
+ }
256
+ return true;
238
257
  }
239
258
 
240
- // Add .trickle to include array
241
- if (include) {
242
- include.push(".trickle");
243
- } else {
244
- (config as Record<string, unknown>).include = ["src", ".trickle"];
259
+ // No include array add one after compilerOptions closing brace
260
+ const compilerEnd = raw.match(/"compilerOptions"\s*:\s*\{/);
261
+ if (compilerEnd) {
262
+ // Find the closing brace of compilerOptions
263
+ const startBrace = raw.indexOf("{", compilerEnd.index! + compilerEnd[0].length - 1);
264
+ let depth = 1;
265
+ let pos = startBrace + 1;
266
+ while (pos < raw.length && depth > 0) {
267
+ if (raw[pos] === "{") depth++;
268
+ else if (raw[pos] === "}") depth--;
269
+ pos++;
270
+ }
271
+ // pos is now right after the closing brace of compilerOptions
272
+ const updated = raw.slice(0, pos) + `,\n "include": ["src", ".trickle"]` + raw.slice(pos);
273
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
274
+ return true;
245
275
  }
246
276
 
247
- fs.writeFileSync(tsConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
248
- return true;
277
+ return false;
249
278
  }
250
279
 
251
280
  function updatePackageJson(dir: string, info: ProjectInfo): { scriptsAdded: string[] } {
@@ -302,6 +331,100 @@ function updatePackageJson(dir: string, info: ProjectInfo): { scriptsAdded: stri
302
331
  return { scriptsAdded: added };
303
332
  }
304
333
 
334
+ function updateVitestConfig(dir: string): boolean {
335
+ // Look for vitest.config.ts, vitest.config.js, vitest.config.mts, vite.config.ts, vite.config.js
336
+ const candidates = [
337
+ "vitest.config.ts",
338
+ "vitest.config.js",
339
+ "vitest.config.mts",
340
+ "vite.config.ts",
341
+ "vite.config.js",
342
+ ];
343
+
344
+ let configFile: string | null = null;
345
+ for (const c of candidates) {
346
+ if (fs.existsSync(path.join(dir, c))) {
347
+ configFile = c;
348
+ break;
349
+ }
350
+ }
351
+
352
+ if (!configFile) return false;
353
+
354
+ const configPath = path.join(dir, configFile);
355
+ const content = fs.readFileSync(configPath, "utf-8");
356
+
357
+ // Already has tricklePlugin
358
+ if (content.includes("tricklePlugin")) return false;
359
+
360
+ // Find the import section end and plugins array
361
+ const lines = content.split("\n");
362
+ let lastImportLine = -1;
363
+ let pluginsLine = -1;
364
+ let pluginsArrayContent = "";
365
+
366
+ for (let i = 0; i < lines.length; i++) {
367
+ const line = lines[i];
368
+ if (/^\s*import\s/.test(line)) {
369
+ // Track multi-line imports
370
+ lastImportLine = i;
371
+ if (!line.includes(";") && !line.includes("from")) {
372
+ // Multi-line import — find the closing line
373
+ for (let j = i + 1; j < lines.length; j++) {
374
+ if (lines[j].includes("from")) {
375
+ lastImportLine = j;
376
+ break;
377
+ }
378
+ }
379
+ }
380
+ }
381
+ if (/plugins\s*:\s*\[/.test(line)) {
382
+ pluginsLine = i;
383
+ pluginsArrayContent = line;
384
+ }
385
+ }
386
+
387
+ if (lastImportLine === -1) return false;
388
+
389
+ // Add import after last import
390
+ const importLine = `import { tricklePlugin } from "trickle-observe/vite-plugin";`;
391
+ lines.splice(lastImportLine + 1, 0, importLine);
392
+
393
+ // Adjust pluginsLine index since we inserted a line
394
+ if (pluginsLine > lastImportLine) {
395
+ pluginsLine += 1;
396
+ }
397
+
398
+ // Add tricklePlugin() to plugins array
399
+ if (pluginsLine !== -1) {
400
+ const line = lines[pluginsLine];
401
+ // Check if plugins array is on one line: plugins: [something()],
402
+ if (/plugins\s*:\s*\[.*\]/.test(line)) {
403
+ // Insert tricklePlugin() before the closing bracket
404
+ lines[pluginsLine] = line.replace(/\]/, ", tricklePlugin()]");
405
+ } else {
406
+ // Multi-line plugins — add after the opening bracket line
407
+ const indent = line.match(/^(\s*)/)?.[1] || "";
408
+ const innerIndent = indent + " ";
409
+ lines.splice(pluginsLine + 1, 0, `${innerIndent}tricklePlugin(),`);
410
+ }
411
+ } else {
412
+ // No plugins array found — need to add one
413
+ // Find defineConfig({ and add plugins after it
414
+ for (let i = 0; i < lines.length; i++) {
415
+ if (/defineConfig\s*\(\s*\{/.test(lines[i])) {
416
+ const indent = lines[i].match(/^(\s*)/)?.[1] || "";
417
+ const innerIndent = indent + " ";
418
+ lines.splice(i + 1, 0, `${innerIndent}plugins: [tricklePlugin()],`);
419
+ break;
420
+ }
421
+ }
422
+ }
423
+
424
+ fs.writeFileSync(configPath, lines.join("\n"), "utf-8");
425
+ return true;
426
+ }
427
+
305
428
  function updateGitignore(dir: string): boolean {
306
429
  const giPath = path.join(dir, ".gitignore");
307
430
  let content = "";
@@ -379,7 +502,15 @@ export async function initCommand(opts: InitOptions): Promise<void> {
379
502
  }
380
503
  }
381
504
 
382
- // Step 5: Update package.json scripts
505
+ // Step 5: Update vitest/vite config with tricklePlugin
506
+ if (!info.isPython) {
507
+ const vitestUpdated = updateVitestConfig(dir);
508
+ if (vitestUpdated) {
509
+ console.log(` ${chalk.green("~")} Updated ${chalk.bold("vitest.config.ts")} — added tricklePlugin() for variable tracing`);
510
+ }
511
+ }
512
+
513
+ // Step 6: Update package.json scripts
383
514
  if (info.hasPackageJson) {
384
515
  const { scriptsAdded } = updatePackageJson(dir, info);
385
516
  if (scriptsAdded.length > 0) {
@@ -389,13 +520,13 @@ export async function initCommand(opts: InitOptions): Promise<void> {
389
520
  }
390
521
  }
391
522
 
392
- // Step 6: Update .gitignore
523
+ // Step 7: Update .gitignore
393
524
  const giUpdated = updateGitignore(dir);
394
525
  if (giUpdated) {
395
526
  console.log(` ${chalk.green("~")} Updated ${chalk.bold(".gitignore")} — added .trickle/`);
396
527
  }
397
528
 
398
- // Step 7: Print next steps
529
+ // Step 8: Print next steps
399
530
  console.log("");
400
531
  console.log(chalk.bold(" Next steps:"));
401
532
  console.log("");
@@ -412,6 +543,7 @@ export async function initCommand(opts: InitOptions): Promise<void> {
412
543
  console.log("");
413
544
  console.log(chalk.gray(" Other commands:"));
414
545
  console.log(chalk.gray(" trickle functions — list observed functions"));
546
+ console.log(chalk.gray(" trickle vars — list captured variable types + values"));
415
547
  console.log(chalk.gray(" trickle types <name> — see types + sample data"));
416
548
  console.log(chalk.gray(" trickle annotate src/ — add type annotations to source files"));
417
549
 
@@ -151,10 +151,17 @@ function autoDetectCommand(input: string): string {
151
151
  }
152
152
 
153
153
  function findTsRunner(): string {
154
+ const { execSync } = require("child_process");
155
+
156
+ // Add node_modules/.bin to PATH so local binaries are found
157
+ const binPath = path.join(process.cwd(), "node_modules", ".bin");
158
+ const currentPath = process.env.PATH || "";
159
+ const augmentedPath = currentPath.includes(binPath) ? currentPath : `${binPath}${path.delimiter}${currentPath}`;
160
+ const execOpts = { stdio: "ignore" as const, env: { ...process.env, PATH: augmentedPath } };
161
+
154
162
  // Check for tsx (fastest, most compatible)
155
163
  try {
156
- const { execSync } = require("child_process");
157
- execSync("tsx --version", { stdio: "ignore" });
164
+ execSync("tsx --version", execOpts);
158
165
  return "tsx";
159
166
  } catch {
160
167
  // not available
@@ -162,8 +169,7 @@ function findTsRunner(): string {
162
169
 
163
170
  // Check for ts-node
164
171
  try {
165
- const { execSync } = require("child_process");
166
- execSync("ts-node --version", { stdio: "ignore" });
172
+ execSync("ts-node --version", execOpts);
167
173
  return "ts-node";
168
174
  } catch {
169
175
  // not available
@@ -171,8 +177,7 @@ function findTsRunner(): string {
171
177
 
172
178
  // Check for bun (supports TS natively)
173
179
  try {
174
- const { execSync } = require("child_process");
175
- execSync("bun --version", { stdio: "ignore" });
180
+ execSync("bun --version", execOpts);
176
181
  return "bun";
177
182
  } catch {
178
183
  // not available
@@ -447,6 +452,17 @@ async function executeSingleRun(
447
452
  }
448
453
  }
449
454
 
455
+ // Show variable/tensor summary if variables.jsonl exists
456
+ const varsJsonlPath = path.join(localDir, "variables.jsonl");
457
+ if (fs.existsSync(varsJsonlPath)) {
458
+ try {
459
+ const { showVarsSummary } = await import("./vars");
460
+ showVarsSummary(varsJsonlPath);
461
+ } catch {
462
+ // vars module not available, skip
463
+ }
464
+ }
465
+
450
466
  console.log(chalk.gray(" " + "─".repeat(50)));
451
467
  console.log("");
452
468
 
@@ -538,6 +554,9 @@ function startLiveBackendTypes(sourceFile: string): () => void {
538
554
  const baseName = path.basename(sourceFile, ext);
539
555
  const sidecarName = isPython ? `${baseName}.pyi` : `${baseName}.d.ts`;
540
556
  const sidecarPath = path.join(dir, sidecarName);
557
+ // Also check .trickle/types/ where auto-codegen now writes
558
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
559
+ const trickleTypesPath = path.join(trickleDir, 'types', `${baseName}.d.ts`);
541
560
 
542
561
  const poll = async () => {
543
562
  if (stopped) return;
@@ -545,8 +564,10 @@ function startLiveBackendTypes(sourceFile: string): () => void {
545
564
  const { stubsCommand } = await import("./stubs");
546
565
  await stubsCommand(dir, { silent: true });
547
566
 
548
- if (fs.existsSync(sidecarPath)) {
549
- const content = fs.readFileSync(sidecarPath, "utf-8");
567
+ // Check both old sidecar path and new .trickle/types/ path
568
+ const effectivePath = fs.existsSync(trickleTypesPath) ? trickleTypesPath : sidecarPath;
569
+ if (fs.existsSync(effectivePath)) {
570
+ const content = fs.readFileSync(effectivePath, "utf-8");
550
571
  const funcCount = (content.match(/export declare function/g) || []).length;
551
572
 
552
573
  if (funcCount > lastFunctionCount) {
@@ -635,12 +656,16 @@ async function autoGenerateSidecar(filePath: string): Promise<void> {
635
656
  const { stubsCommand } = await import("./stubs");
636
657
  await stubsCommand(dir, { silent: true });
637
658
 
638
- // Check if the sidecar was generated
639
- if (fs.existsSync(sidecarPath)) {
640
- const stats = fs.statSync(sidecarPath);
659
+ // Check if types were generated (either sidecar or .trickle/types/)
660
+ const tDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
661
+ const tTypesPath = path.join(tDir, 'types', `${baseName}.d.ts`);
662
+ const effectiveSidecar = fs.existsSync(tTypesPath) ? tTypesPath : sidecarPath;
663
+ const displayName = fs.existsSync(tTypesPath) ? `${baseName}.d.ts` : sidecarName;
664
+ if (fs.existsSync(effectiveSidecar)) {
665
+ const stats = fs.statSync(effectiveSidecar);
641
666
  if (stats.size > 0) {
642
667
  console.log(
643
- chalk.green(`\n Types written to ${chalk.bold(sidecarName)}`),
668
+ chalk.green(`\n Types written to ${chalk.bold(displayName)}`),
644
669
  );
645
670
  }
646
671
  }
@@ -953,9 +978,17 @@ function injectObservation(
953
978
  const useEsm = isEsmFile(command) && observeEsmPath;
954
979
 
955
980
  if (useEsm) {
981
+ // Use both ESM hooks (for exported functions) and CJS hook (for Express auto-detection)
982
+ const modified = command.replace(
983
+ new RegExp(`^${runner}\\s`),
984
+ `${runner} -r ${observePath} --import ${observeEsmPath} `,
985
+ );
986
+ return { instrumentedCommand: modified, env };
987
+ } else if (runner === "tsx") {
988
+ // tsx always uses ESM internally — inject both CJS and ESM hooks
956
989
  const modified = command.replace(
957
990
  new RegExp(`^${runner}\\s`),
958
- `${runner} --import ${observeEsmPath} `,
991
+ `${runner} -r ${observePath} --import ${observeEsmPath} `,
959
992
  );
960
993
  return { instrumentedCommand: modified, env };
961
994
  } else {
@@ -1060,10 +1093,15 @@ function runProcess(
1060
1093
  env: Record<string, string>,
1061
1094
  ): Promise<number> {
1062
1095
  return new Promise((resolve) => {
1096
+ // Add node_modules/.bin to PATH so local binaries (tsx, ts-node, etc.) are found
1097
+ const binPath = path.join(process.cwd(), "node_modules", ".bin");
1098
+ const currentPath = process.env.PATH || "";
1099
+ const augmentedPath = currentPath.includes(binPath) ? currentPath : `${binPath}${path.delimiter}${currentPath}`;
1100
+
1063
1101
  const proc = spawn(command, [], {
1064
1102
  stdio: "inherit",
1065
1103
  shell: true,
1066
- env: { ...process.env, ...env },
1104
+ env: { ...process.env, ...env, PATH: augmentedPath },
1067
1105
  });
1068
1106
 
1069
1107
  proc.on("error", (err) => {