BMAD-METHOD/tools/installer/lib/claude-code-maintenance-sys...

765 lines
21 KiB
JavaScript

const path = require('path');
const fs = require('fs');
/**
* Claude Code CLI Built-in Maintenance System
* Provides automatic workspace repair, optimization, and health monitoring
* specifically designed for Claude Code CLI users
*/
class ClaudeCodeMaintenanceSystem {
constructor(workspaceDir) {
this.workspaceDir = workspaceDir;
this.maintenanceLog = [];
this.healthMetrics = {
lastCheck: null,
overallHealth: 100,
issues: [],
optimizations: []
};
this.autoRepairEnabled = true;
this.backgroundOptimization = true;
}
/**
* Perform comprehensive workspace integrity check on session startup
*/
async performStartupIntegrityCheck() {
console.log('🔍 Performing workspace integrity check...');
const checkResults = {
timestamp: new Date().toISOString(),
checks: [],
issues: [],
repairs: [],
optimizations: [],
overallStatus: 'healthy'
};
try {
// Check workspace directory structure
await this.checkDirectoryStructure(checkResults);
// Check file integrity
await this.checkFileIntegrity(checkResults);
// Check session cleanup
await this.checkSessionCleanup(checkResults);
// Check context file sizes
await this.checkContextSizes(checkResults);
// Check handoff integrity
await this.checkHandoffIntegrity(checkResults);
// Auto-repair issues if enabled
if (this.autoRepairEnabled && checkResults.issues.length > 0) {
await this.performAutoRepair(checkResults);
}
// Update health metrics
this.updateHealthMetrics(checkResults);
// Log results
this.logMaintenanceActivity('startup-integrity-check', checkResults);
// Display results to user
this.displayIntegrityResults(checkResults);
return checkResults;
} catch (error) {
console.error('❌ Integrity check failed:', error.message);
checkResults.overallStatus = 'failed';
checkResults.error = error.message;
return checkResults;
}
}
/**
* Check and repair workspace directory structure
*/
async checkDirectoryStructure(results) {
const requiredDirs = [
'.workspace',
'.workspace/sessions',
'.workspace/context',
'.workspace/handoffs',
'.workspace/decisions',
'.workspace/progress',
'.workspace/quality',
'.workspace/archive',
'.workspace/versions',
'.workspace/locks'
];
for (const dir of requiredDirs) {
const dirPath = path.join(this.workspaceDir, dir);
const exists = fs.existsSync(dirPath);
results.checks.push({
type: 'directory',
path: dir,
status: exists ? 'ok' : 'missing',
timestamp: new Date().toISOString()
});
if (!exists) {
results.issues.push({
type: 'missing_directory',
path: dir,
severity: 'medium',
description: `Required directory missing: ${dir}`
});
// Auto-repair: Create missing directory
try {
fs.mkdirSync(dirPath, { recursive: true });
results.repairs.push({
type: 'directory_created',
path: dir,
status: 'success',
description: `Created missing directory: ${dir}`
});
// Update check status
const checkIndex = results.checks.length - 1;
results.checks[checkIndex].status = 'repaired';
} catch (error) {
results.repairs.push({
type: 'directory_creation_failed',
path: dir,
status: 'failed',
error: error.message
});
}
}
}
}
/**
* Check file integrity and corruption
*/
async checkFileIntegrity(results) {
const criticalFiles = [
'.workspace/workspace-config.json',
'.workspace/context/shared-context.md',
'.workspace/decisions/decisions-log.md',
'.workspace/progress/progress-summary.md'
];
for (const file of criticalFiles) {
const filePath = path.join(this.workspaceDir, file);
const exists = fs.existsSync(filePath);
if (exists) {
try {
// Check if file is readable and valid
const content = fs.readFileSync(filePath, 'utf8');
// Validate JSON files
if (file.endsWith('.json')) {
JSON.parse(content);
}
results.checks.push({
type: 'file_integrity',
path: file,
status: 'ok',
size: content.length
});
} catch (error) {
results.issues.push({
type: 'corrupted_file',
path: file,
severity: 'high',
description: `File corrupted or unreadable: ${file}`,
error: error.message
});
results.checks.push({
type: 'file_integrity',
path: file,
status: 'corrupted',
error: error.message
});
// Auto-repair: Restore from backup or create default
await this.repairCorruptedFile(file, results);
}
} else {
// Critical file missing
results.issues.push({
type: 'missing_file',
path: file,
severity: 'medium',
description: `Critical file missing: ${file}`
});
// Auto-repair: Create default file
await this.createDefaultFile(file, results);
}
}
}
/**
* Check and cleanup old sessions
*/
async checkSessionCleanup(results) {
try {
const sessionsDir = path.join(this.workspaceDir, '.workspace', 'sessions');
if (!fs.existsSync(sessionsDir)) return;
const sessionFiles = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
const cutoffTime = Date.now() - (24 * 60 * 60 * 1000); // 24 hours ago
let cleanedSessions = 0;
for (const sessionFile of sessionFiles) {
const sessionPath = path.join(sessionsDir, sessionFile);
try {
const sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
const lastActivity = new Date(sessionData.lastActivity || sessionData.startTime).getTime();
if (lastActivity < cutoffTime && sessionData.status !== 'active') {
fs.unlinkSync(sessionPath);
cleanedSessions++;
results.optimizations.push({
type: 'session_cleanup',
file: sessionFile,
description: 'Removed old inactive session'
});
}
} catch (error) {
// Remove corrupted session file
fs.unlinkSync(sessionPath);
cleanedSessions++;
results.repairs.push({
type: 'corrupted_session_removed',
file: sessionFile,
status: 'success',
description: 'Removed corrupted session file'
});
}
}
results.checks.push({
type: 'session_cleanup',
status: 'completed',
sessionsProcessed: sessionFiles.length,
sessionsCleaned: cleanedSessions
});
} catch (error) {
results.checks.push({
type: 'session_cleanup',
status: 'failed',
error: error.message
});
}
}
/**
* Check context file sizes and optimize if needed
*/
async checkContextSizes(results) {
const contextFiles = [
'.workspace/context/shared-context.md',
'.workspace/decisions/decisions-log.md',
'.workspace/progress/progress-summary.md'
];
const sizeLimits = {
'shared-context.md': 10 * 1024 * 1024, // 10MB
'decisions-log.md': 5 * 1024 * 1024, // 5MB
'progress-summary.md': 3 * 1024 * 1024 // 3MB
};
for (const file of contextFiles) {
const filePath = path.join(this.workspaceDir, file);
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath);
const fileName = path.basename(file);
const sizeLimit = sizeLimits[fileName] || 10 * 1024 * 1024;
results.checks.push({
type: 'file_size',
path: file,
size: stats.size,
sizeLimit: sizeLimit,
status: stats.size > sizeLimit ? 'oversized' : 'ok'
});
if (stats.size > sizeLimit) {
results.issues.push({
type: 'oversized_file',
path: file,
severity: 'medium',
description: `File exceeds size limit: ${this.formatBytes(stats.size)} > ${this.formatBytes(sizeLimit)}`,
currentSize: stats.size,
sizeLimit: sizeLimit
});
// Auto-optimize: Archive and compress
await this.optimizeOversizedFile(file, results);
}
}
}
}
/**
* Check handoff file integrity
*/
async checkHandoffIntegrity(results) {
try {
const handoffsDir = path.join(this.workspaceDir, '.workspace', 'handoffs');
if (!fs.existsSync(handoffsDir)) return;
const handoffFiles = fs.readdirSync(handoffsDir).filter(f => f.endsWith('.json'));
let corruptedHandoffs = 0;
let expiredHandoffs = 0;
const expirationTime = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days ago
for (const handoffFile of handoffFiles) {
const handoffPath = path.join(handoffsDir, handoffFile);
try {
const handoffData = JSON.parse(fs.readFileSync(handoffPath, 'utf8'));
const handoffTime = new Date(handoffData.timestamp).getTime();
// Check if handoff is expired
if (handoffTime < expirationTime) {
fs.unlinkSync(handoffPath);
expiredHandoffs++;
results.optimizations.push({
type: 'handoff_cleanup',
file: handoffFile,
description: 'Removed expired handoff'
});
}
} catch (error) {
// Remove corrupted handoff file
fs.unlinkSync(handoffPath);
corruptedHandoffs++;
results.repairs.push({
type: 'corrupted_handoff_removed',
file: handoffFile,
status: 'success',
description: 'Removed corrupted handoff file'
});
}
}
results.checks.push({
type: 'handoff_integrity',
status: 'completed',
handoffsProcessed: handoffFiles.length,
corruptedRemoved: corruptedHandoffs,
expiredRemoved: expiredHandoffs
});
} catch (error) {
results.checks.push({
type: 'handoff_integrity',
status: 'failed',
error: error.message
});
}
}
/**
* Perform automatic repairs
*/
async performAutoRepair(results) {
console.log(`🔧 Auto-repairing ${results.issues.length} issues...`);
let repairedCount = 0;
for (const issue of results.issues) {
try {
switch (issue.type) {
case 'missing_directory':
// Already handled in checkDirectoryStructure
break;
case 'corrupted_file':
await this.repairCorruptedFile(issue.path, results);
repairedCount++;
break;
case 'missing_file':
await this.createDefaultFile(issue.path, results);
repairedCount++;
break;
case 'oversized_file':
await this.optimizeOversizedFile(issue.path, results);
repairedCount++;
break;
}
} catch (error) {
results.repairs.push({
type: 'repair_failed',
issue: issue.type,
path: issue.path,
status: 'failed',
error: error.message
});
}
}
if (repairedCount > 0) {
console.log(`✅ Auto-repaired ${repairedCount} issues`);
}
}
/**
* Repair corrupted file
*/
async repairCorruptedFile(filePath, results) {
const fullPath = path.join(this.workspaceDir, filePath);
try {
// Try to restore from backup if available
const backupPath = `${fullPath}.backup`;
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, fullPath);
results.repairs.push({
type: 'file_restored_from_backup',
path: filePath,
status: 'success',
description: 'Restored file from backup'
});
} else {
// Create default file
await this.createDefaultFile(filePath, results);
}
} catch (error) {
results.repairs.push({
type: 'file_repair_failed',
path: filePath,
status: 'failed',
error: error.message
});
}
}
/**
* Create default file
*/
async createDefaultFile(filePath, results) {
const fullPath = path.join(this.workspaceDir, filePath);
const fileName = path.basename(filePath);
try {
let defaultContent = '';
switch (fileName) {
case 'workspace-config.json':
defaultContent = JSON.stringify({
version: '1.0',
created: new Date().toISOString(),
structure: ['sessions', 'context', 'handoffs', 'decisions', 'progress', 'quality', 'archive'],
settings: {
maxContextSize: '10MB',
sessionTimeout: '2h',
archiveAfter: '30d',
maxConcurrentSessions: 5
}
}, null, 2);
break;
case 'shared-context.md':
defaultContent = `# Workspace Context
**Last Updated:** ${new Date().toISOString()}
**Active Sessions:** None
**Primary Agent:** unknown
## Current Focus
No current focus available.
## Key Decisions
- No decisions recorded yet
## Next Steps
- Initialize workspace and begin collaborative development
## Session Notes
No session notes available
`;
break;
case 'decisions-log.md':
defaultContent = `# Architectural & Design Decisions
No decisions recorded yet.
`;
break;
case 'progress-summary.md':
defaultContent = `# Development Progress Summary
**Last Updated:** ${new Date().toISOString()}
**Current Story:** No active story
**Overall Progress:** 0%
## Completed Tasks
None
## Active Tasks
None
## Blockers
None identified
## Quality Metrics
Not assessed
`;
break;
default:
defaultContent = `# ${fileName}
Default content created by Claude Code CLI maintenance system.
Created: ${new Date().toISOString()}
`;
}
fs.writeFileSync(fullPath, defaultContent);
results.repairs.push({
type: 'default_file_created',
path: filePath,
status: 'success',
description: `Created default ${fileName}`
});
} catch (error) {
results.repairs.push({
type: 'default_file_creation_failed',
path: filePath,
status: 'failed',
error: error.message
});
}
}
/**
* Optimize oversized file
*/
async optimizeOversizedFile(filePath, results) {
const fullPath = path.join(this.workspaceDir, filePath);
try {
// Create backup
const backupPath = `${fullPath}.backup`;
fs.copyFileSync(fullPath, backupPath);
// Archive old content
const archiveDir = path.join(this.workspaceDir, '.workspace', 'archive');
if (!fs.existsSync(archiveDir)) {
fs.mkdirSync(archiveDir, { recursive: true });
}
const archivePath = path.join(archiveDir, `${path.basename(filePath)}-${Date.now()}.md`);
fs.copyFileSync(fullPath, archivePath);
// Create condensed version
const content = fs.readFileSync(fullPath, 'utf8');
const condensedContent = this.condenseContent(content, path.basename(filePath));
fs.writeFileSync(fullPath, condensedContent);
results.optimizations.push({
type: 'file_optimized',
path: filePath,
description: 'File archived and condensed',
archivePath: archivePath,
originalSize: fs.statSync(backupPath).size,
newSize: fs.statSync(fullPath).size
});
} catch (error) {
results.repairs.push({
type: 'file_optimization_failed',
path: filePath,
status: 'failed',
error: error.message
});
}
}
/**
* Condense content for oversized files
*/
condenseContent(content, fileName) {
const timestamp = new Date().toISOString();
switch (fileName) {
case 'shared-context.md':
return `# Workspace Context (Condensed)
**Last Updated:** ${timestamp}
**Status:** Condensed due to size optimization
**Original Content:** Archived
## Current Focus
Previous context has been archived for size optimization.
Use *workspace-sync to reload if needed.
## Key Decisions
Most recent decisions preserved. Older decisions archived.
## Next Steps
- Review archived context if needed
- Continue with current development focus
## Session Notes
Content condensed - check archive for full history.
`;
case 'decisions-log.md':
// Keep last 10 decisions, archive the rest
const lines = content.split('\n');
const recentDecisions = lines.slice(-200); // Approximate last 10 decisions
return `# Architectural & Design Decisions (Condensed)
**Condensed:** ${timestamp}
**Full History:** Available in archive
${recentDecisions.join('\n')}
---
*Older decisions archived for size optimization*
`;
case 'progress-summary.md':
return `# Development Progress Summary (Condensed)
**Last Updated:** ${timestamp}
**Previous Content:** Archived for size optimization
## Current Status
Progress history has been archived.
Current session progress will be tracked from this point.
## Recent Activity
Previous activity archived - new tracking begins now.
## Quality Metrics
Historical metrics archived - current assessment required.
`;
default:
return `# ${fileName} (Condensed)
**Condensed:** ${timestamp}
**Reason:** File size optimization
Previous content has been archived.
New content will be tracked from this point forward.
`;
}
}
/**
* Update health metrics
*/
updateHealthMetrics(checkResults) {
this.healthMetrics.lastCheck = checkResults.timestamp;
this.healthMetrics.issues = checkResults.issues;
this.healthMetrics.optimizations = checkResults.optimizations;
// Calculate overall health score
const issueCount = checkResults.issues.length;
const repairCount = checkResults.repairs.filter(r => r.status === 'success').length;
if (issueCount === 0) {
this.healthMetrics.overallHealth = 100;
} else if (repairCount >= issueCount) {
this.healthMetrics.overallHealth = 90; // Issues but all repaired
} else {
this.healthMetrics.overallHealth = Math.max(50, 100 - (issueCount * 10));
}
}
/**
* Log maintenance activity
*/
logMaintenanceActivity(type, data) {
this.maintenanceLog.push({
type: type,
timestamp: new Date().toISOString(),
data: data
});
// Keep only last 100 log entries
if (this.maintenanceLog.length > 100) {
this.maintenanceLog = this.maintenanceLog.slice(-100);
}
}
/**
* Display integrity check results
*/
displayIntegrityResults(results) {
if (results.issues.length === 0 && results.optimizations.length === 0) {
console.log('✅ Workspace integrity check passed - all systems healthy');
return;
}
if (results.repairs.length > 0) {
const successfulRepairs = results.repairs.filter(r => r.status === 'success').length;
console.log(`🔧 Workspace maintenance completed: ${successfulRepairs} issues auto-repaired`);
}
if (results.optimizations.length > 0) {
console.log(`⚡ Workspace optimized: ${results.optimizations.length} optimizations applied`);
}
// Show remaining issues if any
const unrepairedIssues = results.issues.filter(issue =>
!results.repairs.some(repair => repair.path === issue.path && repair.status === 'success')
);
if (unrepairedIssues.length > 0) {
console.log(`⚠️ ${unrepairedIssues.length} issues require manual attention`);
}
}
/**
* Format bytes for display
*/
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Get maintenance summary
*/
getMaintenanceSummary() {
return {
healthMetrics: this.healthMetrics,
recentActivity: this.maintenanceLog.slice(-10),
autoRepairEnabled: this.autoRepairEnabled,
backgroundOptimization: this.backgroundOptimization
};
}
}
module.exports = ClaudeCodeMaintenanceSystem;