use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum LlmProvider {
Ollama,
OpenAi,
Anthropic,
Custom,
}
impl Default for LlmProvider {
fn default() -> Self {
LlmProvider::Ollama
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub provider: LlmProvider,
pub base_url: String,
pub model: String,
#[serde(default)]
pub api_key: String,
}
impl Default for LlmConfig {
fn default() -> Self {
LlmConfig {
provider: LlmProvider::Ollama,
base_url: "http://localhost:11434".to_string(),
model: "codellama".to_string(),
api_key: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryConfig {
#[serde(default = "default_auto_scan")]
pub auto_scan: bool,
#[serde(default)]
pub scan_roots: Vec<String>,
}
fn default_auto_scan() -> bool {
true
}
impl Default for RegistryConfig {
fn default() -> Self {
RegistryConfig {
auto_scan: true,
scan_roots: vec!["/home/feanor/Projects".to_string()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub llm: LlmConfig,
#[serde(default)]
pub registry: RegistryConfig,
}
impl Default for Config {
fn default() -> Self {
Config {
llm: LlmConfig::default(),
registry: RegistryConfig::default(),
}
}
}
pub fn default_config_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home)
.join(".config")
.join("magellan")
.join("config.toml")
}
pub fn load() -> Result<Config> {
let path = default_config_path();
load_from(&path)
}
pub fn load_from(path: &PathBuf) -> Result<Config> {
if !path.exists() {
return Ok(Config::default());
}
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config from {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("Failed to parse config from {}", path.display()))
}
pub fn save(config: &Config) -> Result<()> {
let path = default_config_path();
save_to(config, &path)
}
pub fn save_to(config: &Config, path: &PathBuf) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {}", parent.display()))?;
}
let content = toml::to_string_pretty(config).context("Failed to serialize config to TOML")?;
std::fs::write(path, content)
.with_context(|| format!("Failed to write config to {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.llm.provider, LlmProvider::Ollama);
assert_eq!(config.llm.base_url, "http://localhost:11434");
assert!(config.registry.auto_scan);
}
#[test]
fn test_config_path() {
let path = default_config_path();
assert!(path
.to_string_lossy()
.contains(".config/magellan/config.toml"));
}
#[test]
fn test_load_from_invalid_path() {
let result = load_from(&PathBuf::from("/nonexistent/path/config.toml"));
assert!(result.is_ok());
}
}