trickle-cli 0.1.1 → 0.1.3

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.
@@ -137,6 +137,12 @@ export interface StubsResponse {
137
137
  export declare function fetchStubs(opts?: {
138
138
  env?: string;
139
139
  }): Promise<StubsResponse>;
140
+ export interface FunctionSample {
141
+ functionName: string;
142
+ sampleInput: unknown;
143
+ sampleOutput: unknown;
144
+ }
145
+ export declare function fetchFunctionSamples(): Promise<FunctionSample[]>;
140
146
  export interface MockRoute {
141
147
  method: string;
142
148
  path: string;
@@ -45,6 +45,7 @@ exports.getTypeDiff = getTypeDiff;
45
45
  exports.fetchCodegen = fetchCodegen;
46
46
  exports.fetchAnnotations = fetchAnnotations;
47
47
  exports.fetchStubs = fetchStubs;
48
+ exports.fetchFunctionSamples = fetchFunctionSamples;
48
49
  exports.fetchMockConfig = fetchMockConfig;
49
50
  exports.fetchSnapshot = fetchSnapshot;
50
51
  exports.fetchOpenApiSpec = fetchOpenApiSpec;
@@ -151,6 +152,29 @@ async function fetchStubs(opts) {
151
152
  format: "stubs",
152
153
  });
153
154
  }
155
+ async function fetchFunctionSamples() {
156
+ const { functions } = await listFunctions({ limit: 500 });
157
+ const samples = [];
158
+ for (const fn of functions) {
159
+ try {
160
+ const data = await fetchJson(`/api/types/${encodeURIComponent(fn.function_name)}`);
161
+ if (data.snapshots && data.snapshots.length > 0) {
162
+ const snap = data.snapshots[0];
163
+ if (snap.sample_input || snap.sample_output) {
164
+ samples.push({
165
+ functionName: fn.function_name,
166
+ sampleInput: snap.sample_input,
167
+ sampleOutput: snap.sample_output,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ catch {
173
+ // Skip functions with no data
174
+ }
175
+ }
176
+ return samples;
177
+ }
154
178
  async function fetchMockConfig() {
155
179
  return fetchJson("/api/mock-config");
156
180
  }
@@ -169,8 +169,9 @@ function annotateJSDoc(source, annotations) {
169
169
  // ── TypeScript annotation (inline types) ──
170
170
  /**
171
171
  * Annotate a TypeScript file with inline type annotations on function signatures.
172
+ * When samples are provided, adds JSDoc @example comments even for already-typed functions.
172
173
  */
173
- function annotateTS(source, annotations) {
174
+ function annotateTS(source, annotations, samples) {
174
175
  const lines = source.split("\n");
175
176
  const result = [];
176
177
  for (let i = 0; i < lines.length; i++) {
@@ -181,15 +182,63 @@ function annotateTS(source, annotations) {
181
182
  continue;
182
183
  }
183
184
  const annotation = annotations[info.fnName];
184
- if (!annotation) {
185
+ const sample = samples?.[info.fnName];
186
+ if (!annotation && !sample) {
185
187
  result.push(line);
186
188
  continue;
187
189
  }
188
- // Skip if already typed (has a colon in param list or return annotation)
190
+ // Add JSDoc with @example from sample data (even for already-typed functions)
191
+ if (sample) {
192
+ // Check if there's already a trickle JSDoc comment above
193
+ const prevIdx = result.length - 1;
194
+ let alreadyHasTrickleDoc = false;
195
+ if (prevIdx >= 0) {
196
+ // Look back for a JSDoc block containing @example and "trickle"
197
+ let j = prevIdx;
198
+ while (j >= 0 && !result[j].trim().startsWith("/**"))
199
+ j--;
200
+ if (j >= 0 && result[prevIdx].trim().endsWith("*/")) {
201
+ const block = result.slice(j, prevIdx + 1).join("\n");
202
+ if (block.includes("@example") && block.includes("trickle")) {
203
+ alreadyHasTrickleDoc = true;
204
+ }
205
+ }
206
+ }
207
+ if (!alreadyHasTrickleDoc) {
208
+ const indent = line.match(/^(\s*)/)?.[1] || "";
209
+ const jsdocLines = [];
210
+ jsdocLines.push(`${indent}/** @trickle`);
211
+ if (sample.sampleInput !== undefined && sample.sampleInput !== null) {
212
+ jsdocLines.push(`${indent} * @example`);
213
+ jsdocLines.push(`${indent} * // Sample input:`);
214
+ const inputStr = JSON.stringify(sample.sampleInput, null, 2);
215
+ for (const l of inputStr.split('\n')) {
216
+ jsdocLines.push(`${indent} * ${l}`);
217
+ }
218
+ }
219
+ if (sample.sampleOutput !== undefined && sample.sampleOutput !== null) {
220
+ if (sample.sampleInput === undefined || sample.sampleInput === null) {
221
+ jsdocLines.push(`${indent} * @example`);
222
+ }
223
+ jsdocLines.push(`${indent} * // Sample output:`);
224
+ const outputStr = JSON.stringify(sample.sampleOutput, null, 2);
225
+ for (const l of outputStr.split('\n')) {
226
+ jsdocLines.push(`${indent} * ${l}`);
227
+ }
228
+ }
229
+ jsdocLines.push(`${indent} */`);
230
+ result.push(...jsdocLines);
231
+ }
232
+ }
233
+ // Skip inline type modification if already typed
189
234
  if (info.rawParams.includes(":") || line.includes("): ")) {
190
235
  result.push(line);
191
236
  continue;
192
237
  }
238
+ if (!annotation) {
239
+ result.push(line);
240
+ continue;
241
+ }
193
242
  // Build typed param list
194
243
  const paramNames = getParamNames(info.rawParams);
195
244
  const typedParams = paramNames.map((pName) => {
@@ -315,7 +364,18 @@ async function annotateCommand(file, opts) {
315
364
  env: opts.env,
316
365
  language: apiLanguage,
317
366
  });
318
- if (!annotations || Object.keys(annotations).length === 0) {
367
+ // Fetch sample data for @example JSDoc
368
+ let samplesMap = {};
369
+ try {
370
+ const samples = await (0, api_client_1.fetchFunctionSamples)();
371
+ for (const s of samples) {
372
+ samplesMap[s.functionName] = s;
373
+ }
374
+ }
375
+ catch {
376
+ // Sample data is optional
377
+ }
378
+ if ((!annotations || Object.keys(annotations).length === 0) && Object.keys(samplesMap).length === 0) {
319
379
  console.log(chalk_1.default.yellow("\n No observed types found. Run your code with trickle first.\n"));
320
380
  return;
321
381
  }
@@ -332,8 +392,8 @@ async function annotateCommand(file, opts) {
332
392
  mode = "JSDoc comments";
333
393
  }
334
394
  else {
335
- annotated = annotateTS(source, annotations);
336
- mode = "TypeScript annotations";
395
+ annotated = annotateTS(source, annotations, samplesMap);
396
+ mode = "TypeScript annotations + sample data";
337
397
  }
338
398
  // Count changes
339
399
  const originalLines = source.split("\n");
@@ -309,7 +309,10 @@ async function executeSingleRun(instrumentedCommand, env, opts, singleFile, loca
309
309
  }
310
310
  // Start live type generation for backend mode
311
311
  let liveStop = null;
312
- if (singleFile && !opts.stubs) {
312
+ if (opts.stubs) {
313
+ liveStop = startLiveStubsGeneration(opts.stubs);
314
+ }
315
+ else if (singleFile) {
313
316
  liveStop = startLiveBackendTypes(singleFile);
314
317
  }
315
318
  // Run the instrumented command
@@ -499,6 +502,43 @@ function startLiveBackendTypes(sourceFile) {
499
502
  clearInterval(interval);
500
503
  };
501
504
  }
505
+ // ── Live stubs generation during run ──
506
+ function startLiveStubsGeneration(stubsDir) {
507
+ let lastTotal = 0;
508
+ let stopped = false;
509
+ const poll = async () => {
510
+ if (stopped)
511
+ return;
512
+ try {
513
+ const { stubsCommand } = await Promise.resolve().then(() => __importStar(require("./stubs")));
514
+ const result = await stubsCommand(stubsDir, { silent: true });
515
+ // Count .d.ts files in the stubs dir to track progress
516
+ const files = fs.readdirSync(stubsDir).filter(f => f.endsWith('.d.ts'));
517
+ let funcCount = 0;
518
+ for (const f of files) {
519
+ const content = fs.readFileSync(path.join(stubsDir, f), 'utf-8');
520
+ funcCount += (content.match(/export declare function/g) || []).length;
521
+ }
522
+ if (funcCount > lastTotal) {
523
+ const newCount = funcCount - lastTotal;
524
+ const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
525
+ console.log(chalk_1.default.gray(` [${ts}]`) +
526
+ chalk_1.default.green(` +${newCount} type(s)`) +
527
+ chalk_1.default.gray(` → ${stubsDir}`) +
528
+ chalk_1.default.gray(` (${funcCount} total)`));
529
+ lastTotal = funcCount;
530
+ }
531
+ }
532
+ catch {
533
+ // Never crash — background helper
534
+ }
535
+ };
536
+ const interval = setInterval(poll, 3000);
537
+ return () => {
538
+ stopped = true;
539
+ clearInterval(interval);
540
+ };
541
+ }
502
542
  // ── Auto-generate sidecar type file ──
503
543
  async function autoGenerateSidecar(filePath) {
504
544
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for trickle runtime type observability",
5
5
  "bin": {
6
6
  "trickle": "dist/index.js"
package/src/api-client.ts CHANGED
@@ -244,6 +244,37 @@ export async function fetchStubs(opts?: {
244
244
  });
245
245
  }
246
246
 
247
+ export interface FunctionSample {
248
+ functionName: string;
249
+ sampleInput: unknown;
250
+ sampleOutput: unknown;
251
+ }
252
+
253
+ export async function fetchFunctionSamples(): Promise<FunctionSample[]> {
254
+ const { functions } = await listFunctions({ limit: 500 });
255
+ const samples: FunctionSample[] = [];
256
+ for (const fn of functions) {
257
+ try {
258
+ const data = await fetchJson<{ snapshots: Array<{ sample_input: unknown; sample_output: unknown }> }>(
259
+ `/api/types/${encodeURIComponent(fn.function_name)}`
260
+ );
261
+ if (data.snapshots && data.snapshots.length > 0) {
262
+ const snap = data.snapshots[0];
263
+ if (snap.sample_input || snap.sample_output) {
264
+ samples.push({
265
+ functionName: fn.function_name,
266
+ sampleInput: snap.sample_input,
267
+ sampleOutput: snap.sample_output,
268
+ });
269
+ }
270
+ }
271
+ } catch {
272
+ // Skip functions with no data
273
+ }
274
+ }
275
+ return samples;
276
+ }
277
+
247
278
  export interface MockRoute {
248
279
  method: string;
249
280
  path: string;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import chalk from "chalk";
4
- import { fetchAnnotations, AnnotationEntry } from "../api-client";
4
+ import { fetchAnnotations, fetchFunctionSamples, AnnotationEntry, FunctionSample } from "../api-client";
5
5
 
6
6
  export interface AnnotateOptions {
7
7
  env?: string;
@@ -151,8 +151,9 @@ function annotateJSDoc(source: string, annotations: Record<string, AnnotationEnt
151
151
 
152
152
  /**
153
153
  * Annotate a TypeScript file with inline type annotations on function signatures.
154
+ * When samples are provided, adds JSDoc @example comments even for already-typed functions.
154
155
  */
155
- function annotateTS(source: string, annotations: Record<string, AnnotationEntry>): string {
156
+ function annotateTS(source: string, annotations: Record<string, AnnotationEntry>, samples?: Record<string, FunctionSample>): string {
156
157
  const lines = source.split("\n");
157
158
  const result: string[] = [];
158
159
 
@@ -166,17 +167,71 @@ function annotateTS(source: string, annotations: Record<string, AnnotationEntry>
166
167
  }
167
168
 
168
169
  const annotation = annotations[info.fnName];
169
- if (!annotation) {
170
+ const sample = samples?.[info.fnName];
171
+
172
+ if (!annotation && !sample) {
170
173
  result.push(line);
171
174
  continue;
172
175
  }
173
176
 
174
- // Skip if already typed (has a colon in param list or return annotation)
177
+ // Add JSDoc with @example from sample data (even for already-typed functions)
178
+ if (sample) {
179
+ // Check if there's already a trickle JSDoc comment above
180
+ const prevIdx = result.length - 1;
181
+ let alreadyHasTrickleDoc = false;
182
+ if (prevIdx >= 0) {
183
+ // Look back for a JSDoc block containing @example and "trickle"
184
+ let j = prevIdx;
185
+ while (j >= 0 && !result[j].trim().startsWith("/**")) j--;
186
+ if (j >= 0 && result[prevIdx].trim().endsWith("*/")) {
187
+ const block = result.slice(j, prevIdx + 1).join("\n");
188
+ if (block.includes("@example") && block.includes("trickle")) {
189
+ alreadyHasTrickleDoc = true;
190
+ }
191
+ }
192
+ }
193
+
194
+ if (!alreadyHasTrickleDoc) {
195
+ const indent = line.match(/^(\s*)/)?.[1] || "";
196
+ const jsdocLines: string[] = [];
197
+ jsdocLines.push(`${indent}/** @trickle`);
198
+
199
+ if (sample.sampleInput !== undefined && sample.sampleInput !== null) {
200
+ jsdocLines.push(`${indent} * @example`);
201
+ jsdocLines.push(`${indent} * // Sample input:`);
202
+ const inputStr = JSON.stringify(sample.sampleInput, null, 2);
203
+ for (const l of inputStr.split('\n')) {
204
+ jsdocLines.push(`${indent} * ${l}`);
205
+ }
206
+ }
207
+
208
+ if (sample.sampleOutput !== undefined && sample.sampleOutput !== null) {
209
+ if (sample.sampleInput === undefined || sample.sampleInput === null) {
210
+ jsdocLines.push(`${indent} * @example`);
211
+ }
212
+ jsdocLines.push(`${indent} * // Sample output:`);
213
+ const outputStr = JSON.stringify(sample.sampleOutput, null, 2);
214
+ for (const l of outputStr.split('\n')) {
215
+ jsdocLines.push(`${indent} * ${l}`);
216
+ }
217
+ }
218
+
219
+ jsdocLines.push(`${indent} */`);
220
+ result.push(...jsdocLines);
221
+ }
222
+ }
223
+
224
+ // Skip inline type modification if already typed
175
225
  if (info.rawParams.includes(":") || line.includes("): ")) {
176
226
  result.push(line);
177
227
  continue;
178
228
  }
179
229
 
230
+ if (!annotation) {
231
+ result.push(line);
232
+ continue;
233
+ }
234
+
180
235
  // Build typed param list
181
236
  const paramNames = getParamNames(info.rawParams);
182
237
 
@@ -331,7 +386,18 @@ export async function annotateCommand(
331
386
  language: apiLanguage,
332
387
  });
333
388
 
334
- if (!annotations || Object.keys(annotations).length === 0) {
389
+ // Fetch sample data for @example JSDoc
390
+ let samplesMap: Record<string, FunctionSample> = {};
391
+ try {
392
+ const samples = await fetchFunctionSamples();
393
+ for (const s of samples) {
394
+ samplesMap[s.functionName] = s;
395
+ }
396
+ } catch {
397
+ // Sample data is optional
398
+ }
399
+
400
+ if ((!annotations || Object.keys(annotations).length === 0) && Object.keys(samplesMap).length === 0) {
335
401
  console.log(chalk.yellow("\n No observed types found. Run your code with trickle first.\n"));
336
402
  return;
337
403
  }
@@ -348,8 +414,8 @@ export async function annotateCommand(
348
414
  annotated = annotateJSDoc(source, annotations);
349
415
  mode = "JSDoc comments";
350
416
  } else {
351
- annotated = annotateTS(source, annotations);
352
- mode = "TypeScript annotations";
417
+ annotated = annotateTS(source, annotations, samplesMap);
418
+ mode = "TypeScript annotations + sample data";
353
419
  }
354
420
 
355
421
  // Count changes
@@ -350,7 +350,9 @@ async function executeSingleRun(
350
350
 
351
351
  // Start live type generation for backend mode
352
352
  let liveStop: (() => void) | null = null;
353
- if (singleFile && !opts.stubs) {
353
+ if (opts.stubs) {
354
+ liveStop = startLiveStubsGeneration(opts.stubs);
355
+ } else if (singleFile) {
354
356
  liveStop = startLiveBackendTypes(singleFile);
355
357
  }
356
358
 
@@ -573,6 +575,49 @@ function startLiveBackendTypes(sourceFile: string): () => void {
573
575
  };
574
576
  }
575
577
 
578
+ // ── Live stubs generation during run ──
579
+
580
+ function startLiveStubsGeneration(stubsDir: string): () => void {
581
+ let lastTotal = 0;
582
+ let stopped = false;
583
+
584
+ const poll = async () => {
585
+ if (stopped) return;
586
+ try {
587
+ const { stubsCommand } = await import("./stubs");
588
+ const result = await stubsCommand(stubsDir, { silent: true });
589
+
590
+ // Count .d.ts files in the stubs dir to track progress
591
+ const files = fs.readdirSync(stubsDir).filter(f => f.endsWith('.d.ts'));
592
+ let funcCount = 0;
593
+ for (const f of files) {
594
+ const content = fs.readFileSync(path.join(stubsDir, f), 'utf-8');
595
+ funcCount += (content.match(/export declare function/g) || []).length;
596
+ }
597
+
598
+ if (funcCount > lastTotal) {
599
+ const newCount = funcCount - lastTotal;
600
+ const ts = new Date().toLocaleTimeString("en-US", { hour12: false });
601
+ console.log(
602
+ chalk.gray(` [${ts}]`) +
603
+ chalk.green(` +${newCount} type(s)`) +
604
+ chalk.gray(` → ${stubsDir}`) +
605
+ chalk.gray(` (${funcCount} total)`),
606
+ );
607
+ lastTotal = funcCount;
608
+ }
609
+ } catch {
610
+ // Never crash — background helper
611
+ }
612
+ };
613
+
614
+ const interval = setInterval(poll, 3000);
615
+ return () => {
616
+ stopped = true;
617
+ clearInterval(interval);
618
+ };
619
+ }
620
+
576
621
  // ── Auto-generate sidecar type file ──
577
622
 
578
623
  async function autoGenerateSidecar(filePath: string): Promise<void> {