1use crate::commands;
23
24#[derive(clap::Subcommand)]
25pub enum Commands {
26 Create(commands::create::CreateArgs),
28
29 Setup(commands::setup::SetupArgs),
31
32 Run(commands::run::RunArgs),
34
35 #[command(long_about = commands::ps::PS_LONG_ABOUT)]
37 Ps(commands::ps::PsArgs),
38
39 Msg(commands::ctl::MsgArgs),
41
42 #[command(alias = "kill")]
44 Cancel(commands::ctl::CancelArgs),
45
46 Pause(commands::ctl::PauseArgs),
48
49 Resume(commands::ctl::ResumeArgs),
51
52 Respond(commands::ctl::RespondArgs),
54
55 #[command(long_about = commands::doctor::DOCTOR_LONG_ABOUT)]
57 Doctor(commands::doctor::DoctorArgs),
58
59 List(commands::list::ListArgs),
61
62 Add(commands::add::AddArgs),
64
65 Remove(commands::remove::RemoveArgs),
67
68 Test(commands::test::TestArgs),
70
71 Pack(commands::pack::PackArgs),
73
74 #[command(name = "dash")]
76 Dashboard(commands::dashboard::DashboardArgs),
77
78 Models(commands::models::ModelsArgs),
80
81 Validate(commands::validate::ValidateArgs),
83
84 Tools(commands::tools::ToolsArgs),
86
87 Policy(commands::policy::PolicyArgs),
89
90 Serve(commands::serve::ServeArgs),
92
93 #[command(name = "agent-client")]
95 AgentClient(commands::agent_client::AgentClientArgs),
96
97 Daemon(commands::daemon::DaemonArgs),
99
100 Context(commands::context::ContextArgs),
102
103 Mcp(commands::mcp::McpArgs),
105
106 Auth(commands::auth::AuthArgs),
108}
109
110#[allow(async_fn_in_trait)]
118pub trait RiskyExecutors {
119 async fn run(&self, args: commands::run::RunArgs) -> anyhow::Result<()>;
122 async fn ps(&self, args: commands::ps::PsArgs) -> anyhow::Result<()>;
124 async fn msg(&self, args: commands::ctl::MsgArgs) -> anyhow::Result<()>;
126 async fn cancel(&self, args: commands::ctl::CancelArgs) -> anyhow::Result<()>;
128 async fn pause(&self, args: commands::ctl::PauseArgs) -> anyhow::Result<()>;
130 async fn resume(&self, args: commands::ctl::ResumeArgs) -> anyhow::Result<()>;
132 async fn respond(&self, args: commands::ctl::RespondArgs) -> anyhow::Result<()>;
134 async fn doctor(&self, args: commands::doctor::DoctorArgs) -> anyhow::Result<()>;
137 async fn setup(&self, args: commands::setup::SetupArgs) -> anyhow::Result<()>;
139 async fn dashboard(&self, args: commands::dashboard::DashboardArgs) -> anyhow::Result<()>;
141 async fn serve(&self, args: commands::serve::ServeArgs) -> anyhow::Result<()>;
143 async fn agent_client(
146 &self,
147 args: commands::agent_client::AgentClientArgs,
148 ) -> anyhow::Result<()>;
149 async fn daemon(&self, args: commands::daemon::DaemonArgs) -> anyhow::Result<()>;
151 async fn mcp(&self, args: commands::mcp::McpArgs) -> anyhow::Result<()>;
153
154 async fn auth(&self, args: commands::auth::AuthArgs) -> anyhow::Result<()>;
156}
157
158pub fn apply_region_flags(
162 command: &mut Commands,
163 regions: std::collections::HashMap<String, String>,
164) {
165 if let Commands::Run(args) = command {
166 args.regions = regions;
167 }
168}
169
170pub async fn dispatch(command: Commands, ex: &impl RiskyExecutors) -> anyhow::Result<()> {
174 match command {
175 Commands::Create(args) => commands::create::execute(args).await,
176 Commands::Setup(args) => ex.setup(args).await,
177 Commands::Run(args) => ex.run(args).await,
178 Commands::Ps(args) => ex.ps(args).await,
179 Commands::Msg(args) => ex.msg(args).await,
180 Commands::Cancel(args) => ex.cancel(args).await,
181 Commands::Pause(args) => ex.pause(args).await,
182 Commands::Resume(args) => ex.resume(args).await,
183 Commands::Respond(args) => ex.respond(args).await,
184 Commands::Doctor(args) => ex.doctor(args).await,
185 Commands::List(args) => commands::list::execute(args).await,
186 Commands::Add(args) => commands::add::execute(args).await,
187 Commands::Remove(args) => commands::remove::execute(args).await,
188 Commands::Test(args) => commands::test::execute(args).await,
189 Commands::Pack(args) => commands::pack::execute(args).await,
190 Commands::Dashboard(args) => ex.dashboard(args).await,
191 Commands::Models(args) => commands::models::execute(args).await,
192 Commands::Validate(args) => commands::validate::execute(args).await,
193 Commands::Tools(args) => commands::tools::execute(args).await,
194 Commands::Policy(args) => commands::policy::execute(args).await,
195 Commands::Serve(args) => ex.serve(args).await,
196 Commands::AgentClient(args) => ex.agent_client(args).await,
197 Commands::Daemon(args) => ex.daemon(args).await,
198 Commands::Context(args) => commands::context::execute(args).await,
199 Commands::Mcp(args) => ex.mcp(args).await,
200 Commands::Auth(args) => ex.auth(args).await,
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 struct MockRisky;
212
213 impl RiskyExecutors for MockRisky {
214 async fn run(&self, _args: commands::run::RunArgs) -> anyhow::Result<()> {
215 Ok(())
216 }
217 async fn ps(&self, _args: commands::ps::PsArgs) -> anyhow::Result<()> {
218 Ok(())
219 }
220 async fn msg(&self, _args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
221 Ok(())
222 }
223 async fn respond(&self, _args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
224 Ok(())
225 }
226 async fn doctor(&self, _args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
227 Ok(())
228 }
229 async fn cancel(&self, _args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
230 Ok(())
231 }
232 async fn pause(&self, _args: commands::ctl::PauseArgs) -> anyhow::Result<()> {
233 Ok(())
234 }
235 async fn resume(&self, _args: commands::ctl::ResumeArgs) -> anyhow::Result<()> {
236 Ok(())
237 }
238 async fn setup(&self, _args: commands::setup::SetupArgs) -> anyhow::Result<()> {
239 Ok(())
240 }
241 async fn dashboard(&self, _args: commands::dashboard::DashboardArgs) -> anyhow::Result<()> {
242 Ok(())
243 }
244 async fn serve(&self, _args: commands::serve::ServeArgs) -> anyhow::Result<()> {
245 Ok(())
246 }
247 async fn agent_client(
248 &self,
249 _args: commands::agent_client::AgentClientArgs,
250 ) -> anyhow::Result<()> {
251 Ok(())
252 }
253 async fn daemon(&self, _args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
254 Ok(())
255 }
256 async fn auth(&self, _args: commands::auth::AuthArgs) -> anyhow::Result<()> {
257 Ok(())
258 }
259
260 async fn mcp(&self, _args: commands::mcp::McpArgs) -> anyhow::Result<()> {
261 Ok(())
262 }
263 }
264
265 fn create_args() -> commands::create::CreateArgs {
266 commands::create::CreateArgs {
267 name: "unused".to_string(),
268 template: "software-engineer".to_string(),
269 }
270 }
271
272 #[test]
275 fn apply_region_flags_populates_run_and_noops_other_commands() {
276 let mut run = Commands::Run(commands::run::RunArgs::default());
277 let flags = std::collections::HashMap::from([("criteria".to_string(), "safe".to_string())]);
278 apply_region_flags(&mut run, flags);
279 assert!(
280 matches!(&run, Commands::Run(a) if a.regions.get("criteria").map(String::as_str) == Some("safe")),
281 "region flag was injected into the Run args"
282 );
283 let mut other = Commands::Ps(commands::ps::PsArgs::default());
287 apply_region_flags(&mut other, std::collections::HashMap::new());
288 }
289
290 #[tokio::test]
293 async fn dispatch_run_variant_is_routed_through_the_executor() {
294 let result = dispatch(Commands::Run(commands::run::RunArgs::default()), &MockRisky).await;
295 assert!(result.is_ok());
296 }
297
298 #[tokio::test]
299 async fn dispatch_setup_variant_is_routed_through_the_executor() {
300 let args = commands::setup::SetupArgs {
301 non_interactive: true,
302 no_verify: false,
303 install_agents: false,
304 anthropic_key: None,
305 openai_key: None,
306 google_key: None,
307 openrouter_key: None,
308 ollama_url: None,
309 default_model: None,
310 claude_code: None,
311 claude_code_effort: None,
312 };
313 let result = dispatch(Commands::Setup(args), &MockRisky).await;
314 assert!(result.is_ok());
315 }
316
317 #[tokio::test]
318 async fn dispatch_dashboard_variant_is_routed_through_the_executor() {
319 let args = commands::dashboard::DashboardArgs {};
320 let result = dispatch(Commands::Dashboard(args), &MockRisky).await;
321 assert!(result.is_ok());
322 }
323
324 #[tokio::test]
325 async fn dispatch_msg_variant_is_routed_through_the_executor() {
326 let args = commands::ctl::MsgArgs {
327 agent_id: "a".to_string(),
328 content: "c".to_string(),
329 };
330 assert!(dispatch(Commands::Msg(args), &MockRisky).await.is_ok());
331 }
332
333 #[tokio::test]
334 async fn dispatch_respond_variant_is_routed_through_the_executor() {
335 let args = commands::ctl::RespondArgs {
336 request_id: None,
337 value: None,
338 choice: None,
339 approve: false,
340 deny: false,
341 session: false,
342 };
343 assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
344 }
345
346 #[tokio::test]
347 async fn dispatch_doctor_variant_is_routed_through_the_executor() {
348 let args = commands::doctor::DoctorArgs::default();
351 assert!(dispatch(Commands::Doctor(args), &MockRisky).await.is_ok());
352 }
353
354 #[tokio::test]
355 async fn dispatch_cancel_variant_is_routed_through_the_executor() {
356 let args = commands::ctl::CancelArgs {
357 run_id: "r".to_string(),
358 force: false,
359 };
360 assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
361 }
362
363 #[tokio::test]
364 async fn dispatch_pause_variant_is_routed_through_the_executor() {
365 let args = commands::ctl::PauseArgs {
366 run_id: "r".to_string(),
367 };
368 assert!(dispatch(Commands::Pause(args), &MockRisky).await.is_ok());
369 }
370
371 #[tokio::test]
372 async fn dispatch_resume_variant_is_routed_through_the_executor() {
373 let args = commands::ctl::ResumeArgs {
374 run_id: "r".to_string(),
375 };
376 assert!(dispatch(Commands::Resume(args), &MockRisky).await.is_ok());
377 }
378
379 #[tokio::test]
380 async fn dispatch_ps_variant_is_routed_through_the_executor() {
381 let result = dispatch(Commands::Ps(commands::ps::PsArgs::default()), &MockRisky).await;
382 assert!(result.is_ok());
383 }
384
385 #[tokio::test]
386 async fn dispatch_daemon_variant_is_routed_through_the_executor() {
387 let args = commands::daemon::DaemonArgs {
388 action: None,
389 socket: None,
390 };
391 let result = dispatch(Commands::Daemon(args), &MockRisky).await;
392 assert!(result.is_ok());
393 }
394
395 #[tokio::test]
396 async fn dispatch_auth_variant_is_routed_through_the_executor() {
397 let args = commands::auth::AuthArgs::status_for_test();
398 let result = dispatch(Commands::Auth(args), &MockRisky).await;
399 assert!(result.is_ok());
400 }
401
402 #[tokio::test]
403 async fn dispatch_mcp_variant_is_routed_through_the_executor() {
404 let args = commands::mcp::McpArgs::list_for_test();
405 let result = dispatch(Commands::Mcp(args), &MockRisky).await;
406 assert!(result.is_ok());
407 }
408
409 #[tokio::test]
410 async fn dispatch_serve_variant_is_routed_through_the_executor() {
411 let args = commands::serve::ServeArgs {
412 port: 0,
413 host: "127.0.0.1".to_string(),
414 cors: None,
415 token: Some("test-token".to_string()),
416 allow_admin: false,
417 workdir_root: None,
418 no_remote_yolo: false,
419 };
420 let result = dispatch(Commands::Serve(args), &MockRisky).await;
421 assert!(result.is_ok());
422 }
423
424 #[tokio::test]
425 async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
426 let args = commands::agent_client::AgentClientArgs::default();
427 let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
428 assert!(result.is_ok());
429 }
430
431 #[tokio::test]
434 async fn dispatch_create_variant_is_routed() {
435 let dir = tempfile::tempdir().unwrap();
438 let args = commands::create::CreateArgs {
439 name: dir.path().to_str().unwrap().to_string(),
440 ..create_args()
441 };
442 let result = dispatch(Commands::Create(args), &MockRisky).await;
443 assert!(result.is_err());
444 }
445
446 #[tokio::test]
447 async fn dispatch_list_variant_is_routed() {
448 crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
451 let args = commands::list::ListArgs {
452 filter: "all".to_string(),
453 };
454 let result = dispatch(Commands::List(args), &MockRisky).await;
455 assert!(result.is_ok());
456 })
457 .await;
458 }
459
460 #[tokio::test]
461 async fn dispatch_add_variant_is_routed() {
462 let args = commands::add::AddArgs {
463 package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
464 };
465 let result = crate::config::with_isolated_config_path_async("dispatch-add", |_| {
469 dispatch(Commands::Add(args), &MockRisky)
470 })
471 .await;
472 assert!(result.is_err());
473 }
474
475 #[tokio::test]
476 async fn dispatch_remove_variant_is_routed() {
477 let args = commands::remove::RemoveArgs {
478 name: "definitely-not-an-installed-agent-xyz".to_string(),
479 };
480 let result = dispatch(Commands::Remove(args), &MockRisky).await;
481 assert!(result.is_err());
482 }
483
484 #[tokio::test]
485 async fn dispatch_test_variant_is_routed() {
486 let dir = tempfile::tempdir().unwrap();
487 let args = commands::test::TestArgs {
488 path: Some(dir.path().to_str().unwrap().to_string()),
489 filter: None,
490 dry_run: true,
491 };
492 let result = dispatch(Commands::Test(args), &MockRisky).await;
493 assert!(result.is_err());
494 }
495
496 #[tokio::test]
497 async fn dispatch_pack_variant_is_routed() {
498 let dir = tempfile::tempdir().unwrap();
499 let args = commands::pack::PackArgs {
500 path: Some(dir.path().to_str().unwrap().to_string()),
501 output: None,
502 };
503 let result = dispatch(Commands::Pack(args), &MockRisky).await;
504 assert!(result.is_err());
505 }
506
507 #[tokio::test]
508 async fn dispatch_models_variant_is_routed() {
509 crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
510 let args = commands::models::ModelsArgs {
511 command: commands::models::ModelsCommand::List(commands::models::ListArgs {
512 provider: None,
513 remote: false,
514 all: false,
515 }),
516 };
517 let result = dispatch(Commands::Models(args), &MockRisky).await;
518 assert!(result.is_ok());
519 })
520 .await;
521 }
522
523 #[tokio::test]
524 async fn dispatch_validate_variant_is_routed() {
525 crate::config::with_isolated_config_path_async("dispatch-validate", |_| async {
529 let dir = tempfile::tempdir().unwrap();
530 let args = commands::validate::ValidateArgs {
531 path: dir
532 .path()
533 .join("does-not-exist")
534 .to_str()
535 .unwrap()
536 .to_string(),
537 deny_warnings: false,
538 };
539 let result = dispatch(Commands::Validate(args), &MockRisky).await;
540 assert!(result.is_err());
541 })
542 .await;
543 }
544
545 #[tokio::test]
546 async fn dispatch_tools_variant_is_routed() {
547 let home = tempfile::tempdir().unwrap();
550 let result = temp_env::async_with_vars(
551 [("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
552 async {
553 let args = commands::tools::ToolsArgs { json: false };
554 dispatch(Commands::Tools(args), &MockRisky).await
555 },
556 )
557 .await;
558 assert!(result.is_ok());
559 }
560
561 #[tokio::test]
562 async fn dispatch_context_variant_is_routed() {
563 let args = commands::context::ContextArgs {
565 run_id: "no-such-run-xyzzy".to_string(),
566 json: false,
567 full: false,
568 };
569 let result = dispatch(Commands::Context(args), &MockRisky).await;
570 assert!(result.is_err());
571 }
572
573 #[tokio::test]
574 async fn dispatch_policy_list_variant_is_routed() {
575 let args = commands::policy::PolicyArgs {
576 command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
577 };
578 let result = dispatch(Commands::Policy(args), &MockRisky).await;
579 assert!(result.is_ok());
580 }
581
582 #[tokio::test]
583 async fn dispatch_policy_test_variant_is_routed() {
584 let args = commands::policy::PolicyArgs {
585 command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
586 tool: "shell".to_string(),
587 target: None,
588 taint: "public".to_string(),
589 }),
590 };
591 let result = dispatch(Commands::Policy(args), &MockRisky).await;
592 assert!(result.is_ok());
593 }
594}