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 { plugins: Arc::new(RwLock::new(HashMap::new())) }
54    }
55
56    /// Load a plugin from the specified path
57    pub async fn load_plugin(&self, plugin_path: &Path) -> PluginResult<PluginHandle> {
58        // Validate plugin path
59        if !tokio::fs::try_exists(plugin_path).await.unwrap_or(false) {
60            return Err(PluginError::NotFound(plugin_path.display().to_string().into()));
61        }
62
63        // Load the plugin manifest
64        let manifest = self.load_manifest(plugin_path).await?;
65
66        // Validate the manifest
67        self.validate_manifest(&manifest)?;
68
69        // Create plugin handle
70        let handle = PluginHandle {
71            id: manifest.name.clone().into(),
72            manifest: manifest.clone(),
73            path: plugin_path.to_path_buf(),
74            state: PluginState::Active,
75            loaded_at: Some(std::time::SystemTime::now()),
76        };
77
78        // Store in runtime
79        {
80            let mut plugins = self.plugins.write().await;
81            plugins.insert(manifest.name.clone().into(), handle.clone());
82        }
83
84        Ok(handle)
85    }
86
87    /// Load plugin manifest from path
88    async fn load_manifest(&self, plugin_path: &Path) -> PluginResult<PluginManifest> {
89        let manifest_path = plugin_path.join(".vtcode-plugin/plugin.json");
90
91        if !tokio::fs::try_exists(&manifest_path).await.unwrap_or(false) {
92            return Err(PluginError::ManifestValidationError(format!(
93                "Plugin manifest not found at: {}",
94                manifest_path.display()
95            )));
96        }
97
98        let manifest_content = read_file_with_context(&manifest_path, "plugin manifest")
99            .await
100            .map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {e}")))?;
101
102        let manifest: PluginManifest = serde_json::from_str(&manifest_content)
103            .map_err(|e| PluginError::ManifestValidationError(format!("Invalid manifest JSON: {e}")))?;
104
105        Ok(manifest)
106    }
107
108    /// Validate plugin manifest
109    fn validate_manifest(&self, manifest: &PluginManifest) -> PluginResult<()> {
110        if manifest.name.is_empty() {
111            return Err(PluginError::ManifestValidationError("Plugin name is required".to_string()));
112        }
113
114        // Validate name format (kebab-case)
115        if !self.is_valid_plugin_name(&manifest.name) {
116            return Err(PluginError::ManifestValidationError(
117                "Plugin name must be in kebab-case (lowercase with hyphens)".to_string(),
118            ));
119        }
120
121        Ok(())
122    }
123
124    /// Check if plugin name is valid (kebab-case)
125    fn is_valid_plugin_name(&self, name: &str) -> bool {
126        // Check if name contains only lowercase letters, numbers, and hyphens
127        name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
128            && !name.starts_with('-')
129            && !name.ends_with('-')
130            && !name.is_empty()
131    }
132
133    /// Unload a plugin
134    pub async fn unload_plugin(&self, plugin_id: &str) -> PluginResult<()> {
135        let mut plugins = self.plugins.write().await;
136        if plugins.remove(plugin_id).is_none() {
137            return Err(PluginError::NotFound(plugin_id.to_string().into()));
138        }
139        Ok(())
140    }
141
142    /// Get a plugin handle
143    pub async fn get_plugin(&self, plugin_id: &str) -> PluginResult<PluginHandle> {
144        let plugins = self.plugins.read().await;
145        plugins
146            .get(plugin_id)
147            .cloned()
148            .ok_or_else(|| PluginError::NotFound(plugin_id.to_string().into()))
149    }
150
151    /// List all loaded plugins
152    pub async fn list_plugins(&self) -> Vec<PluginHandle> {
153        let plugins = self.plugins.read().await;
154        plugins.values().cloned().collect()
155    }
156
157    /// Enable a plugin
158    pub async fn enable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
159        let mut plugins = self.plugins.write().await;
160        if let Some(handle) = plugins.get_mut(plugin_id) {
161            handle.state = PluginState::Active;
162            Ok(())
163        } else {
164            Err(PluginError::NotFound(plugin_id.to_string().into()))
165        }
166    }
167
168    /// Disable a plugin
169    pub async fn disable_plugin(&self, plugin_id: &str) -> PluginResult<()> {
170        let mut plugins = self.plugins.write().await;
171        if let Some(handle) = plugins.get_mut(plugin_id) {
172            handle.state = PluginState::Disabled;
173            Ok(())
174        } else {
175            Err(PluginError::NotFound(plugin_id.to_string().into()))
176        }
177    }
178
179    /// Check if a plugin is enabled
180    pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
181        if let Ok(handle) = self.get_plugin(plugin_id).await {
182            handle.state == PluginState::Active
183        } else {
184            false
185        }
186    }
187}