UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

1,232 lines (1,104 loc) 37.6 kB
/** * HealthChecker - Plugin health validation and monitoring * * Validates plugin integrity across manifest, directories, versions, and dependencies. * Returns health status ('healthy' | 'warning' | 'error') with detailed issue reports. * Caches health check results for 5 minutes to optimize performance. * Supports auto-repair for common issues (missing directories, corrupted manifests). * * @module tools/workspace/health-checker * @version 1.0.0 * @since 2025-10-19 * * @example * // Initialize health checker * const checker = new HealthChecker('.aiwg', registry); * * // Check single plugin * const result = await checker.checkPlugin('sdlc-complete'); * console.log(result.status); // 'healthy' | 'warning' | 'error' * console.log(result.issues); // Array of issue objects * * // Check all plugins * const summary = await checker.checkAll(); * console.log(summary.healthy); // Number of healthy plugins * console.log(summary.warnings); // Number with warnings * console.log(summary.errors); // Number with errors * * // Get detailed report * const report = await checker.getHealthReport('sdlc-complete'); * report.issues.forEach(issue => { * console.log(`[${issue.severity}] ${issue.check}: ${issue.message}`); * }); * * // Auto-repair plugin * const repairResult = await checker.repairPlugin('broken-plugin'); * if (repairResult.repaired) { * console.log('Repair actions:', repairResult.actions); * } * * @errors * - HealthCheckError: Health check validation failed * - PluginNotFoundError: Plugin ID not in registry (re-exported from PluginRegistry) */ import fs from 'fs/promises'; import path from 'path'; import { PluginRegistry, PluginNotFoundError } from './registry-manager.mjs'; // Custom error class class HealthCheckError extends Error { constructor(message, details = {}) { super(message); this.name = 'HealthCheckError'; this.details = details; } } /** * HealthChecker - Validate plugin health and integrity * * Performs comprehensive health checks across: * - Manifest integrity (existence, valid JSON, required fields) * - Directory structure (required directories present) * - Version compatibility (add-ons compatible with parent framework) * - Dependencies (parent frameworks exist, no circular dependencies) * - Disk usage (large plugin warnings) * * @class */ export class HealthChecker { /** * Create HealthChecker instance * * @param {string} [basePath='.aiwg'] - Base path to .aiwg workspace directory * @param {PluginRegistry} [registry] - Existing PluginRegistry instance (optional, will create if not provided) * * @example * const checker = new HealthChecker(); * const customChecker = new HealthChecker('/custom/path/.aiwg', existingRegistry); */ constructor(basePath = '.aiwg', registry = null) { this.basePath = path.resolve(basePath); this.registry = registry || new PluginRegistry(path.join(this.basePath, 'frameworks', 'registry.json')); // Cache settings this.cache = new Map(); // pluginId -> { status, issues, timestamp } this.cacheTTL = 5 * 60 * 1000; // 5 minutes in milliseconds // Health check thresholds this.diskUsageWarningThreshold = 500 * 1024 * 1024; // 500 MB } // =========================== // Health Checks // =========================== /** * Check plugin health * * Runs all validation checks and returns health status with issues. * Results cached for 5 minutes for performance. * * @param {string} pluginId - Plugin ID to check * * @returns {Promise<Object>} Health check result * @returns {string} .pluginId - Plugin ID * @returns {string} .status - Health status ('healthy' | 'warning' | 'error') * @returns {Object[]} .issues - Array of issue objects * @returns {string} .timestamp - ISO 8601 timestamp of check * * @throws {PluginNotFoundError} If plugin ID not in registry * * @example * const result = await checker.checkPlugin('sdlc-complete'); * if (result.status === 'error') { * console.error('Critical issues found:', result.issues); * } */ async checkPlugin(pluginId) { // Check cache first const cached = this.getCached(pluginId); if (cached) { return cached; } // Verify plugin exists in registry await this.registry.getPlugin(pluginId); // Throws PluginNotFoundError if missing const issues = []; let status = 'healthy'; // 1. Manifest integrity const manifestCheck = await this.validateManifest(pluginId); if (!manifestCheck.valid) { issues.push(...manifestCheck.issues); if (manifestCheck.issues.some(i => i.severity === 'error')) { status = 'error'; } else if (status === 'healthy') { status = 'warning'; } } // 2. Directory structure const dirCheck = await this.validateDirectories(pluginId); if (!dirCheck.valid) { issues.push(...dirCheck.issues); if (dirCheck.issues.some(i => i.severity === 'error')) { status = 'error'; } else if (status === 'healthy') { status = 'warning'; } } // 3. Version compatibility (for add-ons) const versionCheck = await this.validateVersionCompatibility(pluginId); if (!versionCheck.valid) { issues.push(...versionCheck.issues); if (versionCheck.issues.some(i => i.severity === 'error')) { status = 'error'; } else if (status === 'healthy') { status = 'warning'; } } // 4. Dependencies (for add-ons/extensions) const depCheck = await this.validateDependencies(pluginId); if (!depCheck.valid) { issues.push(...depCheck.issues); if (depCheck.issues.some(i => i.severity === 'error')) { status = 'error'; } else if (status === 'healthy') { status = 'warning'; } } // 5. Disk usage check (warning only) const diskUsage = await this._getDiskUsage(pluginId); if (diskUsage > this.diskUsageWarningThreshold) { issues.push({ check: 'disk-usage', severity: 'warning', message: `Large disk usage: ${this._formatBytes(diskUsage)}`, details: { diskUsage, threshold: this.diskUsageWarningThreshold } }); if (status === 'healthy') { status = 'warning'; } } const result = { pluginId, status, issues, timestamp: new Date().toISOString() }; // Cache result this.cache.set(pluginId, result); // Update registry health status try { await this.registry.updatePlugin(pluginId, { health: status, 'health-checked': result.timestamp }); } catch (error) { console.warn(`[HealthChecker] Failed to update registry health for '${pluginId}':`, error.message); } return result; } /** * Check all plugins * * Runs health checks on all installed plugins and returns summary statistics. * * @returns {Promise<Object>} Summary object * @returns {number} .total - Total number of plugins checked * @returns {number} .healthy - Number of healthy plugins * @returns {number} .warnings - Number with warnings * @returns {number} .errors - Number with errors * @returns {Object[]} .results - Array of individual health check results * * @example * const summary = await checker.checkAll(); * console.log(`${summary.healthy}/${summary.total} plugins healthy`); * summary.results.forEach(r => { * console.log(`${r.pluginId}: ${r.status}`); * }); */ async checkAll() { const plugins = await this.registry.listPlugins(); const results = []; for (const plugin of plugins) { try { const result = await this.checkPlugin(plugin.id); results.push(result); } catch (error) { console.error(`[HealthChecker] Failed to check plugin '${plugin.id}':`, error.message); results.push({ pluginId: plugin.id, status: 'error', issues: [{ check: 'health-check-failed', severity: 'error', message: `Health check failed: ${error.message}`, details: { error: error.message } }], timestamp: new Date().toISOString() }); } } const summary = { total: results.length, healthy: results.filter(r => r.status === 'healthy').length, warnings: results.filter(r => r.status === 'warning').length, errors: results.filter(r => r.status === 'error').length, results }; return summary; } // =========================== // Detailed Reports // =========================== /** * Get detailed health report * * Returns comprehensive health report with issue breakdown by category. * * @param {string} pluginId - Plugin ID * * @returns {Promise<Object>} Detailed health report * @returns {string} .pluginId - Plugin ID * @returns {string} .status - Health status * @returns {Object} .metadata - Plugin metadata from registry * @returns {Object[]} .issues - Array of issues grouped by check type * @returns {Object} .summary - Issue count summary * @returns {string} .timestamp - ISO 8601 timestamp * * @throws {PluginNotFoundError} If plugin ID not in registry * * @example * const report = await checker.getHealthReport('sdlc-complete'); * console.log('Status:', report.status); * console.log('Issues:', report.summary); * report.issues.forEach(issue => { * console.log(`- [${issue.severity}] ${issue.message}`); * }); */ async getHealthReport(pluginId) { const healthCheck = await this.checkPlugin(pluginId); const metadata = await this.registry.getPlugin(pluginId); const summary = { total: healthCheck.issues.length, errors: healthCheck.issues.filter(i => i.severity === 'error').length, warnings: healthCheck.issues.filter(i => i.severity === 'warning').length }; return { pluginId, status: healthCheck.status, metadata, issues: healthCheck.issues, summary, timestamp: healthCheck.timestamp }; } /** * Generate summary across all plugins * * Returns high-level summary of overall plugin ecosystem health. * * @returns {Promise<Object>} Overall health summary * * @example * const summary = await checker.generateSummary(); * console.log(`Overall health: ${summary.overallStatus}`); * console.log(`Plugins: ${summary.plugins.total} total, ${summary.plugins.healthy} healthy`); */ async generateSummary() { const checkResults = await this.checkAll(); const overallStatus = checkResults.errors > 0 ? 'error' : checkResults.warnings > 0 ? 'warning' : 'healthy'; const totalIssues = checkResults.results.reduce((sum, r) => sum + r.issues.length, 0); const errorIssues = checkResults.results.reduce( (sum, r) => sum + r.issues.filter(i => i.severity === 'error').length, 0 ); const warningIssues = checkResults.results.reduce( (sum, r) => sum + r.issues.filter(i => i.severity === 'warning').length, 0 ); return { overallStatus, plugins: { total: checkResults.total, healthy: checkResults.healthy, warnings: checkResults.warnings, errors: checkResults.errors }, issues: { total: totalIssues, errors: errorIssues, warnings: warningIssues }, timestamp: new Date().toISOString() }; } // =========================== // Validation Methods // =========================== /** * Validate plugin manifest integrity * * Checks: * - Manifest file exists at correct path * - Valid JSON parsing * - Required fields present (id, version, type) * * @param {string} pluginId - Plugin ID * * @returns {Promise<Object>} Validation result * @returns {boolean} .valid - True if all checks passed * @returns {Object[]} .issues - Array of issues found * * @example * const result = await checker.validateManifest('sdlc-complete'); * if (!result.valid) { * console.error('Manifest issues:', result.issues); * } */ async validateManifest(pluginId) { const issues = []; try { const plugin = await this.registry.getPlugin(pluginId); const manifestPath = this._getManifestPath(plugin); // Check if manifest file exists try { await fs.access(manifestPath); } catch (error) { if (error.code === 'ENOENT') { issues.push({ check: 'manifest-integrity', severity: 'error', message: `Missing manifest file: ${manifestPath}`, details: { path: manifestPath } }); return { valid: false, issues }; } throw error; } // Try to parse manifest JSON let manifest; try { const manifestContent = await fs.readFile(manifestPath, 'utf-8'); manifest = JSON.parse(manifestContent); } catch (error) { if (error instanceof SyntaxError) { issues.push({ check: 'manifest-integrity', severity: 'error', message: `Corrupted manifest (invalid JSON): ${error.message}`, details: { path: manifestPath, error: error.message } }); return { valid: false, issues }; } throw error; } // Validate required fields const requiredFields = ['id', 'version', 'type']; const missingFields = requiredFields.filter(field => !manifest[field]); if (missingFields.length > 0) { issues.push({ check: 'manifest-integrity', severity: 'error', message: `Missing required fields in manifest: ${missingFields.join(', ')}`, details: { path: manifestPath, missingFields } }); } // Validate ID matches if (manifest.id && manifest.id !== pluginId) { issues.push({ check: 'manifest-integrity', severity: 'warning', message: `Manifest ID mismatch: expected '${pluginId}', got '${manifest.id}'`, details: { expected: pluginId, actual: manifest.id } }); } } catch (error) { if (error instanceof PluginNotFoundError) { throw error; } issues.push({ check: 'manifest-integrity', severity: 'error', message: `Manifest validation failed: ${error.message}`, details: { error: error.message } }); } return { valid: issues.length === 0, issues }; } /** * Validate plugin directory structure * * Checks: * - Framework: repo/ and projects/ directories exist * - Add-on: repo/ directory exists * - Extension: repo/ directory exists * * @param {string} pluginId - Plugin ID * * @returns {Promise<Object>} Validation result * @returns {boolean} .valid - True if all checks passed * @returns {Object[]} .issues - Array of issues found * * @example * const result = await checker.validateDirectories('sdlc-complete'); */ async validateDirectories(pluginId) { const issues = []; try { const plugin = await this.registry.getPlugin(pluginId); const repoPath = path.join(this.basePath, plugin['repo-path']); // Check repo/ directory (required for all plugin types) try { const repoStats = await fs.stat(repoPath); if (!repoStats.isDirectory()) { issues.push({ check: 'directory-structure', severity: 'error', message: `Invalid repo path: ${repoPath} is not a directory`, details: { path: repoPath } }); } } catch (error) { if (error.code === 'ENOENT') { issues.push({ check: 'directory-structure', severity: 'error', message: `Missing required directory: repo/ at ${repoPath}`, details: { path: repoPath } }); } else { throw error; } } // Framework-specific: check projects/ directory if (plugin.type === 'framework') { const projectsPath = path.join(this.basePath, path.dirname(plugin['repo-path']), 'projects'); try { const projectsStats = await fs.stat(projectsPath); if (!projectsStats.isDirectory()) { issues.push({ check: 'directory-structure', severity: 'error', message: `Invalid projects path: ${projectsPath} is not a directory`, details: { path: projectsPath } }); } } catch (error) { if (error.code === 'ENOENT') { issues.push({ check: 'directory-structure', severity: 'error', message: `Missing required directory: projects/ at ${projectsPath}`, details: { path: projectsPath } }); } else { throw error; } } } } catch (error) { if (error instanceof PluginNotFoundError) { throw error; } issues.push({ check: 'directory-structure', severity: 'error', message: `Directory validation failed: ${error.message}`, details: { error: error.message } }); } return { valid: issues.length === 0, issues }; } /** * Validate version compatibility * * For add-ons: checks parent framework version compatibility using semantic versioning. * For frameworks/extensions: no version compatibility checks (returns valid). * * @param {string} pluginId - Plugin ID * * @returns {Promise<Object>} Validation result * @returns {boolean} .valid - True if compatible * @returns {Object[]} .issues - Array of issues found * * @example * const result = await checker.validateVersionCompatibility('gdpr-compliance'); */ async validateVersionCompatibility(pluginId) { const issues = []; try { const plugin = await this.registry.getPlugin(pluginId); // Only check for add-ons if (plugin.type !== 'add-on') { return { valid: true, issues: [] }; } // Verify parent framework exists const parentId = plugin['parent-framework']; if (!parentId) { issues.push({ check: 'version-compatibility', severity: 'error', message: `Add-on missing parent-framework field`, details: { pluginId } }); return { valid: false, issues }; } let parentPlugin; try { parentPlugin = await this.registry.getPlugin(parentId); } catch (error) { if (error instanceof PluginNotFoundError) { issues.push({ check: 'version-compatibility', severity: 'error', message: `Parent framework '${parentId}' not found`, details: { parentId } }); return { valid: false, issues }; } throw error; } // Parse semantic versions const addonVersion = this._parseSemver(plugin.version); const parentVersion = this._parseSemver(parentPlugin.version); if (!addonVersion || !parentVersion) { issues.push({ check: 'version-compatibility', severity: 'warning', message: `Invalid version format (expected semver): add-on=${plugin.version}, parent=${parentPlugin.version}`, details: { addonVersion: plugin.version, parentVersion: parentPlugin.version } }); return { valid: false, issues }; } // Check major version compatibility (major versions must match) if (addonVersion.major !== parentVersion.major) { issues.push({ check: 'version-compatibility', severity: 'error', message: `Incompatible major version: add-on v${plugin.version} requires parent v${parentPlugin.version}`, details: { addonVersion: plugin.version, parentVersion: parentPlugin.version, reason: 'Major version mismatch' } }); } // Warn if add-on minor version exceeds parent if (addonVersion.major === parentVersion.major && addonVersion.minor > parentVersion.minor) { issues.push({ check: 'version-compatibility', severity: 'warning', message: `Add-on minor version exceeds parent: add-on v${plugin.version}, parent v${parentPlugin.version}`, details: { addonVersion: plugin.version, parentVersion: parentPlugin.version } }); } } catch (error) { if (error instanceof PluginNotFoundError) { throw error; } issues.push({ check: 'version-compatibility', severity: 'error', message: `Version compatibility check failed: ${error.message}`, details: { error: error.message } }); } return { valid: issues.filter(i => i.severity === 'error').length === 0, issues }; } /** * Validate plugin dependencies * * Checks: * - Add-ons: parent framework exists and is healthy * - Extensions: extended framework exists * - No circular dependencies * * @param {string} pluginId - Plugin ID * * @returns {Promise<Object>} Validation result * @returns {boolean} .valid - True if dependencies satisfied * @returns {Object[]} .issues - Array of issues found * * @example * const result = await checker.validateDependencies('gdpr-compliance'); */ async validateDependencies(pluginId) { const issues = []; try { const plugin = await this.registry.getPlugin(pluginId); // Check add-on dependencies if (plugin.type === 'add-on') { const parentId = plugin['parent-framework']; if (!parentId) { issues.push({ check: 'dependencies', severity: 'error', message: `Add-on missing parent-framework field`, details: { pluginId } }); } else { // Verify parent framework exists try { const parentPlugin = await this.registry.getPlugin(parentId); // Verify parent is a framework if (parentPlugin.type !== 'framework') { issues.push({ check: 'dependencies', severity: 'error', message: `Parent '${parentId}' is not a framework (type: ${parentPlugin.type})`, details: { parentId, parentType: parentPlugin.type } }); } // Warn if parent framework has errors if (parentPlugin.health === 'error') { issues.push({ check: 'dependencies', severity: 'warning', message: `Parent framework '${parentId}' has health errors`, details: { parentId, parentHealth: parentPlugin.health } }); } } catch (error) { if (error instanceof PluginNotFoundError) { issues.push({ check: 'dependencies', severity: 'error', message: `Missing parent framework: ${parentId}`, details: { parentId } }); } else { throw error; } } } } // Check extension dependencies if (plugin.type === 'extension') { const extendsId = plugin.extends; if (!extendsId) { issues.push({ check: 'dependencies', severity: 'error', message: `Extension missing 'extends' field`, details: { pluginId } }); } else { // Verify extended framework exists try { const extendedPlugin = await this.registry.getPlugin(extendsId); // Verify extended plugin is a framework if (extendedPlugin.type !== 'framework') { issues.push({ check: 'dependencies', severity: 'warning', message: `Extended plugin '${extendsId}' is not a framework (type: ${extendedPlugin.type})`, details: { extendsId, extendedType: extendedPlugin.type } }); } } catch (error) { if (error instanceof PluginNotFoundError) { issues.push({ check: 'dependencies', severity: 'error', message: `Missing extended framework: ${extendsId}`, details: { extendsId } }); } else { throw error; } } } } // Check for circular dependencies const circularCheck = await this._checkCircularDependencies(pluginId); if (!circularCheck.valid) { issues.push(...circularCheck.issues); } } catch (error) { if (error instanceof PluginNotFoundError) { throw error; } issues.push({ check: 'dependencies', severity: 'error', message: `Dependency validation failed: ${error.message}`, details: { error: error.message } }); } return { valid: issues.filter(i => i.severity === 'error').length === 0, issues }; } // =========================== // Cache Management // =========================== /** * Get cached health check result * * @param {string} pluginId - Plugin ID * * @returns {Object|null} Cached result or null if expired/missing * * @example * const cached = checker.getCached('sdlc-complete'); * if (cached) { * console.log('Using cached result:', cached.status); * } */ getCached(pluginId) { const cached = this.cache.get(pluginId); if (!cached) { return null; } const age = Date.now() - new Date(cached.timestamp).getTime(); if (age > this.cacheTTL) { // Cache expired this.cache.delete(pluginId); return null; } return cached; } /** * Clear all cached health checks * * @example * checker.clearCache(); */ clearCache() { this.cache.clear(); } /** * Invalidate cache for specific plugin * * @param {string} pluginId - Plugin ID * * @example * checker.invalidateCache('sdlc-complete'); */ invalidateCache(pluginId) { this.cache.delete(pluginId); } // =========================== // Auto-Repair // =========================== /** * Attempt auto-repair for common plugin issues * * Supports: * - Creating missing directories (repo/, projects/) * - Regenerating missing manifest with defaults * * @param {string} pluginId - Plugin ID to repair * * @returns {Promise<Object>} Repair result * @returns {boolean} .repaired - True if repairs were applied * @returns {string[]} .actions - Array of repair actions taken * @returns {Object[]} .failures - Array of failed repair attempts * * @throws {PluginNotFoundError} If plugin ID not in registry * * @example * const result = await checker.repairPlugin('broken-plugin'); * if (result.repaired) { * console.log('Repairs applied:', result.actions); * } else { * console.log('No repairs needed'); * } */ async repairPlugin(pluginId) { const actions = []; const failures = []; try { const plugin = await this.registry.getPlugin(pluginId); // 1. Check and create missing directories const dirCheck = await this.validateDirectories(pluginId); for (const issue of dirCheck.issues) { if (issue.check === 'directory-structure' && issue.severity === 'error') { const dirPath = issue.details.path; try { await fs.mkdir(dirPath, { recursive: true }); actions.push(`Created missing directory: ${dirPath}`); } catch (error) { failures.push({ action: `Create directory: ${dirPath}`, error: error.message }); } } } // 2. Check and regenerate missing manifest const manifestCheck = await this.validateManifest(pluginId); const missingManifest = manifestCheck.issues.some( i => i.check === 'manifest-integrity' && i.message.includes('Missing manifest file') ); if (missingManifest) { const manifestPath = this._getManifestPath(plugin); try { const defaultManifest = { id: plugin.id, version: plugin.version, type: plugin.type, name: plugin.name, description: `Auto-generated manifest for ${plugin.name}`, 'install-date': plugin['install-date'] }; // Ensure directory exists await fs.mkdir(path.dirname(manifestPath), { recursive: true }); await fs.writeFile(manifestPath, JSON.stringify(defaultManifest, null, 2), 'utf-8'); actions.push(`Regenerated missing manifest: ${manifestPath}`); } catch (error) { failures.push({ action: `Regenerate manifest: ${manifestPath}`, error: error.message }); } } // Invalidate cache after repairs if (actions.length > 0) { this.invalidateCache(pluginId); } } catch (error) { if (error instanceof PluginNotFoundError) { throw error; } failures.push({ action: 'Auto-repair', error: error.message }); } return { repaired: actions.length > 0, actions, failures }; } // =========================== // Internal Helpers // =========================== /** * Get manifest file path for plugin * * Handles both frameworks (with repo/ subdirectory) and add-ons/extensions (without). * * @private * @param {Object} plugin - Plugin metadata object * @returns {string} Absolute path to manifest.json */ _getManifestPath(plugin) { // Normalize repo-path (remove trailing slash) const repoPath = plugin['repo-path'].replace(/\/$/, ''); // Determine plugin directory based on structure let pluginDir; if (repoPath.endsWith('/repo')) { // Framework structure: frameworks/{id}/repo/ → manifest at frameworks/{id}/manifest.json pluginDir = path.join(this.basePath, path.dirname(repoPath)); } else { // Add-on/Extension structure: add-ons/{id}/ → manifest at add-ons/{id}/manifest.json pluginDir = path.join(this.basePath, repoPath); } return path.join(pluginDir, 'manifest.json'); } /** * Calculate disk usage for plugin * * @private * @param {string} pluginId - Plugin ID * @returns {Promise<number>} Disk usage in bytes */ async _getDiskUsage(pluginId) { try { const plugin = await this.registry.getPlugin(pluginId); const repoPath = path.join(this.basePath, plugin['repo-path']); return await this._getDirectorySize(repoPath); } catch (error) { console.warn(`[HealthChecker] Failed to calculate disk usage for '${pluginId}':`, error.message); return 0; } } /** * Recursively calculate directory size * * @private * @param {string} dirPath - Directory path * @returns {Promise<number>} Total size in bytes */ async _getDirectorySize(dirPath) { let totalSize = 0; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalSize += await this._getDirectorySize(fullPath); } else { const stats = await fs.stat(fullPath); totalSize += stats.size; } } } catch (error) { // Ignore permission errors, missing directories if (error.code !== 'ENOENT' && error.code !== 'EACCES') { console.warn(`[HealthChecker] Error calculating directory size for ${dirPath}:`, error.message); } } return totalSize; } /** * Format bytes as human-readable string * * @private * @param {number} bytes - Bytes to format * @returns {string} Formatted string (e.g., "125 MB") */ _formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Parse semantic version string * * @private * @param {string} version - Version string (e.g., "1.2.3") * @returns {Object|null} Parsed version { major, minor, patch } or null if invalid */ _parseSemver(version) { if (!version) return null; const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/); if (!match) return null; return { major: parseInt(match[1], 10), minor: parseInt(match[2], 10), patch: parseInt(match[3], 10) }; } /** * Check for circular dependencies * * @private * @param {string} pluginId - Plugin ID to check * @param {Set<string>} [visited=new Set()] - Set of visited plugin IDs (for recursion) * @returns {Promise<Object>} Validation result */ async _checkCircularDependencies(pluginId, visited = new Set()) { const issues = []; try { // Detect cycle if (visited.has(pluginId)) { issues.push({ check: 'dependencies', severity: 'error', message: `Circular dependency detected: ${Array.from(visited).join(' -> ')} -> ${pluginId}`, details: { cycle: Array.from(visited).concat(pluginId) } }); return { valid: false, issues }; } visited.add(pluginId); const plugin = await this.registry.getPlugin(pluginId); // Check parent framework (add-ons) if (plugin.type === 'add-on' && plugin['parent-framework']) { const parentCheck = await this._checkCircularDependencies( plugin['parent-framework'], new Set(visited) ); issues.push(...parentCheck.issues); } // Check extended framework (extensions) if (plugin.type === 'extension' && plugin.extends) { const extendsCheck = await this._checkCircularDependencies( plugin.extends, new Set(visited) ); issues.push(...extendsCheck.issues); } } catch (error) { if (error instanceof PluginNotFoundError) { // Missing dependency already caught by validateDependencies return { valid: true, issues: [] }; } // Ignore other errors in circular dependency check } return { valid: issues.length === 0, issues }; } } // Export error classes export { HealthCheckError, PluginNotFoundError }; // Example usage if (process.argv[1] === new URL(import.meta.url).pathname) { const checker = new HealthChecker(); // Check single plugin console.log('\n=== Check Single Plugin ==='); try { const result = await checker.checkPlugin('sdlc-complete'); console.log(`Status: ${result.status}`); console.log(`Issues: ${result.issues.length}`); result.issues.forEach(issue => { console.log(` [${issue.severity}] ${issue.check}: ${issue.message}`); }); } catch (error) { console.error('Error:', error.message); } // Check all plugins console.log('\n=== Check All Plugins ==='); try { const summary = await checker.checkAll(); console.log(`Total: ${summary.total}`); console.log(`Healthy: ${summary.healthy}`); console.log(`Warnings: ${summary.warnings}`); console.log(`Errors: ${summary.errors}`); } catch (error) { console.error('Error:', error.message); } // Generate summary console.log('\n=== Overall Summary ==='); try { const summary = await checker.generateSummary(); console.log(`Overall Status: ${summary.overallStatus}`); console.log(`Plugins: ${summary.plugins.healthy}/${summary.plugins.total} healthy`); console.log(`Issues: ${summary.issues.total} total (${summary.issues.errors} errors, ${summary.issues.warnings} warnings)`); } catch (error) { console.error('Error:', error.message); } // Get detailed report console.log('\n=== Detailed Report ==='); try { const report = await checker.getHealthReport('sdlc-complete'); console.log(`Plugin: ${report.pluginId}`); console.log(`Status: ${report.status}`); console.log(`Type: ${report.metadata.type}`); console.log(`Version: ${report.metadata.version}`); console.log(`Summary: ${report.summary.total} issues (${report.summary.errors} errors, ${report.summary.warnings} warnings)`); } catch (error) { console.error('Error:', error.message); } // Auto-repair example console.log('\n=== Auto-Repair ==='); try { const repairResult = await checker.repairPlugin('sdlc-complete'); if (repairResult.repaired) { console.log('Repairs applied:'); repairResult.actions.forEach(action => console.log(` - ${action}`)); } else { console.log('No repairs needed'); } if (repairResult.failures.length > 0) { console.log('Failed repairs:'); repairResult.failures.forEach(failure => { console.log(` - ${failure.action}: ${failure.error}`); }); } } catch (error) { console.error('Error:', error.message); } }