ferry-cli 0.1.20

The ferry command for terminal-native LAN file transfer with QUIC, discovery, resume, TUI, and JSON output.
Documentation
use std::net::SocketAddr;
use std::path::PathBuf;

use clap::{Args, CommandFactory, Parser, Subcommand};

#[derive(Debug, Parser)]
#[command(
    name = "ferry",
    version,
    about = "Terminal-native LAN file transfer",
    long_about = "FileFerry moves files and directories between local-network ferry peers over QUIC, with mDNS discovery, fingerprint pinning, daemon receive mode, a TUI send flow, and newline-delimited JSON for scripts.",
    after_help = "Examples:\n  ferry recv --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry send stephen-mbp ./bundle\n  ferry send --fingerprint <full-fingerprint> 192.168.1.42:53318 ./bundle\n  ferry --json send 192.168.1.42:53318 ./bundle\n  ferry peers trust <alias-or-full-fingerprint>\n  ferry daemon --listen 0.0.0.0:53318 --dest ~/Downloads/ferry"
)]
pub struct Cli {
    #[arg(
        long,
        global = true,
        help = "Override the default command port where supported"
    )]
    pub port: Option<u16>,

    #[arg(
        long,
        global = true,
        help = "Bind command listeners to a specific interface where supported"
    )]
    pub bind: Option<String>,

    #[arg(long, global = true, help = "Emit newline-delimited JSON on stdout")]
    pub json: bool,

    #[arg(long, global = true, help = "Disable mDNS discovery and announcements")]
    pub no_discovery: bool,

    #[arg(short, long, global = true, help = "Reduce human output")]
    pub quiet: bool,

    #[arg(short, long, action = clap::ArgAction::Count, global = true, help = "Increase diagnostic output")]
    pub verbose: u8,

    #[command(subcommand)]
    pub command: Option<Command>,
}

impl Cli {
    pub fn clap_command() -> clap::Command {
        Self::command()
    }
}

#[derive(Debug, Subcommand)]
pub enum Command {
    #[command(
        about = "Send files or directories to a ferry peer",
        after_help = "Examples:\n  ferry send stephen-mbp ./bundle\n  ferry send 192.168.1.42:53318 ./bundle\n  ferry send --fingerprint <full-fingerprint> 192.168.1.42:53318 ./bundle\n  ferry send --psk-file ~/.config/ferry/transfer.psk 192.168.1.42:53318 ./bundle"
    )]
    Send(SendArgs),
    #[command(
        about = "Receive one transfer in the foreground",
        after_help = "Examples:\n  ferry recv --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry recv --no-discovery --listen 127.0.0.1:53318 --dest ./incoming\n  ferry recv --psk-file ~/.config/ferry/transfer.psk --listen 0.0.0.0:53318"
    )]
    Recv(RecvArgs),
    #[command(about = "List and manage trusted or discovered peers")]
    Peers {
        #[command(subcommand)]
        command: Option<PeersCommand>,
    },
    #[command(
        about = "Run a long-lived receiver using persisted daemon settings",
        after_help = "Examples:\n  ferry daemon\n  ferry daemon --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry daemon --psk-file ~/.config/ferry/transfer.psk"
    )]
    Daemon(DaemonArgs),
    #[command(about = "Print the redacted configuration")]
    Config,
    #[command(about = "Print the local alias and device fingerprint")]
    Identity,
    #[command(about = "Print the ferry version")]
    Version,
    #[command(about = "Open the interactive terminal send interface")]
    Tui,
}

#[derive(Debug, Args)]
pub struct SendArgs {
    #[arg(
        long,
        value_name = "FINGERPRINT",
        help = "Require the receiver certificate to match this full fingerprint"
    )]
    pub fingerprint: Option<String>,

    #[arg(
        long,
        value_name = "FILE",
        help = "Read an explicit transfer PSK from a file"
    )]
    pub psk_file: Option<PathBuf>,

    #[arg(help = "Peer alias, hostname, fingerprint prefix, or direct ip:port")]
    pub peer: String,
    #[arg(value_name = "PATH", help = "File or directory to send")]
    pub paths: Vec<PathBuf>,
}

#[derive(Debug, Args)]
pub struct RecvArgs {
    #[arg(long, help = "Listen address for incoming QUIC transfers")]
    pub listen: Option<SocketAddr>,

    #[arg(long, value_name = "DIR", help = "Destination directory")]
    pub dest: Option<PathBuf>,

    #[arg(
        long,
        help = "Accept the incoming transfer without interactive confirmation"
    )]
    pub accept_all: bool,

    #[arg(
        long,
        value_name = "FILE",
        help = "Require an explicit transfer PSK read from a file"
    )]
    pub psk_file: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub struct DaemonArgs {
    #[arg(long, help = "Listen address for incoming QUIC transfers")]
    pub listen: Option<SocketAddr>,

    #[arg(long, value_name = "DIR", help = "Destination directory")]
    pub dest: Option<PathBuf>,

    #[arg(
        long,
        value_name = "FILE",
        help = "Require an explicit transfer PSK read from a file"
    )]
    pub psk_file: Option<PathBuf>,
}

#[derive(Debug, Subcommand)]
pub enum PeersCommand {
    #[command(
        about = "Trust a full fingerprint or discovered peer hint",
        after_help = "Examples:\n  ferry peers trust 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n  ferry peers trust stephen-mbp\n  ferry peers trust 012345"
    )]
    Trust {
        #[arg(value_name = "FINGERPRINT_OR_HINT")]
        fingerprint: String,
    },
    #[command(about = "Remove a trusted peer fingerprint")]
    Forget {
        #[arg(value_name = "FINGERPRINT")]
        fingerprint: String,
    },
}