@girardmedia/bootspring 2.5.0 → 2.5.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/README.md +9 -403
- package/bin/bootspring.js +1 -96
- package/dist/cli/index.js +65134 -0
- package/dist/cli-launcher.js +92 -0
- package/dist/core/index.d.ts +2110 -5582
- package/dist/core/index.js +2 -0
- package/dist/core.js +21123 -5413
- package/dist/mcp/index.d.ts +357 -1
- package/dist/mcp/index.js +2 -0
- package/dist/mcp-server.js +51948 -1976
- package/package.json +27 -63
- package/scripts/postinstall.cjs +144 -0
- package/LICENSE +0 -29
- package/dist/cli/index.cjs +0 -20776
- package/generators/api-docs.js +0 -827
- package/generators/decisions.js +0 -655
- package/generators/generate.js +0 -595
- package/generators/health.js +0 -942
- package/generators/index.ts +0 -82
- package/generators/presets/full.js +0 -28
- package/generators/presets/index.js +0 -12
- package/generators/presets/minimal.js +0 -29
- package/generators/presets/standard.js +0 -28
- package/generators/questionnaire.js +0 -414
- package/generators/sections/advanced.js +0 -136
- package/generators/sections/ai.js +0 -106
- package/generators/sections/auth.js +0 -89
- package/generators/sections/backend.js +0 -146
- package/generators/sections/business.js +0 -118
- package/generators/sections/content.js +0 -300
- package/generators/sections/deployment.js +0 -139
- package/generators/sections/features.js +0 -122
- package/generators/sections/frontend.js +0 -118
- package/generators/sections/identity.js +0 -76
- package/generators/sections/index.js +0 -40
- package/generators/sections/instructions.js +0 -146
- package/generators/sections/payments.js +0 -104
- package/generators/sections/plugins.js +0 -142
- package/generators/sections/pre-build.js +0 -130
- package/generators/sections/security.js +0 -127
- package/generators/sections/technical.js +0 -171
- package/generators/sections/testing.js +0 -125
- package/generators/sections/workflow.js +0 -104
- package/generators/sprint.js +0 -675
- package/generators/templates/agents.template.js +0 -199
- package/generators/templates/assistant-context.template.js +0 -83
- package/generators/templates/build-planning.template.js +0 -708
- package/generators/templates/claude.template.js +0 -379
- package/generators/templates/content.template.js +0 -819
- package/generators/templates/index.js +0 -16
- package/generators/templates/planning.template.js +0 -515
- package/generators/templates/seed.template.js +0 -109
- package/generators/visual-doc-generator.js +0 -910
- package/scripts/postinstall.js +0 -197
- /package/{claude-commands → assets/claude-commands}/agent.md +0 -0
- /package/{claude-commands → assets/claude-commands}/bs.md +0 -0
- /package/{claude-commands → assets/claude-commands}/build.md +0 -0
- /package/{claude-commands → assets/claude-commands}/skill.md +0 -0
- /package/{claude-commands → assets/claude-commands}/todo.md +0 -0
package/generators/api-docs.js
DELETED
|
@@ -1,827 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring API.md Auto-Generator
|
|
3
|
-
*
|
|
4
|
-
* Auto-generates API documentation from routes with endpoint reference,
|
|
5
|
-
* request/response schemas, authentication requirements, and examples.
|
|
6
|
-
*
|
|
7
|
-
* @package bootspring
|
|
8
|
-
* @module generators/api-docs
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* HTTP methods
|
|
16
|
-
*/
|
|
17
|
-
const HTTP_METHODS = {
|
|
18
|
-
GET: { color: 'green', description: 'Retrieve resource(s)' },
|
|
19
|
-
POST: { color: 'yellow', description: 'Create resource' },
|
|
20
|
-
PUT: { color: 'blue', description: 'Update resource (full)' },
|
|
21
|
-
PATCH: { color: 'purple', description: 'Update resource (partial)' },
|
|
22
|
-
DELETE: { color: 'red', description: 'Delete resource' },
|
|
23
|
-
OPTIONS: { color: 'gray', description: 'CORS preflight' },
|
|
24
|
-
HEAD: { color: 'gray', description: 'Get headers only' }
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Common response codes
|
|
29
|
-
*/
|
|
30
|
-
const RESPONSE_CODES = {
|
|
31
|
-
200: { description: 'OK', type: 'success' },
|
|
32
|
-
201: { description: 'Created', type: 'success' },
|
|
33
|
-
204: { description: 'No Content', type: 'success' },
|
|
34
|
-
400: { description: 'Bad Request', type: 'error' },
|
|
35
|
-
401: { description: 'Unauthorized', type: 'error' },
|
|
36
|
-
403: { description: 'Forbidden', type: 'error' },
|
|
37
|
-
404: { description: 'Not Found', type: 'error' },
|
|
38
|
-
409: { description: 'Conflict', type: 'error' },
|
|
39
|
-
422: { description: 'Unprocessable Entity', type: 'error' },
|
|
40
|
-
429: { description: 'Too Many Requests', type: 'error' },
|
|
41
|
-
500: { description: 'Internal Server Error', type: 'error' }
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* API Documentation Generator
|
|
46
|
-
*/
|
|
47
|
-
class ApiDocsGenerator {
|
|
48
|
-
constructor(options = {}) {
|
|
49
|
-
this.projectRoot = options.projectRoot || process.cwd();
|
|
50
|
-
this.apiDataPath = path.join(this.projectRoot, '.bootspring', 'api-docs.json');
|
|
51
|
-
this.baseUrl = options.baseUrl || '/api';
|
|
52
|
-
this.version = options.version || 'v1';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Load API docs data
|
|
57
|
-
*/
|
|
58
|
-
loadApiData() {
|
|
59
|
-
try {
|
|
60
|
-
if (fs.existsSync(this.apiDataPath)) {
|
|
61
|
-
return JSON.parse(fs.readFileSync(this.apiDataPath, 'utf-8'));
|
|
62
|
-
}
|
|
63
|
-
} catch (_err) {
|
|
64
|
-
// Return default
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
project: this.getProjectName(),
|
|
69
|
-
version: this.version,
|
|
70
|
-
baseUrl: this.baseUrl,
|
|
71
|
-
endpoints: [],
|
|
72
|
-
schemas: {},
|
|
73
|
-
authentication: {},
|
|
74
|
-
metadata: {
|
|
75
|
-
created: new Date().toISOString(),
|
|
76
|
-
lastUpdated: null,
|
|
77
|
-
autoGenerated: true
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Save API data
|
|
84
|
-
*/
|
|
85
|
-
saveApiData(data) {
|
|
86
|
-
data.metadata.lastUpdated = new Date().toISOString();
|
|
87
|
-
|
|
88
|
-
const dir = path.dirname(this.apiDataPath);
|
|
89
|
-
if (!fs.existsSync(dir)) {
|
|
90
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
-
}
|
|
92
|
-
fs.writeFileSync(this.apiDataPath, JSON.stringify(data, null, 2));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get project name
|
|
97
|
-
*/
|
|
98
|
-
getProjectName() {
|
|
99
|
-
try {
|
|
100
|
-
const pkgPath = path.join(this.projectRoot, 'package.json');
|
|
101
|
-
if (fs.existsSync(pkgPath)) {
|
|
102
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
103
|
-
return pkg.name || 'API';
|
|
104
|
-
}
|
|
105
|
-
} catch (_err) {
|
|
106
|
-
// Default
|
|
107
|
-
}
|
|
108
|
-
return 'API';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Scan for API routes in the project
|
|
113
|
-
*/
|
|
114
|
-
async scanRoutes() {
|
|
115
|
-
const endpoints = [];
|
|
116
|
-
|
|
117
|
-
// Next.js App Router
|
|
118
|
-
const appApiDir = path.join(this.projectRoot, 'app', 'api');
|
|
119
|
-
if (fs.existsSync(appApiDir)) {
|
|
120
|
-
endpoints.push(...this.scanNextJsAppRouter(appApiDir));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Next.js Pages Router
|
|
124
|
-
const pagesApiDir = path.join(this.projectRoot, 'pages', 'api');
|
|
125
|
-
if (fs.existsSync(pagesApiDir)) {
|
|
126
|
-
endpoints.push(...this.scanNextJsPagesRouter(pagesApiDir));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Express-style routes
|
|
130
|
-
const routesDir = path.join(this.projectRoot, 'routes');
|
|
131
|
-
if (fs.existsSync(routesDir)) {
|
|
132
|
-
endpoints.push(...this.scanExpressRoutes(routesDir));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// src/routes
|
|
136
|
-
const srcRoutesDir = path.join(this.projectRoot, 'src', 'routes');
|
|
137
|
-
if (fs.existsSync(srcRoutesDir)) {
|
|
138
|
-
endpoints.push(...this.scanExpressRoutes(srcRoutesDir));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return endpoints;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Scan Next.js App Router routes
|
|
146
|
-
*/
|
|
147
|
-
scanNextJsAppRouter(apiDir) {
|
|
148
|
-
const endpoints = [];
|
|
149
|
-
|
|
150
|
-
const walk = (dir, basePath = '') => {
|
|
151
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
-
|
|
153
|
-
for (const entry of entries) {
|
|
154
|
-
const fullPath = path.join(dir, entry.name);
|
|
155
|
-
|
|
156
|
-
if (entry.isDirectory()) {
|
|
157
|
-
// Handle dynamic segments [id] -> :id
|
|
158
|
-
let segment = entry.name;
|
|
159
|
-
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
160
|
-
segment = ':' + segment.slice(1, -1);
|
|
161
|
-
}
|
|
162
|
-
walk(fullPath, `${basePath}/${segment}`);
|
|
163
|
-
} else if (entry.name === 'route.ts' || entry.name === 'route.js') {
|
|
164
|
-
const endpoint = this.parseNextJsRouteFile(fullPath, basePath);
|
|
165
|
-
if (endpoint) {
|
|
166
|
-
endpoints.push(...endpoint);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
walk(apiDir);
|
|
173
|
-
return endpoints;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Parse Next.js App Router route file
|
|
178
|
-
*/
|
|
179
|
-
parseNextJsRouteFile(filePath, routePath) {
|
|
180
|
-
const endpoints = [];
|
|
181
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
182
|
-
|
|
183
|
-
// Extract exported methods (GET, POST, etc.)
|
|
184
|
-
const methodPattern = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)/g;
|
|
185
|
-
let match;
|
|
186
|
-
|
|
187
|
-
while ((match = methodPattern.exec(content)) !== null) {
|
|
188
|
-
const method = match[1];
|
|
189
|
-
const endpoint = {
|
|
190
|
-
path: `/api${routePath}`,
|
|
191
|
-
method,
|
|
192
|
-
description: this.extractDescription(content, method),
|
|
193
|
-
parameters: this.extractParameters(routePath, content),
|
|
194
|
-
requestBody: this.extractRequestBody(content, method),
|
|
195
|
-
responses: this.extractResponses(content, method),
|
|
196
|
-
authentication: this.detectAuthentication(content),
|
|
197
|
-
rateLimit: this.detectRateLimit(content),
|
|
198
|
-
tags: this.extractTags(routePath),
|
|
199
|
-
source: filePath
|
|
200
|
-
};
|
|
201
|
-
endpoints.push(endpoint);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return endpoints;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Scan Next.js Pages Router routes
|
|
209
|
-
*/
|
|
210
|
-
scanNextJsPagesRouter(apiDir) {
|
|
211
|
-
const endpoints = [];
|
|
212
|
-
|
|
213
|
-
const walk = (dir, basePath = '') => {
|
|
214
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
215
|
-
|
|
216
|
-
for (const entry of entries) {
|
|
217
|
-
const fullPath = path.join(dir, entry.name);
|
|
218
|
-
|
|
219
|
-
if (entry.isDirectory()) {
|
|
220
|
-
let segment = entry.name;
|
|
221
|
-
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
222
|
-
segment = ':' + segment.slice(1, -1);
|
|
223
|
-
}
|
|
224
|
-
walk(fullPath, `${basePath}/${segment}`);
|
|
225
|
-
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
|
|
226
|
-
let routeName = entry.name.replace(/\.(ts|js)$/, '');
|
|
227
|
-
|
|
228
|
-
// Handle dynamic routes
|
|
229
|
-
if (routeName.startsWith('[') && routeName.endsWith(']')) {
|
|
230
|
-
routeName = ':' + routeName.slice(1, -1);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Skip index routes in path
|
|
234
|
-
const routePath = routeName === 'index'
|
|
235
|
-
? basePath
|
|
236
|
-
: `${basePath}/${routeName}`;
|
|
237
|
-
|
|
238
|
-
const endpoint = this.parsePagesRouterFile(fullPath, routePath);
|
|
239
|
-
if (endpoint) {
|
|
240
|
-
endpoints.push(...endpoint);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
walk(apiDir);
|
|
247
|
-
return endpoints;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Parse Pages Router API file
|
|
252
|
-
*/
|
|
253
|
-
parsePagesRouterFile(filePath, routePath) {
|
|
254
|
-
const endpoints = [];
|
|
255
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
256
|
-
|
|
257
|
-
// Detect methods from switch/if statements
|
|
258
|
-
const detectedMethods = this.detectMethodsFromContent(content);
|
|
259
|
-
|
|
260
|
-
for (const method of detectedMethods) {
|
|
261
|
-
endpoints.push({
|
|
262
|
-
path: `/api${routePath}`,
|
|
263
|
-
method,
|
|
264
|
-
description: this.extractDescription(content, method),
|
|
265
|
-
parameters: this.extractParameters(routePath, content),
|
|
266
|
-
requestBody: this.extractRequestBody(content, method),
|
|
267
|
-
responses: this.extractResponses(content, method),
|
|
268
|
-
authentication: this.detectAuthentication(content),
|
|
269
|
-
rateLimit: this.detectRateLimit(content),
|
|
270
|
-
tags: this.extractTags(routePath),
|
|
271
|
-
source: filePath
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return endpoints;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Scan Express-style routes
|
|
280
|
-
*/
|
|
281
|
-
scanExpressRoutes(routesDir) {
|
|
282
|
-
const endpoints = [];
|
|
283
|
-
|
|
284
|
-
const walk = (dir) => {
|
|
285
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
286
|
-
|
|
287
|
-
for (const entry of entries) {
|
|
288
|
-
const fullPath = path.join(dir, entry.name);
|
|
289
|
-
|
|
290
|
-
if (entry.isDirectory()) {
|
|
291
|
-
walk(fullPath);
|
|
292
|
-
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
|
|
293
|
-
const routeEndpoints = this.parseExpressRouteFile(fullPath);
|
|
294
|
-
endpoints.push(...routeEndpoints);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
walk(routesDir);
|
|
300
|
-
return endpoints;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Parse Express route file
|
|
305
|
-
*/
|
|
306
|
-
parseExpressRouteFile(filePath) {
|
|
307
|
-
const endpoints = [];
|
|
308
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
309
|
-
|
|
310
|
-
// Match router.get, router.post, etc.
|
|
311
|
-
const routePattern = /router\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
312
|
-
let match;
|
|
313
|
-
|
|
314
|
-
while ((match = routePattern.exec(content)) !== null) {
|
|
315
|
-
const method = match[1].toUpperCase();
|
|
316
|
-
const routePath = match[2];
|
|
317
|
-
|
|
318
|
-
endpoints.push({
|
|
319
|
-
path: routePath,
|
|
320
|
-
method,
|
|
321
|
-
description: '',
|
|
322
|
-
parameters: this.extractParameters(routePath, content),
|
|
323
|
-
requestBody: method !== 'GET' && method !== 'DELETE' ? { type: 'object' } : null,
|
|
324
|
-
responses: { 200: { description: 'Success' } },
|
|
325
|
-
authentication: this.detectAuthentication(content),
|
|
326
|
-
tags: this.extractTags(routePath),
|
|
327
|
-
source: filePath
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return endpoints;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Detect methods from content
|
|
336
|
-
*/
|
|
337
|
-
detectMethodsFromContent(content) {
|
|
338
|
-
const methods = new Set();
|
|
339
|
-
|
|
340
|
-
// Check for method handling patterns
|
|
341
|
-
if (content.includes("method === 'GET'") || content.includes('case "GET"') || content.includes("case 'GET'")) {
|
|
342
|
-
methods.add('GET');
|
|
343
|
-
}
|
|
344
|
-
if (content.includes("method === 'POST'") || content.includes('case "POST"') || content.includes("case 'POST'")) {
|
|
345
|
-
methods.add('POST');
|
|
346
|
-
}
|
|
347
|
-
if (content.includes("method === 'PUT'") || content.includes('case "PUT"') || content.includes("case 'PUT'")) {
|
|
348
|
-
methods.add('PUT');
|
|
349
|
-
}
|
|
350
|
-
if (content.includes("method === 'PATCH'") || content.includes('case "PATCH"') || content.includes("case 'PATCH'")) {
|
|
351
|
-
methods.add('PATCH');
|
|
352
|
-
}
|
|
353
|
-
if (content.includes("method === 'DELETE'") || content.includes('case "DELETE"') || content.includes("case 'DELETE'")) {
|
|
354
|
-
methods.add('DELETE');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// If no specific methods found, assume all common ones
|
|
358
|
-
if (methods.size === 0) {
|
|
359
|
-
methods.add('GET');
|
|
360
|
-
methods.add('POST');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return Array.from(methods);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Extract description from comments
|
|
368
|
-
*/
|
|
369
|
-
extractDescription(content, method) {
|
|
370
|
-
// Look for JSDoc comment before method
|
|
371
|
-
const pattern = new RegExp(`\\/\\*\\*([\\s\\S]*?)\\*\\/\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${method}`, 'i');
|
|
372
|
-
const match = content.match(pattern);
|
|
373
|
-
|
|
374
|
-
if (match) {
|
|
375
|
-
// Extract description from JSDoc
|
|
376
|
-
const jsdoc = match[1];
|
|
377
|
-
const descMatch = jsdoc.match(/@description\s+(.+)/);
|
|
378
|
-
if (descMatch) {
|
|
379
|
-
return descMatch[1].trim();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Or first line
|
|
383
|
-
const lines = jsdoc.split('\n').map(l => l.replace(/^\s*\*\s?/, '').trim()).filter(Boolean);
|
|
384
|
-
if (lines.length > 0 && !lines[0].startsWith('@')) {
|
|
385
|
-
return lines[0];
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return '';
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Extract parameters from route path and content
|
|
394
|
-
*/
|
|
395
|
-
extractParameters(routePath, content) {
|
|
396
|
-
const parameters = [];
|
|
397
|
-
|
|
398
|
-
// Path parameters
|
|
399
|
-
const pathParams = routePath.match(/:(\w+)/g) || [];
|
|
400
|
-
for (const param of pathParams) {
|
|
401
|
-
parameters.push({
|
|
402
|
-
name: param.slice(1),
|
|
403
|
-
in: 'path',
|
|
404
|
-
required: true,
|
|
405
|
-
type: 'string',
|
|
406
|
-
description: ''
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Query parameters (heuristic)
|
|
411
|
-
const queryMatches = content.match(/(?:searchParams|query)\.get\s*\(\s*['"`](\w+)['"`]\)/g) || [];
|
|
412
|
-
for (const match of queryMatches) {
|
|
413
|
-
const paramMatch = match.match(/['"`](\w+)['"`]/);
|
|
414
|
-
if (paramMatch) {
|
|
415
|
-
const name = paramMatch[1];
|
|
416
|
-
if (!parameters.some(p => p.name === name)) {
|
|
417
|
-
parameters.push({
|
|
418
|
-
name,
|
|
419
|
-
in: 'query',
|
|
420
|
-
required: false,
|
|
421
|
-
type: 'string',
|
|
422
|
-
description: ''
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return parameters;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Extract request body schema
|
|
433
|
-
*/
|
|
434
|
-
extractRequestBody(content, method) {
|
|
435
|
-
if (method === 'GET' || method === 'DELETE') {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Look for Zod schema
|
|
440
|
-
const zodMatch = content.match(/const\s+(\w+Schema)\s*=\s*z\.object\s*\(\s*\{([^}]+)\}/);
|
|
441
|
-
if (zodMatch) {
|
|
442
|
-
return {
|
|
443
|
-
schema: zodMatch[1],
|
|
444
|
-
type: 'object',
|
|
445
|
-
description: 'Request body validated with Zod'
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Look for type annotation
|
|
450
|
-
const typeMatch = content.match(/body\s*:\s*(\w+)/);
|
|
451
|
-
if (typeMatch) {
|
|
452
|
-
return {
|
|
453
|
-
type: typeMatch[1],
|
|
454
|
-
description: ''
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
type: 'object',
|
|
460
|
-
description: 'Request body'
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Extract response schemas
|
|
466
|
-
*/
|
|
467
|
-
extractResponses(content, method) {
|
|
468
|
-
const responses = {};
|
|
469
|
-
|
|
470
|
-
// Look for NextResponse.json or res.status calls
|
|
471
|
-
const responsePattern = /(?:NextResponse\.json|res\.status)\s*\(\s*(?:(\d+)\s*,\s*)?/g;
|
|
472
|
-
let match;
|
|
473
|
-
|
|
474
|
-
while ((match = responsePattern.exec(content)) !== null) {
|
|
475
|
-
const status = match[1] || '200';
|
|
476
|
-
if (!responses[status]) {
|
|
477
|
-
responses[status] = {
|
|
478
|
-
description: RESPONSE_CODES[status]?.description || 'Response'
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Add defaults
|
|
484
|
-
if (Object.keys(responses).length === 0) {
|
|
485
|
-
responses['200'] = { description: 'Success' };
|
|
486
|
-
if (method !== 'GET') {
|
|
487
|
-
responses['400'] = { description: 'Bad Request' };
|
|
488
|
-
}
|
|
489
|
-
responses['500'] = { description: 'Internal Server Error' };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return responses;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Detect authentication requirements
|
|
497
|
-
*/
|
|
498
|
-
detectAuthentication(content) {
|
|
499
|
-
if (content.includes('getServerSession') || content.includes('auth()') || content.includes('getSession')) {
|
|
500
|
-
return { required: true, type: 'session' };
|
|
501
|
-
}
|
|
502
|
-
if (content.includes('Authorization') || content.includes('Bearer')) {
|
|
503
|
-
return { required: true, type: 'bearer' };
|
|
504
|
-
}
|
|
505
|
-
if (content.includes('apiKey') || content.includes('x-api-key')) {
|
|
506
|
-
return { required: true, type: 'apiKey' };
|
|
507
|
-
}
|
|
508
|
-
return { required: false };
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Detect rate limiting
|
|
513
|
-
*/
|
|
514
|
-
detectRateLimit(content) {
|
|
515
|
-
if (content.includes('rateLimit') || content.includes('rateLimiter')) {
|
|
516
|
-
return { enabled: true };
|
|
517
|
-
}
|
|
518
|
-
return { enabled: false };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Extract tags from route path
|
|
523
|
-
*/
|
|
524
|
-
extractTags(routePath) {
|
|
525
|
-
const segments = routePath.split('/').filter(Boolean);
|
|
526
|
-
if (segments.length > 0) {
|
|
527
|
-
// Use first segment as tag, excluding dynamic parts
|
|
528
|
-
const tag = segments[0].replace(/^:/, '');
|
|
529
|
-
return [tag.charAt(0).toUpperCase() + tag.slice(1)];
|
|
530
|
-
}
|
|
531
|
-
return ['General'];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Generate API.md content
|
|
536
|
-
*/
|
|
537
|
-
generate(data = null) {
|
|
538
|
-
if (!data) {
|
|
539
|
-
data = this.loadApiData();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const now = new Date().toISOString().split('T')[0];
|
|
543
|
-
const sections = [];
|
|
544
|
-
|
|
545
|
-
// Header
|
|
546
|
-
sections.push(`# API Documentation
|
|
547
|
-
|
|
548
|
-
**Project:** ${data.project}
|
|
549
|
-
**Version:** ${data.version}
|
|
550
|
-
**Base URL:** \`${data.baseUrl}\`
|
|
551
|
-
**Last Updated:** ${now}
|
|
552
|
-
|
|
553
|
-
---
|
|
554
|
-
|
|
555
|
-
## Overview
|
|
556
|
-
|
|
557
|
-
This document provides a complete reference for the ${data.project} API endpoints.
|
|
558
|
-
|
|
559
|
-
### Authentication
|
|
560
|
-
|
|
561
|
-
${data.authentication?.type === 'bearer' ? `
|
|
562
|
-
Most endpoints require authentication via Bearer token:
|
|
563
|
-
|
|
564
|
-
\`\`\`
|
|
565
|
-
Authorization: Bearer <token>
|
|
566
|
-
\`\`\`
|
|
567
|
-
` : data.authentication?.type === 'session' ? `
|
|
568
|
-
Authentication is handled via session cookies. Use the auth endpoints to obtain a session.
|
|
569
|
-
` : `
|
|
570
|
-
Authentication requirements vary by endpoint. Check individual endpoint documentation.
|
|
571
|
-
`}
|
|
572
|
-
|
|
573
|
-
### Rate Limiting
|
|
574
|
-
|
|
575
|
-
${data.rateLimit?.enabled ? `
|
|
576
|
-
API requests are rate limited to ${data.rateLimit.limit || 100} requests per ${data.rateLimit.window || 'minute'}.
|
|
577
|
-
` : `
|
|
578
|
-
Rate limiting may be applied to certain endpoints.
|
|
579
|
-
`}
|
|
580
|
-
|
|
581
|
-
---`);
|
|
582
|
-
|
|
583
|
-
// Table of Contents
|
|
584
|
-
const endpointsByTag = this.groupEndpointsByTag(data.endpoints);
|
|
585
|
-
sections.push(`## Endpoints
|
|
586
|
-
|
|
587
|
-
${Object.entries(endpointsByTag).map(([tag, endpoints]) => {
|
|
588
|
-
return `### ${tag}
|
|
589
|
-
|
|
590
|
-
${endpoints.map(e => `- [\`${e.method}\` ${e.path}](#${this.slugify(e.method + '-' + e.path)})`).join('\n')}`;
|
|
591
|
-
}).join('\n\n')}
|
|
592
|
-
|
|
593
|
-
---`);
|
|
594
|
-
|
|
595
|
-
// Endpoint Details
|
|
596
|
-
sections.push('## Endpoint Reference\n');
|
|
597
|
-
|
|
598
|
-
for (const [tag, endpoints] of Object.entries(endpointsByTag)) {
|
|
599
|
-
sections.push(`### ${tag}\n`);
|
|
600
|
-
|
|
601
|
-
for (const endpoint of endpoints) {
|
|
602
|
-
sections.push(this.formatEndpoint(endpoint));
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// Schemas
|
|
607
|
-
if (Object.keys(data.schemas).length > 0) {
|
|
608
|
-
sections.push(`## Schemas
|
|
609
|
-
|
|
610
|
-
${Object.entries(data.schemas).map(([name, schema]) => `
|
|
611
|
-
### ${name}
|
|
612
|
-
|
|
613
|
-
\`\`\`typescript
|
|
614
|
-
${JSON.stringify(schema, null, 2)}
|
|
615
|
-
\`\`\`
|
|
616
|
-
`).join('\n')}
|
|
617
|
-
|
|
618
|
-
---`);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Error Handling
|
|
622
|
-
sections.push(`## Error Handling
|
|
623
|
-
|
|
624
|
-
All endpoints return errors in a consistent format:
|
|
625
|
-
|
|
626
|
-
\`\`\`json
|
|
627
|
-
{
|
|
628
|
-
"error": {
|
|
629
|
-
"code": "ERROR_CODE",
|
|
630
|
-
"message": "Human-readable error message",
|
|
631
|
-
"details": {}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
\`\`\`
|
|
635
|
-
|
|
636
|
-
### Common Error Codes
|
|
637
|
-
|
|
638
|
-
| Code | HTTP Status | Description |
|
|
639
|
-
|------|-------------|-------------|
|
|
640
|
-
| UNAUTHORIZED | 401 | Authentication required |
|
|
641
|
-
| FORBIDDEN | 403 | Insufficient permissions |
|
|
642
|
-
| NOT_FOUND | 404 | Resource not found |
|
|
643
|
-
| VALIDATION_ERROR | 422 | Invalid request data |
|
|
644
|
-
| RATE_LIMITED | 429 | Too many requests |
|
|
645
|
-
| INTERNAL_ERROR | 500 | Server error |
|
|
646
|
-
|
|
647
|
-
---`);
|
|
648
|
-
|
|
649
|
-
// Footer
|
|
650
|
-
sections.push(`---
|
|
651
|
-
|
|
652
|
-
*Generated by [Bootspring](https://bootspring.com) API Documentation Generator*
|
|
653
|
-
|
|
654
|
-
### Commands
|
|
655
|
-
|
|
656
|
-
\`\`\`bash
|
|
657
|
-
bootspring docs api # Generate API documentation
|
|
658
|
-
bootspring docs api --scan # Rescan routes
|
|
659
|
-
bootspring docs api --json # Output as JSON
|
|
660
|
-
\`\`\`
|
|
661
|
-
`);
|
|
662
|
-
|
|
663
|
-
return sections.join('\n\n');
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* Group endpoints by tag
|
|
668
|
-
*/
|
|
669
|
-
groupEndpointsByTag(endpoints) {
|
|
670
|
-
const grouped = {};
|
|
671
|
-
|
|
672
|
-
for (const endpoint of endpoints) {
|
|
673
|
-
const tag = endpoint.tags?.[0] || 'General';
|
|
674
|
-
if (!grouped[tag]) {
|
|
675
|
-
grouped[tag] = [];
|
|
676
|
-
}
|
|
677
|
-
grouped[tag].push(endpoint);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Sort endpoints by path within each group
|
|
681
|
-
for (const tag of Object.keys(grouped)) {
|
|
682
|
-
grouped[tag].sort((a, b) => a.path.localeCompare(b.path));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
return grouped;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Format single endpoint
|
|
690
|
-
*/
|
|
691
|
-
formatEndpoint(endpoint) {
|
|
692
|
-
const methodBadge = this.getMethodBadge(endpoint.method);
|
|
693
|
-
|
|
694
|
-
let md = `#### ${methodBadge} ${endpoint.path}
|
|
695
|
-
|
|
696
|
-
${endpoint.description || '_No description available._'}
|
|
697
|
-
|
|
698
|
-
`;
|
|
699
|
-
|
|
700
|
-
// Authentication
|
|
701
|
-
if (endpoint.authentication?.required) {
|
|
702
|
-
md += `**Authentication:** ${endpoint.authentication.type === 'bearer' ? '🔐 Bearer Token' : endpoint.authentication.type === 'session' ? '🔐 Session' : '🔐 Required'}\n\n`;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// Parameters
|
|
706
|
-
if (endpoint.parameters && endpoint.parameters.length > 0) {
|
|
707
|
-
md += `**Parameters:**
|
|
708
|
-
|
|
709
|
-
| Name | In | Type | Required | Description |
|
|
710
|
-
|------|-----|------|----------|-------------|
|
|
711
|
-
${endpoint.parameters.map(p => `| \`${p.name}\` | ${p.in} | ${p.type} | ${p.required ? 'Yes' : 'No'} | ${p.description || '-'} |`).join('\n')}
|
|
712
|
-
|
|
713
|
-
`;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Request Body
|
|
717
|
-
if (endpoint.requestBody) {
|
|
718
|
-
md += `**Request Body:**
|
|
719
|
-
|
|
720
|
-
\`\`\`json
|
|
721
|
-
{
|
|
722
|
-
// ${endpoint.requestBody.description || endpoint.requestBody.type || 'Request body'}
|
|
723
|
-
}
|
|
724
|
-
\`\`\`
|
|
725
|
-
|
|
726
|
-
`;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Responses
|
|
730
|
-
if (endpoint.responses) {
|
|
731
|
-
md += `**Responses:**
|
|
732
|
-
|
|
733
|
-
| Status | Description |
|
|
734
|
-
|--------|-------------|
|
|
735
|
-
${Object.entries(endpoint.responses).map(([code, res]) => `| ${code} | ${res.description} |`).join('\n')}
|
|
736
|
-
|
|
737
|
-
`;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Example
|
|
741
|
-
md += `**Example:**
|
|
742
|
-
|
|
743
|
-
\`\`\`bash
|
|
744
|
-
curl -X ${endpoint.method} "${this.baseUrl}${endpoint.path}"${endpoint.authentication?.required ? ' \\\n -H "Authorization: Bearer <token>"' : ''}${endpoint.requestBody ? ' \\\n -H "Content-Type: application/json" \\\n -d \'{"key": "value"}\'' : ''}
|
|
745
|
-
\`\`\`
|
|
746
|
-
|
|
747
|
-
---
|
|
748
|
-
|
|
749
|
-
`;
|
|
750
|
-
|
|
751
|
-
return md;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Get method badge
|
|
756
|
-
*/
|
|
757
|
-
getMethodBadge(method) {
|
|
758
|
-
const colors = {
|
|
759
|
-
GET: '🟢',
|
|
760
|
-
POST: '🟡',
|
|
761
|
-
PUT: '🔵',
|
|
762
|
-
PATCH: '🟣',
|
|
763
|
-
DELETE: '🔴'
|
|
764
|
-
};
|
|
765
|
-
return `${colors[method] || '⚪'} \`${method}\``;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Slugify string
|
|
770
|
-
*/
|
|
771
|
-
slugify(str) {
|
|
772
|
-
return str
|
|
773
|
-
.toLowerCase()
|
|
774
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
775
|
-
.replace(/^-|-$/g, '');
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Scan and generate
|
|
780
|
-
*/
|
|
781
|
-
async scanAndGenerate() {
|
|
782
|
-
const endpoints = await this.scanRoutes();
|
|
783
|
-
const data = this.loadApiData();
|
|
784
|
-
data.endpoints = endpoints;
|
|
785
|
-
this.saveApiData(data);
|
|
786
|
-
return this.generate(data);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Generate API.md
|
|
792
|
-
*/
|
|
793
|
-
async function generate(options = {}) {
|
|
794
|
-
const generator = new ApiDocsGenerator(options);
|
|
795
|
-
|
|
796
|
-
if (options.scan !== false) {
|
|
797
|
-
return generator.scanAndGenerate();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const data = generator.loadApiData();
|
|
801
|
-
return generator.generate(data);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Scan routes only
|
|
806
|
-
*/
|
|
807
|
-
async function scanRoutes(options = {}) {
|
|
808
|
-
const generator = new ApiDocsGenerator(options);
|
|
809
|
-
return generator.scanRoutes();
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Load API data
|
|
814
|
-
*/
|
|
815
|
-
function loadApiData(options = {}) {
|
|
816
|
-
const generator = new ApiDocsGenerator(options);
|
|
817
|
-
return generator.loadApiData();
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
module.exports = {
|
|
821
|
-
ApiDocsGenerator,
|
|
822
|
-
generate,
|
|
823
|
-
scanRoutes,
|
|
824
|
-
loadApiData,
|
|
825
|
-
HTTP_METHODS,
|
|
826
|
-
RESPONSE_CODES
|
|
827
|
-
};
|