Skip to main content

basil_bin/
lib.rs

1//! Library surface for the unified `basil` binary.
2//!
3//! This crate is primarily a binary (`basil`), but it also exposes its
4//! command-line definition as a library so tooling, notably the `xtask`
5//! man-page generator, can render the command tree without launching the
6//! process. [`cli`] returns the fully assembled clap command.
7
8#![cfg_attr(test, allow(clippy::indexing_slicing))]
9
10pub mod client_cli;
11
12use basil_core::{agent_cli, bundle_cli, init};
13use clap::{CommandFactory, Parser, Subcommand};
14
15/// Top-level `basil` command-line interface.
16#[derive(Debug, Parser)]
17#[command(name = "basil", version, about = "Basil broker and operator tool")]
18pub struct Cli {
19    /// Path to the agent's Unix socket for over-socket commands.
20    #[arg(long, env = "BASIL_SOCKET", global = true)]
21    pub socket: Option<String>,
22
23    #[command(subcommand)]
24    pub command: Command,
25}
26
27/// Top-level `basil` subcommands.
28#[derive(Debug, Subcommand)]
29pub enum Command {
30    /// Run the broker daemon.
31    Agent(agent_cli::RunArgs),
32    /// Create and manage a sealed credential bundle.
33    #[command(subcommand)]
34    Bundle(Box<bundle_cli::BundleCommand>),
35    /// Offline config and policy operations.
36    #[command(subcommand)]
37    Config(Box<ConfigCommand>),
38    /// Preflight environment and deployment checks.
39    Doctor(agent_cli::DoctorArgs),
40    #[command(flatten)]
41    Client(client_cli::Command),
42}
43
44/// Offline `basil config` subcommands.
45#[derive(Debug, Subcommand)]
46pub enum ConfigCommand {
47    /// Scaffold a first-run starter set.
48    Init(init::InitArgs),
49    /// Lint the catalog against its backends.
50    Check(agent_cli::CheckArgs),
51    /// Evaluate a proposed policy decision offline.
52    Explain(agent_cli::ExplainArgs),
53}
54
55/// Returns the fully assembled top-level clap [`Command`](clap::Command) for the
56/// `basil` binary, for tooling such as man-page and shell-completion generation.
57#[must_use]
58pub fn cli() -> clap::Command {
59    Cli::command()
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    fn parse(args: &[&str]) -> Cli {
67        Cli::try_parse_from(args).expect("parse cli")
68    }
69
70    #[test]
71    fn bundle_is_top_level_command() {
72        let cli = parse(&[
73            "basil",
74            "bundle",
75            "verify",
76            "creds.sealed",
77            "--open",
78            "passphrase:file=/run/pass",
79        ]);
80        assert!(matches!(cli.command, Command::Bundle(_)));
81    }
82
83    #[test]
84    fn old_config_bundle_path_is_not_user_facing() {
85        let err = Cli::try_parse_from(["basil", "config", "bundle", "verify"])
86            .expect_err("config bundle must not remain as a compatibility command");
87        assert!(
88            err.to_string().contains("unrecognized subcommand")
89                || err.to_string().contains("invalid subcommand"),
90            "{err}"
91        );
92    }
93
94    #[test]
95    fn set_backend_accepts_structured_backend_value() {
96        let cli = parse(&[
97            "basil",
98            "bundle",
99            "set-backend",
100            "creds.sealed",
101            "--backend",
102            "id=aws1,type=aws-kms,region=us-east-1,profile=prod",
103            "--open",
104            "passphrase:file=/run/pass",
105        ]);
106        assert!(matches!(cli.command, Command::Bundle(_)));
107    }
108}