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.
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:
rtb_error::hook::install_report_handler()— the miette graphical renderer.rtb_error::hook::install_panic_hook()— panics render through the same pipeline.rtb_error::hook::install_with_footer(...)— only whenToolMetadata::helpis notHelpChannel::None.runtime::install_tracing(LogFormat::auto())— the tracing subscriber.runtime::bind_shutdown_signals(...)— a task that cancelsApp::shutdownonCtrl-C, and on UnixSIGTERM.- Clap parse.
- Every entry in
rtb_app::command::BUILTIN_PRERUN_HOOKS, awaited in slice order. A hook returningErraborts the run before the command executes.rtb-updateregisters one — see self-update policy. - 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:
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.