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 #[command(long_about = commands::update::UPDATE_LONG_ABOUT)]
121 Update(commands::update::UpdateArgs),
122}
123
124pub trait RiskyExecutors {
132 fn run(
143 &self,
144 args: commands::run::RunArgs,
145 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
146 fn ps(
148 &self,
149 args: commands::ps::PsArgs,
150 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
151 fn msg(
153 &self,
154 args: commands::ctl::MsgArgs,
155 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
156 fn cancel(
158 &self,
159 args: commands::ctl::CancelArgs,
160 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
161 fn pause(
163 &self,
164 args: commands::ctl::PauseArgs,
165 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
166 fn resume(
168 &self,
169 args: commands::ctl::ResumeArgs,
170 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
171 fn respond(
173 &self,
174 args: commands::ctl::RespondArgs,
175 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
176 fn doctor(
179 &self,
180 args: commands::doctor::DoctorArgs,
181 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
182 fn setup(
184 &self,
185 args: commands::setup::SetupArgs,
186 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
187 fn dashboard(
189 &self,
190 args: commands::dashboard::DashboardArgs,
191 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
192 fn serve(
194 &self,
195 args: commands::serve::ServeArgs,
196 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
197 fn agent_client(
200 &self,
201 args: commands::agent_client::AgentClientArgs,
202 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
203 fn daemon(
205 &self,
206 args: commands::daemon::DaemonArgs,
207 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
208 fn mcp(
210 &self,
211 args: commands::mcp::McpArgs,
212 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
213
214 fn auth(
216 &self,
217 args: commands::auth::AuthArgs,
218 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
219
220 fn update(
223 &self,
224 args: commands::update::UpdateArgs,
225 ) -> impl std::future::Future<Output = anyhow::Result<()>>;
226}
227
228pub fn apply_region_flags(
232 command: &mut Commands,
233 regions: std::collections::HashMap<String, String>,
234) {
235 if let Commands::Run(args) = command {
236 args.regions = regions;
237 }
238}
239
240pub async fn dispatch(command: Commands, ex: &impl RiskyExecutors) -> anyhow::Result<()> {
244 match command {
245 Commands::Create(args) => commands::create::execute(args).await,
246 Commands::Setup(args) => ex.setup(args).await,
247 Commands::Run(args) => ex.run(args).await,
248 Commands::Ps(args) => ex.ps(args).await,
249 Commands::Msg(args) => ex.msg(args).await,
250 Commands::Cancel(args) => ex.cancel(args).await,
251 Commands::Pause(args) => ex.pause(args).await,
252 Commands::Resume(args) => ex.resume(args).await,
253 Commands::Respond(args) => ex.respond(args).await,
254 Commands::Doctor(args) => ex.doctor(args).await,
255 Commands::List(args) => commands::list::execute(args).await,
256 Commands::Add(args) => commands::add::execute(args).await,
257 Commands::Remove(args) => commands::remove::execute(args).await,
258 Commands::Test(args) => commands::test::execute(args).await,
259 Commands::Pack(args) => commands::pack::execute(args).await,
260 Commands::Dashboard(args) => ex.dashboard(args).await,
261 Commands::Models(args) => commands::models::execute(args).await,
262 Commands::Validate(args) => commands::validate::execute(args).await,
263 Commands::Tools(args) => commands::tools::execute(args).await,
264 Commands::Approvals(args) => commands::approvals::execute(args).await,
265 Commands::Policy(args) => commands::policy::execute(args).await,
266 Commands::Serve(args) => ex.serve(args).await,
267 Commands::AgentClient(args) => ex.agent_client(args).await,
268 Commands::Daemon(args) => ex.daemon(args).await,
269 Commands::Context(args) => commands::context::execute(args).await,
270 Commands::Stages(args) => commands::stages::execute(args).await,
271 Commands::Result(args) => commands::result::execute(args).await,
272 Commands::Mcp(args) => ex.mcp(args).await,
273 Commands::Auth(args) => ex.auth(args).await,
274 Commands::Update(args) => ex.update(args).await,
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 struct MockRisky;
286
287 impl RiskyExecutors for MockRisky {
288 async fn run(&self, _args: commands::run::RunArgs) -> anyhow::Result<()> {
289 Ok(())
290 }
291 async fn ps(&self, _args: commands::ps::PsArgs) -> anyhow::Result<()> {
292 Ok(())
293 }
294 async fn msg(&self, _args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
295 Ok(())
296 }
297 async fn respond(&self, _args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
298 Ok(())
299 }
300 async fn doctor(&self, _args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
301 Ok(())
302 }
303 async fn cancel(&self, _args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
304 Ok(())
305 }
306 async fn pause(&self, _args: commands::ctl::PauseArgs) -> anyhow::Result<()> {
307 Ok(())
308 }
309 async fn resume(&self, _args: commands::ctl::ResumeArgs) -> anyhow::Result<()> {
310 Ok(())
311 }
312 async fn setup(&self, _args: commands::setup::SetupArgs) -> anyhow::Result<()> {
313 Ok(())
314 }
315 async fn dashboard(&self, _args: commands::dashboard::DashboardArgs) -> anyhow::Result<()> {
316 Ok(())
317 }
318 async fn serve(&self, _args: commands::serve::ServeArgs) -> anyhow::Result<()> {
319 Ok(())
320 }
321 async fn agent_client(
322 &self,
323 _args: commands::agent_client::AgentClientArgs,
324 ) -> anyhow::Result<()> {
325 Ok(())
326 }
327 async fn daemon(&self, _args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
328 Ok(())
329 }
330 async fn auth(&self, _args: commands::auth::AuthArgs) -> anyhow::Result<()> {
331 Ok(())
332 }
333
334 async fn mcp(&self, _args: commands::mcp::McpArgs) -> anyhow::Result<()> {
335 Ok(())
336 }
337
338 async fn update(&self, _args: commands::update::UpdateArgs) -> anyhow::Result<()> {
339 Ok(())
340 }
341 }
342
343 fn create_args() -> commands::create::CreateArgs {
344 commands::create::CreateArgs {
345 name: "unused".to_string(),
346 template: "default".to_string(),
347 }
348 }
349
350 #[test]
353 fn apply_region_flags_populates_run_and_noops_other_commands() {
354 let mut run = Commands::Run(commands::run::RunArgs::default());
355 let flags = std::collections::HashMap::from([("criteria".to_string(), "safe".to_string())]);
356 apply_region_flags(&mut run, flags);
357 assert!(
358 matches!(&run, Commands::Run(a) if a.regions.get("criteria").map(String::as_str) == Some("safe")),
359 "region flag was injected into the Run args"
360 );
361 let mut other = Commands::Ps(commands::ps::PsArgs::default());
365 apply_region_flags(&mut other, std::collections::HashMap::new());
366 }
367
368 #[tokio::test]
371 async fn dispatch_run_variant_is_routed_through_the_executor() {
372 let result = dispatch(Commands::Run(commands::run::RunArgs::default()), &MockRisky).await;
373 assert!(result.is_ok());
374 }
375
376 #[tokio::test]
377 async fn dispatch_setup_variant_is_routed_through_the_executor() {
378 let args = commands::setup::SetupArgs {
379 non_interactive: true,
380 no_verify: false,
381 install_agents: false,
382 anthropic_key: None,
383 openai_key: None,
384 google_key: None,
385 openrouter_key: None,
386 ollama_url: None,
387 default_model: None,
388 claude_code: None,
389 claude_code_effort: None,
390 };
391 let result = dispatch(Commands::Setup(args), &MockRisky).await;
392 assert!(result.is_ok());
393 }
394
395 #[tokio::test]
396 async fn dispatch_dashboard_variant_is_routed_through_the_executor() {
397 let args = commands::dashboard::DashboardArgs {};
398 let result = dispatch(Commands::Dashboard(args), &MockRisky).await;
399 assert!(result.is_ok());
400 }
401
402 #[tokio::test]
403 async fn dispatch_msg_variant_is_routed_through_the_executor() {
404 let args = commands::ctl::MsgArgs {
405 agent_id: "a".to_string(),
406 content: "c".to_string(),
407 };
408 assert!(dispatch(Commands::Msg(args), &MockRisky).await.is_ok());
409 }
410
411 #[tokio::test]
412 async fn dispatch_respond_variant_is_routed_through_the_executor() {
413 let args = commands::ctl::RespondArgs {
414 request_id: None,
415 value: None,
416 choice: None,
417 approve: false,
418 deny: false,
419 session: false,
420 stage: false,
421 json: false,
422 };
423 assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
424 }
425
426 #[tokio::test]
427 async fn dispatch_doctor_variant_is_routed_through_the_executor() {
428 let args = commands::doctor::DoctorArgs::default();
431 assert!(dispatch(Commands::Doctor(args), &MockRisky).await.is_ok());
432 }
433
434 #[tokio::test]
435 async fn dispatch_cancel_variant_is_routed_through_the_executor() {
436 let args = commands::ctl::CancelArgs {
437 run_id: "r".to_string(),
438 force: false,
439 };
440 assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
441 }
442
443 #[tokio::test]
444 async fn dispatch_pause_variant_is_routed_through_the_executor() {
445 let args = commands::ctl::PauseArgs {
446 run_id: "r".to_string(),
447 };
448 assert!(dispatch(Commands::Pause(args), &MockRisky).await.is_ok());
449 }
450
451 #[tokio::test]
452 async fn dispatch_resume_variant_is_routed_through_the_executor() {
453 let args = commands::ctl::ResumeArgs {
454 run_id: "r".to_string(),
455 };
456 assert!(dispatch(Commands::Resume(args), &MockRisky).await.is_ok());
457 }
458
459 #[tokio::test]
460 async fn dispatch_ps_variant_is_routed_through_the_executor() {
461 let result = dispatch(Commands::Ps(commands::ps::PsArgs::default()), &MockRisky).await;
462 assert!(result.is_ok());
463 }
464
465 #[tokio::test]
466 async fn dispatch_daemon_variant_is_routed_through_the_executor() {
467 let args = commands::daemon::DaemonArgs {
468 action: None,
469 socket: None,
470 };
471 let result = dispatch(Commands::Daemon(args), &MockRisky).await;
472 assert!(result.is_ok());
473 }
474
475 #[tokio::test]
476 async fn dispatch_auth_variant_is_routed_through_the_executor() {
477 let args = commands::auth::AuthArgs::status_for_test();
478 let result = dispatch(Commands::Auth(args), &MockRisky).await;
479 assert!(result.is_ok());
480 }
481
482 #[tokio::test]
483 async fn dispatch_update_variant_is_routed_through_the_executor() {
484 let args = commands::update::UpdateArgs::default();
488 let result = dispatch(Commands::Update(args), &MockRisky).await;
489 assert!(result.is_ok());
490 }
491
492 #[tokio::test]
493 async fn dispatch_mcp_variant_is_routed_through_the_executor() {
494 let args = commands::mcp::McpArgs::list_for_test();
495 let result = dispatch(Commands::Mcp(args), &MockRisky).await;
496 assert!(result.is_ok());
497 }
498
499 #[tokio::test]
500 async fn dispatch_serve_variant_is_routed_through_the_executor() {
501 let args = commands::serve::ServeArgs {
502 port: 0,
503 host: "127.0.0.1".to_string(),
504 cors: None,
505 token: Some("test-token".to_string()),
506 allow_admin: false,
507 workdir_root: None,
508 no_remote_yolo: false,
509 tls_cert: None,
510 tls_key: None,
511 };
512 let result = dispatch(Commands::Serve(args), &MockRisky).await;
513 assert!(result.is_ok());
514 }
515
516 #[tokio::test]
517 async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
518 let args = commands::agent_client::AgentClientArgs::default();
519 let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
520 assert!(result.is_ok());
521 }
522
523 #[tokio::test]
526 async fn dispatch_create_variant_is_routed() {
527 let dir = tempfile::tempdir().unwrap();
530 let args = commands::create::CreateArgs {
531 name: dir.path().to_str().unwrap().to_string(),
532 ..create_args()
533 };
534 let result = dispatch(Commands::Create(args), &MockRisky).await;
535 assert!(result.is_err());
536 }
537
538 #[tokio::test]
539 async fn dispatch_list_variant_is_routed() {
540 crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
543 let args = commands::list::ListArgs {
544 filter: commands::list::ListFilter::All,
545 json: false,
546 };
547 let result = dispatch(Commands::List(args), &MockRisky).await;
548 assert!(result.is_ok());
549 })
550 .await;
551 }
552
553 #[tokio::test]
554 async fn dispatch_add_variant_is_routed() {
555 let args = commands::add::AddArgs {
556 package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
557 };
558 let result = crate::config::with_isolated_config_path_async("dispatch-add", |_| {
562 dispatch(Commands::Add(args), &MockRisky)
563 })
564 .await;
565 assert!(result.is_err());
566 }
567
568 #[tokio::test]
569 async fn dispatch_remove_variant_is_routed() {
570 let args = commands::remove::RemoveArgs {
571 name: "definitely-not-an-installed-agent-xyz".to_string(),
572 };
573 let result = dispatch(Commands::Remove(args), &MockRisky).await;
574 assert!(result.is_err());
575 }
576
577 #[tokio::test]
578 async fn dispatch_test_variant_is_routed() {
579 let dir = tempfile::tempdir().unwrap();
580 let args = commands::test::TestArgs {
581 path: Some(dir.path().to_str().unwrap().to_string()),
582 filter: None,
583 dry_run: true,
584 };
585 let result = dispatch(Commands::Test(args), &MockRisky).await;
586 assert!(result.is_err());
587 }
588
589 #[tokio::test]
590 async fn dispatch_pack_variant_is_routed() {
591 let dir = tempfile::tempdir().unwrap();
592 let args = commands::pack::PackArgs {
593 path: Some(dir.path().to_str().unwrap().to_string()),
594 output: None,
595 };
596 let result = dispatch(Commands::Pack(args), &MockRisky).await;
597 assert!(result.is_err());
598 }
599
600 #[tokio::test]
601 async fn dispatch_models_variant_is_routed() {
602 crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
603 let args = commands::models::ModelsArgs {
604 command: commands::models::ModelsCommand::List(commands::models::ListArgs {
605 provider: None,
606 remote: false,
607 all: false,
608 json: false,
609 }),
610 };
611 let result = dispatch(Commands::Models(args), &MockRisky).await;
612 assert!(result.is_ok());
613 })
614 .await;
615 }
616
617 #[tokio::test]
618 async fn dispatch_validate_variant_is_routed() {
619 crate::config::with_isolated_config_path_async("dispatch-validate", |_| async {
623 let dir = tempfile::tempdir().unwrap();
624 let args = commands::validate::ValidateArgs {
625 path: dir
626 .path()
627 .join("does-not-exist")
628 .to_str()
629 .unwrap()
630 .to_string(),
631 deny_warnings: false,
632 json: false,
633 };
634 let result = dispatch(Commands::Validate(args), &MockRisky).await;
635 assert!(result.is_err());
636 })
637 .await;
638 }
639
640 #[tokio::test]
641 async fn dispatch_tools_variant_is_routed() {
642 let home = tempfile::tempdir().unwrap();
645 let result = temp_env::async_with_vars(
646 [("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
647 async {
648 let args = commands::tools::ToolsArgs { json: false };
649 dispatch(Commands::Tools(args), &MockRisky).await
650 },
651 )
652 .await;
653 assert!(result.is_ok());
654 }
655
656 #[tokio::test]
657 async fn dispatch_routes_stages() {
658 let result = dispatch(
661 Commands::Stages(commands::stages::StagesArgs {
662 run_id: "no-such-run".to_string(),
663 json: false,
664 regions: false,
665 }),
666 &MockRisky,
667 )
668 .await;
669 assert!(result.is_err(), "no ledger for a run that never ran");
670 }
671
672 #[tokio::test]
673 async fn dispatch_context_variant_is_routed() {
674 let args = commands::context::ContextArgs {
676 run_id: "no-such-run-xyzzy".to_string(),
677 json: false,
678 full: false,
679 };
680 let result = dispatch(Commands::Context(args), &MockRisky).await;
681 assert!(result.is_err());
682 }
683
684 #[tokio::test]
685 async fn dispatch_result_variant_is_routed() {
686 let args = commands::result::ResultArgs {
689 run_id: "no-such-run-xyzzy".to_string(),
690 json: false,
691 raw: false,
692 };
693 let result = dispatch(Commands::Result(args), &MockRisky).await;
694 assert!(result.is_err());
695 }
696
697 #[tokio::test]
698 async fn dispatch_approvals_variant_is_routed() {
699 let home = tempfile::tempdir().unwrap();
702 let config = home.path().join("config.toml");
703 let result = temp_env::async_with_vars(
704 [
705 ("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
706 ("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
707 ],
708 async {
709 let args = commands::approvals::ApprovalsArgs {
712 command: commands::approvals::ApprovalsCommand::Safe(
713 commands::approvals::SafeArgs {
714 agent: Some("coder".to_string()),
715 json: true,
716 },
717 ),
718 };
719 dispatch(Commands::Approvals(args), &MockRisky).await
720 },
721 )
722 .await;
723 assert!(result.is_ok());
724 }
725
726 #[tokio::test]
730 async fn dispatch_approvals_surfaces_a_broken_config() {
731 let home = tempfile::tempdir().unwrap();
732 let config = home.path().join("config.toml");
733 std::fs::write(&config, "this is not = = toml").unwrap();
734 let result = temp_env::async_with_vars(
735 [
736 ("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
737 ("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
738 ],
739 async {
740 let args = commands::approvals::ApprovalsArgs {
741 command: commands::approvals::ApprovalsCommand::Safe(
742 commands::approvals::SafeArgs {
743 agent: None,
744 json: false,
745 },
746 ),
747 };
748 dispatch(Commands::Approvals(args), &MockRisky).await
749 },
750 )
751 .await;
752 assert!(result.is_err());
753 }
754
755 #[tokio::test]
756 async fn dispatch_policy_list_variant_is_routed() {
757 let args = commands::policy::PolicyArgs {
758 command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
759 };
760 let result = dispatch(Commands::Policy(args), &MockRisky).await;
761 assert!(result.is_ok());
762 }
763
764 #[tokio::test]
765 async fn dispatch_policy_test_variant_is_routed() {
766 let args = commands::policy::PolicyArgs {
767 command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
768 tool: "shell".to_string(),
769 target: None,
770 taint: "public".to_string(),
771 }),
772 };
773 let result = dispatch(Commands::Policy(args), &MockRisky).await;
774 assert!(result.is_ok());
775 }
776}