use std::collections::HashMap;
use clap::Subcommand;
use tail_fin_common::TailFinError;
use crate::session::{browser_session, print_json, Ctx};
#[derive(Subcommand)]
pub enum GenAction {
Explore {
url: String,
#[arg(long)]
site: Option<String>,
#[arg(long, default_value_t = 3)]
wait: u64,
},
Cascade { url: String },
Synthesize { site: String },
Generate {
url: String,
#[arg(long)]
goal: Option<String>,
#[arg(long)]
site: Option<String>,
},
List,
}
pub async fn run(action: GenAction, ctx: &Ctx) -> Result<(), TailFinError> {
match action {
GenAction::Explore { url, site, wait } => {
let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
let session = browser_session(chrome_host, ctx.headed).await?;
let result =
tail_fin_gen::explore_url(&session, &url, site.as_deref(), None, wait).await?;
eprintln!("{}", tail_fin_gen::explore::render_explore_summary(&result));
print_json(&result)?;
}
GenAction::Cascade { url } => {
let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
let session = browser_session(chrome_host, ctx.headed).await?;
let parsed = url::Url::parse(&url)
.map_err(|e| TailFinError::Parse(format!("Invalid URL '{}': {}", url, e)))?;
let domain = parsed
.host_str()
.ok_or_else(|| TailFinError::Parse("URL has no host".into()))?;
let origin = format!("{}://{}", parsed.scheme(), domain);
let _ = session.navigate(&origin).await;
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
let result = tail_fin_gen::cascade::cascade_probe(&session, &url).await?;
eprintln!("{}", tail_fin_gen::cascade::render_cascade_result(&result));
print_json(&result)?;
}
GenAction::Synthesize { site } => {
let result = tail_fin_gen::synthesize(&site)?;
eprintln!(
"{}",
tail_fin_gen::synthesize::render_synthesize_summary(&result)
);
print_json(&result)?;
}
GenAction::Generate { url, goal, site } => {
let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
let session = browser_session(chrome_host, ctx.headed).await?;
let result =
tail_fin_gen::generate_cli(&session, &url, goal.as_deref(), site.as_deref())
.await?;
eprintln!(
"{}",
tail_fin_gen::generate::render_generate_summary(&result)
);
print_json(&result)?;
}
GenAction::List => {
let sites = tail_fin_gen::list_dynamic_sites();
if sites.is_empty() {
eprintln!("No generated sites found.");
eprintln!(" Use `tail-fin gen generate <URL>` to create one.");
} else {
for (site, commands) in &sites {
println!("{}:", site);
for cmd in commands {
println!(" - {}", cmd);
}
}
}
}
}
Ok(())
}
pub async fn run_dynamic(
site: String,
command: String,
args: Vec<String>,
ctx: &Ctx,
) -> Result<(), TailFinError> {
let config = tail_fin_gen::load_command_config(&site, &command)?;
let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
let session = browser_session(chrome_host, ctx.headed).await?;
let _ = session
.navigate(&format!("https://{}", config.domain))
.await;
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
let mut extra_args: HashMap<String, String> = HashMap::new();
let mut i = 0;
while i < args.len() {
if let Some(eq_pos) = args[i].find('=') {
let key = args[i][..eq_pos].trim_start_matches('-').to_string();
let val = args[i][eq_pos + 1..].to_string();
extra_args.insert(key, val);
i += 1;
} else if args[i].starts_with("--") && i + 1 < args.len() {
let key = args[i].trim_start_matches('-').to_string();
let val = args[i + 1].clone();
extra_args.insert(key, val);
i += 2;
} else {
eprintln!("Warning: ignoring unparsed arg '{}'", args[i]);
i += 1;
}
}
let result = tail_fin_gen::execute_command(&session, &config, &extra_args).await?;
print_json(&serde_json::json!({
"site": site,
"command": command,
"result": result,
}))?;
Ok(())
}
pub struct Adapter;
impl crate::adapter::CliAdapter for Adapter {
fn name(&self) -> &'static str {
"gen"
}
fn about(&self) -> &'static str {
"Generate CLI adapters for new sites"
}
fn command(&self) -> clap::Command {
<GenAction as clap::Subcommand>::augment_subcommands(
clap::Command::new("gen").about("Generate CLI adapters for new sites"),
)
}
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 = <GenAction as clap::FromArgMatches>::from_arg_matches(matches)
.map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
run(action, ctx).await
})
}
}