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.

483 lines (418 loc) 13.2 kB
/** * Plugin Status Command * * Provides status reporting and health monitoring for all plugin types * (frameworks, add-ons, extensions). * * @module src/plugin/plugin-status */ import * as fs from 'fs/promises'; import * as path from 'path'; /** * Plugin types */ export type PluginType = 'framework' | 'add-on' | 'extension'; /** * Health status levels */ export type HealthStatus = 'healthy' | 'warning' | 'error'; /** * Plugin entry from registry */ export interface PluginEntry { id: string; type: PluginType; name: string; version: string; path: string; installedAt: string; parentFramework?: string; projects?: string[]; health?: { status: HealthStatus; lastCheck: string; issues?: string[]; }; } /** * Plugin status result */ export interface PluginStatusResult { id: string; type: PluginType; name: string; version: string; installedAt: string; path: string; health: HealthStatus; healthDetails: string[]; projects?: string[]; parentFramework?: string; diskUsage?: number; } /** * Status summary */ export interface StatusSummary { totalPlugins: number; healthyCount: number; warningCount: number; errorCount: number; frameworkCount: number; addOnCount: number; extensionCount: number; totalDiskUsage: number; legacyMode: boolean; } /** * Status command options */ export interface StatusOptions { type?: PluginType; pluginId?: string; verbose?: boolean; } /** * PluginStatus provides status reporting for plugins */ export class PluginStatus { private aiwgRoot: string; private registryPath: string; constructor(aiwgRoot: string) { this.aiwgRoot = aiwgRoot; this.registryPath = path.join(aiwgRoot, 'registry.json'); } /** * Get status of all plugins or filtered by options * * @param options - Filter options * @returns Array of plugin status results */ async getStatus(options: StatusOptions = {}): Promise<PluginStatusResult[]> { const plugins = await this.loadPlugins(); let results: PluginStatusResult[] = []; for (const plugin of plugins) { // Filter by type if specified if (options.type && plugin.type !== options.type) { continue; } // Filter by ID if specified if (options.pluginId && plugin.id !== options.pluginId) { continue; } const status = await this.getPluginStatus(plugin, options.verbose); results.push(status); } return results; } /** * Get status summary * * @returns Summary statistics */ async getSummary(): Promise<StatusSummary> { const plugins = await this.loadPlugins(); const statuses = await Promise.all(plugins.map(p => this.getPluginStatus(p, false))); return { totalPlugins: plugins.length, healthyCount: statuses.filter(s => s.health === 'healthy').length, warningCount: statuses.filter(s => s.health === 'warning').length, errorCount: statuses.filter(s => s.health === 'error').length, frameworkCount: plugins.filter(p => p.type === 'framework').length, addOnCount: plugins.filter(p => p.type === 'add-on').length, extensionCount: plugins.filter(p => p.type === 'extension').length, totalDiskUsage: statuses.reduce((sum, s) => sum + (s.diskUsage || 0), 0), legacyMode: await this.isLegacyMode() }; } /** * Get status of a single plugin * * @param plugin - Plugin entry * @param verbose - Include detailed information * @returns Plugin status result */ private async getPluginStatus(plugin: PluginEntry, verbose: boolean = false): Promise<PluginStatusResult> { const healthCheck = await this.performHealthCheck(plugin); const result: PluginStatusResult = { id: plugin.id, type: plugin.type, name: plugin.name, version: plugin.version, installedAt: plugin.installedAt, path: plugin.path, health: healthCheck.status, healthDetails: healthCheck.issues, projects: plugin.projects, parentFramework: plugin.parentFramework }; if (verbose) { result.diskUsage = await this.getPluginDiskUsage(plugin); } return result; } /** * Perform health check on a plugin * * @param plugin - Plugin entry * @returns Health check result */ private async performHealthCheck(plugin: PluginEntry): Promise<{ status: HealthStatus; issues: string[] }> { const issues: string[] = []; let status: HealthStatus = 'healthy'; const pluginPath = path.join(this.aiwgRoot, plugin.path); // Check 1: Directory exists try { const stats = await fs.stat(pluginPath); if (!stats.isDirectory()) { issues.push('Plugin path exists but is not a directory'); status = 'error'; } } catch (error: any) { if (error.code === 'ENOENT') { issues.push('Plugin directory not found'); status = 'error'; } else { issues.push(`Cannot access plugin directory: ${error.message}`); status = 'error'; } } // Check 2: Manifest exists const manifestPath = path.join(pluginPath, 'manifest.json'); try { await fs.stat(manifestPath); } catch (error: any) { if (error.code === 'ENOENT') { issues.push('Manifest file not found'); if (status === 'healthy') status = 'warning'; } } // Check 3: For frameworks, check projects directory if (plugin.type === 'framework' && status !== 'error') { const projectsPath = path.join(pluginPath, 'projects'); try { const stats = await fs.stat(projectsPath); if (!stats.isDirectory()) { issues.push('Projects path exists but is not a directory'); if (status === 'healthy') status = 'warning'; } } catch (error: any) { // Projects directory might not exist yet - just a warning if (error.code === 'ENOENT') { issues.push('No projects directory (framework has no active projects)'); if (status === 'healthy') status = 'warning'; } } } // Check 4: For add-ons, check parent framework exists if (plugin.type === 'add-on' && plugin.parentFramework && status !== 'error') { const plugins = await this.loadPlugins(); const parentExists = plugins.some( p => p.type === 'framework' && p.id === plugin.parentFramework ); if (!parentExists) { issues.push(`Parent framework not found: ${plugin.parentFramework}`); status = 'error'; } } // Check 5: Health staleness (if cached health exists) if (plugin.health?.lastCheck) { const lastCheck = new Date(plugin.health.lastCheck); const now = new Date(); const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / (1000 * 60 * 60); if (hoursSinceCheck > 24 && status === 'healthy') { issues.push('Health check is stale (>24 hours old)'); status = 'warning'; } } return { status, issues }; } /** * Get disk usage for a plugin * * @param plugin - Plugin entry * @returns Disk usage in bytes */ private async getPluginDiskUsage(plugin: PluginEntry): Promise<number> { const pluginPath = path.join(this.aiwgRoot, plugin.path); try { return await this.calculateDirectorySize(pluginPath); } catch (error) { return 0; } } /** * Calculate total size of a directory */ private async calculateDirectorySize(dirPath: string): Promise<number> { let totalSize = 0; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalSize += await this.calculateDirectorySize(entryPath); } else if (entry.isFile()) { const stats = await fs.stat(entryPath); totalSize += stats.size; } } } catch (error) { // Ignore errors } return totalSize; } /** * Check if workspace is in legacy mode * * @returns True if no frameworks directory exists */ private async isLegacyMode(): Promise<boolean> { const frameworksPath = path.join(this.aiwgRoot, 'frameworks'); try { await fs.stat(frameworksPath); return false; } catch (error) { return true; } } /** * Load plugins from registry * * @returns Array of plugin entries */ private async loadPlugins(): Promise<PluginEntry[]> { try { const content = await fs.readFile(this.registryPath, 'utf-8'); const registry = JSON.parse(content); return registry.plugins || []; } catch (error) { return []; } } /** * Generate text report * * @param options - Status options * @returns Formatted text report */ async generateReport(options: StatusOptions = {}): Promise<string> { const statuses = await this.getStatus(options); const summary = await this.getSummary(); const lines: string[] = []; // Header lines.push('AIWG - Plugin Status'); lines.push('='.repeat(80)); lines.push(''); // Summary lines.push('Summary:'); lines.push(` Total Plugins: ${summary.totalPlugins}`); lines.push(` Frameworks: ${summary.frameworkCount}`); lines.push(` Add-ons: ${summary.addOnCount}`); lines.push(` Extensions: ${summary.extensionCount}`); lines.push(''); lines.push(` Healthy: ${summary.healthyCount}`); lines.push(` Warnings: ${summary.warningCount}`); lines.push(` Errors: ${summary.errorCount}`); lines.push(''); lines.push(` Workspace Mode: ${summary.legacyMode ? 'Legacy' : 'Framework-scoped'}`); if (options.verbose) { lines.push(` Total Disk Usage: ${this.formatBytes(summary.totalDiskUsage)}`); } lines.push(''); // Group by type const frameworks = statuses.filter(s => s.type === 'framework'); const addOns = statuses.filter(s => s.type === 'add-on'); const extensions = statuses.filter(s => s.type === 'extension'); // Frameworks if (!options.type || options.type === 'framework') { lines.push(`FRAMEWORKS (${frameworks.length} installed)`); lines.push('-'.repeat(80)); if (frameworks.length === 0) { lines.push(' No frameworks installed.'); } else { for (const fw of frameworks) { const icon = this.getHealthIcon(fw.health); lines.push(` ${icon} ${fw.id} (v${fw.version})`); lines.push(` Path: ${fw.path}`); lines.push(` Projects: ${fw.projects?.length || 0}`); if (options.verbose && fw.healthDetails.length > 0) { lines.push(` Issues:`); for (const issue of fw.healthDetails) { lines.push(` - ${issue}`); } } } } lines.push(''); } // Add-ons if (!options.type || options.type === 'add-on') { lines.push(`ADD-ONS (${addOns.length} installed)`); lines.push('-'.repeat(80)); if (addOns.length === 0) { lines.push(' No add-ons installed.'); } else { for (const addon of addOns) { const icon = this.getHealthIcon(addon.health); lines.push(` ${icon} ${addon.id} (v${addon.version})`); lines.push(` Parent: ${addon.parentFramework || 'none'}`); if (options.verbose && addon.healthDetails.length > 0) { lines.push(` Issues:`); for (const issue of addon.healthDetails) { lines.push(` - ${issue}`); } } } } lines.push(''); } // Extensions if (!options.type || options.type === 'extension') { lines.push(`EXTENSIONS (${extensions.length} installed)`); lines.push('-'.repeat(80)); if (extensions.length === 0) { lines.push(' No extensions installed.'); } else { for (const ext of extensions) { const icon = this.getHealthIcon(ext.health); lines.push(` ${icon} ${ext.id} (v${ext.version})`); if (options.verbose && ext.healthDetails.length > 0) { lines.push(` Issues:`); for (const issue of ext.healthDetails) { lines.push(` - ${issue}`); } } } } lines.push(''); } lines.push('='.repeat(80)); return lines.join('\n'); } /** * Get health status icon */ private getHealthIcon(status: HealthStatus): string { switch (status) { case 'healthy': return '✓'; case 'warning': return '⚠'; case 'error': return '✗'; default: return '?'; } } /** * Format bytes as human-readable string */ private formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } }