rtb_app/command.rs
1//! The [`Command`] trait, its descriptor, and the link-time registration slice.
2//!
3//! A `Command` is an opinionated `async fn(App) -> Result<()>` bundled
4//! with a small static descriptor ([`CommandSpec`]). Commands self-
5//! register via a [`linkme`] distributed slice so no manual wiring is
6//! needed when authoring new commands.
7//!
8//! # Registration pattern
9//!
10//! ```ignore
11//! use rtb_app::command::{BUILTIN_COMMANDS, Command, CommandSpec};
12//! use rtb_app::linkme::distributed_slice;
13//!
14//! pub struct Deploy;
15//!
16//! #[async_trait::async_trait]
17//! impl Command for Deploy {
18//! fn spec(&self) -> &CommandSpec {
19//! static SPEC: CommandSpec = CommandSpec {
20//! name: "deploy",
21//! about: "Deploy the thing",
22//! aliases: &[],
23//! feature: None,
24//! };
25//! &SPEC
26//! }
27//!
28//! async fn run(&self, _app: rtb_app::app::App) -> miette::Result<()> {
29//! Ok(())
30//! }
31//! }
32//!
33//! #[distributed_slice(BUILTIN_COMMANDS)]
34//! fn __register_deploy() -> Box<dyn Command> { Box::new(Deploy) }
35//! ```
36//!
37//! `rtb-cli::Application::run` iterates `BUILTIN_COMMANDS` at startup,
38//! filters by the runtime `Features` set, and registers each remaining
39//! command with clap.
40
41use async_trait::async_trait;
42use linkme::distributed_slice;
43
44use crate::app::App;
45use crate::features::Feature;
46
47/// Static descriptor of a [`Command`].
48///
49/// Every field is `'static` because commands are compile-time entities —
50/// runtime-generated subcommands are a separate (unimplemented) concern.
51#[derive(Debug, Clone)]
52pub struct CommandSpec {
53 /// The subcommand name as it appears on the CLI (`mytool deploy`).
54 pub name: &'static str,
55
56 /// One-line summary shown in `--help`.
57 pub about: &'static str,
58
59 /// Alternative names accepted on the CLI. Displayed in help text.
60 pub aliases: &'static [&'static str],
61
62 /// If `Some`, the command is only visible when the runtime
63 /// [`Features`](crate::features::Features) set has this feature
64 /// enabled. Unconditional commands leave this `None`.
65 pub feature: Option<Feature>,
66}
67
68/// The contract every CLI subcommand implements.
69///
70/// Implementations are typically registered via the
71/// [`BUILTIN_COMMANDS`] distributed slice. `rtb-cli` provides a
72/// `#[rtb::command]` attribute macro that derives the boilerplate for
73/// downstream tools; hand-written impls follow the example in the
74/// module docs.
75#[async_trait]
76pub trait Command: Send + Sync + 'static {
77 /// The command's static descriptor.
78 fn spec(&self) -> &CommandSpec;
79
80 /// Execute the command. `app` is taken by value — `Clone` on `App`
81 /// is O(1) so subcommands that fan out can `.clone()` freely.
82 async fn run(&self, app: App) -> miette::Result<()>;
83
84 /// When `true`, `rtb-cli`'s top-level clap parser passes every
85 /// argument after `<name>` through to [`Self::run`] without
86 /// further validation. Commands that own their own clap subtree
87 /// (e.g. `docs list / show / browse / serve`, `update check / run`)
88 /// opt into this so the inner parser can produce its own help
89 /// and error messages.
90 ///
91 /// Defaults to `false` — most commands let the framework reject
92 /// unknown args at the outer layer.
93 fn subcommand_passthrough(&self) -> bool {
94 false
95 }
96
97 /// When `true`, this command is registered as an MCP tool by
98 /// `rtb_mcp::McpServer`. Defaults to `false` — additive trait
99 /// method, no impact on existing impls.
100 fn mcp_exposed(&self) -> bool {
101 false
102 }
103
104 /// Optional JSON Schema for the command's arguments — surfaced
105 /// to MCP clients in the tool listing. Default: `None`. Tool
106 /// authors with `clap::Args` structs typically derive this via
107 /// `serde_json::to_value(schemars::schema_for!(MyArgs))`.
108 fn mcp_input_schema(&self) -> Option<serde_json::Value> {
109 None
110 }
111}
112
113/// Link-time registry of [`Command`] factory functions.
114///
115/// The factories are thin — each produces a fresh `Box<dyn Command>`
116/// when invoked by `rtb-cli::Application`. They are expected to be
117/// cheap (no I/O, no allocation beyond the box). Heavy work belongs in
118/// `Command::run`.
119///
120/// See the module docs for the registration pattern.
121#[distributed_slice]
122pub static BUILTIN_COMMANDS: [fn() -> Box<dyn Command>];