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.
- package/dist/api-client.d.ts +6 -0
- package/dist/api-client.js +24 -0
- package/dist/commands/annotate.js +66 -6
- package/dist/commands/run.js +41 -1
- package/package.json +1 -1
- package/src/api-client.ts +31 -0
- package/src/commands/annotate.ts +73 -7
- package/src/commands/run.ts +46 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/api-client.js
CHANGED
|
@@ -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
|
-
|
|
185
|
+
const sample = samples?.[info.fnName];
|
|
186
|
+
if (!annotation && !sample) {
|
|
185
187
|
result.push(line);
|
|
186
188
|
continue;
|
|
187
189
|
}
|
|
188
|
-
//
|
|
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
|
-
|
|
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/dist/commands/run.js
CHANGED
|
@@ -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 (
|
|
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
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;
|
package/src/commands/annotate.ts
CHANGED
|
@@ -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
|
-
|
|
170
|
+
const sample = samples?.[info.fnName];
|
|
171
|
+
|
|
172
|
+
if (!annotation && !sample) {
|
|
170
173
|
result.push(line);
|
|
171
174
|
continue;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
//
|
|
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
|
-
|
|
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
|
package/src/commands/run.ts
CHANGED
|
@@ -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 (
|
|
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> {
|