Skip to content

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:

feature: Some(rtb_app::features::Feature::Ai),

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.