Skip to main content

vtcode_core/plugins/
runtime.rs

1//! Plugin runtime system for VT Code
2//!
3//! Manages the lifecycle of plugins including loading, unloading, and execution.
4
5use hashbrown::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use tokio::sync::RwLock;
10
11use super::{PluginError, PluginId, PluginManifest, PluginResult};
12use crate::config::PluginRuntimeConfig;
13use crate::utils::file_utils::read_file_with_context;
14
15/// Plugin state tracking
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PluginState {
18    /// Plugin is loaded and ready
19    Active,
20    /// Plugin is installed but not loaded
21    Installed,
22    /// Plugin is disabled
23    Disabled,
24    /// Plugin is in error state
25    Error,
26}
27
28/// Plugin handle containing runtime information
29#[derive(Debug, Clone)]
30pub struct PluginHandle {
31    /// Plugin identifier
32    pub id: PluginId,
33    /// Plugin manifest
34    pub manifest: PluginManifest,
35    /// Plugin installation path
36    pub path: PathBuf,
37    /// Current state
38    pub state: PluginState,
39    /// Loaded at timestamp
40    pub loaded_at: Option<std::time::SystemTime>,
41}
42
43/// Plugin runtime that manages plugin lifecycle
44#[derive(Debug, Clone)]
45pub struct PluginRuntime {
46    /// Currently loaded plugins
47    plugins: Arc<RwLock<HashMap<PluginId, PluginHandle>>>,
48}
49
50impl PluginRuntime {
51    /// Create a new plugin runtime
52    pub fn new(_config: PluginRuntimeConfig, _base_dir: PathBuf) -> Self {
53        Self {
54            plugins: Arc::new(RwLock::new(HashMap::new())),
55        }
56    }
57
58    /// Load a plugin from the specified path
59    pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<PluginHandle> {
60        // Validate plugin path
61        if !plugin_path.exists() {
62            return Err(PluginError::NotFound(plugin_path.display().to_string()));
63        }
64
65        // Load the plugin manifest
66        let manifest = self.load_manifest(plugin_path).await?;
67
68        // Validate the manifest
69        self.validate_manifest(&manifest)?;
70
71        // Create plugin handle
72        let handle = PluginHandle {
73            id: manifest.name.clone(),
74            manifest: manifest.clone(),
75            path: plugin_path.to_path_buf(),
76            state: PluginState::Active,
77            loaded_at: Some(std::time::SystemTime::now()),
78        };
79
80        // Store in runtime
81        {
82            let mut plugins = self.plugins.write().await;
83            plugins.insert(manifest.name.clone(), handle.clone());
84        }
85
86        Ok(handle)
87    }
88
89    /// Load plugin manifest from path
90    async fn load_manifest(&self, plugin_path: &Path) -> PluginResult<PluginManifest> {
91        let manifest_path = plugin_path.join(".vtcode-plugin/plugin.json");
92
93        if !manifest_path.exists() {
94            return Err(PluginError::ManifestValidationError(format!(
95                "Plugin manifest not found at: {}",
96                manifest_path.display()
97            )));
98        }
99
100        let manifest_content = read_file_with_context(&manifest_path, "plugin manifest")
101            .await
102            .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {}", e)))?;
103
104        let manifest: PluginManifest = serde_json::from_str(&manifest_content).map_err(|e| {
105            PluginError::ManifestValidationError(format!("Invalid manifest JSON: {}", e))
106        })?;
107
108        Ok(manifest)
109    }
110
111    /// Validate plugin manifest
112    fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
113        if manifest.name.is_empty() {
114            return Err(PluginError::ManifestValidationError(
115                "Plugin name is required".to_string(),
116            ));
117        }
118
119        // Validate name format (kebab-case)
120        if !self.is_valid_plugin_name(&manifest.name) {
121            return Err(PluginError::ManifestValidationError(
122                "Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
123            ));
124        }
125
126        Ok(())
127    }
128
129    /// Check if plugin name is valid (kebab-case)
130    fn is_valid_plugin_name(&self, name: &str) -> bool {
131        // Check if name contains only lowercase letters, numbers, and hyphens
132        name.chars()
133            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
134            && !name.starts_with('-')
135            && !name.ends_with('-')
136            && !name.is_empty()
137    }
138
139    /// Unload a plugin
140    pub async fn unload_plugin(&self, plugin_id: &str) -> PluginResult<()> {
141        let mut plugins = self.plugins.write().await;
142        if plugins.remove(plugin_id).is_none() {
143            return Err(PluginError::NotFound(plugin_id.to_string()));
144        }
145        Ok(())
146    }
147
148    /// Get a plugin handle
149    pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<PluginHandle> {
150        let plugins = self.plugins.read().await;
151        plugins
152            .get(plugin_id)
153            .cloned()
154            .ok_or_else(|| PluginError::NotFound(plugin_id.to_string()))
155    }
156
157    /// List all loaded plugins
158    pub async fn list_plugins(&self) -> Vec<PluginHandle> {
159        let plugins = self.plugins.read().await;
160        plugins.values().cloned().collect()
161    }
162
163    /// Enable a plugin
164    pub async fn enable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
165        let mut plugins = self.plugins.write().await;
166        if let Some(handle) = plugins.get_mut(plugin_id) {
167            handle.state = PluginState::Active;
168            Ok(())
169        } else {
170            Err(PluginError::NotFound(plugin_id.to_string()))
171        }
172    }
173
174    /// Disable a plugin
175    pub async fn disable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
176        let mut plugins = self.plugins.write().await;
177        if let Some(handle) = plugins.get_mut(plugin_id) {
178            handle.state = PluginState::Disabled;
179            Ok(())
180        } else {
181            Err(PluginError::NotFound(plugin_id.to_string()))
182        }
183    }
184
185    /// Check if a plugin is enabled
186    pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
187        if let Ok(handle) = self.get_plugin(plugin_id).await {
188            handle.state == PluginState::Active
189        } else {
190            false
191        }
192    }
193}