Skip to content

rtb-cli-bin

rtb-cli-bin produces the rtb binary — the scaffolder and regenerator that creates and maintains tools built on the framework. It is the Rust counterpart to gtb in the Go ecosystem: it borrows gtb's command grammar — the verb structure, the kind names, the common flag names — while the implementation underneath is idiomatic Rust.

This page is about why it is built this way. For the flags themselves, see the CLI reference.

Why the scaffolder is itself an RTB tool

main.rs is a call to rtb_cli::Application::builder() — the same entry point a scaffolded tool uses. The consequence is that rtb inherits every framework default command (version, doctor, update, docs, mcp, config, credentials, telemetry) for free, and only the three scaffolder verbs need registering on top.

That is dogfooding with teeth rather than tidiness. A regression in the built-in command suite breaks the scaffolder itself, which is the tool every contributor runs most often, so it surfaces in a working session rather than in a downstream project weeks later.

It also tunes the runtime feature set in a way worth knowing about: Init is disabled — the scaffolder runs on baked-in defaults and has no per-user config to bootstrap — and Ai is enabled explicitly, because it is not in the framework's default-on set and the codegen path needs it.

Why the CLI grammar is the stable surface

The crate exposes a library so its modules are unit-testable, but that library is not the product. Downstream users depend on rtb generate project, rtb remove flag, rtb regenerate manifest — the command tree — and that is what 1.0 will freeze. Every item in rtb_cli_bin:: is internal and may change without a major bump.

This is why the reference for this crate is a CLI reference rather than an API page, and why the crate is not re-exported from the umbrella rust-tool-base crate. A tool that builds on RTB should never pull minijinja, inquire and the embedded template tree into its own binary. Install the scaffolder on its own:

$ cargo install rtb-cli-bin

Why generated code is tracked by content hash

The scaffolder writes into files you will then edit. Something has to answer "did the user change this, or is it still exactly as generated?" before a regeneration can decide whether overwriting is safe.

A content hash per generated file answers that without keeping a copy of the original: rehash the file, compare with the hash recorded at the last write, and a difference means you touched it. The alternative — re-render the template and diff — would be wrong, because a template update also changes the render, and the two causes need distinguishing.

The manifest keeps both hashes for exactly that reason: the recorded hash identifies your edits, and comparing the fresh render against the recorded hash identifies template drift. See .rtb/manifest.yaml.

Why marker regions instead of whole-file ownership

Whole-file ownership would force a binary choice: either the generator owns src/commands/mod.rs and your registrations get wiped on regeneration, or you own it and template improvements never reach you.

Markers split the file instead. The generator owns everything outside a // rtb:<id>-begin / -end pair; you own everything inside. On regeneration the interiors are spliced out of your file into the fresh render, so both halves stay current. Generated commands follow a one-command-per-file convention for the same reason — it makes "skip this one" a clean file-level decision. The full list of regions is in presets and markers.

Why AI codegen runs a repair loop rather than one call

A single AI call produces code that looks right and often does not compile, which leaves you debugging someone else's draft. The loop closes that gap with the compiler: render the draft, run cargo check, and feed the diagnostics plus the offending code back for another attempt.

It stops on a clean compile, or at --max-repair-iterations (default five), in which case the last attempt is left on disk with the unresolved diagnostics printed — a failed loop hands you a starting point, not an empty file. --agentless opts out and takes the one-shot draft.

Two deliberate ordering choices sit around it. The provider call happens before anything is written, because a bad model name or an expired key is the most likely failure and it should leave nothing scaffolded to clean up. And integration tests stub the provider with wiremock — the suite never calls a live provider.

The loop currently targets the minimal preset only; on cli it refuses with an explanation rather than generating a body that cannot work.

Why validation is the primary defence, not escaping

Every user-influenced field is NFC-normalised and matched against a field-specific character class before it reaches a template. That is the primary defence against template injection; the escape filters at render time are defence in depth behind it.

The order matters. Escaping is a per-site obligation — miss one call site and the guarantee is gone. A character class enforced at the boundary holds for every site at once, including sites added later by someone who has not read this page. The cost is a narrow input class, no underscores and no uppercase in names, which is a real ergonomic price paid deliberately. The rules are listed in validation rules.