tail-fin-cli 0.1.0

Multi-site browser automation CLI — Twitter, YouTube, Instagram, SeekingAlpha, and more
use std::path::PathBuf;

use tail_fin_common::TailFinError;

pub struct Ctx {
    pub connect: Option<String>,
    pub cookies: Option<String>,
    pub headed: bool,
}

/// Default cookies path for a given site.
pub fn default_cookies_path(site: &str) -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home)
        .join(".tail-fin")
        .join(format!("{}-cookies.txt", site))
}

/// Resolve the cookies file path from the --cookies flag value.
pub fn resolve_cookies_path(cookies_flag: &str, site: &str) -> PathBuf {
    if cookies_flag == "auto" {
        default_cookies_path(site)
    } else {
        PathBuf::from(cookies_flag)
    }
}

/// Helper: get TwitterHttpClient from cookies flag.
pub fn twitter_http_client(
    cookies_flag: &str,
) -> Result<tail_fin_twitter::TwitterHttpClient, TailFinError> {
    let path = resolve_cookies_path(cookies_flag, "twitter");
    if !path.exists() {
        eprintln!("Error: Cookie file not found: {}", path.display());
        eprintln!(
            "  Run `tail-fin --connect twitter export-cookies` to export cookies from Chrome."
        );
        std::process::exit(1);
    }
    Ok(tail_fin_twitter::TwitterHttpClient::from_cookie_file(
        &path,
    )?)
}

/// Helper: get BrowserSession from connect flag.
pub async fn browser_session(
    host: &str,
    headed: bool,
) -> Result<night_fury_core::BrowserSession, TailFinError> {
    Ok(night_fury_core::BrowserSession::builder()
        .connect_to(format!("ws://{}", host))
        .headed(headed)
        .build()
        .await?)
}

/// Helper: launch a fresh headless browser (no external Chrome needed).
pub async fn launch_browser(
    headed: bool,
) -> Result<night_fury_core::BrowserSession, TailFinError> {
    Ok(night_fury_core::BrowserSession::builder()
        .headed(headed)
        .build()
        .await?)
}

/// Helper: require browser mode, print error if not available.
pub fn require_browser(connect: &Option<String>, service: &str, action_name: &str) -> String {
    match connect {
        Some(host) => host.clone(),
        None => {
            eprintln!(
                "Error: `{} {}` requires browser mode (--connect).",
                service, action_name
            );
            eprintln!(
                "  Use: tail-fin --connect 127.0.0.1:9222 {} {} ...",
                service, action_name
            );
            std::process::exit(1);
        }
    }
}

/// Browser-only adapter: reject cookies, require --connect, return BrowserSession.
pub async fn require_browser_session(
    ctx: &Ctx,
    service: &str,
) -> Result<night_fury_core::BrowserSession, TailFinError> {
    if ctx.cookies.is_some() {
        eprintln!("Error: {} cookie mode is not supported.", service);
        eprintln!("  Use --connect for browser mode.");
        std::process::exit(1);
    }
    let host = match ctx.connect.as_deref() {
        Some(h) => h,
        None => {
            eprintln!("Error: {} requires --connect.", service);
            eprintln!(
                "  Example: tail-fin --connect 127.0.0.1:9222 {} ...",
                service
            );
            std::process::exit(1);
        }
    };
    browser_session(host, ctx.headed).await
}

/// Print error for no mode specified.
pub fn no_mode_error(service: &str, cmd: &str) -> ! {
    eprintln!("Error: No connection mode specified for {}.", service);
    eprintln!("  Use --connect to use browser mode:");
    eprintln!("    tail-fin --connect 127.0.0.1:9222 {} {}", service, cmd);
    eprintln!("  Or --cookies to use saved cookies:");
    eprintln!("    tail-fin --cookies {} {}", service, cmd);
    std::process::exit(1);
}

/// Print a serializable value as pretty JSON.
pub fn print_json(value: &(impl serde::Serialize + ?Sized)) -> Result<(), TailFinError> {
    println!("{}", serde_json::to_string_pretty(value)?);
    Ok(())
}

/// Print a list result as `{ "key": items, "count": N }`.
pub fn print_list(key: &str, items: &impl serde::Serialize, count: usize) -> Result<(), TailFinError> {
    println!(
        "{}",
        serde_json::to_string_pretty(&serde_json::json!({
            key: items,
            "count": count,
        }))?
    );
    Ok(())
}