trickle-cli 0.1.0
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 +208 -0
- package/dist/api-client.js +237 -0
- package/dist/commands/annotate.d.ts +6 -0
- package/dist/commands/annotate.js +433 -0
- package/dist/commands/audit.d.ts +7 -0
- package/dist/commands/audit.js +82 -0
- package/dist/commands/auto.d.ts +8 -0
- package/dist/commands/auto.js +268 -0
- package/dist/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +271 -0
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.js +408 -0
- package/dist/commands/codegen.d.ts +21 -0
- package/dist/commands/codegen.js +129 -0
- package/dist/commands/coverage.d.ts +13 -0
- package/dist/commands/coverage.js +126 -0
- package/dist/commands/dashboard.d.ts +1 -0
- package/dist/commands/dashboard.js +83 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.js +319 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.js +79 -0
- package/dist/commands/docs.d.ts +13 -0
- package/dist/commands/docs.js +383 -0
- package/dist/commands/errors.d.ts +7 -0
- package/dist/commands/errors.js +180 -0
- package/dist/commands/export.d.ts +18 -0
- package/dist/commands/export.js +238 -0
- package/dist/commands/functions.d.ts +6 -0
- package/dist/commands/functions.js +71 -0
- package/dist/commands/infer.d.ts +14 -0
- package/dist/commands/infer.js +275 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +395 -0
- package/dist/commands/mock.d.ts +5 -0
- package/dist/commands/mock.js +232 -0
- package/dist/commands/openapi.d.ts +8 -0
- package/dist/commands/openapi.js +82 -0
- package/dist/commands/overview.d.ts +11 -0
- package/dist/commands/overview.js +266 -0
- package/dist/commands/pack.d.ts +11 -0
- package/dist/commands/pack.js +133 -0
- package/dist/commands/proxy.d.ts +13 -0
- package/dist/commands/proxy.js +312 -0
- package/dist/commands/replay.d.ts +14 -0
- package/dist/commands/replay.js +289 -0
- package/dist/commands/run.d.ts +17 -0
- package/dist/commands/run.js +997 -0
- package/dist/commands/sample.d.ts +13 -0
- package/dist/commands/sample.js +260 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +80 -0
- package/dist/commands/stubs.d.ts +6 -0
- package/dist/commands/stubs.js +187 -0
- package/dist/commands/tail.d.ts +4 -0
- package/dist/commands/tail.js +76 -0
- package/dist/commands/test-gen.d.ts +13 -0
- package/dist/commands/test-gen.js +237 -0
- package/dist/commands/trace.d.ts +14 -0
- package/dist/commands/trace.js +417 -0
- package/dist/commands/types.d.ts +7 -0
- package/dist/commands/types.js +128 -0
- package/dist/commands/unpack.d.ts +11 -0
- package/dist/commands/unpack.js +166 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.js +310 -0
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.js +267 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +66 -0
- package/dist/formatters/diff-formatter.d.ts +5 -0
- package/dist/formatters/diff-formatter.js +43 -0
- package/dist/formatters/type-formatter.d.ts +22 -0
- package/dist/formatters/type-formatter.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +419 -0
- package/dist/local-codegen.d.ts +22 -0
- package/dist/local-codegen.js +762 -0
- package/dist/ui/badges.d.ts +16 -0
- package/dist/ui/badges.js +71 -0
- package/dist/ui/helpers.d.ts +13 -0
- package/dist/ui/helpers.js +85 -0
- package/package.json +23 -0
- package/src/api-client.ts +407 -0
- package/src/commands/annotate.ts +450 -0
- package/src/commands/audit.ts +103 -0
- package/src/commands/auto.ts +268 -0
- package/src/commands/capture.ts +257 -0
- package/src/commands/check.ts +437 -0
- package/src/commands/codegen.ts +128 -0
- package/src/commands/coverage.ts +170 -0
- package/src/commands/dashboard.ts +46 -0
- package/src/commands/dev.ts +323 -0
- package/src/commands/diff.ts +99 -0
- package/src/commands/docs.ts +392 -0
- package/src/commands/errors.ts +205 -0
- package/src/commands/export.ts +287 -0
- package/src/commands/functions.ts +81 -0
- package/src/commands/infer.ts +260 -0
- package/src/commands/init.ts +419 -0
- package/src/commands/mock.ts +220 -0
- package/src/commands/openapi.ts +53 -0
- package/src/commands/overview.ts +310 -0
- package/src/commands/pack.ts +139 -0
- package/src/commands/proxy.ts +314 -0
- package/src/commands/replay.ts +356 -0
- package/src/commands/run.ts +1190 -0
- package/src/commands/sample.ts +259 -0
- package/src/commands/search.ts +107 -0
- package/src/commands/stubs.ts +211 -0
- package/src/commands/tail.ts +94 -0
- package/src/commands/test-gen.ts +236 -0
- package/src/commands/trace.ts +440 -0
- package/src/commands/types.ts +161 -0
- package/src/commands/unpack.ts +179 -0
- package/src/commands/validate.ts +368 -0
- package/src/commands/watch.ts +277 -0
- package/src/config.ts +38 -0
- package/src/formatters/diff-formatter.ts +51 -0
- package/src/formatters/type-formatter.ts +161 -0
- package/src/index.ts +454 -0
- package/src/local-codegen.ts +859 -0
- package/src/ui/badges.ts +66 -0
- package/src/ui/helpers.ts +80 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local codegen: reads .trickle/observations.jsonl and generates
|
|
4
|
+
* type stubs without needing the backend running.
|
|
5
|
+
*
|
|
6
|
+
* Used by `trickle run` in offline/local mode.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.generateFromJsonl = generateFromJsonl;
|
|
43
|
+
exports.generateLocalStubs = generateLocalStubs;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
// ── Type merging ──
|
|
47
|
+
/**
|
|
48
|
+
* Merge two TypeNodes into a single type that represents both.
|
|
49
|
+
*
|
|
50
|
+
* - Same primitive → keep as-is
|
|
51
|
+
* - Different primitives → union
|
|
52
|
+
* - Two objects → merge properties (missing props become optional via union with undefined)
|
|
53
|
+
* - Two arrays → merge element types
|
|
54
|
+
* - Two tuples with same length → merge positionally
|
|
55
|
+
* - Anything else → union
|
|
56
|
+
*/
|
|
57
|
+
function mergeTypeNodes(a, b) {
|
|
58
|
+
// Identical nodes
|
|
59
|
+
if (typeNodeKey(a) === typeNodeKey(b))
|
|
60
|
+
return a;
|
|
61
|
+
// Both objects: merge properties
|
|
62
|
+
if (a.kind === "object" && b.kind === "object") {
|
|
63
|
+
const aProps = a.properties || {};
|
|
64
|
+
const bProps = b.properties || {};
|
|
65
|
+
const allKeys = new Set([...Object.keys(aProps), ...Object.keys(bProps)]);
|
|
66
|
+
const merged = {};
|
|
67
|
+
for (const key of allKeys) {
|
|
68
|
+
const inA = key in aProps;
|
|
69
|
+
const inB = key in bProps;
|
|
70
|
+
if (inA && inB) {
|
|
71
|
+
// Property exists in both — merge their types
|
|
72
|
+
merged[key] = mergeTypeNodes(aProps[key], bProps[key]);
|
|
73
|
+
}
|
|
74
|
+
else if (inA) {
|
|
75
|
+
// Only in A — mark as optional (union with undefined)
|
|
76
|
+
merged[key] = makeOptional(aProps[key]);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Only in B — mark as optional
|
|
80
|
+
merged[key] = makeOptional(bProps[key]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { kind: "object", properties: merged };
|
|
84
|
+
}
|
|
85
|
+
// Both arrays: merge element types
|
|
86
|
+
if (a.kind === "array" && b.kind === "array" && a.element && b.element) {
|
|
87
|
+
return { kind: "array", element: mergeTypeNodes(a.element, b.element) };
|
|
88
|
+
}
|
|
89
|
+
// Both tuples: merge positionally, handle different lengths
|
|
90
|
+
if (a.kind === "tuple" && b.kind === "tuple") {
|
|
91
|
+
const aEls = a.elements || [];
|
|
92
|
+
const bEls = b.elements || [];
|
|
93
|
+
if (aEls.length === bEls.length) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "tuple",
|
|
96
|
+
elements: aEls.map((el, i) => mergeTypeNodes(el, bEls[i])),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Different lengths: merge common prefix, make extra elements optional
|
|
100
|
+
const shorter = aEls.length < bEls.length ? aEls : bEls;
|
|
101
|
+
const longer = aEls.length < bEls.length ? bEls : aEls;
|
|
102
|
+
const merged = [];
|
|
103
|
+
for (let i = 0; i < longer.length; i++) {
|
|
104
|
+
if (i < shorter.length) {
|
|
105
|
+
merged.push(mergeTypeNodes(shorter[i], longer[i]));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
merged.push(makeOptional(longer[i]));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { kind: "tuple", elements: merged };
|
|
112
|
+
}
|
|
113
|
+
// Both unions: flatten and deduplicate
|
|
114
|
+
if (a.kind === "union" && b.kind === "union") {
|
|
115
|
+
return deduplicateUnion([...(a.members || []), ...(b.members || [])]);
|
|
116
|
+
}
|
|
117
|
+
// One is a union: add the other as a member
|
|
118
|
+
if (a.kind === "union") {
|
|
119
|
+
return deduplicateUnion([...(a.members || []), b]);
|
|
120
|
+
}
|
|
121
|
+
if (b.kind === "union") {
|
|
122
|
+
return deduplicateUnion([a, ...(b.members || [])]);
|
|
123
|
+
}
|
|
124
|
+
// Different kinds: create union
|
|
125
|
+
return deduplicateUnion([a, b]);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Make a type optional by adding undefined to it (for properties that
|
|
129
|
+
* don't appear in every observation).
|
|
130
|
+
*/
|
|
131
|
+
function makeOptional(node) {
|
|
132
|
+
// Already has undefined
|
|
133
|
+
if (node.kind === "primitive" && node.name === "undefined")
|
|
134
|
+
return node;
|
|
135
|
+
if (node.kind === "union") {
|
|
136
|
+
const members = node.members || [];
|
|
137
|
+
if (members.some((m) => m.kind === "primitive" && m.name === "undefined")) {
|
|
138
|
+
return node;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind: "union",
|
|
142
|
+
members: [...members, { kind: "primitive", name: "undefined" }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
kind: "union",
|
|
147
|
+
members: [node, { kind: "primitive", name: "undefined" }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a union type with deduplicated members.
|
|
152
|
+
*/
|
|
153
|
+
function deduplicateUnion(members) {
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const unique = [];
|
|
156
|
+
for (const m of members) {
|
|
157
|
+
// Flatten nested unions
|
|
158
|
+
if (m.kind === "union") {
|
|
159
|
+
for (const inner of m.members || []) {
|
|
160
|
+
const key = typeNodeKey(inner);
|
|
161
|
+
if (!seen.has(key)) {
|
|
162
|
+
seen.add(key);
|
|
163
|
+
unique.push(inner);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const key = typeNodeKey(m);
|
|
169
|
+
if (!seen.has(key)) {
|
|
170
|
+
seen.add(key);
|
|
171
|
+
unique.push(m);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (unique.length === 1)
|
|
176
|
+
return unique[0];
|
|
177
|
+
return { kind: "union", members: unique };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Generate a string key for a TypeNode (for deduplication).
|
|
181
|
+
*/
|
|
182
|
+
function typeNodeKey(node) {
|
|
183
|
+
switch (node.kind) {
|
|
184
|
+
case "primitive":
|
|
185
|
+
return `p:${node.name}`;
|
|
186
|
+
case "unknown":
|
|
187
|
+
return "unknown";
|
|
188
|
+
case "array":
|
|
189
|
+
return `a:${typeNodeKey(node.element)}`;
|
|
190
|
+
case "tuple":
|
|
191
|
+
return `t:[${(node.elements || []).map(typeNodeKey).join(",")}]`;
|
|
192
|
+
case "object": {
|
|
193
|
+
const props = node.properties || {};
|
|
194
|
+
const entries = Object.keys(props)
|
|
195
|
+
.sort()
|
|
196
|
+
.map((k) => `${k}:${typeNodeKey(props[k])}`);
|
|
197
|
+
return `o:{${entries.join(",")}}`;
|
|
198
|
+
}
|
|
199
|
+
case "union": {
|
|
200
|
+
const members = (node.members || []).map(typeNodeKey).sort();
|
|
201
|
+
return `u:(${members.join("|")})`;
|
|
202
|
+
}
|
|
203
|
+
default:
|
|
204
|
+
return JSON.stringify(node);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── Read and merge observations ──
|
|
208
|
+
function readObservations(jsonlPath) {
|
|
209
|
+
if (!fs.existsSync(jsonlPath))
|
|
210
|
+
return [];
|
|
211
|
+
const content = fs.readFileSync(jsonlPath, "utf-8");
|
|
212
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
213
|
+
// Collect all observations per function, then merge types
|
|
214
|
+
const byFunction = new Map();
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
try {
|
|
217
|
+
const payload = JSON.parse(line);
|
|
218
|
+
if (payload.functionName && payload.argsType && payload.returnType) {
|
|
219
|
+
if (!byFunction.has(payload.functionName)) {
|
|
220
|
+
byFunction.set(payload.functionName, { payloads: [] });
|
|
221
|
+
}
|
|
222
|
+
byFunction.get(payload.functionName).payloads.push(payload);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Skip malformed lines
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const results = [];
|
|
230
|
+
for (const [name, { payloads }] of byFunction) {
|
|
231
|
+
// Start with the first observation, merge subsequent ones
|
|
232
|
+
let mergedArgs = payloads[0].argsType;
|
|
233
|
+
let mergedReturn = payloads[0].returnType;
|
|
234
|
+
for (let i = 1; i < payloads.length; i++) {
|
|
235
|
+
// Only merge if the type hash differs (different shape)
|
|
236
|
+
if (payloads[i].typeHash !== payloads[0].typeHash) {
|
|
237
|
+
mergedArgs = mergeTypeNodes(mergedArgs, payloads[i].argsType);
|
|
238
|
+
mergedReturn = mergeTypeNodes(mergedReturn, payloads[i].returnType);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Use paramNames from the latest payload that has them
|
|
242
|
+
const paramNames = payloads.reduce((acc, p) => p.paramNames && p.paramNames.length > 0 ? p.paramNames : acc, undefined);
|
|
243
|
+
// Collect unique type variants (by typeHash) for overload generation
|
|
244
|
+
const seenHashes = new Set();
|
|
245
|
+
const variants = [];
|
|
246
|
+
for (const p of payloads) {
|
|
247
|
+
if (!seenHashes.has(p.typeHash)) {
|
|
248
|
+
seenHashes.add(p.typeHash);
|
|
249
|
+
variants.push({
|
|
250
|
+
argsType: p.argsType,
|
|
251
|
+
returnType: p.returnType,
|
|
252
|
+
paramNames: p.paramNames,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
results.push({
|
|
257
|
+
name,
|
|
258
|
+
argsType: mergedArgs,
|
|
259
|
+
returnType: mergedReturn,
|
|
260
|
+
module: payloads[payloads.length - 1].module, // use latest module
|
|
261
|
+
paramNames,
|
|
262
|
+
variants: variants.length >= 2 && variants.length <= 5 ? variants : undefined,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
// ── Naming helpers ──
|
|
268
|
+
function toPascalCase(name) {
|
|
269
|
+
return name
|
|
270
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
271
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
272
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
273
|
+
.trim()
|
|
274
|
+
.split(/\s+/)
|
|
275
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
276
|
+
.join("");
|
|
277
|
+
}
|
|
278
|
+
function toSnakeCase(name) {
|
|
279
|
+
return name
|
|
280
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
281
|
+
.replace(/[-\s]+/g, "_")
|
|
282
|
+
.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
function typeNodeToTS(node, extracted, parentName, propName, indent) {
|
|
285
|
+
switch (node.kind) {
|
|
286
|
+
case "primitive":
|
|
287
|
+
return node.name || "unknown";
|
|
288
|
+
case "unknown":
|
|
289
|
+
return "unknown";
|
|
290
|
+
case "array": {
|
|
291
|
+
const inner = typeNodeToTS(node.element, extracted, parentName, propName, indent);
|
|
292
|
+
return node.element.kind === "union" || node.element.kind === "function"
|
|
293
|
+
? `Array<${inner}>`
|
|
294
|
+
: `${inner}[]`;
|
|
295
|
+
}
|
|
296
|
+
case "tuple": {
|
|
297
|
+
const elements = (node.elements || []).map((el, i) => typeNodeToTS(el, extracted, parentName, `${propName || "el"}${i}`, indent));
|
|
298
|
+
return `[${elements.join(", ")}]`;
|
|
299
|
+
}
|
|
300
|
+
case "union": {
|
|
301
|
+
const members = (node.members || []).map((m) => typeNodeToTS(m, extracted, parentName, propName, indent));
|
|
302
|
+
return members.join(" | ");
|
|
303
|
+
}
|
|
304
|
+
case "map": {
|
|
305
|
+
const k = typeNodeToTS(node.key, extracted, parentName, "key", indent);
|
|
306
|
+
const v = typeNodeToTS(node.value, extracted, parentName, "value", indent);
|
|
307
|
+
return `Map<${k}, ${v}>`;
|
|
308
|
+
}
|
|
309
|
+
case "set":
|
|
310
|
+
return `Set<${typeNodeToTS(node.element, extracted, parentName, propName, indent)}>`;
|
|
311
|
+
case "promise":
|
|
312
|
+
return `Promise<${typeNodeToTS(node.resolved, extracted, parentName, propName, indent)}>`;
|
|
313
|
+
case "function": {
|
|
314
|
+
const params = (node.params || []).map((p, i) => `arg${i}: ${typeNodeToTS(p, extracted, parentName, `param${i}`, indent)}`);
|
|
315
|
+
const ret = typeNodeToTS(node.returnType, extracted, parentName, "return", indent);
|
|
316
|
+
return `(${params.join(", ")}) => ${ret}`;
|
|
317
|
+
}
|
|
318
|
+
case "object": {
|
|
319
|
+
const keys = Object.keys(node.properties || {});
|
|
320
|
+
if (keys.length === 0)
|
|
321
|
+
return "Record<string, never>";
|
|
322
|
+
if (keys.length > 2 && propName) {
|
|
323
|
+
const ifaceName = toPascalCase(parentName) + toPascalCase(propName);
|
|
324
|
+
if (!extracted.some((e) => e.name === ifaceName)) {
|
|
325
|
+
extracted.push({ name: ifaceName, node });
|
|
326
|
+
}
|
|
327
|
+
return ifaceName;
|
|
328
|
+
}
|
|
329
|
+
const pad = " ".repeat(indent + 1);
|
|
330
|
+
const closePad = " ".repeat(indent);
|
|
331
|
+
const entries = keys.map((key) => {
|
|
332
|
+
const val = typeNodeToTS(node.properties[key], extracted, parentName, key, indent + 1);
|
|
333
|
+
return `${pad}${key}: ${val};`;
|
|
334
|
+
});
|
|
335
|
+
return `{\n${entries.join("\n")}\n${closePad}}`;
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
return "unknown";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Check if a TypeNode is optional (union containing undefined).
|
|
343
|
+
* Returns { isOptional, innerType } where innerType has undefined stripped.
|
|
344
|
+
*/
|
|
345
|
+
function extractOptional(node) {
|
|
346
|
+
if (node.kind !== "union")
|
|
347
|
+
return { isOptional: false, innerType: node };
|
|
348
|
+
const members = node.members || [];
|
|
349
|
+
const hasUndefined = members.some((m) => m.kind === "primitive" && m.name === "undefined");
|
|
350
|
+
if (!hasUndefined)
|
|
351
|
+
return { isOptional: false, innerType: node };
|
|
352
|
+
const withoutUndefined = members.filter((m) => !(m.kind === "primitive" && m.name === "undefined"));
|
|
353
|
+
if (withoutUndefined.length === 0) {
|
|
354
|
+
return { isOptional: true, innerType: { kind: "primitive", name: "undefined" } };
|
|
355
|
+
}
|
|
356
|
+
if (withoutUndefined.length === 1) {
|
|
357
|
+
return { isOptional: true, innerType: withoutUndefined[0] };
|
|
358
|
+
}
|
|
359
|
+
return { isOptional: true, innerType: { kind: "union", members: withoutUndefined } };
|
|
360
|
+
}
|
|
361
|
+
function renderInterface(name, node, allExtracted) {
|
|
362
|
+
const keys = Object.keys(node.properties || {});
|
|
363
|
+
const lines = [`export interface ${name} {`];
|
|
364
|
+
for (const key of keys) {
|
|
365
|
+
const propType = node.properties[key];
|
|
366
|
+
const { isOptional, innerType } = extractOptional(propType);
|
|
367
|
+
const val = typeNodeToTS(innerType, allExtracted, name, key, 1);
|
|
368
|
+
if (isOptional) {
|
|
369
|
+
lines.push(` ${key}?: ${val};`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
lines.push(` ${key}: ${val};`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
lines.push(`}`);
|
|
376
|
+
return lines.join("\n");
|
|
377
|
+
}
|
|
378
|
+
function generateTsForFunction(fn) {
|
|
379
|
+
const baseName = toPascalCase(fn.name);
|
|
380
|
+
const extracted = [];
|
|
381
|
+
const lines = [];
|
|
382
|
+
// Determine args
|
|
383
|
+
let argEntries = [];
|
|
384
|
+
if (fn.argsType.kind === "tuple") {
|
|
385
|
+
const names = fn.paramNames || [];
|
|
386
|
+
argEntries = (fn.argsType.elements || []).map((el, i) => ({
|
|
387
|
+
paramName: names[i] || `arg${i}`,
|
|
388
|
+
typeNode: el,
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
else if (fn.argsType.kind === "object") {
|
|
392
|
+
for (const key of Object.keys(fn.argsType.properties || {})) {
|
|
393
|
+
argEntries.push({ paramName: key, typeNode: fn.argsType.properties[key] });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
argEntries = [{ paramName: "input", typeNode: fn.argsType }];
|
|
398
|
+
}
|
|
399
|
+
const singleObjectArg = argEntries.length === 1 && argEntries[0].typeNode.kind === "object";
|
|
400
|
+
// Input type
|
|
401
|
+
if (singleObjectArg) {
|
|
402
|
+
const inputName = `${baseName}Input`;
|
|
403
|
+
lines.push(`/**`);
|
|
404
|
+
lines.push(` * Input type for \`${fn.name}\``);
|
|
405
|
+
lines.push(` */`);
|
|
406
|
+
lines.push(renderInterface(inputName, argEntries[0].typeNode, extracted));
|
|
407
|
+
lines.push("");
|
|
408
|
+
}
|
|
409
|
+
else if (argEntries.length > 1) {
|
|
410
|
+
for (const entry of argEntries) {
|
|
411
|
+
if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties || {}).length > 0) {
|
|
412
|
+
const typeName = `${baseName}${toPascalCase(entry.paramName)}`;
|
|
413
|
+
lines.push(renderInterface(typeName, entry.typeNode, extracted));
|
|
414
|
+
lines.push("");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Output type
|
|
419
|
+
const outputName = `${baseName}Output`;
|
|
420
|
+
if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties || {}).length > 0) {
|
|
421
|
+
lines.push(`/**`);
|
|
422
|
+
lines.push(` * Output type for \`${fn.name}\``);
|
|
423
|
+
lines.push(` */`);
|
|
424
|
+
lines.push(renderInterface(outputName, fn.returnType, extracted));
|
|
425
|
+
lines.push("");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
|
|
429
|
+
lines.push(`export type ${outputName} = ${retStr};`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
}
|
|
432
|
+
// Extracted interfaces
|
|
433
|
+
const emitted = new Set();
|
|
434
|
+
const extractedLines = [];
|
|
435
|
+
let cursor = 0;
|
|
436
|
+
while (cursor < extracted.length) {
|
|
437
|
+
const iface = extracted[cursor];
|
|
438
|
+
cursor++;
|
|
439
|
+
if (emitted.has(iface.name))
|
|
440
|
+
continue;
|
|
441
|
+
emitted.add(iface.name);
|
|
442
|
+
extractedLines.push(renderInterface(iface.name, iface.node, extracted));
|
|
443
|
+
extractedLines.push("");
|
|
444
|
+
}
|
|
445
|
+
// Function declaration
|
|
446
|
+
const funcIdent = baseName.charAt(0).toLowerCase() + baseName.slice(1);
|
|
447
|
+
const result = [];
|
|
448
|
+
if (extractedLines.length > 0)
|
|
449
|
+
result.push(...extractedLines);
|
|
450
|
+
result.push(...lines);
|
|
451
|
+
// Generate overloads if we have multiple distinct type patterns
|
|
452
|
+
if (fn.variants && fn.variants.length >= 2) {
|
|
453
|
+
for (const variant of fn.variants) {
|
|
454
|
+
const vExt = [];
|
|
455
|
+
const vRet = typeNodeToTS(variant.returnType, vExt, baseName, undefined, 0);
|
|
456
|
+
const vNames = variant.paramNames || fn.paramNames || [];
|
|
457
|
+
let vArgEntries = [];
|
|
458
|
+
if (variant.argsType.kind === "tuple") {
|
|
459
|
+
vArgEntries = (variant.argsType.elements || []).map((el, i) => ({
|
|
460
|
+
paramName: vNames[i] || `arg${i}`,
|
|
461
|
+
typeNode: el,
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
const vParams = vArgEntries.map(e => `${e.paramName}: ${typeNodeToTS(e.typeNode, vExt, baseName, e.paramName, 0)}`);
|
|
465
|
+
result.push(`export declare function ${funcIdent}(${vParams.join(", ")}): ${vRet};`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
let funcDecl;
|
|
470
|
+
if (singleObjectArg) {
|
|
471
|
+
funcDecl = `export declare function ${funcIdent}(input: ${baseName}Input): ${outputName};`;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const params = argEntries.map((entry) => {
|
|
475
|
+
if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties || {}).length > 0) {
|
|
476
|
+
return `${entry.paramName}: ${baseName}${toPascalCase(entry.paramName)}`;
|
|
477
|
+
}
|
|
478
|
+
// Check if parameter is optional (union with undefined)
|
|
479
|
+
const { isOptional, innerType } = extractOptional(entry.typeNode);
|
|
480
|
+
if (isOptional) {
|
|
481
|
+
return `${entry.paramName}?: ${typeNodeToTS(innerType, extracted, baseName, entry.paramName, 0)}`;
|
|
482
|
+
}
|
|
483
|
+
return `${entry.paramName}: ${typeNodeToTS(entry.typeNode, extracted, baseName, entry.paramName, 0)}`;
|
|
484
|
+
});
|
|
485
|
+
funcDecl = `export declare function ${funcIdent}(${params.join(", ")}): ${outputName};`;
|
|
486
|
+
}
|
|
487
|
+
result.push(funcDecl);
|
|
488
|
+
}
|
|
489
|
+
return result.join("\n");
|
|
490
|
+
}
|
|
491
|
+
// ── Python stub generation ──
|
|
492
|
+
function typeNodeToPython(node, extracted, parentName, propName) {
|
|
493
|
+
switch (node.kind) {
|
|
494
|
+
case "primitive":
|
|
495
|
+
switch (node.name) {
|
|
496
|
+
case "string": return "str";
|
|
497
|
+
case "number": return "float";
|
|
498
|
+
case "boolean": return "bool";
|
|
499
|
+
case "null":
|
|
500
|
+
case "undefined": return "None";
|
|
501
|
+
case "bigint": return "int";
|
|
502
|
+
default: return "Any";
|
|
503
|
+
}
|
|
504
|
+
case "unknown": return "Any";
|
|
505
|
+
case "array":
|
|
506
|
+
return `List[${typeNodeToPython(node.element, extracted, parentName, propName)}]`;
|
|
507
|
+
case "tuple": {
|
|
508
|
+
const els = (node.elements || []).map((el, i) => typeNodeToPython(el, extracted, parentName, `el${i}`));
|
|
509
|
+
return `Tuple[${els.join(", ")}]`;
|
|
510
|
+
}
|
|
511
|
+
case "union": {
|
|
512
|
+
const members = (node.members || []).map((m) => typeNodeToPython(m, extracted, parentName, propName));
|
|
513
|
+
if (members.length === 2 && members.includes("None")) {
|
|
514
|
+
const nonNone = members.find((m) => m !== "None");
|
|
515
|
+
return `Optional[${nonNone}]`;
|
|
516
|
+
}
|
|
517
|
+
return `Union[${members.join(", ")}]`;
|
|
518
|
+
}
|
|
519
|
+
case "map": {
|
|
520
|
+
const k = typeNodeToPython(node.key, extracted, parentName, "key");
|
|
521
|
+
const v = typeNodeToPython(node.value, extracted, parentName, "value");
|
|
522
|
+
return `Dict[${k}, ${v}]`;
|
|
523
|
+
}
|
|
524
|
+
case "set":
|
|
525
|
+
return `Set[${typeNodeToPython(node.element, extracted, parentName, propName)}]`;
|
|
526
|
+
case "promise":
|
|
527
|
+
return `Awaitable[${typeNodeToPython(node.resolved, extracted, parentName, propName)}]`;
|
|
528
|
+
case "function": {
|
|
529
|
+
const params = (node.params || []).map((p) => typeNodeToPython(p, extracted, parentName, undefined));
|
|
530
|
+
const ret = typeNodeToPython(node.returnType, extracted, parentName, "return");
|
|
531
|
+
return `Callable[[${params.join(", ")}], ${ret}]`;
|
|
532
|
+
}
|
|
533
|
+
case "object": {
|
|
534
|
+
const keys = Object.keys(node.properties || {});
|
|
535
|
+
if (keys.length === 0)
|
|
536
|
+
return "Dict[str, Any]";
|
|
537
|
+
if (propName) {
|
|
538
|
+
const className = toPascalCase(parentName) + toPascalCase(propName);
|
|
539
|
+
if (!extracted.some((e) => e.name === className)) {
|
|
540
|
+
extracted.push({ name: className, node });
|
|
541
|
+
}
|
|
542
|
+
return className;
|
|
543
|
+
}
|
|
544
|
+
return "Dict[str, Any]";
|
|
545
|
+
}
|
|
546
|
+
default: return "Any";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function renderPythonTypedDict(name, node, extracted) {
|
|
550
|
+
const keys = Object.keys(node.properties || {});
|
|
551
|
+
const lines = [];
|
|
552
|
+
// Check if we have any optional fields — if so, use total=False pattern
|
|
553
|
+
const hasOptional = keys.some((key) => {
|
|
554
|
+
const { isOptional } = extractOptional(node.properties[key]);
|
|
555
|
+
return isOptional;
|
|
556
|
+
});
|
|
557
|
+
if (hasOptional) {
|
|
558
|
+
// Separate required and optional fields
|
|
559
|
+
const required = [];
|
|
560
|
+
const optional = [];
|
|
561
|
+
for (const key of keys) {
|
|
562
|
+
const propType = node.properties[key];
|
|
563
|
+
const { isOptional, innerType } = extractOptional(propType);
|
|
564
|
+
const pyType = isOptional
|
|
565
|
+
? typeNodeToPython(innerType, extracted, name, key)
|
|
566
|
+
: typeNodeToPython(propType, extracted, name, key);
|
|
567
|
+
if (isOptional) {
|
|
568
|
+
optional.push(` ${toSnakeCase(key)}: ${pyType}`);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
required.push(` ${toSnakeCase(key)}: ${pyType}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Use TypedDict with total=False for optional fields
|
|
575
|
+
if (required.length > 0 && optional.length > 0) {
|
|
576
|
+
// Need two TypedDicts: one for required, inherit for optional
|
|
577
|
+
const baseName = `_${name}Required`;
|
|
578
|
+
lines.push(`class ${baseName}(TypedDict):`);
|
|
579
|
+
lines.push(...required);
|
|
580
|
+
lines.push("");
|
|
581
|
+
lines.push("");
|
|
582
|
+
lines.push(`class ${name}(${baseName}, total=False):`);
|
|
583
|
+
lines.push(...optional);
|
|
584
|
+
}
|
|
585
|
+
else if (optional.length > 0) {
|
|
586
|
+
lines.push(`class ${name}(TypedDict, total=False):`);
|
|
587
|
+
lines.push(...optional);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
lines.push(`class ${name}(TypedDict):`);
|
|
591
|
+
lines.push(...required);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
const entries = keys.map((key) => {
|
|
596
|
+
const pyType = typeNodeToPython(node.properties[key], extracted, name, key);
|
|
597
|
+
return ` ${toSnakeCase(key)}: ${pyType}`;
|
|
598
|
+
});
|
|
599
|
+
lines.push(`class ${name}(TypedDict):`);
|
|
600
|
+
if (entries.length === 0) {
|
|
601
|
+
lines.push(" pass");
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
lines.push(...entries);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return lines.join("\n");
|
|
608
|
+
}
|
|
609
|
+
function generatePyForFunction(fn) {
|
|
610
|
+
const baseName = toPascalCase(fn.name);
|
|
611
|
+
const extracted = [];
|
|
612
|
+
const sections = [];
|
|
613
|
+
// Input type
|
|
614
|
+
if (fn.argsType.kind === "tuple" && fn.argsType.elements?.length === 1 && fn.argsType.elements[0].kind === "object") {
|
|
615
|
+
sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType.elements[0], extracted));
|
|
616
|
+
}
|
|
617
|
+
else if (fn.argsType.kind === "object") {
|
|
618
|
+
sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType, extracted));
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const pyType = typeNodeToPython(fn.argsType, extracted, baseName, undefined);
|
|
622
|
+
sections.push(`${baseName}Input = ${pyType}`);
|
|
623
|
+
}
|
|
624
|
+
sections.push("");
|
|
625
|
+
sections.push("");
|
|
626
|
+
// Output type
|
|
627
|
+
if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties || {}).length > 0) {
|
|
628
|
+
sections.push(renderPythonTypedDict(`${baseName}Output`, fn.returnType, extracted));
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const pyType = typeNodeToPython(fn.returnType, extracted, baseName, undefined);
|
|
632
|
+
sections.push(`${baseName}Output = ${pyType}`);
|
|
633
|
+
}
|
|
634
|
+
return sections.join("\n");
|
|
635
|
+
}
|
|
636
|
+
// ── Public API ──
|
|
637
|
+
/**
|
|
638
|
+
* Generate type stubs from a .trickle/observations.jsonl file.
|
|
639
|
+
* Returns { ts, python } content strings, grouped by module.
|
|
640
|
+
*/
|
|
641
|
+
function generateFromJsonl(jsonlPath) {
|
|
642
|
+
const functions = readObservations(jsonlPath);
|
|
643
|
+
if (functions.length === 0)
|
|
644
|
+
return {};
|
|
645
|
+
// Group by module
|
|
646
|
+
const byModule = new Map();
|
|
647
|
+
for (const fn of functions) {
|
|
648
|
+
const mod = fn.module || "_default";
|
|
649
|
+
if (!byModule.has(mod))
|
|
650
|
+
byModule.set(mod, []);
|
|
651
|
+
byModule.get(mod).push(fn);
|
|
652
|
+
}
|
|
653
|
+
const result = {};
|
|
654
|
+
for (const [mod, fns] of byModule) {
|
|
655
|
+
// TypeScript
|
|
656
|
+
const tsSections = [
|
|
657
|
+
"// Auto-generated by trickle from runtime type observations (local mode)",
|
|
658
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
659
|
+
"// Do not edit manually — re-run your code with trickle to update",
|
|
660
|
+
"",
|
|
661
|
+
];
|
|
662
|
+
for (const fn of fns) {
|
|
663
|
+
tsSections.push(generateTsForFunction(fn));
|
|
664
|
+
tsSections.push("");
|
|
665
|
+
}
|
|
666
|
+
// Python
|
|
667
|
+
const pySections = [
|
|
668
|
+
"# Auto-generated by trickle from runtime type observations (local mode)",
|
|
669
|
+
`# Generated at ${new Date().toISOString()}`,
|
|
670
|
+
"# Do not edit manually — re-run your code with trickle to update",
|
|
671
|
+
"",
|
|
672
|
+
"from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypedDict, Union, overload",
|
|
673
|
+
"",
|
|
674
|
+
"",
|
|
675
|
+
];
|
|
676
|
+
for (const fn of fns) {
|
|
677
|
+
pySections.push(generatePyForFunction(fn));
|
|
678
|
+
pySections.push("");
|
|
679
|
+
pySections.push("");
|
|
680
|
+
}
|
|
681
|
+
result[mod] = {
|
|
682
|
+
ts: tsSections.join("\n").trimEnd() + "\n",
|
|
683
|
+
python: pySections.join("\n").trimEnd() + "\n",
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Generate sidecar type files from local observations.
|
|
690
|
+
* Writes .d.ts or .pyi files next to the source file.
|
|
691
|
+
*/
|
|
692
|
+
function generateLocalStubs(sourceFile, jsonlPath) {
|
|
693
|
+
const trickleDir = jsonlPath
|
|
694
|
+
? path.dirname(jsonlPath)
|
|
695
|
+
: path.join(process.cwd(), ".trickle");
|
|
696
|
+
const obsPath = jsonlPath || path.join(trickleDir, "observations.jsonl");
|
|
697
|
+
const stubs = generateFromJsonl(obsPath);
|
|
698
|
+
const written = [];
|
|
699
|
+
let functionCount = 0;
|
|
700
|
+
const ext = path.extname(sourceFile).toLowerCase();
|
|
701
|
+
const isPython = ext === ".py";
|
|
702
|
+
const dir = path.dirname(sourceFile);
|
|
703
|
+
const baseName = path.basename(sourceFile, ext);
|
|
704
|
+
const normalizedBase = baseName.replace(/[-_]/g, "").toLowerCase();
|
|
705
|
+
for (const [mod, content] of Object.entries(stubs)) {
|
|
706
|
+
const normalizedMod = mod.replace(/[-_]/g, "").toLowerCase();
|
|
707
|
+
// Match module name to source file name
|
|
708
|
+
if (normalizedMod === normalizedBase || mod === "_default") {
|
|
709
|
+
const stubExt = isPython ? ".pyi" : ".d.ts";
|
|
710
|
+
const stubPath = path.join(dir, `${baseName}${stubExt}`);
|
|
711
|
+
const stubContent = isPython ? content.python : content.ts;
|
|
712
|
+
fs.writeFileSync(stubPath, stubContent, "utf-8");
|
|
713
|
+
written.push(stubPath);
|
|
714
|
+
// Count functions from observations
|
|
715
|
+
const functions = readObservations(obsPath);
|
|
716
|
+
functionCount = functions.length;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// If no module matched but we have stubs, write them all under the source file name
|
|
721
|
+
if (written.length === 0 && Object.keys(stubs).length > 0) {
|
|
722
|
+
const allFunctions = readObservations(obsPath);
|
|
723
|
+
functionCount = allFunctions.length;
|
|
724
|
+
if (allFunctions.length > 0) {
|
|
725
|
+
const stubExt = isPython ? ".pyi" : ".d.ts";
|
|
726
|
+
const stubPath = path.join(dir, `${baseName}${stubExt}`);
|
|
727
|
+
// Generate combined stubs for all functions
|
|
728
|
+
if (isPython) {
|
|
729
|
+
const sections = [
|
|
730
|
+
"# Auto-generated by trickle from runtime type observations (local mode)",
|
|
731
|
+
`# Generated at ${new Date().toISOString()}`,
|
|
732
|
+
"# Do not edit manually — re-run your code with trickle to update",
|
|
733
|
+
"",
|
|
734
|
+
"from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypedDict, Union, overload",
|
|
735
|
+
"",
|
|
736
|
+
"",
|
|
737
|
+
];
|
|
738
|
+
for (const fn of allFunctions) {
|
|
739
|
+
sections.push(generatePyForFunction(fn));
|
|
740
|
+
sections.push("");
|
|
741
|
+
sections.push("");
|
|
742
|
+
}
|
|
743
|
+
fs.writeFileSync(stubPath, sections.join("\n").trimEnd() + "\n", "utf-8");
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const sections = [
|
|
747
|
+
"// Auto-generated by trickle from runtime type observations (local mode)",
|
|
748
|
+
`// Generated at ${new Date().toISOString()}`,
|
|
749
|
+
"// Do not edit manually — re-run your code with trickle to update",
|
|
750
|
+
"",
|
|
751
|
+
];
|
|
752
|
+
for (const fn of allFunctions) {
|
|
753
|
+
sections.push(generateTsForFunction(fn));
|
|
754
|
+
sections.push("");
|
|
755
|
+
}
|
|
756
|
+
fs.writeFileSync(stubPath, sections.join("\n").trimEnd() + "\n", "utf-8");
|
|
757
|
+
}
|
|
758
|
+
written.push(stubPath);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { written, functionCount };
|
|
762
|
+
}
|