trickle-cli 0.1.4 → 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.
@@ -0,0 +1,321 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+
6
+ interface TypeNode {
7
+ kind: string;
8
+ name?: string;
9
+ class_name?: string;
10
+ element?: TypeNode;
11
+ elements?: TypeNode[];
12
+ properties?: Record<string, TypeNode>;
13
+ members?: TypeNode[];
14
+ params?: TypeNode[];
15
+ returnType?: TypeNode;
16
+ resolved?: TypeNode;
17
+ key?: TypeNode;
18
+ value?: TypeNode;
19
+ }
20
+
21
+ interface VariableObservation {
22
+ kind: "variable";
23
+ varName: string;
24
+ line: number;
25
+ module: string;
26
+ file: string;
27
+ type: TypeNode;
28
+ typeHash: string;
29
+ sample: unknown;
30
+ }
31
+
32
+ export interface VarsOptions {
33
+ file?: string;
34
+ module?: string;
35
+ json?: boolean;
36
+ tensors?: boolean;
37
+ }
38
+
39
+ function renderType(node: TypeNode, depth: number = 0): string {
40
+ if (depth > 3) return "...";
41
+ switch (node.kind) {
42
+ case "primitive":
43
+ return node.name || "unknown";
44
+ case "array":
45
+ return `${renderType(node.element!, depth + 1)}[]`;
46
+ case "object": {
47
+ const props = node.properties || {};
48
+ const keys = Object.keys(props);
49
+ if (keys.length === 0) return node.class_name || "{}";
50
+
51
+ // Special types
52
+ if (keys.length === 1 && keys[0].startsWith("__")) {
53
+ if (keys[0] === "__date") return "Date";
54
+ if (keys[0] === "__regexp") return "RegExp";
55
+ if (keys[0] === "__error") return "Error";
56
+ if (keys[0] === "__buffer") return "Buffer";
57
+ }
58
+
59
+ // Tensor / ndarray — show as "Tensor[1, 16, 32] float32"
60
+ if (node.class_name === "Tensor" || node.class_name === "ndarray") {
61
+ return renderTensorType(node.class_name, props);
62
+ }
63
+
64
+ // Named class — show as "ClassName(field=type, ...)"
65
+ if (node.class_name) {
66
+ if (keys.length > 4) {
67
+ const shown = keys.slice(0, 3).map((k) => `${k}=${renderType(props[k], depth + 1)}`);
68
+ return `${node.class_name}(${shown.join(", ")}, ...)`;
69
+ }
70
+ const entries = keys.map((k) => `${k}=${renderType(props[k], depth + 1)}`);
71
+ return `${node.class_name}(${entries.join(", ")})`;
72
+ }
73
+
74
+ if (keys.length > 4) {
75
+ const shown = keys.slice(0, 3).map((k) => `${k}: ${renderType(props[k], depth + 1)}`);
76
+ return `{ ${shown.join(", ")}, ... }`;
77
+ }
78
+ const entries = keys.map((k) => `${k}: ${renderType(props[k], depth + 1)}`);
79
+ return `{ ${entries.join(", ")} }`;
80
+ }
81
+ case "union":
82
+ return (node.members || []).map((m) => renderType(m, depth + 1)).join(" | ");
83
+ case "tuple":
84
+ return `[${(node.elements || []).map((e) => renderType(e, depth + 1)).join(", ")}]`;
85
+ case "promise":
86
+ return `Promise<${renderType(node.resolved!, depth + 1)}>`;
87
+ case "function":
88
+ if (node.name && node.name !== "anonymous") return `${node.name}(...)`;
89
+ return "Function";
90
+ case "map":
91
+ return `Map<${renderType(node.key!, depth + 1)}, ${renderType(node.value!, depth + 1)}>`;
92
+ case "set":
93
+ return `Set<${renderType(node.element!, depth + 1)}>`;
94
+ default:
95
+ return "unknown";
96
+ }
97
+ }
98
+
99
+ /** Format a tensor type as "Tensor[1, 16, 32] float32 @cuda:0" */
100
+ function renderTensorType(className: string, properties: Record<string, TypeNode>): string {
101
+ const parts: string[] = [className];
102
+
103
+ const shapeProp = properties["shape"];
104
+ if (shapeProp?.kind === "primitive" && shapeProp.name) {
105
+ parts[0] = `${className}${shapeProp.name}`;
106
+ }
107
+
108
+ const dtypeProp = properties["dtype"];
109
+ if (dtypeProp?.kind === "primitive" && dtypeProp.name) {
110
+ let dtype = dtypeProp.name;
111
+ dtype = dtype.replace("torch.", "").replace("numpy.", "");
112
+ parts.push(dtype);
113
+ }
114
+
115
+ const deviceProp = properties["device"];
116
+ if (deviceProp?.kind === "primitive" && deviceProp.name && deviceProp.name !== "cpu") {
117
+ parts.push(`@${deviceProp.name}`);
118
+ }
119
+
120
+ return parts.join(" ");
121
+ }
122
+
123
+ /** Check if a TypeNode represents a tensor type */
124
+ function isTensorType(node: TypeNode): boolean {
125
+ return node.kind === "object" && (node.class_name === "Tensor" || node.class_name === "ndarray");
126
+ }
127
+
128
+ function renderSample(sample: unknown): string {
129
+ if (sample === null) return "null";
130
+ if (sample === undefined) return "undefined";
131
+ const str = JSON.stringify(sample);
132
+ if (str.length > 60) return str.substring(0, 57) + "...";
133
+ return str;
134
+ }
135
+
136
+ export async function varsCommand(opts: VarsOptions): Promise<void> {
137
+ const trickleDir = path.join(process.cwd(), ".trickle");
138
+ const varsFile = path.join(trickleDir, "variables.jsonl");
139
+
140
+ if (!fs.existsSync(varsFile)) {
141
+ console.log(chalk.yellow("\n No variable observations found."));
142
+ console.log(chalk.gray(" Run your code with: trickle run <command>\n"));
143
+ return;
144
+ }
145
+
146
+ const content = fs.readFileSync(varsFile, "utf-8");
147
+ const lines = content.trim().split("\n").filter(Boolean);
148
+ const observations: VariableObservation[] = [];
149
+
150
+ for (const line of lines) {
151
+ try {
152
+ const obs = JSON.parse(line);
153
+ if (obs.kind === "variable") observations.push(obs);
154
+ } catch {
155
+ // skip malformed lines
156
+ }
157
+ }
158
+
159
+ if (observations.length === 0) {
160
+ console.log(chalk.yellow("\n No variable observations found.\n"));
161
+ return;
162
+ }
163
+
164
+ // Filter
165
+ let filtered = observations;
166
+ if (opts.file) {
167
+ const fileFilter = opts.file;
168
+ filtered = filtered.filter(
169
+ (o) => o.file.includes(fileFilter) || o.module.includes(fileFilter)
170
+ );
171
+ }
172
+ if (opts.module) {
173
+ filtered = filtered.filter((o) => o.module === opts.module);
174
+ }
175
+ if (opts.tensors) {
176
+ filtered = filtered.filter((o) => isTensorType(o.type));
177
+ }
178
+
179
+ if (filtered.length === 0) {
180
+ console.log(chalk.yellow("\n No matching variables found.\n"));
181
+ return;
182
+ }
183
+
184
+ if (opts.json) {
185
+ console.log(JSON.stringify(filtered, null, 2));
186
+ return;
187
+ }
188
+
189
+ // Group by file
190
+ const byFile = new Map<string, VariableObservation[]>();
191
+ for (const obs of filtered) {
192
+ const key = obs.file;
193
+ if (!byFile.has(key)) byFile.set(key, []);
194
+ byFile.get(key)!.push(obs);
195
+ }
196
+
197
+ // Sort each file's vars by line number
198
+ for (const [, vars] of byFile) {
199
+ vars.sort((a, b) => a.line - b.line);
200
+ }
201
+
202
+ console.log("");
203
+
204
+ for (const [file, vars] of byFile) {
205
+ // Show relative path
206
+ const relPath = path.relative(process.cwd(), file);
207
+ console.log(chalk.cyan.bold(` ${relPath}`));
208
+ console.log("");
209
+
210
+ const table = new Table({
211
+ head: [
212
+ chalk.gray("Line"),
213
+ chalk.gray("Variable"),
214
+ chalk.gray("Type"),
215
+ chalk.gray("Sample Value"),
216
+ ],
217
+ style: { head: [], border: ["gray"] },
218
+ colWidths: [8, 20, 35, 40],
219
+ wordWrap: true,
220
+ chars: {
221
+ top: "─",
222
+ "top-mid": "┬",
223
+ "top-left": "┌",
224
+ "top-right": "┐",
225
+ bottom: "─",
226
+ "bottom-mid": "┴",
227
+ "bottom-left": "└",
228
+ "bottom-right": "┘",
229
+ left: "│",
230
+ "left-mid": "├",
231
+ mid: "─",
232
+ "mid-mid": "┼",
233
+ right: "│",
234
+ "right-mid": "┤",
235
+ middle: "│",
236
+ },
237
+ });
238
+
239
+ for (const v of vars) {
240
+ table.push([
241
+ chalk.gray(String(v.line)),
242
+ chalk.white.bold(v.varName),
243
+ chalk.green(renderType(v.type)),
244
+ chalk.gray(renderSample(v.sample)),
245
+ ]);
246
+ }
247
+
248
+ console.log(table.toString());
249
+ console.log("");
250
+ }
251
+
252
+ // Summary
253
+ const totalVars = filtered.length;
254
+ const totalFiles = byFile.size;
255
+ const tensorCount = filtered.filter((o) => isTensorType(o.type)).length;
256
+ const summaryParts = [`${totalVars} variable(s) across ${totalFiles} file(s)`];
257
+ if (tensorCount > 0) {
258
+ summaryParts.push(`${tensorCount} tensor(s)`);
259
+ }
260
+ console.log(chalk.gray(` ${summaryParts.join(", ")}\n`));
261
+ }
262
+
263
+ /**
264
+ * Show a brief post-run summary of traced variables (especially tensors).
265
+ * Called by `trickle run` after the user's command finishes.
266
+ */
267
+ export function showVarsSummary(varsFile: string): void {
268
+ if (!fs.existsSync(varsFile)) return;
269
+
270
+ const content = fs.readFileSync(varsFile, "utf-8");
271
+ const lines = content.trim().split("\n").filter(Boolean);
272
+ const observations: VariableObservation[] = [];
273
+
274
+ for (const line of lines) {
275
+ try {
276
+ const obs = JSON.parse(line);
277
+ if (obs.kind === "variable") observations.push(obs);
278
+ } catch {
279
+ // skip
280
+ }
281
+ }
282
+
283
+ if (observations.length === 0) return;
284
+
285
+ const tensorObs = observations.filter((o) => isTensorType(o.type));
286
+ const byFile = new Map<string, VariableObservation[]>();
287
+ for (const obs of tensorObs) {
288
+ if (!byFile.has(obs.file)) byFile.set(obs.file, []);
289
+ byFile.get(obs.file)!.push(obs);
290
+ }
291
+
292
+ // Sort by line within each file
293
+ for (const [, vars] of byFile) {
294
+ vars.sort((a, b) => a.line - b.line);
295
+ }
296
+
297
+ console.log(` Variables traced: ${chalk.bold(String(observations.length))}`);
298
+ if (tensorObs.length > 0) {
299
+ console.log(` Tensor variables: ${chalk.bold(String(tensorObs.length))}`);
300
+ console.log("");
301
+
302
+ // Show up to 15 most interesting tensor variables
303
+ const shown: VariableObservation[] = [];
304
+ for (const [, vars] of byFile) {
305
+ shown.push(...vars);
306
+ }
307
+ const toShow = shown.slice(0, 15);
308
+
309
+ for (const obs of toShow) {
310
+ const relPath = path.relative(process.cwd(), obs.file);
311
+ const typeStr = renderType(obs.type);
312
+ console.log(
313
+ ` ${chalk.gray(`${relPath}:${obs.line}`)} ${chalk.white.bold(obs.varName)} ${chalk.green(typeStr)}`
314
+ );
315
+ }
316
+ if (shown.length > 15) {
317
+ console.log(chalk.gray(` ... and ${shown.length - 15} more`));
318
+ }
319
+ }
320
+ console.log(chalk.gray(` Run ${chalk.white("trickle vars")} for full details`));
321
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ import { unpackCommand } from "./commands/unpack";
35
35
  import { runCommand } from "./commands/run";
36
36
  import { annotateCommand } from "./commands/annotate";
37
37
  import { stubsCommand } from "./commands/stubs";
38
+ import { varsCommand } from "./commands/vars";
38
39
 
39
40
  const program = new Command();
40
41
 
@@ -418,6 +419,18 @@ program
418
419
  await stubsCommand(dir, opts);
419
420
  });
420
421
 
422
+ // trickle vars
423
+ program
424
+ .command("vars")
425
+ .description("Show captured variable types and sample values from runtime observations")
426
+ .option("-f, --file <file>", "Filter by file path or module name")
427
+ .option("-m, --module <module>", "Filter by module name")
428
+ .option("--json", "Output raw JSON")
429
+ .option("--tensors", "Show only tensor/ndarray variables")
430
+ .action(async (opts) => {
431
+ await varsCommand(opts);
432
+ });
433
+
421
434
  // trickle annotate <file>
422
435
  program
423
436
  .command("annotate <file>")