@redocly/cli 2.41.2 → 2.43.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 (73) hide show
  1. package/README.md +15 -0
  2. package/THIRD_PARTY_NOTICES +3 -2
  3. package/lib/chunks/32XCOMOR.js +7 -0
  4. package/lib/chunks/3BCHPW6T.js +22 -0
  5. package/lib/chunks/3VZLNQK3.js +47 -0
  6. package/lib/chunks/4VF6MPJJ.js +11 -0
  7. package/lib/chunks/55GMSQDD.js +7 -0
  8. package/lib/chunks/5E3VKU5T.js +7 -0
  9. package/lib/chunks/AE6JGTY4.js +7 -0
  10. package/lib/chunks/C35TOMVY.js +8 -0
  11. package/lib/chunks/D5T6ETPZ.js +8 -0
  12. package/lib/chunks/DNOXLF53.js +8 -0
  13. package/lib/chunks/E36IFTLX.js +59 -0
  14. package/lib/chunks/ECZUPR2N.js +7 -0
  15. package/lib/chunks/EU6GURJZ.js +7 -0
  16. package/lib/chunks/HXYO6QS2.js +3132 -0
  17. package/lib/chunks/IW4HG6VD.js +19 -0
  18. package/lib/chunks/KIVPQZDC.js +31 -0
  19. package/lib/chunks/KLY5RV3W.js +7 -0
  20. package/lib/chunks/KM7RL6C4.js +2317 -0
  21. package/lib/chunks/L3YH7RCT.js +8 -0
  22. package/lib/chunks/NEWT5A6S.js +159 -0
  23. package/lib/chunks/NGPG6D6W.js +7 -0
  24. package/lib/chunks/O6MCQRCP.js +59 -0
  25. package/lib/chunks/O6PRCLSV.js +13 -0
  26. package/lib/chunks/OFAMJNQ6.js +7 -0
  27. package/lib/chunks/P26PQ2RU.js +7 -0
  28. package/lib/chunks/PFD7KGH7.js +93 -0
  29. package/lib/chunks/QFG7TTKG.js +22 -0
  30. package/lib/chunks/RFWIIJFG.js +7 -0
  31. package/lib/chunks/RS5PUOTT.js +18 -0
  32. package/lib/chunks/TMQJQANP.js +7 -0
  33. package/lib/chunks/UPPWQBGX.js +7 -0
  34. package/lib/chunks/UXH3ZSSV.js +9 -0
  35. package/lib/chunks/UYZXI3NO.js +7 -0
  36. package/lib/chunks/WNTLDT6L.js +184 -0
  37. package/lib/chunks/WZXA5XPB.js +7 -0
  38. package/lib/chunks/X6IKW6K4.js +7 -0
  39. package/lib/index.js +226 -13885
  40. package/package.json +1 -1
  41. package/lib/chunks/3CX2Z7JI.js +0 -496
  42. package/lib/chunks/3UUTQVCJ.js +0 -3614
  43. package/lib/chunks/4FB5ZIPP.js +0 -1133
  44. package/lib/chunks/5ILQMFXK.js +0 -59
  45. package/lib/chunks/7F5O43EU.js +0 -1756
  46. package/lib/chunks/7IX44JLO.js +0 -16
  47. package/lib/chunks/7MOL2QVN.js +0 -92635
  48. package/lib/chunks/A3VFVVHD.js +0 -25
  49. package/lib/chunks/A6JSOHCP.js +0 -41
  50. package/lib/chunks/AIT3U3JT.js +0 -22
  51. package/lib/chunks/ASYQPOQY.js +0 -681
  52. package/lib/chunks/BVP23YP7.js +0 -13374
  53. package/lib/chunks/FO6BDVGE.js +0 -25
  54. package/lib/chunks/GFU5KGSW.js +0 -92
  55. package/lib/chunks/ICPYP2LP.js +0 -401
  56. package/lib/chunks/KPMZLKQG.js +0 -79
  57. package/lib/chunks/KRMBH6JF.js +0 -17902
  58. package/lib/chunks/NLIKC7UP.js +0 -528
  59. package/lib/chunks/NW3XJZXQ.js +0 -175
  60. package/lib/chunks/P6UGG7F6.js +0 -414
  61. package/lib/chunks/RRC5IDC3.js +0 -2416
  62. package/lib/chunks/T6UZU3XM.js +0 -17
  63. package/lib/chunks/TS33ZDKX.js +0 -35
  64. package/lib/chunks/UUU33DK3.js +0 -125
  65. package/lib/chunks/VUDTNFRL.js +0 -42
  66. package/lib/chunks/VXIQEZPR.js +0 -2564
  67. package/lib/chunks/W7G5WVPB.js +0 -44
  68. package/lib/chunks/XB6C62FW.js +0 -18497
  69. package/lib/chunks/XHPY6UJF.js +0 -1638
  70. package/lib/chunks/Y6DTFCLS.js +0 -15
  71. package/lib/chunks/Z2664VV5.js +0 -39331
  72. package/lib/chunks/Z2I5YXYN.js +0 -20272
  73. package/lib/chunks/ZK3QRKL5.js +0 -129
@@ -1,1756 +0,0 @@
1
- import { createRequire as __createRequire } from 'node:module';
2
- const require = __createRequire(import.meta.url);
3
- import {
4
- selectTrafficParser
5
- } from "./P6UGG7F6.js";
6
- import {
7
- getPathWithoutTrailingSlash,
8
- isJsonMime,
9
- isSyntheticHost,
10
- listFilesRecursively,
11
- normalizeContentType,
12
- normalizeFsPath,
13
- normalizeServerPrefix,
14
- resolvePathForServer
15
- } from "./3CX2Z7JI.js";
16
- import {
17
- OPENAPI3_METHOD_NAMES,
18
- Spinner
19
- } from "./KPMZLKQG.js";
20
- import {
21
- exitWithError
22
- } from "./T6UZU3XM.js";
23
- import {
24
- createConfig,
25
- isPlainObject,
26
- lintFromString,
27
- logger,
28
- parseYaml,
29
- pluralize,
30
- stringifyYaml
31
- } from "./Z2664VV5.js";
32
- import "./Z2I5YXYN.js";
33
- import "./5ILQMFXK.js";
34
-
35
- // src/commands/generate-spec/index.ts
36
- import { mkdir, writeFile } from "node:fs/promises";
37
- import path2 from "node:path";
38
-
39
- // src/commands/generate-spec/ai/refine.ts
40
- import * as process2 from "node:process";
41
-
42
- // src/commands/generate-spec/value-inference.ts
43
- var MAX_TRACKED_VALUES = 10;
44
- var MIN_ENUM_OBSERVATIONS = 4;
45
- var MAX_ENUM_VALUES = 5;
46
- var MAX_ENUM_VALUE_LENGTH = 30;
47
- var ENUM_VALUE_RE = /^[a-zA-Z][a-zA-Z0-9]*(?:[_\-.][a-zA-Z0-9]+)*$/;
48
- var HEXISH_RE = /^[0-9a-f-]{8,}$/i;
49
- var FORMAT_MATCHERS = [
50
- ["uuid", /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i],
51
- ["date-time", /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/],
52
- ["date", /^\d{4}-\d{2}-\d{2}$/],
53
- ["email", /^[^\s@]+@[^\s@]+\.[^\s@]+$/],
54
- ["uri", /^https?:\/\/\S+$/],
55
- ["ipv4", /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/]
56
- ];
57
- var observationsByNode = /* @__PURE__ */ new WeakMap();
58
- function observeString(value) {
59
- return {
60
- formats: FORMAT_MATCHERS.filter(([, pattern]) => pattern.test(value)).map(([format]) => format),
61
- values: [value],
62
- count: 1,
63
- overflow: false
64
- };
65
- }
66
- function mergeObservations(a, b) {
67
- if (!a) return b;
68
- if (!b) return a;
69
- const merged = {
70
- formats: a.formats.filter((format) => b.formats.includes(format)),
71
- values: [],
72
- count: a.count + b.count,
73
- overflow: a.overflow || b.overflow
74
- };
75
- if (!merged.overflow) {
76
- const values = [...a.values];
77
- for (const value of b.values) {
78
- if (!values.includes(value)) {
79
- values.push(value);
80
- }
81
- }
82
- if (values.length > MAX_TRACKED_VALUES) {
83
- merged.overflow = true;
84
- } else {
85
- merged.values = values;
86
- }
87
- }
88
- return merged;
89
- }
90
- function setObservations(schema, observations) {
91
- observationsByNode.set(schema, observations);
92
- }
93
- function getObservations(schema) {
94
- return observationsByNode.get(schema);
95
- }
96
- function copyObservations(source, target) {
97
- const observations = observationsByNode.get(source);
98
- if (observations) {
99
- observationsByNode.set(target, observations);
100
- }
101
- }
102
- function isEnumLikeValue(value) {
103
- return value.length <= MAX_ENUM_VALUE_LENGTH && !HEXISH_RE.test(value) && ENUM_VALUE_RE.test(value);
104
- }
105
- function distillObservations(observations, allowEnum) {
106
- if (observations.formats.length > 0) {
107
- return { format: observations.formats[0] };
108
- }
109
- if (allowEnum && !observations.overflow && observations.count >= MIN_ENUM_OBSERVATIONS && observations.values.length > 0 && observations.values.length <= MAX_ENUM_VALUES && observations.values.length * 2 <= observations.count && observations.values.every(isEnumLikeValue)) {
110
- return { enum: [...observations.values].sort() };
111
- }
112
- return {};
113
- }
114
- function applyToSchema(schema) {
115
- const observations = observationsByNode.get(schema);
116
- if (observations) {
117
- const types = typeof schema.type === "string" ? [schema.type] : schema.type ?? [];
118
- if (types.includes("string")) {
119
- Object.assign(schema, distillObservations(observations, types.length === 1));
120
- }
121
- }
122
- for (const property of Object.values(schema.properties ?? {})) {
123
- applyToSchema(property);
124
- }
125
- if (schema.additionalProperties) {
126
- applyToSchema(schema.additionalProperties);
127
- }
128
- if (schema.items) {
129
- applyToSchema(schema.items);
130
- }
131
- for (const member of schema.oneOf ?? []) {
132
- applyToSchema(member);
133
- }
134
- }
135
- function applyValueInference(document) {
136
- for (const pathItem of Object.values(document.paths)) {
137
- for (const operation of Object.values(pathItem)) {
138
- for (const media of Object.values(operation.requestBody?.content ?? {})) {
139
- applyToSchema(media.schema);
140
- }
141
- for (const response of Object.values(operation.responses)) {
142
- for (const media of Object.values(response.content ?? {})) {
143
- applyToSchema(media.schema);
144
- }
145
- }
146
- }
147
- }
148
- for (const schema of Object.values(document.components?.schemas ?? {})) {
149
- applyToSchema(schema);
150
- }
151
- return document;
152
- }
153
-
154
- // src/commands/generate-spec/generator.ts
155
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
156
- var NUMERIC_RE = /^\d+$/;
157
- var ULID_RE = /^[0-9a-hjkmnp-tv-z]{26}$/i;
158
- var PREFIXED_ID_RE = /^[a-z][a-z0-9]*_(?=[0-9a-z]*\d)[0-9a-z]{10,}$/i;
159
- var CUID_RE = /^[a-z](?=[a-z0-9]*\d)[a-z0-9]{23,24}$/;
160
- var HTTP_METHODS = new Set(OPENAPI3_METHOD_NAMES);
161
- var BODYLESS_METHODS = /* @__PURE__ */ new Set(["get", "head"]);
162
- var FORM_URLENCODED_MIME = "application/x-www-form-urlencoded";
163
- var MULTIPART_FORM_MIME = "multipart/form-data";
164
- function looksLikeIdentifier(segment) {
165
- if (!segment) return false;
166
- if (NUMERIC_RE.test(segment)) return true;
167
- if (UUID_RE.test(segment)) return true;
168
- if (ULID_RE.test(segment)) return true;
169
- if (PREFIXED_ID_RE.test(segment)) return true;
170
- if (CUID_RE.test(segment)) return true;
171
- return /^[0-9a-f]{16,}$/i.test(segment);
172
- }
173
- function templatizePath(rawPath) {
174
- const segments = rawPath.split("/");
175
- const params = [];
176
- const used = /* @__PURE__ */ new Set();
177
- const templated = segments.map((segment, index) => {
178
- if (!looksLikeIdentifier(segment)) {
179
- return segment;
180
- }
181
- const previous = segments[index - 1];
182
- let name = previous ? `${pluralize(previous, 1)}Id` : "id";
183
- let suffix = 1;
184
- while (used.has(name)) {
185
- suffix += 1;
186
- name = previous ? `${pluralize(previous, 1)}Id${suffix}` : `id${suffix}`;
187
- }
188
- used.add(name);
189
- params.push({ name, value: segment });
190
- return `{${name}}`;
191
- });
192
- return { template: templated.join("/") || "/", params };
193
- }
194
- function inferSchema(value) {
195
- if (value === null) {
196
- return { type: "null" };
197
- }
198
- if (Array.isArray(value)) {
199
- if (value.length === 0) {
200
- return { type: "array" };
201
- }
202
- const itemSchemas = value.slice(0, 20).map(inferSchema);
203
- return { type: "array", items: mergeSchemas(itemSchemas) };
204
- }
205
- switch (typeof value) {
206
- case "string": {
207
- const schema = { type: "string" };
208
- setObservations(schema, observeString(value));
209
- return schema;
210
- }
211
- case "boolean":
212
- return { type: "boolean" };
213
- case "number":
214
- return Number.isInteger(value) ? { type: "integer" } : { type: "number" };
215
- case "object": {
216
- const entries = Object.entries(value);
217
- if (entries.length > 0) {
218
- const keysAreGeneric = entries.every(([key]) => NUMERIC_RE.test(key)) || entries.every(([key]) => UUID_RE.test(key));
219
- if (keysAreGeneric) {
220
- return {
221
- type: "object",
222
- additionalProperties: mergeSchemas(
223
- entries.map(([, propValue]) => inferSchema(propValue))
224
- )
225
- };
226
- }
227
- }
228
- const properties = {};
229
- const required = [];
230
- for (const [key, propValue] of entries) {
231
- properties[key] = inferSchema(propValue);
232
- required.push(key);
233
- }
234
- const schema = { type: "object", properties };
235
- if (required.length > 0) {
236
- schema.required = required;
237
- }
238
- return schema;
239
- }
240
- default:
241
- return {};
242
- }
243
- }
244
- function mergeSchemas(schemas) {
245
- if (schemas.length === 0) {
246
- return {};
247
- }
248
- const [first, ...rest] = schemas;
249
- let merged = first;
250
- for (const next of rest) {
251
- merged = mergeTwo(merged, next);
252
- }
253
- return merged;
254
- }
255
- function isUnconstrainedSchema(schema) {
256
- return Object.keys(schema).length === 0;
257
- }
258
- function variantKind(variant) {
259
- if (variant.additionalProperties) {
260
- return "map";
261
- }
262
- if (variant.type === "object") {
263
- return "object";
264
- }
265
- if (variant.type === "array") {
266
- return "array";
267
- }
268
- return "scalar";
269
- }
270
- function toVariants(schema) {
271
- if (schema.oneOf) {
272
- return schema.oneOf.flatMap(toVariants);
273
- }
274
- if (Array.isArray(schema.type)) {
275
- return schema.type.map((type) => {
276
- if (type === "object") {
277
- const variant = { type: "object" };
278
- if (schema.properties) {
279
- variant.properties = schema.properties;
280
- }
281
- if (schema.required) {
282
- variant.required = schema.required;
283
- }
284
- if (schema.additionalProperties) {
285
- variant.additionalProperties = schema.additionalProperties;
286
- }
287
- return variant;
288
- }
289
- if (type === "array") {
290
- return schema.items ? { type: "array", items: schema.items } : { type: "array" };
291
- }
292
- const scalar = { type };
293
- if (type === "string") {
294
- copyObservations(schema, scalar);
295
- }
296
- return scalar;
297
- });
298
- }
299
- return [schema];
300
- }
301
- var OBJECT_MERGE_SIMILARITY = 0.5;
302
- var MAX_OBJECT_VARIANTS = 3;
303
- function objectSimilarity(a, b) {
304
- const keysA = Object.keys(a.properties ?? {});
305
- const keysB = Object.keys(b.properties ?? {});
306
- if (keysA.length === 0 || keysB.length === 0) {
307
- return 1;
308
- }
309
- const setB = new Set(keysB);
310
- const shared = keysA.filter((key) => setB.has(key)).length;
311
- return shared / (keysA.length + keysB.length - shared);
312
- }
313
- function areCompatibleVariants(a, b) {
314
- const kind = variantKind(a);
315
- if (kind !== variantKind(b)) {
316
- return false;
317
- }
318
- switch (kind) {
319
- case "scalar":
320
- return a.type === b.type || a.type === "integer" && b.type === "number" || a.type === "number" && b.type === "integer";
321
- case "object":
322
- return objectSimilarity(a, b) >= OBJECT_MERGE_SIMILARITY;
323
- default:
324
- return true;
325
- }
326
- }
327
- function mergeVariantPair(a, b) {
328
- switch (variantKind(a)) {
329
- case "scalar": {
330
- if (a.type !== b.type) {
331
- return { type: "number" };
332
- }
333
- if (a.type === "string") {
334
- const observations = mergeObservations(getObservations(a), getObservations(b));
335
- if (observations) {
336
- const merged = { type: "string" };
337
- setObservations(merged, observations);
338
- return merged;
339
- }
340
- }
341
- return a;
342
- }
343
- case "map":
344
- return {
345
- type: "object",
346
- additionalProperties: mergeTwo(a.additionalProperties ?? {}, b.additionalProperties ?? {})
347
- };
348
- case "object": {
349
- const properties = { ...a.properties ?? {} };
350
- for (const [key, schema] of Object.entries(b.properties ?? {})) {
351
- properties[key] = properties[key] ? mergeTwo(properties[key], schema) : schema;
352
- }
353
- const requiredB = new Set(b.required ?? []);
354
- const required = (a.required ?? []).filter((key) => requiredB.has(key));
355
- const merged = { type: "object", properties };
356
- if (required.length > 0) {
357
- merged.required = required;
358
- }
359
- return merged;
360
- }
361
- case "array": {
362
- if (!a.items || !b.items) {
363
- const items = a.items ?? b.items;
364
- return items ? { type: "array", items } : { type: "array" };
365
- }
366
- return { type: "array", items: mergeTwo(a.items, b.items) };
367
- }
368
- }
369
- }
370
- function foldExcessObjectVariants(variants) {
371
- const result = [...variants];
372
- const objectCount = () => result.filter((v) => variantKind(v) === "object").length;
373
- while (objectCount() > MAX_OBJECT_VARIANTS) {
374
- let bestA = -1;
375
- let bestB = -1;
376
- let bestScore = -1;
377
- for (let i = 0; i < result.length; i++) {
378
- if (variantKind(result[i]) !== "object") continue;
379
- for (let j = i + 1; j < result.length; j++) {
380
- if (variantKind(result[j]) !== "object") continue;
381
- const score = objectSimilarity(result[i], result[j]);
382
- if (score > bestScore) {
383
- bestScore = score;
384
- bestA = i;
385
- bestB = j;
386
- }
387
- }
388
- }
389
- result[bestA] = mergeVariantPair(result[bestA], result[bestB]);
390
- result.splice(bestB, 1);
391
- }
392
- return result;
393
- }
394
- var SCALAR_TYPE_ORDER = ["boolean", "integer", "number", "string", "null"];
395
- function finalizeVariants(variants) {
396
- if (variants.length === 1) {
397
- return variants[0];
398
- }
399
- const structured = [];
400
- const scalarTypes2 = /* @__PURE__ */ new Set();
401
- let stringObservations;
402
- for (const variant of variants) {
403
- if (variantKind(variant) === "scalar" && typeof variant.type === "string") {
404
- scalarTypes2.add(variant.type);
405
- if (variant.type === "string") {
406
- stringObservations = mergeObservations(stringObservations, getObservations(variant));
407
- }
408
- } else {
409
- structured.push(variant);
410
- }
411
- }
412
- const orderedScalars = SCALAR_TYPE_ORDER.filter((type) => scalarTypes2.has(type));
413
- const scalarSchema = (type) => {
414
- const schema = { type };
415
- const includesString = typeof type === "string" ? type === "string" : type.includes("string");
416
- if (includesString && stringObservations) {
417
- setObservations(schema, stringObservations);
418
- }
419
- return schema;
420
- };
421
- if (structured.length === 0) {
422
- return orderedScalars.length === 1 ? scalarSchema(orderedScalars[0]) : scalarSchema(orderedScalars);
423
- }
424
- if (structured.length === 1 && orderedScalars.length === 1 && orderedScalars[0] === "null" && typeof structured[0].type === "string") {
425
- return { ...structured[0], type: [structured[0].type, "null"] };
426
- }
427
- const members = [...structured];
428
- if (orderedScalars.length === 1) {
429
- members.push(scalarSchema(orderedScalars[0]));
430
- } else if (orderedScalars.length > 1) {
431
- members.push(scalarSchema(orderedScalars));
432
- }
433
- return { oneOf: members };
434
- }
435
- function mergeTwo(a, b) {
436
- if (isUnconstrainedSchema(a) || isUnconstrainedSchema(b)) {
437
- return {};
438
- }
439
- const variants = toVariants(a);
440
- for (const candidate of toVariants(b)) {
441
- const index = variants.findIndex((variant) => areCompatibleVariants(variant, candidate));
442
- if (index === -1) {
443
- variants.push(candidate);
444
- } else {
445
- variants[index] = mergeVariantPair(variants[index], candidate);
446
- }
447
- }
448
- return finalizeVariants(foldExcessObjectVariants(variants));
449
- }
450
- function sniffJson(bodyText) {
451
- if (!bodyText) {
452
- return void 0;
453
- }
454
- const trimmed = bodyText.trim();
455
- if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
456
- return void 0;
457
- }
458
- try {
459
- return JSON.parse(trimmed);
460
- } catch {
461
- return void 0;
462
- }
463
- }
464
- function inferMultipartSchema(bodyText, contentType) {
465
- const boundary = contentType?.match(/boundary="?([^";,]+)"?/i)?.[1];
466
- if (!boundary) {
467
- return void 0;
468
- }
469
- const properties = {};
470
- for (const section of bodyText.split(`--${boundary}`)) {
471
- const partMatch = section.match(/^\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)\r?\n$/);
472
- if (!partMatch) {
473
- continue;
474
- }
475
- const [, partHeaders, value] = partMatch;
476
- const fieldName = partHeaders.match(/\bname="([^"]*)"/i)?.[1];
477
- if (fieldName === void 0) {
478
- continue;
479
- }
480
- properties[fieldName] = /\bfilename="/i.test(partHeaders) ? { type: "string", format: "binary" } : inferSchema(value);
481
- }
482
- if (Object.keys(properties).length === 0) {
483
- return void 0;
484
- }
485
- return { type: "object", properties, required: Object.keys(properties) };
486
- }
487
- function inferBodySchema(message) {
488
- const mime = normalizeContentType(message.contentType);
489
- if (message.bodyJson !== void 0) {
490
- return {
491
- mime: isJsonMime(mime) ? mime : "application/json",
492
- schema: inferSchema(message.bodyJson)
493
- };
494
- }
495
- if (mime === FORM_URLENCODED_MIME && message.bodyText) {
496
- const fields = {};
497
- for (const [key, value] of new URLSearchParams(message.bodyText)) {
498
- fields[key] = value;
499
- }
500
- if (Object.keys(fields).length === 0) {
501
- return void 0;
502
- }
503
- return { mime: FORM_URLENCODED_MIME, schema: inferSchema(fields) };
504
- }
505
- if (mime === MULTIPART_FORM_MIME && message.bodyText) {
506
- const schema = inferMultipartSchema(message.bodyText, message.contentType);
507
- return schema ? { mime: MULTIPART_FORM_MIME, schema } : void 0;
508
- }
509
- const sniffed = sniffJson(message.bodyText);
510
- if (sniffed !== void 0) {
511
- return { mime: "application/json", schema: inferSchema(sniffed) };
512
- }
513
- return void 0;
514
- }
515
- function accumulateBodySchema(target, mime, schema) {
516
- const merged = target.get(mime);
517
- target.set(mime, merged ? mergeSchemas([merged, schema]) : schema);
518
- }
519
- async function generateSpecFromTraffic(options) {
520
- const trafficFiles = await listFilesRecursively(options.trafficPath);
521
- if (trafficFiles.length === 0) {
522
- throw new Error("No traffic files found in the provided traffic path.");
523
- }
524
- const server = normalizeServerPrefix(options.server);
525
- const operations = /* @__PURE__ */ new Map();
526
- const observedServers = /* @__PURE__ */ new Set();
527
- let supportedTrafficFileCount = 0;
528
- const observe = (exchange) => {
529
- const method = exchange.request.method.toLowerCase();
530
- if (!HTTP_METHODS.has(method)) {
531
- return;
532
- }
533
- let rawPath = exchange.request.path || "/";
534
- if (server) {
535
- const serverRelativePath = resolvePathForServer(exchange.request, server);
536
- if (serverRelativePath === void 0) {
537
- return;
538
- }
539
- rawPath = serverRelativePath;
540
- } else if (exchange.request.host && !isSyntheticHost(exchange.request.host)) {
541
- observedServers.add(`${exchange.request.protocol}//${exchange.request.host}`);
542
- }
543
- const { template, params } = templatizePath(getPathWithoutTrailingSlash(rawPath));
544
- const key = `${method} ${template}`;
545
- let accumulator = operations.get(key);
546
- if (!accumulator) {
547
- accumulator = {
548
- method,
549
- template,
550
- pathParams: /* @__PURE__ */ new Map(),
551
- queryParams: /* @__PURE__ */ new Map(),
552
- requestBodies: /* @__PURE__ */ new Map(),
553
- responses: /* @__PURE__ */ new Map()
554
- };
555
- operations.set(key, accumulator);
556
- }
557
- for (const { name, value } of params) {
558
- const previous = accumulator.pathParams.get(name);
559
- accumulator.pathParams.set(name, {
560
- numeric: (previous?.numeric ?? true) && NUMERIC_RE.test(value),
561
- observations: mergeObservations(previous?.observations, observeString(value))
562
- });
563
- }
564
- for (const queryKey of new Set(exchange.request.query.keys())) {
565
- const values = exchange.request.query.getAll(queryKey);
566
- const previous = accumulator.queryParams.get(queryKey);
567
- const isNumeric = values.length > 0 && values.every((value) => NUMERIC_RE.test(value));
568
- let observations = previous?.observations;
569
- for (const value of values) {
570
- observations = mergeObservations(observations, observeString(value));
571
- }
572
- accumulator.queryParams.set(queryKey, {
573
- numeric: (previous?.numeric ?? true) && isNumeric,
574
- observations
575
- });
576
- }
577
- if (!BODYLESS_METHODS.has(method)) {
578
- const requestBody = inferBodySchema(exchange.request);
579
- if (requestBody) {
580
- accumulateBodySchema(accumulator.requestBodies, requestBody.mime, requestBody.schema);
581
- }
582
- }
583
- if (exchange.response && exchange.response.status >= 100) {
584
- const status = String(exchange.response.status);
585
- let responseAccumulator = accumulator.responses.get(status);
586
- if (!responseAccumulator) {
587
- responseAccumulator = { bodies: /* @__PURE__ */ new Map() };
588
- accumulator.responses.set(status, responseAccumulator);
589
- }
590
- responseAccumulator.statusText ??= exchange.response.statusText || void 0;
591
- const responseBody = inferBodySchema(exchange.response);
592
- if (responseBody) {
593
- accumulateBodySchema(responseAccumulator.bodies, responseBody.mime, responseBody.schema);
594
- }
595
- }
596
- };
597
- for (const trafficFile of trafficFiles) {
598
- const parser = await selectTrafficParser(trafficFile, options.format);
599
- if (!parser) {
600
- logger.warn(`Skipping traffic file with unrecognized format: ${trafficFile}
601
- `);
602
- continue;
603
- }
604
- supportedTrafficFileCount += 1;
605
- for await (const exchange of parser.parse(trafficFile)) {
606
- observe(exchange);
607
- }
608
- }
609
- if (supportedTrafficFileCount === 0) {
610
- throw new Error("No supported traffic files found to generate an OpenAPI description from.");
611
- }
612
- if (operations.size === 0) {
613
- throw new Error(
614
- server ? `No HTTP exchanges in the traffic matched the server "${server}".` : "No HTTP exchanges were observed in the traffic. Cannot generate an OpenAPI description."
615
- );
616
- }
617
- const serverUrls = server ? [server] : Array.from(observedServers).sort();
618
- return buildDocument(operations, options.title ?? "Generated API", serverUrls);
619
- }
620
- function countOperations(document) {
621
- return Object.values(document.paths).reduce(
622
- (total, pathItem) => total + Object.keys(pathItem).length,
623
- 0
624
- );
625
- }
626
- function parameterSchema(param, allowEnum) {
627
- if (param.numeric) {
628
- return { type: "integer" };
629
- }
630
- const schema = { type: "string" };
631
- if (param.observations) {
632
- Object.assign(schema, distillObservations(param.observations, allowEnum));
633
- }
634
- return schema;
635
- }
636
- function buildDocument(operations, title, serverUrls) {
637
- const paths = {};
638
- for (const accumulator of operations.values()) {
639
- const pathItem = paths[accumulator.template] ??= {};
640
- const parameters = [
641
- ...Array.from(accumulator.pathParams).map(
642
- ([name, param]) => ({
643
- name,
644
- in: "path",
645
- required: true,
646
- schema: parameterSchema(param, false)
647
- })
648
- ),
649
- ...Array.from(accumulator.queryParams).map(
650
- ([name, param]) => ({
651
- name,
652
- in: "query",
653
- required: false,
654
- schema: parameterSchema(param, true)
655
- })
656
- )
657
- ];
658
- const operation = {
659
- operationId: deriveOperationId(accumulator.method, accumulator.template),
660
- responses: {}
661
- };
662
- if (parameters.length > 0) {
663
- operation.parameters = parameters;
664
- }
665
- if (accumulator.requestBodies.size > 0) {
666
- const content = {};
667
- for (const [mime, schema] of accumulator.requestBodies) {
668
- content[mime] = { schema };
669
- }
670
- operation.requestBody = { content };
671
- }
672
- if (accumulator.responses.size === 0) {
673
- operation.responses["default"] = { description: "Observed response" };
674
- } else {
675
- for (const [status, responseAccumulator] of accumulator.responses) {
676
- const response = {
677
- description: responseAccumulator.statusText || "Observed response"
678
- };
679
- if (responseAccumulator.bodies.size > 0) {
680
- const content = {};
681
- for (const [mime, schema] of responseAccumulator.bodies) {
682
- content[mime] = { schema };
683
- }
684
- response.content = content;
685
- }
686
- operation.responses[status] = response;
687
- }
688
- }
689
- pathItem[accumulator.method] = operation;
690
- }
691
- return {
692
- openapi: "3.2.0",
693
- info: { title, version: "1.0.0" },
694
- ...serverUrls.length > 0 ? { servers: serverUrls.map((url) => ({ url })) } : {},
695
- paths
696
- };
697
- }
698
- function deriveOperationId(method, template) {
699
- const cleaned = template.split("/").filter(Boolean).map((segment) => segment.replace(/[{}]/g, "")).join("-");
700
- return `${method}-${cleaned || "root"}`;
701
- }
702
-
703
- // src/commands/generate-spec/samples.ts
704
- function operationSampleKey(method, template) {
705
- return `${method.toUpperCase()} ${template}`;
706
- }
707
- function snapshotBody(message, maxBodyChars) {
708
- let text;
709
- if (message.bodyJson !== void 0) {
710
- try {
711
- text = JSON.stringify(message.bodyJson);
712
- } catch {
713
- text = void 0;
714
- }
715
- }
716
- text ??= message.bodyText;
717
- if (!text) {
718
- return void 0;
719
- }
720
- const trimmed = text.trim();
721
- if (!trimmed) {
722
- return void 0;
723
- }
724
- return trimmed.length > maxBodyChars ? `${trimmed.slice(0, maxBodyChars)}\u2026[truncated]` : trimmed;
725
- }
726
- function shapeSignature(message) {
727
- if (!message) {
728
- return "";
729
- }
730
- const value = message.bodyJson;
731
- if (value === void 0) {
732
- return message.bodyText ? normalizeContentType(message.contentType) || "text" : "";
733
- }
734
- if (value === null) {
735
- return "null";
736
- }
737
- if (Array.isArray(value)) {
738
- return `array<${value.length > 0 ? shapeOfValue(value[0]) : ""}>`;
739
- }
740
- return shapeOfValue(value);
741
- }
742
- function shapeOfValue(value) {
743
- if (isPlainObject(value)) {
744
- return Object.keys(value).sort().join(",");
745
- }
746
- return value === null ? "null" : typeof value;
747
- }
748
- async function collectTrafficSamples(options) {
749
- const perGroup = options.perGroup ?? 2;
750
- const perOperation = options.perOperation ?? 4;
751
- const maxBodyChars = options.maxBodyChars ?? 2e3;
752
- const server = normalizeServerPrefix(options.server);
753
- const trafficFiles = await listFilesRecursively(options.trafficPath);
754
- const operations = /* @__PURE__ */ new Map();
755
- for (const trafficFile of trafficFiles) {
756
- const parser = await selectTrafficParser(trafficFile, options.format);
757
- if (!parser) {
758
- continue;
759
- }
760
- for await (const exchange of parser.parse(trafficFile)) {
761
- const method = exchange.request.method.toUpperCase();
762
- if (!HTTP_METHODS.has(method.toLowerCase())) {
763
- continue;
764
- }
765
- let rawPath = exchange.request.path || "/";
766
- if (server) {
767
- const serverRelativePath = resolvePathForServer(exchange.request, server);
768
- if (serverRelativePath === void 0) {
769
- continue;
770
- }
771
- rawPath = serverRelativePath;
772
- }
773
- const path3 = getPathWithoutTrailingSlash(rawPath);
774
- const { template } = templatizePath(path3);
775
- const response = exchange.response && exchange.response.status >= 100 ? exchange.response : void 0;
776
- const status = response?.status;
777
- const shape = `${shapeSignature(exchange.request)}\u2192${shapeSignature(response)}`;
778
- let groups = operations.get(operationSampleKey(method, template));
779
- if (!groups) {
780
- groups = /* @__PURE__ */ new Map();
781
- operations.set(operationSampleKey(method, template), groups);
782
- }
783
- const groupKey = `${status ?? ""} ${shape}`;
784
- let group = groups.get(groupKey);
785
- if (!group) {
786
- group = [];
787
- groups.set(groupKey, group);
788
- }
789
- if (group.length >= perGroup) {
790
- continue;
791
- }
792
- group.push({
793
- method,
794
- path: path3,
795
- query: exchange.request.query.toString(),
796
- status,
797
- requestContentType: exchange.request.contentType,
798
- requestBody: snapshotBody(exchange.request, maxBodyChars),
799
- responseContentType: response?.contentType,
800
- responseBody: response ? snapshotBody(response, maxBodyChars) : void 0
801
- });
802
- }
803
- }
804
- const samplesByOperation = /* @__PURE__ */ new Map();
805
- for (const [operationKey, groups] of operations) {
806
- const samples = [];
807
- for (let round = 0; round < perGroup && samples.length < perOperation; round++) {
808
- for (const group of groups.values()) {
809
- if (samples.length >= perOperation) {
810
- break;
811
- }
812
- if (group[round]) {
813
- samples.push(group[round]);
814
- }
815
- }
816
- }
817
- samplesByOperation.set(operationKey, samples);
818
- }
819
- return samplesByOperation;
820
- }
821
-
822
- // src/commands/generate-spec/ai/prompt.ts
823
- var SYSTEM_INSTRUCTIONS = `You are an expert API designer. You are given one operation from an OpenAPI 3.2 description that was inferred mechanically from recorded HTTP traffic, the component schemas it references, and a sample of the real recorded HTTP exchanges for that operation. The inferred description is only a hypothesis: types are coarse, every observed property is marked required, and descriptions are missing. Alternative body shapes observed in the traffic appear as "oneOf" variants or type unions. Common string formats (uuid, date-time, date, email, uri, ipv4) and small closed enums are detected conservatively from the observed values.
824
-
825
- Refine the operation into the most accurate OpenAPI 3.2 definition you can justify from the evidence, while staying conservative:
826
- - Keep the path template and the HTTP method exactly as given. Do not rename path parameters, do not move parameters to the path item level, and keep every response status code. The result is programmatically checked and rejected otherwise.
827
- - Keep the operationId unless it is clearly wrong.
828
- - Narrow property types where the samples clearly support it (formats such as date-time, uuid, email, uri; integer vs number; enums when a field only ever takes a small fixed set of values). Verify the formats and enums already detected against the samples; correct or extend them where justified. The detected formats and enums were derived from the full recorded traffic while you see only a subset of it, so never remove a detected format or enum value merely because the samples do not show it \u2014 correct one only when a sample contradicts it.
829
- - Relax "required" for properties that are plausibly optional (e.g. absent in some samples, or clearly nullable). Express nullable values as type unions such as ["string", "null"].
830
- - When a request or response body has alternative shapes \u2014 the baseline shows "oneOf" variants, or the samples show clearly different payloads \u2014 model them explicitly with "oneOf". Name each variant in components/schemas, and add a "discriminator" when a property (such as "type" or "kind") selects the variant.
831
- - Factor structure shared between variants or schemas with "allOf": extract the common base into components/schemas and compose each variant as "allOf" of the base plus its specific properties.
832
- - Keep the $ref references to the component schemas you were given. You may refine the definition of a referenced component when the samples justify it, and you may add new components, but never rename an existing component and never reuse a reserved component name listed in the input.
833
- - Add concise, useful "summary" and "description" fields to the operation, its parameters and important schema properties.
834
- - Add example values drawn from the real samples where helpful, but never copy values that look like secrets or personal data (tokens, API keys, passwords, session identifiers, email addresses, personal names); use clearly fictional placeholder values instead.
835
-
836
- Output ONLY a YAML document with this exact top-level structure:
837
-
838
- paths:
839
- <the path template exactly as given>:
840
- <the method>:
841
- ...the refined operation...
842
- components:
843
- schemas:
844
- ...only the component schemas you changed or added; omit "components" entirely when there are none...
845
-
846
- Do not wrap the output in Markdown code fences. Do not add any commentary before or after the document.`;
847
- function renderSamples(samples) {
848
- if (samples.length === 0) {
849
- return "(no sample exchanges were captured)";
850
- }
851
- return samples.map((sample, index) => {
852
- const lines = [];
853
- const query = sample.query ? `?${sample.query}` : "";
854
- lines.push(`### Exchange ${index + 1}`);
855
- lines.push(`${sample.method} ${sample.path}${query}`);
856
- if (sample.requestContentType) {
857
- lines.push(`Request Content-Type: ${sample.requestContentType}`);
858
- }
859
- if (sample.requestBody) {
860
- lines.push(`Request body: ${sample.requestBody}`);
861
- }
862
- if (sample.status !== void 0) {
863
- lines.push(`Response status: ${sample.status}`);
864
- }
865
- if (sample.responseContentType) {
866
- lines.push(`Response Content-Type: ${sample.responseContentType}`);
867
- }
868
- if (sample.responseBody) {
869
- lines.push(`Response body: ${sample.responseBody}`);
870
- }
871
- return lines.join("\n");
872
- }).join("\n\n");
873
- }
874
- function buildOperationPrompt(options) {
875
- const sections = [];
876
- sections.push(
877
- `# Operation to refine (inferred from traffic)
878
-
879
- \`\`\`yaml
880
- ${stringifyYaml({
881
- paths: { [options.path]: { [options.method]: options.operation } }
882
- })}\`\`\``
883
- );
884
- if (Object.keys(options.components).length > 0) {
885
- sections.push(
886
- `# Component schemas referenced by this operation (current definitions)
887
-
888
- \`\`\`yaml
889
- ${stringifyYaml(
890
- { components: { schemas: options.components } }
891
- )}\`\`\``
892
- );
893
- }
894
- if (options.reservedComponentNames.length > 0) {
895
- sections.push(
896
- `# Reserved component names (used elsewhere in the description; do not redefine or reuse them)
897
-
898
- ${options.reservedComponentNames.join(
899
- ", "
900
- )}`
901
- );
902
- }
903
- sections.push(
904
- `# Recorded HTTP exchanges for this operation
905
-
906
- ${renderSamples(options.samples)}`
907
- );
908
- sections.push(
909
- `Refine ${options.method.toUpperCase()} ${options.path} as instructed and output only the YAML document.`
910
- );
911
- return { system: SYSTEM_INSTRUCTIONS, user: sections.join("\n\n") };
912
- }
913
-
914
- // src/commands/generate-spec/ai/providers.ts
915
- import { spawn } from "node:child_process";
916
- import { mkdtemp } from "node:fs/promises";
917
- import { tmpdir } from "node:os";
918
- import path from "node:path";
919
- var AI_RESPONSE_TIMEOUT_MS = 3e5;
920
- var CliNotFoundError = class extends Error {
921
- };
922
- var emptyCwdPromise;
923
- function getEmptyCwd() {
924
- emptyCwdPromise ??= mkdtemp(path.join(tmpdir(), "redocly-generate-spec-"));
925
- return emptyCwdPromise;
926
- }
927
- async function runCommand(command, args, stdin, env) {
928
- const cwd = await getEmptyCwd();
929
- return new Promise((resolve, reject) => {
930
- const child = spawn(command, args, {
931
- stdio: ["pipe", "pipe", "pipe"],
932
- cwd,
933
- env: { ...process.env, ...env }
934
- });
935
- let stdout = "";
936
- let stderr2 = "";
937
- let timedOut = false;
938
- const timer = setTimeout(() => {
939
- timedOut = true;
940
- child.kill("SIGKILL");
941
- }, AI_RESPONSE_TIMEOUT_MS);
942
- child.stdout.on("data", (chunk) => {
943
- stdout += chunk.toString();
944
- });
945
- child.stderr.on("data", (chunk) => {
946
- stderr2 += chunk.toString();
947
- });
948
- child.on("error", (error) => {
949
- clearTimeout(timer);
950
- if (error.code === "ENOENT") {
951
- reject(
952
- new CliNotFoundError(`Could not find the "${command}" CLI on PATH. Is it installed?`)
953
- );
954
- return;
955
- }
956
- reject(error);
957
- });
958
- child.on("close", (code) => {
959
- clearTimeout(timer);
960
- if (timedOut) {
961
- reject(
962
- new Error(
963
- `The "${command}" CLI did not respond within ${AI_RESPONSE_TIMEOUT_MS / 1e3} seconds.`
964
- )
965
- );
966
- return;
967
- }
968
- resolve({ stdout, stderr: stderr2, code });
969
- });
970
- child.stdin.on("error", () => {
971
- });
972
- child.stdin.end(stdin);
973
- });
974
- }
975
- async function runClaude(request) {
976
- const args = [
977
- "-p",
978
- "--output-format",
979
- "text",
980
- "--tools",
981
- "",
982
- "--strict-mcp-config",
983
- "--setting-sources",
984
- "",
985
- "--no-session-persistence",
986
- "--append-system-prompt",
987
- request.system
988
- ];
989
- if (request.model) {
990
- args.push("--model", request.model);
991
- }
992
- const { stdout, stderr: stderr2, code } = await runCommand("claude", args, request.user, {
993
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
994
- });
995
- if (code !== 0) {
996
- throw new Error(`claude CLI exited with code ${code}: ${stderr2.trim()}`);
997
- }
998
- if (!stdout.trim()) {
999
- throw new Error("claude CLI returned no output.");
1000
- }
1001
- return { text: stdout };
1002
- }
1003
- async function runCodex(request) {
1004
- const args = [
1005
- "exec",
1006
- "--skip-git-repo-check",
1007
- "--sandbox",
1008
- "read-only",
1009
- "-c",
1010
- "mcp_servers={}",
1011
- "-c",
1012
- "project_doc_max_bytes=0"
1013
- ];
1014
- if (request.model) {
1015
- args.push("--model", request.model);
1016
- }
1017
- args.push("-");
1018
- const prompt = `${request.system}
1019
-
1020
- ${request.user}`;
1021
- const { stdout, stderr: stderr2, code } = await runCommand("codex", args, prompt);
1022
- if (code !== 0) {
1023
- throw new Error(`codex CLI exited with code ${code}: ${stderr2.trim()}`);
1024
- }
1025
- if (!stdout.trim()) {
1026
- throw new Error("codex CLI returned no output.");
1027
- }
1028
- return { text: stdout };
1029
- }
1030
- async function runCursor(request) {
1031
- const args = ["-p", "--output-format", "text", "--trust"];
1032
- if (request.model) {
1033
- args.push("--model", request.model);
1034
- }
1035
- const prompt = `${request.system}
1036
-
1037
- ${request.user}`;
1038
- let result;
1039
- try {
1040
- result = await runCommand("cursor-agent", args, prompt);
1041
- } catch (error) {
1042
- if (!(error instanceof CliNotFoundError)) {
1043
- throw error;
1044
- }
1045
- try {
1046
- result = await runCommand("agent", args, prompt);
1047
- } catch (fallbackError) {
1048
- if (fallbackError instanceof CliNotFoundError) {
1049
- throw new CliNotFoundError(
1050
- 'Could not find the Cursor CLI ("cursor-agent" or "agent") on PATH. Is it installed?'
1051
- );
1052
- }
1053
- throw fallbackError;
1054
- }
1055
- }
1056
- const { stdout, stderr: stderr2, code } = result;
1057
- if (code !== 0) {
1058
- throw new Error(`cursor CLI exited with code ${code}: ${stderr2.trim()}`);
1059
- }
1060
- if (!stdout.trim()) {
1061
- throw new Error("cursor CLI returned no output.");
1062
- }
1063
- return { text: stdout };
1064
- }
1065
- async function runProvider(provider, request) {
1066
- switch (provider) {
1067
- case "claude":
1068
- return runClaude(request);
1069
- case "codex":
1070
- return runCodex(request);
1071
- case "cursor":
1072
- return runCursor(request);
1073
- default:
1074
- throw new Error(`Unknown AI provider: ${provider}`);
1075
- }
1076
- }
1077
-
1078
- // src/commands/generate-spec/ai/refine.ts
1079
- function stripCodeFences(text) {
1080
- const trimmed = text.trim();
1081
- const fenceMatch = trimmed.match(/^```(?:ya?ml|json)?\s*\n([\s\S]*?)\n```$/);
1082
- return fenceMatch ? fenceMatch[1] : trimmed;
1083
- }
1084
- var COMPONENT_REF_RE = /^#\/components\/schemas\/(.+)$/;
1085
- function collectComponentRefs(value, into) {
1086
- if (Array.isArray(value)) {
1087
- for (const item of value) {
1088
- collectComponentRefs(item, into);
1089
- }
1090
- return;
1091
- }
1092
- if (!isPlainObject(value)) {
1093
- return;
1094
- }
1095
- if (typeof value.$ref === "string") {
1096
- const refMatch = value.$ref.match(COMPONENT_REF_RE);
1097
- if (refMatch) {
1098
- into.add(refMatch[1]);
1099
- }
1100
- }
1101
- if (isPlainObject(value.discriminator) && isPlainObject(value.discriminator.mapping)) {
1102
- for (const target of Object.values(value.discriminator.mapping)) {
1103
- if (typeof target === "string") {
1104
- const mappingRefMatch = target.match(COMPONENT_REF_RE);
1105
- into.add(mappingRefMatch ? mappingRefMatch[1] : target);
1106
- }
1107
- }
1108
- }
1109
- for (const child of Object.values(value)) {
1110
- collectComponentRefs(child, into);
1111
- }
1112
- }
1113
- function transitivelyReferencedComponents(root, schemas) {
1114
- const referenced = /* @__PURE__ */ new Set();
1115
- const pending = [root];
1116
- while (pending.length > 0) {
1117
- const names = /* @__PURE__ */ new Set();
1118
- collectComponentRefs(pending.pop(), names);
1119
- for (const name of names) {
1120
- if (!referenced.has(name) && name in schemas) {
1121
- referenced.add(name);
1122
- pending.push(schemas[name]);
1123
- }
1124
- }
1125
- }
1126
- return referenced;
1127
- }
1128
- function pruneUnusedComponents(document) {
1129
- const schemas = document.components?.schemas;
1130
- if (!schemas) {
1131
- return;
1132
- }
1133
- const used = transitivelyReferencedComponents(document.paths, schemas);
1134
- for (const name of Object.keys(schemas)) {
1135
- if (!used.has(name)) {
1136
- delete schemas[name];
1137
- }
1138
- }
1139
- if (Object.keys(schemas).length === 0) {
1140
- delete document.components;
1141
- }
1142
- }
1143
- function parseOperationFragment(text, path3, method) {
1144
- const cleaned = stripCodeFences(text);
1145
- let parsed;
1146
- try {
1147
- parsed = parseYaml(cleaned);
1148
- } catch (error) {
1149
- throw new Error(
1150
- `the provider did not return valid YAML: ${error instanceof Error ? error.message : String(error)}`
1151
- );
1152
- }
1153
- if (!isPlainObject(parsed)) {
1154
- throw new Error('the response is not a YAML document with a "paths" object');
1155
- }
1156
- const paths = parsed.paths;
1157
- if (!isPlainObject(paths)) {
1158
- throw new Error('the response is not a YAML document with a "paths" object');
1159
- }
1160
- const pathItem = paths[path3];
1161
- if (!isPlainObject(pathItem)) {
1162
- throw new Error(`the response does not keep the path template "${path3}"`);
1163
- }
1164
- const operation = pathItem[method];
1165
- if (!isPlainObject(operation)) {
1166
- throw new Error(`the response does not contain the ${method.toUpperCase()} operation`);
1167
- }
1168
- const components = parsed.components;
1169
- const schemas = isPlainObject(components) ? components.schemas : void 0;
1170
- return { operation, components: isPlainObject(schemas) ? schemas : {} };
1171
- }
1172
- function missingStatuses(baselineOperation, refinedOperation) {
1173
- const responses = isPlainObject(refinedOperation.responses) ? refinedOperation.responses : {};
1174
- return Object.keys(baselineOperation.responses).filter((status) => !(status in responses));
1175
- }
1176
- async function lintDocumentSource(source, config) {
1177
- const problems = await lintFromString({ source, config });
1178
- const errors = problems.filter((problem) => problem.severity === "error");
1179
- if (errors.length > 0) {
1180
- const summary = errors.slice(0, 5).map((problem) => `${problem.ruleId}: ${problem.message}`).join("; ");
1181
- throw new Error(`the result has ${errors.length} validation problem(s): ${summary}`);
1182
- }
1183
- }
1184
- async function refineOperation(options) {
1185
- const schemas = options.document.components?.schemas ?? {};
1186
- const referenced = transitivelyReferencedComponents(options.operation, schemas);
1187
- const components = {};
1188
- for (const name of referenced) {
1189
- components[name] = schemas[name];
1190
- }
1191
- const reservedComponentNames = Object.keys(schemas).filter((name) => !referenced.has(name));
1192
- const { system, user } = buildOperationPrompt({
1193
- path: options.path,
1194
- method: options.method,
1195
- operation: options.operation,
1196
- components,
1197
- reservedComponentNames,
1198
- samples: options.samples
1199
- });
1200
- const { text } = await runProvider(options.provider, { system, user, model: options.model });
1201
- const fragment = parseOperationFragment(text, options.path, options.method);
1202
- const dropped = missingStatuses(options.operation, fragment.operation);
1203
- if (dropped.length > 0) {
1204
- throw new Error(`the response dropped observed response status(es): ${dropped.join(", ")}`);
1205
- }
1206
- if (typeof fragment.operation.operationId !== "string") {
1207
- throw new Error("the response dropped the operationId");
1208
- }
1209
- const redefined = Object.keys(fragment.components).filter(
1210
- (name) => reservedComponentNames.includes(name)
1211
- );
1212
- if (redefined.length > 0) {
1213
- throw new Error(`the response redefines reserved component(s): ${redefined.join(", ")}`);
1214
- }
1215
- const fragmentDocument = {
1216
- openapi: options.document.openapi,
1217
- info: options.document.info,
1218
- paths: { [options.path]: { [options.method]: fragment.operation } },
1219
- components: { schemas: { ...schemas, ...fragment.components } }
1220
- };
1221
- await lintDocumentSource(stringifyYaml(fragmentDocument), options.config);
1222
- return fragment;
1223
- }
1224
- function applyFragment(document, path3, method, fragment) {
1225
- document.paths[path3][method] = fragment.operation;
1226
- if (Object.keys(fragment.components).length > 0) {
1227
- document.components ??= { schemas: {} };
1228
- Object.assign(document.components.schemas, fragment.components);
1229
- }
1230
- }
1231
- function finishProgress(spinner) {
1232
- spinner.stop();
1233
- if (process2.stderr.isTTY) {
1234
- logger.info("\x1B[2K");
1235
- }
1236
- }
1237
- async function refineSpecWithAi(options) {
1238
- const document = structuredClone(options.baseline);
1239
- const config = await createConfig({ extends: ["spec"] });
1240
- const operations = [];
1241
- for (const [path3, pathItem] of Object.entries(options.baseline.paths)) {
1242
- for (const [method, operation] of Object.entries(pathItem)) {
1243
- operations.push({ path: path3, method, operation });
1244
- }
1245
- }
1246
- const spinner = new Spinner();
1247
- const inFlight = /* @__PURE__ */ new Set();
1248
- let completed = 0;
1249
- let refined = 0;
1250
- const updateSpinner = () => {
1251
- const [firstLabel] = inFlight;
1252
- if (!firstLabel) {
1253
- return;
1254
- }
1255
- const others = inFlight.size > 1 ? ` (+${inFlight.size - 1} more)` : "";
1256
- spinner.start(`[${completed + 1}/${operations.length}] Refining ${firstLabel}${others}`);
1257
- };
1258
- let nextIndex = 0;
1259
- let aborted = false;
1260
- const worker = async () => {
1261
- while (!aborted && nextIndex < operations.length) {
1262
- const { path: path3, method, operation } = operations[nextIndex++];
1263
- const label = `${method.toUpperCase()} ${path3}`;
1264
- const startedAt = Date.now();
1265
- inFlight.add(label);
1266
- updateSpinner();
1267
- try {
1268
- const fragment = await refineOperation({
1269
- provider: options.provider,
1270
- model: options.model,
1271
- document,
1272
- path: path3,
1273
- method,
1274
- operation,
1275
- samples: options.samplesByOperation.get(operationSampleKey(method, path3)) ?? [],
1276
- config
1277
- });
1278
- applyFragment(document, path3, method, fragment);
1279
- refined += 1;
1280
- completed += 1;
1281
- inFlight.delete(label);
1282
- finishProgress(spinner);
1283
- logger.info(
1284
- `[${completed}/${operations.length}] ${label} \u2014 refined (${Math.round(
1285
- (Date.now() - startedAt) / 1e3
1286
- )}s)
1287
- `
1288
- );
1289
- } catch (error) {
1290
- completed += 1;
1291
- inFlight.delete(label);
1292
- finishProgress(spinner);
1293
- if (error instanceof CliNotFoundError) {
1294
- aborted = true;
1295
- throw error;
1296
- }
1297
- logger.warn(
1298
- `[${completed}/${operations.length}] ${label} \u2014 kept the baseline: ${error instanceof Error ? error.message : String(error)}
1299
- `
1300
- );
1301
- }
1302
- updateSpinner();
1303
- }
1304
- };
1305
- const workers = Math.max(1, Math.min(options.concurrency ?? 1, operations.length));
1306
- for (const result of await Promise.allSettled(Array.from({ length: workers }, worker))) {
1307
- if (result.status === "rejected") {
1308
- throw result.reason;
1309
- }
1310
- }
1311
- if (refined === 0) {
1312
- throw new Error(
1313
- `the ${options.provider} provider did not produce a usable refinement for any operation`
1314
- );
1315
- }
1316
- pruneUnusedComponents(document);
1317
- const yaml = stringifyYaml(document);
1318
- await lintDocumentSource(yaml, config);
1319
- return { yaml, document, refined, total: operations.length };
1320
- }
1321
-
1322
- // src/commands/generate-spec/components.ts
1323
- var MIN_COMPONENT_PROPERTIES = 2;
1324
- var MIN_DISTINCT_CONTEXTS = 2;
1325
- var NEAR_DUPLICATE_SIMILARITY = 0.75;
1326
- var MAX_ENVELOPE_PROPERTIES = 2;
1327
- var NUMERIC_TYPES = /* @__PURE__ */ new Set(["integer", "number"]);
1328
- var shapeKeyCache = /* @__PURE__ */ new WeakMap();
1329
- function shapeKey(schema) {
1330
- const cached = shapeKeyCache.get(schema);
1331
- if (cached !== void 0) {
1332
- return cached;
1333
- }
1334
- let key;
1335
- if (schema.$ref !== void 0) {
1336
- key = `ref(${schema.$ref})`;
1337
- } else if (schema.oneOf) {
1338
- key = `oneOf(${schema.oneOf.map(shapeKey).sort().join("|")})`;
1339
- } else {
1340
- const type = Array.isArray(schema.type) ? [...schema.type].sort().join(",") : schema.type;
1341
- const parts = [`type=${type ?? ""}`];
1342
- if (schema.properties) {
1343
- const properties = Object.entries(schema.properties).map(([name, property]) => `${JSON.stringify(name)}:${shapeKey(property)}`).sort().join(",");
1344
- parts.push(`props{${properties}}`);
1345
- }
1346
- if (schema.additionalProperties) {
1347
- parts.push(`values(${shapeKey(schema.additionalProperties)})`);
1348
- }
1349
- if (schema.items) {
1350
- parts.push(`items(${shapeKey(schema.items)})`);
1351
- }
1352
- key = parts.join(";");
1353
- }
1354
- shapeKeyCache.set(schema, key);
1355
- return key;
1356
- }
1357
- function isExtractionCandidate(schema) {
1358
- return schema.$ref === void 0 && schema.type === "object" && schema.oneOf === void 0 && schema.additionalProperties === void 0 && Object.keys(schema.properties ?? {}).length >= MIN_COMPONENT_PROPERTIES;
1359
- }
1360
- function addVotes(votes, hints) {
1361
- for (const hint of hints) {
1362
- votes.set(hint, (votes.get(hint) ?? 0) + 1);
1363
- }
1364
- }
1365
- function collectShapes(schema, context, groups) {
1366
- if (schema.oneOf) {
1367
- schema.oneOf.forEach(
1368
- (member, index) => collectShapes(
1369
- member,
1370
- { ...context, path: `${context.path}/oneOf/${index}`, fallbackHints: [] },
1371
- groups
1372
- )
1373
- );
1374
- return;
1375
- }
1376
- let childContainer = context.container;
1377
- let childPathPrefix = `${context.path}/`;
1378
- if (isExtractionCandidate(schema)) {
1379
- const key = shapeKey(schema);
1380
- let group = groups.get(key);
1381
- if (!group) {
1382
- group = {
1383
- keys: [key],
1384
- schemas: [],
1385
- merged: {},
1386
- contexts: /* @__PURE__ */ new Set(),
1387
- primaryVotes: /* @__PURE__ */ new Map(),
1388
- fallbackVotes: /* @__PURE__ */ new Map()
1389
- };
1390
- groups.set(key, group);
1391
- }
1392
- group.schemas.push(schema);
1393
- group.contexts.add(`${context.container}#${context.path}`);
1394
- addVotes(group.primaryVotes, context.primaryHints);
1395
- addVotes(group.fallbackVotes, context.fallbackHints);
1396
- childContainer = key;
1397
- childPathPrefix = "";
1398
- }
1399
- for (const [name, property] of Object.entries(schema.properties ?? {})) {
1400
- collectShapes(
1401
- property,
1402
- {
1403
- container: childContainer,
1404
- path: `${childPathPrefix}${name}`,
1405
- primaryHints: [name],
1406
- fallbackHints: []
1407
- },
1408
- groups
1409
- );
1410
- }
1411
- if (schema.additionalProperties) {
1412
- collectShapes(
1413
- schema.additionalProperties,
1414
- {
1415
- container: childContainer,
1416
- path: `${childPathPrefix}additionalProperties`,
1417
- primaryHints: context.primaryHints.map((hint) => pluralize(hint, 1)),
1418
- fallbackHints: context.fallbackHints.map((hint) => pluralize(hint, 1))
1419
- },
1420
- groups
1421
- );
1422
- }
1423
- if (schema.items) {
1424
- collectShapes(
1425
- schema.items,
1426
- {
1427
- container: childContainer,
1428
- path: `${childPathPrefix}items`,
1429
- primaryHints: context.primaryHints.map((hint) => pluralize(hint, 1)),
1430
- fallbackHints: context.fallbackHints.map((hint) => pluralize(hint, 1))
1431
- },
1432
- groups
1433
- );
1434
- }
1435
- }
1436
- function lastStaticSegment(template) {
1437
- const staticSegments = template.split("/").filter((segment) => segment && !segment.startsWith("{"));
1438
- return staticSegments.at(-1);
1439
- }
1440
- function collectGroups(document) {
1441
- const groups = /* @__PURE__ */ new Map();
1442
- for (const [template, pathItem] of Object.entries(document.paths)) {
1443
- const entity = lastStaticSegment(template);
1444
- const base = entity ? pluralize(entity, 1) : void 0;
1445
- for (const [method, operation] of Object.entries(pathItem)) {
1446
- for (const [mime, media] of Object.entries(operation.requestBody?.content ?? {})) {
1447
- collectShapes(
1448
- media.schema,
1449
- {
1450
- container: `request ${method} ${template} ${mime}`,
1451
- path: "",
1452
- primaryHints: base ? [base] : [],
1453
- fallbackHints: base ? [`${base} request`] : []
1454
- },
1455
- groups
1456
- );
1457
- }
1458
- for (const [status, response] of Object.entries(operation.responses)) {
1459
- const isError = Number(status) >= 400;
1460
- for (const [mime, media] of Object.entries(response.content ?? {})) {
1461
- collectShapes(
1462
- media.schema,
1463
- {
1464
- container: `response ${method} ${template} ${status} ${mime}`,
1465
- path: "",
1466
- primaryHints: isError ? ["error"] : base ? [base] : [],
1467
- fallbackHints: isError ? base ? [`${base} error`] : [] : base ? [`${base} response`] : []
1468
- },
1469
- groups
1470
- );
1471
- }
1472
- }
1473
- }
1474
- }
1475
- return groups;
1476
- }
1477
- function propertyNames(schema) {
1478
- return Object.keys(schema.properties ?? {});
1479
- }
1480
- function nameSimilarity(a, b) {
1481
- const namesA = propertyNames(a);
1482
- const namesB = new Set(propertyNames(b));
1483
- const shared = namesA.filter((name) => namesB.has(name)).length;
1484
- const unionSize = namesA.length + namesB.size - shared;
1485
- return unionSize === 0 ? 0 : shared / unionSize;
1486
- }
1487
- function scalarTypes(schema) {
1488
- if (schema.$ref !== void 0 || schema.properties || schema.additionalProperties || schema.items || schema.oneOf) {
1489
- return void 0;
1490
- }
1491
- const types = typeof schema.type === "string" ? [schema.type] : schema.type;
1492
- if (!types || types.some((type) => type === "object" || type === "array")) {
1493
- return void 0;
1494
- }
1495
- return types;
1496
- }
1497
- function isUnconstrained(schema) {
1498
- return Object.keys(schema).length === 0;
1499
- }
1500
- function compatibleProperty(a, b) {
1501
- if (shapeKey(a) === shapeKey(b)) {
1502
- return true;
1503
- }
1504
- if (isUnconstrained(a) || isUnconstrained(b)) {
1505
- return true;
1506
- }
1507
- const scalarsA = scalarTypes(a);
1508
- const scalarsB = scalarTypes(b);
1509
- if (scalarsA && scalarsB) {
1510
- const typesA = new Set(scalarsA.filter((type) => type !== "null"));
1511
- const typesB = new Set(scalarsB.filter((type) => type !== "null"));
1512
- if (typesA.size === typesB.size && [...typesA].every((type) => typesB.has(type))) {
1513
- return true;
1514
- }
1515
- return [...typesA].every((type) => NUMERIC_TYPES.has(type)) && [...typesB].every((type) => NUMERIC_TYPES.has(type));
1516
- }
1517
- const isArrayLike = (schema) => schema.type === "array" || schema.items !== void 0;
1518
- if (isArrayLike(a) && isArrayLike(b)) {
1519
- return !a.items || !b.items || compatibleProperty(a.items, b.items);
1520
- }
1521
- return false;
1522
- }
1523
- function canUnify(a, b) {
1524
- const propertiesB = b.merged.properties ?? {};
1525
- return Object.entries(a.merged.properties ?? {}).every(
1526
- ([name, property]) => !(name in propertiesB) || compatibleProperty(property, propertiesB[name])
1527
- );
1528
- }
1529
- function unifyNearDuplicates(groups) {
1530
- const result = [...groups];
1531
- while (result.length > 1) {
1532
- let bestA = -1;
1533
- let bestB = -1;
1534
- let bestScore = 0;
1535
- for (let i = 0; i < result.length; i++) {
1536
- for (let j = i + 1; j < result.length; j++) {
1537
- const score = nameSimilarity(result[i].merged, result[j].merged);
1538
- if (score >= NEAR_DUPLICATE_SIMILARITY && score > bestScore && canUnify(result[i], result[j])) {
1539
- bestScore = score;
1540
- bestA = i;
1541
- bestB = j;
1542
- }
1543
- }
1544
- }
1545
- if (bestA === -1) {
1546
- break;
1547
- }
1548
- const target = result[bestA];
1549
- const source = result[bestB];
1550
- target.keys.push(...source.keys);
1551
- target.schemas.push(...source.schemas);
1552
- target.merged = mergeSchemas([target.merged, source.merged]);
1553
- for (const context of source.contexts) {
1554
- target.contexts.add(context);
1555
- }
1556
- for (const [hint, votes] of source.primaryVotes) {
1557
- target.primaryVotes.set(hint, (target.primaryVotes.get(hint) ?? 0) + votes);
1558
- }
1559
- for (const [hint, votes] of source.fallbackVotes) {
1560
- target.fallbackVotes.set(hint, (target.fallbackVotes.get(hint) ?? 0) + votes);
1561
- }
1562
- result.splice(bestB, 1);
1563
- }
1564
- return result;
1565
- }
1566
- function compareStrings(a, b) {
1567
- return a < b ? -1 : a > b ? 1 : 0;
1568
- }
1569
- function pascalCase(value) {
1570
- return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join("");
1571
- }
1572
- function rankHints(votes) {
1573
- return [...votes.entries()].sort(([hintA, votesA], [hintB, votesB]) => votesB - votesA || compareStrings(hintA, hintB)).map(([hint]) => hint);
1574
- }
1575
- function assignNames(groups) {
1576
- const ordered = [...groups].sort(
1577
- (a, b) => b.contexts.size - a.contexts.size || b.schemas.length - a.schemas.length || compareStrings(a.keys[0], b.keys[0])
1578
- );
1579
- const used = /* @__PURE__ */ new Set();
1580
- return ordered.map((group) => {
1581
- const candidates = [...rankHints(group.primaryVotes), ...rankHints(group.fallbackVotes)].map(pascalCase).filter((candidate) => candidate.length > 0);
1582
- if (candidates.length === 0) {
1583
- candidates.push("Schema");
1584
- }
1585
- let name = candidates.find((candidate) => !used.has(candidate));
1586
- for (let suffix = 2; name === void 0; suffix++) {
1587
- const numbered = `${candidates[0]}${suffix}`;
1588
- if (!used.has(numbered)) {
1589
- name = numbered;
1590
- }
1591
- }
1592
- used.add(name);
1593
- return { group, name };
1594
- });
1595
- }
1596
- function withRefs(schema, refs, isComponentRoot = false) {
1597
- if (!isComponentRoot) {
1598
- const name = refs.get(shapeKey(schema));
1599
- if (name !== void 0) {
1600
- return { $ref: `#/components/schemas/${name}` };
1601
- }
1602
- }
1603
- const result = { ...schema };
1604
- copyObservations(schema, result);
1605
- if (schema.properties) {
1606
- result.properties = Object.fromEntries(
1607
- Object.entries(schema.properties).map(([name, property]) => [name, withRefs(property, refs)])
1608
- );
1609
- }
1610
- if (schema.additionalProperties) {
1611
- result.additionalProperties = withRefs(schema.additionalProperties, refs);
1612
- }
1613
- if (schema.items) {
1614
- result.items = withRefs(schema.items, refs);
1615
- }
1616
- if (schema.oneOf) {
1617
- result.oneOf = schema.oneOf.map((member) => withRefs(member, refs));
1618
- }
1619
- return result;
1620
- }
1621
- function isUnobservedListEnvelope(schema) {
1622
- const properties = Object.values(schema.properties ?? {});
1623
- const unobservedArrays = properties.filter(
1624
- (property) => property.type === "array" && (!property.items || isUnconstrained(property.items))
1625
- );
1626
- return unobservedArrays.length > 0 && properties.length - unobservedArrays.length <= MAX_ENVELOPE_PROPERTIES;
1627
- }
1628
- function hasDominantNameHint(group) {
1629
- let total = 0;
1630
- let top = 0;
1631
- for (const count of group.primaryVotes.values()) {
1632
- total += count;
1633
- top = Math.max(top, count);
1634
- }
1635
- return top * 2 > total;
1636
- }
1637
- function extractSchemaComponents(document) {
1638
- const groups = collectGroups(document);
1639
- for (const group of groups.values()) {
1640
- group.merged = mergeSchemas(group.schemas);
1641
- }
1642
- const unified = unifyNearDuplicates([...groups.values()]);
1643
- const extracted = unified.filter(
1644
- (group) => group.contexts.size >= MIN_DISTINCT_CONTEXTS && (hasDominantNameHint(group) || !group.schemas.some(isUnobservedListEnvelope))
1645
- );
1646
- if (extracted.length === 0) {
1647
- return document;
1648
- }
1649
- const named = assignNames(extracted);
1650
- const refs = /* @__PURE__ */ new Map();
1651
- for (const { group, name } of named) {
1652
- for (const key of group.keys) {
1653
- refs.set(key, name);
1654
- }
1655
- }
1656
- const schemas = {};
1657
- for (const { group, name } of [...named].sort((a, b) => compareStrings(a.name, b.name))) {
1658
- schemas[name] = withRefs(group.merged, refs, true);
1659
- }
1660
- for (const pathItem of Object.values(document.paths)) {
1661
- for (const operation of Object.values(pathItem)) {
1662
- for (const media of Object.values(operation.requestBody?.content ?? {})) {
1663
- media.schema = withRefs(media.schema, refs);
1664
- }
1665
- for (const response of Object.values(operation.responses)) {
1666
- for (const media of Object.values(response.content ?? {})) {
1667
- media.schema = withRefs(media.schema, refs);
1668
- }
1669
- }
1670
- }
1671
- }
1672
- document.components = { schemas };
1673
- return document;
1674
- }
1675
-
1676
- // src/commands/generate-spec/index.ts
1677
- async function writeOutput(outputPath, contents) {
1678
- const normalized = normalizeFsPath(outputPath);
1679
- await mkdir(path2.dirname(normalized), { recursive: true });
1680
- await writeFile(normalized, contents, "utf8");
1681
- }
1682
- function formatElapsed(milliseconds) {
1683
- const totalSeconds = Math.round(milliseconds / 1e3);
1684
- const minutes = Math.floor(totalSeconds / 60);
1685
- const seconds = totalSeconds % 60;
1686
- return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
1687
- }
1688
- async function handleGenerateSpec({ argv }) {
1689
- if (argv.type !== "openapi") {
1690
- return exitWithError(`Unsupported spec type "${argv.type}". Only "openapi" is supported.`);
1691
- }
1692
- const startedAt = Date.now();
1693
- const trafficPath = normalizeFsPath(argv.traffic);
1694
- const trafficFormat = argv["traffic-format"];
1695
- const baseline = applyValueInference(
1696
- extractSchemaComponents(
1697
- await generateSpecFromTraffic({
1698
- trafficPath,
1699
- format: trafficFormat,
1700
- title: argv.title,
1701
- server: argv.server
1702
- })
1703
- )
1704
- );
1705
- logger.info(
1706
- `Inferred a baseline OpenAPI description from traffic: ${countOperations(
1707
- baseline
1708
- )} operation(s).
1709
- `
1710
- );
1711
- let resultYaml = stringifyYaml(baseline);
1712
- if (argv["with-ai"]) {
1713
- const provider = argv["ai-provider"];
1714
- logger.info(`Refining the description with AI provider "${provider}"...
1715
- `);
1716
- logger.warn(
1717
- "Note: --with-ai sends samples of the recorded traffic (URLs, query strings, request and response bodies) to the selected AI provider. Make sure the traffic contains no secrets or personal data you are not allowed to share.\n"
1718
- );
1719
- const samplesByOperation = await collectTrafficSamples({
1720
- trafficPath,
1721
- format: trafficFormat,
1722
- server: argv.server
1723
- });
1724
- try {
1725
- const refined = await refineSpecWithAi({
1726
- provider,
1727
- model: argv["ai-model"],
1728
- baseline,
1729
- samplesByOperation,
1730
- concurrency: argv["ai-concurrency"]
1731
- });
1732
- resultYaml = refined.yaml;
1733
- logger.info(
1734
- `AI refinement complete: ${refined.refined} of ${refined.total} operation(s) refined (${provider}).
1735
- `
1736
- );
1737
- } catch (error) {
1738
- logger.warn(
1739
- `AI refinement failed, falling back to the baseline description: ${error instanceof Error ? error.message : String(error)}
1740
- `
1741
- );
1742
- }
1743
- }
1744
- if (argv.output) {
1745
- await writeOutput(argv.output, resultYaml);
1746
- logger.info(`Written to: ${normalizeFsPath(argv.output)}
1747
- `);
1748
- } else {
1749
- logger.output(resultYaml);
1750
- }
1751
- logger.info(`Done in ${formatElapsed(Date.now() - startedAt)}.
1752
- `);
1753
- }
1754
- export {
1755
- handleGenerateSpec
1756
- };