trickle-cli 0.1.3 → 0.1.5

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.
@@ -157,7 +157,7 @@ async function fetchFunctionSamples() {
157
157
  const samples = [];
158
158
  for (const fn of functions) {
159
159
  try {
160
- const data = await fetchJson(`/api/types/${encodeURIComponent(fn.function_name)}`);
160
+ const data = await fetchJson(`/api/types/${fn.id}`);
161
161
  if (data.snapshots && data.snapshots.length > 0) {
162
162
  const snap = data.snapshots[0];
163
163
  if (snap.sample_input || snap.sample_output) {
@@ -67,11 +67,13 @@ function detectProject(dir, forcePython) {
67
67
  if (fs.existsSync(tsPath)) {
68
68
  info.hasTsConfig = true;
69
69
  try {
70
- // Strip comments (simple approach: remove // and /* */ comments)
70
+ // Strip comments but preserve glob patterns like /**/*.ts
71
71
  const raw = fs.readFileSync(tsPath, "utf-8");
72
+ // Only strip // comments that are NOT inside strings
73
+ // And /* */ comments that are NOT inside strings (careful with globs)
72
74
  const cleaned = raw
73
75
  .replace(/\/\/.*$/gm, "")
74
- .replace(/\/\*[\s\S]*?\*\//g, "");
76
+ .replace(/("[^"]*")|\/\*[\s\S]*?\*\//g, (match, str) => str || "");
75
77
  info.tsConfig = JSON.parse(cleaned);
76
78
  }
77
79
  catch {
@@ -205,7 +207,7 @@ function writeInitialTypes(trickleDir, isPython) {
205
207
  }
206
208
  function updateTsConfig(dir, info) {
207
209
  const tsConfigPath = path.join(dir, "tsconfig.json");
208
- if (!info.hasTsConfig || !info.tsConfig) {
210
+ if (!info.hasTsConfig) {
209
211
  // No tsconfig — create a minimal one that includes .trickle
210
212
  if (!info.isPython) {
211
213
  const newConfig = {
@@ -225,23 +227,52 @@ function updateTsConfig(dir, info) {
225
227
  }
226
228
  return false;
227
229
  }
228
- // Read the raw file to preserve formatting as much as possible
230
+ // Use text-based insertion to preserve formatting and glob patterns
231
+ // (JSON.parse + stringify corrupts /**/ globs by treating them as comments)
229
232
  const raw = fs.readFileSync(tsConfigPath, "utf-8");
230
- const config = info.tsConfig;
231
233
  // Check if .trickle is already included
232
- const include = config.include;
233
- if (include && include.some((p) => p === ".trickle" || p.startsWith(".trickle/"))) {
234
- return false; // Already configured
235
- }
236
- // Add .trickle to include array
237
- if (include) {
238
- include.push(".trickle");
239
- }
240
- else {
241
- config.include = ["src", ".trickle"];
234
+ if (raw.includes(".trickle"))
235
+ return false;
236
+ // Find the "include" array and insert ".trickle" at the end
237
+ const includeMatch = raw.match(/"include"\s*:\s*\[([^\]]*)\]/);
238
+ if (includeMatch) {
239
+ const bracket = includeMatch.index + includeMatch[0].length - 1; // position of ]
240
+ const inside = includeMatch[1];
241
+ // Detect if it's single-line or multi-line
242
+ if (inside.includes("\n")) {
243
+ // Multi-line add before the closing bracket with same indentation
244
+ const lastEntry = inside.match(/.*\S[^\n]*/g);
245
+ const indent = lastEntry ? lastEntry[lastEntry.length - 1].match(/^(\s*)/)?.[1] || " " : " ";
246
+ const updated = raw.slice(0, bracket) + `,\n${indent}".trickle"` + raw.slice(bracket);
247
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
248
+ }
249
+ else {
250
+ // Single-line: ["src/**/*.ts", "tmp/**/*.ts"] → add ".trickle"
251
+ const updated = raw.slice(0, bracket) + ', ".trickle"' + raw.slice(bracket);
252
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
253
+ }
254
+ return true;
255
+ }
256
+ // No include array — add one after compilerOptions closing brace
257
+ const compilerEnd = raw.match(/"compilerOptions"\s*:\s*\{/);
258
+ if (compilerEnd) {
259
+ // Find the closing brace of compilerOptions
260
+ const startBrace = raw.indexOf("{", compilerEnd.index + compilerEnd[0].length - 1);
261
+ let depth = 1;
262
+ let pos = startBrace + 1;
263
+ while (pos < raw.length && depth > 0) {
264
+ if (raw[pos] === "{")
265
+ depth++;
266
+ else if (raw[pos] === "}")
267
+ depth--;
268
+ pos++;
269
+ }
270
+ // pos is now right after the closing brace of compilerOptions
271
+ const updated = raw.slice(0, pos) + `,\n "include": ["src", ".trickle"]` + raw.slice(pos);
272
+ fs.writeFileSync(tsConfigPath, updated, "utf-8");
273
+ return true;
242
274
  }
243
- fs.writeFileSync(tsConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
244
- return true;
275
+ return false;
245
276
  }
246
277
  function updatePackageJson(dir, info) {
247
278
  if (!info.hasPackageJson || !info.packageJson) {
@@ -291,6 +322,93 @@ function updatePackageJson(dir, info) {
291
322
  }
292
323
  return { scriptsAdded: added };
293
324
  }
325
+ function updateVitestConfig(dir) {
326
+ // Look for vitest.config.ts, vitest.config.js, vitest.config.mts, vite.config.ts, vite.config.js
327
+ const candidates = [
328
+ "vitest.config.ts",
329
+ "vitest.config.js",
330
+ "vitest.config.mts",
331
+ "vite.config.ts",
332
+ "vite.config.js",
333
+ ];
334
+ let configFile = null;
335
+ for (const c of candidates) {
336
+ if (fs.existsSync(path.join(dir, c))) {
337
+ configFile = c;
338
+ break;
339
+ }
340
+ }
341
+ if (!configFile)
342
+ return false;
343
+ const configPath = path.join(dir, configFile);
344
+ const content = fs.readFileSync(configPath, "utf-8");
345
+ // Already has tricklePlugin
346
+ if (content.includes("tricklePlugin"))
347
+ return false;
348
+ // Find the import section end and plugins array
349
+ const lines = content.split("\n");
350
+ let lastImportLine = -1;
351
+ let pluginsLine = -1;
352
+ let pluginsArrayContent = "";
353
+ for (let i = 0; i < lines.length; i++) {
354
+ const line = lines[i];
355
+ if (/^\s*import\s/.test(line)) {
356
+ // Track multi-line imports
357
+ lastImportLine = i;
358
+ if (!line.includes(";") && !line.includes("from")) {
359
+ // Multi-line import — find the closing line
360
+ for (let j = i + 1; j < lines.length; j++) {
361
+ if (lines[j].includes("from")) {
362
+ lastImportLine = j;
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ }
368
+ if (/plugins\s*:\s*\[/.test(line)) {
369
+ pluginsLine = i;
370
+ pluginsArrayContent = line;
371
+ }
372
+ }
373
+ if (lastImportLine === -1)
374
+ return false;
375
+ // Add import after last import
376
+ const importLine = `import { tricklePlugin } from "trickle-observe/vite-plugin";`;
377
+ lines.splice(lastImportLine + 1, 0, importLine);
378
+ // Adjust pluginsLine index since we inserted a line
379
+ if (pluginsLine > lastImportLine) {
380
+ pluginsLine += 1;
381
+ }
382
+ // Add tricklePlugin() to plugins array
383
+ if (pluginsLine !== -1) {
384
+ const line = lines[pluginsLine];
385
+ // Check if plugins array is on one line: plugins: [something()],
386
+ if (/plugins\s*:\s*\[.*\]/.test(line)) {
387
+ // Insert tricklePlugin() before the closing bracket
388
+ lines[pluginsLine] = line.replace(/\]/, ", tricklePlugin()]");
389
+ }
390
+ else {
391
+ // Multi-line plugins — add after the opening bracket line
392
+ const indent = line.match(/^(\s*)/)?.[1] || "";
393
+ const innerIndent = indent + " ";
394
+ lines.splice(pluginsLine + 1, 0, `${innerIndent}tricklePlugin(),`);
395
+ }
396
+ }
397
+ else {
398
+ // No plugins array found — need to add one
399
+ // Find defineConfig({ and add plugins after it
400
+ for (let i = 0; i < lines.length; i++) {
401
+ if (/defineConfig\s*\(\s*\{/.test(lines[i])) {
402
+ const indent = lines[i].match(/^(\s*)/)?.[1] || "";
403
+ const innerIndent = indent + " ";
404
+ lines.splice(i + 1, 0, `${innerIndent}plugins: [tricklePlugin()],`);
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ fs.writeFileSync(configPath, lines.join("\n"), "utf-8");
410
+ return true;
411
+ }
294
412
  function updateGitignore(dir) {
295
413
  const giPath = path.join(dir, ".gitignore");
296
414
  let content = "";
@@ -359,7 +477,14 @@ async function initCommand(opts) {
359
477
  console.log(` ${chalk_1.default.gray("-")} tsconfig.json already includes .trickle`);
360
478
  }
361
479
  }
362
- // Step 5: Update package.json scripts
480
+ // Step 5: Update vitest/vite config with tricklePlugin
481
+ if (!info.isPython) {
482
+ const vitestUpdated = updateVitestConfig(dir);
483
+ if (vitestUpdated) {
484
+ console.log(` ${chalk_1.default.green("~")} Updated ${chalk_1.default.bold("vitest.config.ts")} — added tricklePlugin() for variable tracing`);
485
+ }
486
+ }
487
+ // Step 6: Update package.json scripts
363
488
  if (info.hasPackageJson) {
364
489
  const { scriptsAdded } = updatePackageJson(dir, info);
365
490
  if (scriptsAdded.length > 0) {
@@ -368,12 +493,12 @@ async function initCommand(opts) {
368
493
  }
369
494
  }
370
495
  }
371
- // Step 6: Update .gitignore
496
+ // Step 7: Update .gitignore
372
497
  const giUpdated = updateGitignore(dir);
373
498
  if (giUpdated) {
374
499
  console.log(` ${chalk_1.default.green("~")} Updated ${chalk_1.default.bold(".gitignore")} — added .trickle/`);
375
500
  }
376
- // Step 7: Print next steps
501
+ // Step 8: Print next steps
377
502
  console.log("");
378
503
  console.log(chalk_1.default.bold(" Next steps:"));
379
504
  console.log("");
@@ -389,6 +514,7 @@ async function initCommand(opts) {
389
514
  console.log("");
390
515
  console.log(chalk_1.default.gray(" Other commands:"));
391
516
  console.log(chalk_1.default.gray(" trickle functions — list observed functions"));
517
+ console.log(chalk_1.default.gray(" trickle vars — list captured variable types + values"));
392
518
  console.log(chalk_1.default.gray(" trickle types <name> — see types + sample data"));
393
519
  console.log(chalk_1.default.gray(" trickle annotate src/ — add type annotations to source files"));
394
520
  console.log("");
@@ -150,10 +150,15 @@ function autoDetectCommand(input) {
150
150
  }
151
151
  }
152
152
  function findTsRunner() {
153
+ const { execSync } = require("child_process");
154
+ // Add node_modules/.bin to PATH so local binaries are found
155
+ const binPath = path.join(process.cwd(), "node_modules", ".bin");
156
+ const currentPath = process.env.PATH || "";
157
+ const augmentedPath = currentPath.includes(binPath) ? currentPath : `${binPath}${path.delimiter}${currentPath}`;
158
+ const execOpts = { stdio: "ignore", env: { ...process.env, PATH: augmentedPath } };
153
159
  // Check for tsx (fastest, most compatible)
154
160
  try {
155
- const { execSync } = require("child_process");
156
- execSync("tsx --version", { stdio: "ignore" });
161
+ execSync("tsx --version", execOpts);
157
162
  return "tsx";
158
163
  }
159
164
  catch {
@@ -161,8 +166,7 @@ function findTsRunner() {
161
166
  }
162
167
  // Check for ts-node
163
168
  try {
164
- const { execSync } = require("child_process");
165
- execSync("ts-node --version", { stdio: "ignore" });
169
+ execSync("ts-node --version", execOpts);
166
170
  return "ts-node";
167
171
  }
168
172
  catch {
@@ -170,8 +174,7 @@ function findTsRunner() {
170
174
  }
171
175
  // Check for bun (supports TS natively)
172
176
  try {
173
- const { execSync } = require("child_process");
174
- execSync("bun --version", { stdio: "ignore" });
177
+ execSync("bun --version", execOpts);
175
178
  return "bun";
176
179
  }
177
180
  catch {
@@ -388,6 +391,17 @@ async function executeSingleRun(instrumentedCommand, env, opts, singleFile, loca
388
391
  console.log(chalk_1.default.green(` Types written to ${chalk_1.default.bold(relPath)}`));
389
392
  }
390
393
  }
394
+ // Show variable/tensor summary if variables.jsonl exists
395
+ const varsJsonlPath = path.join(localDir, "variables.jsonl");
396
+ if (fs.existsSync(varsJsonlPath)) {
397
+ try {
398
+ const { showVarsSummary } = await Promise.resolve().then(() => __importStar(require("./vars")));
399
+ showVarsSummary(varsJsonlPath);
400
+ }
401
+ catch {
402
+ // vars module not available, skip
403
+ }
404
+ }
391
405
  console.log(chalk_1.default.gray(" " + "─".repeat(50)));
392
406
  console.log("");
393
407
  return exitCode;
@@ -471,14 +485,19 @@ function startLiveBackendTypes(sourceFile) {
471
485
  const baseName = path.basename(sourceFile, ext);
472
486
  const sidecarName = isPython ? `${baseName}.pyi` : `${baseName}.d.ts`;
473
487
  const sidecarPath = path.join(dir, sidecarName);
488
+ // Also check .trickle/types/ where auto-codegen now writes
489
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
490
+ const trickleTypesPath = path.join(trickleDir, 'types', `${baseName}.d.ts`);
474
491
  const poll = async () => {
475
492
  if (stopped)
476
493
  return;
477
494
  try {
478
495
  const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
479
496
  await stubsCommand(dir, { silent: true });
480
- if (fs.existsSync(sidecarPath)) {
481
- const content = fs.readFileSync(sidecarPath, "utf-8");
497
+ // Check both old sidecar path and new .trickle/types/ path
498
+ const effectivePath = fs.existsSync(trickleTypesPath) ? trickleTypesPath : sidecarPath;
499
+ if (fs.existsSync(effectivePath)) {
500
+ const content = fs.readFileSync(effectivePath, "utf-8");
482
501
  const funcCount = (content.match(/export declare function/g) || []).length;
483
502
  if (funcCount > lastFunctionCount) {
484
503
  const newCount = funcCount - lastFunctionCount;
@@ -552,11 +571,15 @@ async function autoGenerateSidecar(filePath) {
552
571
  // Use the stubs command to generate stubs for the file's directory
553
572
  const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
554
573
  await stubsCommand(dir, { silent: true });
555
- // Check if the sidecar was generated
556
- if (fs.existsSync(sidecarPath)) {
557
- const stats = fs.statSync(sidecarPath);
574
+ // Check if types were generated (either sidecar or .trickle/types/)
575
+ const tDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
576
+ const tTypesPath = path.join(tDir, 'types', `${baseName}.d.ts`);
577
+ const effectiveSidecar = fs.existsSync(tTypesPath) ? tTypesPath : sidecarPath;
578
+ const displayName = fs.existsSync(tTypesPath) ? `${baseName}.d.ts` : sidecarName;
579
+ if (fs.existsSync(effectiveSidecar)) {
580
+ const stats = fs.statSync(effectiveSidecar);
558
581
  if (stats.size > 0) {
559
- console.log(chalk_1.default.green(`\n Types written to ${chalk_1.default.bold(sidecarName)}`));
582
+ console.log(chalk_1.default.green(`\n Types written to ${chalk_1.default.bold(displayName)}`));
560
583
  }
561
584
  }
562
585
  }
@@ -828,7 +851,13 @@ function injectObservation(command, backendUrl, opts) {
828
851
  const runner = nodeMatch[1];
829
852
  const useEsm = isEsmFile(command) && observeEsmPath;
830
853
  if (useEsm) {
831
- const modified = command.replace(new RegExp(`^${runner}\\s`), `${runner} --import ${observeEsmPath} `);
854
+ // Use both ESM hooks (for exported functions) and CJS hook (for Express auto-detection)
855
+ const modified = command.replace(new RegExp(`^${runner}\\s`), `${runner} -r ${observePath} --import ${observeEsmPath} `);
856
+ return { instrumentedCommand: modified, env };
857
+ }
858
+ else if (runner === "tsx") {
859
+ // tsx always uses ESM internally — inject both CJS and ESM hooks
860
+ const modified = command.replace(new RegExp(`^${runner}\\s`), `${runner} -r ${observePath} --import ${observeEsmPath} `);
832
861
  return { instrumentedCommand: modified, env };
833
862
  }
834
863
  else {
@@ -907,10 +936,14 @@ function resolveObserveEsmPath() {
907
936
  }
908
937
  function runProcess(command, env) {
909
938
  return new Promise((resolve) => {
939
+ // Add node_modules/.bin to PATH so local binaries (tsx, ts-node, etc.) are found
940
+ const binPath = path.join(process.cwd(), "node_modules", ".bin");
941
+ const currentPath = process.env.PATH || "";
942
+ const augmentedPath = currentPath.includes(binPath) ? currentPath : `${binPath}${path.delimiter}${currentPath}`;
910
943
  const proc = (0, child_process_1.spawn)(command, [], {
911
944
  stdio: "inherit",
912
945
  shell: true,
913
- env: { ...process.env, ...env },
946
+ env: { ...process.env, ...env, PATH: augmentedPath },
914
947
  });
915
948
  proc.on("error", (err) => {
916
949
  console.error(chalk_1.default.red(`\n Failed to start: ${err.message}\n`));
@@ -0,0 +1,12 @@
1
+ export interface VarsOptions {
2
+ file?: string;
3
+ module?: string;
4
+ json?: boolean;
5
+ tensors?: boolean;
6
+ }
7
+ export declare function varsCommand(opts: VarsOptions): Promise<void>;
8
+ /**
9
+ * Show a brief post-run summary of traced variables (especially tensors).
10
+ * Called by `trickle run` after the user's command finishes.
11
+ */
12
+ export declare function showVarsSummary(varsFile: string): void;
@@ -0,0 +1,305 @@
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.varsCommand = varsCommand;
40
+ exports.showVarsSummary = showVarsSummary;
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const cli_table3_1 = __importDefault(require("cli-table3"));
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ function renderType(node, depth = 0) {
46
+ if (depth > 3)
47
+ return "...";
48
+ switch (node.kind) {
49
+ case "primitive":
50
+ return node.name || "unknown";
51
+ case "array":
52
+ return `${renderType(node.element, depth + 1)}[]`;
53
+ case "object": {
54
+ const props = node.properties || {};
55
+ const keys = Object.keys(props);
56
+ if (keys.length === 0)
57
+ return node.class_name || "{}";
58
+ // Special types
59
+ if (keys.length === 1 && keys[0].startsWith("__")) {
60
+ if (keys[0] === "__date")
61
+ return "Date";
62
+ if (keys[0] === "__regexp")
63
+ return "RegExp";
64
+ if (keys[0] === "__error")
65
+ return "Error";
66
+ if (keys[0] === "__buffer")
67
+ return "Buffer";
68
+ }
69
+ // Tensor / ndarray — show as "Tensor[1, 16, 32] float32"
70
+ if (node.class_name === "Tensor" || node.class_name === "ndarray") {
71
+ return renderTensorType(node.class_name, props);
72
+ }
73
+ // Named class — show as "ClassName(field=type, ...)"
74
+ if (node.class_name) {
75
+ if (keys.length > 4) {
76
+ const shown = keys.slice(0, 3).map((k) => `${k}=${renderType(props[k], depth + 1)}`);
77
+ return `${node.class_name}(${shown.join(", ")}, ...)`;
78
+ }
79
+ const entries = keys.map((k) => `${k}=${renderType(props[k], depth + 1)}`);
80
+ return `${node.class_name}(${entries.join(", ")})`;
81
+ }
82
+ if (keys.length > 4) {
83
+ const shown = keys.slice(0, 3).map((k) => `${k}: ${renderType(props[k], depth + 1)}`);
84
+ return `{ ${shown.join(", ")}, ... }`;
85
+ }
86
+ const entries = keys.map((k) => `${k}: ${renderType(props[k], depth + 1)}`);
87
+ return `{ ${entries.join(", ")} }`;
88
+ }
89
+ case "union":
90
+ return (node.members || []).map((m) => renderType(m, depth + 1)).join(" | ");
91
+ case "tuple":
92
+ return `[${(node.elements || []).map((e) => renderType(e, depth + 1)).join(", ")}]`;
93
+ case "promise":
94
+ return `Promise<${renderType(node.resolved, depth + 1)}>`;
95
+ case "function":
96
+ if (node.name && node.name !== "anonymous")
97
+ return `${node.name}(...)`;
98
+ return "Function";
99
+ case "map":
100
+ return `Map<${renderType(node.key, depth + 1)}, ${renderType(node.value, depth + 1)}>`;
101
+ case "set":
102
+ return `Set<${renderType(node.element, depth + 1)}>`;
103
+ default:
104
+ return "unknown";
105
+ }
106
+ }
107
+ /** Format a tensor type as "Tensor[1, 16, 32] float32 @cuda:0" */
108
+ function renderTensorType(className, properties) {
109
+ const parts = [className];
110
+ const shapeProp = properties["shape"];
111
+ if (shapeProp?.kind === "primitive" && shapeProp.name) {
112
+ parts[0] = `${className}${shapeProp.name}`;
113
+ }
114
+ const dtypeProp = properties["dtype"];
115
+ if (dtypeProp?.kind === "primitive" && dtypeProp.name) {
116
+ let dtype = dtypeProp.name;
117
+ dtype = dtype.replace("torch.", "").replace("numpy.", "");
118
+ parts.push(dtype);
119
+ }
120
+ const deviceProp = properties["device"];
121
+ if (deviceProp?.kind === "primitive" && deviceProp.name && deviceProp.name !== "cpu") {
122
+ parts.push(`@${deviceProp.name}`);
123
+ }
124
+ return parts.join(" ");
125
+ }
126
+ /** Check if a TypeNode represents a tensor type */
127
+ function isTensorType(node) {
128
+ return node.kind === "object" && (node.class_name === "Tensor" || node.class_name === "ndarray");
129
+ }
130
+ function renderSample(sample) {
131
+ if (sample === null)
132
+ return "null";
133
+ if (sample === undefined)
134
+ return "undefined";
135
+ const str = JSON.stringify(sample);
136
+ if (str.length > 60)
137
+ return str.substring(0, 57) + "...";
138
+ return str;
139
+ }
140
+ async function varsCommand(opts) {
141
+ const trickleDir = path.join(process.cwd(), ".trickle");
142
+ const varsFile = path.join(trickleDir, "variables.jsonl");
143
+ if (!fs.existsSync(varsFile)) {
144
+ console.log(chalk_1.default.yellow("\n No variable observations found."));
145
+ console.log(chalk_1.default.gray(" Run your code with: trickle run <command>\n"));
146
+ return;
147
+ }
148
+ const content = fs.readFileSync(varsFile, "utf-8");
149
+ const lines = content.trim().split("\n").filter(Boolean);
150
+ const observations = [];
151
+ for (const line of lines) {
152
+ try {
153
+ const obs = JSON.parse(line);
154
+ if (obs.kind === "variable")
155
+ observations.push(obs);
156
+ }
157
+ catch {
158
+ // skip malformed lines
159
+ }
160
+ }
161
+ if (observations.length === 0) {
162
+ console.log(chalk_1.default.yellow("\n No variable observations found.\n"));
163
+ return;
164
+ }
165
+ // Filter
166
+ let filtered = observations;
167
+ if (opts.file) {
168
+ const fileFilter = opts.file;
169
+ filtered = filtered.filter((o) => o.file.includes(fileFilter) || o.module.includes(fileFilter));
170
+ }
171
+ if (opts.module) {
172
+ filtered = filtered.filter((o) => o.module === opts.module);
173
+ }
174
+ if (opts.tensors) {
175
+ filtered = filtered.filter((o) => isTensorType(o.type));
176
+ }
177
+ if (filtered.length === 0) {
178
+ console.log(chalk_1.default.yellow("\n No matching variables found.\n"));
179
+ return;
180
+ }
181
+ if (opts.json) {
182
+ console.log(JSON.stringify(filtered, null, 2));
183
+ return;
184
+ }
185
+ // Group by file
186
+ const byFile = new Map();
187
+ for (const obs of filtered) {
188
+ const key = obs.file;
189
+ if (!byFile.has(key))
190
+ byFile.set(key, []);
191
+ byFile.get(key).push(obs);
192
+ }
193
+ // Sort each file's vars by line number
194
+ for (const [, vars] of byFile) {
195
+ vars.sort((a, b) => a.line - b.line);
196
+ }
197
+ console.log("");
198
+ for (const [file, vars] of byFile) {
199
+ // Show relative path
200
+ const relPath = path.relative(process.cwd(), file);
201
+ console.log(chalk_1.default.cyan.bold(` ${relPath}`));
202
+ console.log("");
203
+ const table = new cli_table3_1.default({
204
+ head: [
205
+ chalk_1.default.gray("Line"),
206
+ chalk_1.default.gray("Variable"),
207
+ chalk_1.default.gray("Type"),
208
+ chalk_1.default.gray("Sample Value"),
209
+ ],
210
+ style: { head: [], border: ["gray"] },
211
+ colWidths: [8, 20, 35, 40],
212
+ wordWrap: true,
213
+ chars: {
214
+ top: "─",
215
+ "top-mid": "┬",
216
+ "top-left": "┌",
217
+ "top-right": "┐",
218
+ bottom: "─",
219
+ "bottom-mid": "┴",
220
+ "bottom-left": "└",
221
+ "bottom-right": "┘",
222
+ left: "│",
223
+ "left-mid": "├",
224
+ mid: "─",
225
+ "mid-mid": "┼",
226
+ right: "│",
227
+ "right-mid": "┤",
228
+ middle: "│",
229
+ },
230
+ });
231
+ for (const v of vars) {
232
+ table.push([
233
+ chalk_1.default.gray(String(v.line)),
234
+ chalk_1.default.white.bold(v.varName),
235
+ chalk_1.default.green(renderType(v.type)),
236
+ chalk_1.default.gray(renderSample(v.sample)),
237
+ ]);
238
+ }
239
+ console.log(table.toString());
240
+ console.log("");
241
+ }
242
+ // Summary
243
+ const totalVars = filtered.length;
244
+ const totalFiles = byFile.size;
245
+ const tensorCount = filtered.filter((o) => isTensorType(o.type)).length;
246
+ const summaryParts = [`${totalVars} variable(s) across ${totalFiles} file(s)`];
247
+ if (tensorCount > 0) {
248
+ summaryParts.push(`${tensorCount} tensor(s)`);
249
+ }
250
+ console.log(chalk_1.default.gray(` ${summaryParts.join(", ")}\n`));
251
+ }
252
+ /**
253
+ * Show a brief post-run summary of traced variables (especially tensors).
254
+ * Called by `trickle run` after the user's command finishes.
255
+ */
256
+ function showVarsSummary(varsFile) {
257
+ if (!fs.existsSync(varsFile))
258
+ return;
259
+ const content = fs.readFileSync(varsFile, "utf-8");
260
+ const lines = content.trim().split("\n").filter(Boolean);
261
+ const observations = [];
262
+ for (const line of lines) {
263
+ try {
264
+ const obs = JSON.parse(line);
265
+ if (obs.kind === "variable")
266
+ observations.push(obs);
267
+ }
268
+ catch {
269
+ // skip
270
+ }
271
+ }
272
+ if (observations.length === 0)
273
+ return;
274
+ const tensorObs = observations.filter((o) => isTensorType(o.type));
275
+ const byFile = new Map();
276
+ for (const obs of tensorObs) {
277
+ if (!byFile.has(obs.file))
278
+ byFile.set(obs.file, []);
279
+ byFile.get(obs.file).push(obs);
280
+ }
281
+ // Sort by line within each file
282
+ for (const [, vars] of byFile) {
283
+ vars.sort((a, b) => a.line - b.line);
284
+ }
285
+ console.log(` Variables traced: ${chalk_1.default.bold(String(observations.length))}`);
286
+ if (tensorObs.length > 0) {
287
+ console.log(` Tensor variables: ${chalk_1.default.bold(String(tensorObs.length))}`);
288
+ console.log("");
289
+ // Show up to 15 most interesting tensor variables
290
+ const shown = [];
291
+ for (const [, vars] of byFile) {
292
+ shown.push(...vars);
293
+ }
294
+ const toShow = shown.slice(0, 15);
295
+ for (const obs of toShow) {
296
+ const relPath = path.relative(process.cwd(), obs.file);
297
+ const typeStr = renderType(obs.type);
298
+ console.log(` ${chalk_1.default.gray(`${relPath}:${obs.line}`)} ${chalk_1.default.white.bold(obs.varName)} ${chalk_1.default.green(typeStr)}`);
299
+ }
300
+ if (shown.length > 15) {
301
+ console.log(chalk_1.default.gray(` ... and ${shown.length - 15} more`));
302
+ }
303
+ }
304
+ console.log(chalk_1.default.gray(` Run ${chalk_1.default.white("trickle vars")} for full details`));
305
+ }