use std::ffi::OsString;
use clap::{Args, Subcommand};
use crate::clients::ClientKind;
#[must_use]
pub fn protect_client_arguments(arguments: Vec<OsString>, nested: bool) -> Vec<OsString> {
let start = if nested {
arguments
.iter()
.position(|argument| argument == "with")
.map_or(arguments.len(), |position| position + 1)
} else {
1
};
let options = wrapper_options();
let clients: Vec<&str> = ClientKind::ALL
.iter()
.flat_map(|kind| [kind.canonical_name(), kind.legacy_name()])
.collect();
let mut position = start;
while position < arguments.len() {
let value = arguments[position].to_string_lossy().into_owned();
if value == "--" {
position += 1;
continue;
}
if value.starts_with('-') {
let name = value
.split_once('=')
.map_or(value.as_str(), |(name, _)| name);
let takes_a_value = !value.contains('=') && options.contains(&(name.to_string(), true));
position += if takes_a_value { 2 } else { 1 };
continue;
}
if !clients.contains(&value.as_str()) {
position += 1;
continue;
}
let mut normalized = arguments[..=position].to_vec();
let forwarded = &arguments[position + 1..];
if forwarded.is_empty() {
return normalized;
}
let explicit = forwarded.first().is_some_and(|argument| argument == "--");
let forwarded = if explicit { &forwarded[1..] } else { forwarded };
normalized.push("--".into());
normalized.extend(forwarded.iter().cloned());
return normalized;
}
arguments
}
fn wrapper_options() -> std::collections::HashSet<(String, bool)> {
use clap::CommandFactory as _;
let mut options = std::collections::HashSet::new();
let mut collect = |command: &clap::Command| {
for argument in command.get_arguments() {
let takes_a_value = argument.get_num_args().map_or_else(
|| {
!matches!(
argument.get_action(),
clap::ArgAction::SetTrue
| clap::ArgAction::SetFalse
| clap::ArgAction::Count
| clap::ArgAction::Help
| clap::ArgAction::Version
)
},
|range| range.takes_values(),
);
if let Some(long) = argument.get_long() {
options.insert((format!("--{long}"), takes_a_value));
}
for alias in argument.get_all_aliases().unwrap_or_default() {
options.insert((format!("--{alias}"), takes_a_value));
}
if let Some(short) = argument.get_short() {
options.insert((format!("-{short}"), takes_a_value));
}
}
};
let root = crate::cli::Cli::command();
collect(&root);
if let Some(with) = root.find_subcommand("with") {
collect(with);
}
options
}
#[derive(Clone, Debug, Args)]
#[command(trailing_var_arg = true)]
pub struct WithArgs {
#[arg(long)]
pub global: bool,
#[arg(long, requires = "global")]
pub undo: bool,
#[arg(long, conflicts_with = "interactive")]
pub non_interactive: bool,
#[arg(long, conflicts_with = "non_interactive")]
pub interactive: bool,
#[arg(long)]
pub extend_global_config: bool,
#[arg(long, conflicts_with = "extend_global_config")]
pub isolated_config: bool,
#[arg(long, conflicts_with = "server")]
pub managed: bool,
#[arg(long)]
pub server: Option<String>,
#[arg(long, conflicts_with_all = ["server", "managed"])]
pub local: bool,
#[arg(long, hide_env_values = true, conflicts_with = "token_stdin")]
pub token: Option<String>,
#[arg(long, conflicts_with = "token")]
pub token_stdin: bool,
#[arg(long)]
pub model: Option<String>,
#[arg(long, conflicts_with = "model")]
pub pick_model: bool,
#[arg(long)]
pub label: Option<String>,
#[arg(long, alias = "ttl-hours", default_value_t = 24 * 7)]
pub run_ttl_hours: i64,
#[arg(long)]
pub fixed_run_ttl: bool,
#[arg(long)]
pub run_max_requests: Option<u64>,
#[arg(value_enum)]
pub client: ClientKind,
#[arg(value_name = "CLIENT_ARGS", allow_hyphen_values = true)]
pub client_args: Vec<OsString>,
}
#[derive(Debug, Subcommand)]
pub enum ServerOp {
Use {
server: Option<String>,
#[arg(long, hide_env_values = true, conflicts_with = "token_stdin")]
token: Option<String>,
#[arg(long, conflicts_with = "token")]
token_stdin: bool,
#[arg(long)]
clear: bool,
#[arg(long)]
run_max_requests: Option<u64>,
},
Status,
Start,
Claim,
Stop,
Remove {
#[arg(long)]
yes: bool,
},
#[command(hide = true)]
Reap { pid: u32 },
}
impl WithArgs {
#[must_use]
pub fn as_configure(&self) -> crate::cli::ConfigureArgs {
crate::cli::ConfigureArgs {
client: Some(self.client),
all: false,
undo: self.undo,
target: crate::cli::AuthTarget {
local: self.local,
server: self.server.clone(),
managed: self.managed,
},
token: self.token.clone(),
token_stdin: self.token_stdin,
ttl_hours: 8760,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn split(arguments: &[&str], nested: bool) -> Vec<String> {
protect_client_arguments(arguments.iter().map(OsString::from).collect(), nested)
.iter()
.map(|value| value.to_string_lossy().into_owned())
.collect()
}
#[test]
fn everything_after_the_client_name_reaches_the_client() {
for option in [
"--global",
"--undo",
"--non-interactive",
"--interactive",
"--token-stdin",
"--model",
"--server",
"--managed",
"--isolated-config",
"--port",
] {
let split = split(&["router", "with", "codex", option, "value"], true);
assert_eq!(
split,
["router", "with", "codex", "--", option, "value"],
"{option} after the client name must reach the client"
);
}
}
#[test]
fn a_client_model_reaches_the_client_and_a_router_model_does_not() {
let split = split(
&["with-router", "--model", "A", "qwen", "--model", "B"],
false,
);
let boundary = split.iter().position(|value| value == "--").expect("--");
assert!(split[..boundary].windows(2).any(|p| p == ["--model", "A"]));
assert!(
split[boundary + 1..]
.windows(2)
.any(|p| p == ["--model", "B"])
);
}
#[test]
fn an_option_value_that_names_a_client_is_not_the_boundary() {
for option in ["--model", "--data-dir", "--upstream-provider"] {
let split = split(&["with-router", option, "codex", "qwen", "hello"], false);
assert_eq!(
split,
["with-router", option, "codex", "qwen", "--", "hello"],
"{option}'s value must not be read as the client name"
);
}
}
#[test]
fn an_explicit_boundary_is_not_doubled() {
let split = split(&["with-router", "codex", "--", "--global", "hi"], false);
assert_eq!(split, ["with-router", "codex", "--", "--global", "hi"]);
assert_eq!(split.iter().filter(|value| *value == "--").count(), 1);
}
#[test]
fn a_bare_client_is_left_alone() {
assert_eq!(
split(&["with-router", "codex"], false),
["with-router", "codex"]
);
}
#[test]
fn the_option_table_is_read_from_the_parser() {
let options = wrapper_options();
assert!(
options.contains(&("--model".to_string(), true)),
"--model takes a value"
);
assert!(
options.contains(&("--global".to_string(), false)),
"--global does not"
);
assert!(
options.iter().any(|(name, _)| name == "--isolated-config"),
"an option missing from a hand-kept list is the defect this prevents"
);
}
}