BMAD-METHOD/workspace-utils/health.js

549 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* BMAD Workspace Health Check Utility
* Cross-IDE workspace health monitoring and diagnostics
*/
const fs = require('fs');
const path = require('path');
/**
* Check directory structure integrity
*/
function checkDirectoryStructure(workspacePath) {
const requiredDirs = [
{ name: 'sessions', critical: true, description: 'Session management' },
{ name: 'context', critical: true, description: 'Shared context storage' },
{ name: 'handoffs', critical: true, description: 'Agent handoff coordination' },
{ name: 'decisions', critical: false, description: 'Decision tracking' },
{ name: 'progress', critical: false, description: 'Progress monitoring' },
{ name: 'quality', critical: false, description: 'Quality reports' },
{ name: 'archive', critical: false, description: 'Archived data' },
{ name: 'hooks', critical: false, description: 'Integration hooks' },
{ name: 'templates', critical: false, description: 'Workspace templates' },
{ name: 'logs', critical: true, description: 'Activity logging' }
];
const results = {
score: 100,
issues: [],
missing: [],
present: []
};
for (const dir of requiredDirs) {
const dirPath = path.join(workspacePath, dir.name);
if (fs.existsSync(dirPath)) {
results.present.push(dir);
} else {
results.missing.push(dir);
const penalty = dir.critical ? 15 : 5;
results.score -= penalty;
results.issues.push(`Missing ${dir.critical ? 'critical' : 'optional'} directory: ${dir.name} (${dir.description})`);
}
}
return results;
}
/**
* Check workspace configuration
*/
function checkWorkspaceConfig(workspacePath) {
const configPath = path.join(workspacePath, 'workspace-config.json');
const results = {
score: 100,
issues: [],
valid: false,
config: null
};
if (!fs.existsSync(configPath)) {
results.score = 0;
results.issues.push('Missing workspace configuration file');
return results;
}
try {
const configContent = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configContent);
// Validate required fields
const requiredFields = ['version', 'created', 'features', 'settings'];
for (const field of requiredFields) {
if (!config[field]) {
results.score -= 20;
results.issues.push(`Missing required config field: ${field}`);
}
}
// Check feature flags
if (config.features) {
const expectedFeatures = ['crossIDESupport', 'sessionManagement', 'contextPersistence', 'agentHandoffs'];
for (const feature of expectedFeatures) {
if (config.features[feature] !== true) {
results.score -= 5;
results.issues.push(`Feature not enabled: ${feature}`);
}
}
}
results.valid = true;
results.config = config;
} catch (e) {
results.score = 0;
results.issues.push(`Corrupted configuration file: ${e.message}`);
}
return results;
}
/**
* Check session health
*/
function checkSessionHealth(workspacePath) {
const sessionsPath = path.join(workspacePath, 'sessions');
const results = {
score: 100,
issues: [],
totalSessions: 0,
activeSessions: 0,
staleSessions: 0,
corruptedSessions: 0,
sessions: []
};
if (!fs.existsSync(sessionsPath)) {
results.score = 0;
results.issues.push('Sessions directory not found');
return results;
}
const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json'));
results.totalSessions = sessionFiles.length;
const now = new Date();
for (const file of sessionFiles) {
try {
const sessionPath = path.join(sessionsPath, file);
const sessionContent = fs.readFileSync(sessionPath, 'utf8');
const sessionData = JSON.parse(sessionContent);
// Validate session structure
const requiredFields = ['id', 'created', 'lastHeartbeat', 'ide', 'user'];
let isValid = true;
for (const field of requiredFields) {
if (!sessionData[field]) {
isValid = false;
break;
}
}
if (!isValid) {
results.corruptedSessions++;
results.score -= 5;
results.issues.push(`Invalid session structure: ${file}`);
continue;
}
// Check session freshness
const lastHeartbeat = new Date(sessionData.lastHeartbeat);
const timeDiff = now - lastHeartbeat;
if (timeDiff < 3600000) { // 1 hour
results.activeSessions++;
sessionData.status = 'active';
} else if (timeDiff < 86400000) { // 24 hours
sessionData.status = 'idle';
} else {
results.staleSessions++;
sessionData.status = 'stale';
}
sessionData.timeSinceLastHeartbeat = timeDiff;
results.sessions.push(sessionData);
} catch (e) {
results.corruptedSessions++;
results.score -= 10;
results.issues.push(`Corrupted session file: ${file}`);
}
}
// Penalty for too many stale sessions
if (results.staleSessions > 5) {
results.score -= (results.staleSessions - 5) * 2;
results.issues.push(`Excessive stale sessions: ${results.staleSessions}`);
}
return results;
}
/**
* Check file system permissions
*/
function checkFileSystemPermissions(workspacePath) {
const results = {
score: 100,
issues: [],
canRead: false,
canWrite: false,
canExecute: false
};
try {
// Test read permission
fs.readdirSync(workspacePath);
results.canRead = true;
// Test write permission
const testFile = path.join(workspacePath, '.health-check-write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
results.canWrite = true;
// Test execute permission (create and run a temporary script)
const testScript = path.join(workspacePath, '.health-check-exec-test.js');
fs.writeFileSync(testScript, 'console.log("test");');
// Try to require the file to test execution capability
require(testScript);
fs.unlinkSync(testScript);
results.canExecute = true;
} catch (e) {
if (!results.canRead) {
results.score = 0;
results.issues.push('Cannot read workspace directory');
} else if (!results.canWrite) {
results.score -= 50;
results.issues.push('Cannot write to workspace directory');
} else if (!results.canExecute) {
results.score -= 20;
results.issues.push('Limited script execution permissions');
}
}
return results;
}
/**
* Check log file health
*/
function checkLogHealth(workspacePath) {
const logPath = path.join(workspacePath, 'logs', 'workspace.log');
const results = {
score: 100,
issues: [],
exists: false,
size: 0,
recentEntries: 0,
corruptedEntries: 0
};
if (!fs.existsSync(logPath)) {
results.score -= 30;
results.issues.push('Workspace log file not found');
return results;
}
try {
const stats = fs.statSync(logPath);
results.exists = true;
results.size = stats.size;
// Check log size
const logSizeMB = stats.size / (1024 * 1024);
if (logSizeMB > 50) {
results.score -= 15;
results.issues.push(`Large log file: ${logSizeMB.toFixed(1)}MB`);
}
// Analyze recent log entries
const logContent = fs.readFileSync(logPath, 'utf8');
const logLines = logContent.trim().split('\n');
const now = new Date();
const oneDayAgo = now - 86400000; // 24 hours
for (const line of logLines.slice(-100)) { // Check last 100 entries
if (line.trim() === '') continue;
try {
const entry = JSON.parse(line);
const entryTime = new Date(entry.timestamp);
if (entryTime > oneDayAgo) {
results.recentEntries++;
}
} catch (e) {
results.corruptedEntries++;
}
}
if (results.corruptedEntries > 10) {
results.score -= results.corruptedEntries;
results.issues.push(`Multiple corrupted log entries: ${results.corruptedEntries}`);
}
if (results.recentEntries === 0) {
results.score -= 20;
results.issues.push('No recent activity in logs');
}
} catch (e) {
results.score -= 25;
results.issues.push(`Cannot analyze log file: ${e.message}`);
}
return results;
}
/**
* Check cross-IDE compatibility features
*/
function checkCrossIDECompatibility(workspacePath) {
const results = {
score: 100,
issues: [],
ideSupport: {},
templateCount: 0,
hookCount: 0
};
// Check for IDE-specific templates
const templatesPath = path.join(workspacePath, 'templates');
if (fs.existsSync(templatesPath)) {
const templateFiles = fs.readdirSync(templatesPath).filter(f => f.endsWith('.md'));
results.templateCount = templateFiles.length;
const supportedIDEs = ['cursor', 'windsurf', 'vscode', 'trae', 'roo', 'cline', 'gemini', 'github-copilot'];
for (const ide of supportedIDEs) {
const ideTemplate = templateFiles.find(f => f.includes(ide));
results.ideSupport[ide] = !!ideTemplate;
if (!ideTemplate) {
results.score -= 5;
}
}
if (results.templateCount < 4) {
results.issues.push(`Limited IDE template support: ${results.templateCount} templates found`);
}
} else {
results.score -= 30;
results.issues.push('IDE templates directory not found');
}
// Check for integration hooks
const hooksPath = path.join(workspacePath, 'hooks');
if (fs.existsSync(hooksPath)) {
const hookFiles = fs.readdirSync(hooksPath);
results.hookCount = hookFiles.length;
if (results.hookCount === 0) {
results.score -= 10;
results.issues.push('No integration hooks configured');
}
}
return results;
}
/**
* Generate comprehensive health report
*/
function generateHealthReport(workspacePath) {
const report = {
timestamp: new Date().toISOString(),
overallScore: 0,
status: 'unknown',
checks: {
directoryStructure: checkDirectoryStructure(workspacePath),
workspaceConfig: checkWorkspaceConfig(workspacePath),
sessionHealth: checkSessionHealth(workspacePath),
fileSystemPermissions: checkFileSystemPermissions(workspacePath),
logHealth: checkLogHealth(workspacePath),
crossIDECompatibility: checkCrossIDECompatibility(workspacePath)
},
summary: {
totalIssues: 0,
criticalIssues: 0,
recommendations: []
}
};
// Calculate overall score and issues
const checks = Object.values(report.checks);
const totalScore = checks.reduce((sum, check) => sum + check.score, 0);
report.overallScore = Math.round(totalScore / checks.length);
// Collect all issues
const allIssues = checks.flatMap(check => check.issues || []);
report.summary.totalIssues = allIssues.length;
report.summary.criticalIssues = allIssues.filter(issue =>
issue.includes('Missing critical') ||
issue.includes('Cannot') ||
issue.includes('Corrupted')
).length;
// Determine status
if (report.overallScore >= 90) {
report.status = 'excellent';
} else if (report.overallScore >= 80) {
report.status = 'good';
} else if (report.overallScore >= 70) {
report.status = 'fair';
} else if (report.overallScore >= 60) {
report.status = 'poor';
} else {
report.status = 'critical';
}
// Generate recommendations
if (report.checks.directoryStructure.missing.length > 0) {
report.summary.recommendations.push('Run `npm run workspace-cleanup` to repair directory structure');
}
if (report.checks.sessionHealth.staleSessions > 5) {
report.summary.recommendations.push('Clean up stale sessions with `npm run workspace-cleanup`');
}
if (report.checks.logHealth.size > 52428800) { // 50MB
report.summary.recommendations.push('Archive large log files to improve performance');
}
if (report.checks.crossIDECompatibility.templateCount < 4) {
report.summary.recommendations.push('Generate additional IDE-specific templates for better compatibility');
}
if (report.summary.criticalIssues > 0) {
report.summary.recommendations.push('Address critical issues immediately before continuing development');
}
return report;
}
/**
* Display health report
*/
function displayHealthReport(report) {
const statusEmoji = {
excellent: '💚',
good: '💙',
fair: '💛',
poor: '🧡',
critical: '❤️'
};
console.log('🏥 BMAD Workspace Health Check');
console.log('==============================');
console.log(`${statusEmoji[report.status]} Overall Health: ${report.overallScore}/100 (${report.status.toUpperCase()})`);
console.log(`📊 Issues Found: ${report.summary.totalIssues} (${report.summary.criticalIssues} critical)`);
console.log(`🕐 Checked: ${new Date(report.timestamp).toLocaleString()}`);
// Display individual check results
console.log('\n📋 Detailed Results:');
Object.entries(report.checks).forEach(([checkName, result]) => {
const emoji = result.score >= 90 ? '✅' : result.score >= 70 ? '⚠️' : '❌';
const name = checkName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
console.log(`${emoji} ${name}: ${result.score}/100`);
if (result.issues && result.issues.length > 0) {
result.issues.slice(0, 3).forEach(issue => {
console.log(`${issue}`);
});
if (result.issues.length > 3) {
console.log(` • ... and ${result.issues.length - 3} more issues`);
}
}
});
// Show session summary
if (report.checks.sessionHealth) {
const sessions = report.checks.sessionHealth;
console.log(`\n👥 Sessions: ${sessions.totalSessions} total, ${sessions.activeSessions} active, ${sessions.staleSessions} stale`);
}
// Show recommendations
if (report.summary.recommendations.length > 0) {
console.log('\n💡 Recommendations:');
report.summary.recommendations.forEach(rec => {
console.log(`${rec}`);
});
}
// Quick actions
console.log('\n🚀 Quick Actions:');
console.log(' npm run workspace-cleanup # Repair and optimize workspace');
console.log(' npm run workspace-status # Check current activity');
console.log(' npm run workspace-sync # Synchronize context');
if (report.overallScore < 70) {
console.log('\n⚠ Workspace needs attention. Address the issues above for optimal performance.');
} else if (report.overallScore >= 90) {
console.log('\n🎉 Excellent! Your workspace is healthy and ready for collaborative development.');
}
}
/**
* Main health check function
*/
async function checkWorkspaceHealth(options = {}) {
try {
const workspacePath = path.join(process.cwd(), '.workspace');
if (!fs.existsSync(workspacePath)) {
console.error('❌ Workspace directory not found.');
console.error(' Run `npm run workspace-init` to initialize workspace');
process.exit(1);
}
const report = generateHealthReport(workspacePath);
if (options.json) {
console.log(JSON.stringify(report, null, 2));
return;
}
displayHealthReport(report);
// Save health report
const reportPath = path.join(workspacePath, 'quality', 'health-report.json');
const qualityDir = path.dirname(reportPath);
if (!fs.existsSync(qualityDir)) {
fs.mkdirSync(qualityDir, { recursive: true });
}
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📄 Detailed report saved: .workspace/quality/health-report.json`);
// Exit with appropriate code for CI/CD
if (options.exitCode && report.summary.criticalIssues > 0) {
process.exit(1);
}
} catch (error) {
console.error('❌ Failed to check workspace health:', error.message);
process.exit(1);
}
}
// Command line execution
if (require.main === module) {
const args = process.argv.slice(2);
const options = {
json: args.includes('--json'),
exitCode: args.includes('--exit-code'),
verbose: args.includes('--verbose')
};
checkWorkspaceHealth(options);
}
module.exports = { checkWorkspaceHealth, generateHealthReport };