rust-tool-base 0.10.3

Rust Tool Base — the batteries-included CLI application framework. Umbrella crate re-exporting the full public API.
# Rust Tool Base (RTB)

**A batteries-included application framework for building Rust CLI tools: idiomatic, composable and AI-ready.**

> **Status:** pre-1.0. The crates are implemented and published, but the
> public API is not frozen, so expect breaking changes within 0.x. The
> authoritative contract for each subsystem lives in
> [`0008-rust-tool-base`]https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0008-rust-tool-base.

RTB is a Rust sibling of [Go Tool Base (GTB)](https://gitlab.com/phpboyscout/go-tool-base).
It is **not** a line-for-line port. Go Tool Base leans on Go's idioms
(`context.Context`, functional options, package-level `init()`,
`interface{}`-keyed containers); RTB leans on Rust's (`?` + `miette`,
typestate builders, `linkme` distributed slices, strongly-typed `serde`
config, `tokio` structured concurrency).

## What RTB gives you

* **Opinionated application scaffolding.** `Application::builder()` is a
  typestate assembler that wires clap, figment, tracing, miette, tokio and
  a signal-bound `CancellationToken` in one call.
* **Strongly-typed layered config.** Env vars > user files > embedded
  defaults, materialised into *your* `serde::Deserialize` struct. Hot-reload
  (the `hot-reload` feature of `rtb-config`) uses `notify` and `arc-swap`,
  with a `watch::Receiver` for observers.
* **Embedded assets with overrides.** `rtb-assets` layers files compiled
  in with `rust-embed`, directories on disk and in-memory maps behind one
  read-only `Assets` API. A read takes the file from the last-registered
  layer that has it; YAML and JSON can instead be deep-merged across every
  layer (RFC 7396 merge-patch).
* **Diagnostic errors, not error handlers.** Every crate derives
  `thiserror::Error + miette::Diagnostic`; `main()` returns
  `miette::Result<()>` and a framework-installed hook renders them with
  labels, help, and severity.
* **Self-update.** `self-replace` plus minisign-signed releases (the
  prehashed `ED` variant, `ed25519(BLAKE2b-512(archive))`), verified with
  `minisign-verify`. That is the crate `cargo-binstall` uses, so one
  signature serves both. The staged binary is self-tested before an
  atomic swap.
* **Release sources and git.** `rtb-forge` reads releases from GitHub,
  GitLab, Bitbucket, Gitea, Codeberg or a bare HTTPS URL, which is where
  `rtb update` looks for a new version, and its `Repo` type runs git
  operations on `gix`.
* **AI.** `genai` covers OpenAI, Gemini, Ollama and OpenAI-compatible
  endpoints; Anthropic goes through a direct `reqwest` client so prompt
  caching, extended thinking and citations work. Structured output is
  validated with `jsonschema`.
* **MCP.** Built on the official `rmcp` SDK. A command opts in by
  overriding `Command::mcp_exposed()` to return `true`, and can describe
  its arguments to MCP clients through `Command::mcp_input_schema()`.
  `mcp serve` (stdio by default) and `mcp list` ship as built-in
  subcommands.
* **TUI docs browser.** `ratatui` with `tui-markdown`, plus streaming AI Q&A.
* **Credentials.** `keyring` wrapped in a `CredentialStore` trait, with
  `secrecy::SecretString` end-to-end, zeroed on drop.
* **Telemetry.** Opt-in, salted `machine-uid`, pluggable sinks
  (noop, file, HTTP, OTLP).

## Workspace layout

Only the umbrella crate and the `rtb` binary live here. Every other
crate is a standalone module of the
[phpboyscout Rust toolkit](https://rust.phpboyscout.uk), consumed here
as an ordinary dependency:

* **Foundations:** `rtb-error`, `rtb-redact`, `rtb-config`,
  `rtb-assets`, `rtb-app`, `rtb-tui`, `rtb-credentials`,
  `rtb-telemetry`, `rtb-test-support`, `rtb-chat` (formerly `rtb-ai`),
  `rtb-forge` (formerly `rtb-vcs`).
* **CLI family:** `rtb-cli`, `rtb-update`, `rtb-docs`, `rtb-mcp`,
  in [`phpboyscout/rust/cli`]https://gitlab.com/phpboyscout/rust/cli.

```text
rust-tool-base/
├── Cargo.toml                      # workspace root
├── crates/
│   ├── rtb/                        # umbrella crate (public API)
│   └── rtb-cli-bin/                # the `rtb` scaffolder binary
├── examples/
│   └── minimal/                    # smoke-test tool
├── docs/                           # user-facing documentation
│                                   # (specs live in the project wiki)
├── .gitlab-ci.yml                  # CI + the tagged `dist` release stage
├── deny.toml                       # cargo-deny policy
├── rustfmt.toml
├── rust-toolchain.toml             # pinned stable
├── justfile                        # common tasks
├── LICENSE                         # MIT
└── SECURITY.md
```

## Quick start

A working end-to-end example lives at
[`examples/minimal`](examples/minimal/src/main.rs). Run it with:

```console
$ cargo run -p rtb-example-minimal -- version
$ cargo run -p rtb-example-minimal -- greet
$ cargo run -p rtb-example-minimal -- doctor
$ cargo run -p rtb-example-minimal -- --help
```

### Anatomy

A minimal RTB-based tool has three parts: (1) a custom `Command`
implementation, (2) a `linkme` registration that inserts it into the
framework's command slice, and (3) an `Application::builder()` call
in `main`.

```rust,no_run
use async_trait::async_trait;
use linkme::distributed_slice;
use rtb::core::app::App;
use rtb::core::command::{BUILTIN_COMMANDS, Command, CommandSpec};
use rtb::prelude::*;

// 1. Implement the Command trait.
struct Greet;

#[async_trait]
impl Command for Greet {
    fn spec(&self) -> &CommandSpec {
        static SPEC: CommandSpec = CommandSpec {
            name: "greet",
            about: "Print a friendly greeting",
            aliases: &["hi"],
            ..CommandSpec::DEFAULT
        };
        &SPEC
    }

    async fn run(&self, app: App) -> miette::Result<()> {
        println!("hello from {}", app.metadata.name);
        Ok(())
    }
}

// 2. Register into the framework's command slice at link time.
#[distributed_slice(BUILTIN_COMMANDS)]
fn register_greet() -> Box<dyn Command> { Box::new(Greet) }

// 3. Wire the Application.
#[tokio::main]
async fn main() -> miette::Result<()> {
    rtb::cli::Application::builder()
        .metadata(
            ToolMetadata::builder()
                .name("mytool")
                .summary("my CLI tool")
                .build(),
        )
        .version(version_info!())
        .build()?
        .run()
        .await
}
```

`version_info!()` has to be a macro: it expands `env!("CARGO_PKG_VERSION")`
at *your* call site, so it reports your tool's version. A function
couldn't: its `env!` would expand inside the framework and report the
framework's version instead.

Downstream tools need the framework, `tokio`, `miette`, `async-trait` and
`linkme` as direct dependencies. The framework publishes as
`rust-tool-base` and the examples import it as `rtb`:

```toml
[dependencies]
rtb = { package = "rust-tool-base", version = "0.9" }
```

`linkme` must be direct because its `#[distributed_slice]` attribute
expands to `::linkme::…` paths.

## Development

```bash
just ci              # fmt, clippy, rustdoc, tests, cargo-deny, coverage
just check           # cargo check, all targets, default features
just rtb -- --help   # run the scaffolder CLI
```

`just pre-push` is the merge-request gate without coverage. AGENTS.md
explains why it needs `cargo-nextest`.

## License

MIT. See [LICENSE](LICENSE).