vx_cli/commands/
config.rs

1// Config command implementation
2
3use crate::ui::UI;
4use std::collections::HashMap;
5use vx_core::{config_figment::FigmentConfigManager, Result, VxError};
6
7pub async fn handle() -> Result<()> {
8    show_config().await
9}
10
11pub async fn handle_init(tools: Vec<String>, template: Option<String>) -> Result<()> {
12    let spinner = UI::new_spinner("Initializing configuration...");
13
14    let config_content = if let Some(template) = template {
15        generate_template_config(&template, &tools)?
16    } else {
17        generate_default_config(&tools)?
18    };
19
20    std::fs::write(".vx.toml", config_content)?;
21    spinner.finish_and_clear();
22
23    UI::success("Initialized .vx.toml in current directory");
24    Ok(())
25}
26
27async fn show_config() -> Result<()> {
28    let spinner = UI::new_spinner("Loading configuration...");
29
30    let config_manager = FigmentConfigManager::new()?;
31    let status = config_manager.get_status();
32
33    spinner.finish_and_clear();
34
35    UI::header("Current Configuration");
36
37    // Show configuration layers
38    UI::info("Configuration Layers:");
39    for layer in &status.layers {
40        let status_icon = if layer.available { "✓" } else { "✗" };
41        UI::item(&format!(
42            "{} {} (priority: {})",
43            status_icon, layer.name, layer.priority
44        ));
45    }
46
47    // Show project information if available
48    if let Some(project_info) = &status.project_info {
49        UI::info(&format!("Project Type: {:?}", project_info.project_type));
50        UI::info(&format!(
51            "Config File: {}",
52            project_info.config_file.display()
53        ));
54
55        if !project_info.tool_versions.is_empty() {
56            UI::info("Detected Tool Versions:");
57            for (tool, version) in &project_info.tool_versions {
58                UI::item(&format!("{}: {}", tool, version));
59            }
60        }
61    }
62
63    // Show current configuration
64    let config = config_manager.config();
65    if !config.tools.is_empty() {
66        UI::info("Configured Tools:");
67        for (tool_name, tool_config) in &config.tools {
68            let version = tool_config.version.as_deref().unwrap_or("not specified");
69            UI::item(&format!("{}: {}", tool_name, version));
70        }
71    }
72
73    Ok(())
74}
75
76pub async fn handle_set(key: &str, value: &str) -> Result<()> {
77    set_config(key, value).await
78}
79
80pub async fn handle_get(key: &str) -> Result<()> {
81    get_config(key).await
82}
83
84async fn set_config(key: &str, value: &str) -> Result<()> {
85    let spinner = UI::new_spinner("Updating configuration...");
86
87    // Parse the key to determine if it's a tool config or global setting
88    let parts: Vec<&str> = key.split('.').collect();
89
90    match parts.as_slice() {
91        ["tools", tool_name, "version"] => {
92            // Set tool version
93            set_tool_version(tool_name, value).await?;
94            spinner.finish_and_clear();
95            UI::success(&format!("Set {} version to {}", tool_name, value));
96        }
97        ["defaults", setting] => {
98            // Set global default setting
99            set_global_setting(setting, value).await?;
100            spinner.finish_and_clear();
101            UI::success(&format!("Set {} to {}", setting, value));
102        }
103        _ => {
104            spinner.finish_and_clear();
105            return Err(VxError::Other {
106                message: format!(
107                    "Invalid config key: {}. Use format 'tools.<tool>.version' or 'defaults.<setting>'",
108                    key
109                ),
110            });
111        }
112    }
113
114    Ok(())
115}
116
117async fn get_config(key: &str) -> Result<()> {
118    let spinner = UI::new_spinner("Loading configuration...");
119
120    let config_manager = FigmentConfigManager::new()?;
121    let config = config_manager.config();
122
123    spinner.finish_and_clear();
124
125    // Parse the key to determine what to retrieve
126    let parts: Vec<&str> = key.split('.').collect();
127
128    match parts.as_slice() {
129        ["tools", tool_name, "version"] => {
130            if let Some(tool_config) = config.tools.get(*tool_name) {
131                let version = tool_config.version.as_deref().unwrap_or("not specified");
132                UI::info(&format!("{}: {}", key, version));
133            } else {
134                UI::warn(&format!("Tool '{}' not configured", tool_name));
135            }
136        }
137        ["defaults", setting] => match *setting {
138            "auto_install" => UI::info(&format!("{}: {}", key, config.defaults.auto_install)),
139            "check_updates" => UI::info(&format!("{}: {}", key, config.defaults.check_updates)),
140            "update_interval" => UI::info(&format!("{}: {}", key, config.defaults.update_interval)),
141            _ => {
142                UI::warn(&format!("Unknown setting: {}", setting));
143            }
144        },
145        _ => {
146            return Err(VxError::Other {
147                message: format!(
148                    "Invalid config key: {}. Use format 'tools.<tool>.version' or 'defaults.<setting>'",
149                    key
150                ),
151            });
152        }
153    }
154
155    Ok(())
156}
157
158pub async fn handle_reset(key: Option<String>) -> Result<()> {
159    reset_config(key).await
160}
161
162pub async fn handle_edit() -> Result<()> {
163    edit_config().await
164}
165
166async fn reset_config(key: Option<String>) -> Result<()> {
167    let spinner = UI::new_spinner("Resetting configuration...");
168
169    match key {
170        Some(key) => {
171            // Reset specific key
172            let parts: Vec<&str> = key.split('.').collect();
173            match parts.as_slice() {
174                ["tools", tool_name, "version"] => {
175                    remove_tool_config(tool_name).await?;
176                    spinner.finish_and_clear();
177                    UI::success(&format!("Reset {} configuration", tool_name));
178                }
179                _ => {
180                    spinner.finish_and_clear();
181                    return Err(VxError::Other {
182                        message: format!("Cannot reset key: {}", key),
183                    });
184                }
185            }
186        }
187        None => {
188            // Reset entire project config
189            let config_path = std::env::current_dir()
190                .map_err(|e| VxError::Other {
191                    message: format!("Failed to get current directory: {}", e),
192                })?
193                .join(".vx.toml");
194
195            if config_path.exists() {
196                std::fs::remove_file(&config_path)?;
197                spinner.finish_and_clear();
198                UI::success("Reset project configuration (.vx.toml removed)");
199            } else {
200                spinner.finish_and_clear();
201                UI::info("No project configuration to reset");
202            }
203        }
204    }
205
206    Ok(())
207}
208
209async fn edit_config() -> Result<()> {
210    let config_path = std::env::current_dir()
211        .map_err(|e| VxError::Other {
212            message: format!("Failed to get current directory: {}", e),
213        })?
214        .join(".vx.toml");
215
216    // Create config if it doesn't exist
217    if !config_path.exists() {
218        UI::info("Creating .vx.toml...");
219        generate_default_config(&[]).and_then(|content| {
220            std::fs::write(&config_path, content).map_err(|e| VxError::Other {
221                message: format!("Failed to create .vx.toml: {}", e),
222            })
223        })?;
224    }
225
226    // Try to open with system editor
227    let editor = std::env::var("EDITOR")
228        .or_else(|_| std::env::var("VISUAL"))
229        .unwrap_or_else(|_| {
230            if cfg!(windows) {
231                "notepad".to_string()
232            } else {
233                "nano".to_string()
234            }
235        });
236
237    UI::info(&format!(
238        "Opening {} with {}...",
239        config_path.display(),
240        editor
241    ));
242
243    let status = std::process::Command::new(&editor)
244        .arg(&config_path)
245        .status()
246        .map_err(|e| VxError::Other {
247            message: format!("Failed to open editor '{}': {}", editor, e),
248        })?;
249
250    if status.success() {
251        UI::success("Configuration edited successfully");
252    } else {
253        UI::warn("Editor exited with non-zero status");
254    }
255
256    Ok(())
257}
258
259async fn remove_tool_config(tool_name: &str) -> Result<()> {
260    let config_path = std::env::current_dir()
261        .map_err(|e| VxError::Other {
262            message: format!("Failed to get current directory: {}", e),
263        })?
264        .join(".vx.toml");
265
266    if !config_path.exists() {
267        return Ok(()); // Nothing to remove
268    }
269
270    let content = std::fs::read_to_string(&config_path)?;
271    let mut project_config =
272        toml::from_str::<vx_core::venv::ProjectConfig>(&content).map_err(|e| VxError::Other {
273            message: format!("Failed to parse .vx.toml: {}", e),
274        })?;
275
276    project_config.tools.remove(tool_name);
277
278    let toml_content = toml::to_string_pretty(&project_config).map_err(|e| VxError::Other {
279        message: format!("Failed to serialize configuration: {}", e),
280    })?;
281
282    let header = "# VX Project Configuration\n# This file defines the tools and versions required for this project.\n\n";
283    let full_content = format!("{}{}", header, toml_content);
284
285    std::fs::write(&config_path, full_content).map_err(|e| VxError::Other {
286        message: format!("Failed to write .vx.toml: {}", e),
287    })?;
288
289    Ok(())
290}
291
292fn generate_default_config(tools: &[String]) -> Result<String> {
293    let mut config = String::from("# vx configuration file\n");
294    config.push_str("# This file configures tool versions for this project\n\n");
295
296    if tools.is_empty() {
297        config.push_str("[tools.uv]\nversion = \"latest\"\n\n");
298        config.push_str("[tools.node]\nversion = \"lts\"\n");
299    } else {
300        for tool in tools {
301            config.push_str(&format!("[tools.{tool}]\nversion = \"latest\"\n\n"));
302        }
303    }
304
305    Ok(config)
306}
307
308async fn set_tool_version(tool_name: &str, version: &str) -> Result<()> {
309    // Load or create project config
310    let config_path = std::env::current_dir()
311        .map_err(|e| VxError::Other {
312            message: format!("Failed to get current directory: {}", e),
313        })?
314        .join(".vx.toml");
315
316    let mut tools = HashMap::new();
317
318    // If config exists, load existing tools
319    if config_path.exists() {
320        let content = std::fs::read_to_string(&config_path)?;
321        if let Ok(project_config) = toml::from_str::<vx_core::venv::ProjectConfig>(&content) {
322            tools = project_config.tools;
323        }
324    }
325
326    // Update the tool version
327    tools.insert(tool_name.to_string(), version.to_string());
328
329    // Create updated project config
330    let project_config = vx_core::venv::ProjectConfig {
331        tools,
332        ..Default::default()
333    };
334
335    // Write back to file
336    let toml_content = toml::to_string_pretty(&project_config).map_err(|e| VxError::Other {
337        message: format!("Failed to serialize configuration: {}", e),
338    })?;
339
340    let header = "# VX Project Configuration\n# This file defines the tools and versions required for this project.\n\n";
341    let full_content = format!("{}{}", header, toml_content);
342
343    std::fs::write(&config_path, full_content).map_err(|e| VxError::Other {
344        message: format!("Failed to write .vx.toml: {}", e),
345    })?;
346
347    Ok(())
348}
349
350async fn set_global_setting(setting: &str, value: &str) -> Result<()> {
351    // For now, just show a message that global settings aren't implemented
352    // In a full implementation, this would update the global config file
353    UI::warning(&format!(
354        "Global setting '{}' = '{}' - Global config modification not yet implemented",
355        setting, value
356    ));
357    Ok(())
358}
359
360fn generate_template_config(template: &str, additional_tools: &[String]) -> Result<String> {
361    let mut config = String::from("# vx configuration file\n");
362    config.push_str(&format!("# Generated from {template} template\n\n"));
363
364    match template {
365        "node" | "javascript" | "js" => {
366            config.push_str("[tools.node]\nversion = \"lts\"\n\n");
367            config.push_str("[tools.npm]\nversion = \"latest\"\n\n");
368        }
369        "python" | "py" => {
370            config.push_str("[tools.uv]\nversion = \"latest\"\n\n");
371            config.push_str("[tools.python]\nversion = \"3.11\"\n\n");
372        }
373        "rust" => {
374            config.push_str("[tools.rust]\nversion = \"stable\"\n\n");
375            config.push_str("[tools.cargo]\nversion = \"latest\"\n\n");
376        }
377        "go" => {
378            config.push_str("[tools.go]\nversion = \"latest\"\n\n");
379        }
380        _ => {
381            return Err(VxError::Other {
382                message: format!("Unknown template: {template}"),
383            });
384        }
385    }
386
387    for tool in additional_tools {
388        config.push_str(&format!("[tools.{tool}]\nversion = \"latest\"\n\n"));
389    }
390
391    Ok(config)
392}