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 json: false,
343 };
344 assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
345 }
346
347 #[tokio::test]
348 async fn dispatch_doctor_variant_is_routed_through_the_executor() {
349 let args = commands::doctor::DoctorArgs::default();
352 assert!(dispatch(Commands::Doctor(args), &MockRisky).await.is_ok());
353 }
354
355 #[tokio::test]
356 async fn dispatch_cancel_variant_is_routed_through_the_executor() {
357 let args = commands::ctl::CancelArgs {
358 run_id: "r".to_string(),
359 force: false,
360 };
361 assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
362 }
363
364 #[tokio::test]
365 async fn dispatch_pause_variant_is_routed_through_the_executor() {
366 let args = commands::ctl::PauseArgs {
367 run_id: "r".to_string(),
368 };
369 assert!(dispatch(Commands::Pause(args), &MockRisky).await.is_ok());
370 }
371
372 #[tokio::test]
373 async fn dispatch_resume_variant_is_routed_through_the_executor() {
374 let args = commands::ctl::ResumeArgs {
375 run_id: "r".to_string(),
376 };
377 assert!(dispatch(Commands::Resume(args), &MockRisky).await.is_ok());
378 }
379
380 #[tokio::test]
381 async fn dispatch_ps_variant_is_routed_through_the_executor() {
382 let result = dispatch(Commands::Ps(commands::ps::PsArgs::default()), &MockRisky).await;
383 assert!(result.is_ok());
384 }
385
386 #[tokio::test]
387 async fn dispatch_daemon_variant_is_routed_through_the_executor() {
388 let args = commands::daemon::DaemonArgs {
389 action: None,
390 socket: None,
391 };
392 let result = dispatch(Commands::Daemon(args), &MockRisky).await;
393 assert!(result.is_ok());
394 }
395
396 #[tokio::test]
397 async fn dispatch_auth_variant_is_routed_through_the_executor() {
398 let args = commands::auth::AuthArgs::status_for_test();
399 let result = dispatch(Commands::Auth(args), &MockRisky).await;
400 assert!(result.is_ok());
401 }
402
403 #[tokio::test]
404 async fn dispatch_mcp_variant_is_routed_through_the_executor() {
405 let args = commands::mcp::McpArgs::list_for_test();
406 let result = dispatch(Commands::Mcp(args), &MockRisky).await;
407 assert!(result.is_ok());
408 }
409
410 #[tokio::test]
411 async fn dispatch_serve_variant_is_routed_through_the_executor() {
412 let args = commands::serve::ServeArgs {
413 port: 0,
414 host: "127.0.0.1".to_string(),
415 cors: None,
416 token: Some("test-token".to_string()),
417 allow_admin: false,
418 workdir_root: None,
419 no_remote_yolo: false,
420 };
421 let result = dispatch(Commands::Serve(args), &MockRisky).await;
422 assert!(result.is_ok());
423 }
424
425 #[tokio::test]
426 async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
427 let args = commands::agent_client::AgentClientArgs::default();
428 let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
429 assert!(result.is_ok());
430 }
431
432 #[tokio::test]
435 async fn dispatch_create_variant_is_routed() {
436 let dir = tempfile::tempdir().unwrap();
439 let args = commands::create::CreateArgs {
440 name: dir.path().to_str().unwrap().to_string(),
441 ..create_args()
442 };
443 let result = dispatch(Commands::Create(args), &MockRisky).await;
444 assert!(result.is_err());
445 }
446
447 #[tokio::test]
448 async fn dispatch_list_variant_is_routed() {
449 crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
452 let args = commands::list::ListArgs {
453 filter: "all".to_string(),
454 json: false,
455 };
456 let result = dispatch(Commands::List(args), &MockRisky).await;
457 assert!(result.is_ok());
458 })
459 .await;
460 }
461
462 #[tokio::test]
463 async fn dispatch_add_variant_is_routed() {
464 let args = commands::add::AddArgs {
465 package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
466 };
467 let result = crate::config::with_isolated_config_path_async("dispatch-add", |_| {
471 dispatch(Commands::Add(args), &MockRisky)
472 })
473 .await;
474 assert!(result.is_err());
475 }
476
477 #[tokio::test]
478 async fn dispatch_remove_variant_is_routed() {
479 let args = commands::remove::RemoveArgs {
480 name: "definitely-not-an-installed-agent-xyz".to_string(),
481 };
482 let result = dispatch(Commands::Remove(args), &MockRisky).await;
483 assert!(result.is_err());
484 }
485
486 #[tokio::test]
487 async fn dispatch_test_variant_is_routed() {
488 let dir = tempfile::tempdir().unwrap();
489 let args = commands::test::TestArgs {
490 path: Some(dir.path().to_str().unwrap().to_string()),
491 filter: None,
492 dry_run: true,
493 };
494 let result = dispatch(Commands::Test(args), &MockRisky).await;
495 assert!(result.is_err());
496 }
497
498 #[tokio::test]
499 async fn dispatch_pack_variant_is_routed() {
500 let dir = tempfile::tempdir().unwrap();
501 let args = commands::pack::PackArgs {
502 path: Some(dir.path().to_str().unwrap().to_string()),
503 output: None,
504 };
505 let result = dispatch(Commands::Pack(args), &MockRisky).await;
506 assert!(result.is_err());
507 }
508
509 #[tokio::test]
510 async fn dispatch_models_variant_is_routed() {
511 crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
512 let args = commands::models::ModelsArgs {
513 command: commands::models::ModelsCommand::List(commands::models::ListArgs {
514 provider: None,
515 remote: false,
516 all: false,
517 json: false,
518 }),
519 };
520 let result = dispatch(Commands::Models(args), &MockRisky).await;
521 assert!(result.is_ok());
522 })
523 .await;
524 }
525
526 #[tokio::test]
527 async fn dispatch_validate_variant_is_routed() {
528 crate::config::with_isolated_config_path_async("dispatch-validate", |_| async {
532 let dir = tempfile::tempdir().unwrap();
533 let args = commands::validate::ValidateArgs {
534 path: dir
535 .path()
536 .join("does-not-exist")
537 .to_str()
538 .unwrap()
539 .to_string(),
540 deny_warnings: false,
541 json: false,
542 };
543 let result = dispatch(Commands::Validate(args), &MockRisky).await;
544 assert!(result.is_err());
545 })
546 .await;
547 }
548
549 #[tokio::test]
550 async fn dispatch_tools_variant_is_routed() {
551 let home = tempfile::tempdir().unwrap();
554 let result = temp_env::async_with_vars(
555 [("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
556 async {
557 let args = commands::tools::ToolsArgs { json: false };
558 dispatch(Commands::Tools(args), &MockRisky).await
559 },
560 )
561 .await;
562 assert!(result.is_ok());
563 }
564
565 #[tokio::test]
566 async fn dispatch_context_variant_is_routed() {
567 let args = commands::context::ContextArgs {
569 run_id: "no-such-run-xyzzy".to_string(),
570 json: false,
571 full: false,
572 };
573 let result = dispatch(Commands::Context(args), &MockRisky).await;
574 assert!(result.is_err());
575 }
576
577 #[tokio::test]
578 async fn dispatch_policy_list_variant_is_routed() {
579 let args = commands::policy::PolicyArgs {
580 command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
581 };
582 let result = dispatch(Commands::Policy(args), &MockRisky).await;
583 assert!(result.is_ok());
584 }
585
586 #[tokio::test]
587 async fn dispatch_policy_test_variant_is_routed() {
588 let args = commands::policy::PolicyArgs {
589 command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
590 tool: "shell".to_string(),
591 target: None,
592 taint: "public".to_string(),
593 }),
594 };
595 let result = dispatch(Commands::Policy(args), &MockRisky).await;
596 assert!(result.is_ok());
597 }
598}