1use crate::{config::build_figment, detection::detect_project_info, types::*, Result};
4use figment::Figment;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8pub struct ConfigManager {
10 figment: Figment,
11 config: VxConfig,
12 project_info: Option<ProjectInfo>,
13}
14
15impl ConfigManager {
16 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 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 pub fn get_tool_config(&self, tool_name: &str) -> Option<&ToolConfig> {
45 self.config.tools.get(tool_name)
46 }
47
48 pub fn get_tool_version(&self, tool_name: &str) -> Option<String> {
50 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 if let Some(tool_config) = self.config.tools.get(tool_name) {
59 return tool_config.version.clone();
60 }
61
62 None
63 }
64
65 pub fn get_available_tools(&self) -> Vec<String> {
67 let mut tools: Vec<String> = self.config.tools.keys().cloned().collect();
68
69 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 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 pub fn supports_tool(&self, tool_name: &str) -> bool {
93 if self.config.tools.contains_key(tool_name) {
95 return true;
96 }
97
98 if self.config.defaults.fallback_to_builtin {
100 return ["uv", "node", "go", "rust"].contains(&tool_name);
101 }
102
103 false
104 }
105
106 pub fn config(&self) -> &VxConfig {
108 &self.config
109 }
110
111 pub fn project_info(&self) -> &Option<ProjectInfo> {
113 &self.project_info
114 }
115
116 pub fn figment(&self) -> &Figment {
118 &self.figment
119 }
120
121 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 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 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 if let Some(tools) = tools {
159 project_config.tools = tools;
160 } else if interactive {
161 if let Some(project_info) = &self.project_info {
164 project_config.tools = project_info.tool_versions.clone();
165 }
166 }
167
168 project_config.settings.auto_install = true;
170 project_config.settings.cache_duration = "7d".to_string();
171
172 project_config
173 }
174
175 pub fn validate(&self) -> Result<Vec<String>> {
177 let mut warnings = Vec::new();
178
179 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 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 pub async fn sync_project(&self, _force: bool) -> Result<Vec<String>> {
195 let mut installed_tools = Vec::new();
196
197 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 pub fn get_download_url(&self, tool_name: &str, version: &str) -> Result<String> {
210 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 Err(crate::error::ConfigError::Other {
221 message: format!("No download URL configured for tool: {}", tool_name),
222 })
223 }
224}
225
226fn 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
242fn create_builtin_layer_info() -> LayerInfo {
244 LayerInfo {
245 name: "builtin".to_string(),
246 available: true,
247 priority: 10,
248 }
249}
250
251fn 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
263fn 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
273fn 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
282fn 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
293fn 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
303fn 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
316fn 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}