tail-fin-cli 0.5.1

Multi-site browser automation CLI — attaches to Chrome or auto-launches a stealth browser to drive 15+ sites
use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{browser_session, launch_stealth_session, print_json, print_list, Ctx};

#[derive(Subcommand)]
pub enum CoupangAction {
    /// Search for products
    Search {
        /// Search query
        query: String,
        #[arg(long, default_value_t = 20)]
        limit: usize,
        #[arg(long, default_value_t = 1)]
        page: usize,
        #[arg(long)]
        sort: Option<String>,
        #[arg(long)]
        rocket_only: bool,
        #[arg(long)]
        region: Option<String>,
    },
    /// View product details
    Detail {
        /// Product ID
        product_id: String,
        #[arg(long)]
        region: Option<String>,
    },
    /// View shopping cart
    Cart {
        #[arg(long)]
        region: Option<String>,
    },
    /// Add a product to cart (browser mode only)
    AddToCart {
        /// Product ID
        product_id: String,
        #[arg(long)]
        region: Option<String>,
    },
}

/// Get or launch a browser session for Coupang.
///
/// Priority: --connect (existing Chrome) > --cookies (HTTP mode) > auto-launch stealth.
async fn coupang_session(
    ctx: &Ctx,
    region: Option<&str>,
) -> Result<night_fury_core::BrowserSession, TailFinError> {
    let start_url = tail_fin_coupang::base_url_for_region(region);
    if let Some(ref host) = ctx.connect {
        browser_session(host, ctx.headed).await
    } else {
        // Auto-launch stealth Chrome — no --connect needed
        eprintln!("Launching stealth browser for Coupang...");
        launch_stealth_session(&start_url, ctx.headed).await
    }
}

pub async fn run(action: CoupangAction, ctx: &Ctx) -> Result<(), TailFinError> {
    match action {
        CoupangAction::Search {
            query,
            limit,
            page,
            sort,
            rocket_only,
            region,
        } => {
            // Cookie mode: HTTP search
            if let Some(ref cookies_flag) = ctx.cookies {
                let path = if cookies_flag == "auto" {
                    crate::session::default_cookies_path("coupang")
                } else {
                    std::path::PathBuf::from(cookies_flag)
                };
                let client = tail_fin_coupang::CoupangHttpClient::from_cookie_file(
                    &path,
                    region.as_deref(),
                )?;
                let products = client
                    .search(&query, limit, page, sort.as_deref(), rocket_only)
                    .await?;
                print_list("products", &products, products.len())?;
            } else {
                // Browser mode (connect or auto-launch)
                let session = coupang_session(ctx, region.as_deref()).await?;
                let client = tail_fin_coupang::CoupangClient::new(session);
                let products = client
                    .search(&query, limit, page, sort.as_deref(), rocket_only)
                    .await?;
                print_list("products", &products, products.len())?;
            }
        }
        CoupangAction::Detail { product_id, region } => {
            let session = coupang_session(ctx, region.as_deref()).await?;
            let client = tail_fin_coupang::CoupangClient::new(session);
            let detail = client.detail(&product_id).await?;
            print_json(&detail)?;
        }
        CoupangAction::Cart { region } => {
            let session = coupang_session(ctx, region.as_deref()).await?;
            let client = tail_fin_coupang::CoupangClient::new(session);
            let items = client.cart().await?;
            print_list("cart_items", &items, items.len())?;
        }
        CoupangAction::AddToCart { product_id, region } => {
            let session = coupang_session(ctx, region.as_deref()).await?;
            let client = tail_fin_coupang::CoupangClient::new(session);
            let result = client.add_to_cart(&product_id).await?;
            print_json(&result)?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "coupang"
    }

    fn about(&self) -> &'static str {
        "Coupang e-commerce operations"
    }

    fn command(&self) -> clap::Command {
        <CoupangAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("coupang").about("Coupang e-commerce operations"),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <CoupangAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}