use anyhow::{Context, Result};
use std::fs;
use super::Config;
impl Config {
fn initialize_config(&mut self) {}
pub fn ensure_octomind_dir() -> Result<std::path::PathBuf> {
crate::directories::get_octomind_data_dir()
}
pub fn copy_default_config_template(config_path: &std::path::Path) -> Result<()> {
const DEFAULT_CONFIG_TEMPLATE: &str = include_str!("../../config-templates/default.toml");
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context(format!(
"Failed to create config directory: {}",
parent.display()
))?;
}
fs::write(config_path, DEFAULT_CONFIG_TEMPLATE).context(format!(
"Failed to write default config template to {}",
config_path.display()
))?;
println!("Created default configuration at {}", config_path.display());
println!("Please edit the configuration file to set your API keys and preferences.");
Ok(())
}
pub fn create_default_config() -> Result<std::path::PathBuf> {
let config_path = crate::directories::get_config_file_path()?;
if !config_path.exists() {
Self::copy_default_config_template(&config_path)?;
}
Ok(config_path)
}
fn inject_default_config() -> Result<Self> {
const DEFAULT_CONFIG_TEMPLATE: &str = include_str!("../../config-templates/default.toml");
let mut config: Config = toml::from_str(DEFAULT_CONFIG_TEMPLATE)
.context("Failed to parse default configuration template")?;
config.build_role_map();
Ok(config)
}
pub fn load() -> Result<Self> {
let config_path = crate::directories::get_config_file_path()?;
if !config_path.exists() {
let default_config = Self::inject_default_config()?;
default_config.save_to_path(&config_path)?;
}
super::migrations::check_and_upgrade_config(&config_path)
.context("Failed to check/upgrade config version")?;
let config_str = fs::read_to_string(&config_path).context(format!(
"Failed to read config from {}",
config_path.display()
))?;
let mut config: Config = toml::from_str(&config_str).context(
"Failed to parse TOML configuration. All required fields must be present in strict mode."
)?;
config.config_path = Some(config_path);
config.initialize_config();
config.build_role_map();
config.validate()?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
self.validate()?;
let config_path = if let Some(path) = &self.config_path {
path.clone()
} else {
crate::directories::get_config_file_path()?
};
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context(format!(
"Failed to create config directory: {}",
parent.display()
))?;
}
let config_str =
toml::to_string_pretty(self).context("Failed to serialize configuration to TOML")?;
fs::write(&config_path, config_str).context(format!(
"Failed to write config to {}",
config_path.display()
))?;
println!("Configuration saved to {}", config_path.display());
Ok(())
}
pub fn load_from_path(path: &std::path::Path) -> Result<Self> {
let config_str = fs::read_to_string(path)
.context(format!("Failed to read config from {}", path.display()))?;
let mut config: Config =
toml::from_str(&config_str).context("Failed to parse TOML configuration")?;
config.config_path = Some(path.to_path_buf());
config.initialize_config();
config.build_role_map();
config.validate()?;
Ok(config)
}
pub fn save_to_path(&self, path: &std::path::Path) -> Result<()> {
self.validate()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context(format!(
"Failed to create config directory: {}",
parent.display()
))?;
}
let config_str =
toml::to_string_pretty(self).context("Failed to serialize configuration to TOML")?;
fs::write(path, config_str)
.context(format!("Failed to write config to {}", path.display()))?;
println!("Configuration saved to {}", path.display());
Ok(())
}
pub fn create_clean_copy_for_saving(&self) -> Self {
self.clone()
}
pub fn update_and_save<F>(&mut self, updater: F) -> Result<()>
where
F: FnOnce(&mut Self),
{
self.validate()?;
let config_path = if let Some(path) = &self.config_path {
path.clone()
} else {
crate::directories::get_config_file_path()?
};
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context(format!(
"Failed to create config directory: {}",
parent.display()
))?;
}
let clean_config = self.create_clean_copy_for_saving();
let config_str =
toml::to_string(&clean_config).context("Failed to serialize configuration to TOML")?;
fs::write(&config_path, config_str).context(format!(
"Failed to write config to {}",
config_path.display()
))?;
updater(self);
Ok(())
}
pub fn update_specific_field<F>(&mut self, updater: F) -> Result<()>
where
F: Fn(&mut Config),
{
let config_path = if let Some(path) = &self.config_path {
path.clone()
} else {
crate::directories::get_config_file_path()?
};
let mut disk_config = if config_path.exists() {
let config_str = fs::read_to_string(&config_path).context(format!(
"Failed to read config from {}",
config_path.display()
))?;
let mut config: Config =
toml::from_str(&config_str).context("Failed to parse TOML configuration")?;
config.config_path = Some(config_path.clone());
config
} else {
return Err(anyhow::anyhow!(
"No configuration file found at {}. Run with --init to create a default configuration.",
config_path.display()
));
};
updater(&mut disk_config);
disk_config.validate()?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context(format!(
"Failed to create config directory: {}",
parent.display()
))?;
}
let clean_config = disk_config.create_clean_copy_for_saving();
let config_str =
toml::to_string(&clean_config).context("Failed to serialize configuration to TOML")?;
fs::write(&config_path, config_str).context(format!(
"Failed to write config to {}",
config_path.display()
))?;
updater(self);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_test_config_with_custom_role() -> String {
let template_content = include_str!("../../config-templates/default.toml");
let mut config = template_content.to_string();
config.push_str(
r#"
# Test role for unit testing
[[roles]]
name = "tester"
enable_layers = false
temperature = 0.7
top_p = 0.9
top_k = 50
layer_refs = []
system = "You are a test assistant."
welcome = "Hello! Test tester role."
mcp = { server_refs = ["test_server", "clt"], allowed_tools = [] }
# Additional test MCP servers for tester role
[[mcp.servers]]
name = "test_server"
type = "stdin"
command = "test_command"
args = ["mcp"]
timeout_seconds = 30
tools = []
[[mcp.servers]]
name = "clt"
type = "stdin"
command = "clt"
args = ["mcp"]
timeout_seconds = 30
tools = []
"#,
);
config
}
#[test]
fn test_role_parsing() {
let test_config = get_test_config_with_custom_role();
let mut config: Config = toml::from_str(&test_config).expect("Failed to parse test config");
config.build_role_map();
assert_eq!(config.roles.len(), 3);
assert_eq!(config.role_map.len(), 3);
assert!(config.role_map.contains_key("tester"));
let tester_role = config.role_map.get("tester").unwrap();
assert_eq!(tester_role.mcp.server_refs, vec!["test_server", "clt"]);
assert!(!tester_role.config.enable_layers);
let (role_config, mcp_config, _, _, _) = config.get_role_config("tester");
assert!(!role_config.enable_layers);
assert_eq!(mcp_config.server_refs, vec!["test_server", "clt"]);
let merged_config = config.get_merged_config_for_role("tester");
let server_names: Vec<&str> = merged_config.mcp.servers.iter().map(|s| s.name()).collect();
assert!(server_names.contains(&"test_server"));
assert!(server_names.contains(&"clt"));
assert!(!server_names.contains(&"developer"));
assert!(!server_names.contains(&"filesystem"));
}
#[test]
fn test_role_merged_config() {
let test_config = get_test_config_with_custom_role();
let mut config: Config = toml::from_str(&test_config).expect("Failed to parse test config");
config.build_role_map();
let merged_config = config.get_merged_config_for_role("tester");
let server_names: Vec<&str> = merged_config.mcp.servers.iter().map(|s| s.name()).collect();
assert!(server_names.contains(&"test_server"));
assert!(server_names.contains(&"clt"));
assert!(!server_names.contains(&"developer")); assert!(!server_names.contains(&"filesystem")); }
#[test]
fn test_max_tokens_inheritance() {
let test_config = get_test_config_with_custom_role();
let mut config: Config = toml::from_str(&test_config).expect("Failed to parse test config");
config.build_role_map();
assert_eq!(config.get_max_tokens("developer"), 16384);
assert_eq!(config.get_max_tokens("assistant"), 16384);
assert_eq!(config.get_max_tokens("tester"), 16384);
assert_eq!(config.get_max_tokens("nonexistent_role"), 16384);
assert_eq!(config.get_effective_max_tokens(), 16384);
let (role_config, _, _, _, _) = config.get_role_config("tester");
assert!(!role_config.enable_layers); }
}