use hashbrown::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use super::{PluginError, PluginId, PluginManifest, PluginResult};
use crate::config::PluginRuntimeConfig;
use crate::utils::file_utils::read_file_with_context;
#[derive(Debug, Clone, PartialEq)]
pub enum PluginState {
Active,
Installed,
Disabled,
Error,
}
#[derive(Debug, Clone)]
pub struct PluginHandle {
pub id: PluginId,
pub manifest: PluginManifest,
pub path: PathBuf,
pub state: PluginState,
pub loaded_at: Option<std::time::SystemTime>,
}
#[derive(Debug, Clone)]
pub struct PluginRuntime {
plugins: Arc<RwLock<HashMap<PluginId, PluginHandle>>>,
}
impl PluginRuntime {
pub fn new(_config: PluginRuntimeConfig, _base_dir: PathBuf) -> Self {
Self {
plugins: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<PluginHandle> {
if !plugin_path.exists() {
return Err(PluginError::NotFound(plugin_path.display().to_string()));
}
let manifest = self.load_manifest(plugin_path).await?;
self.validate_manifest(&manifest)?;
let handle = PluginHandle {
id: manifest.name.clone(),
manifest: manifest.clone(),
path: plugin_path.to_path_buf(),
state: PluginState::Active,
loaded_at: Some(std::time::SystemTime::now()),
};
{
let mut plugins = self.plugins.write().await;
plugins.insert(manifest.name.clone(), handle.clone());
}
Ok(handle)
}
async fn load_manifest(&self, plugin_path: &Path) -> PluginResult<PluginManifest> {
let manifest_path = plugin_path.join(".vtcode-plugin/plugin.json");
if !manifest_path.exists() {
return Err(PluginError::ManifestValidationError(format!(
"Plugin manifest not found at: {}",
manifest_path.display()
)));
}
let manifest_content = read_file_with_context(&manifest_path, "plugin manifest")
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
let manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
})?;
Ok(manifest)
}
fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
if manifest.name.is_empty() {
return Err(PluginError::ManifestValidationError(
"Plugin name is required".to_string(),
));
}
if !self.is_valid_plugin_name(&manifest.name) {
return Err(PluginError::ManifestValidationError(
"Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
));
}
Ok(())
}
fn is_valid_plugin_name(&self, name: &str) -> bool {
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& !name.starts_with('-')
&& !name.ends_with('-')
&& !name.is_empty()
}
pub async fn unload_plugin(&self, plugin_id: &str) -> PluginResult<()> {
let mut plugins = self.plugins.write().await;
if plugins.remove(plugin_id).is_none() {
return Err(PluginError::NotFound(plugin_id.to_string()));
}
Ok(())
}
pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<PluginHandle> {
let plugins = self.plugins.read().await;
plugins
.get(plugin_id)
.cloned()
.ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))
}
pub async fn list_plugins(&self) -> Vec<PluginHandle> {
let plugins = self.plugins.read().await;
plugins.values().cloned().collect()
}
pub async fn enable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
let mut plugins = self.plugins.write().await;
if let Some(handle) = plugins.get_mut(plugin_id) {
handle.state = PluginState::Active;
Ok(())
} else {
Err(PluginError::NotFound(plugin_id.to_string()))
}
}
pub async fn disable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
let mut plugins = self.plugins.write().await;
if let Some(handle) = plugins.get_mut(plugin_id) {
handle.state = PluginState::Disabled;
Ok(())
} else {
Err(PluginError::NotFound(plugin_id.to_string()))
}
}
pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
if let Ok(handle) = self.get_plugin(plugin_id).await {
handle.state == PluginState::Active
} else {
false
}
}
}