/** * rigstate daemon - Unified Guardian Daemon * * Usage: * rigstate daemon # Start daemon in foreground * rigstate daemon status # Check if daemon is running * rigstate daemon --no-bridge # Disable Agent Bridge * rigstate daemon --verbose # Enable verbose output */ /* eslint-disable no-console */ import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import fs from 'fs/promises'; import path from 'path'; import { createDaemon } from '../daemon/factory.js'; import { enableDaemon, disableDaemon } from '../utils/service-manager.js'; const PID_FILE = '.rigstate/daemon.pid'; const STATE_FILE = '.rigstate/daemon.state.json'; export function createDaemonCommand(): Command { const daemon = new Command('daemon') .description('Start the Guardian daemon for continuous monitoring'); // Main daemon command (start in foreground) daemon .argument('[action]', 'Action: start (default) or status', 'start') .option('--project ', 'Project ID (or use .rigstate manifest)') .option('--path ', 'Path to watch', '.') .option('--no-bridge', 'Disable Agent Bridge connection') .option('--verbose', 'Enable verbose output') .action(async (action: string, options: { project?: string; path?: string; bridge?: boolean; verbose?: boolean; }) => { if (action === 'status') { await showStatus(); return; } if (action === 'enable') { await enableDaemon(); return; } if (action === 'disable') { await disableDaemon(); return; } const spinner = ora(); try { // Check if already running - be silent if it's just a stale PID file const pidPath = path.join(process.cwd(), PID_FILE); try { const content = await fs.readFile(pidPath, 'utf-8'); const pid = parseInt(content.trim(), 10); try { process.kill(pid, 0); // If we get here, the process is actually running console.log(chalk.yellow('⚠ Another daemon instance is active (PID ' + pid + ').')); console.log(chalk.dim(` Run "rigstate daemon status" for details or Ctrl+C to stop.\n`)); } catch { // Process is dead, cleanup stale file silently await fs.unlink(pidPath).catch(() => { }); } } catch { // No PID file, all good } // Create daemon spinner.start('Initializing Guardian Daemon...'); const daemonInstance = await createDaemon({ project: options.project, path: options.path, noBridge: options.bridge === false, verbose: options.verbose }); spinner.stop(); // Write PID file await writePidFile(); // Handle shutdown gracefully process.on('SIGINT', async () => { console.log(chalk.dim('\n\nShutting down...')); await daemonInstance.stop(); await cleanupPidFile(); process.exit(0); }); process.on('SIGTERM', async () => { await daemonInstance.stop(); await cleanupPidFile(); process.exit(0); }); // Update state file periodically const stateInterval = setInterval(async () => { await writeStateFile(daemonInstance.getState()); }, 5000); daemonInstance.on('stopped', () => { clearInterval(stateInterval); }); // Start the daemon await daemonInstance.start(); // Keep the process alive await new Promise(() => { }); } catch (error: any) { spinner.fail(chalk.red('Failed to start daemon')); console.error(chalk.red('Error:'), error.message); process.exit(1); } }); return daemon; } async function isRunning(): Promise { try { const pidPath = path.join(process.cwd(), PID_FILE); const content = await fs.readFile(pidPath, 'utf-8'); const pid = parseInt(content.trim(), 10); // Check if process exists try { process.kill(pid, 0); return true; } catch { // Process doesn't exist, clean up stale PID file await fs.unlink(pidPath); return false; } } catch { return false; } } async function writePidFile(): Promise { try { const dir = path.join(process.cwd(), '.rigstate'); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(path.join(dir, 'daemon.pid'), process.pid.toString()); } catch { // Silently fail } } async function cleanupPidFile(): Promise { try { await fs.unlink(path.join(process.cwd(), PID_FILE)); await fs.unlink(path.join(process.cwd(), STATE_FILE)); } catch { // Silently fail } } async function writeStateFile(state: any): Promise { try { const dir = path.join(process.cwd(), '.rigstate'); await fs.mkdir(dir, { recursive: true }); await fs.writeFile( path.join(dir, 'daemon.state.json'), JSON.stringify(state, null, 2) ); } catch { // Silently fail } } async function showStatus(): Promise { console.log(chalk.bold('\nšŸ›”ļø Guardian Daemon Status\n')); const running = await isRunning(); if (!running) { console.log(chalk.yellow('Status: Not running')); console.log(chalk.dim('Use "rigstate daemon" to start.\n')); return; } console.log(chalk.green('Status: Running')); // Read state file try { const statePath = path.join(process.cwd(), STATE_FILE); const content = await fs.readFile(statePath, 'utf-8'); const state = JSON.parse(content); console.log(chalk.dim('─'.repeat(40))); console.log(`Started at: ${state.startedAt || 'Unknown'}`); console.log(`Files checked: ${state.filesChecked || 0}`); console.log(`Violations: ${state.violationsFound || 0}`); console.log(`Tasks processed: ${state.tasksProcessed || 0}`); console.log(`Last activity: ${state.lastActivity || 'None'}`); console.log(chalk.dim('─'.repeat(40))); } catch { console.log(chalk.dim('(State file not found)')); } // Read PID try { const pidPath = path.join(process.cwd(), PID_FILE); const pid = await fs.readFile(pidPath, 'utf-8'); console.log(chalk.dim(`PID: ${pid.trim()}`)); } catch { // No PID file } console.log(''); }