1use crate::commands;
23
24#[derive(clap::Subcommand)]
26pub enum Commands {
27 Create(commands::create::CreateArgs),
29
30 Setup(commands::setup::SetupArgs),
32
33 Run(commands::run::RunArgs),
35
36 #[command(long_about = commands::ps::PS_LONG_ABOUT)]
38 Ps(commands::ps::PsArgs),
39
40 Msg(commands::ctl::MsgArgs),
42
43 #[command(alias = "kill")]
45 Cancel(commands::ctl::CancelArgs),
46
47 Pause(commands::ctl::PauseArgs),
49
50 Resume(commands::ctl::ResumeArgs),
52
53 Respond(commands::ctl::RespondArgs),
55
56 #[command(long_about = commands::doctor::DOCTOR_LONG_ABOUT)]
58 Doctor(commands::doctor::DoctorArgs),
59
60 List(commands::list::ListArgs),
62
63 Add(commands::add::AddArgs),
65
66 Remove(commands::remove::RemoveArgs),
68
69 Test(commands::test::TestArgs),
71
72 Pack(commands::pack::PackArgs),
74
75 #[command(name = "dash")]
77 Dashboard(commands::dashboard::DashboardArgs),
78
79 Models(commands::models::ModelsArgs),
81
82 Validate(commands::validate::ValidateArgs),
84
85 Tools(commands::tools::ToolsArgs),
87
88 Approvals(commands::approvals::ApprovalsArgs),
90
91 Policy(commands::policy::PolicyArgs),
93
94 Serve(commands::serve::ServeArgs),
96
97 #[command(name = "agent-client")]
99 AgentClient(commands::agent_client::AgentClientArgs),
100
101 Daemon(commands::daemon::DaemonArgs),
103
104 Context(commands::context::ContextArgs),
106
107 Stages(commands::stages::StagesArgs),
109
110 Result(commands::result::ResultArgs),
112
113 Mcp(commands::mcp::McpArgs),
115
116 Auth(commands::auth::AuthArgs),
118}
119
120pub trait RiskyExecutors {
128 fn run(
139 &self,
140 args: commands::run::RunArgs,
141 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
142 fn ps(
144 &self,
145 args: commands::ps::PsArgs,
146 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
147 fn msg(
149 &self,
150 args: commands::ctl::MsgArgs,
151 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
152 fn cancel(
154 &self,
155 args: commands::ctl::CancelArgs,
156 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
157 fn pause(
159 &self,
160 args: commands::ctl::PauseArgs,
161 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
162 fn resume(
164 &self,
165 args: commands::ctl::ResumeArgs,
166 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
167 fn respond(
169 &self,
170 args: commands::ctl::RespondArgs,
171 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
172 fn doctor(
175 &self,
176 args: commands::doctor::DoctorArgs,
177 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
178 fn setup(
180 &self,
181 args: commands::setup::SetupArgs,
182 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
183 fn dashboard(
185 &self,
186 args: commands::dashboard::DashboardArgs,
187 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
188 fn serve(
190 &self,
191 args: commands::serve::ServeArgs,
192 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
193 fn agent_client(
196 &self,
197 args: commands::agent_client::AgentClientArgs,
198 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
199 fn daemon(
201 &self,
202 args: commands::daemon::DaemonArgs,
203 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
204 fn mcp(
206 &self,
207 args: commands::mcp::McpArgs,
208 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
209
210 fn auth(
212 &self,
213 args: commands::auth::AuthArgs,
214 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
215}
216
217pub fn apply_region_flags(
221 command: &mut Commands,
222 regions: std::collections::HashMap<String, String>,
223) {
224 if let Commands::Run(args) = command {
225 args.regions = regions;
226 }
227}
228
229pub async fn dispatch(command: Commands, ex: &impl RiskyExecutors) -> anyhow::Result<()> {
233 match command {
234 Commands::Create(args) => commands::create::execute(args).await,
235 Commands::Setup(args) => ex.setup(args).await,
236 Commands::Run(args) => ex.run(args).await,
237 Commands::Ps(args) => ex.ps(args).await,
238 Commands::Msg(args) => ex.msg(args).await,
239 Commands::Cancel(args) => ex.cancel(args).await,
240 Commands::Pause(args) => ex.pause(args).await,
241 Commands::Resume(args) => ex.resume(args).await,
242 Commands::Respond(args) => ex.respond(args).await,
243 Commands::Doctor(args) => ex.doctor(args).await,
244 Commands::List(args) => commands::list::execute(args).await,
245 Commands::Add(args) => commands::add::execute(args).await,
246 Commands::Remove(args) => commands::remove::execute(args).await,
247 Commands::Test(args) => commands::test::execute(args).await,
248 Commands::Pack(args) => commands::pack::execute(args).await,
249 Commands::Dashboard(args) => ex.dashboard(args).await,
250 Commands::Models(args) => commands::models::execute(args).await,
251 Commands::Validate(args) => commands::validate::execute(args).await,
252 Commands::Tools(args) => commands::tools::execute(args).await,
253 Commands::Approvals(args) => commands::approvals::execute(args).await,
254 Commands::Policy(args) => commands::policy::execute(args).await,
255 Commands::Serve(args) => ex.serve(args).await,
256 Commands::AgentClient(args) => ex.agent_client(args).await,
257 Commands::Daemon(args) => ex.daemon(args).await,
258 Commands::Context(args) => commands::context::execute(args).await,
259 Commands::Stages(args) => commands::stages::execute(args).await,
260 Commands::Result(args) => commands::result::execute(args).await,
261 Commands::Mcp(args) => ex.mcp(args).await,
262 Commands::Auth(args) => ex.auth(args).await,
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 struct MockRisky;
274
275 impl RiskyExecutors for MockRisky {
276 async fn run(&self, _args: commands::run::RunArgs) -> anyhow::Result<()> {
277 Ok(())
278 }
279 async fn ps(&self, _args: commands::ps::PsArgs) -> anyhow::Result<()> {
280 Ok(())
281 }
282 async fn msg(&self, _args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
283 Ok(())
284 }
285 async fn respond(&self, _args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
286 Ok(())
287 }
288 async fn doctor(&self, _args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
289 Ok(())
290 }
291 async fn cancel(&self, _args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
292 Ok(())
293 }
294 async fn pause(&self, _args: commands::ctl::PauseArgs) -> anyhow::Result<()> {
295 Ok(())
296 }
297 async fn resume(&self, _args: commands::ctl::ResumeArgs) -> anyhow::Result<()> {
298 Ok(())
299 }
300 async fn setup(&self, _args: commands::setup::SetupArgs) -> anyhow::Result<()> {
301 Ok(())
302 }
303 async fn dashboard(&self, _args: commands::dashboard::DashboardArgs) -> anyhow::Result<()> {
304 Ok(())
305 }
306 async fn serve(&self, _args: commands::serve::ServeArgs) -> anyhow::Result<()> {
307 Ok(())
308 }
309 async fn agent_client(
310 &self,
311 _args: commands::agent_client::AgentClientArgs,
312 ) -> anyhow::Result<()> {
313 Ok(())
314 }
315 async fn daemon(&self, _args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
316 Ok(())
317 }
318 async fn auth(&self, _args: commands::auth::AuthArgs) -> anyhow::Result<()> {
319 Ok(())
320 }
321
322 async fn mcp(&self, _args: commands::mcp::McpArgs) -> anyhow::Result<()> {
323 Ok(())
324 }
325 }
326
327 fn create_args() -> commands::create::CreateArgs {
328 commands::create::CreateArgs {
329 name: "unused".to_string(),
330 template: "software-engineer".to_string(),
331 }
332 }
333
334 #[test]
337 fn apply_region_flags_populates_run_and_noops_other_commands() {
338 let mut run = Commands::Run(commands::run::RunArgs::default());
339 let flags = std::collections::HashMap::from([("criteria".to_string(), "safe".to_string())]);
340 apply_region_flags(&mut run, flags);
341 assert!(
342 matches!(&run, Commands::Run(a) if a.regions.get("criteria").map(String::as_str) == Some("safe")),
343 "region flag was injected into the Run args"
344 );
345 let mut other = Commands::Ps(commands::ps::PsArgs::default());
349 apply_region_flags(&mut other, std::collections::HashMap::new());
350 }
351
352 #[tokio::test]
355 async fn dispatch_run_variant_is_routed_through_the_executor() {
356 let result = dispatch(Commands::Run(commands::run::RunArgs::default()), &MockRisky).await;
357 assert!(result.is_ok());
358 }
359
360 #[tokio::test]
361 async fn dispatch_setup_variant_is_routed_through_the_executor() {
362 let args = commands::setup::SetupArgs {
363 non_interactive: true,
364 no_verify: false,
365 install_agents: false,
366 anthropic_key: None,
367 openai_key: None,
368 google_key: None,
369 openrouter_key: None,
370 ollama_url: None,
371 default_model: None,
372 claude_code: None,
373 claude_code_effort: None,
374 };
375 let result = dispatch(Commands::Setup(args), &MockRisky).await;
376 assert!(result.is_ok());
377 }
378
379 #[tokio::test]
380 async fn dispatch_dashboard_variant_is_routed_through_the_executor() {
381 let args = commands::dashboard::DashboardArgs {};
382 let result = dispatch(Commands::Dashboard(args), &MockRisky).await;
383 assert!(result.is_ok());
384 }
385
386 #[tokio::test]
387 async fn dispatch_msg_variant_is_routed_through_the_executor() {
388 let args = commands::ctl::MsgArgs {
389 agent_id: "a".to_string(),
390 content: "c".to_string(),
391 };
392 assert!(dispatch(Commands::Msg(args), &MockRisky).await.is_ok());
393 }
394
395 #[tokio::test]
396 async fn dispatch_respond_variant_is_routed_through_the_executor() {
397 let args = commands::ctl::RespondArgs {
398 request_id: None,
399 value: None,
400 choice: None,
401 approve: false,
402 deny: false,
403 session: false,
404 stage: false,
405 json: false,
406 };
407 assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
408 }
409
410 #[tokio::test]
411 async fn dispatch_doctor_variant_is_routed_through_the_executor() {
412 let args = commands::doctor::DoctorArgs::default();
415 assert!(dispatch(Commands::Doctor(args), &MockRisky).await.is_ok());
416 }
417
418 #[tokio::test]
419 async fn dispatch_cancel_variant_is_routed_through_the_executor() {
420 let args = commands::ctl::CancelArgs {
421 run_id: "r".to_string(),
422 force: false,
423 };
424 assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
425 }
426
427 #[tokio::test]
428 async fn dispatch_pause_variant_is_routed_through_the_executor() {
429 let args = commands::ctl::PauseArgs {
430 run_id: "r".to_string(),
431 };
432 assert!(dispatch(Commands::Pause(args), &MockRisky).await.is_ok());
433 }
434
435 #[tokio::test]
436 async fn dispatch_resume_variant_is_routed_through_the_executor() {
437 let args = commands::ctl::ResumeArgs {
438 run_id: "r".to_string(),
439 };
440 assert!(dispatch(Commands::Resume(args), &MockRisky).await.is_ok());
441 }
442
443 #[tokio::test]
444 async fn dispatch_ps_variant_is_routed_through_the_executor() {
445 let result = dispatch(Commands::Ps(commands::ps::PsArgs::default()), &MockRisky).await;
446 assert!(result.is_ok());
447 }
448
449 #[tokio::test]
450 async fn dispatch_daemon_variant_is_routed_through_the_executor() {
451 let args = commands::daemon::DaemonArgs {
452 action: None,
453 socket: None,
454 };
455 let result = dispatch(Commands::Daemon(args), &MockRisky).await;
456 assert!(result.is_ok());
457 }
458
459 #[tokio::test]
460 async fn dispatch_auth_variant_is_routed_through_the_executor() {
461 let args = commands::auth::AuthArgs::status_for_test();
462 let result = dispatch(Commands::Auth(args), &MockRisky).await;
463 assert!(result.is_ok());
464 }
465
466 #[tokio::test]
467 async fn dispatch_mcp_variant_is_routed_through_the_executor() {
468 let args = commands::mcp::McpArgs::list_for_test();
469 let result = dispatch(Commands::Mcp(args), &MockRisky).await;
470 assert!(result.is_ok());
471 }
472
473 #[tokio::test]
474 async fn dispatch_serve_variant_is_routed_through_the_executor() {
475 let args = commands::serve::ServeArgs {
476 port: 0,
477 host: "127.0.0.1".to_string(),
478 cors: None,
479 token: Some("test-token".to_string()),
480 allow_admin: false,
481 workdir_root: None,
482 no_remote_yolo: false,
483 tls_cert: None,
484 tls_key: None,
485 };
486 let result = dispatch(Commands::Serve(args), &MockRisky).await;
487 assert!(result.is_ok());
488 }
489
490 #[tokio::test]
491 async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
492 let args = commands::agent_client::AgentClientArgs::default();
493 let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
494 assert!(result.is_ok());
495 }
496
497 #[tokio::test]
500 async fn dispatch_create_variant_is_routed() {
501 let dir = tempfile::tempdir().unwrap();
504 let args = commands::create::CreateArgs {
505 name: dir.path().to_str().unwrap().to_string(),
506 ..create_args()
507 };
508 let result = dispatch(Commands::Create(args), &MockRisky).await;
509 assert!(result.is_err());
510 }
511
512 #[tokio::test]
513 async fn dispatch_list_variant_is_routed() {
514 crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
517 let args = commands::list::ListArgs {
518 filter: commands::list::ListFilter::All,
519 json: false,
520 };
521 let result = dispatch(Commands::List(args), &MockRisky).await;
522 assert!(result.is_ok());
523 })
524 .await;
525 }
526
527 #[tokio::test]
528 async fn dispatch_add_variant_is_routed() {
529 let args = commands::add::AddArgs {
530 package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
531 };
532 let result = crate::config::with_isolated_config_path_async("dispatch-add", |_| {
536 dispatch(Commands::Add(args), &MockRisky)
537 })
538 .await;
539 assert!(result.is_err());
540 }
541
542 #[tokio::test]
543 async fn dispatch_remove_variant_is_routed() {
544 let args = commands::remove::RemoveArgs {
545 name: "definitely-not-an-installed-agent-xyz".to_string(),
546 };
547 let result = dispatch(Commands::Remove(args), &MockRisky).await;
548 assert!(result.is_err());
549 }
550
551 #[tokio::test]
552 async fn dispatch_test_variant_is_routed() {
553 let dir = tempfile::tempdir().unwrap();
554 let args = commands::test::TestArgs {
555 path: Some(dir.path().to_str().unwrap().to_string()),
556 filter: None,
557 dry_run: true,
558 };
559 let result = dispatch(Commands::Test(args), &MockRisky).await;
560 assert!(result.is_err());
561 }
562
563 #[tokio::test]
564 async fn dispatch_pack_variant_is_routed() {
565 let dir = tempfile::tempdir().unwrap();
566 let args = commands::pack::PackArgs {
567 path: Some(dir.path().to_str().unwrap().to_string()),
568 output: None,
569 };
570 let result = dispatch(Commands::Pack(args), &MockRisky).await;
571 assert!(result.is_err());
572 }
573
574 #[tokio::test]
575 async fn dispatch_models_variant_is_routed() {
576 crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
577 let args = commands::models::ModelsArgs {
578 command: commands::models::ModelsCommand::List(commands::models::ListArgs {
579 provider: None,
580 remote: false,
581 all: false,
582 json: false,
583 }),
584 };
585 let result = dispatch(Commands::Models(args), &MockRisky).await;
586 assert!(result.is_ok());
587 })
588 .await;
589 }
590
591 #[tokio::test]
592 async fn dispatch_validate_variant_is_routed() {
593 crate::config::with_isolated_config_path_async("dispatch-validate", |_| async {
597 let dir = tempfile::tempdir().unwrap();
598 let args = commands::validate::ValidateArgs {
599 path: dir
600 .path()
601 .join("does-not-exist")
602 .to_str()
603 .unwrap()
604 .to_string(),
605 deny_warnings: false,
606 json: false,
607 };
608 let result = dispatch(Commands::Validate(args), &MockRisky).await;
609 assert!(result.is_err());
610 })
611 .await;
612 }
613
614 #[tokio::test]
615 async fn dispatch_tools_variant_is_routed() {
616 let home = tempfile::tempdir().unwrap();
619 let result = temp_env::async_with_vars(
620 [("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
621 async {
622 let args = commands::tools::ToolsArgs { json: false };
623 dispatch(Commands::Tools(args), &MockRisky).await
624 },
625 )
626 .await;
627 assert!(result.is_ok());
628 }
629
630 #[tokio::test]
631 async fn dispatch_routes_stages() {
632 let result = dispatch(
635 Commands::Stages(commands::stages::StagesArgs {
636 run_id: "no-such-run".to_string(),
637 json: false,
638 regions: false,
639 }),
640 &MockRisky,
641 )
642 .await;
643 assert!(result.is_err(), "no ledger for a run that never ran");
644 }
645
646 #[tokio::test]
647 async fn dispatch_context_variant_is_routed() {
648 let args = commands::context::ContextArgs {
650 run_id: "no-such-run-xyzzy".to_string(),
651 json: false,
652 full: false,
653 };
654 let result = dispatch(Commands::Context(args), &MockRisky).await;
655 assert!(result.is_err());
656 }
657
658 #[tokio::test]
659 async fn dispatch_result_variant_is_routed() {
660 let args = commands::result::ResultArgs {
663 run_id: "no-such-run-xyzzy".to_string(),
664 json: false,
665 raw: false,
666 };
667 let result = dispatch(Commands::Result(args), &MockRisky).await;
668 assert!(result.is_err());
669 }
670
671 #[tokio::test]
672 async fn dispatch_approvals_variant_is_routed() {
673 let home = tempfile::tempdir().unwrap();
676 let config = home.path().join("config.toml");
677 let result = temp_env::async_with_vars(
678 [
679 ("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
680 ("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
681 ],
682 async {
683 let args = commands::approvals::ApprovalsArgs {
686 command: commands::approvals::ApprovalsCommand::Safe(
687 commands::approvals::SafeArgs {
688 agent: Some("coder".to_string()),
689 json: true,
690 },
691 ),
692 };
693 dispatch(Commands::Approvals(args), &MockRisky).await
694 },
695 )
696 .await;
697 assert!(result.is_ok());
698 }
699
700 #[tokio::test]
704 async fn dispatch_approvals_surfaces_a_broken_config() {
705 let home = tempfile::tempdir().unwrap();
706 let config = home.path().join("config.toml");
707 std::fs::write(&config, "this is not = = toml").unwrap();
708 let result = temp_env::async_with_vars(
709 [
710 ("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
711 ("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
712 ],
713 async {
714 let args = commands::approvals::ApprovalsArgs {
715 command: commands::approvals::ApprovalsCommand::Safe(
716 commands::approvals::SafeArgs {
717 agent: None,
718 json: false,
719 },
720 ),
721 };
722 dispatch(Commands::Approvals(args), &MockRisky).await
723 },
724 )
725 .await;
726 assert!(result.is_err());
727 }
728
729 #[tokio::test]
730 async fn dispatch_policy_list_variant_is_routed() {
731 let args = commands::policy::PolicyArgs {
732 command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
733 };
734 let result = dispatch(Commands::Policy(args), &MockRisky).await;
735 assert!(result.is_ok());
736 }
737
738 #[tokio::test]
739 async fn dispatch_policy_test_variant_is_routed() {
740 let args = commands::policy::PolicyArgs {
741 command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
742 tool: "shell".to_string(),
743 target: None,
744 taint: "public".to_string(),
745 }),
746 };
747 let result = dispatch(Commands::Policy(args), &MockRisky).await;
748 assert!(result.is_ok());
749 }
750}