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.

741 lines (641 loc) 22.8 kB
/** * CLI Status Command - Display plugin health and status * * Provides user-friendly status reporting for all installed plugins (frameworks, add-ons, extensions). * Displays health status, version info, project counts, and workspace summary in ASCII tables. * Supports filtering by type, checking specific plugins, and verbose mode for detailed reports. * * @module tools/cli/status-command * @version 1.0.0 * @since 2025-10-19 * * @usage * # Show all plugins * node tools/cli/status-command.mjs * aiwg -status * * # Filter by type * node tools/cli/status-command.mjs --type frameworks * aiwg -status --type add-ons * * # Check specific plugin * node tools/cli/status-command.mjs sdlc-complete * aiwg -status gdpr-compliance * * # Verbose mode * node tools/cli/status-command.mjs --verbose * aiwg -status sdlc-complete --verbose * * @example * // Output example: * AIWG - Plugin Status * ================================================================================ * * FRAMEWORKS (2 installed) * ┌────────────────┬─────────┬──────────────┬──────────┬─────────────────┐ * │ ID │ Version │ Installed │ Projects │ Health │ * ├────────────────┼─────────┼──────────────┼──────────┼─────────────────┤ * │ sdlc-complete │ 1.0.0 │ 2025-10-18 │ 2 │ ✓ HEALTHY │ * │ marketing-flow │ 1.0.0 │ 2025-10-19 │ 1 │ ✓ HEALTHY │ * └────────────────┴─────────┴──────────────┴──────────┴─────────────────┘ * * WORKSPACE * Base Path: .aiwg/ * Legacy Mode: No (framework-scoped workspace active) * Total Plugins: 2 * Disk Usage: 125 MB */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { PluginRegistry } from '../workspace/registry-manager.mjs'; import { HealthChecker } from '../workspace/health-checker.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // =========================== // ASCII Table Formatting // =========================== /** * Format data as ASCII table with box-drawing characters * * @param {Object[]} data - Array of row objects * @param {Object} columns - Column configuration { columnName: { header, width, align } } * @returns {string} Formatted ASCII table * * @example * const data = [ * { id: 'sdlc-complete', version: '1.0.0', health: 'healthy' }, * { id: 'marketing-flow', version: '1.0.0', health: 'healthy' } * ]; * const columns = { * id: { header: 'ID', width: 16, align: 'left' }, * version: { header: 'Version', width: 7, align: 'left' }, * health: { header: 'Health', width: 15, align: 'left' } * }; * console.log(formatTable(data, columns)); */ function formatTable(data, columns) { if (data.length === 0) { return ''; } const lines = []; // Column order const columnKeys = Object.keys(columns); // Top border const topBorder = '┌' + columnKeys.map(key => '─'.repeat(columns[key].width + 2)).join('┬') + '┐'; lines.push(topBorder); // Header row const headerRow = '│' + columnKeys.map(key => { const col = columns[key]; return ' ' + padString(col.header, col.width, col.align || 'left') + ' '; }).join('│') + '│'; lines.push(headerRow); // Header separator const headerSeparator = '├' + columnKeys.map(key => '─'.repeat(columns[key].width + 2)).join('┼') + '┤'; lines.push(headerSeparator); // Data rows data.forEach(row => { const dataRow = '│' + columnKeys.map(key => { const col = columns[key]; const value = row[key] !== undefined && row[key] !== null ? String(row[key]) : ''; return ' ' + padString(value, col.width, col.align || 'left') + ' '; }).join('│') + '│'; lines.push(dataRow); }); // Bottom border const bottomBorder = '└' + columnKeys.map(key => '─'.repeat(columns[key].width + 2)).join('┴') + '┘'; lines.push(bottomBorder); return lines.join('\n'); } /** * Pad string to specified width with alignment * * @param {string} str - String to pad * @param {number} width - Target width * @param {string} align - Alignment ('left' | 'right' | 'center') * @returns {string} Padded string */ function padString(str, width, align = 'left') { // Truncate if too long if (str.length > width) { return str.substring(0, width - 1) + '…'; } const padding = width - str.length; if (align === 'right') { return ' '.repeat(padding) + str; } else if (align === 'center') { const leftPad = Math.floor(padding / 2); const rightPad = padding - leftPad; return ' '.repeat(leftPad) + str + ' '.repeat(rightPad); } else { return str + ' '.repeat(padding); } } // =========================== // Formatting Helpers // =========================== /** * Format health status with icon * * @param {string} health - Health status ('healthy' | 'warning' | 'error' | 'unknown') * @returns {string} Formatted health string with icon */ function formatHealth(health) { switch (health) { case 'healthy': return '✓ HEALTHY'; case 'warning': return '⚠️ WARNING'; case 'error': return '❌ ERROR'; default: return '? UNKNOWN'; } } /** * Format date as YYYY-MM-DD * * @param {string} isoDate - ISO 8601 date string * @returns {string} Formatted date */ function formatDate(isoDate) { if (!isoDate) return 'N/A'; try { const date = new Date(isoDate); return date.toISOString().split('T')[0]; } catch { return 'Invalid'; } } /** * Format bytes as human-readable string * * @param {number} bytes - Bytes to format * @returns {string} Formatted string (e.g., "125 MB") */ function 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]; } /** * Truncate plugin ID for display * * @param {string} id - Plugin ID * @param {number} maxLength - Maximum length * @returns {string} Truncated ID */ function truncateId(id, maxLength = 16) { if (id.length <= maxLength) { return id; } return id.substring(0, maxLength - 1) + '…'; } /** * Abbreviate framework name for display * * @param {string} frameworkId - Framework ID * @returns {string} Abbreviated name */ function abbreviateFramework(frameworkId) { if (frameworkId.length <= 12) { return frameworkId; } // Try to abbreviate intelligently const parts = frameworkId.split('-'); if (parts.length > 1) { return parts[0] + '-' + parts[parts.length - 1].substring(0, 4) + '.'; } return frameworkId.substring(0, 11) + '.'; } // =========================== // Table Display Functions // =========================== /** * Display frameworks table * * @param {Object[]} frameworks - Array of framework plugin objects * @param {Map<string, Object>} healthResults - Map of plugin ID to health check result */ function displayFrameworksTable(frameworks, healthResults) { const columns = { id: { header: 'ID', width: 16, align: 'left' }, version: { header: 'Version', width: 7, align: 'left' }, installed: { header: 'Installed', width: 12, align: 'left' }, projects: { header: 'Projects', width: 8, align: 'right' }, health: { header: 'Health', width: 17, align: 'left' } }; const data = frameworks.map(plugin => { const healthResult = healthResults.get(plugin.id); const projectCount = plugin.projects ? plugin.projects.length : 0; return { id: truncateId(plugin.id), version: plugin.version || 'N/A', installed: formatDate(plugin['install-date']), projects: String(projectCount), health: formatHealth(healthResult?.status || plugin.health || 'unknown') }; }); console.log(formatTable(data, columns)); } /** * Display add-ons table * * @param {Object[]} addOns - Array of add-on plugin objects * @param {Map<string, Object>} healthResults - Map of plugin ID to health check result */ function displayAddOnsTable(addOns, healthResults) { const columns = { id: { header: 'ID', width: 17, align: 'left' }, version: { header: 'Version', width: 7, align: 'left' }, installed: { header: 'Installed', width: 12, align: 'left' }, framework: { header: 'Framework', width: 12, align: 'left' }, health: { header: 'Health', width: 17, align: 'left' } }; const data = addOns.map(plugin => { const healthResult = healthResults.get(plugin.id); return { id: truncateId(plugin.id, 17), version: plugin.version || 'N/A', installed: formatDate(plugin['install-date']), framework: abbreviateFramework(plugin['parent-framework'] || 'N/A'), health: formatHealth(healthResult?.status || plugin.health || 'unknown') }; }); console.log(formatTable(data, columns)); } /** * Display extensions table * * @param {Object[]} extensions - Array of extension plugin objects * @param {Map<string, Object>} healthResults - Map of plugin ID to health check result */ function displayExtensionsTable(extensions, healthResults) { const columns = { id: { header: 'ID', width: 17, align: 'left' }, version: { header: 'Version', width: 7, align: 'left' }, installed: { header: 'Installed', width: 12, align: 'left' }, extends: { header: 'Extends', width: 12, align: 'left' }, health: { header: 'Health', width: 17, align: 'left' } }; const data = extensions.map(plugin => { const healthResult = healthResults.get(plugin.id); return { id: truncateId(plugin.id, 17), version: plugin.version || 'N/A', installed: formatDate(plugin['install-date']), extends: abbreviateFramework(plugin.extends || 'N/A'), health: formatHealth(healthResult?.status || plugin.health || 'unknown') }; }); console.log(formatTable(data, columns)); } // =========================== // Workspace Summary // =========================== /** * Display workspace summary * * @param {Object[]} plugins - All plugins * @param {string} basePath - Base path to .aiwg directory */ async function displayWorkspaceSummary(plugins, basePath) { console.log('\nWORKSPACE'); // Calculate total disk usage let totalDiskUsage = 0; for (const plugin of plugins) { try { const repoPath = path.join(basePath, plugin['repo-path']); const usage = await getDirectorySize(repoPath); totalDiskUsage += usage; } catch { // Ignore errors } } // Detect legacy mode const legacyMode = await detectLegacyMode(basePath); console.log(` Base Path: ${basePath}`); console.log(` Legacy Mode: ${legacyMode ? 'Yes (shared workspace)' : 'No (framework-scoped workspace active)'}`); console.log(` Total Plugins: ${plugins.length}`); console.log(` Disk Usage: ${formatBytes(totalDiskUsage)}`); } /** * Detect legacy workspace mode * * @param {string} basePath - Base path to .aiwg directory * @returns {Promise<boolean>} True if legacy mode detected */ async function detectLegacyMode(basePath) { try { // Legacy mode: .aiwg/intake/ exists (shared workspace) // New mode: .aiwg/frameworks/{framework-id}/projects/{project-id}/intake/ const intakePath = path.join(basePath, 'intake'); await fs.access(intakePath); return true; } catch { return false; } } /** * Calculate directory size recursively * * @param {string} dirPath - Directory path * @returns {Promise<number>} Total size in bytes */ async function 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 getDirectorySize(fullPath); } else { const stats = await fs.stat(fullPath); totalSize += stats.size; } } } catch { // Ignore permission errors, missing directories } return totalSize; } // =========================== // Verbose Mode Display // =========================== /** * Display verbose status for single plugin * * @param {Object} plugin - Plugin metadata * @param {Object} healthResult - Health check result * @param {string} basePath - Base path to .aiwg directory */ async function displayVerbose(plugin, healthResult, basePath) { console.log(`\nPlugin: ${plugin.id} (${plugin.type.charAt(0).toUpperCase() + plugin.type.slice(1)})`); console.log('='.repeat(80)); console.log(`Name: ${plugin.name}`); console.log(`Version: ${plugin.version}`); console.log(`Installed: ${formatDate(plugin['install-date'])}`); console.log(`Health: ${formatHealth(healthResult.status)}`); console.log(`Last Checked: ${healthResult.timestamp ? new Date(healthResult.timestamp).toLocaleString() : 'Never'}`); // Directories console.log('\nDIRECTORIES'); const repoPath = path.join(basePath, plugin['repo-path']); console.log(` Repo Path: ${repoPath}`); if (plugin.type === 'framework') { const projectsPath = path.join(basePath, path.dirname(plugin['repo-path']), 'projects'); console.log(` Projects: ${projectsPath}`); } // Projects (for frameworks) if (plugin.type === 'framework') { console.log(`\nPROJECTS (${plugin.projects?.length || 0})`); if (plugin.projects && plugin.projects.length > 0) { plugin.projects.forEach(projectId => { console.log(` - ${projectId}`); }); } else { console.log(' No active projects'); } } // Dependencies (for add-ons/extensions) if (plugin.type === 'add-on') { console.log(`\nDEPENDENCIES`); console.log(` Parent Framework: ${plugin['parent-framework'] || 'N/A'}`); } if (plugin.type === 'extension') { console.log(`\nDEPENDENCIES`); console.log(` Extends: ${plugin.extends || 'N/A'}`); } // Health check details console.log('\nHEALTH CHECK'); // Count issues by category const manifestIssues = healthResult.issues.filter(i => i.check === 'manifest-integrity'); const dirIssues = healthResult.issues.filter(i => i.check === 'directory-structure'); const versionIssues = healthResult.issues.filter(i => i.check === 'version-compatibility'); const depIssues = healthResult.issues.filter(i => i.check === 'dependencies'); const diskIssues = healthResult.issues.filter(i => i.check === 'disk-usage'); console.log(` Manifest: ${manifestIssues.length === 0 ? '✓ Valid' : `❌ ${manifestIssues.length} issue(s)`}`); console.log(` Directories: ${dirIssues.length === 0 ? '✓ Present' : `❌ ${dirIssues.length} issue(s)`}`); if (plugin.type === 'add-on' || plugin.type === 'extension') { console.log(` Dependencies: ${depIssues.length === 0 ? '✓ Satisfied' : `❌ ${depIssues.length} issue(s)`}`); } else { console.log(` Dependencies: N/A (framework has no dependencies)`); } // Disk usage try { const diskUsage = await getDirectorySize(repoPath); console.log(` Disk Usage: ${formatBytes(diskUsage)}`); } catch { console.log(` Disk Usage: Unknown`); } // Issue details if (healthResult.issues.length > 0) { console.log('\nISSUES FOUND'); healthResult.issues.forEach((issue, index) => { const icon = issue.severity === 'error' ? '❌' : '⚠️'; console.log(` ${index + 1}. ${icon} [${issue.severity.toUpperCase()}] ${issue.check}`); console.log(` ${issue.message}`); }); } else { console.log('\nNo issues found.'); } } // =========================== // Command Logic // =========================== /** * Parse command-line arguments * * @param {string[]} args - Command-line arguments (process.argv.slice(2)) * @returns {Object} Parsed options */ function parseArgs(args) { const options = { type: null, verbose: false, pluginId: null, help: false }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--type') { options.type = args[++i]; if (!['frameworks', 'add-ons', 'extensions'].includes(options.type)) { throw new Error(`Invalid type '${options.type}': must be 'frameworks', 'add-ons', or 'extensions'`); } } else if (arg === '--verbose') { options.verbose = true; } else if (arg === '--help' || arg === '-h') { options.help = true; } else if (!arg.startsWith('--')) { // Plugin ID options.pluginId = arg; } } return options; } /** * Filter plugins based on options * * @param {Object[]} plugins - All plugins * @param {Object} options - Command options * @returns {Object[]} Filtered plugins */ function filterPlugins(plugins, options) { let filtered = plugins; // Filter by type if (options.type) { const typeMap = { 'frameworks': 'framework', 'add-ons': 'add-on', 'extensions': 'extension' }; filtered = filtered.filter(p => p.type === typeMap[options.type]); } // Filter by ID if (options.pluginId) { filtered = filtered.filter(p => p.id === options.pluginId); } return filtered; } /** * Display help message */ function displayHelp() { console.log(` AIWG - Plugin Status Command USAGE aiwg -status [options] [plugin-id] node tools/cli/status-command.mjs [options] [plugin-id] OPTIONS --type <frameworks|add-ons|extensions> Filter by plugin type --verbose Show detailed information --help, -h Show this help message ARGUMENTS plugin-id Check specific plugin (optional) EXAMPLES # Show all plugins aiwg -status # Filter by type aiwg -status --type frameworks aiwg -status --type add-ons # Check specific plugin aiwg -status sdlc-complete # Verbose mode aiwg -status --verbose aiwg -status sdlc-complete --verbose OUTPUT Displays health status for installed plugins with: - Plugin ID, version, install date - Health status (✓ HEALTHY, ⚠️ WARNING, ❌ ERROR) - Project count (frameworks) - Parent framework (add-ons) - Workspace summary (base path, legacy mode, disk usage) `); } /** * Main status command * * @param {string[]} args - Command-line arguments * @returns {Promise<void>} */ export async function statusCommand(args) { try { // Parse arguments const options = parseArgs(args); // Show help if (options.help) { displayHelp(); return; } // Initialize registry and health checker (support test override) const registryPath = process.env.AIWG_REGISTRY_PATH || path.join(path.resolve(".aiwg"), "frameworks", "registry.json"); const basePath = path.dirname(path.dirname(registryPath)); const registry = new PluginRegistry(registryPath); const healthChecker = new HealthChecker(basePath, registry); // Get all plugins let plugins = await registry.listPlugins(); // Filter plugins plugins = filterPlugins(plugins, options); // Check if any plugins found if (plugins.length === 0) { if (options.pluginId) { console.error(`Error: Plugin '${options.pluginId}' not found in registry.`); console.error(`Install plugins via: aiwg -deploy-agents --mode sdlc`); process.exit(1); } else if (options.type) { console.log(`\nNo ${options.type} installed.`); return; } else { console.log('\nNo plugins installed.'); console.log('Install plugins via: aiwg -deploy-agents --mode sdlc'); return; } } // Run health checks for all plugins const healthResults = new Map(); for (const plugin of plugins) { try { const result = await healthChecker.checkPlugin(plugin.id); healthResults.set(plugin.id, result); } catch (error) { console.warn(`Warning: Health check failed for '${plugin.id}': ${error.message}`); healthResults.set(plugin.id, { status: 'error', issues: [{ check: 'health-check', severity: 'error', message: error.message }], timestamp: new Date().toISOString() }); } } // Verbose mode for single plugin if (options.verbose && options.pluginId && plugins.length === 1) { await displayVerbose(plugins[0], healthResults.get(plugins[0].id), basePath); return; } // Display header console.log('\nAIWG - Plugin Status'); console.log('='.repeat(80)); // Group by type const frameworks = plugins.filter(p => p.type === 'framework'); const addOns = plugins.filter(p => p.type === 'add-on'); const extensions = plugins.filter(p => p.type === 'extension'); // Display frameworks table if (frameworks.length > 0 && (!options.type || options.type === 'frameworks')) { console.log(`\nFRAMEWORKS (${frameworks.length} installed)`); displayFrameworksTable(frameworks, healthResults); } // Display add-ons table if (addOns.length > 0 && (!options.type || options.type === 'add-ons')) { console.log(`\nADD-ONS (${addOns.length} installed)`); displayAddOnsTable(addOns, healthResults); } // Display extensions table if (extensions.length > 0 && (!options.type || options.type === 'extensions')) { console.log(`\nEXTENSIONS (${extensions.length} installed)`); displayExtensionsTable(extensions, healthResults); } else if (!options.type && extensions.length === 0 && addOns.length === 0 && frameworks.length > 0) { // Only show "no extensions" message if showing all types and we have other plugins console.log('\nEXTENSIONS (0 installed)'); console.log('No custom extensions installed.'); } // Display workspace summary (only if not filtering by type or ID) if (!options.type && !options.pluginId) { await displayWorkspaceSummary(plugins, basePath); } } catch (error) { console.error(`\nError: ${error.message}`); if (error.stack && process.env.DEBUG) { console.error(error.stack); } process.exit(1); } } // =========================== // CLI Entry Point // =========================== if (process.argv[1] === fileURLToPath(import.meta.url)) { const args = process.argv.slice(2); await statusCommand(args); }