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//! ```no_run
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//! ..CommandSpec::DEFAULT
23//! };
24//! &SPEC
25//! }
26//!
27//! async fn run(&self, _app: rtb_app::app::App) -> miette::Result<()> {
28//! Ok(())
29//! }
30//! }
31//!
32//! #[distributed_slice(BUILTIN_COMMANDS)]
33//! fn __register_deploy() -> Box<dyn Command> { Box::new(Deploy) }
34//! ```
35//!
36//! `rtb-cli::Application::run` iterates `BUILTIN_COMMANDS` at startup,
37//! filters by the runtime `Features` set, and registers each remaining
38//! command with clap.
39
40use async_trait::async_trait;
41use linkme::distributed_slice;
42
43use crate::app::App;
44use crate::features::Feature;
45
46/// Static descriptor of a [`Command`].
47///
48/// Every field is `'static` because commands are compile-time entities —
49/// runtime-generated subcommands are a separate (unimplemented) concern.
50///
51/// Construct with struct-update over [`CommandSpec::DEFAULT`] so new
52/// optional fields do not churn every literal:
53///
54/// ```no_run
55/// # use rtb_app::command::CommandSpec;
56/// static SPEC: CommandSpec = CommandSpec {
57/// name: "deploy",
58/// about: "Deploy the thing",
59/// ..CommandSpec::DEFAULT
60/// };
61/// ```
62#[derive(Debug, Clone, Copy)]
63pub struct CommandSpec {
64 /// The subcommand name as it appears on the CLI (`mytool deploy`).
65 pub name: &'static str,
66
67 /// One-line summary shown in `--help`.
68 pub about: &'static str,
69
70 /// Alternative names accepted on the CLI. Displayed in help text.
71 pub aliases: &'static [&'static str],
72
73 /// If `Some`, the command is only visible when the runtime
74 /// [`Features`](crate::features::Features) set has this feature
75 /// enabled. Unconditional commands leave this `None`.
76 pub feature: Option<Feature>,
77
78 /// Single-character short-flag alias — `mytool -d` invokes the command
79 /// like `mytool deploy`. `None` for most commands; set by
80 /// `rtb generate command --short`.
81 pub short: Option<char>,
82
83 /// Long-form help shown in the command's own `--help`. `None` falls
84 /// back to [`about`](Self::about); set by
85 /// `rtb generate command --long`.
86 pub long_about: Option<&'static str>,
87}
88
89impl CommandSpec {
90 /// A zero-valued descriptor for struct-update construction — see the
91 /// [type docs](Self). Only the fields that differ need be set at each
92 /// call site, so adding a new optional field later touches no existing
93 /// literal.
94 pub const DEFAULT: Self =
95 Self { name: "", about: "", aliases: &[], feature: None, short: None, long_about: None };
96}
97
98/// The contract every CLI subcommand implements.
99///
100/// Implementations are typically registered via the
101/// [`BUILTIN_COMMANDS`] distributed slice. `rtb-cli` provides a
102/// `#[rtb::command]` attribute macro that derives the boilerplate for
103/// downstream tools; hand-written impls follow the example in the
104/// module docs.
105#[async_trait]
106pub trait Command: Send + Sync + 'static {
107 /// The command's static descriptor.
108 fn spec(&self) -> &CommandSpec;
109
110 /// Execute the command. `app` is taken by value — `Clone` on `App`
111 /// is O(1) so subcommands that fan out can `.clone()` freely.
112 async fn run(&self, app: App) -> miette::Result<()>;
113
114 /// When `true`, `rtb-cli`'s top-level clap parser passes every
115 /// argument after `<name>` through to [`Self::run`] without
116 /// further validation. Commands that own their own clap subtree
117 /// (e.g. `docs list / show / browse / serve`, `update check / run`)
118 /// opt into this so the inner parser can produce its own help
119 /// and error messages.
120 ///
121 /// Defaults to `false` — most commands let the framework reject
122 /// unknown args at the outer layer.
123 fn subcommand_passthrough(&self) -> bool {
124 false
125 }
126
127 /// When `true`, this command is registered as an MCP tool by
128 /// `rtb_mcp::McpServer`. Defaults to `false` — additive trait
129 /// method, no impact on existing impls.
130 fn mcp_exposed(&self) -> bool {
131 false
132 }
133
134 /// Optional JSON Schema for the command's arguments — surfaced
135 /// to MCP clients in the tool listing. Default: `None`. Tool
136 /// authors with `clap::Args` structs typically derive this via
137 /// `serde_json::to_value(schemars::schema_for!(MyArgs))`.
138 fn mcp_input_schema(&self) -> Option<serde_json::Value> {
139 None
140 }
141}
142
143/// Link-time registry of [`Command`] factory functions.
144///
145/// The factories are thin — each produces a fresh `Box<dyn Command>`
146/// when invoked by `rtb-cli::Application`. They are expected to be
147/// cheap (no I/O, no allocation beyond the box). Heavy work belongs in
148/// `Command::run`.
149///
150/// See the module docs for the registration pattern.
151#[distributed_slice]
152pub static BUILTIN_COMMANDS: [fn() -> Box<dyn Command>];
153
154/// A boxed, `Send` future returned by a [`PreRunHook`].
155pub type PreRunFuture =
156 std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<()>> + Send>>;
157
158/// A pre-run hook, invoked with the [`App`] before command dispatch.
159///
160/// Runs after CLI parsing and before the matched command. Leaf crates
161/// register cross-cutting pre-dispatch logic here (e.g. the self-update
162/// policy check) via [`distributed_slice`], keeping `rtb-cli` decoupled
163/// from them — exactly like [`BUILTIN_COMMANDS`].
164///
165/// **Contract:** a hook must be fast and **order-independent** — link-time
166/// registration order across crates is not deterministic. A hook returning
167/// `Err` aborts the run before the command executes, so advisory hooks that
168/// must not block the command should swallow their own errors.
169pub type PreRunHook = fn(App) -> PreRunFuture;
170
171/// Link-time registry of [`PreRunHook`]s run before command dispatch.
172///
173/// `rtb-cli::Application::run` awaits each registered hook (in unspecified
174/// order) after parsing succeeds and before dispatching the matched
175/// command. See [`PreRunHook`] for the contract.
176#[distributed_slice]
177pub static BUILTIN_PRERUN_HOOKS: [PreRunHook];