1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{self, Write as _};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Stage {
11 Experimental,
12 Beta,
13 Stable,
14 Deprecated,
15 Removed,
16}
17
18impl Stage {
19 pub fn as_str(self) -> &'static str {
20 match self {
21 Self::Experimental => "experimental",
22 Self::Beta => "beta",
23 Self::Stable => "stable",
24 Self::Deprecated => "deprecated",
25 Self::Removed => "removed",
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum Feature {
33 ShellTool,
35 Subagents,
37 WebSearch,
39 ApplyPatch,
41 Mcp,
43 ExecPolicy,
45}
46
47impl fmt::Display for Stage {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(self.as_str())
50 }
51}
52
53impl Feature {
54 pub fn key(self) -> &'static str {
55 self.info().key
56 }
57
58 pub fn stage(self) -> Stage {
59 self.info().stage
60 }
61
62 pub fn default_enabled(self) -> bool {
63 self.info().default_enabled
64 }
65
66 fn info(self) -> &'static FeatureSpec {
67 FEATURES
68 .iter()
69 .find(|spec| spec.id == self)
70 .unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
71 }
72}
73
74#[derive(Debug, Clone, Default, PartialEq)]
76pub struct Features {
77 enabled: BTreeSet<Feature>,
78}
79
80impl Features {
81 pub fn with_defaults() -> Self {
83 let mut set = BTreeSet::new();
84 for spec in FEATURES {
85 if spec.default_enabled {
86 set.insert(spec.id);
87 }
88 }
89 Self { enabled: set }
90 }
91
92 pub fn enabled(&self, feature: Feature) -> bool {
93 self.enabled.contains(&feature)
94 }
95
96 pub fn enable(&mut self, feature: Feature) -> &mut Self {
97 self.enabled.insert(feature);
98 self
99 }
100
101 pub fn disable(&mut self, feature: Feature) -> &mut Self {
102 self.enabled.remove(&feature);
103 self
104 }
105
106 pub fn apply_map(&mut self, entries: &BTreeMap<String, bool>) {
107 for (key, enabled) in entries {
108 if let Some(feature) = feature_from_key(key) {
109 if *enabled {
110 self.enable(feature);
111 } else {
112 self.disable(feature);
113 }
114 }
115 }
116 }
117
118 pub fn enabled_features(&self) -> Vec<Feature> {
119 let mut list: Vec<_> = self.enabled.iter().copied().collect();
120 list.sort();
121 list
122 }
123}
124
125pub fn is_known_feature_key(key: &str) -> bool {
127 FEATURES.iter().any(|spec| spec.key == key)
128}
129
130pub fn feature_from_key(key: &str) -> Option<Feature> {
131 FEATURES
132 .iter()
133 .find(|spec| spec.key == key)
134 .map(|spec| spec.id)
135}
136
137pub fn feature_spec_by_key(key: &str) -> Option<&'static FeatureSpec> {
138 FEATURES.iter().find(|spec| spec.key == key)
139}
140
141pub fn render_feature_table(features: &Features) -> String {
142 let mut output = String::from("feature\tstage\tenabled\n");
143 for spec in FEATURES {
144 let _ = writeln!(
145 output,
146 "{}\t{}\t{}",
147 spec.key,
148 spec.stage,
149 features.enabled(spec.id)
150 );
151 }
152 output
153}
154
155#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
157pub struct FeaturesToml {
158 #[serde(flatten)]
159 pub entries: BTreeMap<String, bool>,
160}
161
162#[derive(Debug, Clone, Copy)]
164pub struct FeatureSpec {
165 pub id: Feature,
166 pub key: &'static str,
167 pub stage: Stage,
168 pub default_enabled: bool,
169}
170
171pub const FEATURES: &[FeatureSpec] = &[
172 FeatureSpec {
173 id: Feature::ShellTool,
174 key: "shell_tool",
175 stage: Stage::Stable,
176 default_enabled: true,
177 },
178 FeatureSpec {
179 id: Feature::Subagents,
180 key: "subagents",
181 stage: Stage::Experimental,
182 default_enabled: true,
183 },
184 FeatureSpec {
185 id: Feature::WebSearch,
186 key: "web_search",
187 stage: Stage::Experimental,
188 default_enabled: true,
189 },
190 FeatureSpec {
191 id: Feature::ApplyPatch,
192 key: "apply_patch",
193 stage: Stage::Experimental,
194 default_enabled: true,
195 },
196 FeatureSpec {
197 id: Feature::Mcp,
198 key: "mcp",
199 stage: Stage::Experimental,
200 default_enabled: true,
201 },
202 FeatureSpec {
203 id: Feature::ExecPolicy,
204 key: "exec_policy",
205 stage: Stage::Experimental,
206 default_enabled: true,
207 },
208];