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 — 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¶
- CLI reference — the tree it builds.
- How the application is assembled — why it is built this way.
- Add a command
- API reference on docs.rs
- Source