/** * Add command for Torm CLI. * * Adds a torrent from a magnet link or .torrent file. * * @module cli/commands/add */ import React, { useEffect, useState } from 'react'; import { render, Text, Box } from 'ink'; import { existsSync } from 'fs'; import { resolve } from 'path'; import { Torrent } from '../../engine/types.js'; import { getDaemonClient } from '../../daemon/index.js'; import { formatBytes, isMagnetUri, successMessage, errorMessage, formatKeyValue, } from '../utils/output.js'; // ============================================================================= // Types // ============================================================================= export interface AddCommandOptions { /** The torrent source (magnet link or file path) */ source: string; /** Optional download path override */ downloadPath?: string; /** Whether to start the torrent immediately */ start?: boolean; } interface AddResultProps { torrent: Torrent | null; error: string | null; loading: boolean; } // ============================================================================= // Components // ============================================================================= /** * Component to display the result of adding a torrent */ const AddResult: React.FC = ({ torrent, error, loading }) => { if (loading) { return ( Adding torrent... ); } if (error) { return ( [ERROR] {error} ); } if (torrent) { return ( [OK] Torrent added successfully Name: {torrent.name} Hash: {torrent.infoHash} Size: {formatBytes(torrent.size)} Files: {torrent.files.length} file(s) ); } return null; }; // ============================================================================= // Main Add Function // ============================================================================= /** * Validate and prepare the source string. * Returns the source string to send to daemon. */ async function validateSource(source: string): Promise { // Check if it's a magnet link if (isMagnetUri(source)) { return source; } // Check if it's a file path const absolutePath = resolve(source); if (existsSync(absolutePath)) { return absolutePath; } throw new Error( `Invalid torrent source: "${source}"\n` + 'Expected a magnet link (magnet:?...) or path to a .torrent file.\n' + 'Tip: Use quotes around magnet links or use "torm add -c" to paste from clipboard.' ); } /** * Execute the add command. * * @param options - Command options */ export async function executeAdd(options: AddCommandOptions): Promise { const { source, downloadPath, start = true } = options; let client; try { // Connect to daemon (starts it if not running) client = await getDaemonClient(); // Prepare the source (validate and get the string source) // For file paths, we need to read the file and the daemon will parse it // Actually, the daemon only accepts string sources (magnet or file path) // Let's validate first const sourceToSend = await validateSource(source); // Add the torrent via daemon const torrent = await client.addTorrent(sourceToSend, { downloadPath: downloadPath, startImmediately: start, }); // Display success console.log(successMessage('Torrent added successfully')); console.log(); console.log(formatKeyValue('Name', torrent.name)); console.log(formatKeyValue('Hash', torrent.infoHash)); console.log(formatKeyValue('Size', formatBytes(torrent.size))); console.log(formatKeyValue('Files', `${torrent.files.length} file(s)`)); client.disconnect(); } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(errorMessage(message)); if (client) { client.disconnect(); } process.exit(1); } } /** * Interactive add command using Ink for rendering */ export function AddCommand({ source, downloadPath, start = true, }: AddCommandOptions): React.ReactElement { const [torrent, setTorrent] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const doAdd = async () => { let client; try { // Connect to daemon (starts it if not running) client = await getDaemonClient(); // Validate source const sourceToSend = await validateSource(source); // Add torrent via daemon const addedTorrent = await client.addTorrent(sourceToSend, { downloadPath: downloadPath, startImmediately: start, }); setTorrent(addedTorrent); client.disconnect(); } catch (err) { const message = err instanceof Error ? err.message : String(err); setError(message); if (client) { client.disconnect(); } } finally { setLoading(false); } }; doAdd(); }, [source, downloadPath, start]); return ; } /** * Run the add command with Ink rendering */ export function runAdd(options: AddCommandOptions): void { const { waitUntilExit } = render(); waitUntilExit().then(() => { process.exit(0); }); } export default executeAdd;