use night_fury_cli_core::repl::{CommandHandler, ReplEngine};
use tail_fin_common::TailFinError;
use crate::adapter::CliAdapter;
use crate::session::Ctx;
fn build_repl_command(adapters: &[Box<dyn CliAdapter>]) -> clap::Command {
let mut cmd = clap::Command::new("tail-fin")
.about("Multi-site browser automation CLI (REPL)")
.no_binary_name(true)
.subcommand_required(true);
#[cfg(feature = "gen")]
{
cmd = cmd.subcommand(
clap::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
}
pub struct ReplHandler {
pub ctx: Ctx,
pub adapters: Vec<Box<dyn CliAdapter>>,
}
impl CommandHandler for ReplHandler {
async fn dispatch(&mut self, line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() {
return false;
}
if matches!(trimmed, "quit" | "exit") {
return true;
}
let argv: Vec<String> = trimmed.split_whitespace().map(String::from).collect();
let cmd = build_repl_command(&self.adapters);
let matches = match cmd.try_get_matches_from(&argv) {
Ok(m) => m,
Err(e) => {
let _ = e.print();
return false;
}
};
let Some((subcommand, sub_matches)) = matches.subcommand() else {
eprintln!("no subcommand (type `--help` for commands, `quit` to exit)");
return false;
};
#[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();
if let Err(e) = crate::cli::gen::run_dynamic(site, command, args, &self.ctx).await {
eprintln!("Error: {e}");
}
return false;
}
for adapter in &self.adapters {
if adapter.name() == subcommand {
if let Err(e) = adapter.dispatch(sub_matches, &self.ctx).await {
eprintln!("Error: {e}");
}
return false;
}
}
eprintln!("unknown command '{subcommand}'");
false
}
}
pub async fn run_repl(ctx: Ctx, adapters: Vec<Box<dyn CliAdapter>>) -> Result<(), TailFinError> {
let mut handler = ReplHandler { ctx, adapters };
let engine = ReplEngine::new("tail-fin> ");
engine
.run(&mut handler)
.await
.map_err(|e| TailFinError::Api(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_handler() -> ReplHandler {
ReplHandler {
ctx: Ctx {
connect: None,
cookies: None,
headed: false,
},
adapters: Vec::new(),
}
}
#[tokio::test]
async fn quit_returns_true() {
let mut h = empty_handler();
assert!(h.dispatch("quit").await);
}
#[tokio::test]
async fn exit_returns_true() {
let mut h = empty_handler();
assert!(h.dispatch("exit").await);
}
#[tokio::test]
async fn empty_line_returns_false() {
let mut h = empty_handler();
assert!(!h.dispatch("").await);
assert!(!h.dispatch(" ").await);
}
#[tokio::test]
async fn unknown_command_returns_false() {
let mut h = empty_handler();
assert!(!h.dispatch("bogus-nonexistent-cmd").await);
}
#[tokio::test]
async fn invalid_arg_returns_false() {
let mut h = empty_handler();
assert!(!h.dispatch("--no-such-flag").await);
}
}