Skip to main content

plugin_packager/
config.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Configuration management for plugin-packager
5//!
6//! This module provides a configuration system for plugin-packager that allows
7//! users to customize installation paths, registry behavior, and other settings.
8
9use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14/// Configuration for plugin-packager
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Config {
17    /// Default plugin installation directory
18    #[serde(default = "Config::default_plugin_dir")]
19    pub plugin_dir: PathBuf,
20
21    /// Registry file path
22    #[serde(default = "Config::default_registry_file")]
23    pub registry_file: PathBuf,
24
25    /// Whether to auto-verify artifacts on install
26    #[serde(default = "Config::default_verify_on_install")]
27    pub verify_on_install: bool,
28
29    /// Whether to auto-register plugins in local registry
30    #[serde(default = "Config::default_auto_register")]
31    pub auto_register: bool,
32
33    /// Whether to check dependencies before installation
34    #[serde(default = "Config::default_check_dependencies")]
35    pub check_dependencies: bool,
36
37    /// Maximum concurrent plugin installations
38    #[serde(default = "Config::default_max_concurrent")]
39    pub max_concurrent_installs: usize,
40
41    /// Cache directory for downloaded packages
42    #[serde(default = "Config::default_cache_dir")]
43    pub cache_dir: PathBuf,
44
45    /// Enable verbose logging
46    #[serde(default = "Config::default_verbose")]
47    pub verbose: bool,
48
49    /// Backup existing plugins before upgrade
50    #[serde(default = "Config::default_backup_on_upgrade")]
51    pub backup_on_upgrade: bool,
52}
53
54impl Config {
55    /// Default plugin installation directory
56    fn default_plugin_dir() -> PathBuf {
57        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
58        home.join(".skylet").join("plugins")
59    }
60
61    /// Default registry file path
62    fn default_registry_file() -> PathBuf {
63        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
64        home.join(".skylet").join("registry.json")
65    }
66
67    /// Default verify on install
68    fn default_verify_on_install() -> bool {
69        true
70    }
71
72    /// Default auto register
73    fn default_auto_register() -> bool {
74        true
75    }
76
77    /// Default check dependencies
78    fn default_check_dependencies() -> bool {
79        false // Can be expensive, opt-in
80    }
81
82    /// Default max concurrent installs
83    fn default_max_concurrent() -> usize {
84        4
85    }
86
87    /// Default cache directory
88    fn default_cache_dir() -> PathBuf {
89        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
90        home.join(".skylet").join("cache")
91    }
92
93    /// Default verbose logging
94    fn default_verbose() -> bool {
95        false
96    }
97
98    /// Default backup on upgrade
99    fn default_backup_on_upgrade() -> bool {
100        true
101    }
102
103    /// Load configuration from file, or return default if not found
104    pub fn load_or_default(path: &Path) -> Result<Self> {
105        if path.exists() {
106            Self::load(path)
107        } else {
108            Ok(Self::default())
109        }
110    }
111
112    /// Load configuration from file
113    pub fn load(path: &Path) -> Result<Self> {
114        let content = fs::read_to_string(path).context("reading config file")?;
115        let config: Self = toml::from_str(&content).context("parsing config file")?;
116        Ok(config)
117    }
118
119    /// Save configuration to file
120    pub fn save(&self, path: &Path) -> Result<()> {
121        let parent = path.parent();
122        if let Some(parent_path) = parent {
123            fs::create_dir_all(parent_path).context("creating config directory")?;
124        }
125
126        let content = toml::to_string_pretty(self).context("serializing config")?;
127        fs::write(path, content).context("writing config file")?;
128        Ok(())
129    }
130
131    /// Get the configuration file path (creating parent directories if needed)
132    pub fn config_path() -> Result<PathBuf> {
133        let home = dirs::home_dir().context("could not determine home directory")?;
134        let config_path = home.join(".skylet").join("config.toml");
135        Ok(config_path)
136    }
137
138    /// Ensure plugin directory exists
139    pub fn ensure_plugin_dir(&self) -> Result<()> {
140        fs::create_dir_all(&self.plugin_dir).context("failed to create plugin directory")?;
141        Ok(())
142    }
143
144    /// Ensure cache directory exists
145    pub fn ensure_cache_dir(&self) -> Result<()> {
146        fs::create_dir_all(&self.cache_dir).context("failed to create cache directory")?;
147        Ok(())
148    }
149
150    /// Ensure registry directory exists
151    pub fn ensure_registry_dir(&self) -> Result<()> {
152        if let Some(parent) = self.registry_file.parent() {
153            fs::create_dir_all(parent).context("failed to create registry directory")?;
154        }
155        Ok(())
156    }
157
158    /// Ensure all required directories exist
159    pub fn ensure_all_dirs(&self) -> Result<()> {
160        self.ensure_plugin_dir()?;
161        self.ensure_cache_dir()?;
162        self.ensure_registry_dir()?;
163        Ok(())
164    }
165}
166
167impl Default for Config {
168    fn default() -> Self {
169        Self {
170            plugin_dir: Config::default_plugin_dir(),
171            registry_file: Config::default_registry_file(),
172            verify_on_install: Config::default_verify_on_install(),
173            auto_register: Config::default_auto_register(),
174            check_dependencies: Config::default_check_dependencies(),
175            max_concurrent_installs: Config::default_max_concurrent(),
176            cache_dir: Config::default_cache_dir(),
177            verbose: Config::default_verbose(),
178            backup_on_upgrade: Config::default_backup_on_upgrade(),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use tempfile::tempdir;
187
188    #[test]
189    fn test_config_default() {
190        let config = Config::default();
191        assert!(config.verify_on_install);
192        assert!(config.auto_register);
193        assert!(!config.check_dependencies);
194        assert_eq!(config.max_concurrent_installs, 4);
195        assert!(!config.verbose);
196        assert!(config.backup_on_upgrade);
197    }
198
199    #[test]
200    fn test_config_save_and_load() -> Result<()> {
201        let temp_dir = tempdir()?;
202        let config_path = temp_dir.path().join("config.toml");
203
204        // Create custom config
205        let mut config = Config::default();
206        config.verbose = true;
207        config.max_concurrent_installs = 8;
208        config.check_dependencies = true;
209
210        // Save
211        config.save(&config_path)?;
212        assert!(config_path.exists());
213
214        // Load
215        let loaded = Config::load(&config_path)?;
216        assert_eq!(loaded.verbose, true);
217        assert_eq!(loaded.max_concurrent_installs, 8);
218        assert_eq!(loaded.check_dependencies, true);
219
220        Ok(())
221    }
222
223    #[test]
224    fn test_config_load_or_default() -> Result<()> {
225        let temp_dir = tempdir()?;
226        let config_path = temp_dir.path().join("nonexistent.toml");
227
228        // Should return default if file doesn't exist
229        let config = Config::load_or_default(&config_path)?;
230        assert_eq!(config.max_concurrent_installs, 4);
231
232        Ok(())
233    }
234
235    #[test]
236    fn test_config_ensure_dirs() -> Result<()> {
237        let temp_dir = tempdir()?;
238        let mut config = Config::default();
239        config.plugin_dir = temp_dir.path().join("plugins");
240        config.cache_dir = temp_dir.path().join("cache");
241        config.registry_file = temp_dir.path().join("registry.json");
242
243        config.ensure_all_dirs()?;
244
245        assert!(config.plugin_dir.exists());
246        assert!(config.cache_dir.exists());
247
248        Ok(())
249    }
250
251    #[test]
252    fn test_config_toml_serialization() -> Result<()> {
253        let config = Config::default();
254        let toml_str = toml::to_string_pretty(&config)?;
255
256        // Should contain key settings
257        assert!(toml_str.contains("plugin_dir"));
258        assert!(toml_str.contains("registry_file"));
259        assert!(toml_str.contains("verify_on_install"));
260
261        Ok(())
262    }
263}