leviath_cli/commands/run/
mod.rs1pub mod manifest;
10pub mod session;
11pub mod task;
12
13use std::collections::HashMap;
14
15use clap::Args;
16
17pub use session::{
19 ProviderCreds, build_provider_registry, build_provider_registry_from_config,
20 provider_creds_from_config,
21};
22
23#[derive(Args, Debug, Clone, Default)]
25pub struct RunArgs {
26 #[arg(value_name = "PATH")]
28 pub path: Option<String>,
29
30 #[arg(short, long, value_name = "TEXT|FILE")]
33 pub task: Option<String>,
34
35 #[arg(short, long)]
37 pub model: Option<String>,
38
39 #[arg(long)]
49 pub yolo: bool,
50
51 #[arg(long)]
53 pub allow: Vec<String>,
54
55 #[arg(long)]
57 pub max_depth: Option<usize>,
58
59 #[arg(long)]
63 pub no_seed_commands: bool,
64
65 #[arg(long, value_name = "DIR")]
69 pub workdir: Option<std::path::PathBuf>,
70
71 #[arg(long)]
74 pub json: bool,
75
76 #[arg(skip)]
80 pub regions: HashMap<String, String>,
81}
82
83const KNOWN_RUN_FLAGS: &[&str] = &[
86 "task",
87 "model",
88 "yolo",
89 "allow",
90 "max-depth",
91 "no-seed-commands",
92 "workdir",
93 "json",
97 "verbose",
98 "help",
99 "version",
100];
101
102pub fn effective_workdir(
107 flag: Option<std::path::PathBuf>,
108 cwd: std::path::PathBuf,
109) -> anyhow::Result<String> {
110 let dir = match flag {
111 Some(dir) => {
112 let canonical = std::fs::canonicalize(&dir).map_err(|e| {
113 anyhow::anyhow!(
114 "--workdir '{}' is not a usable directory: {e}",
115 dir.display()
116 )
117 })?;
118 if !canonical.is_dir() {
119 anyhow::bail!("--workdir '{}' is not a directory", dir.display());
120 }
121 canonical
122 }
123 None => cwd,
124 };
125 Ok(dir.to_string_lossy().to_string())
126}
127
128pub fn extract_region_flags(argv: Vec<String>) -> (Vec<String>, HashMap<String, String>) {
138 let sub_pos = argv
141 .iter()
142 .enumerate()
143 .skip(1)
144 .find(|(_, t)| !t.starts_with('-'))
145 .map(|(i, _)| i);
146 let Some(sub_pos) = sub_pos else {
147 return (argv, HashMap::new());
148 };
149 if argv[sub_pos] != "run" {
150 return (argv, HashMap::new());
151 }
152
153 let mut out: Vec<String> = argv[..=sub_pos].to_vec();
154 let mut regions = HashMap::new();
155 let mut i = sub_pos + 1;
156 while i < argv.len() {
157 let token = &argv[i];
158 if let Some(name) = token.strip_prefix("--") {
159 let (name, inline) = match name.split_once('=') {
161 Some((n, v)) => (n, Some(v.to_string())),
162 None => (name, None),
163 };
164 if !name.is_empty() && !KNOWN_RUN_FLAGS.contains(&name) {
165 let value = match inline {
166 Some(v) => v,
167 None => {
168 i += 1;
170 argv.get(i).cloned().unwrap_or_default()
171 }
172 };
173 regions.insert(name.to_string(), value);
174 i += 1;
175 continue;
176 }
177 }
178 out.push(token.clone());
179 i += 1;
180 }
181 (out, regions)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn argv(parts: &[&str]) -> Vec<String> {
189 parts.iter().map(|s| s.to_string()).collect()
190 }
191
192 #[test]
193 fn every_flag_run_declares_is_a_known_run_flag() {
194 let command = <RunArgs as clap::Args>::augment_args(clap::Command::new("run"));
199 for arg in command.get_arguments() {
200 if let Some(long) = arg.get_long() {
201 assert!(
202 KNOWN_RUN_FLAGS.contains(&long),
203 "`--{long}` is missing from KNOWN_RUN_FLAGS",
204 );
205 }
206 }
207 }
208
209 #[test]
210 fn extracts_dynamic_region_flags_and_preserves_known_ones() {
211 let (out, regions) = extract_region_flags(argv(&[
212 "lev",
213 "run",
214 "agents/reviewer",
215 "--task",
216 "review it",
217 "--files",
218 "@src/main.rs",
219 "--review-criteria",
220 "@policy.md",
221 "--yolo",
222 ]));
223 assert_eq!(
225 out,
226 argv(&[
227 "lev",
228 "run",
229 "agents/reviewer",
230 "--task",
231 "review it",
232 "--yolo",
233 ])
234 );
235 assert_eq!(
236 regions.get("files").map(String::as_str),
237 Some("@src/main.rs")
238 );
239 assert_eq!(
240 regions.get("review-criteria").map(String::as_str),
241 Some("@policy.md")
242 );
243 }
244
245 #[test]
246 fn extracts_equals_joined_region_flag() {
247 let (out, regions) = extract_region_flags(argv(&["lev", "run", "a", "--criteria=be safe"]));
248 assert_eq!(out, argv(&["lev", "run", "a"]));
249 assert_eq!(regions.get("criteria").map(String::as_str), Some("be safe"));
250 }
251
252 #[test]
253 fn no_region_flags_leaves_argv_unchanged() {
254 let input = argv(&["lev", "run", "a", "--task", "t"]);
255 let (out, regions) = extract_region_flags(input.clone());
256 assert_eq!(out, input);
257 assert!(regions.is_empty());
258 }
259
260 #[test]
261 fn non_run_subcommand_is_untouched() {
262 let input = argv(&["lev", "ps", "--weird", "x"]);
264 let (out, regions) = extract_region_flags(input.clone());
265 assert_eq!(out, input);
266 assert!(regions.is_empty());
267 }
268
269 #[test]
270 fn no_subcommand_token_is_untouched() {
271 let input = argv(&["lev", "--verbose"]);
273 let (out, regions) = extract_region_flags(input.clone());
274 assert_eq!(out, input);
275 assert!(regions.is_empty());
276 }
277
278 #[test]
279 fn trailing_region_flag_without_value_maps_to_empty() {
280 let (out, regions) = extract_region_flags(argv(&["lev", "run", "a", "--spec"]));
282 assert_eq!(out, argv(&["lev", "run", "a"]));
283 assert_eq!(regions.get("spec").map(String::as_str), Some(""));
284 }
285
286 #[test]
287 fn global_verbose_before_run_still_activates() {
288 let (_out, regions) = extract_region_flags(argv(&["lev", "-v", "run", "a", "--spec", "x"]));
289 assert_eq!(regions.get("spec").map(String::as_str), Some("x"));
290 }
291
292 #[test]
295 fn workdir_flag_is_not_eaten_as_a_region() {
296 let input = argv(&["lev", "run", "a", "--workdir", "/elsewhere", "--task", "t"]);
297 let (out, regions) = extract_region_flags(input.clone());
298 assert_eq!(out, input);
299 assert!(regions.is_empty());
300 }
301
302 #[test]
303 fn effective_workdir_uses_the_flag_canonicalized() {
304 let dir = tempfile::tempdir().unwrap();
305 let got = effective_workdir(
306 Some(dir.path().to_path_buf()),
307 std::path::PathBuf::from("/unused"),
308 )
309 .unwrap();
310 assert_eq!(
311 got,
312 std::fs::canonicalize(dir.path())
313 .unwrap()
314 .to_string_lossy()
315 .to_string()
316 );
317 }
318
319 #[test]
320 fn effective_workdir_defaults_to_the_supplied_cwd() {
321 let cwd = tempfile::tempdir().unwrap();
322 let got = effective_workdir(None, cwd.path().to_path_buf()).unwrap();
323 assert_eq!(got, cwd.path().to_string_lossy().to_string());
324 }
325
326 #[test]
329 fn effective_workdir_refuses_a_missing_or_non_directory_path() {
330 let cwd = std::path::PathBuf::from("/unused");
331 let err = effective_workdir(
332 Some(std::path::PathBuf::from("/definitely/not/a/real/dir")),
333 cwd.clone(),
334 )
335 .unwrap_err();
336 assert!(err.to_string().contains("not a usable directory"), "{err}");
337
338 let dir = tempfile::tempdir().unwrap();
339 let file = dir.path().join("f.txt");
340 std::fs::write(&file, "x").unwrap();
341 let err = effective_workdir(Some(file), cwd).unwrap_err();
342 assert!(err.to_string().contains("not a directory"), "{err}");
343 }
344}