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 Ps(commands::ps::PsArgs),
37
38 Msg(commands::ctl::MsgArgs),
40
41 #[command(alias = "kill")]
43 Cancel(commands::ctl::CancelArgs),
44
45 Respond(commands::ctl::RespondArgs),
47
48 List(commands::list::ListArgs),
50
51 Add(commands::add::AddArgs),
53
54 Remove(commands::remove::RemoveArgs),
56
57 Test(commands::test::TestArgs),
59
60 Pack(commands::pack::PackArgs),
62
63 #[command(name = "dash")]
65 Dashboard(commands::dashboard::DashboardArgs),
66
67 Models(commands::models::ModelsArgs),
69
70 Validate(commands::validate::ValidateArgs),
72
73 Tools(commands::tools::ToolsArgs),
75
76 Policy(commands::policy::PolicyArgs),
78
79 Serve(commands::serve::ServeArgs),
81
82 #[command(name = "agent-client")]
84 AgentClient(commands::agent_client::AgentClientArgs),
85
86 Daemon(commands::daemon::DaemonArgs),
88
89 Context(commands::context::ContextArgs),
91
92 Mcp(commands::mcp::McpArgs),
94
95 Auth(commands::auth::AuthArgs),
97}
98
99#[allow(async_fn_in_trait)]
107pub trait RiskyExecutors {
108 async fn run(&self, args: commands::run::RunArgs) -> anyhow::Result<()>;
111 async fn ps(&self, args: commands::ps::PsArgs) -> anyhow::Result<()>;
113 async fn msg(&self, args: commands::ctl::MsgArgs) -> anyhow::Result<()>;
115 async fn cancel(&self, args: commands::ctl::CancelArgs) -> anyhow::Result<()>;
117 async fn respond(&self, args: commands::ctl::RespondArgs) -> anyhow::Result<()>;
119 async fn setup(&self, args: commands::setup::SetupArgs) -> anyhow::Result<()>;
121 async fn dashboard(&self, args: commands::dashboard::DashboardArgs) -> anyhow::Result<()>;
123 async fn serve(&self, args: commands::serve::ServeArgs) -> anyhow::Result<()>;
125 async fn agent_client(
128 &self,
129 args: commands::agent_client::AgentClientArgs,
130 ) -> anyhow::Result<()>;
131 async fn daemon(&self, args: commands::daemon::DaemonArgs) -> anyhow::Result<()>;
133 async fn mcp(&self, args: commands::mcp::McpArgs) -> anyhow::Result<()>;
135
136 async fn auth(&self, args: commands::auth::AuthArgs) -> anyhow::Result<()>;
138}
139
140pub fn apply_region_flags(
144 command: &mut Commands,
145 regions: std::collections::HashMap<String, String>,
146) {
147 if let Commands::Run(args) = command {
148 args.regions = regions;
149 }
150}
151
152pub async fn dispatch(command: Commands, ex: &impl RiskyExecutors) -> anyhow::Result<()> {
156 match command {
157 Commands::Create(args) => commands::create::execute(args).await,
158 Commands::Setup(args) => ex.setup(args).await,
159 Commands::Run(args) => ex.run(args).await,
160 Commands::Ps(args) => ex.ps(args).await,
161 Commands::Msg(args) => ex.msg(args).await,
162 Commands::Cancel(args) => ex.cancel(args).await,
163 Commands::Respond(args) => ex.respond(args).await,
164 Commands::List(args) => commands::list::execute(args).await,
165 Commands::Add(args) => commands::add::execute(args).await,
166 Commands::Remove(args) => commands::remove::execute(args).await,
167 Commands::Test(args) => commands::test::execute(args).await,
168 Commands::Pack(args) => commands::pack::execute(args).await,
169 Commands::Dashboard(args) => ex.dashboard(args).await,
170 Commands::Models(args) => commands::models::execute(args).await,
171 Commands::Validate(args) => commands::validate::execute(args).await,
172 Commands::Tools(args) => commands::tools::execute(args).await,
173 Commands::Policy(args) => commands::policy::execute(args).await,
174 Commands::Serve(args) => ex.serve(args).await,
175 Commands::AgentClient(args) => ex.agent_client(args).await,
176 Commands::Daemon(args) => ex.daemon(args).await,
177 Commands::Context(args) => commands::context::execute(args).await,
178 Commands::Mcp(args) => ex.mcp(args).await,
179 Commands::Auth(args) => ex.auth(args).await,
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 struct MockRisky;
191
192 impl RiskyExecutors for MockRisky {
193 async fn run(&self, _args: commands::run::RunArgs) -> anyhow::Result<()> {
194 Ok(())
195 }
196 async fn ps(&self, _args: commands::ps::PsArgs) -> anyhow::Result<()> {
197 Ok(())
198 }
199 async fn msg(&self, _args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
200 Ok(())
201 }
202 async fn respond(&self, _args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
203 Ok(())
204 }
205 async fn cancel(&self, _args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
206 Ok(())
207 }
208 async fn setup(&self, _args: commands::setup::SetupArgs) -> anyhow::Result<()> {
209 Ok(())
210 }
211 async fn dashboard(&self, _args: commands::dashboard::DashboardArgs) -> anyhow::Result<()> {
212 Ok(())
213 }
214 async fn serve(&self, _args: commands::serve::ServeArgs) -> anyhow::Result<()> {
215 Ok(())
216 }
217 async fn agent_client(
218 &self,
219 _args: commands::agent_client::AgentClientArgs,
220 ) -> anyhow::Result<()> {
221 Ok(())
222 }
223 async fn daemon(&self, _args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
224 Ok(())
225 }
226 async fn auth(&self, _args: commands::auth::AuthArgs) -> anyhow::Result<()> {
227 Ok(())
228 }
229
230 async fn mcp(&self, _args: commands::mcp::McpArgs) -> anyhow::Result<()> {
231 Ok(())
232 }
233 }
234
235 fn create_args() -> commands::create::CreateArgs {
236 commands::create::CreateArgs {
237 name: "unused".to_string(),
238 template: "software-engineer".to_string(),
239 }
240 }
241
242 #[test]
245 fn apply_region_flags_populates_run_and_noops_other_commands() {
246 let mut run = Commands::Run(commands::run::RunArgs::default());
247 let flags = std::collections::HashMap::from([("criteria".to_string(), "safe".to_string())]);
248 apply_region_flags(&mut run, flags);
249 assert!(
250 matches!(&run, Commands::Run(a) if a.regions.get("criteria").map(String::as_str) == Some("safe")),
251 "region flag was injected into the Run args"
252 );
253 let mut other = Commands::Ps(commands::ps::PsArgs::default());
257 apply_region_flags(&mut other, std::collections::HashMap::new());
258 }
259
260 #[tokio::test]
263 async fn dispatch_run_variant_is_routed_through_the_executor() {
264 let result = dispatch(Commands::Run(commands::run::RunArgs::default()), &MockRisky).await;
265 assert!(result.is_ok());
266 }
267
268 #[tokio::test]
269 async fn dispatch_setup_variant_is_routed_through_the_executor() {
270 let args = commands::setup::SetupArgs {
271 non_interactive: true,
272 no_verify: false,
273 install_agents: false,
274 anthropic_key: None,
275 openai_key: None,
276 google_key: None,
277 openrouter_key: None,
278 ollama_url: None,
279 default_model: None,
280 claude_code: None,
281 claude_code_effort: None,
282 };
283 let result = dispatch(Commands::Setup(args), &MockRisky).await;
284 assert!(result.is_ok());
285 }
286
287 #[tokio::test]
288 async fn dispatch_dashboard_variant_is_routed_through_the_executor() {
289 let args = commands::dashboard::DashboardArgs {};
290 let result = dispatch(Commands::Dashboard(args), &MockRisky).await;
291 assert!(result.is_ok());
292 }
293
294 #[tokio::test]
295 async fn dispatch_msg_variant_is_routed_through_the_executor() {
296 let args = commands::ctl::MsgArgs {
297 agent_id: "a".to_string(),
298 content: "c".to_string(),
299 };
300 assert!(dispatch(Commands::Msg(args), &MockRisky).await.is_ok());
301 }
302
303 #[tokio::test]
304 async fn dispatch_respond_variant_is_routed_through_the_executor() {
305 let args = commands::ctl::RespondArgs {
306 request_id: None,
307 value: None,
308 choice: None,
309 approve: false,
310 deny: false,
311 session: false,
312 };
313 assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
314 }
315
316 #[tokio::test]
317 async fn dispatch_cancel_variant_is_routed_through_the_executor() {
318 let args = commands::ctl::CancelArgs {
319 run_id: "r".to_string(),
320 force: false,
321 };
322 assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
323 }
324
325 #[tokio::test]
326 async fn dispatch_ps_variant_is_routed_through_the_executor() {
327 let result = dispatch(Commands::Ps(commands::ps::PsArgs {}), &MockRisky).await;
328 assert!(result.is_ok());
329 }
330
331 #[tokio::test]
332 async fn dispatch_daemon_variant_is_routed_through_the_executor() {
333 let args = commands::daemon::DaemonArgs {
334 action: None,
335 socket: None,
336 };
337 let result = dispatch(Commands::Daemon(args), &MockRisky).await;
338 assert!(result.is_ok());
339 }
340
341 #[tokio::test]
342 async fn dispatch_auth_variant_is_routed_through_the_executor() {
343 let args = commands::auth::AuthArgs::status_for_test();
344 let result = dispatch(Commands::Auth(args), &MockRisky).await;
345 assert!(result.is_ok());
346 }
347
348 #[tokio::test]
349 async fn dispatch_mcp_variant_is_routed_through_the_executor() {
350 let args = commands::mcp::McpArgs::list_for_test();
351 let result = dispatch(Commands::Mcp(args), &MockRisky).await;
352 assert!(result.is_ok());
353 }
354
355 #[tokio::test]
356 async fn dispatch_serve_variant_is_routed_through_the_executor() {
357 let args = commands::serve::ServeArgs {
358 port: 0,
359 host: "127.0.0.1".to_string(),
360 cors: None,
361 token: Some("test-token".to_string()),
362 allow_admin: false,
363 workdir_root: None,
364 no_remote_yolo: false,
365 };
366 let result = dispatch(Commands::Serve(args), &MockRisky).await;
367 assert!(result.is_ok());
368 }
369
370 #[tokio::test]
371 async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
372 let args = commands::agent_client::AgentClientArgs::default();
373 let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
374 assert!(result.is_ok());
375 }
376
377 #[tokio::test]
380 async fn dispatch_create_variant_is_routed() {
381 let dir = tempfile::tempdir().unwrap();
384 let args = commands::create::CreateArgs {
385 name: dir.path().to_str().unwrap().to_string(),
386 ..create_args()
387 };
388 let result = dispatch(Commands::Create(args), &MockRisky).await;
389 assert!(result.is_err());
390 }
391
392 #[tokio::test]
393 async fn dispatch_list_variant_is_routed() {
394 crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
397 let args = commands::list::ListArgs {
398 filter: "all".to_string(),
399 };
400 let result = dispatch(Commands::List(args), &MockRisky).await;
401 assert!(result.is_ok());
402 })
403 .await;
404 }
405
406 #[tokio::test]
407 async fn dispatch_add_variant_is_routed() {
408 let args = commands::add::AddArgs {
409 package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
410 };
411 let result = dispatch(Commands::Add(args), &MockRisky).await;
412 assert!(result.is_err());
413 }
414
415 #[tokio::test]
416 async fn dispatch_remove_variant_is_routed() {
417 let args = commands::remove::RemoveArgs {
418 name: "definitely-not-an-installed-agent-xyz".to_string(),
419 };
420 let result = dispatch(Commands::Remove(args), &MockRisky).await;
421 assert!(result.is_err());
422 }
423
424 #[tokio::test]
425 async fn dispatch_test_variant_is_routed() {
426 let dir = tempfile::tempdir().unwrap();
427 let args = commands::test::TestArgs {
428 path: Some(dir.path().to_str().unwrap().to_string()),
429 filter: None,
430 dry_run: true,
431 };
432 let result = dispatch(Commands::Test(args), &MockRisky).await;
433 assert!(result.is_err());
434 }
435
436 #[tokio::test]
437 async fn dispatch_pack_variant_is_routed() {
438 let dir = tempfile::tempdir().unwrap();
439 let args = commands::pack::PackArgs {
440 path: Some(dir.path().to_str().unwrap().to_string()),
441 output: None,
442 };
443 let result = dispatch(Commands::Pack(args), &MockRisky).await;
444 assert!(result.is_err());
445 }
446
447 #[tokio::test]
448 async fn dispatch_models_variant_is_routed() {
449 crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
450 let args = commands::models::ModelsArgs {
451 command: commands::models::ModelsCommand::List(commands::models::ListArgs {
452 provider: None,
453 remote: false,
454 all: false,
455 }),
456 };
457 let result = dispatch(Commands::Models(args), &MockRisky).await;
458 assert!(result.is_ok());
459 })
460 .await;
461 }
462
463 #[tokio::test]
464 async fn dispatch_validate_variant_is_routed() {
465 let dir = tempfile::tempdir().unwrap();
466 let args = commands::validate::ValidateArgs {
467 path: dir
468 .path()
469 .join("does-not-exist")
470 .to_str()
471 .unwrap()
472 .to_string(),
473 };
474 let result = dispatch(Commands::Validate(args), &MockRisky).await;
475 assert!(result.is_err());
476 }
477
478 #[tokio::test]
479 async fn dispatch_tools_variant_is_routed() {
480 let home = tempfile::tempdir().unwrap();
483 let result = temp_env::async_with_vars(
484 [("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
485 async {
486 let args = commands::tools::ToolsArgs { json: false };
487 dispatch(Commands::Tools(args), &MockRisky).await
488 },
489 )
490 .await;
491 assert!(result.is_ok());
492 }
493
494 #[tokio::test]
495 async fn dispatch_context_variant_is_routed() {
496 let args = commands::context::ContextArgs {
498 run_id: "no-such-run-xyzzy".to_string(),
499 json: false,
500 full: false,
501 };
502 let result = dispatch(Commands::Context(args), &MockRisky).await;
503 assert!(result.is_err());
504 }
505
506 #[tokio::test]
507 async fn dispatch_policy_list_variant_is_routed() {
508 let args = commands::policy::PolicyArgs {
509 command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
510 };
511 let result = dispatch(Commands::Policy(args), &MockRisky).await;
512 assert!(result.is_ok());
513 }
514
515 #[tokio::test]
516 async fn dispatch_policy_test_variant_is_routed() {
517 let args = commands::policy::PolicyArgs {
518 command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
519 tool: "shell".to_string(),
520 target: None,
521 taint: "public".to_string(),
522 }),
523 };
524 let result = dispatch(Commands::Policy(args), &MockRisky).await;
525 assert!(result.is_ok());
526 }
527}