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,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
|
+
}
|