use std::path::PathBuf;
use crate::canonical::{CanonicalError, Content, ErrorKind};
use crate::config::partial::OutMode;
use crate::config::{EnvSnapshot, PartialConfig};
use crate::store::Secret;
pub struct Args {
pub argv: Vec<String>,
pub env: EnvSnapshot,
pub tty: bool,
pub stdout_tty: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
pub struct Flags {
pub config: PartialConfig,
pub prompt: Option<String>,
pub files: Vec<PathBuf>,
pub input: Option<PathBuf>,
pub config_path: Option<PathBuf>,
pub dump_config: bool,
pub login: bool,
pub list_models: bool,
pub browser: bool,
pub help: bool,
pub version: bool,
}
pub enum Route {
Login,
ListModels,
Run,
}
pub fn route(argv: &[String]) -> Route {
match parse_args(argv) {
Ok(f) if f.login => Route::Login,
Ok(f) if f.list_models => Route::ListModels,
_ => Route::Run,
}
}
fn usage(message: impl Into<String>) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Usage,
message: message.into(),
provider_detail: None,
}
}
pub fn parse_args(argv: &[String]) -> Result<Flags, CanonicalError> {
let mut flags = Flags::default();
let mut i = 0;
while i < argv.len() {
let arg = &argv[i];
if arg == "--" {
let tail = &argv[i + 1..];
if !tail.is_empty() {
flags.prompt = Some(tail.join(" "));
}
break;
}
if !arg.starts_with('-') || arg == "-" {
flags.prompt = Some(argv[i..].join(" "));
break;
}
let (key, inline) = match arg.split_once('=') {
Some((k, v)) => (k, Some(v.to_owned())),
None => (arg.as_str(), None),
};
let cfg = &mut flags.config;
match key {
"--text" => cfg.output = Some(OutMode::Text),
"--json" => cfg.output = Some(OutMode::Ndjson),
"--raw" => cfg.output = Some(OutMode::Raw),
"--thinking" => cfg.thinking = Some(true),
"--stream" => cfg.stream = Some(true),
"--no-stream" => cfg.stream = Some(false),
"--dump-config" => flags.dump_config = true,
"--login" => flags.login = true,
"--list-models" => flags.list_models = true,
"--browser" => flags.browser = true,
"--help" | "-h" => flags.help = true,
"--version" | "-V" => flags.version = true,
"--provider" => cfg.provider = Some(value(key, inline, argv, &mut i)?),
"--model" => cfg.model = Some(value(key, inline, argv, &mut i)?),
"--api-key" => cfg.api_key = Some(Secret::new(value(key, inline, argv, &mut i)?)),
"--max-tokens" => {
cfg.max_tokens = Some(number(key, value(key, inline, argv, &mut i)?)?)
}
"--temperature" => {
cfg.temperature = Some(number(key, value(key, inline, argv, &mut i)?)?)
}
"--top-p" => cfg.top_p = Some(number(key, value(key, inline, argv, &mut i)?)?),
"--timeout-connect" => {
cfg.timeout_connect = Some(number(key, value(key, inline, argv, &mut i)?)?)
}
"--timeout-response" => {
cfg.timeout_response = Some(number(key, value(key, inline, argv, &mut i)?)?)
}
"--timeout-idle" => {
cfg.timeout_idle = Some(number(key, value(key, inline, argv, &mut i)?)?)
}
"--system" => cfg.system = Some(vec![Content::Text(value(key, inline, argv, &mut i)?)]),
"--file" | "-f" => flags
.files
.push(PathBuf::from(value(key, inline, argv, &mut i)?)),
"--input" => flags.input = Some(PathBuf::from(value(key, inline, argv, &mut i)?)),
"--config" => {
flags.config_path = Some(PathBuf::from(value(key, inline, argv, &mut i)?))
}
_ => return Err(usage(format!("unknown flag `{key}` (try `bz --help`)"))),
}
i += 1;
}
if !flags.help
&& !flags.version
&& u8::from(flags.dump_config) + u8::from(flags.list_models) + u8::from(flags.login) > 1
{
return Err(usage(
"control operations --login / --list-models / --dump-config are mutually exclusive",
));
}
Ok(flags)
}
fn value(
key: &str,
inline: Option<String>,
argv: &[String],
i: &mut usize,
) -> Result<String, CanonicalError> {
if let Some(v) = inline {
return Ok(v);
}
*i += 1;
argv.get(*i)
.cloned()
.ok_or_else(|| usage(format!("flag `{key}` needs a value")))
}
fn number<T: std::str::FromStr>(key: &str, raw: String) -> Result<T, CanonicalError> {
raw.parse()
.map_err(|_| usage(format!("flag `{key}` needs a number, got `{raw}`")))
}