tail-fin-daemon 0.7.3

Long-running browser-session daemon for tail-fin (tfd binary). Keeps Chrome tabs warm across invocations via a Unix-socket protocol; registers Site implementations through a runtime Arc<dyn Site> registry.
Documentation
use clap::{Parser, Subcommand};

pub const DEFAULT_CDP_HOST: &str = "127.0.0.1:9222";

#[derive(Parser)]
#[command(name = "tfd", about = "tail-fin daemon & client", version)]
pub struct Cli {
    /// Unix socket path
    #[arg(
        long,
        default_value = "~/.tail-fin/daemon.sock",
        env = "TFD_SOCKET",
        global = true
    )]
    pub socket: String,

    /// Chrome CDP host (HOST:PORT) — used by business commands
    #[arg(
        long,
        default_value = DEFAULT_CDP_HOST,
        env = "TFD_HOST",
        global = true
    )]
    pub host: String,

    /// Explicit session_id (skips auto acquire/release)
    #[arg(long, env = "TFD_SESSION", global = true)]
    pub session: Option<String>,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Daemon lifecycle
    Daemon {
        #[command(subcommand)]
        cmd: DaemonCmd,
    },
    /// Session pool operations
    Session {
        #[command(subcommand)]
        cmd: SessionCmd,
    },
    /// SeekingAlpha commands
    Sa {
        #[command(subcommand)]
        cmd: crate::dispatch::sa::SaCmd,
    },
    /// Twitter commands
    Twitter {
        #[command(subcommand)]
        cmd: crate::dispatch::twitter::TwitterCmd,
    },
    /// Grok commands
    Grok {
        #[command(subcommand)]
        cmd: crate::dispatch::grok::GrokCmd,
    },
    /// Internal: alias for `daemon start` used by spawn::ensure_daemon.
    #[command(hide = true)]
    Start {
        #[arg(long, default_value_t = 300)]
        idle_timeout: u64,
        #[arg(long, default_value_t = 900)]
        daemon_idle: u64,
        #[arg(long, default_value_t = 3)]
        max_sessions: usize,
        /// Accepted for compatibility with night-fury's `ensure_daemon`. The
        /// process does not actually daemonize — the parent's `Command::spawn`
        /// already detaches it.
        #[arg(long, default_value_t = false)]
        detach: bool,
    },
}

#[derive(Subcommand)]
pub enum DaemonCmd {
    /// Start the daemon in the foreground
    Start {
        /// Session idle timeout in seconds (default 300)
        #[arg(long, default_value_t = 300)]
        idle_timeout: u64,
        /// Daemon idle shutdown after all sessions gone (default 900 = 15min)
        #[arg(long, default_value_t = 900)]
        daemon_idle: u64,
        /// Max sessions per pool (0 = unlimited, default 3)
        #[arg(long, default_value_t = 3)]
        max_sessions: usize,
    },
    /// Stop the daemon
    Stop,
    /// Show daemon status
    Status,
}

#[derive(Subcommand)]
pub enum SessionCmd {
    /// Acquire a session for a site (long-lived)
    Acquire {
        #[arg(long)]
        site: String,
        #[arg(long, default_value = DEFAULT_CDP_HOST)]
        host: String,
    },
    /// Release an acquired session back to the pool
    Release { session_id: String },
    /// List sessions in all pools
    List,
    /// Forcefully destroy a session and close its tab
    Destroy { session_id: String },
}

/// Expand leading `~/` to the user's home directory.
pub fn expand_home(path: &str) -> String {
    if let Some(rest) = path.strip_prefix("~/") {
        if let Ok(home) = std::env::var("HOME") {
            return format!("{home}/{rest}");
        }
    }
    path.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn expand_home_replaces_tilde() {
        std::env::set_var("HOME", "/tmp/fakehome");
        assert_eq!(expand_home("~/foo"), "/tmp/fakehome/foo");
    }

    #[test]
    fn expand_home_leaves_absolute_path() {
        assert_eq!(expand_home("/var/run/x.sock"), "/var/run/x.sock");
    }
}