UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

597 lines 24.7 kB
/** * V3 CLI Doctor Command * System diagnostics, dependency checks, config validation * * Created with ruv.io */ import { output } from '../output.js'; import { existsSync, readFileSync, statSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { execSync, exec } from 'child_process'; import { promisify } from 'util'; // Promisified exec with proper shell and env inheritance for cross-platform support const execAsync = promisify(exec); /** * Execute command asynchronously with proper environment inheritance * Critical for Windows where PATH may not be inherited properly */ async function runCommand(command, timeoutMs = 5000) { const { stdout } = await execAsync(command, { encoding: 'utf8', timeout: timeoutMs, shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh', // Use proper shell per platform env: { ...process.env }, // Explicitly inherit full environment windowsHide: true, // Hide window on Windows }); return stdout.trim(); } // Check Node.js version async function checkNodeVersion() { const requiredMajor = 20; const version = process.version; const major = parseInt(version.slice(1).split('.')[0], 10); if (major >= requiredMajor) { return { name: 'Node.js Version', status: 'pass', message: `${version} (>= ${requiredMajor} required)` }; } else if (major >= 18) { return { name: 'Node.js Version', status: 'warn', message: `${version} (>= ${requiredMajor} recommended)`, fix: 'nvm install 20 && nvm use 20' }; } else { return { name: 'Node.js Version', status: 'fail', message: `${version} (>= ${requiredMajor} required)`, fix: 'nvm install 20 && nvm use 20' }; } } // Check npm version (async with proper env inheritance) async function checkNpmVersion() { try { const version = await runCommand('npm --version'); const major = parseInt(version.split('.')[0], 10); if (major >= 9) { return { name: 'npm Version', status: 'pass', message: `v${version}` }; } else { return { name: 'npm Version', status: 'warn', message: `v${version} (>= 9 recommended)`, fix: 'npm install -g npm@latest' }; } } catch { return { name: 'npm Version', status: 'fail', message: 'npm not found', fix: 'Install Node.js from https://nodejs.org' }; } } // Check config file async function checkConfigFile() { const configPaths = [ '.claude-flow/config.json', 'claude-flow.config.json', '.claude-flow.json' ]; for (const configPath of configPaths) { if (existsSync(configPath)) { try { const content = readFileSync(configPath, 'utf8'); JSON.parse(content); return { name: 'Config File', status: 'pass', message: `Found: ${configPath}` }; } catch (e) { return { name: 'Config File', status: 'fail', message: `Invalid JSON: ${configPath}`, fix: 'Fix JSON syntax in config file' }; } } } return { name: 'Config File', status: 'warn', message: 'No config file (using defaults)', fix: 'claude-flow config init' }; } // Check daemon status async function checkDaemonStatus() { try { const pidFile = '.claude-flow/daemon.pid'; if (existsSync(pidFile)) { const pid = readFileSync(pidFile, 'utf8').trim(); try { process.kill(parseInt(pid, 10), 0); // Check if process exists return { name: 'Daemon Status', status: 'pass', message: `Running (PID: ${pid})` }; } catch { return { name: 'Daemon Status', status: 'warn', message: 'Stale PID file', fix: 'rm .claude-flow/daemon.pid && claude-flow daemon start' }; } } return { name: 'Daemon Status', status: 'warn', message: 'Not running', fix: 'claude-flow daemon start' }; } catch { return { name: 'Daemon Status', status: 'warn', message: 'Unable to check', fix: 'claude-flow daemon status' }; } } // Check memory database async function checkMemoryDatabase() { const dbPaths = [ '.claude-flow/memory.db', '.swarm/memory.db', 'data/memory.db' ]; for (const dbPath of dbPaths) { if (existsSync(dbPath)) { try { const stats = statSync(dbPath); const sizeMB = (stats.size / 1024 / 1024).toFixed(2); return { name: 'Memory Database', status: 'pass', message: `${dbPath} (${sizeMB} MB)` }; } catch { return { name: 'Memory Database', status: 'warn', message: `${dbPath} (unable to stat)` }; } } } return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' }; } // Check API keys async function checkApiKeys() { const keys = ['ANTHROPIC_API_KEY', 'CLAUDE_API_KEY', 'OPENAI_API_KEY']; const found = []; for (const key of keys) { if (process.env[key]) { found.push(key); } } if (found.includes('ANTHROPIC_API_KEY') || found.includes('CLAUDE_API_KEY')) { return { name: 'API Keys', status: 'pass', message: `Found: ${found.join(', ')}` }; } else if (found.length > 0) { return { name: 'API Keys', status: 'warn', message: `Found: ${found.join(', ')} (no Claude key)`, fix: 'export ANTHROPIC_API_KEY=your_key' }; } else { return { name: 'API Keys', status: 'warn', message: 'No API keys found', fix: 'export ANTHROPIC_API_KEY=your_key' }; } } // Check git (async with proper env inheritance) async function checkGit() { try { const version = await runCommand('git --version'); return { name: 'Git', status: 'pass', message: version.replace('git version ', 'v') }; } catch { return { name: 'Git', status: 'warn', message: 'Not installed', fix: 'Install git from https://git-scm.com' }; } } // Check if in git repo (async with proper env inheritance) async function checkGitRepo() { try { await runCommand('git rev-parse --git-dir'); return { name: 'Git Repository', status: 'pass', message: 'In a git repository' }; } catch { return { name: 'Git Repository', status: 'warn', message: 'Not a git repository', fix: 'git init' }; } } // Check MCP servers async function checkMcpServers() { const mcpConfigPaths = [ join(process.env.HOME || '', '.claude/claude_desktop_config.json'), join(process.env.HOME || '', '.config/claude/mcp.json'), '.mcp.json' ]; for (const configPath of mcpConfigPaths) { if (existsSync(configPath)) { try { const content = JSON.parse(readFileSync(configPath, 'utf8')); const servers = content.mcpServers || content.servers || {}; const count = Object.keys(servers).length; const hasClaudeFlow = 'claude-flow' in servers || 'claude-flow_alpha' in servers; if (hasClaudeFlow) { return { name: 'MCP Servers', status: 'pass', message: `${count} servers (claude-flow configured)` }; } else { return { name: 'MCP Servers', status: 'warn', message: `${count} servers (claude-flow not found)`, fix: 'claude mcp add claude-flow npx @claude-flow/cli@v3alpha mcp start' }; } } catch { // continue to next path } } } return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add claude-flow npx @claude-flow/cli@v3alpha mcp start' }; } // Check disk space (async with proper env inheritance) async function checkDiskSpace() { try { if (process.platform === 'win32') { return { name: 'Disk Space', status: 'pass', message: 'Check skipped on Windows' }; } const output_str = await runCommand('df -h . | tail -1'); const parts = output_str.split(/\s+/); const available = parts[3]; const usePercent = parseInt(parts[4]?.replace('%', '') || '0', 10); if (usePercent > 90) { return { name: 'Disk Space', status: 'fail', message: `${available} available (${usePercent}% used)`, fix: 'Free up disk space' }; } else if (usePercent > 80) { return { name: 'Disk Space', status: 'warn', message: `${available} available (${usePercent}% used)` }; } return { name: 'Disk Space', status: 'pass', message: `${available} available` }; } catch { return { name: 'Disk Space', status: 'warn', message: 'Unable to check' }; } } // Check TypeScript/build (async with proper env inheritance) async function checkBuildTools() { try { const tscVersion = await runCommand('npx tsc --version', 10000); // tsc can be slow if (!tscVersion || tscVersion.includes('not found')) { return { name: 'TypeScript', status: 'warn', message: 'Not installed locally', fix: 'npm install -D typescript' }; } return { name: 'TypeScript', status: 'pass', message: tscVersion.replace('Version ', 'v') }; } catch { return { name: 'TypeScript', status: 'warn', message: 'Not installed locally', fix: 'npm install -D typescript' }; } } // Check for stale npx cache (version freshness) async function checkVersionFreshness() { try { // Get current CLI version from package.json // Use import.meta.url to reliably locate our own package.json, // regardless of how deep the compiled file sits (e.g. dist/src/commands/). let currentVersion = '0.0.0'; try { const thisFile = fileURLToPath(import.meta.url); let dir = dirname(thisFile); // Walk up from the current file's directory until we find the // package.json that belongs to @claude-flow/cli (or claude-flow/cli). // Walk until dirname(dir) === dir (filesystem root on any platform). for (;;) { const candidate = join(dir, 'package.json'); try { if (existsSync(candidate)) { const pkg = JSON.parse(readFileSync(candidate, 'utf8')); if (pkg.version && typeof pkg.name === 'string' && (pkg.name === '@claude-flow/cli' || pkg.name === 'claude-flow' || pkg.name === 'ruflo')) { currentVersion = pkg.version; break; } } } catch { // Unreadable/invalid JSON -- skip and keep walking up } const parent = dirname(dir); if (parent === dir) break; // reached root dir = parent; } } catch { // Fall back to a default currentVersion = '0.0.0'; } // Check if running via npx (look for _npx in process path or argv) const isNpx = process.argv[1]?.includes('_npx') || process.env.npm_execpath?.includes('npx') || process.cwd().includes('_npx'); // Query npm for latest version (using alpha tag since that's what we publish to) let latestVersion = currentVersion; try { const npmInfo = await runCommand('npm view @claude-flow/cli@alpha version', 5000); latestVersion = npmInfo.trim(); } catch { // Can't reach npm registry - skip check return { name: 'Version Freshness', status: 'warn', message: `v${currentVersion} (cannot check registry)` }; } // Parse version numbers for comparison (handle prerelease like 3.0.0-alpha.84) const parseVersion = (v) => { const match = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-[a-zA-Z]+\.(\d+))?/); if (!match) return { major: 0, minor: 0, patch: 0, prerelease: 0 }; return { major: parseInt(match[1], 10) || 0, minor: parseInt(match[2], 10) || 0, patch: parseInt(match[3], 10) || 0, prerelease: parseInt(match[4], 10) || 0 }; }; const current = parseVersion(currentVersion); const latest = parseVersion(latestVersion); // Compare versions (including prerelease number) const isOutdated = (latest.major > current.major || (latest.major === current.major && latest.minor > current.minor) || (latest.major === current.major && latest.minor === current.minor && latest.patch > current.patch) || (latest.major === current.major && latest.minor === current.minor && latest.patch === current.patch && latest.prerelease > current.prerelease)); if (isOutdated) { const fix = isNpx ? 'rm -rf ~/.npm/_npx/* && npx -y @claude-flow/cli@latest' : 'npm update @claude-flow/cli'; return { name: 'Version Freshness', status: 'warn', message: `v${currentVersion} (latest: v${latestVersion})${isNpx ? ' [npx cache stale]' : ''}`, fix }; } return { name: 'Version Freshness', status: 'pass', message: `v${currentVersion} (up to date)` }; } catch (error) { return { name: 'Version Freshness', status: 'warn', message: 'Unable to check version freshness' }; } } // Check Claude Code CLI (async with proper env inheritance) async function checkClaudeCode() { try { const version = await runCommand('claude --version'); // Parse version from output like "claude 1.0.0" or "Claude Code v1.0.0" const versionMatch = version.match(/v?(\d+\.\d+\.\d+)/); const versionStr = versionMatch ? `v${versionMatch[1]}` : version; return { name: 'Claude Code CLI', status: 'pass', message: versionStr }; } catch { return { name: 'Claude Code CLI', status: 'warn', message: 'Not installed', fix: 'npm install -g @anthropic-ai/claude-code' }; } } // Install Claude Code CLI async function installClaudeCode() { try { output.writeln(); output.writeln(output.bold('Installing Claude Code CLI...')); execSync('npm install -g @anthropic-ai/claude-code', { encoding: 'utf8', stdio: 'inherit' }); output.writeln(output.success('Claude Code CLI installed successfully!')); return true; } catch (error) { output.writeln(output.error('Failed to install Claude Code CLI')); if (error instanceof Error) { output.writeln(output.dim(error.message)); } return false; } } // Check agentic-flow v3 integration (filesystem-based to avoid slow WASM/DB init) async function checkAgenticFlow() { try { // Walk common node_modules paths to find agentic-flow/package.json const candidates = [ join(process.cwd(), 'node_modules', 'agentic-flow', 'package.json'), join(process.cwd(), '..', 'node_modules', 'agentic-flow', 'package.json'), ]; let pkgJsonPath = null; for (const p of candidates) { if (existsSync(p)) { pkgJsonPath = p; break; } } if (!pkgJsonPath) { return { name: 'agentic-flow', status: 'warn', message: 'Not installed (optional — embeddings/routing will use fallbacks)', fix: 'npm install agentic-flow@latest' }; } const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')); const version = pkg.version || 'unknown'; const exports = pkg.exports || {}; const features = [ exports['./reasoningbank'] ? 'ReasoningBank' : null, exports['./router'] ? 'Router' : null, exports['./transport/quic'] ? 'QUIC' : null, ].filter(Boolean); return { name: 'agentic-flow', status: 'pass', message: `v${version} (${features.join(', ')})` }; } catch { return { name: 'agentic-flow', status: 'warn', message: 'Check failed' }; } } // Format health check result function formatCheck(check) { const icon = check.status === 'pass' ? output.success('✓') : check.status === 'warn' ? output.warning('⚠') : output.error('✗'); return `${icon} ${check.name}: ${check.message}`; } // Main doctor command export const doctorCommand = { name: 'doctor', description: 'System diagnostics and health checks', options: [ { name: 'fix', short: 'f', description: 'Show fix commands for issues', type: 'boolean', default: false }, { name: 'install', short: 'i', description: 'Auto-install missing dependencies (Claude Code CLI)', type: 'boolean', default: false }, { name: 'component', short: 'c', description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript)', type: 'string' }, { name: 'verbose', short: 'v', description: 'Verbose output', type: 'boolean', default: false } ], examples: [ { command: 'claude-flow doctor', description: 'Run full health check' }, { command: 'claude-flow doctor --fix', description: 'Show fixes for issues' }, { command: 'claude-flow doctor --install', description: 'Auto-install missing dependencies' }, { command: 'claude-flow doctor -c version', description: 'Check for stale npx cache' }, { command: 'claude-flow doctor -c claude', description: 'Check Claude Code CLI only' } ], action: async (ctx) => { const showFix = ctx.flags.fix; const autoInstall = ctx.flags.install; const component = ctx.flags.component; const verbose = ctx.flags.verbose; output.writeln(); output.writeln(output.bold('Claude Flow Doctor')); output.writeln(output.dim('System diagnostics and health check')); output.writeln(output.dim('─'.repeat(50))); output.writeln(); const allChecks = [ checkVersionFreshness, checkNodeVersion, checkNpmVersion, checkClaudeCode, checkGit, checkGitRepo, checkConfigFile, checkDaemonStatus, checkMemoryDatabase, checkApiKeys, checkMcpServers, checkDiskSpace, checkBuildTools, checkAgenticFlow ]; const componentMap = { 'version': checkVersionFreshness, 'freshness': checkVersionFreshness, 'node': checkNodeVersion, 'npm': checkNpmVersion, 'claude': checkClaudeCode, 'config': checkConfigFile, 'daemon': checkDaemonStatus, 'memory': checkMemoryDatabase, 'api': checkApiKeys, 'git': checkGit, 'mcp': checkMcpServers, 'disk': checkDiskSpace, 'typescript': checkBuildTools, 'agentic-flow': checkAgenticFlow }; let checksToRun = allChecks; if (component && componentMap[component]) { checksToRun = [componentMap[component]]; } const results = []; const fixes = []; // OPTIMIZATION: Run all checks in parallel for 3-5x faster execution const spinner = output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' }); spinner.start(); try { // Execute all checks concurrently const checkResults = await Promise.allSettled(checksToRun.map(check => check())); spinner.stop(); // Process results in order for (const settledResult of checkResults) { if (settledResult.status === 'fulfilled') { const result = settledResult.value; results.push(result); output.writeln(formatCheck(result)); if (result.fix && (result.status === 'fail' || result.status === 'warn')) { fixes.push(`${result.name}: ${result.fix}`); } } else { const errorResult = { name: 'Check', status: 'fail', message: settledResult.reason?.message || 'Unknown error' }; results.push(errorResult); output.writeln(formatCheck(errorResult)); } } } catch (error) { spinner.stop(); output.writeln(output.error('Failed to run health checks')); } // Auto-install missing dependencies if requested if (autoInstall) { const claudeCodeResult = results.find(r => r.name === 'Claude Code CLI'); if (claudeCodeResult && claudeCodeResult.status !== 'pass') { const installed = await installClaudeCode(); if (installed) { // Re-check Claude Code after installation const newCheck = await checkClaudeCode(); const idx = results.findIndex(r => r.name === 'Claude Code CLI'); if (idx !== -1) { results[idx] = newCheck; // Update fixes list const fixIdx = fixes.findIndex(f => f.startsWith('Claude Code CLI:')); if (fixIdx !== -1 && newCheck.status === 'pass') { fixes.splice(fixIdx, 1); } } output.writeln(formatCheck(newCheck)); } } } // Summary const passed = results.filter(r => r.status === 'pass').length; const warnings = results.filter(r => r.status === 'warn').length; const failed = results.filter(r => r.status === 'fail').length; output.writeln(); output.writeln(output.dim('─'.repeat(50))); output.writeln(); const summaryParts = [ output.success(`${passed} passed`), warnings > 0 ? output.warning(`${warnings} warnings`) : null, failed > 0 ? output.error(`${failed} failed`) : null ].filter(Boolean); output.writeln(`Summary: ${summaryParts.join(', ')}`); // Show fixes if (showFix && fixes.length > 0) { output.writeln(); output.writeln(output.bold('Suggested Fixes:')); output.writeln(); for (const fix of fixes) { output.writeln(output.dim(` ${fix}`)); } } else if (fixes.length > 0 && !showFix) { output.writeln(); output.writeln(output.dim(`Run with --fix to see ${fixes.length} suggested fix${fixes.length > 1 ? 'es' : ''}`)); } // Overall result if (failed > 0) { output.writeln(); output.writeln(output.error('Some checks failed. Please address the issues above.')); return { success: false, exitCode: 1, data: { passed, warnings, failed, results } }; } else if (warnings > 0) { output.writeln(); output.writeln(output.warning('All checks passed with some warnings.')); return { success: true, data: { passed, warnings, failed, results } }; } else { output.writeln(); output.writeln(output.success('All checks passed! System is healthy.')); return { success: true, data: { passed, warnings, failed, results } }; } } }; export default doctorCommand; //# sourceMappingURL=doctor.js.map