openapi-ai-generator 0.1.0 → 0.1.2

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/cli.js CHANGED
@@ -26,150 +26,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var import_commander = require("commander");
28
28
 
29
- // src/config.ts
30
- var import_fs = require("fs");
31
- var import_path = require("path");
32
- var import_url = require("url");
33
- var defaults = {
34
- jsdocMode: "context",
35
- cache: true,
36
- cacheDir: ".openapi-cache",
37
- include: ["src/app/api/**/route.ts"],
38
- exclude: []
39
- };
40
- function resolveConfig(config) {
41
- return {
42
- ...defaults,
43
- ...config,
44
- output: {
45
- scalarDocs: false,
46
- scalarPath: "src/app/api/docs/route.ts",
47
- ...config.output
48
- },
49
- openapi: {
50
- description: "",
51
- servers: [],
52
- ...config.openapi
53
- }
54
- };
55
- }
56
- async function loadConfig(configPath) {
57
- const searchPaths = configPath ? [configPath] : [
58
- "openapi-gen.config.ts",
59
- "openapi-gen.config.js",
60
- "openapi-gen.config.mjs",
61
- "openapi-gen.config.cjs"
62
- ];
63
- for (const p of searchPaths) {
64
- const abs = (0, import_path.resolve)(process.cwd(), p);
65
- if ((0, import_fs.existsSync)(abs)) {
66
- const mod = await importConfig(abs);
67
- const config = mod.default ?? mod;
68
- return resolveConfig(config);
69
- }
70
- }
71
- throw new Error(
72
- "No openapi-gen.config.ts found. Create one at your project root."
73
- );
74
- }
75
- async function importConfig(filePath) {
76
- if (filePath.endsWith(".ts")) {
77
- return importTypeScriptConfig(filePath);
78
- }
79
- const url = (0, import_url.pathToFileURL)(filePath).href;
80
- return import(url);
81
- }
82
- async function importTypeScriptConfig(filePath) {
83
- try {
84
- const { register } = await import("module");
85
- const url = (0, import_url.pathToFileURL)(filePath).href;
86
- return await import(url);
87
- } catch {
88
- try {
89
- require("ts-node/register");
90
- return require(filePath);
91
- } catch {
92
- throw new Error(
93
- `Cannot load TypeScript config file: ${filePath}. Install tsx or ts-node, or use a .js config file.`
94
- );
95
- }
96
- }
97
- }
98
-
99
- // src/scanner.ts
100
- var import_fs2 = require("fs");
101
- var import_glob = require("glob");
102
- var import_path2 = require("path");
103
- async function scanRoutes(include, exclude, cwd = process.cwd()) {
104
- const files = await (0, import_glob.glob)(include, {
105
- cwd,
106
- ignore: exclude,
107
- absolute: true
108
- });
109
- return files.map((filePath) => parseRoute(filePath, cwd));
110
- }
111
- function parseRoute(filePath, cwd) {
112
- const relativePath = (0, import_path2.relative)(cwd, filePath);
113
- const sourceCode = (0, import_fs2.readFileSync)(filePath, "utf8");
114
- const urlPath = filePathToUrlPath(relativePath);
115
- const { jsdocComments, hasExactJsdoc, exactPathItem } = extractJsdoc(sourceCode);
116
- return {
117
- filePath,
118
- relativePath,
119
- urlPath,
120
- sourceCode,
121
- jsdocComments,
122
- hasExactJsdoc,
123
- exactPathItem
124
- };
125
- }
126
- function filePathToUrlPath(filePath) {
127
- let path = filePath.replace(/\\/g, "/");
128
- path = path.replace(/^(src\/)?app\//, "");
129
- path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
130
- path = path.replace(/\[([^\]]+)\]/g, (_, param) => {
131
- if (param.startsWith("...")) {
132
- return `{${param.slice(3)}}`;
133
- }
134
- return `{${param}}`;
135
- });
136
- if (!path.startsWith("/")) {
137
- path = "/" + path;
138
- }
139
- return path;
140
- }
141
- function extractJsdoc(sourceCode) {
142
- const jsdocRegex = /\/\*\*([\s\S]*?)\*\//g;
143
- const jsdocComments = [];
144
- let hasExactJsdoc = false;
145
- let exactPathItem;
146
- let match;
147
- while ((match = jsdocRegex.exec(sourceCode)) !== null) {
148
- const comment = match[0];
149
- jsdocComments.push(comment);
150
- if (/@openapi-exact/.test(comment)) {
151
- hasExactJsdoc = true;
152
- const openapiMatch = comment.match(/@openapi\s+([\s\S]*?)(?=\s*\*\/|\s*\*\s*@)/);
153
- if (openapiMatch) {
154
- try {
155
- const jsonStr = openapiMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
156
- exactPathItem = JSON.parse(jsonStr);
157
- } catch {
158
- hasExactJsdoc = false;
159
- }
160
- }
161
- }
162
- }
163
- return { jsdocComments, hasExactJsdoc, exactPathItem };
164
- }
165
-
166
29
  // src/analyzer.ts
167
30
  var import_ai = require("ai");
168
31
 
169
32
  // src/cache.ts
170
33
  var import_crypto = require("crypto");
171
- var import_fs3 = require("fs");
172
- var import_path3 = require("path");
34
+ var import_fs = require("fs");
35
+ var import_path = require("path");
173
36
  function computeHash(content, provider, modelId) {
174
37
  return (0, import_crypto.createHash)("sha256").update(content).update(provider).update(modelId).digest("hex");
175
38
  }
@@ -179,15 +42,15 @@ var RouteCache = class {
179
42
  this.cacheDir = cacheDir;
180
43
  }
181
44
  ensureDir() {
182
- if (!(0, import_fs3.existsSync)(this.cacheDir)) {
183
- (0, import_fs3.mkdirSync)(this.cacheDir, { recursive: true });
45
+ if (!(0, import_fs.existsSync)(this.cacheDir)) {
46
+ (0, import_fs.mkdirSync)(this.cacheDir, { recursive: true });
184
47
  }
185
48
  }
186
49
  get(hash) {
187
- const filePath = (0, import_path3.join)(this.cacheDir, `${hash}.json`);
188
- if (!(0, import_fs3.existsSync)(filePath)) return null;
50
+ const filePath = (0, import_path.join)(this.cacheDir, `${hash}.json`);
51
+ if (!(0, import_fs.existsSync)(filePath)) return null;
189
52
  try {
190
- const entry = JSON.parse((0, import_fs3.readFileSync)(filePath, "utf8"));
53
+ const entry = JSON.parse((0, import_fs.readFileSync)(filePath, "utf8"));
191
54
  return entry.pathItem;
192
55
  } catch {
193
56
  return null;
@@ -200,8 +63,8 @@ var RouteCache = class {
200
63
  pathItem,
201
64
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
202
65
  };
203
- const filePath = (0, import_path3.join)(this.cacheDir, `${hash}.json`);
204
- (0, import_fs3.writeFileSync)(filePath, JSON.stringify(entry, null, 2), "utf8");
66
+ const filePath = (0, import_path.join)(this.cacheDir, `${hash}.json`);
67
+ (0, import_fs.writeFileSync)(filePath, JSON.stringify(entry, null, 2), "utf8");
205
68
  }
206
69
  };
207
70
 
@@ -222,10 +85,11 @@ function createModel(provider) {
222
85
  }
223
86
  function createAzureModel() {
224
87
  const endpoint = requireEnv("AZURE_OPENAI_ENDPOINT");
88
+ const resourceName = endpoint?.match(/https?:\/\/([^.]+)/)?.[1];
225
89
  const apiKey = requireEnv("AZURE_OPENAI_API_KEY");
226
90
  const deployment = requireEnv("AZURE_OPENAI_DEPLOYMENT");
227
91
  const { createAzure } = require("@ai-sdk/azure");
228
- const azure = createAzure({ endpoint, apiKey });
92
+ const azure = createAzure({ resourceName, apiKey });
229
93
  return azure(deployment);
230
94
  }
231
95
  function createOpenAIModel() {
@@ -307,7 +171,25 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
307
171
  };
308
172
  }
309
173
  }
310
- const pathItem = await callLLM(route, options.jsdocMode, getModel());
174
+ let pathItem;
175
+ try {
176
+ pathItem = await callLLM(route, options.jsdocMode, getModel());
177
+ } catch (err) {
178
+ const message = err instanceof Error ? err.message : String(err);
179
+ const isContentFilter = message.includes("content filtering policy") || message.includes("content_filter") || err?.status === 400;
180
+ if (isContentFilter) {
181
+ console.warn(
182
+ `Warning: Content filter blocked response for ${route.urlPath}. Skipping route. Use @openapi-exact JSDoc to provide the spec manually.`
183
+ );
184
+ return {
185
+ urlPath: route.urlPath,
186
+ pathItem: {},
187
+ fromCache: false,
188
+ skippedLLM: false
189
+ };
190
+ }
191
+ throw err;
192
+ }
311
193
  if (cache) {
312
194
  cache.set(hash, pathItem);
313
195
  }
@@ -321,30 +203,40 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
321
203
  function buildPrompt(route, jsdocMode) {
322
204
  const jsDocSection = route.jsdocComments.length > 0 ? `JSDoc COMMENTS (use as ${jsdocMode === "context" ? "additional context" : "primary source"}):
323
205
  ${route.jsdocComments.join("\n\n")}` : "No JSDoc comments found.";
324
- return `You are an OpenAPI 3.1 specification generator. Analyze the following Next.js API route and extract a complete OpenAPI PathItem object.
206
+ return `You are a technical documentation tool that reads existing source code and produces OpenAPI 3.1 documentation data. You do not write or execute code \u2014 you only read and describe it.
207
+
208
+ ## Task
325
209
 
326
- FILE PATH: ${route.relativePath}
327
- INFERRED URL PATH: ${route.urlPath}
210
+ Read the Next.js API route source file below and produce a JSON object that documents its HTTP endpoints according to the OpenAPI 3.1 PathItem schema.
211
+
212
+ ## Route metadata
213
+
214
+ - File: ${route.relativePath}
215
+ - URL path: ${route.urlPath}
216
+
217
+ ## Source file contents
328
218
 
329
- SOURCE CODE:
330
219
  \`\`\`typescript
331
220
  ${route.sourceCode}
332
221
  \`\`\`
333
222
 
334
223
  ${jsDocSection}
335
224
 
336
- Extract the following for EACH exported HTTP method handler (GET, POST, PUT, PATCH, DELETE):
337
- - operationId (camelCase, unique)
338
- - summary (short description)
339
- - description (detailed description)
340
- - path parameters (from URL segments like [id])
341
- - query parameters (from NextRequest.nextUrl.searchParams usage)
342
- - request body schema (from request.json() usage and TypeScript types)
343
- - response schemas (per status code, from NextResponse.json() calls and return types)
344
- - tags (infer from path segments)
345
- - security requirements (if auth middleware or token checks are present)
225
+ ## Instructions
226
+
227
+ For each exported function named GET, POST, PUT, PATCH, or DELETE, document:
228
+ - operationId: a unique camelCase identifier
229
+ - summary: a short one-line description
230
+ - description: a fuller explanation of what the endpoint does
231
+ - parameters: path params from URL segments like {id}, and query params from searchParams usage
232
+ - requestBody: schema inferred from request.json() calls and TypeScript types (POST/PUT/PATCH only)
233
+ - responses: per status code, inferred from NextResponse.json() calls and return type annotations
234
+ - tags: inferred from the URL path segments
235
+ - security: noted if the code checks for auth tokens, session cookies, or middleware guards
346
236
 
347
- Return ONLY a valid JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown, no code blocks. Just the raw JSON object.`;
237
+ ## Output format
238
+
239
+ Return a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text \u2014 only the JSON object.`;
348
240
  }
349
241
  async function callLLM(route, jsdocMode, model) {
350
242
  const prompt = buildPrompt(route, jsdocMode);
@@ -375,9 +267,75 @@ function parsePathItem(text, urlPath) {
375
267
  }
376
268
  }
377
269
 
270
+ // src/config.ts
271
+ var import_fs2 = require("fs");
272
+ var import_path2 = require("path");
273
+ var import_url = require("url");
274
+ var defaults = {
275
+ jsdocMode: "context",
276
+ cache: true,
277
+ cacheDir: ".openapi-cache",
278
+ include: ["src/app/api/**/route.ts"],
279
+ exclude: [],
280
+ envFile: [".env", ".env.local"]
281
+ };
282
+ function resolveConfig(config) {
283
+ let envFile;
284
+ if (config.envFile === false) {
285
+ envFile = false;
286
+ } else if (typeof config.envFile === "string") {
287
+ envFile = [config.envFile];
288
+ } else {
289
+ envFile = config.envFile ?? defaults.envFile;
290
+ }
291
+ return {
292
+ ...defaults,
293
+ ...config,
294
+ envFile,
295
+ output: {
296
+ scalarDocs: false,
297
+ scalarPath: "src/app/api/docs/route.ts",
298
+ ...config.output
299
+ },
300
+ openapi: {
301
+ description: "",
302
+ servers: [],
303
+ ...config.openapi
304
+ }
305
+ };
306
+ }
307
+ async function loadConfig(configPath) {
308
+ const searchPaths = configPath ? [configPath] : [
309
+ "openapi-gen.config.ts",
310
+ "openapi-gen.config.js",
311
+ "openapi-gen.config.mjs",
312
+ "openapi-gen.config.cjs"
313
+ ];
314
+ for (const p of searchPaths) {
315
+ const abs = (0, import_path2.resolve)(process.cwd(), p);
316
+ if ((0, import_fs2.existsSync)(abs)) {
317
+ const mod = await importConfig(abs);
318
+ const config = mod.default ?? mod;
319
+ return resolveConfig(config);
320
+ }
321
+ }
322
+ throw new Error("No openapi-gen.config.ts found. Create one at your project root.");
323
+ }
324
+ async function importConfig(filePath) {
325
+ if (filePath.endsWith(".ts")) {
326
+ return importTypeScriptConfig(filePath);
327
+ }
328
+ const url = (0, import_url.pathToFileURL)(filePath).href;
329
+ return import(url);
330
+ }
331
+ async function importTypeScriptConfig(filePath) {
332
+ require("tsx/cjs");
333
+ return require(filePath);
334
+ }
335
+
378
336
  // src/generator.ts
379
- var import_fs4 = require("fs");
380
- var import_path4 = require("path");
337
+ var import_fs3 = require("fs");
338
+ var import_path3 = require("path");
381
339
  function assembleSpec(config, routes) {
382
340
  const paths = {};
383
341
  for (const route of routes) {
@@ -406,11 +364,11 @@ function writeOutputFiles(config, spec, cwd = process.cwd()) {
406
364
  }
407
365
  }
408
366
  function writeSpecFiles(config, spec, cwd) {
409
- const specRoutePath = (0, import_path4.resolve)(cwd, config.output.specPath);
410
- const specDir = (0, import_path4.dirname)(specRoutePath);
367
+ const specRoutePath = (0, import_path3.resolve)(cwd, config.output.specPath);
368
+ const specDir = (0, import_path3.dirname)(specRoutePath);
411
369
  ensureDir(specDir);
412
- const specJsonPath = (0, import_path4.join)(specDir, "spec.json");
413
- (0, import_fs4.writeFileSync)(specJsonPath, JSON.stringify(spec, null, 2), "utf8");
370
+ const specJsonPath = (0, import_path3.join)(specDir, "spec.json");
371
+ (0, import_fs3.writeFileSync)(specJsonPath, JSON.stringify(spec, null, 2), "utf8");
414
372
  const routeContent = `import spec from './spec.json';
415
373
 
416
374
  export const dynamic = 'force-static';
@@ -419,11 +377,11 @@ export function GET() {
419
377
  return Response.json(spec);
420
378
  }
421
379
  `;
422
- (0, import_fs4.writeFileSync)(specRoutePath, routeContent, "utf8");
380
+ (0, import_fs3.writeFileSync)(specRoutePath, routeContent, "utf8");
423
381
  }
424
382
  function writeScalarRoute(config, cwd) {
425
- const scalarRoutePath = (0, import_path4.resolve)(cwd, config.output.scalarPath);
426
- ensureDir((0, import_path4.dirname)(scalarRoutePath));
383
+ const scalarRoutePath = (0, import_path3.resolve)(cwd, config.output.scalarPath);
384
+ ensureDir((0, import_path3.dirname)(scalarRoutePath));
427
385
  const specUrl = filePathToApiUrl(config.output.specPath);
428
386
  const routeContent = `export const dynamic = 'force-static';
429
387
 
@@ -448,27 +406,101 @@ export function GET() {
448
406
  );
449
407
  }
450
408
  `;
451
- (0, import_fs4.writeFileSync)(scalarRoutePath, routeContent, "utf8");
409
+ (0, import_fs3.writeFileSync)(scalarRoutePath, routeContent, "utf8");
452
410
  }
453
411
  function filePathToApiUrl(filePath) {
454
412
  let path = filePath.replace(/\\/g, "/");
455
413
  path = path.replace(/^(src\/)?app\//, "");
456
414
  path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
457
- if (!path.startsWith("/")) path = "/" + path;
415
+ if (!path.startsWith("/")) path = `/${path}`;
458
416
  return path;
459
417
  }
460
418
  function ensureDir(dir) {
461
- if (!(0, import_fs4.existsSync)(dir)) {
462
- (0, import_fs4.mkdirSync)(dir, { recursive: true });
419
+ if (!(0, import_fs3.existsSync)(dir)) {
420
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
463
421
  }
464
422
  }
465
423
 
424
+ // src/scanner.ts
425
+ var import_fs4 = require("fs");
426
+ var import_path4 = require("path");
427
+ async function scanRoutes(include, exclude, cwd = process.cwd()) {
428
+ const { default: fg } = await import("fast-glob");
429
+ const files = await fg(include, {
430
+ cwd,
431
+ ignore: exclude,
432
+ absolute: true
433
+ });
434
+ return files.map((filePath) => parseRoute(filePath, cwd));
435
+ }
436
+ function parseRoute(filePath, cwd) {
437
+ const relativePath = (0, import_path4.relative)(cwd, filePath);
438
+ const sourceCode = (0, import_fs4.readFileSync)(filePath, "utf8");
439
+ const urlPath = filePathToUrlPath(relativePath);
440
+ const { jsdocComments, hasExactJsdoc, exactPathItem } = extractJsdoc(sourceCode);
441
+ return {
442
+ filePath,
443
+ relativePath,
444
+ urlPath,
445
+ sourceCode,
446
+ jsdocComments,
447
+ hasExactJsdoc,
448
+ exactPathItem
449
+ };
450
+ }
451
+ function filePathToUrlPath(filePath) {
452
+ let path = filePath.replace(/\\/g, "/");
453
+ path = path.replace(/^(src\/)?app\//, "");
454
+ path = path.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
455
+ path = path.replace(/\[([^\]]+)\]/g, (_, param) => {
456
+ if (param.startsWith("...")) {
457
+ return `{${param.slice(3)}}`;
458
+ }
459
+ return `{${param}}`;
460
+ });
461
+ if (!path.startsWith("/")) {
462
+ path = `/${path}`;
463
+ }
464
+ return path;
465
+ }
466
+ function extractJsdoc(sourceCode) {
467
+ const jsdocRegex = /\/\*\*([\s\S]*?)\*\//g;
468
+ const jsdocComments = [];
469
+ let hasExactJsdoc = false;
470
+ let exactPathItem;
471
+ const match = jsdocRegex.exec(sourceCode);
472
+ while (match !== null) {
473
+ const comment = match[0];
474
+ jsdocComments.push(comment);
475
+ if (/@openapi-exact/.test(comment)) {
476
+ hasExactJsdoc = true;
477
+ const openapiMatch = comment.match(/@openapi\s+([\s\S]*?)(?=\s*\*\/|\s*\*\s*@)/);
478
+ if (openapiMatch) {
479
+ try {
480
+ const jsonStr = openapiMatch[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
481
+ exactPathItem = JSON.parse(jsonStr);
482
+ } catch {
483
+ hasExactJsdoc = false;
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return { jsdocComments, hasExactJsdoc, exactPathItem };
489
+ }
490
+
466
491
  // src/index.ts
492
+ var import_node_path = require("path");
467
493
  async function generate(options = {}) {
468
494
  const cwd = options.cwd ?? process.cwd();
469
495
  const config = await loadConfig(options.config);
470
496
  if (options.provider) config.provider = options.provider;
471
497
  if (options.cache === false) config.cache = false;
498
+ if (config.envFile !== false) {
499
+ const { config: dotenvConfig } = await import("dotenv");
500
+ for (const file of config.envFile) {
501
+ dotenvConfig({ path: (0, import_node_path.resolve)(cwd, file), override: false });
502
+ }
503
+ }
472
504
  console.log(`[openapi-ai-generator] Scanning routes...`);
473
505
  const routes = await scanRoutes(config.include, config.exclude, cwd);
474
506
  console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);
@@ -501,10 +533,7 @@ async function generate(options = {}) {
501
533
  // src/cli.ts
502
534
  var program = new import_commander.Command();
503
535
  program.name("openapi-ai-generator").description("Generate OpenAPI 3.1 specs from Next.js API routes using AI").version("0.1.0");
504
- program.command("generate").description("Scan Next.js API routes and generate an OpenAPI spec").option("-c, --config <path>", "Path to config file (default: openapi-gen.config.ts)").option(
505
- "-p, --provider <provider>",
506
- "Override provider (azure | openai | anthropic)"
507
- ).option("--no-cache", "Disable caching (always re-analyze all routes)").action(async (opts) => {
536
+ program.command("generate").description("Scan Next.js API routes and generate an OpenAPI spec").option("-c, --config <path>", "Path to config file (default: openapi-gen.config.ts)").option("-p, --provider <provider>", "Override provider (azure | openai | anthropic)").option("--no-cache", "Disable caching (always re-analyze all routes)").action(async (opts) => {
508
537
  try {
509
538
  const result = await generate({
510
539
  config: opts.config,
@@ -515,7 +544,9 @@ program.command("generate").description("Scan Next.js API routes and generate an
515
544
  \u2713 OpenAPI spec generated successfully`);
516
545
  console.log(` Routes analyzed: ${result.routesAnalyzed}`);
517
546
  console.log(` From cache: ${result.routesFromCache}`);
518
- console.log(` LLM calls made: ${result.routesAnalyzed - result.routesFromCache - (result.routesSkippedLLM - result.routesFromCache)}`);
547
+ console.log(
548
+ ` LLM calls made: ${result.routesAnalyzed - result.routesFromCache - (result.routesSkippedLLM - result.routesFromCache)}`
549
+ );
519
550
  console.log(` Spec written to: ${result.specPath}`);
520
551
  } catch (err) {
521
552
  console.error("[openapi-ai-generator] Error:", err instanceof Error ? err.message : err);