use crate::cmd::{CliError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub version: String,
pub profiles: HashMap<String, Profile>,
pub default_profile: String,
pub global: GlobalConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
pub name: String,
pub default_model: Option<String>,
pub default_provider: Option<String>,
pub api_base_url: Option<String>,
pub timeout: Option<u64>,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub settings: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalConfig {
pub auto_save_sessions: bool,
pub session_history_limit: usize,
pub log_level: String,
pub enable_telemetry: bool,
pub check_for_updates: bool,
pub theme: String,
}
impl Default for Config {
fn default() -> Self {
let mut profiles = HashMap::new();
profiles.insert("default".to_string(), Profile::default());
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
profiles,
default_profile: "default".to_string(),
global: GlobalConfig::default(),
}
}
}
impl Default for Profile {
fn default() -> Self {
Self {
name: "default".to_string(),
default_model: None,
default_provider: None,
api_base_url: None,
timeout: Some(30),
max_tokens: Some(4000),
temperature: Some(0.7),
settings: HashMap::new(),
}
}
}
impl Default for GlobalConfig {
fn default() -> Self {
Self {
auto_save_sessions: true,
session_history_limit: 100,
log_level: "info".to_string(),
enable_telemetry: false,
check_for_updates: true,
theme: "default".to_string(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if !config_path.exists() {
let config = Self::default();
config.save()?;
return Ok(config);
}
let content = fs::read_to_string(&config_path)
.map_err(|e| CliError::Config(format!("Failed to read config file: {}", e)))?;
let mut config: Config = serde_json::from_str(&content)
.map_err(|e| CliError::Config(format!("Invalid config format: {}", e)))?;
config.migrate()?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)
.map_err(|e| CliError::Config(format!("Failed to create config directory: {}", e)))?;
}
let content = serde_json::to_string_pretty(self)
.map_err(|e| CliError::Config(format!("Failed to serialize config: {}", e)))?;
fs::write(&config_path, content)
.map_err(|e| CliError::Config(format!("Failed to write config file: {}", e)))?;
Ok(())
}
pub fn config_path() -> Result<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| CliError::Config("Unable to determine config directory".to_string()))?;
Ok(config_dir.join("code-mesh").join("config.json"))
}
pub fn data_dir() -> Result<PathBuf> {
let data_dir = dirs::data_dir()
.ok_or_else(|| CliError::Config("Unable to determine data directory".to_string()))?;
Ok(data_dir.join("code-mesh"))
}
pub fn cache_dir() -> Result<PathBuf> {
let cache_dir = dirs::cache_dir()
.ok_or_else(|| CliError::Config("Unable to determine cache directory".to_string()))?;
Ok(cache_dir.join("code-mesh"))
}
pub fn current_profile(&self) -> Result<&Profile> {
self.profiles
.get(&self.default_profile)
.ok_or_else(|| CliError::Config(format!("Profile '{}' not found", self.default_profile)))
}
pub fn current_profile_mut(&mut self) -> Result<&mut Profile> {
let profile_name = self.default_profile.clone();
self.profiles
.get_mut(&profile_name)
.ok_or_else(|| CliError::Config(format!("Profile '{}' not found", profile_name)))
}
pub fn set_profile(&mut self, name: String, profile: Profile) {
self.profiles.insert(name, profile);
}
pub fn remove_profile(&mut self, name: &str) -> Result<()> {
if name == "default" {
return Err(CliError::Config("Cannot remove default profile".to_string()));
}
if name == self.default_profile {
self.default_profile = "default".to_string();
}
self.profiles.remove(name);
Ok(())
}
pub fn switch_profile(&mut self, name: &str) -> Result<()> {
if !self.profiles.contains_key(name) {
return Err(CliError::Config(format!("Profile '{}' not found", name)));
}
self.default_profile = name.to_string();
Ok(())
}
fn migrate(&mut self) -> Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
if self.version != current_version {
self.version = current_version.to_string();
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
if !self.profiles.contains_key(&self.default_profile) {
return Err(CliError::Config(format!(
"Default profile '{}' not found",
self.default_profile
)));
}
for (name, profile) in &self.profiles {
profile.validate().map_err(|e| {
CliError::Config(format!("Profile '{}' is invalid: {}", name, e))
})?;
}
Ok(())
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
impl Profile {
pub fn validate(&self) -> Result<()> {
if let Some(temp) = self.temperature {
if !(0.0..=2.0).contains(&temp) {
return Err(CliError::Config(
"Temperature must be between 0.0 and 2.0".to_string(),
));
}
}
if let Some(max_tokens) = self.max_tokens {
if max_tokens == 0 {
return Err(CliError::Config(
"Max tokens must be greater than 0".to_string(),
));
}
}
if let Some(timeout) = self.timeout {
if timeout == 0 {
return Err(CliError::Config(
"Timeout must be greater than 0".to_string(),
));
}
}
Ok(())
}
pub fn effective_model(&self) -> Option<String> {
self.default_model.clone()
.or_else(|| Some("anthropic/claude-3-sonnet-20240229".to_string()))
}
pub fn effective_provider(&self) -> Option<String> {
self.default_provider.clone()
.or_else(|| Some("anthropic".to_string()))
}
}
pub struct ConfigManager {
config: Config,
}
impl ConfigManager {
pub fn new() -> Result<Self> {
let config = Config::load()?;
Ok(Self { config })
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
pub fn save(&self) -> Result<()> {
self.config.save()
}
pub fn reload(&mut self) -> Result<()> {
self.config = Config::load()?;
Ok(())
}
pub fn init(force: bool) -> Result<()> {
let config_path = Config::config_path()?;
if config_path.exists() && !force {
return Err(CliError::Config(
"Configuration already exists. Use --force to overwrite".to_string(),
));
}
let config = Config::default();
config.save()?;
let data_dir = Config::data_dir()?;
let cache_dir = Config::cache_dir()?;
fs::create_dir_all(&data_dir)
.map_err(|e| CliError::Config(format!("Failed to create data directory: {}", e)))?;
fs::create_dir_all(&cache_dir)
.map_err(|e| CliError::Config(format!("Failed to create cache directory: {}", e)))?;
Ok(())
}
}