link_assistant_router/cli/
with.rs1use std::ffi::OsString;
4
5use clap::{Args, Subcommand};
6
7use crate::clients::ClientKind;
8
9#[must_use]
15pub fn protect_client_arguments(arguments: Vec<OsString>, nested: bool) -> Vec<OsString> {
16 let start = if nested {
17 arguments
18 .iter()
19 .position(|argument| argument == "with")
20 .map_or(arguments.len(), |position| position + 1)
21 } else {
22 1
23 };
24 let value_options = [
25 "--server",
26 "--token",
27 "--model",
28 "--run-ttl-hours",
29 "--run-max-requests",
30 ];
31 let clients: Vec<&str> = crate::clients::ClientKind::ALL
35 .iter()
36 .flat_map(|kind| [kind.canonical_name(), kind.legacy_name()])
37 .collect();
38 let boolean_options = [
39 "--global",
40 "--undo",
41 "--non-interactive",
42 "--interactive",
43 "--token-stdin",
44 ];
45 let mut position = start;
46 while position < arguments.len() {
47 let value = arguments[position].to_string_lossy();
48 if value_options.contains(&value.as_ref()) {
49 position += 2;
50 continue;
51 }
52 if value_options
53 .iter()
54 .any(|option| value.starts_with(&format!("{option}=")))
55 {
56 position += 1;
57 continue;
58 }
59 if clients.contains(&value.as_ref()) {
60 let client = arguments[position].clone();
61 let prefix = arguments[..position].to_vec();
62 let mut wrapper = Vec::new();
63 let mut forwarded = Vec::new();
64 let mut cursor = position + 1;
65 let mut explicit_boundary = false;
66 while cursor < arguments.len() {
67 let item = arguments[cursor].to_string_lossy();
68 if explicit_boundary {
69 forwarded.push(arguments[cursor].clone());
70 cursor += 1;
71 continue;
72 }
73 if item == "--" {
74 explicit_boundary = true;
75 cursor += 1;
76 continue;
77 }
78 if boolean_options.contains(&item.as_ref()) {
79 wrapper.push(arguments[cursor].clone());
80 cursor += 1;
81 continue;
82 }
83 if value_options.contains(&item.as_ref()) {
84 wrapper.push(arguments[cursor].clone());
85 if let Some(value) = arguments.get(cursor + 1) {
86 wrapper.push(value.clone());
87 cursor += 2;
88 } else {
89 cursor += 1;
90 }
91 continue;
92 }
93 if value_options
94 .iter()
95 .any(|option| item.starts_with(&format!("{option}=")))
96 {
97 wrapper.push(arguments[cursor].clone());
98 cursor += 1;
99 continue;
100 }
101 forwarded.push(arguments[cursor].clone());
102 cursor += 1;
103 }
104 let mut normalized = prefix;
105 normalized.extend(wrapper);
106 normalized.push(client);
107 if !forwarded.is_empty() {
108 normalized.push("--".into());
109 normalized.extend(forwarded);
110 }
111 return normalized;
112 }
113 position += 1;
114 }
115 arguments
116}
117
118#[derive(Clone, Debug, Args)]
120#[command(trailing_var_arg = true)]
121pub struct WithArgs {
122 #[arg(long)]
124 pub global: bool,
125 #[arg(long, requires = "global")]
127 pub undo: bool,
128 #[arg(long, conflicts_with = "interactive")]
130 pub non_interactive: bool,
131 #[arg(long, conflicts_with = "non_interactive")]
133 pub interactive: bool,
134 #[arg(long)]
136 pub server: Option<String>,
137 #[arg(long, hide_env_values = true, conflicts_with = "token_stdin")]
139 pub token: Option<String>,
140 #[arg(long, conflicts_with = "token")]
142 pub token_stdin: bool,
143 #[arg(long)]
145 pub model: Option<String>,
146 #[arg(long, default_value_t = 1)]
148 pub run_ttl_hours: i64,
149 #[arg(long)]
151 pub run_max_requests: Option<u64>,
152 #[arg(value_enum)]
154 pub client: ClientKind,
155 #[arg(value_name = "CLIENT_ARGS", allow_hyphen_values = true)]
157 pub client_args: Vec<OsString>,
158}
159
160#[derive(Debug, Subcommand)]
162pub enum ServerOp {
163 Use {
165 server: Option<String>,
167 #[arg(long, hide_env_values = true, conflicts_with = "token_stdin")]
169 token: Option<String>,
170 #[arg(long, conflicts_with = "token")]
172 token_stdin: bool,
173 #[arg(long)]
175 clear: bool,
176 #[arg(long)]
178 run_max_requests: Option<u64>,
179 },
180 Status,
182 Start,
184 Claim,
186 Stop,
188 Remove {
190 #[arg(long)]
192 yes: bool,
193 },
194 #[command(hide = true)]
196 Reap { pid: u32 },
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn wrapper_flags_after_client_are_protected() {
205 for option in [
206 "--global",
207 "--undo",
208 "--non-interactive",
209 "--interactive",
210 "--token-stdin",
211 ] {
212 let arguments = ["router", "with", "codex", option, "prompt"]
213 .into_iter()
214 .map(OsString::from)
215 .collect();
216 assert_eq!(
217 protect_client_arguments(arguments, true),
218 ["router", "with", option, "codex", "--", "prompt"].map(OsString::from),
219 "{option} after the client must remain wrapper-owned"
220 );
221 }
222 }
223
224 #[test]
225 fn value_wrapper_flags_after_client_are_accepted() {
226 for (option, value) in [
227 ("--server", "https://router.test"),
228 ("--token", "test-token"),
229 ("--model", "gpt-test"),
230 ("--run-ttl-hours", "2"),
231 ("--run-max-requests", "3"),
232 ] {
233 let arguments = ["with-router", "codex", option, value, "hi"]
234 .into_iter()
235 .map(OsString::from)
236 .collect();
237 assert_eq!(
238 protect_client_arguments(arguments, false),
239 ["with-router", option, value, "codex", "--", "hi"].map(OsString::from),
240 "{option} VALUE after the client must remain wrapper-owned"
241 );
242
243 let equals = format!("{option}={value}");
244 let arguments = ["with-router", "codex", &equals, "hi"]
245 .into_iter()
246 .map(OsString::from)
247 .collect();
248 assert_eq!(
249 protect_client_arguments(arguments, false),
250 [
251 OsString::from("with-router"),
252 OsString::from(&equals),
253 OsString::from("codex"),
254 OsString::from("--"),
255 OsString::from("hi"),
256 ],
257 "{option}=VALUE after the client must remain wrapper-owned"
258 );
259 }
260 }
261
262 #[test]
263 fn explicit_boundary_forwards_every_colliding_wrapper_flag_verbatim() {
264 for option in [
265 "--global",
266 "--undo",
267 "--non-interactive",
268 "--interactive",
269 "--token-stdin",
270 "--server",
271 "--token",
272 "--model",
273 "--run-ttl-hours",
274 "--run-max-requests",
275 ] {
276 let arguments = ["with-router", "codex", "--", option, "client-value"]
277 .into_iter()
278 .map(OsString::from)
279 .collect::<Vec<_>>();
280 assert_eq!(
281 protect_client_arguments(arguments.clone(), false),
282 arguments,
283 "{option} after -- must be forwarded to the client"
284 );
285 }
286 }
287
288 #[test]
289 fn option_values_that_match_clients_are_not_boundaries() {
290 let arguments = ["with-router", "--model", "codex", "qwen", "hello"]
291 .into_iter()
292 .map(OsString::from)
293 .collect();
294 assert_eq!(
295 protect_client_arguments(arguments, false),
296 ["with-router", "--model", "codex", "qwen", "--", "hello"].map(OsString::from)
297 );
298 }
299}