Build your first tool¶
By the end of this you'll have a small Rust CLI called greeter that you
scaffolded rather than typed, with a greet subcommand, a --name flag,
and a config field — and it will print hello, Ada! when you run it.
Allow about fifteen minutes, most of which is the first cargo build.
What you need first¶
- Rust 1.82 or newer. That's the minimum the workspace declares.
- The
rtbbinary. Install it withcargo install rtb-cli-bin. It's a separate install from the framework crate on purpose — a tool built on RTB never pulls the scaffolder's templates into its own binary. - A directory you don't mind creating a project in.
You don't need an AI provider or an API key. Everything here runs from the templates baked into the binary, so it works offline.
Check the binary is on your path — note that it's a subcommand, not a
flag, so rtb --version won't work:
Scaffold the project¶
rtb generate project renders a whole tool from a preset. Run it from
the directory you want the project to appear in — it creates
./greeter for you:
$ rtb generate project --name greeter --description "A greeting tool" --non-interactive
Created `greeter` (6 files written to greeter)
--non-interactive skips the wizard, which means every required field
has to be a flag; drop it and rtb will prompt you for anything you
left out. The name has to be lowercase with hyphens — ^[a-z][a-z0-9-]{0,63}$
— so greeter is fine and Greeter isn't.
You now have six files and a git repository:
greeter/
├── .git/
├── .gitignore
├── .rtb/manifest.yaml
├── Cargo.toml
├── README.md
└── src/
├── commands/mod.rs
└── main.rs
That default preset is minimal. It depends on clap and linkme and
nothing else, which is why it builds without fetching the framework. The
other bundled preset, cli, builds on the full framework and inherits
commands like version, doctor and config — worth switching to
later, but it's more moving parts than a first run needs.
.rtb/manifest.yaml is the important one. It records which preset built
the tree and a hash of every file the generator owns, and every command
below reads it. That's also why the rest of this tutorial runs from
inside the project directory:
Add a subcommand¶
$ rtb generate command greet --about "Print a greeting"
Created `src/commands/greet.rs` and registered in `src/commands/mod.rs`
Two things happened: a new file, and a pub mod greet; line inserted
between the // rtb:commands-begin and // rtb:commands-end markers in
src/commands/mod.rs. Those markers matter — they're the region the
scaffolder owns, and anything inside them survives a later regeneration.
Command names are single-segment only. rtb generate command kube/ctx
fails with "nested command paths are not yet supported", so build nested
trees by hand for now.
Add a flag to it¶
$ rtb generate flag --command greet name --type string --description "Who to greet"
Added `--name` to `src/commands/greet.rs`
Pass the flag's long name without the leading dashes — name, not
--name. Passing --name there is rejected rather than quietly
stripped.
--type takes string, bool, int, float or string_slice. The
argument lands between that file's // rtb:flags-begin / -end markers,
as a normal clap builder call:
// rtb:flags-begin
.arg(
clap::Arg::new("name")
.long("name")
.help("Who to greet"),
)
// rtb:flags-end
Add a setting¶
A flag is something the user types this run. A setting is something they
configure once — it lives on the tool's AppConfig struct rather than on
a command:
Keep setting names to a single lowercase word. The help text says
snake_case, but the validator behind it only allows
^[a-z][a-z0-9-]{0,63}$: default_name is rejected outright, and
default-name is accepted and then renders a field name that isn't
valid Rust. greeting is safe.
Look at src/main.rs and you'll see the field, plus a comment recording
the default:
That comment is documentation, not behaviour — at runtime the field still
takes String::default(), an empty string. Wire a real default with
#[serde(default = "…")] when you need one.
Run what you have¶
$ cargo run -- --help
A greeting tool
Usage: greeter [COMMAND]
Commands:
greet Print a greeting
help Print this message or the help of the given subcommand(s)
Options:
--version Print version information
-h, --help Print help
The first build fetches and compiles clap, so give it a minute. Your
subcommand and its flag are both there:
$ cargo run -- greet --help
Print a greeting
Usage: greeter greet [OPTIONS]
Options:
--name <name> Who to greet
-h, --help Print help
And running it tells you exactly where you've got to:
Fill in the body¶
The scaffolder writes structure, not behaviour. Open
src/commands/greet.rs and replace the run body:
fn run(&self, matches: &ArgMatches) -> Result<(), String> {
let name = matches.get_one::<String>("name").map_or("world", String::as_str);
println!("hello, {name}!");
Ok(())
}
That's a working tool.
Check that regeneration leaves your work alone¶
rtb regenerate project re-renders the preset and reconciles it against
your tree. Run it now, with your hand-written body in place:
Nothing was touched, and src/commands/greet.rs still says
hello, Ada!. Two mechanisms did that. Files the generator owns are
compared against the hashes in the manifest, so an edited file is skipped
rather than clobbered. And for files it does refresh, the interiors of
the // rtb:* marker regions are copied off disk into the new render,
so your registrations and settings come across.
One thing to know before you rely on it: regeneration prints file names,
never a diff, and --dry-run doesn't preview what it would write. Commit
before you regenerate and read git diff afterwards — that's the real
preview.
Where to go next¶
rtb generatereference — every flag on every kind, with defaults.- What RTB does not do — read this before you plan around a feature.
- Flags vs settings — why those are two separate verbs.
- Add a custom command — the same job on the full framework, by hand.