/** * Daemon command for Torm CLI. * * Manages the background daemon process. * * @module cli/commands/daemon */ import React, { useEffect, useState } from 'react'; import { render, Text, Box } from 'ink'; import { DaemonManager, type DaemonInfo } from '../../daemon/index.js'; import { formatBytes, formatDuration, successMessage, errorMessage, formatKeyValue, } from '../utils/output.js'; // ============================================================================= // Types // ============================================================================= export type DaemonAction = 'start' | 'stop' | 'status' | 'restart'; export interface DaemonCommandOptions { action: DaemonAction; } interface DaemonResultProps { info: DaemonInfo | null; action: DaemonAction; error: string | null; loading: boolean; message: string | null; } // ============================================================================= // Components // ============================================================================= /** * Component to display daemon status */ const DaemonStatusDisplay: React.FC<{ info: DaemonInfo }> = ({ info }) => { if (!info.running) { return ( Daemon is not running Start with: torm daemon start ); } return ( Daemon is running {info.pid && ( PID: {info.pid} )} {info.uptime !== undefined && ( Uptime: {formatDuration(info.uptime)} )} {info.torrents !== undefined && ( Active torrents: {info.torrents} )} {info.downloadSpeed !== undefined && ( Download: {formatBytes(info.downloadSpeed)}/s )} {info.uploadSpeed !== undefined && ( Upload: {formatBytes(info.uploadSpeed)}/s )} ); }; /** * Component to display the result of a daemon action */ const DaemonResult: React.FC = ({ info, action, error, loading, message, }) => { if (loading) { const actionText = { start: 'Starting daemon...', stop: 'Stopping daemon...', status: 'Checking daemon status...', restart: 'Restarting daemon...', }[action]; return ( {actionText} ); } if (error) { return ( [ERROR] {error} ); } if (message) { return ( {message} ); } if (info && action === 'status') { return ; } return null; }; // ============================================================================= // Main Daemon Functions // ============================================================================= /** * Execute the daemon start command */ async function executeStart(): Promise<{ pid: number }> { const manager = new DaemonManager(); // Check if already running const running = await manager.isRunning(); if (running) { const info = await manager.getStatus(); throw new Error(`Daemon is already running (PID: ${info.pid})`); } const pid = await manager.start(); return { pid }; } /** * Execute the daemon stop command */ async function executeStop(): Promise { const manager = new DaemonManager(); const running = await manager.isRunning(); if (!running) { throw new Error('Daemon is not running'); } await manager.stop(); } /** * Execute the daemon status command */ async function executeStatus(): Promise { const manager = new DaemonManager(); return manager.getStatus(); } /** * Execute the daemon command. * * @param options - Command options */ export async function executeDaemon( options: DaemonCommandOptions ): Promise { const { action } = options; try { switch (action) { case 'start': { const { pid } = await executeStart(); console.log(successMessage(`Daemon started (PID: ${pid})`)); break; } case 'stop': { await executeStop(); console.log(successMessage('Daemon stopped')); break; } case 'status': { const info = await executeStatus(); if (!info.running) { console.log('Daemon is not running'); } else { console.log(successMessage('Daemon is running')); console.log(formatKeyValue('PID', String(info.pid ?? 'unknown'))); if (info.uptime !== undefined) { console.log(formatKeyValue('Uptime', formatDuration(info.uptime))); } if (info.torrents !== undefined) { console.log(formatKeyValue('Torrents', String(info.torrents))); } if (info.downloadSpeed !== undefined) { console.log( formatKeyValue('Download', `${formatBytes(info.downloadSpeed)}/s`) ); } if (info.uploadSpeed !== undefined) { console.log( formatKeyValue('Upload', `${formatBytes(info.uploadSpeed)}/s`) ); } } break; } case 'restart': { try { await executeStop(); } catch { // Ignore - daemon may not have been running } const { pid } = await executeStart(); console.log(successMessage(`Daemon restarted (PID: ${pid})`)); break; } } } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(errorMessage(message)); process.exit(1); } } /** * Interactive daemon command using Ink for rendering */ export function DaemonCommand({ action, }: DaemonCommandOptions): React.ReactElement { const [info, setInfo] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [message, setMessage] = useState(null); useEffect(() => { const doAction = async () => { try { switch (action) { case 'start': { const { pid } = await executeStart(); setMessage(`Daemon started (PID: ${pid})`); break; } case 'stop': { await executeStop(); setMessage('Daemon stopped'); break; } case 'status': { const status = await executeStatus(); setInfo(status); break; } case 'restart': { try { await executeStop(); } catch { // Ignore } const { pid } = await executeStart(); setMessage(`Daemon restarted (PID: ${pid})`); break; } } } catch (err) { const errMessage = err instanceof Error ? err.message : String(err); setError(errMessage); } finally { setLoading(false); } }; doAction(); }, [action]); return ( ); } /** * Run the daemon command with Ink rendering */ export function runDaemon(options: DaemonCommandOptions): void { const { waitUntilExit } = render(); waitUntilExit().then(() => { process.exit(0); }); } export default executeDaemon;