trickle-cli 0.1.2 → 0.1.4

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/${fn.id}`);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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/${fn.id}`
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