tail-fin-daemon 0.6.4

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::Subcommand;
use night_fury_daemon_core::cli::make_req;
use night_fury_daemon_core::{client, spawn};
use serde_json::json;

use super::{acquire_then_exec, ExecArgs};

#[derive(Subcommand)]
pub enum SaCmd {
    /// Income statement (annual|quarterly)
    IncomeStatement {
        ticker: String,
        #[arg(long, default_value = "annual")]
        period: String,
    },
    /// Balance sheet
    BalanceSheet {
        ticker: String,
        #[arg(long, default_value = "annual")]
        period: String,
    },
    /// Cash flow statement
    CashFlow {
        ticker: String,
        #[arg(long, default_value = "annual")]
        period: String,
    },
    /// Live quote
    Quote { ticker: String },
    /// News articles
    News {
        ticker: String,
        #[arg(long, default_value_t = 20)]
        count: u64,
        #[arg(long)]
        since: Option<i64>,
        #[arg(long)]
        until: Option<i64>,
    },
    /// Analysis articles
    Analysis {
        ticker: String,
        #[arg(long, default_value_t = 20)]
        count: u64,
        #[arg(long)]
        since: Option<i64>,
        #[arg(long)]
        until: Option<i64>,
    },
    /// Full article content
    Article { id: String },
}

pub async fn run(
    cmd: SaCmd,
    socket: &str,
    host: &str,
    session: Option<String>,
) -> anyhow::Result<()> {
    let (cmd_name, params) = match cmd {
        SaCmd::IncomeStatement { ticker, period } => (
            "sa.income-statement",
            json!({"ticker": ticker, "period": period}),
        ),
        SaCmd::BalanceSheet { ticker, period } => (
            "sa.balance-sheet",
            json!({"ticker": ticker, "period": period}),
        ),
        SaCmd::CashFlow { ticker, period } => {
            ("sa.cash-flow", json!({"ticker": ticker, "period": period}))
        }
        SaCmd::Quote { ticker } => ("sa.quote", json!({"ticker": ticker})),
        SaCmd::News {
            ticker,
            count,
            since,
            until,
        } => (
            "sa.news",
            json!({"ticker": ticker, "count": count, "since": since, "until": until}),
        ),
        SaCmd::Analysis {
            ticker,
            count,
            since,
            until,
        } => (
            "sa.analysis",
            json!({"ticker": ticker, "count": count, "since": since, "until": until}),
        ),
        SaCmd::Article { id } => ("sa.article", json!({"id": id})),
    };

    if let Some(sid) = session {
        // Explicit session — no acquire/release wrapping.
        spawn::ensure_daemon(socket).await?;
        let req = make_req(cmd_name, Some(&sid), params);
        let resp = client::send(socket, &req).await?;
        super::print_and_check(&resp);
        return Ok(());
    }

    let args = ExecArgs {
        site: "sa",
        host,
        cmd: cmd_name,
        params,
    };
    acquire_then_exec(socket, args).await
}