Skip to content

rtb-cli

The crate a downstream tool's main() touches. It turns ToolMetadata and VersionInfo into a running application: a clap command tree, a diagnostic pipeline, a tracing subscriber, signal handling, and dispatch.

What main() looks like

use rtb_cli::prelude::*;

#[tokio::main]
async fn main() -> std::process::ExitCode {
    let app = Application::builder()
        .metadata(ToolMetadata::builder().name("mytool").summary("a tool").build())
        .version(rtb_app::version_info!())
        .build();

    match app {
        Ok(app) => app.run_and_exit().await,
        Err(report) => rtb_cli::report_to_exit_code(&report),
    }
}

run_and_exit is the boundary that renders a failure through the installed diagnostic handler and maps it to a process exit code, honouring rtb_error::WithExitCode. report_to_exit_code is exported so a builder error (raised before run_and_exit is reachable) takes the same path.

A main returning miette::Result<()> also works and is shorter. It gives up the custom exit code: Rust's Termination impl always exits 1 on error.

The six built-ins it implements itself

version, doctor, init, config, credentials and telemetry. The other three commands in a full build (docs, update, mcp) come from their own crates.

Each is gated on a runtime Feature, and each is registered through the same BUILTIN_COMMANDS slice a downstream command uses. There is no privileged registration path. That is what makes replacing one a matter of registering your own command with the same name.

The extension points

You want You register into Consumed by
A new subcommand rtb_app::command::BUILTIN_COMMANDS The clap tree
A diagnostic check rtb_cli::health::HEALTH_CHECKS doctor
A first-run setup step rtb_cli::init::INITIALISERS init
Something that runs before every command rtb_app::command::BUILTIN_PRERUN_HOOKS Application::run_with_args

All four are linkme distributed slices, so a crate contributes to them without the tool author wiring anything, and does not contribute at all if nothing references it. See Crates, features and registries.

Why HealthCheck and Initialiser are separate traits

They look similar: both are async, both take an &App, both are collected from a slice. But they answer different questions and fail differently.

HealthCheck answers "is this working?" and has three outcomes: Ok, Warn and Fail. It never changes anything, Warn is a first-class result rather than a soft failure, and doctor runs every check regardless of what the others reported.

Initialiser answers "is this set up, and if not, set it up". It is expected to mutate: write a config file, prompt for a credential. init skips anything already configured, and the first failure stops the run. There is no point configuring step four when step two did not take.

Collapsing them would mean either a health check that can write to your disk or a setup step that reports Warn and carries on.

Testing hooks

run_with_args(iter) dispatches from a supplied argument list instead of std::env::args_os(), so a test drives the whole application in-process. install_hooks(false) suppresses the miette report and panic hooks, which are process-global and set-once, so a test binary that installs them affects every other test in the same process.

Together they are why the crate's own suite can run cucumber scenarios against a real Application without spawning a subprocess.

Where to go next