use crate::commands;
#[derive(clap::Subcommand)]
pub enum Commands {
Create(commands::create::CreateArgs),
Setup(commands::setup::SetupArgs),
Run(commands::run::RunArgs),
#[command(long_about = commands::ps::PS_LONG_ABOUT)]
Ps(commands::ps::PsArgs),
Msg(commands::ctl::MsgArgs),
#[command(alias = "kill")]
Cancel(commands::ctl::CancelArgs),
Pause(commands::ctl::PauseArgs),
Resume(commands::ctl::ResumeArgs),
Respond(commands::ctl::RespondArgs),
#[command(long_about = commands::doctor::DOCTOR_LONG_ABOUT)]
Doctor(commands::doctor::DoctorArgs),
List(commands::list::ListArgs),
Add(commands::add::AddArgs),
Remove(commands::remove::RemoveArgs),
Test(commands::test::TestArgs),
Pack(commands::pack::PackArgs),
#[command(name = "dash")]
Dashboard(commands::dashboard::DashboardArgs),
Models(commands::models::ModelsArgs),
Validate(commands::validate::ValidateArgs),
Tools(commands::tools::ToolsArgs),
Approvals(commands::approvals::ApprovalsArgs),
Policy(commands::policy::PolicyArgs),
Serve(commands::serve::ServeArgs),
#[command(name = "agent-client")]
AgentClient(commands::agent_client::AgentClientArgs),
Daemon(commands::daemon::DaemonArgs),
Context(commands::context::ContextArgs),
Stages(commands::stages::StagesArgs),
Result(commands::result::ResultArgs),
Mcp(commands::mcp::McpArgs),
Auth(commands::auth::AuthArgs),
#[command(long_about = commands::update::UPDATE_LONG_ABOUT)]
Update(commands::update::UpdateArgs),
}
pub trait RiskyExecutors {
fn run(
&self,
args: commands::run::RunArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn ps(
&self,
args: commands::ps::PsArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn msg(
&self,
args: commands::ctl::MsgArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn cancel(
&self,
args: commands::ctl::CancelArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn pause(
&self,
args: commands::ctl::PauseArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn resume(
&self,
args: commands::ctl::ResumeArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn respond(
&self,
args: commands::ctl::RespondArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn doctor(
&self,
args: commands::doctor::DoctorArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn setup(
&self,
args: commands::setup::SetupArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn dashboard(
&self,
args: commands::dashboard::DashboardArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn serve(
&self,
args: commands::serve::ServeArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn agent_client(
&self,
args: commands::agent_client::AgentClientArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn daemon(
&self,
args: commands::daemon::DaemonArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn mcp(
&self,
args: commands::mcp::McpArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn auth(
&self,
args: commands::auth::AuthArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
fn update(
&self,
args: commands::update::UpdateArgs,
) -> impl std::future::Future<Output = anyhow::Result<()>>;
}
pub fn apply_region_flags(
command: &mut Commands,
regions: std::collections::HashMap<String, String>,
) {
if let Commands::Run(args) = command {
args.regions = regions;
}
}
pub async fn dispatch(command: Commands, ex: &impl RiskyExecutors) -> anyhow::Result<()> {
match command {
Commands::Create(args) => commands::create::execute(args).await,
Commands::Setup(args) => ex.setup(args).await,
Commands::Run(args) => ex.run(args).await,
Commands::Ps(args) => ex.ps(args).await,
Commands::Msg(args) => ex.msg(args).await,
Commands::Cancel(args) => ex.cancel(args).await,
Commands::Pause(args) => ex.pause(args).await,
Commands::Resume(args) => ex.resume(args).await,
Commands::Respond(args) => ex.respond(args).await,
Commands::Doctor(args) => ex.doctor(args).await,
Commands::List(args) => commands::list::execute(args).await,
Commands::Add(args) => commands::add::execute(args).await,
Commands::Remove(args) => commands::remove::execute(args).await,
Commands::Test(args) => commands::test::execute(args).await,
Commands::Pack(args) => commands::pack::execute(args).await,
Commands::Dashboard(args) => ex.dashboard(args).await,
Commands::Models(args) => commands::models::execute(args).await,
Commands::Validate(args) => commands::validate::execute(args).await,
Commands::Tools(args) => commands::tools::execute(args).await,
Commands::Approvals(args) => commands::approvals::execute(args).await,
Commands::Policy(args) => commands::policy::execute(args).await,
Commands::Serve(args) => ex.serve(args).await,
Commands::AgentClient(args) => ex.agent_client(args).await,
Commands::Daemon(args) => ex.daemon(args).await,
Commands::Context(args) => commands::context::execute(args).await,
Commands::Stages(args) => commands::stages::execute(args).await,
Commands::Result(args) => commands::result::execute(args).await,
Commands::Mcp(args) => ex.mcp(args).await,
Commands::Auth(args) => ex.auth(args).await,
Commands::Update(args) => ex.update(args).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockRisky;
impl RiskyExecutors for MockRisky {
async fn run(&self, _args: commands::run::RunArgs) -> anyhow::Result<()> {
Ok(())
}
async fn ps(&self, _args: commands::ps::PsArgs) -> anyhow::Result<()> {
Ok(())
}
async fn msg(&self, _args: commands::ctl::MsgArgs) -> anyhow::Result<()> {
Ok(())
}
async fn respond(&self, _args: commands::ctl::RespondArgs) -> anyhow::Result<()> {
Ok(())
}
async fn doctor(&self, _args: commands::doctor::DoctorArgs) -> anyhow::Result<()> {
Ok(())
}
async fn cancel(&self, _args: commands::ctl::CancelArgs) -> anyhow::Result<()> {
Ok(())
}
async fn pause(&self, _args: commands::ctl::PauseArgs) -> anyhow::Result<()> {
Ok(())
}
async fn resume(&self, _args: commands::ctl::ResumeArgs) -> anyhow::Result<()> {
Ok(())
}
async fn setup(&self, _args: commands::setup::SetupArgs) -> anyhow::Result<()> {
Ok(())
}
async fn dashboard(&self, _args: commands::dashboard::DashboardArgs) -> anyhow::Result<()> {
Ok(())
}
async fn serve(&self, _args: commands::serve::ServeArgs) -> anyhow::Result<()> {
Ok(())
}
async fn agent_client(
&self,
_args: commands::agent_client::AgentClientArgs,
) -> anyhow::Result<()> {
Ok(())
}
async fn daemon(&self, _args: commands::daemon::DaemonArgs) -> anyhow::Result<()> {
Ok(())
}
async fn auth(&self, _args: commands::auth::AuthArgs) -> anyhow::Result<()> {
Ok(())
}
async fn mcp(&self, _args: commands::mcp::McpArgs) -> anyhow::Result<()> {
Ok(())
}
async fn update(&self, _args: commands::update::UpdateArgs) -> anyhow::Result<()> {
Ok(())
}
}
fn create_args() -> commands::create::CreateArgs {
commands::create::CreateArgs {
name: "unused".to_string(),
template: "default".to_string(),
}
}
#[test]
fn apply_region_flags_populates_run_and_noops_other_commands() {
let mut run = Commands::Run(commands::run::RunArgs::default());
let flags = std::collections::HashMap::from([("criteria".to_string(), "safe".to_string())]);
apply_region_flags(&mut run, flags);
assert!(
matches!(&run, Commands::Run(a) if a.regions.get("criteria").map(String::as_str) == Some("safe")),
"region flag was injected into the Run args"
);
let mut other = Commands::Ps(commands::ps::PsArgs::default());
apply_region_flags(&mut other, std::collections::HashMap::new());
}
#[tokio::test]
async fn dispatch_run_variant_is_routed_through_the_executor() {
let result = dispatch(Commands::Run(commands::run::RunArgs::default()), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_setup_variant_is_routed_through_the_executor() {
let args = commands::setup::SetupArgs {
non_interactive: true,
no_verify: false,
install_agents: false,
anthropic_key: None,
openai_key: None,
google_key: None,
openrouter_key: None,
ollama_url: None,
default_model: None,
claude_code: None,
claude_code_effort: None,
};
let result = dispatch(Commands::Setup(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_dashboard_variant_is_routed_through_the_executor() {
let args = commands::dashboard::DashboardArgs {};
let result = dispatch(Commands::Dashboard(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_msg_variant_is_routed_through_the_executor() {
let args = commands::ctl::MsgArgs {
agent_id: "a".to_string(),
content: "c".to_string(),
};
assert!(dispatch(Commands::Msg(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_respond_variant_is_routed_through_the_executor() {
let args = commands::ctl::RespondArgs {
request_id: None,
value: None,
choice: None,
approve: false,
deny: false,
session: false,
stage: false,
json: false,
};
assert!(dispatch(Commands::Respond(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_doctor_variant_is_routed_through_the_executor() {
let args = commands::doctor::DoctorArgs::default();
assert!(dispatch(Commands::Doctor(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_cancel_variant_is_routed_through_the_executor() {
let args = commands::ctl::CancelArgs {
run_id: "r".to_string(),
force: false,
};
assert!(dispatch(Commands::Cancel(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_pause_variant_is_routed_through_the_executor() {
let args = commands::ctl::PauseArgs {
run_id: "r".to_string(),
};
assert!(dispatch(Commands::Pause(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_resume_variant_is_routed_through_the_executor() {
let args = commands::ctl::ResumeArgs {
run_id: "r".to_string(),
};
assert!(dispatch(Commands::Resume(args), &MockRisky).await.is_ok());
}
#[tokio::test]
async fn dispatch_ps_variant_is_routed_through_the_executor() {
let result = dispatch(Commands::Ps(commands::ps::PsArgs::default()), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_daemon_variant_is_routed_through_the_executor() {
let args = commands::daemon::DaemonArgs {
action: None,
socket: None,
};
let result = dispatch(Commands::Daemon(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_auth_variant_is_routed_through_the_executor() {
let args = commands::auth::AuthArgs::status_for_test();
let result = dispatch(Commands::Auth(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_update_variant_is_routed_through_the_executor() {
let args = commands::update::UpdateArgs::default();
let result = dispatch(Commands::Update(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_mcp_variant_is_routed_through_the_executor() {
let args = commands::mcp::McpArgs::list_for_test();
let result = dispatch(Commands::Mcp(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_serve_variant_is_routed_through_the_executor() {
let args = commands::serve::ServeArgs {
port: 0,
host: "127.0.0.1".to_string(),
cors: None,
token: Some("test-token".to_string()),
allow_admin: false,
workdir_root: None,
no_remote_yolo: false,
tls_cert: None,
tls_key: None,
};
let result = dispatch(Commands::Serve(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_agent_client_variant_is_routed_through_the_executor() {
let args = commands::agent_client::AgentClientArgs::default();
let result = dispatch(Commands::AgentClient(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_create_variant_is_routed() {
let dir = tempfile::tempdir().unwrap();
let args = commands::create::CreateArgs {
name: dir.path().to_str().unwrap().to_string(),
..create_args()
};
let result = dispatch(Commands::Create(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_list_variant_is_routed() {
crate::config::with_isolated_config_path_async("dispatch-list", |_fake_dir| async move {
let args = commands::list::ListArgs {
filter: commands::list::ListFilter::All,
json: false,
};
let result = dispatch(Commands::List(args), &MockRisky).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn dispatch_add_variant_is_routed() {
let args = commands::add::AddArgs {
package: "definitely-not-a-real-bundle-xyz.leviath-bundle".to_string(),
};
let result = crate::config::with_isolated_config_path_async("dispatch-add", |_| {
dispatch(Commands::Add(args), &MockRisky)
})
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_remove_variant_is_routed() {
let args = commands::remove::RemoveArgs {
name: "definitely-not-an-installed-agent-xyz".to_string(),
};
let result = dispatch(Commands::Remove(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_test_variant_is_routed() {
let dir = tempfile::tempdir().unwrap();
let args = commands::test::TestArgs {
path: Some(dir.path().to_str().unwrap().to_string()),
filter: None,
dry_run: true,
};
let result = dispatch(Commands::Test(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_pack_variant_is_routed() {
let dir = tempfile::tempdir().unwrap();
let args = commands::pack::PackArgs {
path: Some(dir.path().to_str().unwrap().to_string()),
output: None,
};
let result = dispatch(Commands::Pack(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_models_variant_is_routed() {
crate::config::with_isolated_config_path_async("dispatch-models", |_fake_dir| async move {
let args = commands::models::ModelsArgs {
command: commands::models::ModelsCommand::List(commands::models::ListArgs {
provider: None,
remote: false,
all: false,
json: false,
}),
};
let result = dispatch(Commands::Models(args), &MockRisky).await;
assert!(result.is_ok());
})
.await;
}
#[tokio::test]
async fn dispatch_validate_variant_is_routed() {
crate::config::with_isolated_config_path_async("dispatch-validate", |_| async {
let dir = tempfile::tempdir().unwrap();
let args = commands::validate::ValidateArgs {
path: dir
.path()
.join("does-not-exist")
.to_str()
.unwrap()
.to_string(),
deny_warnings: false,
json: false,
};
let result = dispatch(Commands::Validate(args), &MockRisky).await;
assert!(result.is_err());
})
.await;
}
#[tokio::test]
async fn dispatch_tools_variant_is_routed() {
let home = tempfile::tempdir().unwrap();
let result = temp_env::async_with_vars(
[("LEVIATH_HOME", Some(home.path().to_str().unwrap()))],
async {
let args = commands::tools::ToolsArgs { json: false };
dispatch(Commands::Tools(args), &MockRisky).await
},
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_routes_stages() {
let result = dispatch(
Commands::Stages(commands::stages::StagesArgs {
run_id: "no-such-run".to_string(),
json: false,
regions: false,
}),
&MockRisky,
)
.await;
assert!(result.is_err(), "no ledger for a run that never ran");
}
#[tokio::test]
async fn dispatch_context_variant_is_routed() {
let args = commands::context::ContextArgs {
run_id: "no-such-run-xyzzy".to_string(),
json: false,
full: false,
};
let result = dispatch(Commands::Context(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_result_variant_is_routed() {
let args = commands::result::ResultArgs {
run_id: "no-such-run-xyzzy".to_string(),
json: false,
raw: false,
};
let result = dispatch(Commands::Result(args), &MockRisky).await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_approvals_variant_is_routed() {
let home = tempfile::tempdir().unwrap();
let config = home.path().join("config.toml");
let result = temp_env::async_with_vars(
[
("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
],
async {
let args = commands::approvals::ApprovalsArgs {
command: commands::approvals::ApprovalsCommand::Safe(
commands::approvals::SafeArgs {
agent: Some("coder".to_string()),
json: true,
},
),
};
dispatch(Commands::Approvals(args), &MockRisky).await
},
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_approvals_surfaces_a_broken_config() {
let home = tempfile::tempdir().unwrap();
let config = home.path().join("config.toml");
std::fs::write(&config, "this is not = = toml").unwrap();
let result = temp_env::async_with_vars(
[
("LEVIATH_HOME", Some(home.path().to_str().unwrap())),
("LEVIATH_CONFIG_PATH", Some(config.to_str().unwrap())),
],
async {
let args = commands::approvals::ApprovalsArgs {
command: commands::approvals::ApprovalsCommand::Safe(
commands::approvals::SafeArgs {
agent: None,
json: false,
},
),
};
dispatch(Commands::Approvals(args), &MockRisky).await
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn dispatch_policy_list_variant_is_routed() {
let args = commands::policy::PolicyArgs {
command: commands::policy::PolicyCommand::List(commands::policy::PolicyListArgs {}),
};
let result = dispatch(Commands::Policy(args), &MockRisky).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn dispatch_policy_test_variant_is_routed() {
let args = commands::policy::PolicyArgs {
command: commands::policy::PolicyCommand::Test(commands::policy::PolicyTestArgs {
tool: "shell".to_string(),
target: None,
taint: "public".to_string(),
}),
};
let result = dispatch(Commands::Policy(args), &MockRisky).await;
assert!(result.is_ok());
}
}