vx_config/
manager.rs

1//! Main configuration manager implementation
2
3use crate::{config::build_figment, detection::detect_project_info, types::*, Result};
4use figment::Figment;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8/// Main configuration manager using Figment for layered configuration
9pub struct ConfigManager {
10    figment: Figment,
11    config: VxConfig,
12    project_info: Option<ProjectInfo>,
13}
14
15impl ConfigManager {
16    /// Create a new configuration manager with full layered configuration
17    pub async fn new() -> Result<Self> {
18        let project_info = detect_project_info()?;
19        let figment = build_figment(&project_info)?;
20        let config = figment.extract()?;
21
22        Ok(Self {
23            figment,
24            config,
25            project_info,
26        })
27    }
28
29    /// Create a minimal configuration manager (builtin defaults only)
30    pub fn minimal() -> Result<Self> {
31        use figment::providers::Serialized;
32
33        let figment = Figment::from(Serialized::defaults(VxConfig::default()));
34        let config = figment.extract()?;
35
36        Ok(Self {
37            figment,
38            config,
39            project_info: None,
40        })
41    }
42
43    /// Get tool configuration
44    pub fn get_tool_config(&self, tool_name: &str) -> Option<&ToolConfig> {
45        self.config.tools.get(tool_name)
46    }
47
48    /// Get tool version from configuration
49    pub fn get_tool_version(&self, tool_name: &str) -> Option<String> {
50        // First check if we have project info with tool versions
51        if let Some(project_info) = &self.project_info {
52            if let Some(version) = project_info.tool_versions.get(tool_name) {
53                return Some(version.clone());
54            }
55        }
56
57        // Then check tool-specific configuration
58        if let Some(tool_config) = self.config.tools.get(tool_name) {
59            return tool_config.version.clone();
60        }
61
62        None
63    }
64
65    /// Get list of available tools
66    pub fn get_available_tools(&self) -> Vec<String> {
67        let mut tools: Vec<String> = self.config.tools.keys().cloned().collect();
68
69        // Add tools from project info
70        if let Some(project_info) = &self.project_info {
71            for tool in project_info.tool_versions.keys() {
72                if !tools.contains(tool) {
73                    tools.push(tool.clone());
74                }
75            }
76        }
77
78        // Add builtin tools if fallback is enabled
79        if self.config.defaults.fallback_to_builtin {
80            for builtin_tool in &["uv", "node", "go", "rust"] {
81                if !tools.contains(&builtin_tool.to_string()) {
82                    tools.push(builtin_tool.to_string());
83                }
84            }
85        }
86
87        tools.sort();
88        tools
89    }
90
91    /// Check if a tool is supported
92    pub fn supports_tool(&self, tool_name: &str) -> bool {
93        // Check if configured
94        if self.config.tools.contains_key(tool_name) {
95            return true;
96        }
97
98        // Check builtin if fallback enabled
99        if self.config.defaults.fallback_to_builtin {
100            return ["uv", "node", "go", "rust"].contains(&tool_name);
101        }
102
103        false
104    }
105
106    /// Get the current configuration
107    pub fn config(&self) -> &VxConfig {
108        &self.config
109    }
110
111    /// Get project information
112    pub fn project_info(&self) -> &Option<ProjectInfo> {
113        &self.project_info
114    }
115
116    /// Get the underlying figment for advanced usage
117    pub fn figment(&self) -> &Figment {
118        &self.figment
119    }
120
121    /// Get configuration status for diagnostics
122    pub fn get_status(&self) -> ConfigStatus {
123        let layers = collect_layer_info();
124
125        ConfigStatus {
126            layers,
127            available_tools: self.get_available_tools(),
128            fallback_enabled: self.config.defaults.fallback_to_builtin,
129            project_info: self.project_info.clone(),
130        }
131    }
132
133    /// Initialize a new .vx.toml configuration file in the current directory
134    pub async fn init_project_config(
135        &self,
136        tools: Option<HashMap<String, String>>,
137        interactive: bool,
138    ) -> Result<()> {
139        let config_path = get_project_config_path()?;
140        validate_config_not_exists(&config_path)?;
141
142        let project_config = self.create_project_config(tools, interactive);
143        let content = generate_config_content(&project_config)?;
144        write_config_file(&config_path, &content)?;
145
146        Ok(())
147    }
148
149    /// Create project configuration with tools and settings
150    fn create_project_config(
151        &self,
152        tools: Option<HashMap<String, String>>,
153        interactive: bool,
154    ) -> ProjectConfig {
155        let mut project_config = ProjectConfig::default();
156
157        // Add provided tools or detect from project
158        if let Some(tools) = tools {
159            project_config.tools = tools;
160        } else if interactive {
161            // In interactive mode, we could prompt for tools
162            // For now, just detect from existing project files
163            if let Some(project_info) = &self.project_info {
164                project_config.tools = project_info.tool_versions.clone();
165            }
166        }
167
168        // Set sensible defaults
169        project_config.settings.auto_install = true;
170        project_config.settings.cache_duration = "7d".to_string();
171
172        project_config
173    }
174
175    /// Validate the current configuration
176    pub fn validate(&self) -> Result<Vec<String>> {
177        let mut warnings = Vec::new();
178
179        // Check for common configuration issues
180        if self.config.tools.is_empty() && self.project_info.is_none() {
181            warnings.push("No tools configured and no project detected".to_string());
182        }
183
184        // Check if auto_install is disabled but tools are missing
185        if !self.config.defaults.auto_install {
186            warnings
187                .push("Auto-install is disabled - tools may need manual installation".to_string());
188        }
189
190        Ok(warnings)
191    }
192
193    /// Sync project configuration - install all required tools
194    pub async fn sync_project(&self, _force: bool) -> Result<Vec<String>> {
195        let mut installed_tools = Vec::new();
196
197        // For now, just return what would be installed
198        // TODO: Implement actual tool installation
199        if let Some(project_info) = &self.project_info {
200            for (tool_name, version) in &project_info.tool_versions {
201                installed_tools.push(format!("{}@{}", tool_name, version));
202            }
203        }
204
205        Ok(installed_tools)
206    }
207
208    /// Get download URL for a specific tool and version
209    pub fn get_download_url(&self, tool_name: &str, version: &str) -> Result<String> {
210        // Check if tool has custom sources configured
211        if let Some(tool_config) = self.config.tools.get(tool_name) {
212            if let Some(custom_sources) = &tool_config.custom_sources {
213                if let Some(url) = custom_sources.first() {
214                    return Ok(format!("{}/{}", url, version));
215                }
216            }
217        }
218
219        // Return error if no custom URL found - let caller handle fallback
220        Err(crate::error::ConfigError::Other {
221            message: format!("No download URL configured for tool: {}", tool_name),
222        })
223    }
224}
225
226/// Collect information about all configuration layers
227fn collect_layer_info() -> Vec<LayerInfo> {
228    let mut layers = Vec::new();
229
230    layers.push(create_builtin_layer_info());
231
232    if let Some(user_layer) = create_user_layer_info() {
233        layers.push(user_layer);
234    }
235
236    layers.push(create_project_layer_info());
237    layers.push(create_environment_layer_info());
238
239    layers
240}
241
242/// Create layer info for builtin configuration
243fn create_builtin_layer_info() -> LayerInfo {
244    LayerInfo {
245        name: "builtin".to_string(),
246        available: true,
247        priority: 10,
248    }
249}
250
251/// Create layer info for user configuration
252fn create_user_layer_info() -> Option<LayerInfo> {
253    dirs::config_dir().map(|config_dir| {
254        let global_config = config_dir.join("vx").join("config.toml");
255        LayerInfo {
256            name: "user".to_string(),
257            available: global_config.exists(),
258            priority: 50,
259        }
260    })
261}
262
263/// Create layer info for project configuration
264fn create_project_layer_info() -> LayerInfo {
265    let project_config = PathBuf::from(".vx.toml");
266    LayerInfo {
267        name: "project".to_string(),
268        available: project_config.exists(),
269        priority: 80,
270    }
271}
272
273/// Create layer info for environment variables
274fn create_environment_layer_info() -> LayerInfo {
275    LayerInfo {
276        name: "environment".to_string(),
277        available: std::env::vars().any(|(k, _)| k.starts_with("VX_")),
278        priority: 100,
279    }
280}
281
282/// Get the path for the project configuration file
283fn get_project_config_path() -> Result<PathBuf> {
284    let config_path = std::env::current_dir()
285        .map_err(|e| crate::error::ConfigError::Io {
286            message: format!("Failed to get current directory: {}", e),
287            source: e,
288        })?
289        .join(".vx.toml");
290    Ok(config_path)
291}
292
293/// Validate that the configuration file doesn't already exist
294fn validate_config_not_exists(config_path: &Path) -> Result<()> {
295    if config_path.exists() {
296        return Err(crate::error::ConfigError::Validation {
297            message: "Configuration file .vx.toml already exists".to_string(),
298        });
299    }
300    Ok(())
301}
302
303/// Generate the complete configuration file content
304fn generate_config_content(project_config: &ProjectConfig) -> Result<String> {
305    let toml_content = toml::to_string_pretty(project_config)?;
306
307    let header = r#"# VX Project Configuration
308# This file defines the tools and versions required for this project.
309# Run 'vx sync' to install all required tools.
310
311"#;
312
313    Ok(format!("{}{}", header, toml_content))
314}
315
316/// Write configuration content to file
317fn write_config_file(config_path: &PathBuf, content: &str) -> Result<()> {
318    std::fs::write(config_path, content).map_err(|e| crate::error::ConfigError::Io {
319        message: format!("Failed to write .vx.toml: {}", e),
320        source: e,
321    })
322}