use clap::Parser;
use std::io::Write;
use std::path::PathBuf;
const DEFAULT_PORT: u16 = 2112;
#[derive(Parser, Debug)]
#[command(
name = "llmposter",
about = "Mock LLM API server — fixture-driven, deterministic responses for testing"
)]
pub struct Cli {
#[arg(short, long)]
pub fixtures: PathBuf,
#[arg(long)]
pub validate: bool,
#[arg(short, long, default_value_t = DEFAULT_PORT)]
pub port: u16,
#[arg(short, long, default_value = "127.0.0.1")]
pub bind: String,
#[arg(short, long)]
pub verbose: bool,
#[cfg(feature = "watch")]
#[arg(short = 'w', long)]
pub watch: bool,
#[arg(long, default_value_t = 1000)]
pub capture_capacity: usize,
#[arg(long)]
pub diagnostics: bool,
#[cfg(feature = "ui")]
#[arg(long)]
pub ui: bool,
}
pub async fn run(cli: &Cli) -> Result<Option<crate::MockServer>, Box<dyn std::error::Error>> {
run_with_output(cli, &mut std::io::stderr()).await
}
pub async fn run_with_output(
cli: &Cli,
out: &mut (dyn Write + Send),
) -> Result<Option<crate::MockServer>, Box<dyn std::error::Error>> {
if cli.validate {
let fixtures = if cli.fixtures.is_dir() {
crate::fixture::load_yaml_dir(&cli.fixtures)?
} else {
crate::fixture::load_yaml_file(&cli.fixtures)?
};
if fixtures.is_empty() {
return Err("No fixtures found — nothing to validate".into());
}
writeln!(out, "Validated {} fixtures successfully", fixtures.len())?;
return Ok(None);
}
let warn_port_ignored = |out: &mut dyn Write,
bind_port: &dyn std::fmt::Display,
cli_port: u16|
-> std::io::Result<()> {
writeln!(
out,
"Warning: --port {} ignored because --bind already includes port {}",
cli_port, bind_port
)
};
let bind_addr = if let Ok(sa) = cli.bind.parse::<std::net::SocketAddr>() {
if cli.port != DEFAULT_PORT {
warn_port_ignored(out, &sa.port(), cli.port)?;
}
cli.bind.clone()
} else if let Ok(ip) = cli.bind.parse::<std::net::IpAddr>() {
match ip {
std::net::IpAddr::V6(_) => format!("[{}]:{}", cli.bind, cli.port),
std::net::IpAddr::V4(_) => format!("{}:{}", cli.bind, cli.port),
}
} else if let Some((host, port_str)) = cli.bind.rsplit_once(':') {
if !host.is_empty() && port_str.parse::<u16>().is_ok() {
if cli.port != DEFAULT_PORT {
warn_port_ignored(out, &port_str, cli.port)?;
}
cli.bind.clone()
} else {
format!("{}:{}", cli.bind, cli.port)
}
} else {
format!("{}:{}", cli.bind, cli.port)
};
let mut builder = crate::ServerBuilder::new();
builder = if cli.fixtures.is_dir() {
builder.load_yaml_dir(&cli.fixtures)?
} else {
builder.load_yaml(&cli.fixtures)?
};
if builder.fixture_count() == 0 {
writeln!(
out,
"Warning: no fixtures loaded from {}",
cli.fixtures.display()
)?;
}
#[cfg(feature = "watch")]
{
builder = builder.watch(cli.watch);
}
#[cfg(feature = "ui")]
{
builder = builder.ui(cli.ui);
}
let server = builder
.bind(&bind_addr)
.verbose(cli.verbose)
.capture_capacity(cli.capture_capacity)
.diagnostics(cli.diagnostics)
.build()
.await?;
writeln!(out, "llmposter listening on {}", server.url())?;
#[cfg(feature = "ui")]
if cli.ui {
writeln!(out, "Debug UI at {}/ui", server.url())?;
}
#[cfg(feature = "watch")]
if cli.watch {
writeln!(out, "Watching {} for changes", cli.fixtures.display())?;
}
#[cfg(unix)]
writeln!(
out,
"Send SIGHUP (kill -HUP {}) to reload fixtures",
std::process::id()
)?;
writeln!(out, "Press Ctrl+C to stop")?;
Ok(Some(server))
}