Skip to content

CLI reference

Every tool built on rtb-cli gets the same outer command tree. This page documents that tree; the pages beneath it document each built-in subcommand's own flags.

Throughout, mytool stands for whatever ToolMetadata::name the host tool set.

What the root command looks like

rtb-cli builds the clap root from ToolMetadata:

Clap setting Value
Command name ToolMetadata::name
about ToolMetadata::summary
long_about ToolMetadata::description, only when non-empty
arg_required_else_help true
subcommand_required true

There is no default subcommand at the root. Running mytool with no arguments prints the help text — but as a failed run: clap raises ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, which rtb-cli does not treat as a success, so the help goes to stderr wrapped in a miette diagnostic and the process exits 1. mytool --help is the success path.

The --output flag

--output is the only flag rtb-cli declares at the root. It is declared once with Arg::global(true), so clap accepts it before or after an ordinary subcommand name.

Value Rendering
text tabled table. Default.
json Pretty-printed JSON array, one element per row

Any other value is a clap parse error at the outer layer.

mytool --output json version      # accepted
mytool version --output json      # accepted

Subcommands that print structured rows honour it through rtb_cli::render::output. Subcommands with nothing tabular to print — init, update run, mcp serve, docs browse — ignore it.

Where --output is accepted, per command

Every built-in that owns its own clap subtree re-reads the process arguments positionally, dropping the first two tokens, which makes the flag's position matter.

Command mytool --output json <cmd> … mytool <cmd> … --output json
version, doctor, init accepted, ignored accepted, ignored
config, credentials, telemetry fails: unrecognized subcommand 'json' works
docs, update, mcp fails fails: unexpected argument '--output'

The failure in the middle row happens because the flag and its value displace the subcommand name in the positional slice. The bottom row fails because those three never strip the flag before handing the remainder to their inner parser.

A command you write yourself with parse_passthrough behaves differently again — see Add a command.

Why passthrough parsing works this way explains the mechanism.

The root accepts --help but not --version

--help and -h print clap's help to stdout and exit 0. Pre-run hooks do not fire, because clap short-circuits before dispatch.

--version is not declared at the root. rtb-cli never calls clap's Command::version, so clap does not add the flag, and mytool --version is an unknown argument:

$ mytool --version
rtb::command_not_found

  x command not found: --version
  help: run `--help` to list available commands

Use mytool version instead. This matters beyond ergonomics: rtb-update's staged-binary self-test invokes <staged-binary> --version and requires exit 0 — see self-test.

What runs before your command does

Application::run_with_args performs the following, in order, on every invocation:

  1. rtb_error::hook::install_report_handler() — the miette graphical renderer.
  2. rtb_error::hook::install_panic_hook() — panics render through the same pipeline.
  3. rtb_error::hook::install_with_footer(...) — only when ToolMetadata::help is not HelpChannel::None.
  4. runtime::install_tracing(LogFormat::auto()) — the tracing subscriber.
  5. runtime::bind_shutdown_signals(...) — a task that cancels App::shutdown on Ctrl-C, and on Unix SIGTERM.
  6. Clap parse.
  7. Every entry in rtb_app::command::BUILTIN_PRERUN_HOOKS, awaited in slice order. A hook returning Err aborts the run before the command executes. rtb-update registers one — see self-update policy.
  8. The matched command's run(app).

Steps 1–3 are skipped when the tool calls ApplicationBuilder::install_hooks(false).

The built-in command set

Each built-in is gated on a runtime Feature. A command whose feature is not in the Features set is never registered with clap, so invoking it produces clap's unknown subcommand error rather than a "disabled" message.

Subcommand Feature Registered by Reference
version Version rtb-cli Core commands
doctor Doctor rtb-cli Core commands
init Init rtb-cli Core commands
config Config rtb-cli config
credentials Credentials rtb-cli credentials
telemetry Telemetry rtb-cli telemetry
docs Docs rtb-docs docs
update Update rtb-update update
mcp Mcp rtb-mcp mcp

Features::default() enables Init, Version, Update, Docs, Mcp, Doctor, Credentials, Telemetry and Config. Ai and Changelog exist as Feature variants but are not in the default set.

A command only appears if its crate is actually linked

docs, update and mcp register themselves through linkme from rtb-docs, rtb-update and rtb-mcp. Listing one of those crates in Cargo.toml is not enough — if nothing in your binary references the crate, rustc does not link it, the registration never runs, and the subcommand silently does not exist. There is no error; the command is simply absent from --help.

The fix is a linking reference in main.rs:

use rtb_docs as _;
use rtb_mcp as _;
use rtb_update as _;

Register the optional commands covers this in context.

How a duplicate command name is resolved

Application::build deduplicates commands by CommandSpec::name, keeping the last entry in linkme slice order, then sorts the survivors by name so --help is deterministic. A downstream crate that registers its own version command replaces the framework's.

Slice order is decided at link time and is not stable across compiler versions or dependency-graph changes. Registering two commands with the same name from crates you do not control is therefore not a supported way to pick a winner.

Exit codes

Situation Exit code
Command returned Ok(()) 0
--help / -h 0
Command returned an error with no attached code 1
Command returned an error carrying rtb_error::WithExitCode that code

These codes apply when the binary's main funnels through Application::run_and_exit, which renders the diagnostic to stderr and maps it via rtb_error::exit_code_of. A main that returns miette::Result<()> directly gets Rust's Termination behaviour instead, which is always 1 on error.

report_to_exit_code is public so a builder error — raised before run_and_exit is reachable — can be mapped through the same path.

Unknown subcommands

An unrecognised subcommand or an unknown argument is mapped to rtb_error::Error::CommandNotFound, which renders with the diagnostic code rtb::command_not_found. Every other clap failure surfaces as a plain miette report wrapping clap's own message — including the missing-subcommand case above.