#![cfg_attr(test, allow(clippy::indexing_slicing))]
pub mod client_cli;
#[cfg(feature = "keystore-backend")]
use basil_core::demo;
use basil_core::{agent_cli, bundle_cli, init};
use clap::{Args, CommandFactory, Parser, Subcommand};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[command(
name = "basil",
version,
about = "Basil broker and operator tool",
long_about = "Basil is a host-local secrets broker: your app never touches the key. The \
kernel attests who's calling, a default-deny policy decides, the key is used \
where it lives (OpenBao/Vault, KMS, or a sealed local store), and every \
operation is audited.\n\nThe one `basil` binary is the broker daemon (`basil \
agent`), the offline operator tooling (`init`, `bundle`, `explain`, `doctor`, \
`demo`), and the over-socket client for every broker operation."
)]
pub struct Cli {
#[arg(long, env = "BASIL_SOCKET", global = true)]
pub socket: Option<String>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init(Box<init::InitArgs>),
#[cfg(feature = "keystore-backend")]
Demo(demo::DemoArgs),
Completions(CompletionsArgs),
Agent(agent_cli::RunArgs),
#[command(subcommand)]
Bundle(Box<bundle_cli::BundleCommand>),
Explain(agent_cli::ExplainArgs),
Doctor(agent_cli::DoctorArgs),
#[command(flatten)]
Client(client_cli::Command),
}
#[derive(Debug, Args)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
#[must_use]
pub fn cli() -> clap::Command {
Cli::command()
}
pub fn write_completions(
shell: clap_complete::Shell,
out: &mut dyn std::io::Write,
) -> std::io::Result<()> {
let mut buf = Vec::new();
clap_complete::generate(shell, &mut cli(), "basil", &mut buf);
out.write_all(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Cli {
Cli::try_parse_from(args).expect("parse cli")
}
#[test]
fn bundle_is_top_level_command() {
let cli = parse(&[
"basil",
"bundle",
"verify",
"creds.sealed",
"--open",
"passphrase:file=/run/pass",
]);
assert!(matches!(cli.command, Command::Bundle(_)));
}
#[test]
fn old_config_bundle_path_is_not_user_facing() {
let err = Cli::try_parse_from(["basil", "config", "bundle", "verify"])
.expect_err("config bundle must not remain as a compatibility command");
assert!(
err.to_string().contains("unrecognized subcommand")
|| err.to_string().contains("invalid subcommand"),
"{err}"
);
}
#[test]
fn set_backend_accepts_structured_backend_value() {
let cli = parse(&[
"basil",
"bundle",
"set-backend",
"creds.sealed",
"--backend",
"id=aws1,type=aws-kms,region=us-east-1,profile=prod",
"--open",
"passphrase:file=/run/pass",
]);
assert!(matches!(cli.command, Command::Bundle(_)));
}
}