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