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.
Files changed (125) hide show
  1. package/dist/api-client.d.ts +208 -0
  2. package/dist/api-client.js +237 -0
  3. package/dist/commands/annotate.d.ts +6 -0
  4. package/dist/commands/annotate.js +433 -0
  5. package/dist/commands/audit.d.ts +7 -0
  6. package/dist/commands/audit.js +82 -0
  7. package/dist/commands/auto.d.ts +8 -0
  8. package/dist/commands/auto.js +268 -0
  9. package/dist/commands/capture.d.ts +14 -0
  10. package/dist/commands/capture.js +271 -0
  11. package/dist/commands/check.d.ts +6 -0
  12. package/dist/commands/check.js +408 -0
  13. package/dist/commands/codegen.d.ts +21 -0
  14. package/dist/commands/codegen.js +129 -0
  15. package/dist/commands/coverage.d.ts +13 -0
  16. package/dist/commands/coverage.js +126 -0
  17. package/dist/commands/dashboard.d.ts +1 -0
  18. package/dist/commands/dashboard.js +83 -0
  19. package/dist/commands/dev.d.ts +14 -0
  20. package/dist/commands/dev.js +319 -0
  21. package/dist/commands/diff.d.ts +7 -0
  22. package/dist/commands/diff.js +79 -0
  23. package/dist/commands/docs.d.ts +13 -0
  24. package/dist/commands/docs.js +383 -0
  25. package/dist/commands/errors.d.ts +7 -0
  26. package/dist/commands/errors.js +180 -0
  27. package/dist/commands/export.d.ts +18 -0
  28. package/dist/commands/export.js +238 -0
  29. package/dist/commands/functions.d.ts +6 -0
  30. package/dist/commands/functions.js +71 -0
  31. package/dist/commands/infer.d.ts +14 -0
  32. package/dist/commands/infer.js +275 -0
  33. package/dist/commands/init.d.ts +5 -0
  34. package/dist/commands/init.js +395 -0
  35. package/dist/commands/mock.d.ts +5 -0
  36. package/dist/commands/mock.js +232 -0
  37. package/dist/commands/openapi.d.ts +8 -0
  38. package/dist/commands/openapi.js +82 -0
  39. package/dist/commands/overview.d.ts +11 -0
  40. package/dist/commands/overview.js +266 -0
  41. package/dist/commands/pack.d.ts +11 -0
  42. package/dist/commands/pack.js +133 -0
  43. package/dist/commands/proxy.d.ts +13 -0
  44. package/dist/commands/proxy.js +312 -0
  45. package/dist/commands/replay.d.ts +14 -0
  46. package/dist/commands/replay.js +289 -0
  47. package/dist/commands/run.d.ts +17 -0
  48. package/dist/commands/run.js +997 -0
  49. package/dist/commands/sample.d.ts +13 -0
  50. package/dist/commands/sample.js +260 -0
  51. package/dist/commands/search.d.ts +5 -0
  52. package/dist/commands/search.js +80 -0
  53. package/dist/commands/stubs.d.ts +6 -0
  54. package/dist/commands/stubs.js +187 -0
  55. package/dist/commands/tail.d.ts +4 -0
  56. package/dist/commands/tail.js +76 -0
  57. package/dist/commands/test-gen.d.ts +13 -0
  58. package/dist/commands/test-gen.js +237 -0
  59. package/dist/commands/trace.d.ts +14 -0
  60. package/dist/commands/trace.js +417 -0
  61. package/dist/commands/types.d.ts +7 -0
  62. package/dist/commands/types.js +128 -0
  63. package/dist/commands/unpack.d.ts +11 -0
  64. package/dist/commands/unpack.js +166 -0
  65. package/dist/commands/validate.d.ts +13 -0
  66. package/dist/commands/validate.js +310 -0
  67. package/dist/commands/watch.d.ts +9 -0
  68. package/dist/commands/watch.js +267 -0
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.js +66 -0
  71. package/dist/formatters/diff-formatter.d.ts +5 -0
  72. package/dist/formatters/diff-formatter.js +43 -0
  73. package/dist/formatters/type-formatter.d.ts +22 -0
  74. package/dist/formatters/type-formatter.js +135 -0
  75. package/dist/index.d.ts +2 -0
  76. package/dist/index.js +419 -0
  77. package/dist/local-codegen.d.ts +22 -0
  78. package/dist/local-codegen.js +762 -0
  79. package/dist/ui/badges.d.ts +16 -0
  80. package/dist/ui/badges.js +71 -0
  81. package/dist/ui/helpers.d.ts +13 -0
  82. package/dist/ui/helpers.js +85 -0
  83. package/package.json +23 -0
  84. package/src/api-client.ts +407 -0
  85. package/src/commands/annotate.ts +450 -0
  86. package/src/commands/audit.ts +103 -0
  87. package/src/commands/auto.ts +268 -0
  88. package/src/commands/capture.ts +257 -0
  89. package/src/commands/check.ts +437 -0
  90. package/src/commands/codegen.ts +128 -0
  91. package/src/commands/coverage.ts +170 -0
  92. package/src/commands/dashboard.ts +46 -0
  93. package/src/commands/dev.ts +323 -0
  94. package/src/commands/diff.ts +99 -0
  95. package/src/commands/docs.ts +392 -0
  96. package/src/commands/errors.ts +205 -0
  97. package/src/commands/export.ts +287 -0
  98. package/src/commands/functions.ts +81 -0
  99. package/src/commands/infer.ts +260 -0
  100. package/src/commands/init.ts +419 -0
  101. package/src/commands/mock.ts +220 -0
  102. package/src/commands/openapi.ts +53 -0
  103. package/src/commands/overview.ts +310 -0
  104. package/src/commands/pack.ts +139 -0
  105. package/src/commands/proxy.ts +314 -0
  106. package/src/commands/replay.ts +356 -0
  107. package/src/commands/run.ts +1190 -0
  108. package/src/commands/sample.ts +259 -0
  109. package/src/commands/search.ts +107 -0
  110. package/src/commands/stubs.ts +211 -0
  111. package/src/commands/tail.ts +94 -0
  112. package/src/commands/test-gen.ts +236 -0
  113. package/src/commands/trace.ts +440 -0
  114. package/src/commands/types.ts +161 -0
  115. package/src/commands/unpack.ts +179 -0
  116. package/src/commands/validate.ts +368 -0
  117. package/src/commands/watch.ts +277 -0
  118. package/src/config.ts +38 -0
  119. package/src/formatters/diff-formatter.ts +51 -0
  120. package/src/formatters/type-formatter.ts +161 -0
  121. package/src/index.ts +454 -0
  122. package/src/local-codegen.ts +859 -0
  123. package/src/ui/badges.ts +66 -0
  124. package/src/ui/helpers.ts +80 -0
  125. package/tsconfig.json +8 -0
@@ -0,0 +1,437 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import chalk from "chalk";
4
+ import { fetchSnapshot, CheckSnapshot, SnapshotFunction } from "../api-client";
5
+
6
+ export interface CheckOptions {
7
+ save?: string;
8
+ against?: string;
9
+ env?: string;
10
+ }
11
+
12
+ interface BreakingChange {
13
+ functionName: string;
14
+ severity: "breaking" | "non-breaking";
15
+ description: string;
16
+ path: string;
17
+ }
18
+
19
+ /**
20
+ * Recursively diff two type nodes and classify changes as breaking or non-breaking.
21
+ *
22
+ * Breaking changes (for responses):
23
+ * - Field removed from response object
24
+ * - Field type changed in response
25
+ * - Array element type changed
26
+ *
27
+ * Non-breaking changes (for responses):
28
+ * - Field added to response object
29
+ *
30
+ * Breaking changes (for requests/args):
31
+ * - New required field added to request body
32
+ * - Field type changed in request
33
+ *
34
+ * Non-breaking changes (for requests/args):
35
+ * - Field removed from request (server no longer requires it)
36
+ */
37
+ function classifyChanges(
38
+ baseline: unknown,
39
+ current: unknown,
40
+ basePath: string,
41
+ context: "request" | "response",
42
+ ): BreakingChange[] {
43
+ const changes: BreakingChange[] = [];
44
+ const b = baseline as Record<string, unknown>;
45
+ const c = current as Record<string, unknown>;
46
+
47
+ if (!b || !c) return changes;
48
+
49
+ // Different kinds
50
+ if (b.kind !== c.kind) {
51
+ changes.push({
52
+ functionName: "",
53
+ severity: "breaking",
54
+ description: `Type changed from ${b.kind} to ${c.kind}`,
55
+ path: basePath || "(root)",
56
+ });
57
+ return changes;
58
+ }
59
+
60
+ switch (b.kind) {
61
+ case "primitive": {
62
+ if (b.name !== (c as Record<string, unknown>).name) {
63
+ changes.push({
64
+ functionName: "",
65
+ severity: "breaking",
66
+ description: `Type changed from ${b.name} to ${(c as Record<string, unknown>).name}`,
67
+ path: basePath || "(root)",
68
+ });
69
+ }
70
+ break;
71
+ }
72
+
73
+ case "object": {
74
+ const bProps = b.properties as Record<string, unknown>;
75
+ const cProps = (c as Record<string, unknown>).properties as Record<string, unknown>;
76
+ const bKeys = new Set(Object.keys(bProps || {}));
77
+ const cKeys = new Set(Object.keys(cProps || {}));
78
+
79
+ // Fields in baseline but not in current
80
+ for (const key of bKeys) {
81
+ const childPath = basePath ? `${basePath}.${key}` : key;
82
+ if (!cKeys.has(key)) {
83
+ if (context === "response") {
84
+ // Removing a response field is breaking (clients may depend on it)
85
+ changes.push({
86
+ functionName: "",
87
+ severity: "breaking",
88
+ description: `Field removed from response`,
89
+ path: childPath,
90
+ });
91
+ } else {
92
+ // Removing a request field is non-breaking (server no longer needs it)
93
+ changes.push({
94
+ functionName: "",
95
+ severity: "non-breaking",
96
+ description: `Field removed from request (no longer required)`,
97
+ path: childPath,
98
+ });
99
+ }
100
+ } else {
101
+ // Recursively check
102
+ changes.push(...classifyChanges(bProps[key], cProps[key], childPath, context));
103
+ }
104
+ }
105
+
106
+ // Fields in current but not in baseline
107
+ for (const key of cKeys) {
108
+ if (!bKeys.has(key)) {
109
+ const childPath = basePath ? `${basePath}.${key}` : key;
110
+ if (context === "response") {
111
+ // Adding a response field is non-breaking
112
+ changes.push({
113
+ functionName: "",
114
+ severity: "non-breaking",
115
+ description: `Field added to response`,
116
+ path: childPath,
117
+ });
118
+ } else {
119
+ // Adding a request field is breaking (callers don't send it yet)
120
+ changes.push({
121
+ functionName: "",
122
+ severity: "breaking",
123
+ description: `New required field added to request`,
124
+ path: childPath,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ break;
130
+ }
131
+
132
+ case "array": {
133
+ const bEl = b.element as Record<string, unknown>;
134
+ const cEl = (c as Record<string, unknown>).element as Record<string, unknown>;
135
+ changes.push(...classifyChanges(bEl, cEl, `${basePath || "(root)"}[]`, context));
136
+ break;
137
+ }
138
+
139
+ case "tuple": {
140
+ const bEls = b.elements as unknown[];
141
+ const cEls = (c as Record<string, unknown>).elements as unknown[];
142
+ const maxLen = Math.max(bEls?.length || 0, cEls?.length || 0);
143
+ for (let i = 0; i < maxLen; i++) {
144
+ const elPath = `${basePath || "(root)"}[${i}]`;
145
+ if (i >= (bEls?.length || 0)) {
146
+ changes.push({
147
+ functionName: "",
148
+ severity: context === "response" ? "non-breaking" : "breaking",
149
+ description: `Element added`,
150
+ path: elPath,
151
+ });
152
+ } else if (i >= (cEls?.length || 0)) {
153
+ changes.push({
154
+ functionName: "",
155
+ severity: "breaking",
156
+ description: `Element removed`,
157
+ path: elPath,
158
+ });
159
+ } else {
160
+ changes.push(...classifyChanges(bEls[i], cEls[i], elPath, context));
161
+ }
162
+ }
163
+ break;
164
+ }
165
+
166
+ case "union": {
167
+ const bMembers = (b.members as unknown[]).map((m) => JSON.stringify(m));
168
+ const cMembers = ((c as Record<string, unknown>).members as unknown[]).map((m) => JSON.stringify(m));
169
+ const bSet = new Set(bMembers);
170
+ const cSet = new Set(cMembers);
171
+
172
+ for (const m of bMembers) {
173
+ if (!cSet.has(m)) {
174
+ changes.push({
175
+ functionName: "",
176
+ severity: "breaking",
177
+ description: `Union member removed`,
178
+ path: basePath || "(root)",
179
+ });
180
+ }
181
+ }
182
+ for (const m of cMembers) {
183
+ if (!bSet.has(m)) {
184
+ changes.push({
185
+ functionName: "",
186
+ severity: "non-breaking",
187
+ description: `Union member added`,
188
+ path: basePath || "(root)",
189
+ });
190
+ }
191
+ }
192
+ break;
193
+ }
194
+
195
+ case "map": {
196
+ changes.push(...classifyChanges(b.value, (c as Record<string, unknown>).value, `${basePath}<value>`, context));
197
+ break;
198
+ }
199
+
200
+ case "set": {
201
+ changes.push(...classifyChanges(b.element, (c as Record<string, unknown>).element, `${basePath}<element>`, context));
202
+ break;
203
+ }
204
+
205
+ case "promise": {
206
+ changes.push(...classifyChanges(b.resolved, (c as Record<string, unknown>).resolved, basePath, context));
207
+ break;
208
+ }
209
+
210
+ // unknown, function — no deep diff
211
+ }
212
+
213
+ return changes;
214
+ }
215
+
216
+ export async function checkCommand(opts: CheckOptions): Promise<void> {
217
+ try {
218
+ // Mode 1: Save current snapshot
219
+ if (opts.save) {
220
+ const snapshot = await fetchSnapshot({ env: opts.env });
221
+
222
+ if (snapshot.functions.length === 0) {
223
+ console.error(chalk.yellow("\n No functions observed yet. Run your app first to populate types.\n"));
224
+ process.exit(1);
225
+ }
226
+
227
+ const outPath = path.resolve(opts.save);
228
+ const dir = path.dirname(outPath);
229
+ if (!fs.existsSync(dir)) {
230
+ fs.mkdirSync(dir, { recursive: true });
231
+ }
232
+ fs.writeFileSync(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
233
+
234
+ console.log("");
235
+ console.log(chalk.green(` Baseline saved to ${chalk.bold(opts.save)}`));
236
+ console.log(chalk.gray(` ${snapshot.functions.length} function${snapshot.functions.length !== 1 ? "s" : ""} captured at ${snapshot.createdAt}`));
237
+ console.log("");
238
+ return;
239
+ }
240
+
241
+ // Mode 2: Check against baseline
242
+ if (opts.against) {
243
+ const baselinePath = path.resolve(opts.against);
244
+ if (!fs.existsSync(baselinePath)) {
245
+ console.error(chalk.red(`\n Baseline file not found: ${opts.against}\n`));
246
+ process.exit(1);
247
+ }
248
+
249
+ let baseline: CheckSnapshot;
250
+ try {
251
+ baseline = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
252
+ } catch {
253
+ console.error(chalk.red(`\n Invalid baseline file: ${opts.against}\n`));
254
+ process.exit(1);
255
+ return; // unreachable but satisfies TS
256
+ }
257
+
258
+ const current = await fetchSnapshot({ env: opts.env });
259
+
260
+ if (current.functions.length === 0) {
261
+ console.error(chalk.yellow("\n No functions observed yet. Run your app first to populate types.\n"));
262
+ process.exit(1);
263
+ }
264
+
265
+ console.log("");
266
+ console.log(chalk.white.bold(" trickle check"));
267
+ console.log(chalk.gray(` Baseline: ${opts.against} (${baseline.createdAt})`));
268
+ console.log(chalk.gray(` Current: ${current.functions.length} functions observed`));
269
+ console.log(chalk.gray(" " + "─".repeat(50)));
270
+
271
+ // Build lookup maps
272
+ const baselineMap = new Map<string, SnapshotFunction>();
273
+ for (const fn of baseline.functions) {
274
+ baselineMap.set(fn.name, fn);
275
+ }
276
+
277
+ const currentMap = new Map<string, SnapshotFunction>();
278
+ for (const fn of current.functions) {
279
+ currentMap.set(fn.name, fn);
280
+ }
281
+
282
+ const allChanges: BreakingChange[] = [];
283
+ const removedFunctions: string[] = [];
284
+ const addedFunctions: string[] = [];
285
+
286
+ // Check for removed functions
287
+ for (const [name] of baselineMap) {
288
+ if (!currentMap.has(name)) {
289
+ removedFunctions.push(name);
290
+ allChanges.push({
291
+ functionName: name,
292
+ severity: "breaking",
293
+ description: "Function/route removed entirely",
294
+ path: "(function)",
295
+ });
296
+ }
297
+ }
298
+
299
+ // Check for added functions
300
+ for (const [name] of currentMap) {
301
+ if (!baselineMap.has(name)) {
302
+ addedFunctions.push(name);
303
+ allChanges.push({
304
+ functionName: name,
305
+ severity: "non-breaking",
306
+ description: "New function/route added",
307
+ path: "(function)",
308
+ });
309
+ }
310
+ }
311
+
312
+ // Check for type changes in existing functions
313
+ for (const [name, baselineFn] of baselineMap) {
314
+ const currentFn = currentMap.get(name);
315
+ if (!currentFn) continue;
316
+
317
+ // Compare return types (response)
318
+ const returnChanges = classifyChanges(
319
+ baselineFn.returnType,
320
+ currentFn.returnType,
321
+ "response",
322
+ "response",
323
+ );
324
+ for (const change of returnChanges) {
325
+ change.functionName = name;
326
+ allChanges.push(change);
327
+ }
328
+
329
+ // Compare args types (request)
330
+ const argsChanges = classifyChanges(
331
+ baselineFn.argsType,
332
+ currentFn.argsType,
333
+ "request",
334
+ "request",
335
+ );
336
+ for (const change of argsChanges) {
337
+ change.functionName = name;
338
+ allChanges.push(change);
339
+ }
340
+ }
341
+
342
+ // Separate breaking vs non-breaking
343
+ const breaking = allChanges.filter((c) => c.severity === "breaking");
344
+ const nonBreaking = allChanges.filter((c) => c.severity === "non-breaking");
345
+
346
+ // Display results
347
+ if (breaking.length === 0 && nonBreaking.length === 0) {
348
+ console.log("");
349
+ console.log(chalk.green(" No type changes detected. API is compatible with baseline."));
350
+ console.log("");
351
+ return;
352
+ }
353
+
354
+ if (breaking.length > 0) {
355
+ console.log("");
356
+ console.log(chalk.red.bold(` ${breaking.length} BREAKING CHANGE${breaking.length !== 1 ? "S" : ""}`));
357
+ console.log("");
358
+
359
+ // Group by function
360
+ const grouped = new Map<string, BreakingChange[]>();
361
+ for (const change of breaking) {
362
+ const list = grouped.get(change.functionName) || [];
363
+ list.push(change);
364
+ grouped.set(change.functionName, list);
365
+ }
366
+
367
+ for (const [fnName, changes] of grouped) {
368
+ console.log(chalk.white(` ${fnName}`));
369
+ for (const change of changes) {
370
+ console.log(
371
+ chalk.red(" ✗ ") +
372
+ chalk.gray(change.path) +
373
+ chalk.red(` — ${change.description}`)
374
+ );
375
+ }
376
+ }
377
+ }
378
+
379
+ if (nonBreaking.length > 0) {
380
+ console.log("");
381
+ console.log(chalk.yellow(` ${nonBreaking.length} non-breaking change${nonBreaking.length !== 1 ? "s" : ""}`));
382
+ console.log("");
383
+
384
+ const grouped = new Map<string, BreakingChange[]>();
385
+ for (const change of nonBreaking) {
386
+ const list = grouped.get(change.functionName) || [];
387
+ list.push(change);
388
+ grouped.set(change.functionName, list);
389
+ }
390
+
391
+ for (const [fnName, changes] of grouped) {
392
+ console.log(chalk.white(` ${fnName}`));
393
+ for (const change of changes) {
394
+ console.log(
395
+ chalk.green(" + ") +
396
+ chalk.gray(change.path) +
397
+ chalk.gray(` — ${change.description}`)
398
+ );
399
+ }
400
+ }
401
+ }
402
+
403
+ console.log("");
404
+
405
+ // Summary
406
+ if (breaking.length > 0) {
407
+ console.log(chalk.red.bold(" FAIL") + chalk.red(` — ${breaking.length} breaking change${breaking.length !== 1 ? "s" : ""} detected`));
408
+ console.log("");
409
+ process.exit(1);
410
+ } else {
411
+ console.log(chalk.green.bold(" PASS") + chalk.green(` — ${nonBreaking.length} non-breaking change${nonBreaking.length !== 1 ? "s" : ""}, no breaking changes`));
412
+ console.log("");
413
+ }
414
+
415
+ return;
416
+ }
417
+
418
+ // No flags — show usage
419
+ console.log("");
420
+ console.log(chalk.white.bold(" trickle check") + chalk.gray(" — detect breaking API changes"));
421
+ console.log("");
422
+ console.log(chalk.white(" Save a baseline:"));
423
+ console.log(chalk.cyan(" trickle check --save baseline.json"));
424
+ console.log("");
425
+ console.log(chalk.white(" Check against baseline:"));
426
+ console.log(chalk.cyan(" trickle check --against baseline.json"));
427
+ console.log("");
428
+ console.log(chalk.gray(" Exit code 0 = compatible, exit code 1 = breaking changes"));
429
+ console.log("");
430
+
431
+ } catch (err: unknown) {
432
+ if (err instanceof Error) {
433
+ console.error(chalk.red(`\n Error: ${err.message}\n`));
434
+ }
435
+ process.exit(1);
436
+ }
437
+ }
@@ -0,0 +1,128 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import chalk from "chalk";
4
+ import { fetchCodegen } from "../api-client";
5
+
6
+ export interface CodegenOptions {
7
+ out?: string;
8
+ env?: string;
9
+ python?: boolean;
10
+ client?: boolean;
11
+ handlers?: boolean;
12
+ zod?: boolean;
13
+ reactQuery?: boolean;
14
+ guards?: boolean;
15
+ middleware?: boolean;
16
+ msw?: boolean;
17
+ jsonSchema?: boolean;
18
+ swr?: boolean;
19
+ pydantic?: boolean;
20
+ classValidator?: boolean;
21
+ graphql?: boolean;
22
+ trpc?: boolean;
23
+ axios?: boolean;
24
+ watch?: boolean;
25
+ }
26
+
27
+ export async function codegenCommand(
28
+ functionName: string | undefined,
29
+ opts: CodegenOptions,
30
+ ): Promise<void> {
31
+ const language = opts.python ? "python" : undefined;
32
+ const format = opts.axios ? "axios" : opts.trpc ? "trpc" : opts.graphql ? "graphql" : opts.classValidator ? "class-validator" : opts.pydantic ? "pydantic" : opts.swr ? "swr" : opts.jsonSchema ? "json-schema" : opts.msw ? "msw" : opts.middleware ? "middleware" : opts.guards ? "guards" : opts.reactQuery ? "react-query" : opts.zod ? "zod" : opts.handlers ? "handlers" : opts.client ? "client" : undefined;
33
+
34
+ async function generate(): Promise<string> {
35
+ const result = await fetchCodegen({
36
+ functionName,
37
+ env: opts.env,
38
+ language,
39
+ format,
40
+ });
41
+ return result.types;
42
+ }
43
+
44
+ if (opts.watch) {
45
+ console.log(chalk.gray("\n Watching for type changes (polling every 5s)...\n"));
46
+ console.log(chalk.gray(" Press Ctrl+C to stop.\n"));
47
+
48
+ let lastOutput = "";
49
+
50
+ const poll = async () => {
51
+ try {
52
+ const types = await generate();
53
+ if (types !== lastOutput) {
54
+ lastOutput = types;
55
+ if (opts.out) {
56
+ writeToFile(opts.out, types, opts.python);
57
+ console.log(
58
+ chalk.green(` Updated ${chalk.bold(opts.out)}`) +
59
+ chalk.gray(` at ${new Date().toLocaleTimeString()}`),
60
+ );
61
+ } else {
62
+ console.clear();
63
+ console.log(types);
64
+ }
65
+ }
66
+ } catch (err: unknown) {
67
+ if (err instanceof Error) {
68
+ console.error(chalk.red(` Error: ${err.message}`));
69
+ }
70
+ }
71
+ };
72
+
73
+ await poll();
74
+ const interval = setInterval(poll, 5000);
75
+
76
+ // Keep process alive until Ctrl+C
77
+ process.on("SIGINT", () => {
78
+ clearInterval(interval);
79
+ console.log(chalk.gray("\n Stopped watching.\n"));
80
+ process.exit(0);
81
+ });
82
+
83
+ // Prevent the process from exiting
84
+ await new Promise(() => {});
85
+ return;
86
+ }
87
+
88
+ // One-shot generation
89
+ try {
90
+ const types = await generate();
91
+
92
+ if (opts.out) {
93
+ writeToFile(opts.out, types, opts.python);
94
+ const ext = opts.python ? ".pyi" : ".d.ts";
95
+ console.log("");
96
+ console.log(
97
+ chalk.green(` Types written to ${chalk.bold(opts.out)}`),
98
+ );
99
+ console.log(
100
+ chalk.gray(` ${countInterfaces(types)} type definitions generated.`),
101
+ );
102
+ console.log("");
103
+ } else {
104
+ console.log("");
105
+ console.log(types);
106
+ }
107
+ } catch (err: unknown) {
108
+ if (err instanceof Error) {
109
+ console.error(chalk.red(`\n Error: ${err.message}\n`));
110
+ }
111
+ process.exit(1);
112
+ }
113
+ }
114
+
115
+ function writeToFile(filePath: string, content: string, _python?: boolean): void {
116
+ const resolvedPath = path.resolve(filePath);
117
+ const dir = path.dirname(resolvedPath);
118
+ if (!fs.existsSync(dir)) {
119
+ fs.mkdirSync(dir, { recursive: true });
120
+ }
121
+ fs.writeFileSync(resolvedPath, content, "utf-8");
122
+ }
123
+
124
+ function countInterfaces(types: string): number {
125
+ const tsMatches = types.match(/export (interface|type) /g);
126
+ const pyMatches = types.match(/class \w+\(TypedDict\)/g);
127
+ return (tsMatches?.length ?? 0) + (pyMatches?.length ?? 0);
128
+ }