Add a custom command¶
Commands implement the Command trait and
register themselves into the BUILTIN_COMMANDS distributed slice.
rtb-cli::Application::run walks the slice at startup, so there is no
central registry to edit — adding a file is enough.
1. Implement the trait¶
use async_trait::async_trait;
use rtb_app::app::App;
use rtb_app::command::{Command, CommandSpec, BUILTIN_COMMANDS};
use rtb_app::linkme::distributed_slice;
struct Deploy;
#[async_trait]
impl Command for Deploy {
fn spec(&self) -> &CommandSpec {
static SPEC: CommandSpec = CommandSpec {
name: "deploy",
about: "Push the current build to the target environment",
aliases: &["ship"],
feature: None,
};
&SPEC
}
async fn run(&self, app: App) -> miette::Result<()> {
// `app` is cheap to clone — every field is Arc-wrapped.
// `app.config.get()` returns the current typed config snapshot.
let _cfg = app.config.get();
Ok(())
}
}
2. Register it¶
#[distributed_slice(BUILTIN_COMMANDS)]
fn __register_deploy() -> Box<dyn Command> {
Box::new(Deploy)
}
The linkme slice is populated at link time — no OnceLock registry,
no manual wiring. (This is why crates that register commands run
#![deny(unsafe_code)] rather than forbid: the slice registration
emits a #[link_section] attribute.)
3. Gate it behind a runtime feature (optional)¶
Set CommandSpec::feature to make the command appear only when a
Feature is enabled in the runtime
Features set — orthogonal to Cargo features:
4. Expose it over MCP (optional)¶
Override the default Command::mcp_exposed / mcp_input_schema
methods to surface the command as a Model Context Protocol tool —
see rtb-mcp.
Testing¶
Drive the compiled binary with assert_cmd + insta snapshots; new
CLI commands must ship an assert_cmd scenario. See the
BDD pattern and
Engineering Standards.