@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,942 +0,0 @@
1
- /**
2
- * Bootspring HEALTH.md Generator
3
- *
4
- * Generates project health dashboard with code quality metrics,
5
- * test coverage, dependency health, security posture, and performance.
6
- *
7
- * @package bootspring
8
- * @module generators/health
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const { execSync } = require('child_process');
14
-
15
- /**
16
- * Health status levels
17
- */
18
- const HEALTH_STATUS = {
19
- EXCELLENT: { label: 'Excellent', emoji: '🟢', color: 'green', min: 90 },
20
- GOOD: { label: 'Good', emoji: '🟡', color: 'yellow', min: 70 },
21
- FAIR: { label: 'Fair', emoji: '🟠', color: 'orange', min: 50 },
22
- POOR: { label: 'Poor', emoji: '🔴', color: 'red', min: 0 }
23
- };
24
-
25
- /**
26
- * Metric categories
27
- */
28
- const METRIC_CATEGORIES = {
29
- CODE_QUALITY: { label: 'Code Quality', emoji: '📊', weight: 0.25 },
30
- TEST_COVERAGE: { label: 'Test Coverage', emoji: '🧪', weight: 0.25 },
31
- DEPENDENCIES: { label: 'Dependencies', emoji: '📦', weight: 0.20 },
32
- SECURITY: { label: 'Security', emoji: '🔐', weight: 0.20 },
33
- PERFORMANCE: { label: 'Performance', emoji: '⚡', weight: 0.10 }
34
- };
35
-
36
- /**
37
- * Health Document Generator
38
- */
39
- class HealthGenerator {
40
- constructor(options = {}) {
41
- this.projectRoot = options.projectRoot || process.cwd();
42
- this.healthDataPath = path.join(this.projectRoot, '.bootspring', 'health.json');
43
- this.includeHistory = options.includeHistory !== false;
44
- this.maxHistoryDays = options.maxHistoryDays || 30;
45
- }
46
-
47
- /**
48
- * Load health history data
49
- */
50
- loadHealthData() {
51
- try {
52
- if (fs.existsSync(this.healthDataPath)) {
53
- return JSON.parse(fs.readFileSync(this.healthDataPath, 'utf-8'));
54
- }
55
- } catch (_err) {
56
- // Return default
57
- }
58
-
59
- return {
60
- project: this.getProjectName(),
61
- history: [],
62
- metadata: {
63
- created: new Date().toISOString(),
64
- lastUpdated: null
65
- }
66
- };
67
- }
68
-
69
- /**
70
- * Save health data
71
- */
72
- saveHealthData(data) {
73
- data.metadata.lastUpdated = new Date().toISOString();
74
-
75
- const dir = path.dirname(this.healthDataPath);
76
- if (!fs.existsSync(dir)) {
77
- fs.mkdirSync(dir, { recursive: true });
78
- }
79
- fs.writeFileSync(this.healthDataPath, JSON.stringify(data, null, 2));
80
- }
81
-
82
- /**
83
- * Get project name
84
- */
85
- getProjectName() {
86
- try {
87
- const pkgPath = path.join(this.projectRoot, 'package.json');
88
- if (fs.existsSync(pkgPath)) {
89
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
90
- return pkg.name || 'Project';
91
- }
92
- } catch (_err) {
93
- // Default
94
- }
95
- return 'Project';
96
- }
97
-
98
- /**
99
- * Collect all health metrics
100
- */
101
- async collectMetrics() {
102
- const metrics = {
103
- timestamp: new Date().toISOString(),
104
- codeQuality: await this.collectCodeQualityMetrics(),
105
- testCoverage: await this.collectTestCoverageMetrics(),
106
- dependencies: await this.collectDependencyMetrics(),
107
- security: await this.collectSecurityMetrics(),
108
- performance: await this.collectPerformanceMetrics()
109
- };
110
-
111
- // Calculate overall score
112
- metrics.overallScore = this.calculateOverallScore(metrics);
113
- metrics.status = this.getStatusFromScore(metrics.overallScore);
114
-
115
- return metrics;
116
- }
117
-
118
- /**
119
- * Collect code quality metrics
120
- */
121
- async collectCodeQualityMetrics() {
122
- const metrics = {
123
- score: 0,
124
- linting: { errors: 0, warnings: 0, passed: true },
125
- typescript: { errors: 0, strict: false },
126
- complexity: { average: 0, high: 0 },
127
- codeSmells: [],
128
- duplications: 0,
129
- filesAnalyzed: 0
130
- };
131
-
132
- try {
133
- // Check for eslint
134
- const eslintConfig = this.findFile(['.eslintrc', '.eslintrc.js', '.eslintrc.json', 'eslint.config.js']);
135
- if (eslintConfig) {
136
- try {
137
- const output = execSync('npx eslint . --format json --quiet 2>/dev/null || true', {
138
- cwd: this.projectRoot,
139
- encoding: 'utf-8',
140
- timeout: 30000
141
- });
142
- const results = JSON.parse(output || '[]');
143
- metrics.linting.errors = results.reduce((sum, r) => sum + r.errorCount, 0);
144
- metrics.linting.warnings = results.reduce((sum, r) => sum + r.warningCount, 0);
145
- metrics.linting.passed = metrics.linting.errors === 0;
146
- metrics.filesAnalyzed = results.length;
147
- } catch (_err) {
148
- // ESLint failed or not configured
149
- }
150
- }
151
-
152
- // Check for TypeScript
153
- const tsConfig = this.findFile(['tsconfig.json']);
154
- if (tsConfig) {
155
- metrics.typescript.configured = true;
156
- try {
157
- const tsConfigContent = JSON.parse(fs.readFileSync(tsConfig, 'utf-8'));
158
- metrics.typescript.strict = tsConfigContent.compilerOptions?.strict === true;
159
- } catch (_err) {
160
- // Ignore
161
- }
162
-
163
- try {
164
- const output = execSync('npx tsc --noEmit 2>&1 || true', {
165
- cwd: this.projectRoot,
166
- encoding: 'utf-8',
167
- timeout: 60000
168
- });
169
- const errorCount = (output.match(/error TS\d+:/g) || []).length;
170
- metrics.typescript.errors = errorCount;
171
- } catch (_err) {
172
- // TypeScript check failed
173
- }
174
- }
175
-
176
- // Calculate score
177
- let score = 100;
178
-
179
- // Deduct for lint errors
180
- score -= Math.min(30, metrics.linting.errors * 2);
181
- score -= Math.min(10, metrics.linting.warnings * 0.5);
182
-
183
- // Deduct for TypeScript errors
184
- score -= Math.min(20, metrics.typescript.errors * 2);
185
-
186
- // Bonus for strict TypeScript
187
- if (metrics.typescript.strict) {
188
- score += 5;
189
- }
190
-
191
- metrics.score = Math.max(0, Math.min(100, Math.round(score)));
192
- } catch (_err) {
193
- metrics.score = 50; // Default if analysis fails
194
- }
195
-
196
- return metrics;
197
- }
198
-
199
- /**
200
- * Collect test coverage metrics
201
- */
202
- async collectTestCoverageMetrics() {
203
- const metrics = {
204
- score: 0,
205
- coverage: null,
206
- testFiles: 0,
207
- totalTests: 0,
208
- passingTests: 0,
209
- failingTests: 0,
210
- skippedTests: 0,
211
- coverageReport: null
212
- };
213
-
214
- try {
215
- // Count test files
216
- const testPatterns = ['**/*.test.js', '**/*.test.ts', '**/*.spec.js', '**/*.spec.ts', '**/__tests__/**/*.js'];
217
- for (const pattern of testPatterns) {
218
- const files = this.globSync(pattern);
219
- metrics.testFiles += files.length;
220
- }
221
-
222
- // Look for coverage report
223
- const coveragePaths = [
224
- 'coverage/coverage-summary.json',
225
- 'coverage/lcov-report/index.html',
226
- '.nyc_output/coverage.json'
227
- ];
228
-
229
- for (const coveragePath of coveragePaths) {
230
- const fullPath = path.join(this.projectRoot, coveragePath);
231
- if (fs.existsSync(fullPath)) {
232
- if (coveragePath.endsWith('.json')) {
233
- try {
234
- const coverage = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
235
- if (coverage.total) {
236
- metrics.coverage = {
237
- lines: coverage.total.lines?.pct || 0,
238
- statements: coverage.total.statements?.pct || 0,
239
- branches: coverage.total.branches?.pct || 0,
240
- functions: coverage.total.functions?.pct || 0
241
- };
242
- }
243
- } catch (_err) {
244
- // Ignore
245
- }
246
- }
247
- metrics.coverageReport = coveragePath;
248
- break;
249
- }
250
- }
251
-
252
- // Calculate score
253
- if (metrics.coverage) {
254
- // Average of all coverage types
255
- const avgCoverage = (
256
- metrics.coverage.lines +
257
- metrics.coverage.statements +
258
- metrics.coverage.branches +
259
- metrics.coverage.functions
260
- ) / 4;
261
- metrics.score = Math.round(avgCoverage);
262
- } else if (metrics.testFiles > 0) {
263
- // Some tests but no coverage report
264
- metrics.score = 40;
265
- } else {
266
- // No tests
267
- metrics.score = 0;
268
- }
269
- } catch (_err) {
270
- metrics.score = 0;
271
- }
272
-
273
- return metrics;
274
- }
275
-
276
- /**
277
- * Collect dependency metrics
278
- */
279
- async collectDependencyMetrics() {
280
- const metrics = {
281
- score: 100,
282
- total: 0,
283
- direct: 0,
284
- dev: 0,
285
- outdated: [],
286
- vulnerabilities: { critical: 0, high: 0, medium: 0, low: 0 },
287
- deprecated: [],
288
- duplicates: 0
289
- };
290
-
291
- try {
292
- const pkgPath = path.join(this.projectRoot, 'package.json');
293
- if (fs.existsSync(pkgPath)) {
294
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
295
- metrics.direct = Object.keys(pkg.dependencies || {}).length;
296
- metrics.dev = Object.keys(pkg.devDependencies || {}).length;
297
- metrics.total = metrics.direct + metrics.dev;
298
-
299
- // Check for outdated packages
300
- try {
301
- const output = execSync('npm outdated --json 2>/dev/null || echo "{}"', {
302
- cwd: this.projectRoot,
303
- encoding: 'utf-8',
304
- timeout: 30000
305
- });
306
- const outdated = JSON.parse(output);
307
- metrics.outdated = Object.keys(outdated).map(name => ({
308
- name,
309
- current: outdated[name].current,
310
- wanted: outdated[name].wanted,
311
- latest: outdated[name].latest
312
- }));
313
- } catch (_err) {
314
- // npm outdated failed
315
- }
316
-
317
- // Check for vulnerabilities
318
- try {
319
- const output = execSync('npm audit --json 2>/dev/null || echo "{}"', {
320
- cwd: this.projectRoot,
321
- encoding: 'utf-8',
322
- timeout: 30000
323
- });
324
- const audit = JSON.parse(output);
325
- if (audit.metadata?.vulnerabilities) {
326
- metrics.vulnerabilities = audit.metadata.vulnerabilities;
327
- }
328
- } catch (_err) {
329
- // npm audit failed
330
- }
331
- }
332
-
333
- // Calculate score
334
- let score = 100;
335
-
336
- // Deduct for vulnerabilities
337
- score -= metrics.vulnerabilities.critical * 20;
338
- score -= metrics.vulnerabilities.high * 10;
339
- score -= metrics.vulnerabilities.medium * 3;
340
- score -= metrics.vulnerabilities.low * 1;
341
-
342
- // Deduct for outdated (minor deduction)
343
- score -= Math.min(15, metrics.outdated.length * 0.5);
344
-
345
- metrics.score = Math.max(0, Math.min(100, Math.round(score)));
346
- } catch (_err) {
347
- metrics.score = 50;
348
- }
349
-
350
- return metrics;
351
- }
352
-
353
- /**
354
- * Collect security metrics
355
- */
356
- async collectSecurityMetrics() {
357
- const metrics = {
358
- score: 100,
359
- secretsExposed: 0,
360
- authConfigured: false,
361
- httpsEnforced: false,
362
- envVarsSecure: true,
363
- csrfProtection: false,
364
- inputValidation: false,
365
- issues: []
366
- };
367
-
368
- try {
369
- // Check for secrets in code
370
- const secretPatterns = [
371
- /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/gi,
372
- /secret\s*[:=]\s*['"][^'"]+['"]/gi,
373
- /password\s*[:=]\s*['"][^'"]+['"]/gi,
374
- /token\s*[:=]\s*['"][^'"]+['"]/gi
375
- ];
376
-
377
- const srcFiles = this.globSync('**/*.{js,ts,jsx,tsx}', ['node_modules', 'dist', 'build']);
378
- for (const file of srcFiles.slice(0, 100)) {
379
- try {
380
- const content = fs.readFileSync(file, 'utf-8');
381
- for (const pattern of secretPatterns) {
382
- const matches = content.match(pattern) || [];
383
- if (matches.length > 0) {
384
- // Filter out common false positives
385
- const realMatches = matches.filter(m =>
386
- !m.includes('process.env') &&
387
- !m.includes('${') &&
388
- !m.includes("''") &&
389
- !m.includes('""')
390
- );
391
- metrics.secretsExposed += realMatches.length;
392
- }
393
- }
394
- } catch (_err) {
395
- // Skip file
396
- }
397
- }
398
-
399
- // Check for auth configuration
400
- const authFiles = ['lib/auth.ts', 'lib/auth.js', 'auth.config.ts', 'auth.config.js', 'next-auth.config.ts'];
401
- for (const authFile of authFiles) {
402
- if (fs.existsSync(path.join(this.projectRoot, authFile))) {
403
- metrics.authConfigured = true;
404
- break;
405
- }
406
- }
407
-
408
- // Check for middleware (CSRF, etc.)
409
- if (fs.existsSync(path.join(this.projectRoot, 'middleware.ts')) ||
410
- fs.existsSync(path.join(this.projectRoot, 'middleware.js'))) {
411
- metrics.csrfProtection = true;
412
- }
413
-
414
- // Check for Zod or other validation
415
- const pkgPath = path.join(this.projectRoot, 'package.json');
416
- if (fs.existsSync(pkgPath)) {
417
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
418
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
419
- if (deps.zod || deps.yup || deps.joi) {
420
- metrics.inputValidation = true;
421
- }
422
- }
423
-
424
- // Check for .env.example
425
- if (fs.existsSync(path.join(this.projectRoot, '.env.example'))) {
426
- metrics.envVarsSecure = true;
427
- }
428
-
429
- // Calculate score
430
- let score = 100;
431
-
432
- // Major deductions
433
- score -= metrics.secretsExposed * 20;
434
-
435
- // Minor deductions
436
- if (!metrics.authConfigured) {
437
- metrics.issues.push('No authentication configuration detected');
438
- score -= 10;
439
- }
440
- if (!metrics.inputValidation) {
441
- metrics.issues.push('No input validation library detected (zod, yup, joi)');
442
- score -= 10;
443
- }
444
- if (!metrics.csrfProtection) {
445
- metrics.issues.push('No middleware detected for CSRF protection');
446
- score -= 5;
447
- }
448
-
449
- metrics.score = Math.max(0, Math.min(100, Math.round(score)));
450
- } catch (_err) {
451
- metrics.score = 50;
452
- }
453
-
454
- return metrics;
455
- }
456
-
457
- /**
458
- * Collect performance metrics
459
- */
460
- async collectPerformanceMetrics() {
461
- const metrics = {
462
- score: 70, // Default score
463
- bundleSize: null,
464
- buildTime: null,
465
- lighthouseScore: null,
466
- issues: []
467
- };
468
-
469
- try {
470
- // Check for bundle analysis
471
- const bundleAnalysis = path.join(this.projectRoot, '.next', 'analyze', 'client.html');
472
- if (fs.existsSync(bundleAnalysis)) {
473
- metrics.bundleAnalysis = true;
474
- }
475
-
476
- // Check build output size
477
- const buildDirs = ['dist', 'build', '.next'];
478
- for (const dir of buildDirs) {
479
- const buildPath = path.join(this.projectRoot, dir);
480
- if (fs.existsSync(buildPath)) {
481
- try {
482
- const size = this.getDirectorySize(buildPath);
483
- metrics.bundleSize = size;
484
- break;
485
- } catch (_err) {
486
- // Ignore
487
- }
488
- }
489
- }
490
-
491
- // Check for performance optimizations
492
- const pkgPath = path.join(this.projectRoot, 'package.json');
493
- if (fs.existsSync(pkgPath)) {
494
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
495
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
496
-
497
- // Check for common performance tools
498
- if (deps['@next/bundle-analyzer'] || deps['webpack-bundle-analyzer']) {
499
- metrics.score += 5;
500
- }
501
- if (deps['lighthouse'] || deps['@lhci/cli']) {
502
- metrics.score += 5;
503
- }
504
- }
505
-
506
- // Check next.config for optimizations
507
- const nextConfig = path.join(this.projectRoot, 'next.config.js');
508
- if (fs.existsSync(nextConfig)) {
509
- const content = fs.readFileSync(nextConfig, 'utf-8');
510
- if (content.includes('images:') && content.includes('domains:')) {
511
- metrics.score += 5;
512
- }
513
- }
514
-
515
- metrics.score = Math.min(100, metrics.score);
516
- } catch (_err) {
517
- // Keep default score
518
- }
519
-
520
- return metrics;
521
- }
522
-
523
- /**
524
- * Calculate overall health score
525
- */
526
- calculateOverallScore(metrics) {
527
- let totalWeight = 0;
528
- let weightedScore = 0;
529
-
530
- for (const [key, config] of Object.entries(METRIC_CATEGORIES)) {
531
- const metricKey = this.categoryToMetricKey(key);
532
- const score = metrics[metricKey]?.score || 0;
533
- weightedScore += score * config.weight;
534
- totalWeight += config.weight;
535
- }
536
-
537
- return Math.round(weightedScore / totalWeight);
538
- }
539
-
540
- /**
541
- * Get status from score
542
- */
543
- getStatusFromScore(score) {
544
- for (const [key, config] of Object.entries(HEALTH_STATUS)) {
545
- if (score >= config.min) {
546
- return key;
547
- }
548
- }
549
- return 'POOR';
550
- }
551
-
552
- /**
553
- * Map category to metric key
554
- */
555
- categoryToMetricKey(category) {
556
- const mapping = {
557
- CODE_QUALITY: 'codeQuality',
558
- TEST_COVERAGE: 'testCoverage',
559
- DEPENDENCIES: 'dependencies',
560
- SECURITY: 'security',
561
- PERFORMANCE: 'performance'
562
- };
563
- return mapping[category];
564
- }
565
-
566
- /**
567
- * Generate HEALTH.md content
568
- */
569
- generate(metrics) {
570
- const status = HEALTH_STATUS[metrics.status];
571
- const now = new Date().toISOString().split('T')[0];
572
-
573
- const sections = [];
574
-
575
- // Header
576
- sections.push(`# Project Health Dashboard
577
-
578
- **Project:** ${this.getProjectName()}
579
- **Last Updated:** ${now}
580
- **Overall Status:** ${status.emoji} ${status.label} (${metrics.overallScore}/100)
581
-
582
- ---`);
583
-
584
- // Overall Score
585
- const scoreBar = this.generateProgressBar(metrics.overallScore);
586
- sections.push(`## Overall Health Score
587
-
588
- ${scoreBar} **${metrics.overallScore}%**
589
-
590
- | Category | Score | Status |
591
- |----------|-------|--------|
592
- ${Object.entries(METRIC_CATEGORIES).map(([key, config]) => {
593
- const metricKey = this.categoryToMetricKey(key);
594
- const score = metrics[metricKey]?.score || 0;
595
- const catStatus = this.getStatusFromScore(score);
596
- const statusConfig = HEALTH_STATUS[catStatus];
597
- return `| ${config.emoji} ${config.label} | ${score}/100 | ${statusConfig.emoji} ${statusConfig.label} |`;
598
- }).join('\n')}
599
-
600
- ---`);
601
-
602
- // Code Quality
603
- const cq = metrics.codeQuality;
604
- sections.push(`## ${METRIC_CATEGORIES.CODE_QUALITY.emoji} Code Quality
605
-
606
- **Score:** ${cq.score}/100
607
-
608
- | Metric | Value |
609
- |--------|-------|
610
- | Lint Errors | ${cq.linting.errors} |
611
- | Lint Warnings | ${cq.linting.warnings} |
612
- | TypeScript Errors | ${cq.typescript.errors || 'N/A'} |
613
- | Strict Mode | ${cq.typescript.strict ? '✅ Yes' : '❌ No'} |
614
- | Files Analyzed | ${cq.filesAnalyzed} |
615
-
616
- ${cq.linting.errors > 0 ? `
617
- ### Issues to Fix
618
-
619
- - Run \`npm run lint -- --fix\` to auto-fix lint errors
620
- ` : ''}
621
-
622
- ---`);
623
-
624
- // Test Coverage
625
- const tc = metrics.testCoverage;
626
- sections.push(`## ${METRIC_CATEGORIES.TEST_COVERAGE.emoji} Test Coverage
627
-
628
- **Score:** ${tc.score}/100
629
-
630
- | Metric | Value |
631
- |--------|-------|
632
- | Test Files | ${tc.testFiles} |
633
- | Line Coverage | ${tc.coverage?.lines ? tc.coverage.lines + '%' : 'N/A'} |
634
- | Statement Coverage | ${tc.coverage?.statements ? tc.coverage.statements + '%' : 'N/A'} |
635
- | Branch Coverage | ${tc.coverage?.branches ? tc.coverage.branches + '%' : 'N/A'} |
636
- | Function Coverage | ${tc.coverage?.functions ? tc.coverage.functions + '%' : 'N/A'} |
637
-
638
- ${tc.coverage ? this.generateCoverageChart(tc.coverage) : '_No coverage data available. Run `npm test -- --coverage` to generate._'}
639
-
640
- ---`);
641
-
642
- // Dependencies
643
- const deps = metrics.dependencies;
644
- sections.push(`## ${METRIC_CATEGORIES.DEPENDENCIES.emoji} Dependencies
645
-
646
- **Score:** ${deps.score}/100
647
-
648
- | Metric | Value |
649
- |--------|-------|
650
- | Total Dependencies | ${deps.total} |
651
- | Direct | ${deps.direct} |
652
- | Dev | ${deps.dev} |
653
- | Outdated | ${deps.outdated.length} |
654
-
655
- ### Vulnerabilities
656
-
657
- | Severity | Count |
658
- |----------|-------|
659
- | 🔴 Critical | ${deps.vulnerabilities.critical || 0} |
660
- | 🟠 High | ${deps.vulnerabilities.high || 0} |
661
- | 🟡 Medium | ${deps.vulnerabilities.medium || 0} |
662
- | 🟢 Low | ${deps.vulnerabilities.low || 0} |
663
-
664
- ${deps.outdated.length > 0 ? `
665
- ### Outdated Packages (${deps.outdated.length})
666
-
667
- | Package | Current | Wanted | Latest |
668
- |---------|---------|--------|--------|
669
- ${deps.outdated.slice(0, 10).map(p => `| ${p.name} | ${p.current} | ${p.wanted} | ${p.latest} |`).join('\n')}
670
- ${deps.outdated.length > 10 ? `\n_...and ${deps.outdated.length - 10} more_` : ''}
671
- ` : '✅ All packages are up to date'}
672
-
673
- ---`);
674
-
675
- // Security
676
- const sec = metrics.security;
677
- sections.push(`## ${METRIC_CATEGORIES.SECURITY.emoji} Security
678
-
679
- **Score:** ${sec.score}/100
680
-
681
- | Check | Status |
682
- |-------|--------|
683
- | Secrets Exposed | ${sec.secretsExposed === 0 ? '✅ None found' : `❌ ${sec.secretsExposed} potential issues`} |
684
- | Authentication | ${sec.authConfigured ? '✅ Configured' : '⚠️ Not detected'} |
685
- | Input Validation | ${sec.inputValidation ? '✅ Configured' : '⚠️ Not detected'} |
686
- | CSRF Protection | ${sec.csrfProtection ? '✅ Middleware found' : '⚠️ Not detected'} |
687
-
688
- ${sec.issues.length > 0 ? `
689
- ### Issues
690
-
691
- ${sec.issues.map(i => `- ⚠️ ${i}`).join('\n')}
692
- ` : ''}
693
-
694
- ---`);
695
-
696
- // Performance
697
- const perf = metrics.performance;
698
- sections.push(`## ${METRIC_CATEGORIES.PERFORMANCE.emoji} Performance
699
-
700
- **Score:** ${perf.score}/100
701
-
702
- | Metric | Value |
703
- |--------|-------|
704
- | Bundle Size | ${perf.bundleSize ? this.formatBytes(perf.bundleSize) : 'N/A'} |
705
- | Build Time | ${perf.buildTime ? perf.buildTime + 's' : 'N/A'} |
706
- | Lighthouse Score | ${perf.lighthouseScore || 'N/A'} |
707
-
708
- ### Recommendations
709
-
710
- - Run \`npm run build\` and check bundle size
711
- - Use \`@next/bundle-analyzer\` to identify large dependencies
712
- - Run Lighthouse audits for production performance
713
-
714
- ---`);
715
-
716
- // Trends (if history available)
717
- const healthData = this.loadHealthData();
718
- if (healthData.history.length > 1) {
719
- const recent = healthData.history.slice(-7);
720
- sections.push(`## Trends (Last 7 Days)
721
-
722
- \`\`\`
723
- ${this.generateTrendChart(recent)}
724
- \`\`\`
725
-
726
- ---`);
727
- }
728
-
729
- // Footer
730
- sections.push(`---
731
-
732
- *Generated by [Bootspring](https://bootspring.com) Health Generator*
733
-
734
- ### Commands
735
-
736
- \`\`\`bash
737
- bootspring docs health # Generate this report
738
- bootspring docs health --json # Output as JSON
739
- bootspring docs health --watch # Continuous monitoring
740
- \`\`\`
741
- `);
742
-
743
- return sections.join('\n\n');
744
- }
745
-
746
- /**
747
- * Generate progress bar
748
- */
749
- generateProgressBar(percent, width = 20) {
750
- const filled = Math.round((percent / 100) * width);
751
- const empty = width - filled;
752
- return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
753
- }
754
-
755
- /**
756
- * Generate coverage chart
757
- */
758
- generateCoverageChart(coverage) {
759
- return `
760
- \`\`\`
761
- Lines: ${this.generateProgressBar(coverage.lines)} ${coverage.lines}%
762
- Statements: ${this.generateProgressBar(coverage.statements)} ${coverage.statements}%
763
- Branches: ${this.generateProgressBar(coverage.branches)} ${coverage.branches}%
764
- Functions: ${this.generateProgressBar(coverage.functions)} ${coverage.functions}%
765
- \`\`\`
766
- `;
767
- }
768
-
769
- /**
770
- * Generate trend chart
771
- */
772
- generateTrendChart(history) {
773
- const height = 8;
774
- const maxScore = 100;
775
-
776
- const chart = [];
777
- for (let row = 0; row <= height; row++) {
778
- const scoreThreshold = maxScore - (row * (maxScore / height));
779
- let line = `${Math.round(scoreThreshold).toString().padStart(3)} |`;
780
-
781
- for (const entry of history) {
782
- const score = entry.overallScore || 0;
783
- if (score >= scoreThreshold - (maxScore / height / 2) && score < scoreThreshold + (maxScore / height / 2)) {
784
- line += '●';
785
- } else if (score > scoreThreshold) {
786
- line += '│';
787
- } else {
788
- line += ' ';
789
- }
790
- }
791
- chart.push(line);
792
- }
793
-
794
- chart.push(' +' + '─'.repeat(history.length));
795
-
796
- return chart.join('\n');
797
- }
798
-
799
- /**
800
- * Format bytes
801
- */
802
- formatBytes(bytes) {
803
- if (bytes < 1024) return bytes + ' B';
804
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
805
- return (bytes / 1024 / 1024).toFixed(1) + ' MB';
806
- }
807
-
808
- /**
809
- * Find file in project
810
- */
811
- findFile(names) {
812
- for (const name of names) {
813
- const filePath = path.join(this.projectRoot, name);
814
- if (fs.existsSync(filePath)) {
815
- return filePath;
816
- }
817
- }
818
- return null;
819
- }
820
-
821
- /**
822
- * Simple glob sync
823
- */
824
- globSync(pattern, ignore = []) {
825
- const files = [];
826
- const baseDir = this.projectRoot;
827
-
828
- const walk = (dir) => {
829
- try {
830
- const entries = fs.readdirSync(dir, { withFileTypes: true });
831
- for (const entry of entries) {
832
- const fullPath = path.join(dir, entry.name);
833
- const relativePath = path.relative(baseDir, fullPath);
834
-
835
- // Skip ignored
836
- if (ignore.some(i => relativePath.includes(i))) continue;
837
-
838
- if (entry.isDirectory()) {
839
- walk(fullPath);
840
- } else if (entry.isFile()) {
841
- // Simple pattern matching
842
- if (this.matchPattern(relativePath, pattern)) {
843
- files.push(fullPath);
844
- }
845
- }
846
- }
847
- } catch (_err) {
848
- // Skip inaccessible directories
849
- }
850
- };
851
-
852
- walk(baseDir);
853
- return files.slice(0, 500); // Limit for performance
854
- }
855
-
856
- /**
857
- * Match file against pattern
858
- */
859
- matchPattern(file, pattern) {
860
- // Convert glob to regex
861
- const regex = pattern
862
- .replace(/\*\*/g, '.*')
863
- .replace(/\*/g, '[^/]*')
864
- .replace(/\./g, '\\.')
865
- .replace(/\{([^}]+)\}/g, (_, p) => `(${p.split(',').join('|')})`);
866
-
867
- return new RegExp(regex).test(file);
868
- }
869
-
870
- /**
871
- * Get directory size
872
- */
873
- getDirectorySize(dirPath) {
874
- let size = 0;
875
- const walk = (dir) => {
876
- const entries = fs.readdirSync(dir, { withFileTypes: true });
877
- for (const entry of entries) {
878
- const fullPath = path.join(dir, entry.name);
879
- if (entry.isDirectory()) {
880
- walk(fullPath);
881
- } else {
882
- size += fs.statSync(fullPath).size;
883
- }
884
- }
885
- };
886
- walk(dirPath);
887
- return size;
888
- }
889
-
890
- /**
891
- * Record metrics to history
892
- */
893
- recordMetrics(metrics) {
894
- const data = this.loadHealthData();
895
-
896
- data.history.push({
897
- date: new Date().toISOString().split('T')[0],
898
- overallScore: metrics.overallScore,
899
- codeQuality: metrics.codeQuality.score,
900
- testCoverage: metrics.testCoverage.score,
901
- dependencies: metrics.dependencies.score,
902
- security: metrics.security.score,
903
- performance: metrics.performance.score
904
- });
905
-
906
- // Keep only last N days
907
- const cutoff = new Date();
908
- cutoff.setDate(cutoff.getDate() - this.maxHistoryDays);
909
- data.history = data.history.filter(h => new Date(h.date) >= cutoff);
910
-
911
- this.saveHealthData(data);
912
- }
913
- }
914
-
915
- /**
916
- * Generate HEALTH.md
917
- */
918
- async function generate(options = {}) {
919
- const generator = new HealthGenerator(options);
920
- const metrics = await generator.collectMetrics();
921
-
922
- // Record to history
923
- generator.recordMetrics(metrics);
924
-
925
- return generator.generate(metrics);
926
- }
927
-
928
- /**
929
- * Collect metrics only
930
- */
931
- async function collectMetrics(options = {}) {
932
- const generator = new HealthGenerator(options);
933
- return generator.collectMetrics();
934
- }
935
-
936
- module.exports = {
937
- HealthGenerator,
938
- generate,
939
- collectMetrics,
940
- HEALTH_STATUS,
941
- METRIC_CATEGORIES
942
- };