tail-fin-cli 0.1.0

Multi-site browser automation CLI — Twitter, YouTube, Instagram, SeekingAlpha, and more
mod cli;
mod session;

use clap::{Parser, Subcommand};
use session::Ctx;
use tail_fin_common::TailFinError;

#[derive(Parser)]
#[command(name = "tail-fin", about = "Multi-site browser automation CLI")]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    /// Connect to Chrome browser at <HOST:PORT> (e.g. 127.0.0.1:9222)
    #[arg(long, global = true, value_name = "HOST:PORT")]
    connect: Option<String>,

    /// Use saved cookies instead of browser.
    /// Without a path: reads from ~/.tail-fin/<site>-cookies.txt
    /// With a path: --cookies=/path/to/file
    #[arg(long, global = true, value_name = "PATH", num_args = 0..=1, default_missing_value = "auto", require_equals = true)]
    cookies: Option<String>,

    /// Run browser in headed mode
    #[arg(long, global = true, default_value_t = false)]
    headed: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Twitter/X operations
    Twitter {
        #[command(subcommand)]
        action: cli::twitter::TwitterAction,
    },
    /// Grok AI operations
    Grok {
        #[command(subcommand)]
        action: cli::grok::GrokAction,
    },
    /// Xiaohongshu operations
    Xhs {
        #[command(subcommand)]
        action: cli::xhs::XhsAction,
    },
    /// Instagram operations
    Instagram {
        #[command(subcommand)]
        action: cli::instagram::InstagramAction,
    },
    /// YouTube operations
    Youtube {
        #[command(subcommand)]
        action: cli::youtube::YoutubeAction,
    },
    /// 591 rental platform operations
    #[command(name = "591")]
    Rent591 {
        #[command(subcommand)]
        action: cli::s591::Rent591Action,
    },
    /// SeekingAlpha financial data
    Sa {
        #[command(subcommand)]
        action: cli::sa::SeekingAlphaAction,
    },
    /// Generate CLI adapters for new sites
    Gen {
        #[command(subcommand)]
        action: cli::gen::GenAction,
    },
    /// Run a dynamically generated site command
    Run {
        /// Site name
        site: String,
        /// Command name
        command: String,
        /// Extra arguments (key=value or --key value)
        #[arg(trailing_var_arg = true)]
        args: Vec<String>,
    },
}

#[tokio::main]
async fn main() -> Result<(), TailFinError> {
    let cli = Cli::parse();

    // Validate: --connect and --cookies are mutually exclusive
    if cli.connect.is_some() && cli.cookies.is_some() {
        eprintln!("Error: --connect and --cookies cannot be used together.");
        eprintln!("  Use --connect for browser mode, or --cookies for cookie mode.");
        std::process::exit(1);
    }

    let ctx = Ctx {
        connect: cli.connect,
        cookies: cli.cookies,
        headed: cli.headed,
    };

    match cli.command {
        Commands::Twitter { action } => cli::twitter::run(action, &ctx).await?,
        Commands::Grok { action } => cli::grok::run(action, &ctx).await?,
        Commands::Xhs { action } => cli::xhs::run(action, &ctx).await?,
        Commands::Instagram { action } => cli::instagram::run(action, &ctx).await?,
        Commands::Youtube { action } => cli::youtube::run(action, &ctx).await?,
        Commands::Rent591 { action } => cli::s591::run(action, &ctx).await?,
        Commands::Sa { action } => cli::sa::run(action, &ctx).await?,
        Commands::Gen { action } => cli::gen::run(action, &ctx).await?,
        Commands::Run { site, command, args } => {
            cli::gen::run_dynamic(site, command, args, &ctx).await?
        }
    }

    Ok(())
}