use crate::cli::error::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub verbose: bool,
#[serde(default)]
pub quiet: bool,
pub data_dir: Option<PathBuf>,
#[serde(default)]
pub session: SessionDefaults,
#[serde(default)]
pub claude: ClaudeDefaults,
#[serde(default)]
pub output: OutputDefaults,
#[serde(default)]
pub analytics: AnalyticsDefaults,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionDefaults {
pub model: Option<String>,
pub system_prompt: Option<String>,
pub allowed_tools: Option<Vec<String>>,
pub auto_archive_days: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeDefaults {
#[serde(default)]
pub stream_format: String,
pub max_tokens: Option<usize>,
pub api_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputDefaults {
#[serde(default = "default_true")]
pub color: bool,
#[serde(default = "default_true")]
pub progress: bool,
#[serde(default)]
pub format: String,
#[serde(default)]
pub timestamps: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticsDefaults {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_analytics_retention")]
pub retention_days: u32,
#[serde(default)]
pub auto_reports: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
timeout_secs: default_timeout(),
verbose: false,
quiet: false,
data_dir: None,
session: SessionDefaults::default(),
claude: ClaudeDefaults::default(),
output: OutputDefaults::default(),
analytics: AnalyticsDefaults::default(),
}
}
}
impl Default for SessionDefaults {
fn default() -> Self {
Self {
model: None,
system_prompt: None,
allowed_tools: None,
auto_archive_days: Some(30),
}
}
}
impl Default for ClaudeDefaults {
fn default() -> Self {
Self {
stream_format: "text".to_string(),
max_tokens: None,
api_timeout_secs: Some(30),
}
}
}
impl Default for OutputDefaults {
fn default() -> Self {
Self {
color: default_true(),
progress: default_true(),
format: "pretty".to_string(),
timestamps: false,
}
}
}
impl Default for AnalyticsDefaults {
fn default() -> Self {
Self {
enabled: default_true(),
retention_days: default_analytics_retention(),
auto_reports: false,
}
}
}
impl Config {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
if !path.exists() {
let config = Self::default();
config.save_to_file(path)?;
return Ok(config);
}
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content).map_err(|e| {
crate::cli::error::InteractiveError::Configuration(format!(
"Failed to parse config file: {}",
e
))
})?;
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self).map_err(|e| {
crate::cli::error::InteractiveError::Configuration(format!(
"Failed to serialize config: {}",
e
))
})?;
std::fs::write(path, content)?;
Ok(())
}
pub fn default_path() -> Result<PathBuf> {
let data_dir = crate::cli::ensure_data_dir()?;
Ok(data_dir.join("config.toml"))
}
pub fn load_default() -> Result<Self> {
let path = Self::default_path()?;
Self::load_from_file(path)
}
pub fn merge_with_cli_args(mut self, cli_args: &crate::cli::cli::Cli) -> Self {
if cli_args.verbose {
self.verbose = true;
}
if cli_args.quiet {
self.quiet = true;
}
if cli_args.timeout != default_timeout() {
self.timeout_secs = cli_args.timeout;
}
if let Some(ref data_dir) = cli_args.data_dir {
self.data_dir = Some(data_dir.clone());
}
self
}
}
fn default_timeout() -> u64 {
60
}
fn default_true() -> bool {
true
}
fn default_analytics_retention() -> u32 {
90
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(config.timeout_secs, 60);
assert!(!config.verbose);
assert!(!config.quiet);
assert!(config.output.color);
assert!(config.analytics.enabled);
}
#[test]
fn test_config_save_load() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.toml");
let mut config = Config::default();
config.verbose = true;
config.timeout_secs = 120;
config.session.model = Some("claude-3-opus-20240229".to_string());
config.save_to_file(&config_path).unwrap();
let loaded_config = Config::load_from_file(&config_path).unwrap();
assert_eq!(loaded_config.verbose, true);
assert_eq!(loaded_config.timeout_secs, 120);
assert_eq!(
loaded_config.session.model,
Some("claude-3-opus-20240229".to_string())
);
}
#[test]
fn test_config_file_creation() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("config.toml");
let config = Config::load_from_file(&config_path).unwrap();
assert!(config_path.exists());
assert_eq!(config.timeout_secs, default_timeout());
}
}