use std::path::PathBuf;
use crate::canonical::{CanonicalError, Content, ErrorKind, ReasoningEffort};
use crate::config::partial::OutMode;
use crate::ingress::{dialect_id, IngressId};
use crate::store::Secret;
use super::Flags;
fn usage(message: impl Into<String>) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Usage,
message: message.into(),
provider_detail: None,
retry_after_seconds: 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" => raw_direction(inline.as_deref(), cfg)?,
"--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,
"--list-providers" => flags.list_providers = true,
"--count-tokens" => flags.count_tokens = true,
"--serve" => flags.serve = true,
"--browser" => flags.browser = true,
"--in" => flags.in_dialect = Some(dialect(key, value(key, inline, argv, &mut i)?)?),
"--skill" => flags.skill = true,
"--help" | "-h" => flags.help = true,
"--version" | "-V" => flags.version = true,
"--provider" => cfg.provider = Some(value(key, inline, argv, &mut i)?),
"--model" | "-m" => cfg.model = Some(value(key, inline, argv, &mut i)?),
"--base-url" => cfg.base_url = 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)?)?),
"--reasoning" => {
cfg.reasoning = Some(reasoning(key, value(key, inline, argv, &mut i)?)?)
}
"--timeout" => cfg.timeout = 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
&& !flags.skill
&& u8::from(flags.dump_config)
+ u8::from(flags.list_models)
+ u8::from(flags.list_providers)
+ u8::from(flags.login)
+ u8::from(flags.count_tokens)
+ u8::from(flags.serve)
> 1
{
return Err(usage(
"control operations --login / --list-models / --list-providers / --count-tokens / --dump-config / --serve are mutually exclusive",
));
}
if flags.in_dialect.is_some() && flags.prompt.is_some() {
return Err(usage(
"--in reads one dialect request from stdin and cannot be combined with a positional prompt",
));
}
Ok(flags)
}
fn dialect(key: &str, raw: String) -> Result<IngressId, CanonicalError> {
dialect_id(&raw).ok_or_else(|| {
usage(format!(
"flag `{key}` needs a known ingress dialect (openai_chat, anthropic_messages), got `{raw}`"
))
})
}
fn raw_direction(
dir: Option<&str>,
cfg: &mut crate::config::PartialConfig,
) -> Result<(), CanonicalError> {
match dir {
None | Some("both") => cfg.output = Some(OutMode::Raw),
Some("in") => cfg.raw_in = Some(true),
Some("out") => {
cfg.output = Some(OutMode::Raw);
cfg.raw_in = Some(false);
}
Some(other) => {
return Err(usage(format!(
"flag `--raw` takes no value or `in`/`out`/`both`, got `{other}`"
)))
}
}
Ok(())
}
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}`")))
}
fn reasoning(key: &str, raw: String) -> Result<ReasoningEffort, CanonicalError> {
raw.parse()
.map_err(|()| usage(format!("flag `{key}` needs low|medium|high, got `{raw}`")))
}