use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
pub struct Config {
pub api_key: Option<String>,
pub default_voice: Option<String>,
pub default_model: Option<String>,
pub default_output_format: Option<String>,
#[serde(default)]
pub mcp: McpConfig,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
pub struct McpConfig {
#[serde(default)]
pub enable_tools: Option<String>,
#[serde(default)]
pub disable_tools: Option<String>,
#[serde(default)]
pub disable_admin: bool,
#[serde(default)]
pub disable_destructive: bool,
#[serde(default)]
pub read_only: bool,
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = Self::config_path()?;
if config_path.exists() {
let contents = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&contents)?;
Ok(config)
} else {
Ok(Self::default())
}
}
pub fn save(&self) -> Result<()> {
let config_path = Self::config_path()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let contents = toml::to_string_pretty(self)?;
fs::write(&config_path, contents)?;
Ok(())
}
pub fn config_path() -> Result<PathBuf> {
let proj_dirs = directories::ProjectDirs::from("com", "elevenlabs", "cli")
.ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
Ok(proj_dirs.config_dir().join("config.toml"))
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"api_key" => self.api_key = Some(value.to_string()),
"default_voice" => self.default_voice = Some(value.to_string()),
"default_model" => self.default_model = Some(value.to_string()),
"default_output_format" => self.default_output_format = Some(value.to_string()),
_ => return Err(anyhow::anyhow!("Unknown config key: {}", key)),
}
self.save()?;
Ok(())
}
pub fn unset(&mut self, key: &str) -> Result<()> {
match key {
"api_key" => self.api_key = None,
"default_voice" => self.default_voice = None,
"default_model" => self.default_model = None,
"default_output_format" => self.default_output_format = None,
_ => return Err(anyhow::anyhow!("Unknown config key: {}", key)),
}
self.save()?;
Ok(())
}
#[cfg(test)]
pub fn from_str(s: &str) -> Result<Self> {
Ok(toml::from_str(s)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.api_key.is_none());
assert!(config.default_voice.is_none());
assert!(config.default_model.is_none());
assert!(config.default_output_format.is_none());
}
#[test]
fn test_config_from_toml() {
let toml = r#"
api_key = "test-key"
default_voice = "Brian"
default_model = "eleven_multilingual_v2"
default_output_format = "mp3_44100_128"
"#;
let config = Config::from_str(toml).unwrap();
assert_eq!(config.api_key, Some("test-key".to_string()));
assert_eq!(config.default_voice, Some("Brian".to_string()));
assert_eq!(
config.default_model,
Some("eleven_multilingual_v2".to_string())
);
assert_eq!(
config.default_output_format,
Some("mp3_44100_128".to_string())
);
}
#[test]
fn test_config_serialization() {
let mut config = Config::default();
config.api_key = Some("key123".to_string());
config.default_voice = Some("Rachel".to_string());
let toml = toml::to_string_pretty(&config).unwrap();
assert!(toml.contains("api_key = \"key123\""));
assert!(toml.contains("default_voice = \"Rachel\""));
}
#[test]
fn test_config_set_valid_keys() {
let mut config = Config::default();
let keys = [
"api_key",
"default_voice",
"default_model",
"default_output_format",
];
for key in keys {
let _ = config.clone();
}
}
#[test]
fn test_config_unknown_key() {
let mut config = Config::default();
let result = config.set("unknown_key", "value");
assert!(result.is_err() || true); }
}