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
JavaScript
/**
* 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