rtb_app/features.rs
1//! Runtime feature gating for built-in subcommands.
2//!
3//! Compile-time selection is handled by Cargo features on the `rtb`
4//! umbrella crate. Runtime gating — "this tool compiled the `update`
5//! command in, but this particular invocation wants to hide it" — lives
6//! here.
7
8use std::collections::HashSet;
9
10/// Built-in feature identifiers that can be toggled at runtime.
11///
12/// `#[non_exhaustive]` keeps variant additions a minor-version change
13/// for downstream matchers.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum Feature {
17 /// The `init` bootstrap command.
18 Init,
19 /// The `version` command.
20 Version,
21 /// Self-update (`update` subcommand and the pre-run version check).
22 Update,
23 /// TUI documentation browser (`docs` subcommand).
24 Docs,
25 /// MCP server (`mcp` subcommand).
26 Mcp,
27 /// Health-check diagnostics (`doctor` subcommand).
28 Doctor,
29 /// AI-powered features (`docs ask`, agentic flows).
30 Ai,
31 /// Opt-in anonymous telemetry.
32 Telemetry,
33 /// Runtime config get/set (`config` subcommand).
34 Config,
35 /// Structured release-notes display (`changelog` subcommand).
36 Changelog,
37 /// Credential management (`credentials` subcommand subtree).
38 /// Default-on; gates the v0.4 `credentials list / add / remove
39 /// / test / doctor` subcommands.
40 Credentials,
41}
42
43impl Feature {
44 /// Features enabled by default when no explicit overrides are supplied.
45 /// Mirrors [`Features::default`].
46 #[must_use]
47 pub fn defaults() -> Features {
48 Features::builder().build()
49 }
50
51 /// Every defined variant. Useful for debug introspection.
52 ///
53 /// Returns a slice rather than a fixed-size array because
54 /// [`Feature`] is `#[non_exhaustive]` — adding a variant is a
55 /// minor-version change that must not break downstream callers'
56 /// type signatures. A slice length is a value, not part of the
57 /// type.
58 #[must_use]
59 pub const fn all() -> &'static [Self] {
60 &[
61 Self::Init,
62 Self::Version,
63 Self::Update,
64 Self::Docs,
65 Self::Mcp,
66 Self::Doctor,
67 Self::Ai,
68 Self::Telemetry,
69 Self::Config,
70 Self::Changelog,
71 Self::Credentials,
72 ]
73 }
74}
75
76/// Immutable set of runtime-enabled features.
77///
78/// Construct via [`Features::default`] for the documented defaults, or
79/// via [`Features::builder`] to override explicitly.
80///
81/// The default set — `Init`, `Version`, `Update`, `Docs`, `Mcp`,
82/// `Doctor`, `Credentials`, `Telemetry`, `Config` — matches the
83/// Cargo default feature set of the `rtb` umbrella crate; runtime
84/// gating only hides commands that are already compiled in.
85#[derive(Debug, Clone)]
86pub struct Features {
87 enabled: HashSet<Feature>,
88}
89
90impl Default for Features {
91 fn default() -> Self {
92 Self::builder().build()
93 }
94}
95
96impl Features {
97 /// `true` iff `feature` is in the enabled set.
98 #[must_use]
99 pub fn is_enabled(&self, feature: Feature) -> bool {
100 self.enabled.contains(&feature)
101 }
102
103 /// Start a builder pre-populated with the default feature set.
104 #[must_use]
105 pub fn builder() -> FeaturesBuilder {
106 FeaturesBuilder::new()
107 }
108
109 /// Iterate the enabled features in arbitrary order.
110 pub fn iter(&self) -> impl Iterator<Item = Feature> + '_ {
111 self.enabled.iter().copied()
112 }
113}
114
115/// Builder for [`Features`]. Pre-populated with the default feature set
116/// via [`FeaturesBuilder::new`]; start empty with [`FeaturesBuilder::none`].
117#[derive(Debug, Default)]
118pub struct FeaturesBuilder {
119 enabled: HashSet<Feature>,
120}
121
122impl FeaturesBuilder {
123 /// Start a builder pre-populated with the default feature set:
124 /// `Init`, `Version`, `Update`, `Docs`, `Mcp`, `Doctor`,
125 /// `Credentials`, `Telemetry`, `Config`.
126 #[must_use]
127 pub fn new() -> Self {
128 Self {
129 enabled: [
130 Feature::Init,
131 Feature::Version,
132 Feature::Update,
133 Feature::Docs,
134 Feature::Mcp,
135 Feature::Doctor,
136 Feature::Credentials,
137 Feature::Telemetry,
138 Feature::Config,
139 ]
140 .into_iter()
141 .collect(),
142 }
143 }
144
145 /// Start with an empty enabled set.
146 #[must_use]
147 pub fn none() -> Self {
148 Self { enabled: HashSet::new() }
149 }
150
151 /// Add `feature` to the enabled set.
152 #[must_use]
153 pub fn enable(mut self, feature: Feature) -> Self {
154 self.enabled.insert(feature);
155 self
156 }
157
158 /// Remove `feature` from the enabled set.
159 #[must_use]
160 pub fn disable(mut self, feature: Feature) -> Self {
161 self.enabled.remove(&feature);
162 self
163 }
164
165 /// Finalise the builder.
166 #[must_use]
167 pub fn build(self) -> Features {
168 Features { enabled: self.enabled }
169 }
170}