@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/health.js
DELETED
|
@@ -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
|
-
};
|