zero-doc 1.0.4 → 1.0.6
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/README.md +1 -1
- package/dist/ai-analyzer.d.ts +4 -8
- package/dist/ai-analyzer.js +274 -263
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +86 -151
- package/dist/config.json +1 -1
- package/package.json +4 -1
- package/scripts/inject-config.js +25 -19
- package/viewer/src/App.tsx +8 -45
- package/viewer/src/components/CodeSnippet.tsx +0 -1
- package/viewer/vite.config.ts +17 -1
package/dist/ai-analyzer.js
CHANGED
|
@@ -34,23 +34,19 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AIAnalyzer = void 0;
|
|
37
|
-
const generative_ai_1 = require("@google/generative-ai");
|
|
38
37
|
const fs = __importStar(require("fs"));
|
|
39
38
|
const path = __importStar(require("path"));
|
|
40
39
|
const glob_1 = require("glob");
|
|
41
40
|
class AIAnalyzer {
|
|
42
41
|
constructor(options) {
|
|
43
|
-
this.
|
|
44
|
-
this.model = this.genAI.getGenerativeModel({ model: 'gemini-3-flash-preview' });
|
|
42
|
+
this.apiUrl = options.apiUrl;
|
|
45
43
|
this.projectRoot = options.projectRoot;
|
|
46
44
|
this.filePatterns = options.filePatterns;
|
|
47
45
|
}
|
|
48
46
|
async analyzeProject() {
|
|
49
|
-
// Find all relevant files
|
|
50
47
|
const files = await this.findProjectFiles();
|
|
51
|
-
// Read and analyze files
|
|
52
48
|
const fileContents = [];
|
|
53
|
-
for (const file of files.slice(0, 50)) {
|
|
49
|
+
for (const file of files.slice(0, 50)) {
|
|
54
50
|
try {
|
|
55
51
|
const content = fs.readFileSync(file, 'utf-8');
|
|
56
52
|
fileContents.push({ path: path.relative(this.projectRoot, file), content });
|
|
@@ -58,40 +54,34 @@ class AIAnalyzer {
|
|
|
58
54
|
catch (error) {
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
|
-
|
|
57
|
+
if (fileContents.length === 0) {
|
|
58
|
+
throw new Error('No files found to analyze. Check your file patterns and project structure.');
|
|
59
|
+
}
|
|
62
60
|
let endpoints = await this.analyzeWithAI(fileContents);
|
|
63
|
-
// Filter duplicates and root routes
|
|
64
61
|
endpoints = this.filterAndCleanEndpoints(endpoints);
|
|
65
|
-
// Detect base URL from common prefix
|
|
66
62
|
const baseUrl = this.detectBaseUrl(endpoints);
|
|
67
|
-
// Detect programming language
|
|
68
63
|
const language = this.detectLanguage(files);
|
|
69
|
-
|
|
70
|
-
const stats = this.calculateStats(endpoints);
|
|
64
|
+
const framework = this.detectFramework(files, endpoints);
|
|
71
65
|
const inventory = {
|
|
72
66
|
version: '1.0.0',
|
|
73
67
|
generatedAt: new Date().toISOString(),
|
|
74
|
-
baseUrl: baseUrl
|
|
68
|
+
baseUrl: baseUrl,
|
|
75
69
|
project: {
|
|
76
|
-
name:
|
|
77
|
-
version:
|
|
78
|
-
description:
|
|
79
|
-
framework:
|
|
70
|
+
name: '',
|
|
71
|
+
version: '',
|
|
72
|
+
description: '',
|
|
73
|
+
framework: framework,
|
|
80
74
|
language: language,
|
|
81
75
|
},
|
|
82
|
-
endpoints,
|
|
83
|
-
stats
|
|
76
|
+
endpoints: endpoints,
|
|
77
|
+
stats: {
|
|
78
|
+
totalEndpoints: endpoints.length,
|
|
79
|
+
byMethod: endpoints.reduce((acc, ep) => {
|
|
80
|
+
acc[ep.method] = (acc[ep.method] || 0) + 1;
|
|
81
|
+
return acc;
|
|
82
|
+
}, {}),
|
|
83
|
+
},
|
|
84
84
|
};
|
|
85
|
-
// Validate inventory structure
|
|
86
|
-
if (!inventory.endpoints || !Array.isArray(inventory.endpoints)) {
|
|
87
|
-
inventory.endpoints = [];
|
|
88
|
-
}
|
|
89
|
-
if (!inventory.stats) {
|
|
90
|
-
inventory.stats = {
|
|
91
|
-
totalEndpoints: inventory.endpoints.length,
|
|
92
|
-
byMethod: {},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
85
|
return inventory;
|
|
96
86
|
}
|
|
97
87
|
async findProjectFiles() {
|
|
@@ -101,43 +91,51 @@ class AIAnalyzer {
|
|
|
101
91
|
const files = await (0, glob_1.glob)(pattern, {
|
|
102
92
|
cwd: this.projectRoot,
|
|
103
93
|
absolute: true,
|
|
104
|
-
ignore: [
|
|
94
|
+
ignore: [
|
|
95
|
+
'**/node_modules/**',
|
|
96
|
+
'**/dist/**',
|
|
97
|
+
'**/build/**',
|
|
98
|
+
'**/.git/**',
|
|
99
|
+
'**/coverage/**',
|
|
100
|
+
'**/*.test.*',
|
|
101
|
+
'**/*.spec.*',
|
|
102
|
+
],
|
|
105
103
|
});
|
|
106
104
|
allFiles.push(...files);
|
|
107
105
|
}
|
|
108
106
|
catch (error) {
|
|
109
|
-
// Silently ignore pattern errors
|
|
110
107
|
}
|
|
111
108
|
}
|
|
112
|
-
const uniqueFiles = [...new Set(allFiles)];
|
|
109
|
+
const uniqueFiles = [...new Set(allFiles)].sort();
|
|
113
110
|
if (uniqueFiles.length === 0) {
|
|
114
|
-
// Try alternative patterns
|
|
115
111
|
const alternativePatterns = [
|
|
116
112
|
'**/*.ts',
|
|
117
113
|
'**/*.js',
|
|
118
|
-
'**/*.
|
|
119
|
-
'**/*.
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'lib/**/*.ts',
|
|
125
|
-
'lib/**/*.js',
|
|
114
|
+
'**/*.py',
|
|
115
|
+
'**/*.java',
|
|
116
|
+
'**/*.go',
|
|
117
|
+
'**/*.rs',
|
|
118
|
+
'**/*.php',
|
|
119
|
+
'**/*.rb',
|
|
126
120
|
];
|
|
127
|
-
for (const
|
|
121
|
+
for (const pattern of alternativePatterns) {
|
|
128
122
|
try {
|
|
129
|
-
const files = await (0, glob_1.glob)(
|
|
123
|
+
const files = await (0, glob_1.glob)(pattern, {
|
|
130
124
|
cwd: this.projectRoot,
|
|
131
125
|
absolute: true,
|
|
132
|
-
ignore: [
|
|
126
|
+
ignore: [
|
|
127
|
+
'**/node_modules/**',
|
|
128
|
+
'**/dist/**',
|
|
129
|
+
'**/build/**',
|
|
130
|
+
'**/.git/**',
|
|
131
|
+
],
|
|
133
132
|
});
|
|
134
133
|
if (files.length > 0) {
|
|
135
134
|
uniqueFiles.push(...files);
|
|
136
|
-
break;
|
|
135
|
+
break;
|
|
137
136
|
}
|
|
138
137
|
}
|
|
139
138
|
catch (error) {
|
|
140
|
-
// Continue
|
|
141
139
|
}
|
|
142
140
|
}
|
|
143
141
|
}
|
|
@@ -145,64 +143,210 @@ class AIAnalyzer {
|
|
|
145
143
|
}
|
|
146
144
|
async analyzeWithAI(files) {
|
|
147
145
|
const prompt = this.buildAnalysisPrompt(files);
|
|
146
|
+
if (!this.apiUrl) {
|
|
147
|
+
throw new Error('Proxy API URL not configured');
|
|
148
|
+
}
|
|
149
|
+
const url = this.apiUrl.trim();
|
|
150
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
151
|
+
throw new Error(`Invalid API URL: "${url}". URL must start with http:// or https://`);
|
|
152
|
+
}
|
|
148
153
|
try {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({ prompt }),
|
|
160
|
+
});
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const errorText = await response.text().catch(() => 'No error details');
|
|
163
|
+
throw new Error(`Proxy API error (${response.status}): ${response.statusText}. ${errorText}`);
|
|
156
164
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const responseText = await response.text();
|
|
166
|
+
let endpoints = [];
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(responseText);
|
|
169
|
+
if (parsed.endpoints && Array.isArray(parsed.endpoints)) {
|
|
170
|
+
endpoints = parsed.endpoints;
|
|
171
|
+
}
|
|
172
|
+
else if (Array.isArray(parsed)) {
|
|
173
|
+
endpoints = parsed;
|
|
174
|
+
}
|
|
175
|
+
else if (parsed.data && Array.isArray(parsed.data)) {
|
|
176
|
+
endpoints = parsed.data;
|
|
177
|
+
}
|
|
178
|
+
else if (parsed.response && typeof parsed.response === 'string') {
|
|
179
|
+
try {
|
|
180
|
+
const innerParsed = JSON.parse(parsed.response);
|
|
181
|
+
if (Array.isArray(innerParsed)) {
|
|
182
|
+
endpoints = innerParsed;
|
|
183
|
+
}
|
|
184
|
+
else if (innerParsed.endpoints && Array.isArray(innerParsed.endpoints)) {
|
|
185
|
+
endpoints = innerParsed.endpoints;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
const jsonMatch = parsed.response.match(/\{[\s\S]*"endpoints"[\s\S]*\}/);
|
|
190
|
+
if (jsonMatch) {
|
|
191
|
+
const extracted = JSON.parse(jsonMatch[0]);
|
|
192
|
+
if (extracted.endpoints && Array.isArray(extracted.endpoints)) {
|
|
193
|
+
endpoints = extracted.endpoints;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (endpoints.length === 0) {
|
|
199
|
+
throw new Error(`Invalid response format. Expected { endpoints: [...] } or array of endpoints. Received: ${JSON.stringify(parsed).substring(0, 200)}`);
|
|
200
|
+
}
|
|
160
201
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
202
|
+
catch (parseError) {
|
|
203
|
+
const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/);
|
|
204
|
+
if (codeBlockMatch) {
|
|
205
|
+
try {
|
|
206
|
+
const extracted = JSON.parse(codeBlockMatch[1]);
|
|
207
|
+
if (extracted.endpoints && Array.isArray(extracted.endpoints)) {
|
|
208
|
+
endpoints = extracted.endpoints;
|
|
209
|
+
}
|
|
210
|
+
else if (Array.isArray(extracted)) {
|
|
211
|
+
endpoints = extracted;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
}
|
|
166
216
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
file: ep.file || 'unknown',
|
|
173
|
-
line: ep.line || 0,
|
|
174
|
-
handler: {
|
|
175
|
-
name: ep.handler?.name,
|
|
176
|
-
isAsync: ep.handler?.isAsync || false,
|
|
177
|
-
parameters: ep.handler?.parameters || [],
|
|
178
|
-
},
|
|
179
|
-
parameters: Array.isArray(ep.parameters) ? ep.parameters : [],
|
|
180
|
-
requestBody: ep.requestBody || undefined,
|
|
181
|
-
responses: Array.isArray(ep.responses) ? ep.responses : [],
|
|
182
|
-
authentication: ep.authentication || undefined,
|
|
183
|
-
headers: Array.isArray(ep.headers) ? ep.headers : [],
|
|
184
|
-
metadata: {
|
|
185
|
-
title: ep.metadata?.title || ep.title || `${method} ${ep.path || '/'}`,
|
|
186
|
-
description: ep.metadata?.description || ep.description || '',
|
|
187
|
-
aiGenerated: true,
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
});
|
|
191
|
-
// Log first endpoint for debugging
|
|
192
|
-
// Logs removed for clean progress bar output
|
|
193
|
-
return mappedEndpoints;
|
|
217
|
+
if (endpoints.length === 0) {
|
|
218
|
+
throw new Error(`Failed to parse API response. Expected JSON with endpoints array. Response preview: ${responseText.substring(0, 300)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return this.mapEndpoints(endpoints);
|
|
194
222
|
}
|
|
195
223
|
catch (error) {
|
|
196
|
-
|
|
224
|
+
if (error instanceof TypeError && error.message.includes('fetch failed')) {
|
|
225
|
+
throw new Error(`Failed to connect to API at ${this.apiUrl}. Check if the server is running and the URL is correct.`);
|
|
226
|
+
}
|
|
227
|
+
throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
197
228
|
}
|
|
198
229
|
}
|
|
230
|
+
mapEndpoints(endpoints) {
|
|
231
|
+
return endpoints.map((ep, index) => {
|
|
232
|
+
const method = (ep.method || 'GET').toUpperCase();
|
|
233
|
+
return {
|
|
234
|
+
id: `ep-${index}`,
|
|
235
|
+
method: method,
|
|
236
|
+
path: ep.path || '/',
|
|
237
|
+
group: ep.group || 'Default',
|
|
238
|
+
file: ep.file || '',
|
|
239
|
+
line: ep.line || 0,
|
|
240
|
+
handler: ep.handler || {
|
|
241
|
+
name: 'anonymous',
|
|
242
|
+
isAsync: false,
|
|
243
|
+
parameters: [],
|
|
244
|
+
},
|
|
245
|
+
parameters: ep.parameters || [],
|
|
246
|
+
requestBody: ep.requestBody || { required: false, fields: [] },
|
|
247
|
+
responses: ep.responses || [],
|
|
248
|
+
authentication: ep.authentication || { required: false, type: 'none' },
|
|
249
|
+
headers: ep.headers || [],
|
|
250
|
+
metadata: ep.metadata || { title: '', description: '' },
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
filterAndCleanEndpoints(endpoints) {
|
|
255
|
+
const seen = new Map();
|
|
256
|
+
for (const endpoint of endpoints) {
|
|
257
|
+
const key = `${endpoint.method}:${endpoint.path}`;
|
|
258
|
+
if (endpoint.path === '/' && endpoint.group === 'Root') {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!seen.has(key)) {
|
|
262
|
+
seen.set(key, endpoint);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return Array.from(seen.values());
|
|
266
|
+
}
|
|
267
|
+
detectBaseUrl(endpoints) {
|
|
268
|
+
if (endpoints.length === 0)
|
|
269
|
+
return 'https://api.example.com';
|
|
270
|
+
const paths = endpoints.map(e => e.path).filter(p => p && p !== '/');
|
|
271
|
+
if (paths.length === 0)
|
|
272
|
+
return 'https://api.example.com';
|
|
273
|
+
const firstPath = paths[0];
|
|
274
|
+
let commonPrefix = '';
|
|
275
|
+
for (let i = 0; i < firstPath.length; i++) {
|
|
276
|
+
const char = firstPath[i];
|
|
277
|
+
if (paths.every(p => p[i] === char)) {
|
|
278
|
+
commonPrefix += char;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const lastSlash = commonPrefix.lastIndexOf('/');
|
|
285
|
+
if (lastSlash > 0) {
|
|
286
|
+
return `https://api.example.com${commonPrefix.substring(0, lastSlash + 1)}`;
|
|
287
|
+
}
|
|
288
|
+
return 'https://api.example.com';
|
|
289
|
+
}
|
|
290
|
+
detectLanguage(files) {
|
|
291
|
+
const extensions = new Map();
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
const ext = path.extname(file).toLowerCase();
|
|
294
|
+
const langMap = {
|
|
295
|
+
'.ts': 'TypeScript',
|
|
296
|
+
'.js': 'JavaScript',
|
|
297
|
+
'.py': 'Python',
|
|
298
|
+
'.java': 'Java',
|
|
299
|
+
'.go': 'Go',
|
|
300
|
+
'.rs': 'Rust',
|
|
301
|
+
'.php': 'PHP',
|
|
302
|
+
'.rb': 'Ruby',
|
|
303
|
+
};
|
|
304
|
+
if (langMap[ext]) {
|
|
305
|
+
extensions.set(langMap[ext], (extensions.get(langMap[ext]) || 0) + 1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (extensions.size === 0)
|
|
309
|
+
return 'Unknown';
|
|
310
|
+
const sorted = Array.from(extensions.entries()).sort((a, b) => b[1] - a[1]);
|
|
311
|
+
return sorted[0][0];
|
|
312
|
+
}
|
|
313
|
+
detectFramework(files, endpoints) {
|
|
314
|
+
const fileNames = files.map(f => path.basename(f).toLowerCase());
|
|
315
|
+
const allFiles = files.join(' ');
|
|
316
|
+
if (fileNames.some(f => f.includes('express')) || allFiles.includes('express')) {
|
|
317
|
+
return 'express';
|
|
318
|
+
}
|
|
319
|
+
if (fileNames.some(f => f.includes('fastify')) || allFiles.includes('fastify')) {
|
|
320
|
+
return 'fastify';
|
|
321
|
+
}
|
|
322
|
+
if (fileNames.some(f => f.includes('koa')) || allFiles.includes('koa')) {
|
|
323
|
+
return 'koa';
|
|
324
|
+
}
|
|
325
|
+
if (fileNames.some(f => f.includes('django')) || allFiles.includes('django')) {
|
|
326
|
+
return 'django';
|
|
327
|
+
}
|
|
328
|
+
if (fileNames.some(f => f.includes('flask')) || allFiles.includes('flask')) {
|
|
329
|
+
return 'flask';
|
|
330
|
+
}
|
|
331
|
+
if (fileNames.some(f => f.includes('spring')) || allFiles.includes('spring')) {
|
|
332
|
+
return 'spring';
|
|
333
|
+
}
|
|
334
|
+
return 'unknown';
|
|
335
|
+
}
|
|
199
336
|
buildAnalysisPrompt(files) {
|
|
337
|
+
if (files.length === 0) {
|
|
338
|
+
throw new Error('No project files found to analyze');
|
|
339
|
+
}
|
|
200
340
|
const filesContext = files.map(f => `// File: ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
201
|
-
return `You are an expert API documentation analyzer. Your task is to analyze
|
|
341
|
+
return `You are an expert API documentation analyzer. Your task is to analyze the provided codebase files and extract ALL API endpoints with COMPLETE information.
|
|
202
342
|
|
|
203
|
-
|
|
343
|
+
IMPORTANT: Below are the ACTUAL SOURCE CODE FILES from the user's project. Analyze these files to find all API endpoints, routes, handlers, parameters, request bodies, responses, and authentication requirements.
|
|
344
|
+
|
|
345
|
+
PROJECT FILES TO ANALYZE (${files.length} file${files.length > 1 ? 's' : ''}):
|
|
204
346
|
${filesContext}
|
|
205
347
|
|
|
348
|
+
---
|
|
349
|
+
|
|
206
350
|
CRITICAL INSTRUCTIONS:
|
|
207
351
|
1. Analyze the ENTIRE codebase structure to understand how routes are organized
|
|
208
352
|
2. Follow ALL route prefixes and middleware chains (router.use, app.use, etc.)
|
|
@@ -219,21 +363,21 @@ For EACH endpoint found, you MUST extract:
|
|
|
219
363
|
2. FULL PATH: Complete path including all prefixes
|
|
220
364
|
- Follow router.use('/prefix', router) chains
|
|
221
365
|
- Combine prefixes correctly
|
|
222
|
-
- Example: router.use('/api/
|
|
366
|
+
- Example: router.use('/api/users', userRoutes) + router.get('/auth', ...) = /api/users/auth
|
|
223
367
|
|
|
224
|
-
3. GROUP NAME: Based on the RESOURCE, not controller
|
|
225
|
-
- /api/
|
|
226
|
-
- /api/
|
|
227
|
-
- /api/
|
|
228
|
-
- Group related endpoints together (auth + auth/callback)
|
|
229
|
-
- DO NOT create a "Root" group unless absolutely necessary
|
|
230
|
-
- Skip root routes (path: "/") that are just status/health checks
|
|
368
|
+
3. GROUP NAME: Based on the RESOURCE, not controller/service name
|
|
369
|
+
- /api/users/auth → "Auth" (NOT "Users")
|
|
370
|
+
- /api/users/auth/callback → "Auth" (same group as /auth)
|
|
371
|
+
- /api/users/posts/:userId → "Posts" (NOT "Users")
|
|
372
|
+
- Group related endpoints together (auth + auth/callback belong in same group)
|
|
231
373
|
|
|
232
374
|
4. PARAMETERS: Extract ALL parameters from:
|
|
233
375
|
- Path parameters: /users/:id → {name: "id", in: "path"}
|
|
234
376
|
- Query parameters: req.query.page → {name: "page", in: "query"}
|
|
235
377
|
- Body parameters: req.body.email, const {email, password} = req.body → {name: "email", in: "body"}
|
|
236
378
|
- Header parameters: req.headers.authorization → {name: "Authorization", in: "header"}
|
|
379
|
+
- IMPORTANT: HTTP headers are case-insensitive (X-API-Key, x-api-key, X-Api-Key are all the same)
|
|
380
|
+
- DO NOT duplicate headers if you see different casings - use only ONE entry with the most common casing
|
|
237
381
|
|
|
238
382
|
5. REQUEST BODY: Complete structure if POST/PUT/PATCH
|
|
239
383
|
- Extract all fields from req.body or request.body
|
|
@@ -254,10 +398,16 @@ For EACH endpoint found, you MUST extract:
|
|
|
254
398
|
8. HEADERS: Extract required headers
|
|
255
399
|
- From req.headers.fieldName or request.headers.fieldName
|
|
256
400
|
- Include Authorization if authentication is required
|
|
401
|
+
- CRITICAL: HTTP headers are case-insensitive - X-API-Key, x-api-key, X-Api-Key are THE SAME header
|
|
402
|
+
- If you see the same header with different casings, include it ONLY ONCE (use the most common casing)
|
|
403
|
+
- DO NOT create duplicate header entries for the same header with different casings
|
|
257
404
|
|
|
258
405
|
9. HANDLER INFO: Function name, async status, parameters
|
|
259
406
|
|
|
260
|
-
|
|
407
|
+
OUTPUT FORMAT - YOU MUST RETURN EXACTLY THIS STRUCTURE:
|
|
408
|
+
|
|
409
|
+
Return a JSON object with this EXACT structure. DO NOT wrap it in markdown code blocks, DO NOT add explanations, DO NOT add any text before or after. Return ONLY the raw JSON:
|
|
410
|
+
|
|
261
411
|
{
|
|
262
412
|
"endpoints": [
|
|
263
413
|
{
|
|
@@ -315,7 +465,7 @@ Return a JSON object with this EXACT structure:
|
|
|
315
465
|
],
|
|
316
466
|
"authentication": {
|
|
317
467
|
"required": true,
|
|
318
|
-
"type": "bearer|basic|api-key|custom",
|
|
468
|
+
"type": "bearer|basic|api-key|custom|none",
|
|
319
469
|
"description": "Authentication description"
|
|
320
470
|
},
|
|
321
471
|
"headers": [
|
|
@@ -335,168 +485,29 @@ Return a JSON object with this EXACT structure:
|
|
|
335
485
|
]
|
|
336
486
|
}
|
|
337
487
|
|
|
338
|
-
CRITICAL:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
seen.add(key);
|
|
365
|
-
filtered.push(endpoint);
|
|
366
|
-
}
|
|
367
|
-
return filtered;
|
|
368
|
-
}
|
|
369
|
-
detectBaseUrl(endpoints) {
|
|
370
|
-
if (endpoints.length === 0)
|
|
371
|
-
return undefined;
|
|
372
|
-
// Find common prefix in all paths (excluding root)
|
|
373
|
-
const paths = endpoints.map(ep => ep.path).filter(p => p !== '/' && p.length > 1);
|
|
374
|
-
if (paths.length === 0)
|
|
375
|
-
return undefined;
|
|
376
|
-
// Find longest common prefix
|
|
377
|
-
let commonPrefix = paths[0];
|
|
378
|
-
for (let i = 1; i < paths.length; i++) {
|
|
379
|
-
const path = paths[i];
|
|
380
|
-
let j = 0;
|
|
381
|
-
while (j < commonPrefix.length && j < path.length && commonPrefix[j] === path[j]) {
|
|
382
|
-
j++;
|
|
383
|
-
}
|
|
384
|
-
commonPrefix = commonPrefix.substring(0, j);
|
|
385
|
-
}
|
|
386
|
-
// Extract base URL (e.g., /api/instagram from /api/instagram/auth)
|
|
387
|
-
if (commonPrefix && commonPrefix.length > 1) {
|
|
388
|
-
// Find complete segments
|
|
389
|
-
const segments = commonPrefix.split('/').filter(s => s);
|
|
390
|
-
if (segments.length >= 2) {
|
|
391
|
-
// Take first 2 segments as base (e.g., /api/instagram)
|
|
392
|
-
const basePath = '/' + segments.slice(0, 2).join('/');
|
|
393
|
-
// Remove base path from endpoint paths
|
|
394
|
-
endpoints.forEach(ep => {
|
|
395
|
-
if (ep.path.startsWith(basePath)) {
|
|
396
|
-
ep.path = ep.path.substring(basePath.length) || '/';
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
return `https://api.example.com${basePath}`;
|
|
400
|
-
}
|
|
401
|
-
else if (segments.length === 1) {
|
|
402
|
-
const basePath = '/' + segments[0];
|
|
403
|
-
// Remove base path from endpoint paths
|
|
404
|
-
endpoints.forEach(ep => {
|
|
405
|
-
if (ep.path.startsWith(basePath)) {
|
|
406
|
-
ep.path = ep.path.substring(basePath.length) || '/';
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
return `https://api.example.com${basePath}`;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
detectLanguage(files) {
|
|
415
|
-
const extensions = new Map();
|
|
416
|
-
for (const file of files) {
|
|
417
|
-
const ext = path.extname(file).toLowerCase();
|
|
418
|
-
if (ext) {
|
|
419
|
-
extensions.set(ext, (extensions.get(ext) || 0) + 1);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
// Find most common extension
|
|
423
|
-
let maxCount = 0;
|
|
424
|
-
let mostCommon = 'unknown';
|
|
425
|
-
for (const [ext, count] of extensions.entries()) {
|
|
426
|
-
if (count > maxCount) {
|
|
427
|
-
maxCount = count;
|
|
428
|
-
mostCommon = ext;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const languageMap = {
|
|
432
|
-
'.ts': 'TypeScript',
|
|
433
|
-
'.js': 'JavaScript',
|
|
434
|
-
'.tsx': 'TypeScript React',
|
|
435
|
-
'.jsx': 'JavaScript React',
|
|
436
|
-
'.py': 'Python',
|
|
437
|
-
'.java': 'Java',
|
|
438
|
-
'.go': 'Go',
|
|
439
|
-
'.rs': 'Rust',
|
|
440
|
-
'.php': 'PHP',
|
|
441
|
-
'.rb': 'Ruby',
|
|
442
|
-
};
|
|
443
|
-
return languageMap[mostCommon] || 'unknown';
|
|
444
|
-
}
|
|
445
|
-
detectFramework(endpoints, language) {
|
|
446
|
-
// Simple detection based on common patterns
|
|
447
|
-
if (language.includes('TypeScript') || language.includes('JavaScript')) {
|
|
448
|
-
return 'express'; // Default for Node.js
|
|
449
|
-
}
|
|
450
|
-
return 'unknown';
|
|
451
|
-
}
|
|
452
|
-
calculateStats(endpoints) {
|
|
453
|
-
const byMethod = {};
|
|
454
|
-
endpoints.forEach(endpoint => {
|
|
455
|
-
byMethod[endpoint.method] = (byMethod[endpoint.method] || 0) + 1;
|
|
456
|
-
});
|
|
457
|
-
return {
|
|
458
|
-
totalEndpoints: endpoints.length,
|
|
459
|
-
byMethod,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
extractProjectName() {
|
|
463
|
-
try {
|
|
464
|
-
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
|
465
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
466
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
467
|
-
return packageJson.name;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
// Ignore
|
|
472
|
-
}
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
extractProjectVersion() {
|
|
476
|
-
try {
|
|
477
|
-
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
|
478
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
479
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
480
|
-
return packageJson.version;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
catch (error) {
|
|
484
|
-
// Ignore
|
|
485
|
-
}
|
|
486
|
-
return undefined;
|
|
487
|
-
}
|
|
488
|
-
extractProjectDescription() {
|
|
489
|
-
try {
|
|
490
|
-
const packageJsonPath = path.join(this.projectRoot, 'package.json');
|
|
491
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
492
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
493
|
-
return packageJson.description;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
// Ignore
|
|
498
|
-
}
|
|
499
|
-
return undefined;
|
|
488
|
+
CRITICAL OUTPUT REQUIREMENTS:
|
|
489
|
+
1. Return ONLY the raw JSON object, starting with { and ending with }
|
|
490
|
+
2. DO NOT wrap in markdown code blocks (no triple backticks)
|
|
491
|
+
3. DO NOT add any text before or after the JSON
|
|
492
|
+
4. DO NOT add explanations, comments, or any other text
|
|
493
|
+
5. The root object MUST have an "endpoints" key containing an array
|
|
494
|
+
6. If no endpoints are found, return: { "endpoints": [] }
|
|
495
|
+
7. The JSON MUST be valid and parseable
|
|
496
|
+
8. Extract EVERYTHING from the code - be thorough
|
|
497
|
+
9. Group by resource name, not controller name
|
|
498
|
+
10. Include ALL responses found in the code
|
|
499
|
+
11. Schema should be direct object, not wrapped in {type: "object", properties: {...}}
|
|
500
|
+
|
|
501
|
+
EXAMPLE OF CORRECT OUTPUT:
|
|
502
|
+
{"endpoints":[{"method":"GET","path":"/api/users","group":"Users","file":"src/routes/users.ts","line":10,"handler":{"name":"getUsers","isAsync":true,"parameters":["req","res"]},"parameters":[],"requestBody":{"required":false,"fields":[]},"responses":[{"statusCode":200,"type":"application/json","description":"List of users","schema":{"users":{"type":"array"}}}],"authentication":{"required":false,"type":"none"},"headers":[],"metadata":{"title":"Get Users","description":"Retrieve all users"}}]}
|
|
503
|
+
|
|
504
|
+
DO NOT return this format (WRONG):
|
|
505
|
+
Markdown code blocks with triple backticks
|
|
506
|
+
DO NOT return this format (WRONG):
|
|
507
|
+
Text explanations before or after the JSON
|
|
508
|
+
|
|
509
|
+
Return ONLY this format (CORRECT):
|
|
510
|
+
{ "endpoints": [...] }`;
|
|
500
511
|
}
|
|
501
512
|
}
|
|
502
513
|
exports.AIAnalyzer = AIAnalyzer;
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
export {};
|