Skip to main content

vtcode_core/marketplace/
config.rs

1//! Marketplace configuration system for VT Code
2//!
3//! This module handles the integration of marketplace settings with VT Code's configuration system.
4
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result, bail};
8use serde::{Deserialize, Serialize};
9
10use crate::marketplace::MarketplaceSource;
11use crate::utils::file_utils::{read_file_with_context, write_file_with_context};
12
13/// Configuration for marketplace settings that integrates with VT Code's config system
14#[derive(Debug, Clone, Deserialize, Serialize, Default)]
15pub struct MarketplaceSettings {
16    /// List of configured marketplaces
17    #[serde(default)]
18    pub marketplaces: Vec<MarketplaceSource>,
19
20    /// List of installed plugins with their settings
21    #[serde(default)]
22    pub installed_plugins: Vec<InstalledPlugin>,
23
24    /// Auto-update settings
25    #[serde(default)]
26    pub auto_update: AutoUpdateSettings,
27
28    /// Security and trust settings
29    #[serde(default)]
30    pub security: SecuritySettings,
31}
32
33/// Information about an installed plugin
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct InstalledPlugin {
36    /// Plugin ID
37    pub id: String,
38
39    /// Name of the plugin
40    pub name: String,
41
42    /// Version of the plugin
43    pub version: String,
44
45    /// Source marketplace
46    pub source: String,
47
48    /// Installation path
49    pub install_path: PathBuf,
50
51    /// Whether the plugin is enabled
52    pub enabled: bool,
53
54    /// Trust level of the plugin
55    pub trust_level: crate::config::PluginTrustLevel,
56
57    /// Installation timestamp
58    pub installed_at: String, // Using string for simplicity, could be a proper datetime type
59}
60
61/// Auto-update settings for marketplaces and plugins
62#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct AutoUpdateSettings {
64    /// Whether to auto-update marketplaces
65    #[serde(default = "default_true")]
66    pub marketplaces: bool,
67
68    /// Whether to auto-update plugins
69    #[serde(default = "default_true")]
70    pub plugins: bool,
71
72    /// Check for updates interval in hours
73    #[serde(default = "default_update_interval")]
74    pub check_interval_hours: u32,
75}
76
77/// Security and trust settings
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct SecuritySettings {
80    /// Default trust level for new plugins
81    #[serde(default)]
82    pub default_trust_level: crate::config::PluginTrustLevel,
83
84    /// Whether to require confirmation for untrusted plugins
85    #[serde(default = "default_true")]
86    pub require_confirmation: bool,
87
88    /// List of allowed plugin sources (whitelist)
89    #[serde(default)]
90    pub allowed_sources: Vec<String>,
91
92    /// List of blocked plugin sources (blacklist)
93    #[serde(default)]
94    pub blocked_sources: Vec<String>,
95}
96
97impl Default for AutoUpdateSettings {
98    fn default() -> Self {
99        Self {
100            marketplaces: true,
101            plugins: true,
102            check_interval_hours: 24,
103        }
104    }
105}
106
107impl Default for SecuritySettings {
108    fn default() -> Self {
109        Self {
110            default_trust_level: crate::config::PluginTrustLevel::Sandbox,
111            require_confirmation: true,
112            allowed_sources: Vec::new(),
113            blocked_sources: Vec::new(),
114        }
115    }
116}
117
118fn default_true() -> bool {
119    true
120}
121
122fn default_update_interval() -> u32 {
123    24
124}
125
126impl MarketplaceSettings {
127    /// Load marketplace settings from a configuration file
128    pub async fn load_from_file(config_path: &Path) -> Result<Self> {
129        if !config_path.exists() {
130            // Return default settings if file doesn't exist
131            return Ok(Self::default());
132        }
133
134        let content = read_file_with_context(config_path, "marketplace config file").await?;
135
136        let settings: MarketplaceSettings = toml::from_str(&content).with_context(|| {
137            format!(
138                "Failed to parse marketplace config file: {}",
139                config_path.display()
140            )
141        })?;
142
143        Ok(settings)
144    }
145
146    /// Save marketplace settings to a configuration file
147    pub async fn save_to_file(&self, config_path: &Path) -> Result<()> {
148        let content =
149            toml::to_string(&self).with_context(|| "Failed to serialize marketplace settings")?;
150
151        write_file_with_context(config_path, &content, "marketplace config file").await?;
152
153        Ok(())
154    }
155
156    /// Add a marketplace to the configuration
157    pub fn add_marketplace(&mut self, marketplace: MarketplaceSource) {
158        // Check if marketplace already exists
159        if !self.marketplaces.iter().any(|m| m.id() == marketplace.id()) {
160            self.marketplaces.push(marketplace);
161        }
162    }
163
164    /// Remove a marketplace from the configuration
165    pub fn remove_marketplace(&mut self, id: &str) -> bool {
166        let initial_len = self.marketplaces.len();
167        self.marketplaces.retain(|m| m.id() != id);
168        self.marketplaces.len() != initial_len
169    }
170
171    /// Check if a plugin source is allowed based on security settings
172    pub fn is_source_allowed(&self, source_url: &str) -> bool {
173        // If allowed sources list is empty, all sources are allowed (except blocked ones)
174        let allowed = self.security.allowed_sources.is_empty()
175            || self
176                .security
177                .allowed_sources
178                .iter()
179                .any(|s| source_url.contains(s));
180
181        // Check if source is blocked
182        let blocked = self
183            .security
184            .blocked_sources
185            .iter()
186            .any(|s| source_url.contains(s));
187
188        allowed && !blocked
189    }
190
191    /// Add an installed plugin to the configuration
192    pub fn add_installed_plugin(&mut self, plugin: InstalledPlugin) {
193        // Check if plugin already exists and update it, or add as new
194        match self
195            .installed_plugins
196            .iter_mut()
197            .find(|p| p.id == plugin.id)
198        {
199            Some(existing) => {
200                // Update existing plugin info
201                existing.name = plugin.name;
202                existing.version = plugin.version;
203                existing.source = plugin.source;
204                existing.install_path = plugin.install_path;
205                existing.enabled = plugin.enabled;
206                existing.trust_level = plugin.trust_level;
207                existing.installed_at = plugin.installed_at;
208            }
209            None => {
210                self.installed_plugins.push(plugin);
211            }
212        }
213    }
214
215    /// Remove an installed plugin from the configuration
216    pub fn remove_installed_plugin(&mut self, plugin_id: &str) -> bool {
217        let initial_len = self.installed_plugins.len();
218        self.installed_plugins.retain(|p| p.id != plugin_id);
219        self.installed_plugins.len() != initial_len
220    }
221
222    /// Get an installed plugin by ID
223    pub fn get_installed_plugin(&self, plugin_id: &str) -> Option<&InstalledPlugin> {
224        self.installed_plugins.iter().find(|p| p.id == plugin_id)
225    }
226
227    /// Enable a plugin
228    pub fn enable_plugin(&mut self, plugin_id: &str) -> Result<()> {
229        match self
230            .installed_plugins
231            .iter_mut()
232            .find(|p| p.id == plugin_id)
233        {
234            Some(plugin) => {
235                plugin.enabled = true;
236                Ok(())
237            }
238            None => bail!("Plugin '{}' not found in installed plugins", plugin_id),
239        }
240    }
241
242    /// Disable a plugin
243    pub fn disable_plugin(&mut self, plugin_id: &str) -> Result<()> {
244        match self
245            .installed_plugins
246            .iter_mut()
247            .find(|p| p.id == plugin_id)
248        {
249            Some(plugin) => {
250                plugin.enabled = false;
251                Ok(())
252            }
253            None => bail!("Plugin '{}' not found in installed plugins", plugin_id),
254        }
255    }
256}
257
258/// Helper function to get the marketplace config path based on VT Code's config directory structure
259pub fn get_marketplace_config_path(base_config_dir: &Path) -> PathBuf {
260    base_config_dir.join("marketplace.toml")
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[tokio::test]
268    async fn test_marketplace_settings_default() {
269        let settings = MarketplaceSettings::default();
270        assert!(settings.marketplaces.is_empty());
271        assert!(settings.installed_plugins.is_empty());
272        assert!(settings.auto_update.marketplaces);
273        assert_eq!(settings.auto_update.check_interval_hours, 24);
274        assert_eq!(
275            settings.security.default_trust_level,
276            crate::config::PluginTrustLevel::Sandbox
277        );
278    }
279
280    #[tokio::test]
281    async fn test_marketplace_settings_save_load() {
282        let temp_dir = tempfile::tempdir().unwrap();
283        let config_path = temp_dir.path().join("test_marketplace.toml");
284
285        let mut settings = MarketplaceSettings::default();
286        settings.auto_update.marketplaces = false;
287        settings.security.default_trust_level = crate::config::PluginTrustLevel::Trusted;
288
289        settings.save_to_file(&config_path).await.unwrap();
290
291        let loaded_settings = MarketplaceSettings::load_from_file(&config_path)
292            .await
293            .unwrap();
294        assert_eq!(
295            settings.auto_update.marketplaces,
296            loaded_settings.auto_update.marketplaces
297        );
298        assert_eq!(
299            settings.security.default_trust_level,
300            loaded_settings.security.default_trust_level
301        );
302    }
303
304    #[tokio::test]
305    async fn test_add_remove_marketplace() {
306        let mut settings = MarketplaceSettings::default();
307
308        let marketplace = MarketplaceSource::Git {
309            id: "test".to_string(),
310            url: "https://example.com/test".to_string(),
311            refspec: None,
312        };
313
314        settings.add_marketplace(marketplace.clone());
315        assert_eq!(settings.marketplaces.len(), 1);
316
317        settings.remove_marketplace("test");
318        assert_eq!(settings.marketplaces.len(), 0);
319    }
320
321    #[tokio::test]
322    async fn test_plugin_enable_disable() {
323        let mut settings = MarketplaceSettings::default();
324
325        let plugin = InstalledPlugin {
326            id: "test-plugin".to_string(),
327            name: "Test Plugin".to_string(),
328            version: "1.0.0".to_string(),
329            source: "test-marketplace".to_string(),
330            install_path: PathBuf::from("/tmp/test"),
331            enabled: false,
332            trust_level: crate::config::PluginTrustLevel::Sandbox,
333            installed_at: "2023-01-01".to_string(),
334        };
335
336        settings.add_installed_plugin(plugin);
337
338        settings.enable_plugin("test-plugin").unwrap();
339        assert!(
340            settings
341                .get_installed_plugin("test-plugin")
342                .unwrap()
343                .enabled
344        );
345
346        settings.disable_plugin("test-plugin").unwrap();
347        assert!(
348            !settings
349                .get_installed_plugin("test-plugin")
350                .unwrap()
351                .enabled
352        );
353    }
354}