mod adapter;
mod cli;
mod repl;
mod session;
use clap::Command;
use session::Ctx;
use tail_fin_common::TailFinError;
fn build_command(adapters: &[Box<dyn adapter::CliAdapter>]) -> Command {
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),
)
.arg(
clap::Arg::new("repl")
.long("repl")
.help("Start an interactive REPL (default when no subcommand given)")
.action(clap::ArgAction::SetTrue),
);
#[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
}
#[tokio::main]
async fn main() -> Result<(), TailFinError> {
let adapters = cli::adapters();
let cmd = build_command(&adapters);
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");
let repl_flag = matches.get_flag("repl");
if connect.is_some() && cookies.is_some() {
return Err(TailFinError::Api(
"--connect and --cookies cannot be used together. Use --connect for browser mode, or --cookies for cookie mode.".into(),
));
}
let ctx = Ctx {
connect,
cookies,
headed,
};
if repl_flag && matches.subcommand().is_some() {
return Err(TailFinError::Api(
"--repl cannot be combined with a subcommand".into(),
));
}
if repl_flag || matches.subcommand().is_none() {
return repl::run_repl(ctx, adapters).await;
}
let (subcommand, sub_matches) = matches
.subcommand()
.expect("subcommand presence checked above");
#[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
)))
}