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
885 lines • 38.3 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 { createHash } from 'crypto';
import { execSync, exec } from 'child_process';
import { promisify } from 'util';
import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
import { isEncryptedBlob } from '../encryption/vault.js';
// 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() {
// JSON configs (parse-validated). The first three are LEGACY shapes from
// pre-v3 init flows; v3 init writes only `.claude-flow/config.yaml`.
const jsonPaths = [
'.claude-flow/config.json',
'claude-flow.config.json',
'.claude-flow.json'
];
// YAML configs (existence-checked only — no heavy yaml parser dependency).
const yamlPaths = [
'.claude-flow/config.yaml',
'.claude-flow/config.yml',
'claude-flow.config.yaml'
];
// #1798 — collect ALL configs that exist instead of returning at the first
// hit. The previous early-return masked silent collisions: if both a v2
// JSON and a v3 YAML existed, doctor reported only the JSON while the
// daemon was actually reading from the YAML. Surfacing both lets the user
// see and resolve the disagreement.
const foundJson = [];
const invalidJson = [];
for (const configPath of jsonPaths) {
if (!existsSync(configPath))
continue;
try {
JSON.parse(readFileSync(configPath, 'utf8'));
foundJson.push(configPath);
}
catch {
invalidJson.push(configPath);
}
}
const foundYaml = yamlPaths.filter(p => existsSync(p));
// Hard failures first: malformed JSON wins.
if (invalidJson.length > 0) {
return { name: 'Config File', status: 'fail', message: `Invalid JSON: ${invalidJson.join(', ')}`, fix: 'Fix JSON syntax in config file' };
}
// #1798 — collision: legacy JSON + new YAML both present. Subsystems can
// disagree on which to read; surface this as a warn with the recommended
// resolution (keep the YAML, archive the JSON).
if (foundJson.length > 0 && foundYaml.length > 0) {
return {
name: 'Config File',
status: 'warn',
message: `Config collision: legacy ${foundJson.join(', ')} + ${foundYaml.join(', ')} — subsystems may disagree silently`,
fix: `Archive the legacy JSON (mv ${foundJson[0]} ${foundJson[0]}.bak) and keep ${foundYaml[0]} as the canonical config`,
};
}
if (foundYaml.length > 0) {
return { name: 'Config File', status: 'pass', message: `Found: ${foundYaml[0]}` };
}
if (foundJson.length > 0) {
return { name: 'Config File', status: 'pass', message: `Found: ${foundJson[0]}` };
}
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() {
// Authoritative path comes from `getMemoryRoot()` (honors
// `CLAUDE_FLOW_MEMORY_PATH`, claude-flow.config.json's `memory.persistPath`,
// then defaults to `.swarm/`). #1946: the previous hard-coded list missed
// `data/memory/memory.db` (a common config) and ignored the env var
// entirely, so doctor reported "Not initialized" on perfectly-init'd DBs.
// Try the configured path first, then fall back to the historic candidates.
const candidates = [];
try {
const { getMemoryRoot } = await import('../memory/memory-initializer.js');
candidates.push(join(getMemoryRoot(), 'memory.db'));
}
catch {
/* memory-initializer not available — fall through to legacy candidates */
}
candidates.push('.swarm/memory.db', '.claude-flow/memory.db', 'data/memory/memory.db', // matches `CLAUDE_FLOW_MEMORY_PATH=data/memory`
'data/memory.db');
for (const dbPath of candidates) {
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);
}
}
// Detect Claude Code environment — API keys are managed internally
const inClaudeCode = !!(process.env.CLAUDE_CODE || process.env.CLAUDE_PROJECT_DIR || process.env.MCP_SESSION_ID);
if (found.includes('ANTHROPIC_API_KEY') || found.includes('CLAUDE_API_KEY')) {
return { name: 'API Keys', status: 'pass', message: `Found: ${found.join(', ')}` };
}
else if (inClaudeCode) {
return { name: 'API Keys', status: 'pass', message: 'Claude Code (managed internally)' };
}
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)
//
// #1791.7 — `git rev-parse` was reported as failing on hosts where `.git`
// clearly exists in cwd (linux-arm64 daemon contexts). Treat the git binary
// as authoritative when it succeeds, but fall back to a `.git` walk-up so a
// present repository is recognized even when the git invocation fails for
// environment reasons (PATH, broken global config, EBADCWD, etc.).
async function checkGitRepo() {
try {
await runCommand('git rev-parse --is-inside-work-tree');
return { name: 'Git Repository', status: 'pass', message: 'In a git repository' };
}
catch {
// Walk parents of cwd for a .git directory before reporting "not a repo"
let dir = process.cwd();
while (true) {
if (existsSync(join(dir, '.git'))) {
return {
name: 'Git Repository',
status: 'warn',
message: `Repo detected on disk (${join(dir, '.git')}) but \`git rev-parse\` failed — check git installation and PATH`,
fix: 'Verify git is on PATH (try `git --version`) and that the working tree is not corrupted',
};
}
const parent = dirname(dir);
if (parent === dir)
break;
dir = parent;
}
return { name: 'Git Repository', status: 'warn', message: 'Not a git repository', fix: 'git init' };
}
}
// Check AIDefence package availability (#1807)
//
// `aidefence_*` MCP tools (scan, analyze, has_pii, stats, learn) require
// `@claude-flow/aidefence` to be installed and loadable. The package is an
// optional dependency — present in some installs (project-local) but
// missing in others (npm-global of `claude-flow`). Without it, every
// aidefence MCP call fails at runtime with "Cannot find module".
//
// Surface that state in `doctor` so operators know BEFORE they rely on
// AI-defence scanning. The probe is the same dynamic `import()` the MCP
// tool's handler uses, so a `pass` here means the actual tools will work.
async function checkAIDefence() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
await import('@claude-flow/aidefence');
return {
name: 'AIDefence',
status: 'pass',
message: '@claude-flow/aidefence loadable — aidefence_* MCP tools functional',
};
}
catch {
return {
name: 'AIDefence',
status: 'warn',
message: '@claude-flow/aidefence not loadable — aidefence_* MCP tools will fail (optional package)',
fix: 'npm install --save @claude-flow/aidefence (in your project), or run `claude-flow mcp start` from a directory that has it installed',
};
}
}
/**
* ADR-097 Phase 4: federation peer-state surface for doctor.
*
* Probes the federation plugin loadability + asserts the breaker entity
* layer is present in the installed version. Without the plugin
* installed this is a "not configured" pass — federation is opt-in.
*
* Live coordinator state (per-peer counts) requires a running MCP server
* with `federation_init` called; operators inspect that via the
* `federation_breaker_status` MCP tool, not the doctor (which is a
* one-shot CLI process with no coordinator session).
*/
async function checkFederationBreaker() {
try {
// Optional plugin — not a hard dep of @claude-flow/cli. Build the
// module specifier dynamically so TypeScript cannot statically
// resolve it (which would emit TS2307); at runtime the import
// either resolves (plugin installed) or throws (handled below).
const specifier = ['@claude-flow', 'plugin-agent-federation'].join('/');
const mod = await import(specifier);
if (!mod.FederationNodeState) {
return {
name: 'Federation Breaker',
status: 'warn',
message: '@claude-flow/plugin-agent-federation loaded but FederationNodeState export missing — version older than ADR-097 Phase 2',
fix: 'Upgrade: npm install @claude-flow/plugin-agent-federation@alpha',
};
}
return {
name: 'Federation Breaker',
status: 'pass',
message: 'ADR-097 breaker loadable — federation_breaker_status / federation_evict / federation_reactivate MCP tools available',
};
}
catch {
return {
name: 'Federation Breaker',
status: 'pass',
message: 'Federation plugin not installed (optional) — install only if you need cross-installation peering',
fix: 'npm install --save @claude-flow/plugin-agent-federation@alpha',
};
}
}
// Check MCP servers
async function checkMcpServers() {
const home = process.env.HOME || process.env.USERPROFILE || '';
// #1842: ~/.claude.json holds project-scoped registrations under
// parsed.projects[<projectPath>].mcpServers.ruflo, in addition to any
// top-level mcpServers. Check both shapes plus the legacy desktop and
// local .mcp.json paths.
const mcpConfigPaths = [
join(home, '.claude.json'),
join(home, '.claude/claude_desktop_config.json'),
join(home, '.config/claude/mcp.json'),
'.mcp.json',
];
const isRufloKey = (k) => k === 'ruflo' || k === 'ruflo_alpha' || k === 'claude-flow' || k === 'claude-flow_alpha';
for (const configPath of mcpConfigPaths) {
if (!existsSync(configPath))
continue;
try {
const content = JSON.parse(readFileSync(configPath, 'utf8'));
// Top-level mcpServers (legacy / desktop form)
const topServers = content.mcpServers || content.servers || {};
const topServerKeys = Object.keys(topServers);
const topHasRuflo = topServerKeys.some(isRufloKey);
// Project-scoped (Claude Code shape): projects[*].mcpServers.ruflo
let projectHits = 0;
let projectScannedServers = 0;
if (content.projects && typeof content.projects === 'object') {
for (const projectVal of Object.values(content.projects)) {
const pm = projectVal?.mcpServers;
if (pm && typeof pm === 'object') {
const keys = Object.keys(pm);
projectScannedServers += keys.length;
if (keys.some(isRufloKey))
projectHits += 1;
}
}
}
const totalServers = topServerKeys.length + projectScannedServers;
if (topHasRuflo || projectHits > 0) {
const where = topHasRuflo
? 'top-level'
: `${projectHits} project-scoped`;
return {
name: 'MCP Servers',
status: 'pass',
message: `${totalServers} servers (ruflo configured: ${where})`,
};
}
if (totalServers > 0) {
return {
name: 'MCP Servers',
status: 'warn',
message: `${totalServers} servers (ruflo not found)`,
fix: 'claude mcp add ruflo -- npx -y ruflo@latest mcp start',
};
}
}
catch {
// continue to next path
}
}
return {
name: 'MCP Servers',
status: 'warn',
message: 'No MCP config found',
fix: 'claude mcp add ruflo -- npx -y ruflo@latest 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' };
}
// Use df -Ph for POSIX mode (guarantees single-line output even with long device names)
const output_str = await runCommand('df -Ph . | tail -1');
const parts = output_str.split(/\s+/);
// POSIX format: Filesystem Size Used Avail Capacity Mounted
const available = parts[3];
const usePercent = parseInt(parts[4]?.replace('%', '') || '0', 10);
if (isNaN(usePercent)) {
return { name: 'Disk Space', status: 'warn', message: `${available || 'unknown'} available (unable to parse usage)` };
}
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' };
}
}
// Check encryption-at-rest status (ADR-096 Phase 5)
//
// Reports four facets without disclosing the key itself:
// 1. Gate status — is CLAUDE_FLOW_ENCRYPT_AT_REST set?
// 2. Key resolution — does CLAUDE_FLOW_ENCRYPTION_KEY resolve to a valid
// 32-byte key (env-var path only; keychain/passphrase are deferred)?
// 3. Key fingerprint — first 16 hex chars of sha256(key) so users can
// sanity-check across machines without ever logging the key bytes.
// 4. High-tier store presence — for sessions/, terminals/, .swarm/memory.db
// report whether on-disk bytes carry the RFE1 magic (encrypted) or not.
async function checkEncryptionAtRest() {
if (!isEncryptionEnabled()) {
return {
name: 'Encryption at Rest',
status: 'warn',
message: 'Off — session/terminal/memory stores are plaintext (mode 0600 only)',
fix: 'export CLAUDE_FLOW_ENCRYPT_AT_REST=1 && export CLAUDE_FLOW_ENCRYPTION_KEY=<64-char-hex>',
};
}
// Gate is on — try to resolve the key. Fail-closed if missing or malformed.
const rawKey = process.env.CLAUDE_FLOW_ENCRYPTION_KEY;
if (!rawKey) {
return {
name: 'Encryption at Rest',
status: 'fail',
message: 'Gate is on but CLAUDE_FLOW_ENCRYPTION_KEY is unset (fail-closed)',
fix: 'Generate a key: openssl rand -hex 32 → export CLAUDE_FLOW_ENCRYPTION_KEY=<value>',
};
}
let keyFingerprint;
try {
const key = decodeKey(rawKey);
keyFingerprint = createHash('sha256').update(key).digest('hex').slice(0, 16);
}
catch (err) {
return {
name: 'Encryption at Rest',
status: 'fail',
message: `CLAUDE_FLOW_ENCRYPTION_KEY invalid: ${err instanceof Error ? err.message : String(err)}`,
fix: 'Provide a 64-char hex or 44-char base64 key (32 bytes)',
};
}
// Check the three high-tier store paths for RFE1 magic
const cwd = process.cwd();
const stores = [
{ label: 'sessions/', path: join(cwd, '.claude-flow', 'sessions') },
{ label: 'terminals', path: join(cwd, '.claude-flow', 'terminals', 'store.json') },
{ label: 'memory.db', path: join(cwd, '.swarm', 'memory.db') },
];
const status = [];
for (const s of stores) {
if (!existsSync(s.path)) {
status.push(`${s.label}=∅`);
continue;
}
try {
const stat = statSync(s.path);
if (stat.isDirectory()) {
// Sessions: probe the first .json file
const { readdirSync } = await import('fs');
const files = readdirSync(s.path).filter(f => f.endsWith('.json'));
if (files.length === 0) {
status.push(`${s.label}=∅`);
continue;
}
const first = readFileSync(join(s.path, files[0]));
status.push(`${s.label}=${isEncryptedBlob(first) ? 'enc' : 'plain'}`);
}
else {
const buf = readFileSync(s.path);
status.push(`${s.label}=${isEncryptedBlob(buf) ? 'enc' : 'plain'}`);
}
}
catch {
status.push(`${s.label}=err`);
}
}
return {
name: 'Encryption at Rest',
status: 'pass',
message: `On — key fp:${keyFingerprint}… (${status.join(' ')})`,
};
}
// 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',
// #1791.5 — flag name was misleading: it does NOT auto-apply fixes,
// it only prints the suggested commands so the user can run them
// themselves. Make that explicit in the help output.
description: 'Print suggested fix commands (does not auto-apply — copy/paste them yourself)',
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: 'Print suggested fix commands (does not auto-apply)' },
{ 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('RuFlo 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,
checkAIDefence, // #1807
checkDiskSpace,
checkBuildTools,
checkAgenticFlow,
checkEncryptionAtRest, // ADR-096 Phase 5
checkFederationBreaker, // ADR-097 Phase 4
];
const componentMap = {
'version': checkVersionFreshness,
'freshness': checkVersionFreshness,
'node': checkNodeVersion,
'npm': checkNpmVersion,
'claude': checkClaudeCode,
'config': checkConfigFile,
'daemon': checkDaemonStatus,
'memory': checkMemoryDatabase,
'api': checkApiKeys,
'git': checkGit,
'mcp': checkMcpServers,
'aidefence': checkAIDefence, // #1807
'disk': checkDiskSpace,
'typescript': checkBuildTools,
'agentic-flow': checkAgenticFlow,
'encryption': checkEncryptionAtRest, // ADR-096 Phase 5
'federation': checkFederationBreaker, // ADR-097 Phase 4
};
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 — #1791.5: header makes it explicit these are commands you
// run yourself, not actions doctor took.
if (showFix && fixes.length > 0) {
output.writeln();
output.writeln(output.bold('Suggested commands (run them yourself):'));
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 command${fixes.length > 1 ? 's' : ''} (does not auto-apply)`));
}
// 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