use crate::{config::build_figment, detection::detect_project_info, types::*, Result};
use figment::Figment;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct ConfigManager {
figment: Figment,
config: VxConfig,
project_info: Option<ProjectInfo>,
}
impl ConfigManager {
pub async fn new() -> Result<Self> {
let project_info = detect_project_info()?;
let figment = build_figment(&project_info)?;
let config = figment.extract()?;
Ok(Self {
figment,
config,
project_info,
})
}
pub fn minimal() -> Result<Self> {
use figment::providers::Serialized;
let figment = Figment::from(Serialized::defaults(VxConfig::default()));
let config = figment.extract()?;
Ok(Self {
figment,
config,
project_info: None,
})
}
pub fn get_tool_config(&self, tool_name: &str) -> Option<&ToolConfig> {
self.config.tools.get(tool_name)
}
pub fn get_tool_version(&self, tool_name: &str) -> Option<String> {
if let Some(project_info) = &self.project_info {
if let Some(version) = project_info.tool_versions.get(tool_name) {
return Some(version.clone());
}
}
if let Some(tool_config) = self.config.tools.get(tool_name) {
return tool_config.version.clone();
}
None
}
pub fn get_available_tools(&self) -> Vec<String> {
let mut tools: Vec<String> = self.config.tools.keys().cloned().collect();
if let Some(project_info) = &self.project_info {
for tool in project_info.tool_versions.keys() {
if !tools.contains(tool) {
tools.push(tool.clone());
}
}
}
if self.config.defaults.fallback_to_builtin {
for builtin_tool in &["uv", "node", "go", "rust"] {
if !tools.contains(&builtin_tool.to_string()) {
tools.push(builtin_tool.to_string());
}
}
}
tools.sort();
tools
}
pub fn supports_tool(&self, tool_name: &str) -> bool {
if self.config.tools.contains_key(tool_name) {
return true;
}
if self.config.defaults.fallback_to_builtin {
return ["uv", "node", "go", "rust"].contains(&tool_name);
}
false
}
pub fn config(&self) -> &VxConfig {
&self.config
}
pub fn project_info(&self) -> &Option<ProjectInfo> {
&self.project_info
}
pub fn figment(&self) -> &Figment {
&self.figment
}
pub fn get_status(&self) -> ConfigStatus {
let layers = collect_layer_info();
ConfigStatus {
layers,
available_tools: self.get_available_tools(),
fallback_enabled: self.config.defaults.fallback_to_builtin,
project_info: self.project_info.clone(),
}
}
pub async fn init_project_config(
&self,
tools: Option<HashMap<String, String>>,
interactive: bool,
) -> Result<()> {
let config_path = get_project_config_path()?;
validate_config_not_exists(&config_path)?;
let project_config = self.create_project_config(tools, interactive);
let content = generate_config_content(&project_config)?;
write_config_file(&config_path, &content)?;
Ok(())
}
fn create_project_config(
&self,
tools: Option<HashMap<String, String>>,
interactive: bool,
) -> ProjectConfig {
let mut project_config = ProjectConfig::default();
if let Some(tools) = tools {
project_config.tools = tools;
} else if interactive {
if let Some(project_info) = &self.project_info {
project_config.tools = project_info.tool_versions.clone();
}
}
project_config.settings.auto_install = true;
project_config.settings.cache_duration = "7d".to_string();
project_config
}
pub fn validate(&self) -> Result<Vec<String>> {
let mut warnings = Vec::new();
if self.config.tools.is_empty() && self.project_info.is_none() {
warnings.push("No tools configured and no project detected".to_string());
}
if !self.config.defaults.auto_install {
warnings
.push("Auto-install is disabled - tools may need manual installation".to_string());
}
Ok(warnings)
}
pub async fn sync_project(&self, _force: bool) -> Result<Vec<String>> {
let mut installed_tools = Vec::new();
if let Some(project_info) = &self.project_info {
for (tool_name, version) in &project_info.tool_versions {
installed_tools.push(format!("{}@{}", tool_name, version));
}
}
Ok(installed_tools)
}
pub fn get_download_url(&self, tool_name: &str, version: &str) -> Result<String> {
if let Some(tool_config) = self.config.tools.get(tool_name) {
if let Some(custom_sources) = &tool_config.custom_sources {
if let Some(url) = custom_sources.first() {
return Ok(format!("{}/{}", url, version));
}
}
}
Err(crate::error::ConfigError::Other {
message: format!("No download URL configured for tool: {}", tool_name),
})
}
}
fn collect_layer_info() -> Vec<LayerInfo> {
let mut layers = Vec::new();
layers.push(create_builtin_layer_info());
if let Some(user_layer) = create_user_layer_info() {
layers.push(user_layer);
}
layers.push(create_project_layer_info());
layers.push(create_environment_layer_info());
layers
}
fn create_builtin_layer_info() -> LayerInfo {
LayerInfo {
name: "builtin".to_string(),
available: true,
priority: 10,
}
}
fn create_user_layer_info() -> Option<LayerInfo> {
dirs::config_dir().map(|config_dir| {
let global_config = config_dir.join("vx").join("config.toml");
LayerInfo {
name: "user".to_string(),
available: global_config.exists(),
priority: 50,
}
})
}
fn create_project_layer_info() -> LayerInfo {
let project_config = PathBuf::from(".vx.toml");
LayerInfo {
name: "project".to_string(),
available: project_config.exists(),
priority: 80,
}
}
fn create_environment_layer_info() -> LayerInfo {
LayerInfo {
name: "environment".to_string(),
available: std::env::vars().any(|(k, _)| k.starts_with("VX_")),
priority: 100,
}
}
fn get_project_config_path() -> Result<PathBuf> {
let config_path = std::env::current_dir()
.map_err(|e| crate::error::ConfigError::Io {
message: format!("Failed to get current directory: {}", e),
source: e,
})?
.join(".vx.toml");
Ok(config_path)
}
fn validate_config_not_exists(config_path: &Path) -> Result<()> {
if config_path.exists() {
return Err(crate::error::ConfigError::Validation {
message: "Configuration file .vx.toml already exists".to_string(),
});
}
Ok(())
}
fn generate_config_content(project_config: &ProjectConfig) -> Result<String> {
let toml_content = toml::to_string_pretty(project_config)?;
let header = r#"# VX Project Configuration
# This file defines the tools and versions required for this project.
# Run 'vx sync' to install all required tools.
"#;
Ok(format!("{}{}", header, toml_content))
}
fn write_config_file(config_path: &PathBuf, content: &str) -> Result<()> {
std::fs::write(config_path, content).map_err(|e| crate::error::ConfigError::Io {
message: format!("Failed to write .vx.toml: {}", e),
source: e,
})
}