use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use crate::plugin::types::{LoadedPlugin, PluginManifest};
use crate::types::plugin::BuiltinPluginDefinition;
const BUILTIN_MARKETPLACE_NAME: &str = "builtin";
static BUILTIN_PLUGINS: Mutex<Vec<BuiltinPluginDefinition>> = Mutex::new(Vec::new());
pub const BUILTIN_MARKETPLACE_NAME_CONST: &str = BUILTIN_MARKETPLACE_NAME;
pub fn register_builtin_plugin(definition: BuiltinPluginDefinition) {
let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
plugins.push(definition);
}
pub fn is_builtin_plugin_id(plugin_id: &str) -> bool {
plugin_id.ends_with(&format!("@{}", BUILTIN_MARKETPLACE_NAME))
}
pub fn get_builtin_plugin_definition(name: &str) -> Option<BuiltinPluginSummary> {
let plugins = BUILTIN_PLUGINS.lock().unwrap();
plugins
.iter()
.find(|p| p.name == name)
.map(|d| BuiltinPluginSummary {
name: d.name.clone(),
description: d.description.clone(),
version: d.version.clone(),
has_skills: d.skills.is_some(),
has_hooks: d.hooks.is_some(),
has_mcp_servers: d.mcp_servers.is_some(),
default_enabled: d.default_enabled,
})
}
#[derive(Debug, Clone)]
pub struct BuiltinPluginSummary {
pub name: String,
pub description: String,
pub version: Option<String>,
pub has_skills: bool,
pub has_hooks: bool,
pub has_mcp_servers: bool,
pub default_enabled: Option<bool>,
}
#[derive(Debug, Default)]
pub struct BuiltinPluginResult {
pub enabled: Vec<LoadedPlugin>,
pub disabled: Vec<LoadedPlugin>,
}
pub fn get_builtin_plugins() -> BuiltinPluginResult {
let plugins = BUILTIN_PLUGINS.lock().unwrap();
let mut enabled = Vec::new();
let mut disabled = Vec::new();
let user_enabled_plugins = load_user_enabled_plugins();
for definition in plugins.iter() {
if let Some(is_avail) = &definition.is_available {
if !is_avail() {
continue;
}
}
let plugin_id = format!("{}@{}", definition.name, BUILTIN_MARKETPLACE_NAME);
let user_setting = user_enabled_plugins.get(&plugin_id);
let is_enabled = match user_setting {
Some(&true) => true,
Some(&false) => false,
None => definition.default_enabled.unwrap_or(true),
};
let plugin = LoadedPlugin {
name: definition.name.clone(),
manifest: PluginManifest {
name: definition.name.clone(),
version: definition.version.clone(),
description: Some(definition.description.clone()),
author: None,
homepage: None,
repository: None,
license: None,
keywords: None,
dependencies: None,
commands: None,
agents: None,
skills: None,
hooks: None,
output_styles: None,
channels: None,
mcp_servers: None,
lsp_servers: None,
settings: None,
user_config: None,
},
path: BUILTIN_MARKETPLACE_NAME.to_string(),
source: plugin_id.clone(),
repository: plugin_id,
enabled: Some(is_enabled),
is_builtin: Some(true),
sha: None,
commands_path: None,
commands_paths: None,
commands_metadata: None,
agents_path: None,
agents_paths: None,
skills_path: None,
skills_paths: None,
output_styles_path: None,
output_styles_paths: None,
hooks_config: definition.hooks.clone(),
mcp_servers: definition.mcp_servers.clone(),
lsp_servers: None,
settings: None,
};
if is_enabled {
enabled.push(plugin);
} else {
disabled.push(plugin);
}
}
BuiltinPluginResult { enabled, disabled }
}
fn load_user_enabled_plugins() -> HashMap<String, bool> {
let settings_dir = match std::env::var("AI_CODE_CONFIG_HOME") {
Ok(dir) => dir,
Err(_) => {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
format!("{}/.ai", home)
}
};
let settings_path = format!("{}/settings.json", settings_dir);
let content = match std::fs::read_to_string(&settings_path) {
Ok(c) => c,
Err(_) => return HashMap::new(),
};
let settings: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => return HashMap::new(),
};
let mut result = HashMap::new();
if let Some(enabled_plugins) = settings.get("enabledPlugins").and_then(|v| v.as_object()) {
for (plugin_id, enabled) in enabled_plugins {
if let Some(val) = enabled.as_bool() {
result.insert(plugin_id.clone(), val);
}
}
}
result
}
pub fn get_builtin_plugin_skill_definitions() -> Vec<String> {
let BuiltinPluginResult { enabled, .. } = get_builtin_plugins();
let enabled_names: HashSet<&str> = enabled.iter().map(|p| p.name.as_str()).collect();
let plugins = BUILTIN_PLUGINS.lock().unwrap();
plugins
.iter()
.filter(|d| enabled_names.contains(d.name.as_str()) && d.skills.is_some())
.map(|d| d.name.clone())
.collect()
}
pub fn clear_builtin_plugins() {
let mut plugins = BUILTIN_PLUGINS.lock().unwrap();
plugins.clear();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_and_get_builtin_plugin() {
clear_builtin_plugins();
let definition = BuiltinPluginDefinition {
name: "test-plugin".to_string(),
description: "A test built-in plugin".to_string(),
version: Some("1.0.0".to_string()),
skills: None,
hooks: None,
mcp_servers: None,
is_available: None,
default_enabled: Some(true),
};
register_builtin_plugin(definition);
let result = get_builtin_plugin_definition("test-plugin");
assert!(result.is_some());
assert_eq!(result.unwrap().description, "A test built-in plugin");
clear_builtin_plugins();
}
#[test]
fn test_is_builtin_plugin_id() {
assert!(is_builtin_plugin_id("my-plugin@builtin"));
assert!(!is_builtin_plugin_id("my-plugin@marketplace"));
assert!(!is_builtin_plugin_id("my-plugin"));
}
#[test]
fn test_get_builtin_plugins_enabled_disabled() {
clear_builtin_plugins();
let enabled_plugin = BuiltinPluginDefinition {
name: "enabled-plugin".to_string(),
description: "Should be enabled".to_string(),
version: None,
skills: None,
hooks: None,
mcp_servers: None,
is_available: None,
default_enabled: Some(true),
};
let disabled_plugin = BuiltinPluginDefinition {
name: "disabled-plugin".to_string(),
description: "Should be disabled".to_string(),
version: None,
skills: None,
hooks: None,
mcp_servers: None,
is_available: None,
default_enabled: Some(false),
};
register_builtin_plugin(enabled_plugin);
register_builtin_plugin(disabled_plugin);
let result = get_builtin_plugins();
assert_eq!(result.enabled.len(), 1);
assert_eq!(result.disabled.len(), 1);
assert_eq!(result.enabled[0].name, "enabled-plugin");
assert_eq!(result.disabled[0].name, "disabled-plugin");
clear_builtin_plugins();
}
#[test]
fn test_get_builtin_plugins_filters_unavailable() {
clear_builtin_plugins();
let unavailable = BuiltinPluginDefinition {
name: "unavailable-plugin".to_string(),
description: "Should be filtered".to_string(),
version: None,
skills: None,
hooks: None,
mcp_servers: None,
is_available: Some(Box::new(|| false)),
default_enabled: Some(true),
};
let available = BuiltinPluginDefinition {
name: "available-plugin".to_string(),
description: "Should be included".to_string(),
version: None,
skills: None,
hooks: None,
mcp_servers: None,
is_available: Some(Box::new(|| true)),
default_enabled: Some(true),
};
register_builtin_plugin(unavailable);
register_builtin_plugin(available);
let result = get_builtin_plugins();
assert_eq!(result.enabled.len(), 1);
assert_eq!(result.enabled[0].name, "available-plugin");
clear_builtin_plugins();
}
#[test]
fn test_clear_builtin_plugins() {
clear_builtin_plugins();
let definition = BuiltinPluginDefinition {
name: "to-clear".to_string(),
description: "Will be cleared".to_string(),
version: None,
skills: None,
hooks: None,
mcp_servers: None,
is_available: None,
default_enabled: None,
};
register_builtin_plugin(definition);
assert!(get_builtin_plugin_definition("to-clear").is_some());
clear_builtin_plugins();
assert!(get_builtin_plugin_definition("to-clear").is_none());
}
}