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)]
42 pub yolo: bool,
43
44 #[arg(long)]
46 pub allow: Vec<String>,
47
48 #[arg(long)]
50 pub max_depth: Option<usize>,
51
52 #[arg(long)]
56 pub no_seed_commands: bool,
57
58 #[arg(long, value_name = "DIR")]
62 pub workdir: Option<std::path::PathBuf>,
63
64 #[arg(skip)]
68 pub regions: HashMap<String, String>,
69}
70
71const KNOWN_RUN_FLAGS: &[&str] = &[
74 "task",
75 "model",
76 "yolo",
77 "allow",
78 "max-depth",
79 "no-seed-commands",
80 "workdir",
81 "verbose",
82 "help",
83 "version",
84];
85
86pub fn effective_workdir(
91 flag: Option<std::path::PathBuf>,
92 cwd: std::path::PathBuf,
93) -> anyhow::Result<String> {
94 let dir = match flag {
95 Some(dir) => {
96 let canonical = std::fs::canonicalize(&dir).map_err(|e| {
97 anyhow::anyhow!(
98 "--workdir '{}' is not a usable directory: {e}",
99 dir.display()
100 )
101 })?;
102 if !canonical.is_dir() {
103 anyhow::bail!("--workdir '{}' is not a directory", dir.display());
104 }
105 canonical
106 }
107 None => cwd,
108 };
109 Ok(dir.to_string_lossy().to_string())
110}
111
112pub fn extract_region_flags(argv: Vec<String>) -> (Vec<String>, HashMap<String, String>) {
122 let sub_pos = argv
125 .iter()
126 .enumerate()
127 .skip(1)
128 .find(|(_, t)| !t.starts_with('-'))
129 .map(|(i, _)| i);
130 let Some(sub_pos) = sub_pos else {
131 return (argv, HashMap::new());
132 };
133 if argv[sub_pos] != "run" {
134 return (argv, HashMap::new());
135 }
136
137 let mut out: Vec<String> = argv[..=sub_pos].to_vec();
138 let mut regions = HashMap::new();
139 let mut i = sub_pos + 1;
140 while i < argv.len() {
141 let token = &argv[i];
142 if let Some(name) = token.strip_prefix("--") {
143 let (name, inline) = match name.split_once('=') {
145 Some((n, v)) => (n, Some(v.to_string())),
146 None => (name, None),
147 };
148 if !name.is_empty() && !KNOWN_RUN_FLAGS.contains(&name) {
149 let value = match inline {
150 Some(v) => v,
151 None => {
152 i += 1;
154 argv.get(i).cloned().unwrap_or_default()
155 }
156 };
157 regions.insert(name.to_string(), value);
158 i += 1;
159 continue;
160 }
161 }
162 out.push(token.clone());
163 i += 1;
164 }
165 (out, regions)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn argv(parts: &[&str]) -> Vec<String> {
173 parts.iter().map(|s| s.to_string()).collect()
174 }
175
176 #[test]
177 fn extracts_dynamic_region_flags_and_preserves_known_ones() {
178 let (out, regions) = extract_region_flags(argv(&[
179 "lev",
180 "run",
181 "agents/reviewer",
182 "--task",
183 "review it",
184 "--files",
185 "@src/main.rs",
186 "--review-criteria",
187 "@policy.md",
188 "--yolo",
189 ]));
190 assert_eq!(
192 out,
193 argv(&[
194 "lev",
195 "run",
196 "agents/reviewer",
197 "--task",
198 "review it",
199 "--yolo",
200 ])
201 );
202 assert_eq!(
203 regions.get("files").map(String::as_str),
204 Some("@src/main.rs")
205 );
206 assert_eq!(
207 regions.get("review-criteria").map(String::as_str),
208 Some("@policy.md")
209 );
210 }
211
212 #[test]
213 fn extracts_equals_joined_region_flag() {
214 let (out, regions) = extract_region_flags(argv(&["lev", "run", "a", "--criteria=be safe"]));
215 assert_eq!(out, argv(&["lev", "run", "a"]));
216 assert_eq!(regions.get("criteria").map(String::as_str), Some("be safe"));
217 }
218
219 #[test]
220 fn no_region_flags_leaves_argv_unchanged() {
221 let input = argv(&["lev", "run", "a", "--task", "t"]);
222 let (out, regions) = extract_region_flags(input.clone());
223 assert_eq!(out, input);
224 assert!(regions.is_empty());
225 }
226
227 #[test]
228 fn non_run_subcommand_is_untouched() {
229 let input = argv(&["lev", "ps", "--weird", "x"]);
231 let (out, regions) = extract_region_flags(input.clone());
232 assert_eq!(out, input);
233 assert!(regions.is_empty());
234 }
235
236 #[test]
237 fn no_subcommand_token_is_untouched() {
238 let input = argv(&["lev", "--verbose"]);
240 let (out, regions) = extract_region_flags(input.clone());
241 assert_eq!(out, input);
242 assert!(regions.is_empty());
243 }
244
245 #[test]
246 fn trailing_region_flag_without_value_maps_to_empty() {
247 let (out, regions) = extract_region_flags(argv(&["lev", "run", "a", "--spec"]));
249 assert_eq!(out, argv(&["lev", "run", "a"]));
250 assert_eq!(regions.get("spec").map(String::as_str), Some(""));
251 }
252
253 #[test]
254 fn global_verbose_before_run_still_activates() {
255 let (_out, regions) = extract_region_flags(argv(&["lev", "-v", "run", "a", "--spec", "x"]));
256 assert_eq!(regions.get("spec").map(String::as_str), Some("x"));
257 }
258
259 #[test]
262 fn workdir_flag_is_not_eaten_as_a_region() {
263 let input = argv(&["lev", "run", "a", "--workdir", "/elsewhere", "--task", "t"]);
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 effective_workdir_uses_the_flag_canonicalized() {
271 let dir = tempfile::tempdir().unwrap();
272 let got = effective_workdir(
273 Some(dir.path().to_path_buf()),
274 std::path::PathBuf::from("/unused"),
275 )
276 .unwrap();
277 assert_eq!(
278 got,
279 std::fs::canonicalize(dir.path())
280 .unwrap()
281 .to_string_lossy()
282 .to_string()
283 );
284 }
285
286 #[test]
287 fn effective_workdir_defaults_to_the_supplied_cwd() {
288 let cwd = tempfile::tempdir().unwrap();
289 let got = effective_workdir(None, cwd.path().to_path_buf()).unwrap();
290 assert_eq!(got, cwd.path().to_string_lossy().to_string());
291 }
292
293 #[test]
296 fn effective_workdir_refuses_a_missing_or_non_directory_path() {
297 let cwd = std::path::PathBuf::from("/unused");
298 let err = effective_workdir(
299 Some(std::path::PathBuf::from("/definitely/not/a/real/dir")),
300 cwd.clone(),
301 )
302 .unwrap_err();
303 assert!(err.to_string().contains("not a usable directory"), "{err}");
304
305 let dir = tempfile::tempdir().unwrap();
306 let file = dir.path().join("f.txt");
307 std::fs::write(&file, "x").unwrap();
308 let err = effective_workdir(Some(file), cwd).unwrap_err();
309 assert!(err.to_string().contains("not a directory"), "{err}");
310 }
311}