@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.
Files changed (59) hide show
  1. package/README.md +9 -403
  2. package/bin/bootspring.js +1 -96
  3. package/dist/cli/index.js +65134 -0
  4. package/dist/cli-launcher.js +92 -0
  5. package/dist/core/index.d.ts +2110 -5582
  6. package/dist/core/index.js +2 -0
  7. package/dist/core.js +21123 -5413
  8. package/dist/mcp/index.d.ts +357 -1
  9. package/dist/mcp/index.js +2 -0
  10. package/dist/mcp-server.js +51948 -1976
  11. package/package.json +27 -63
  12. package/scripts/postinstall.cjs +144 -0
  13. package/LICENSE +0 -29
  14. package/dist/cli/index.cjs +0 -20776
  15. package/generators/api-docs.js +0 -827
  16. package/generators/decisions.js +0 -655
  17. package/generators/generate.js +0 -595
  18. package/generators/health.js +0 -942
  19. package/generators/index.ts +0 -82
  20. package/generators/presets/full.js +0 -28
  21. package/generators/presets/index.js +0 -12
  22. package/generators/presets/minimal.js +0 -29
  23. package/generators/presets/standard.js +0 -28
  24. package/generators/questionnaire.js +0 -414
  25. package/generators/sections/advanced.js +0 -136
  26. package/generators/sections/ai.js +0 -106
  27. package/generators/sections/auth.js +0 -89
  28. package/generators/sections/backend.js +0 -146
  29. package/generators/sections/business.js +0 -118
  30. package/generators/sections/content.js +0 -300
  31. package/generators/sections/deployment.js +0 -139
  32. package/generators/sections/features.js +0 -122
  33. package/generators/sections/frontend.js +0 -118
  34. package/generators/sections/identity.js +0 -76
  35. package/generators/sections/index.js +0 -40
  36. package/generators/sections/instructions.js +0 -146
  37. package/generators/sections/payments.js +0 -104
  38. package/generators/sections/plugins.js +0 -142
  39. package/generators/sections/pre-build.js +0 -130
  40. package/generators/sections/security.js +0 -127
  41. package/generators/sections/technical.js +0 -171
  42. package/generators/sections/testing.js +0 -125
  43. package/generators/sections/workflow.js +0 -104
  44. package/generators/sprint.js +0 -675
  45. package/generators/templates/agents.template.js +0 -199
  46. package/generators/templates/assistant-context.template.js +0 -83
  47. package/generators/templates/build-planning.template.js +0 -708
  48. package/generators/templates/claude.template.js +0 -379
  49. package/generators/templates/content.template.js +0 -819
  50. package/generators/templates/index.js +0 -16
  51. package/generators/templates/planning.template.js +0 -515
  52. package/generators/templates/seed.template.js +0 -109
  53. package/generators/visual-doc-generator.js +0 -910
  54. package/scripts/postinstall.js +0 -197
  55. /package/{claude-commands → assets/claude-commands}/agent.md +0 -0
  56. /package/{claude-commands → assets/claude-commands}/bs.md +0 -0
  57. /package/{claude-commands → assets/claude-commands}/build.md +0 -0
  58. /package/{claude-commands → assets/claude-commands}/skill.md +0 -0
  59. /package/{claude-commands → assets/claude-commands}/todo.md +0 -0
@@ -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
- };