1use std::collections::{HashMap, HashSet};
8use std::sync::Mutex;
9
10use crate::plugin::types::{LoadedPlugin, PluginManifest};
11use crate::types::plugin::BuiltinPluginDefinition;
12
13const BUILTIN_MARKETPLACE_NAME: &str = "builtin";
14
15static BUILTIN_PLUGINS: Mutex<Vec<BuiltinPluginDefinition>> = Mutex::new(Vec::new());
16
17pub const BUILTIN_MARKETPLACE_NAME_CONST: &str = BUILTIN_MARKETPLACE_NAME;
19
20pub fn register_builtin_plugin(definition: BuiltinPluginDefinition) {
22 let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
23 plugins.push(definition);
24}
25
26pub fn is_builtin_plugin_id(plugin_id: &str) -> bool {
28 plugin_id.ends_with(&format!("@{}", BUILTIN_MARKETPLACE_NAME))
29}
30
31pub fn get_builtin_plugin_definition(name: &str) -> Option<BuiltinPluginSummary> {
35 let plugins = BUILTIN_PLUGINS.lock().unwrap();
36 plugins
37 .iter()
38 .find(|p| p.name == name)
39 .map(|d| BuiltinPluginSummary {
40 name: d.name.clone(),
41 description: d.description.clone(),
42 version: d.version.clone(),
43 has_skills: d.skills.is_some(),
44 has_hooks: d.hooks.is_some(),
45 has_mcp_servers: d.mcp_servers.is_some(),
46 default_enabled: d.default_enabled,
47 })
48}
49
50#[derive(Debug, Clone)]
52pub struct BuiltinPluginSummary {
53 pub name: String,
54 pub description: String,
55 pub version: Option<String>,
56 pub has_skills: bool,
57 pub has_hooks: bool,
58 pub has_mcp_servers: bool,
59 pub default_enabled: Option<bool>,
60}
61
62#[derive(Debug, Default)]
64pub struct BuiltinPluginResult {
65 pub enabled: Vec<LoadedPlugin>,
66 pub disabled: Vec<LoadedPlugin>,
67}
68
69pub fn get_builtin_plugins() -> BuiltinPluginResult {
73 let plugins = BUILTIN_PLUGINS.lock().unwrap();
74 let mut enabled = Vec::new();
75 let mut disabled = Vec::new();
76
77 let user_enabled_plugins = load_user_enabled_plugins();
79
80 for definition in plugins.iter() {
81 if let Some(is_avail) = &definition.is_available {
83 if !is_avail() {
84 continue;
85 }
86 }
87
88 let plugin_id = format!("{}@{}", definition.name, BUILTIN_MARKETPLACE_NAME);
89 let user_setting = user_enabled_plugins.get(&plugin_id);
90
91 let is_enabled = match user_setting {
93 Some(&true) => true,
94 Some(&false) => false,
95 None => definition.default_enabled.unwrap_or(true),
96 };
97
98 let plugin = LoadedPlugin {
99 name: definition.name.clone(),
100 manifest: PluginManifest {
101 name: definition.name.clone(),
102 version: definition.version.clone(),
103 description: Some(definition.description.clone()),
104 author: None,
105 homepage: None,
106 repository: None,
107 license: None,
108 keywords: None,
109 dependencies: None,
110 commands: None,
111 agents: None,
112 skills: None,
113 hooks: None,
114 output_styles: None,
115 channels: None,
116 mcp_servers: None,
117 lsp_servers: None,
118 settings: None,
119 user_config: None,
120 },
121 path: BUILTIN_MARKETPLACE_NAME.to_string(),
122 source: plugin_id.clone(),
123 repository: plugin_id,
124 enabled: Some(is_enabled),
125 is_builtin: Some(true),
126 sha: None,
127 commands_path: None,
128 commands_paths: None,
129 commands_metadata: None,
130 agents_path: None,
131 agents_paths: None,
132 skills_path: None,
133 skills_paths: None,
134 output_styles_path: None,
135 output_styles_paths: None,
136 hooks_config: definition.hooks.clone(),
137 mcp_servers: definition.mcp_servers.clone(),
138 lsp_servers: None,
139 settings: None,
140 };
141
142 if is_enabled {
143 enabled.push(plugin);
144 } else {
145 disabled.push(plugin);
146 }
147 }
148
149 BuiltinPluginResult { enabled, disabled }
150}
151
152fn load_user_enabled_plugins() -> HashMap<String, bool> {
155 let settings_dir = match std::env::var("AI_CODE_CONFIG_HOME") {
156 Ok(dir) => dir,
157 Err(_) => {
158 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
159 format!("{}/.ai", home)
160 }
161 };
162
163 let settings_path = format!("{}/settings.json", settings_dir);
164 let content = match std::fs::read_to_string(&settings_path) {
165 Ok(c) => c,
166 Err(_) => return HashMap::new(),
167 };
168
169 let settings: serde_json::Value = match serde_json::from_str(&content) {
170 Ok(v) => v,
171 Err(_) => return HashMap::new(),
172 };
173
174 let mut result = HashMap::new();
175 if let Some(enabled_plugins) = settings.get("enabledPlugins").and_then(|v| v.as_object()) {
176 for (plugin_id, enabled) in enabled_plugins {
177 if let Some(val) = enabled.as_bool() {
178 result.insert(plugin_id.clone(), val);
179 }
180 }
181 }
182
183 result
184}
185
186pub fn get_builtin_plugin_skill_definitions() -> Vec<String> {
190 let BuiltinPluginResult { enabled, .. } = get_builtin_plugins();
191
192 let enabled_names: HashSet<&str> = enabled.iter().map(|p| p.name.as_str()).collect();
194
195 let plugins = BUILTIN_PLUGINS.lock().unwrap();
196 plugins
197 .iter()
198 .filter(|d| enabled_names.contains(d.name.as_str()) && d.skills.is_some())
199 .map(|d| d.name.clone())
200 .collect()
201}
202
203pub fn clear_builtin_plugins() {
205 let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
206 plugins.clear();
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_register_and_get_builtin_plugin() {
215 clear_builtin_plugins();
216
217 let definition = BuiltinPluginDefinition {
218 name: "test-plugin".to_string(),
219 description: "A test built-in plugin".to_string(),
220 version: Some("1.0.0".to_string()),
221 skills: None,
222 hooks: None,
223 mcp_servers: None,
224 is_available: None,
225 default_enabled: Some(true),
226 };
227
228 register_builtin_plugin(definition);
229
230 let result = get_builtin_plugin_definition("test-plugin");
231 assert!(result.is_some());
232 assert_eq!(result.unwrap().description, "A test built-in plugin");
233
234 clear_builtin_plugins();
235 }
236
237 #[test]
238 fn test_is_builtin_plugin_id() {
239 assert!(is_builtin_plugin_id("my-plugin@builtin"));
240 assert!(!is_builtin_plugin_id("my-plugin@marketplace"));
241 assert!(!is_builtin_plugin_id("my-plugin"));
242 }
243
244 #[test]
245 fn test_get_builtin_plugins_enabled_disabled() {
246 clear_builtin_plugins();
247
248 let enabled_plugin = BuiltinPluginDefinition {
249 name: "enabled-plugin".to_string(),
250 description: "Should be enabled".to_string(),
251 version: None,
252 skills: None,
253 hooks: None,
254 mcp_servers: None,
255 is_available: None,
256 default_enabled: Some(true),
257 };
258
259 let disabled_plugin = BuiltinPluginDefinition {
260 name: "disabled-plugin".to_string(),
261 description: "Should be disabled".to_string(),
262 version: None,
263 skills: None,
264 hooks: None,
265 mcp_servers: None,
266 is_available: None,
267 default_enabled: Some(false),
268 };
269
270 register_builtin_plugin(enabled_plugin);
271 register_builtin_plugin(disabled_plugin);
272
273 let result = get_builtin_plugins();
274 assert_eq!(result.enabled.len(), 1);
275 assert_eq!(result.disabled.len(), 1);
276 assert_eq!(result.enabled[0].name, "enabled-plugin");
277 assert_eq!(result.disabled[0].name, "disabled-plugin");
278
279 clear_builtin_plugins();
280 }
281
282 #[test]
283 fn test_get_builtin_plugins_filters_unavailable() {
284 clear_builtin_plugins();
285
286 let unavailable = BuiltinPluginDefinition {
287 name: "unavailable-plugin".to_string(),
288 description: "Should be filtered".to_string(),
289 version: None,
290 skills: None,
291 hooks: None,
292 mcp_servers: None,
293 is_available: Some(Box::new(|| false)),
294 default_enabled: Some(true),
295 };
296
297 let available = BuiltinPluginDefinition {
298 name: "available-plugin".to_string(),
299 description: "Should be included".to_string(),
300 version: None,
301 skills: None,
302 hooks: None,
303 mcp_servers: None,
304 is_available: Some(Box::new(|| true)),
305 default_enabled: Some(true),
306 };
307
308 register_builtin_plugin(unavailable);
309 register_builtin_plugin(available);
310
311 let result = get_builtin_plugins();
312 assert_eq!(result.enabled.len(), 1);
313 assert_eq!(result.enabled[0].name, "available-plugin");
314
315 clear_builtin_plugins();
316 }
317
318 #[test]
319 fn test_clear_builtin_plugins() {
320 clear_builtin_plugins();
321
322 let definition = BuiltinPluginDefinition {
323 name: "to-clear".to_string(),
324 description: "Will be cleared".to_string(),
325 version: None,
326 skills: None,
327 hooks: None,
328 mcp_servers: None,
329 is_available: None,
330 default_enabled: None,
331 };
332
333 register_builtin_plugin(definition);
334 assert!(get_builtin_plugin_definition("to-clear").is_some());
335
336 clear_builtin_plugins();
337 assert!(get_builtin_plugin_definition("to-clear").is_none());
338 }
339}