mod adapter;
mod cli;
mod session;
use clap::Command;
use session::Ctx;
use tail_fin_common::TailFinError;
fn build_cli() -> (Command, Vec<Box<dyn adapter::CliAdapter>>) {
let adapters = cli::adapters();
let mut cmd = Command::new("tail-fin")
.about("Multi-site browser automation CLI")
.arg(
clap::Arg::new("connect")
.long("connect")
.value_name("HOST:PORT")
.help("Connect to Chrome browser at <HOST:PORT> (e.g. 127.0.0.1:9222)")
.global(true),
)
.arg(
clap::Arg::new("cookies")
.long("cookies")
.value_name("PATH")
.help("Use saved cookies instead of browser")
.num_args(0..=1)
.default_missing_value("auto")
.require_equals(true)
.global(true),
)
.arg(
clap::Arg::new("headed")
.long("headed")
.help("Run browser in headed mode")
.action(clap::ArgAction::SetTrue)
.global(true),
);
#[cfg(feature = "gen")]
{
cmd = cmd.subcommand(
Command::new("run")
.about("Run a dynamically generated site command")
.arg(clap::Arg::new("site").required(true).help("Site name"))
.arg(
clap::Arg::new("command")
.required(true)
.help("Command name"),
)
.arg(
clap::Arg::new("args")
.num_args(..)
.trailing_var_arg(true)
.help("Extra arguments"),
),
);
}
for adapter in &adapters {
cmd = cmd.subcommand(adapter.command());
}
(cmd, adapters)
}
#[tokio::main]
async fn main() -> Result<(), TailFinError> {
let (cmd, adapters) = build_cli();
let matches = cmd.get_matches();
let connect = matches.get_one::<String>("connect").cloned();
let cookies = matches.get_one::<String>("cookies").cloned();
let headed = matches.get_flag("headed");
if connect.is_some() && 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,
cookies,
headed,
};
let (subcommand, sub_matches) = matches.subcommand().ok_or_else(|| {
TailFinError::Api("No command provided. Use --help to see available commands.".into())
})?;
#[cfg(feature = "gen")]
if subcommand == "run" {
let site = sub_matches
.get_one::<String>("site")
.cloned()
.unwrap_or_default();
let command = sub_matches
.get_one::<String>("command")
.cloned()
.unwrap_or_default();
let args: Vec<String> = sub_matches
.get_many::<String>("args")
.map(|v| v.cloned().collect())
.unwrap_or_default();
return cli::gen::run_dynamic(site, command, args, &ctx).await;
}
for adapter in &adapters {
if adapter.name() == subcommand {
return adapter.dispatch(sub_matches, &ctx).await;
}
}
Err(TailFinError::Api(format!(
"Unknown command '{}'. Use --help to see available commands.",
subcommand
)))
}