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

1,093 lines 52.6 kB
/**
 * V3 CLI Daemon Command
 * Manages background worker daemon (Node.js-based, similar to shell helpers)
 */
import { output } from '../output.js';
import { getDaemon, startDaemon, stopDaemon } from '../services/worker-daemon.js';
import { fork } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import * as fs from 'fs';
// Start daemon subcommand
const startCommand = {
    name: 'start',
    description: 'Start the worker daemon with all enabled background workers',
    options: [
        { name: 'workers', short: 'w', type: 'string', description: 'Comma-separated list of workers to enable (default: map,audit,optimize,consolidate,testgaps)' },
        { name: 'quiet', short: 'Q', type: 'boolean', description: 'Suppress output' },
        { name: 'background', short: 'b', type: 'boolean', description: 'Run daemon in background (detached process)', default: true },
        { name: 'foreground', short: 'f', type: 'boolean', description: 'Run daemon in foreground (blocks terminal)' },
        { name: 'headless', type: 'boolean', description: 'Enable headless worker execution (E2B sandbox)' },
        { name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
        { name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
        { name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
        // #1914: workspace root for this daemon. Set automatically when the
        // background launcher forks the foreground child so the daemon process
        // carries its workspace path in argv — `killStaleDaemons` then only
        // reaps daemons belonging to the current workspace (ADR-014 scope).
        { name: 'workspace', type: 'string', description: 'Workspace root for this daemon (internal — set automatically when forking)' },
    ],
    examples: [
        { command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
        { command: 'claude-flow daemon start --foreground', description: 'Start in foreground (blocks terminal)' },
        { command: 'claude-flow daemon start -w map,audit,optimize', description: 'Start with specific workers' },
        { command: 'claude-flow daemon start --headless --sandbox strict', description: 'Start with headless workers in strict sandbox' },
    ],
    action: async (ctx) => {
        const quiet = ctx.flags.quiet;
        const foreground = ctx.flags.foreground;
        // #1914: a forked daemon child receives --workspace <root>; the launcher
        // and interactive invocations have no flag and fall back to cwd.
        const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace) ?? process.cwd();
        const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
        // Parse resource threshold overrides from CLI flags
        const config = {};
        const rawMaxCpu = ctx.flags['max-cpu-load'];
        const rawMinMem = ctx.flags['min-free-memory'];
        // Strict numeric pattern to prevent command injection when forwarding to subprocess (S1)
        const NUMERIC_RE = /^\d+(\.\d+)?$/;
        const sanitize = (s) => s.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
        if (rawMaxCpu || rawMinMem) {
            const thresholds = {};
            if (rawMaxCpu) {
                const val = parseFloat(rawMaxCpu);
                if (NUMERIC_RE.test(rawMaxCpu) && isFinite(val) && val > 0 && val <= 1000) {
                    thresholds.maxCpuLoad = val;
                }
                else if (!quiet) {
                    output.printWarning(`Ignoring invalid --max-cpu-load value: ${sanitize(rawMaxCpu)}`);
                }
            }
            if (rawMinMem) {
                const val = parseFloat(rawMinMem);
                if (NUMERIC_RE.test(rawMinMem) && isFinite(val) && val >= 0 && val <= 100) {
                    thresholds.minFreeMemoryPercent = val;
                }
                else if (!quiet) {
                    output.printWarning(`Ignoring invalid --min-free-memory value: ${sanitize(rawMinMem)}`);
                }
            }
            if (thresholds.maxCpuLoad !== undefined || thresholds.minFreeMemoryPercent !== undefined) {
                config.resourceThresholds = thresholds;
            }
        }
        // Check if background daemon already running (skip if we ARE the daemon process)
        if (!isDaemonProcess) {
            const bgPid = getBackgroundDaemonPid(projectRoot);
            if (bgPid && isProcessRunning(bgPid)) {
                if (!quiet) {
                    output.printWarning(`Daemon already running in background (PID: ${bgPid}). Stop it first with: daemon stop`);
                }
                return { success: true };
            }
            // #1551: Kill any stale daemon processes that weren't tracked by PID file
            await killStaleDaemons(projectRoot, quiet);
        }
        // Background mode (default): fork a detached process.
        // #1968: previously only forwarded resource thresholds — `--workers`,
        // `--headless`, and `--sandbox` were dropped on the floor when the
        // launcher forked the foreground child, so `daemon start --workers map`
        // got the full default worker set instead.
        if (!foreground) {
            return startBackgroundDaemon(projectRoot, quiet, {
                maxCpuLoad: rawMaxCpu,
                minFreeMemory: rawMinMem,
                workers: ctx.flags.workers,
                headless: ctx.flags.headless,
                sandbox: ctx.flags.sandbox,
            });
        }
        // Foreground mode: run in current process (blocks terminal)
        try {
            const stateDir = join(projectRoot, '.claude-flow');
            const pidFile = join(stateDir, 'daemon.pid');
            // Ensure state directory exists
            if (!fs.existsSync(stateDir)) {
                fs.mkdirSync(stateDir, { recursive: true });
            }
            // NOTE: Do NOT write PID file here — startDaemon() writes it internally.
            // Writing it before startDaemon() causes checkExistingDaemon() to detect
            // our own PID and return early, leaving no workers scheduled (#1478 Bug 1).
            // Clean up PID file on exit
            const cleanup = () => {
                try {
                    if (fs.existsSync(pidFile)) {
                        fs.unlinkSync(pidFile);
                    }
                }
                catch { /* ignore */ }
            };
            process.on('exit', cleanup);
            process.on('SIGINT', () => { cleanup(); process.exit(0); });
            process.on('SIGTERM', () => { cleanup(); process.exit(0); });
            // Ignore SIGHUP on macOS/Linux — prevents daemon death when terminal closes (#1283)
            if (process.platform !== 'win32') {
                process.on('SIGHUP', () => { });
            }
            if (!quiet) {
                const spinner = output.createSpinner({ text: 'Starting worker daemon...', spinner: 'dots' });
                spinner.start();
                const daemon = await startDaemon(projectRoot, config);
                const status = daemon.getStatus();
                spinner.succeed('Worker daemon started (foreground mode)');
                output.writeln();
                output.printBox([
                    `PID: ${status.pid}`,
                    `Started: ${status.startedAt?.toISOString()}`,
                    `Workers: ${status.config.workers.filter(w => w.enabled).length} enabled`,
                    `Max Concurrent: ${status.config.maxConcurrent}`,
                    `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
                    `Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
                ].join('\n'), 'Daemon Status');
                output.writeln();
                output.writeln(output.bold('Scheduled Workers'));
                output.printTable({
                    columns: [
                        { key: 'type', header: 'Worker', width: 15 },
                        { key: 'interval', header: 'Interval', width: 12 },
                        { key: 'priority', header: 'Priority', width: 10 },
                        { key: 'description', header: 'Description', width: 30 },
                    ],
                    data: status.config.workers
                        .filter(w => w.enabled)
                        .map(w => ({
                        type: output.highlight(w.type),
                        interval: `${Math.round(w.intervalMs / 60000)}min`,
                        priority: w.priority === 'critical' ? output.error(w.priority) :
                            w.priority === 'high' ? output.warning(w.priority) :
                                output.dim(w.priority),
                        description: w.description,
                    })),
                });
                output.writeln();
                output.writeln(output.dim('Press Ctrl+C to stop daemon'));
                // Listen for worker events
                daemon.on('worker:start', ({ type }) => {
                    output.writeln(output.dim(`[daemon] Worker starting: ${type}`));
                });
                daemon.on('worker:complete', ({ type, durationMs }) => {
                    output.writeln(output.success(`[daemon] Worker completed: ${type} (${durationMs}ms)`));
                });
                daemon.on('worker:error', ({ type, error }) => {
                    output.writeln(output.error(`[daemon] Worker failed: ${type} - ${error}`));
                });
                // Keep process alive — setInterval creates a ref'd handle that prevents
                // Node.js from exiting even when startDaemon's timers are unref'd (#1478 Bug 2).
                setInterval(() => { }, 60_000);
                await new Promise(() => { }); // Never resolves - daemon runs until killed
            }
            else {
                await startDaemon(projectRoot, config);
                setInterval(() => { }, 60_000); // Keep alive with ref'd handle (#1478)
                await new Promise(() => { }); // Keep alive
            }
            return { success: true };
        }
        catch (error) {
            output.printError(`Failed to start daemon: ${error instanceof Error ? error.message : String(error)}`);
            return { success: false, exitCode: 1 };
        }
    },
};
/**
 * Validate path for security - prevents path traversal and injection
 */
function validatePath(path, label) {
    // Must be absolute after resolution
    const resolved = resolve(path);
    // Check for null bytes (injection attack)
    if (path.includes('\0')) {
        throw new Error(`${label} contains null bytes`);
    }
    // Check for shell metacharacters in path components
    if (/[;&|`$<>]/.test(path)) {
        throw new Error(`${label} contains shell metacharacters`);
    }
    // Prevent path traversal outside expected directories
    if (!resolved.includes('.claude-flow') && !resolved.includes('bin')) {
        // Allow only paths within project structure
        const cwd = process.cwd();
        if (!resolved.startsWith(cwd)) {
            throw new Error(`${label} escapes project directory`);
        }
    }
}
/**
 * #1914: Resolve the `--workspace` flag to an absolute path, or return null
 * if it is absent / not a usable string. Rejects values with null bytes or
 * shell metacharacters (defence-in-depth — the value is later embedded in a
 * forked child's argv and compared against `ps`/`tasklist` output).
 */
export function resolveWorkspaceFlag(raw) {
    if (typeof raw !== 'string')
        return null;
    const trimmed = raw.trim();
    if (!trimmed)
        return null;
    if (trimmed.includes('\0') || /[;&|`$<>]/.test(trimmed))
        return null;
    return resolve(trimmed);
}
/**
 * #1914: True when a process command line (from `ps -eo command` on POSIX or
 * the tasklist Window Title column on Windows) belongs to a daemon started
 * for `workspaceRoot`. The launcher (`startBackgroundDaemon`) always appends
 * `--workspace <root>` as the FINAL argv entry, so an exact trailing match
 * after stripping trailing whitespace/quotes is unambiguous — even for
 * workspace paths containing spaces — and never a bare path-prefix match,
 * so workspace `/a/proj` does not reap `/a/proj-other`'s daemon. A daemon
 * whose argv puts `--workspace` mid-list (only possible via a hand-rolled
 * invocation) simply won't be auto-reaped — `daemon stop` still handles it
 * via the PID file.
 */
export function daemonCommandLineBelongsToWorkspace(commandLine, workspaceRoot) {
    return commandLine.replace(/[\s"']+$/u, '').endsWith(`--workspace ${workspaceRoot}`);
}
async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
    const { maxCpuLoad, minFreeMemory, workers, headless, sandbox } = forwarded;
    // Validate and resolve project root
    const resolvedRoot = resolve(projectRoot);
    validatePath(resolvedRoot, 'Project root');
    const stateDir = join(resolvedRoot, '.claude-flow');
    const pidFile = join(stateDir, 'daemon.pid');
    const logFile = join(stateDir, 'daemon.log');
    // Validate all paths
    validatePath(stateDir, 'State directory');
    validatePath(pidFile, 'PID file');
    validatePath(logFile, 'Log file');
    // Ensure state directory exists
    if (!fs.existsSync(stateDir)) {
        fs.mkdirSync(stateDir, { recursive: true });
    }
    // Get path to CLI (from dist/src/commands/daemon.js -> bin/cli.js)
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    // dist/src/commands -> dist/src -> dist -> package root -> bin/cli.js
    const cliPath = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
    validatePath(cliPath, 'CLI path');
    // Verify CLI path exists
    if (!fs.existsSync(cliPath)) {
        output.printError(`CLI not found at: ${cliPath}`);
        return { success: false, exitCode: 1 };
    }
    // Platform-aware spawn flags. We use child_process.fork() because the daemon
    // child is itself a Node script — fork() spawns Node directly and skips the
    // cmd.exe interpretation pass that broke Windows + Node 25 when
    // process.execPath contained a space (#1691). It also avoids the [DEP0190]
    // shell:true security warning.
    const isWin = process.platform === 'win32';
    const forkOpts = {
        cwd: resolvedRoot,
        // detached: true on every platform (#1766). On Windows, leaving detached:false
        // kept the child in the parent's process group AND the IPC pipe held the
        // child to npx — when npx exited, the IPC pipe tore down and the daemon
        // died within ~1s. detached:true + child.disconnect() (below) gives the
        // child its own session/pgid and breaks the IPC pipe so the daemon
        // genuinely survives parent exit. On POSIX, detached:true was already the
        // path; this just makes Windows match.
        detached: true,
        // Use 'ignore' for all stdio + 'ignore' for the IPC channel via silent:true off.
        // fork() defaults to creating an IPC channel; we don't need it here, so we
        // pass stdio explicitly. Passing fs.openSync() FDs causes the child to die
        // on Windows when the parent exits and closes the FDs (#1478 Bug 3) — the
        // daemon writes its own logs via appendFileSync to .claude-flow/logs/.
        stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
        windowsHide: true,
        env: {
            ...process.env,
            CLAUDE_FLOW_DAEMON: '1',
            // Prevent macOS SIGHUP kill when terminal closes
            ...(process.platform === 'darwin' ? { NOHUP: '1' } : {}),
        },
    };
    // Forward args to the foreground child. fork() resolves the script path
    // via Node's normal module resolution, so cliPath does not need to be
    // shell-quoted even when it contains spaces.
    const forkArgs = ['daemon', 'start', '--foreground', '--quiet'];
    // Validate with strict numeric pattern to prevent injection via crafted flags.
    const SPAWN_NUMERIC_RE = /^\d+(\.\d+)?$/;
    if (maxCpuLoad && SPAWN_NUMERIC_RE.test(maxCpuLoad)) {
        forkArgs.push('--max-cpu-load', maxCpuLoad);
    }
    if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
        forkArgs.push('--min-free-memory', minFreeMemory);
    }
    // #1968: forward worker-selection / sandbox flags. The previous launcher
    // dropped these, so `daemon start --workers map` ran with the default
    // five-worker set instead of just `map`. Validate each before passing
    // through — argv goes straight to a forked process so reject anything
    // that doesn't look like a comma-separated worker-name list or one of
    // the allowed sandbox modes.
    const WORKERS_RE = /^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/;
    if (typeof workers === 'string' && workers.length > 0 && WORKERS_RE.test(workers)) {
        forkArgs.push('--workers', workers);
    }
    if (headless === true) {
        forkArgs.push('--headless');
    }
    if (typeof sandbox === 'string' && (sandbox === 'strict' || sandbox === 'permissive' || sandbox === 'disabled')) {
        forkArgs.push('--sandbox', sandbox);
    }
    // #1914: stamp the workspace into argv (kept LAST) so the foreground daemon
    // process is self-identifying and `killStaleDaemons` only reaps daemons
    // belonging to this workspace. resolvedRoot was validatePath()'d above.
    forkArgs.push('--workspace', resolvedRoot);
    const child = fork(cliPath, forkArgs, forkOpts);
    // Get PID from spawned process directly (no shell echo needed)
    const pid = child.pid;
    if (!pid || pid <= 0) {
        output.printError('Failed to get daemon PID');
        return { success: false, exitCode: 1 };
    }
    // Unref BEFORE writing PID file — prevents race where parent exits
    // but child hasn't fully detached yet (fixes macOS daemon death #1283).
    child.unref();
    // #1766: also break the IPC pipe explicitly. unref() releases the libuv
    // handle but does NOT close the IPC channel; on Windows the open IPC
    // pipe keeps the daemon tied to its parent npx, and when npx exits the
    // pipe is torn down and the daemon exits with it. disconnect() severs
    // the IPC pipe so the daemon truly stands on its own. Wrapped in try
    // because disconnect() throws if the IPC channel is already gone.
    try {
        child.disconnect();
    }
    catch { /* IPC channel already closed */ }
    // Longer delay to let the child process start and write its own PID file.
    // 100ms was too short on Windows; the child's checkExistingDaemon() would
    // find the parent-written PID and return early (#1478 Bug 1).
    await new Promise(resolve => setTimeout(resolve, 500));
    // Write PID file only if the child hasn't already written its own.
    // The foreground child calls writePidFile() internally, but on some platforms
    // it may not have started yet, so we write as a fallback.
    if (!fs.existsSync(pidFile)) {
        fs.writeFileSync(pidFile, String(pid));
    }
    if (!quiet) {
        output.printSuccess(`Daemon started in background (PID: ${pid})`);
        output.printInfo(`Logs: ${logFile}`);
        output.printInfo(`Stop with: claude-flow daemon stop`);
    }
    return { success: true };
}
// Stop daemon subcommand
const stopCommand = {
    name: 'stop',
    description: 'Stop the worker daemon and all background workers',
    options: [
        { name: 'quiet', short: 'Q', type: 'boolean', description: 'Suppress output' },
    ],
    examples: [
        { command: 'claude-flow daemon stop', description: 'Stop the daemon' },
    ],
    action: async (ctx) => {
        const quiet = ctx.flags.quiet;
        const projectRoot = process.cwd();
        try {
            if (!quiet) {
                const spinner = output.createSpinner({ text: 'Stopping worker daemon...', spinner: 'dots' });
                spinner.start();
                // Try to stop in-process daemon first
                await stopDaemon();
                // Also kill any background daemon by PID
                const killed = await killBackgroundDaemon(projectRoot);
                // #1551: Also kill stale daemon processes not tracked by PID file
                await killStaleDaemons(projectRoot, true);
                spinner.succeed(killed ? 'Worker daemon stopped' : 'Worker daemon was not running');
            }
            else {
                await stopDaemon();
                await killBackgroundDaemon(projectRoot);
                await killStaleDaemons(projectRoot, true);
            }
            return { success: true };
        }
        catch (error) {
            output.printError(`Failed to stop daemon: ${error instanceof Error ? error.message : String(error)}`);
            return { success: false, exitCode: 1 };
        }
    },
};
/**
 * Kill background daemon process using PID file
 */
async function killBackgroundDaemon(projectRoot) {
    const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
    if (!fs.existsSync(pidFile)) {
        return false;
    }
    try {
        const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
        if (isNaN(pid)) {
            fs.unlinkSync(pidFile);
            return false;
        }
        // Check if process is running
        try {
            process.kill(pid, 0); // Signal 0 = check if alive
        }
        catch {
            // Process not running, clean up stale PID file
            fs.unlinkSync(pidFile);
            return false;
        }
        // Kill the process
        process.kill(pid, 'SIGTERM');
        // Wait a moment then force kill if needed
        await new Promise(resolve => setTimeout(resolve, 1000));
        try {
            process.kill(pid, 0);
            // Still alive, force kill
            process.kill(pid, 'SIGKILL');
        }
        catch {
            // Process terminated
        }
        // Clean up PID file
        if (fs.existsSync(pidFile)) {
            fs.unlinkSync(pidFile);
        }
        return true;
    }
    catch (error) {
        // Clean up PID file on any error
        if (fs.existsSync(pidFile)) {
            fs.unlinkSync(pidFile);
        }
        return false;
    }
}
/**
 * Kill stale daemon processes not tracked by the PID file (#1551, #1857).
 * Uses `ps` on POSIX and `tasklist` on Windows to find all daemon
 * processes for this project and kill them.
 */
async function killStaleDaemons(projectRoot, quiet) {
    if (process.platform === 'win32') {
        return killStaleDaemonsWindows(projectRoot, quiet);
    }
    return killStaleDaemonsPosix(projectRoot, quiet);
}
async function killStaleDaemonsPosix(projectRoot, quiet) {
    try {
        const { execFileSync } = await import('child_process');
        const psOutput = execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
        const lines = psOutput.split('\n');
        const currentPid = process.pid;
        const trackedPid = getBackgroundDaemonPid(projectRoot);
        // #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
        const resolvedRoot = resolve(projectRoot);
        let killed = 0;
        for (const line of lines) {
            if (!line.includes('daemon start --foreground'))
                continue;
            if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
                continue;
            // #1914: skip daemons from other workspaces (or pre-#1914 versions that
            // didn't stamp --workspace — let `daemon stop` handle those via PID file).
            if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
                continue;
            const pidStr = line.trim().split(/\s+/)[0];
            const pid = parseInt(pidStr, 10);
            if (isNaN(pid) || pid === currentPid || pid === trackedPid)
                continue;
            if (!isProcessRunning(pid))
                continue;
            try {
                process.kill(pid, 'SIGTERM');
                killed++;
                if (!quiet) {
                    output.printWarning(`Killed stale daemon process (PID: ${pid})`);
                }
            }
            catch { /* ignore — may have exited between check and kill */ }
        }
        if (killed > 0 && !quiet) {
            output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
        }
    }
    catch {
        // ps not available or failed — skip stale cleanup
    }
}
/**
 * #1857: Windows replacement for the POSIX `ps -eo pid,command` path.
 * Uses `tasklist /v /fo csv` which returns CSV with the full Window
 * Title column (last field) — Node-spawned daemon processes carry
 * their command line there. Best-effort like the POSIX path: any
 * tooling failure (tasklist missing, parse error, etc.) is swallowed
 * silently so cleanup doesn't break daemon start.
 */
async function killStaleDaemonsWindows(projectRoot, quiet) {
    try {
        const { execFileSync } = await import('child_process');
        // /v includes the Window Title; /fo csv uses comma-separated quoted fields
        const out = execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 });
        const lines = out.split(/\r?\n/);
        const currentPid = process.pid;
        const trackedPid = getBackgroundDaemonPid(projectRoot);
        // #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
        const resolvedRoot = resolve(projectRoot);
        let killed = 0;
        for (const line of lines) {
            if (!line.trim())
                continue;
            // Match daemon command line markers — the Window Title field
            // typically holds the full invocation. Skip rows that aren't ours.
            if (!line.includes('daemon start --foreground'))
                continue;
            if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
                continue;
            // #1914: skip daemons from other workspaces (or pre-#1914 versions).
            if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
                continue;
            // Parse CSV: tasklist quotes each field, so split on `","`
            const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
            // fields[0] = Image Name, fields[1] = PID, …
            const pidStr = fields[1];
            const pid = parseInt(pidStr ?? '', 10);
            if (isNaN(pid) || pid === currentPid || pid === trackedPid)
                continue;
            if (!isProcessRunning(pid))
                continue;
            try {
                // taskkill is the Windows equivalent of kill — /pid <n> /f forces.
                // Use SIGTERM-equivalent (no /f) first; the daemon's signal handler
                // catches and cleans up; force-kill is the next start's job.
                execFileSync('taskkill', ['/pid', String(pid), '/t'], { encoding: 'utf-8', timeout: 5000 });
                killed++;
                if (!quiet) {
                    output.printWarning(`Killed stale daemon process (PID: ${pid})`);
                }
            }
            catch { /* taskkill failed — process may have exited; ignore */ }
        }
        if (killed > 0 && !quiet) {
            output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
        }
    }
    catch {
        // tasklist not available or failed — skip stale cleanup. Defensive
        // shape matches the POSIX path. Not tested on Windows by the
        // maintainer; please report regressions on the issue tracker.
    }
}
/**
 * Get PID of background daemon from PID file
 */
function getBackgroundDaemonPid(projectRoot) {
    const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
    if (!fs.existsSync(pidFile)) {
        return null;
    }
    try {
        const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
        return isNaN(pid) ? null : pid;
    }
    catch {
        return null;
    }
}
/**
 * Check if a process is running
 */
function isProcessRunning(pid) {
    try {
        process.kill(pid, 0); // Signal 0 = check if alive
        return true;
    }
    catch {
        return false;
    }
}
// Status subcommand
const statusCommand = {
    name: 'status',
    description: 'Show daemon and worker status',
    options: [
        { name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed worker statistics' },
        { name: 'show-modes', type: 'boolean', description: 'Show worker execution modes (local/headless) and sandbox settings' },
    ],
    examples: [
        { command: 'claude-flow daemon status', description: 'Show daemon status' },
        { command: 'claude-flow daemon status -v', description: 'Show detailed status' },
        { command: 'claude-flow daemon status --show-modes', description: 'Show worker execution modes' },
    ],
    action: async (ctx) => {
        const verbose = ctx.flags.verbose;
        const showModes = ctx.flags['show-modes'];
        const projectRoot = process.cwd();
        try {
            const daemon = getDaemon(projectRoot);
            const status = daemon.getStatus();
            // Also check for background daemon
            const bgPid = getBackgroundDaemonPid(projectRoot);
            const bgRunning = bgPid ? isProcessRunning(bgPid) : false;
            const isRunning = status.running || bgRunning;
            const displayPid = bgPid || status.pid;
            output.writeln();
            // Daemon status box
            const statusIcon = isRunning ? output.success('●') : output.error('○');
            const statusText = isRunning ? output.success('RUNNING') : output.error('STOPPED');
            const mode = bgRunning ? output.dim(' (background)') : status.running ? output.dim(' (foreground)') : '';
            output.printBox([
                `Status: ${statusIcon} ${statusText}${mode}`,
                `PID: ${displayPid}`,
                status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
                `Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
                `Max Concurrent: ${status.config.maxConcurrent}`,
                `Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
                `Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
            ].filter(Boolean).join('\n'), 'RuFlo Daemon');
            output.writeln();
            output.writeln(output.bold('Worker Status'));
            const workerData = status.config.workers.map(w => {
                const state = status.workers.get(w.type);
                // Check for headless mode from worker config or state
                const isHeadless = w.headless || state?.headless || false;
                const sandboxMode = w.sandbox || state?.sandbox || null;
                return {
                    type: w.enabled ? output.highlight(w.type) : output.dim(w.type),
                    enabled: w.enabled ? output.success('✓') : output.dim('○'),
                    status: state?.isRunning ? output.warning('running') :
                        w.enabled ? output.success('idle') : output.dim('disabled'),
                    runs: state?.runCount ?? 0,
                    success: state ? `${Math.round((state.successCount / Math.max(state.runCount, 1)) * 100)}%` : '-',
                    lastRun: state?.lastRun ? formatTimeAgo(state.lastRun) : output.dim('never'),
                    nextRun: state?.nextRun && w.enabled ? formatTimeUntil(state.nextRun) : output.dim('-'),
                    mode: isHeadless ? output.highlight('headless') : output.dim('local'),
                    sandbox: isHeadless ? (sandboxMode || 'strict') : output.dim('-'),
                };
            });
            // Build columns based on --show-modes flag
            const baseColumns = [
                { key: 'type', header: 'Worker', width: 12 },
                { key: 'enabled', header: 'On', width: 4 },
                { key: 'status', header: 'Status', width: 10 },
                { key: 'runs', header: 'Runs', width: 6 },
                { key: 'success', header: 'Success', width: 8 },
                { key: 'lastRun', header: 'Last Run', width: 12 },
                { key: 'nextRun', header: 'Next Run', width: 12 },
            ];
            const modeColumns = showModes ? [
                { key: 'mode', header: 'Mode', width: 10 },
                { key: 'sandbox', header: 'Sandbox', width: 12 },
            ] : [];
            output.printTable({
                columns: [...baseColumns, ...modeColumns],
                data: workerData,
            });
            if (verbose) {
                output.writeln();
                output.writeln(output.bold('Worker Configuration'));
                output.printTable({
                    columns: [
                        { key: 'type', header: 'Worker', width: 12 },
                        { key: 'interval', header: 'Interval', width: 10 },
                        { key: 'priority', header: 'Priority', width: 10 },
                        { key: 'avgDuration', header: 'Avg Duration', width: 12 },
                        { key: 'description', header: 'Description', width: 30 },
                    ],
                    data: status.config.workers.map(w => {
                        const state = status.workers.get(w.type);
                        return {
                            type: w.type,
                            interval: `${Math.round(w.intervalMs / 60000)}min`,
                            priority: w.priority,
                            avgDuration: state?.averageDurationMs ? `${Math.round(state.averageDurationMs)}ms` : '-',
                            description: w.description,
                        };
                    }),
                });
            }
            return { success: true, data: status };
        }
        catch (error) {
            // Daemon not initialized
            output.writeln();
            output.printBox([
                `Status: ${output.error('○')} ${output.error('NOT INITIALIZED')}`,
                '',
                'Run "claude-flow daemon start" to start the daemon',
            ].join('\n'), 'RuFlo Daemon');
            return { success: true };
        }
    },
};
// Trigger subcommand - manually run a worker
const triggerCommand = {
    name: 'trigger',
    description: 'Manually trigger a specific worker',
    options: [
        { name: 'worker', short: 'w', type: 'string', description: 'Worker type to trigger', required: true },
        { name: 'headless', type: 'boolean', description: 'Run triggered worker in headless mode (E2B sandbox)' },
    ],
    examples: [
        { command: 'claude-flow daemon trigger -w map', description: 'Trigger the map worker' },
        { command: 'claude-flow daemon trigger -w audit', description: 'Trigger security audit' },
        { command: 'claude-flow daemon trigger -w audit --headless', description: 'Trigger audit in headless sandbox' },
    ],
    action: async (ctx) => {
        const workerType = ctx.flags.worker;
        if (!workerType) {
            output.printError('Worker type is required. Use --worker or -w flag.');
            output.writeln();
            output.writeln('Available workers: map, audit, optimize, consolidate, testgaps, predict, document, ultralearn, refactor, benchmark, deepdive, preload');
            return { success: false, exitCode: 1 };
        }
        try {
            const daemon = getDaemon(process.cwd());
            const spinner = output.createSpinner({ text: `Running ${workerType} worker...`, spinner: 'dots' });
            spinner.start();
            const result = await daemon.triggerWorker(workerType);
            if (result.success) {
                spinner.succeed(`Worker ${workerType} completed in ${result.durationMs}ms`);
                if (result.output) {
                    output.writeln();
                    output.writeln(output.bold('Output'));
                    output.printJson(result.output);
                }
            }
            else {
                spinner.fail(`Worker ${workerType} failed: ${result.error}`);
            }
            return { success: result.success, data: result };
        }
        catch (error) {
            output.printError(`Failed to trigger worker: ${error instanceof Error ? error.message : String(error)}`);
            return { success: false, exitCode: 1 };
        }
    },
};
// Enable/disable worker subcommand
const enableCommand = {
    name: 'enable',
    description: 'Enable or disable a specific worker',
    options: [
        { name: 'worker', short: 'w', type: 'string', description: 'Worker type', required: true },
        { name: 'disable', short: 'd', type: 'boolean', description: 'Disable instead of enable' },
    ],
    examples: [
        { command: 'claude-flow daemon enable -w predict', description: 'Enable predict worker' },
        { command: 'claude-flow daemon enable -w document --disable', description: 'Disable document worker' },
    ],
    action: async (ctx) => {
        const workerType = ctx.flags.worker;
        const disable = ctx.flags.disable;
        if (!workerType) {
            output.printError('Worker type is required. Use --worker or -w flag.');
            return { success: false, exitCode: 1 };
        }
        try {
            const daemon = getDaemon(process.cwd());
            daemon.setWorkerEnabled(workerType, !disable);
            output.printSuccess(`Worker ${workerType} ${disable ? 'disabled' : 'enabled'}`);
            return { success: true };
        }
        catch (error) {
            output.printError(`Failed to ${disable ? 'disable' : 'enable'} worker: ${error instanceof Error ? error.message : String(error)}`);
            return { success: false, exitCode: 1 };
        }
    },
};
// Helper functions for time formatting
function formatTimeAgo(date) {
    const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
    if (seconds < 60)
        return `${seconds}s ago`;
    if (seconds < 3600)
        return `${Math.floor(seconds / 60)}m ago`;
    if (seconds < 86400)
        return `${Math.floor(seconds / 3600)}h ago`;
    return `${Math.floor(seconds / 86400)}d ago`;
}
function formatTimeUntil(date) {
    const seconds = Math.floor((date.getTime() - Date.now()) / 1000);
    if (seconds < 0)
        return 'now';
    if (seconds < 60)
        return `in ${seconds}s`;
    if (seconds < 3600)
        return `in ${Math.floor(seconds / 60)}m`;
    if (seconds < 86400)
        return `in ${Math.floor(seconds / 3600)}h`;
    return `in ${Math.floor(seconds / 86400)}d`;
}
// #1565: Supervisor installer subcommand. Writes a native auto-restart
// unit (launchd plist on macOS, systemd-user .service on Linux) so the
// daemon survives crashes and reboots without requiring the operator
// to manually run `daemon start` after every failure.
const installSupervisorCommand = {
    name: 'install-supervisor',
    description: 'Install OS-level auto-restart supervisor (launchd on macOS, systemd-user on Linux)',
    options: [
        { name: 'force', short: 'f', type: 'boolean', description: 'Overwrite existing unit file', default: 'false' },
        { name: 'load', type: 'boolean', description: 'Load/enable the unit immediately', default: 'true' },
        { name: 'dry-run', type: 'boolean', description: 'Print the unit file content without writing', default: 'false' },
    ],
    examples: [
        { command: 'claude-flow daemon install-supervisor', description: 'Install + load (auto-restart enabled)' },
        { command: 'claude-flow daemon install-supervisor --no-load', description: 'Write unit file but do not enable yet' },
        { command: 'claude-flow daemon install-supervisor --dry-run', description: 'Preview the unit file' },
    ],
    action: async (ctx) => {
        const force = ctx.flags.force === true;
        const load = ctx.flags.load !== false;
        const dryRun = ctx.flags['dry-run'] === true || ctx.flags.dryRun === true;
        const projectRoot = process.cwd();
        const platform = process.platform;
        if (platform === 'win32') {
            output.printError('Windows scheduled-task installer is not yet implemented.');
            output.printInfo('Use Task Scheduler manually, or follow this issue: https://github.com/ruvnet/ruflo/issues/1565');
            return { success: false, exitCode: 1 };
        }
        if (platform !== 'darwin' && platform !== 'linux') {
            output.printError(`Unsupported platform: ${platform}. Supported: darwin (launchd), linux (systemd-user).`);
            return { success: false, exitCode: 1 };
        }
        // Resolve absolute paths the unit file will reference.
        const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
        if (!home) {
            output.printError('HOME/USERPROFILE not set; cannot resolve user unit path.');
            return { success: false, exitCode: 1 };
        }
        const nodeBin = process.execPath;
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = dirname(__filename);
        const cliJs = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
        if (!fs.existsSync(cliJs)) {
            output.printError(`CLI not found at: ${cliJs}`);
            return { success: false, exitCode: 1 };
        }
        if (platform === 'darwin') {
            const plistDir = join(home, 'Library', 'LaunchAgents');
            const plistPath = join(plistDir, 'io.ruv.ruflo.daemon.plist');
            const logDir = join(projectRoot, '.claude-flow', 'logs');
            const plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>io.ruv.ruflo.daemon</string>
    <key>ProgramArguments</key>
    <array>
        <string>${nodeBin}</string>
        <string>${cliJs}</string>
        <string>daemon</string><string>start</string><string>--foreground</string><string>--quiet</string>
    </array>
    <key>WorkingDirectory</key><string>${projectRoot}</string>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key><false/>
        <key>Crashed</key><true/>
    </dict>
    <key>ThrottleInterval</key><integer>10</integer>
    <key>StandardOutPath</key><string>${logDir}/supervisor.out.log</string>
    <key>StandardErrorPath</key><string>${logDir}/supervisor.err.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>CLAUDE_FLOW_DAEMON</key><string>1</string>
    </dict>
</dict>
</plist>
`;
            if (dryRun) {
                output.writeln(plist);
                return { success: true };
            }
            if (fs.existsSync(plistPath) && !force) {
                output.printWarning(`Already installed: ${plistPath}`);
                output.printInfo('Use --force to overwrite.');
                return { success: false, exitCode: 1 };
            }
            if (!fs.existsSync(plistDir))
                fs.mkdirSync(plistDir, { recursive: true });
            if (!fs.existsSync(logDir))
                fs.mkdirSync(logDir, { recursive: true });
            fs.writeFileSync(plistPath, plist, 'utf-8');
            output.printSuccess(`Wrote ${plistPath}`);
            if (load) {
                try {
                    const { execFileSync } = await import('child_process');
                    // unload first in case a previous version is loaded
                    try {
                        execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
                    }
                    catch { /* ok */ }
                    execFileSync('launchctl', ['load', '-w', plistPath], { encoding: 'utf-8', timeout: 5000 });
                    output.printSuccess('Supervisor loaded — daemon will auto-restart on crash and survive reboot.');
                }
                catch (err) {
                    output.printWarning(`launchctl load failed: ${err instanceof Error ? err.message : String(err)}`);
                    output.printInfo(`Run manually: launchctl load -w ${plistPath}`);
                }
            }
            else {
                output.printInfo(`Run when ready:  launchctl load -w ${plistPath}`);
            }
            return { success: true };
        }
        // Linux: systemd-user
        const unitDir = join(home, '.config', 'systemd', 'user');
        const unitPath = join(unitDir, 'ruflo-daemon.service');
        const unit = `[Unit]
Description=RuFlo background worker daemon
After=default.target

[Service]
Type=simple
WorkingDirectory=${projectRoot}
Environment=CLAUDE_FLOW_DAEMON=1
ExecStart=${nodeBin} ${cliJs} daemon start --foreground --quiet
Restart=on-failure
RestartSec=10
# Restart on Crashed (signal) too
StartLimitIntervalSec=300
StartLimitBurst=5

[Install]
WantedBy=default.target
`;
        if (dryRun) {
            output.writeln(unit);
            return { success: true };
        }
        if (fs.existsSync(unitPath) && !force) {
            output.printWarning(`Already installed: ${unitPath}`);
            output.printInfo('Use --force to overwrite.');
            return { success: false, exitCode: 1 };
        }
        if (!fs.existsSync(unitDir))
            fs.mkdirSync(unitDir, { recursive: true });
        fs.writeFileSync(unitPath, unit, 'utf-8');
        output.printSuccess(`Wrote ${unitPath}`);
        if (load) {
            try {
                const { execFileSync } = await import('child_process');
                execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8', timeout: 5000 });
                execFileSync('systemctl', ['--user', 'enable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 10000 });
                output.printSuccess('Supervisor enabled — daemon will auto-restart on crash and survive reboot.');
                output.printInfo('Note: requires `loginctl enable-linger $USER` for restart-after-logout on some distros.');
            }
            catch (err) {
                output.printWarning(`systemctl --user enable failed: ${err instanceof Error ? err.message : String(err)}`);
                output.printInfo(`Run manually: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
            }
        }
        else {
            output.printInfo(`Run when ready:  systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
        }
        return { success: true };
    },
};
const uninstallSupervisorCommand = {
    name: 'uninstall-supervisor',
    description: 'Remove the auto-restart supervisor unit (launchd on macOS, systemd-user on Linux)',
    options: [],
    action: async () => {
        const platform = process.platform;
        const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
        if (platform === 'darwin') {
            const plistPath = join(home, 'Library', 'LaunchAgents', 'io.ruv.ruflo.daemon.plist');
            try {
                const { execFileSync } = await import('child_process');
                try {
                    execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
                }
                catch { /* ok */ }
            }
            catch { /* ignore */ }
            if (fs.existsSync(plistPath)) {
                fs.unlinkSync(plistPath);
                output.printSuccess(`Removed ${plistPath}`);
            }
            else {
                output.printInfo(`Not installed: ${plistPath}`);
            }
            return { success: true };
        }
        if (platform === 'linux') {
            const unitPath = join(home, '.config', 'systemd', 'user', 'ruflo-daemon.service');
            try {
                const { execFileSync } = await import('child_process');
                try {
                    execFileSync('systemctl', ['--user', 'disable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 5000 });
                }
                catch { /* ok */ }
            }
            catch { /* ignore */ }
            if (fs.existsSync(unitPath)) {
                fs.unlinkSync(unitPath);
                output.printSuccess(`Removed ${unitPath}`);
            }
            else {
                output.printInfo(`Not installed: ${unitPath}`);
            }
            return { success: true };
        }
        output.printError(`Unsupported platform: ${platform}`);
        return { success: false, exitCode: 1 };
    },
};
// Main daemon command
export const daemonCommand = {
    name: 'daemon',
    description: 'Manage background worker daemon (Node.js-based, auto-runs like shell helpers)',
    subcommands: [
        startCommand,
        stopCommand,
        statusCommand,
        triggerCommand,
        enableCommand,
        installSupervisorCommand,
        uninstallSupervisorCommand,
    ],
    options: [],
    examples: [
        { command: 'claude-flow daemon start', description: 'Start the daemon' },
        { command: 'claude-flow daemon start --headless', description: 'Start with headless workers (E2B sandbox)' },
        { command: 'claude-flow daemon status', description: 'Check daemon status' },
        { command: 'claude-flow daemon stop', description: 'Stop the daemon' },
        { command: 'claude-flow daemon trigger -w audit', description: 'Run security audit' },
    ],
    action: async () => {
        output.writeln();
        output.writeln(output.bold('RuFlo Daemon - Background Task Management'));
        output.writeln();
        output.writeln('Node.js-based background worker system that auto-runs like shell daemons.');
        output.writeln('Manages 12 specialized workers for continuous optimization and monitoring.');
        output.writeln();
        output.writeln(output.bold('Headless Mode'));
        output.writeln('Workers can run in headless mode using E2B sandboxes for isolated execution.');
        output.writeln('Use --headless flag with start/trigger commands. Sandbox modes: strict, permissive, disabled.');
        output.writeln();
        output.writeln(output.bold('Available Workers'));
        output.printList([
            `${output.highlight('map')}         - Codebase mapping (5 min interval)`,
            `${output.highlight('audit')}       - Security analysis (10 min interval)`,
            `${output.highlight('optimize')}    - Performance optimization (15 min interval)`,
            `${output.highlight('consolidate')} - Memory consolidation (30 min interval)`,
            `${output.highlight('testgaps')}    - Test coverage analysis (20 min interval)`,
            `${output.highlight('predict')}     - Predictive preloading (2 min, disabled by default)`,
            `${output.highlight('document')}    - Auto-documentation (60 min, disabled by default)`,
            `${output.highlight('ultralearn')}  - Deep knowledge acquisition (manual trigger)`,
            `${output.highlight('refactor')}    - Code refactoring suggestions (manual trigger)`,
            `${output.highlight('benchmark')}   - Performance benchmarking (manual trigger)`,
            `${output.highlight('deepdive')}    - Deep code analysis (manual trigger)`,
            `${output.highlight('preload')}     - Resource preloading (manual trigger)`,
        ]);
        output.writeln();
        output.writeln(output.bold('Subcommands'));
        output.printList([
            `${output.highlight('start')}   - Start the daemon`,
            `${output.highlight('stop')}    - Stop the daemon`,
            `${output.highlight('status')}  - Show daemon status`,
            `${output.highlight('trigger')} - Manually run a worker`,
            `${output.highlight('enable')}  - Enable/disable a worker`,
        ]);
        output.writeln();
        output.writeln('Run "claude-flow daemon <subcommand> --help" for details');
        return { success: true };
    },
};
export default daemonCommand;
//# sourceMappingURL=daemon.js.map