use super::paths::{config_file_path, ensure_config_dir, secrets_file_path};
use super::secrets::{
decrypt_key, extract_secrets, load_secrets, save_secrets, update_gitignore_for_secrets,
};
use super::types::{ConfigFile, LegacyForkSection, LegacyHiveSection, ProviderEntry};
use crate::agent::swarm::config::{CollaborationMode, CollaborationSection};
use crate::common::{AgentError, Result};
use fs2::FileExt;
use std::path::Path;
pub(super) fn migrate_legacy_sections(
fork: &Option<LegacyForkSection>,
hive: &Option<LegacyHiveSection>,
collab: &mut CollaborationSection,
) {
if collab.mode.is_some() {
return;
}
if let Some(old_hive) = hive
&& old_hive.enabled.unwrap_or(false)
{
eprintln!(
"\x1b[33m[DEPRECATION]\x1b[0m Found old [hive] section in config.toml. \
Please migrate to:\n [collaboration]\n mode = \"hive\"\n"
);
collab.mode = Some(CollaborationMode::Hive);
collab.max_agents = collab.max_agents.or(old_hive.max_agents);
collab.worker_model = collab
.worker_model
.take()
.or_else(|| old_hive.worker_model.clone());
collab.coordinator_model = collab
.coordinator_model
.take()
.or_else(|| old_hive.coordinator_model.clone());
collab.strategy = collab.strategy.take().or_else(|| old_hive.strategy.clone());
collab.require_consensus = collab.require_consensus.or(old_hive.require_consensus);
collab.conflict_resolution = collab
.conflict_resolution
.take()
.or_else(|| old_hive.conflict_resolution.clone());
collab.auto_suggest = collab.auto_suggest.or(old_hive.auto_suggest);
return;
}
if let Some(old_fork) = fork
&& old_fork.enabled.unwrap_or(false)
{
eprintln!(
"\x1b[33m[DEPRECATION]\x1b[0m Found old [fork] section in config.toml. \
Please migrate to:\n [collaboration]\n mode = \"fork\"\n"
);
collab.mode = Some(CollaborationMode::Fork);
collab.max_agents = collab.max_agents.or(old_fork.max_agents);
collab.worker_model = collab
.worker_model
.take()
.or_else(|| old_fork.worker_model.clone());
collab.worktree = collab.worktree.or(old_fork.worktree);
collab.auto_suggest = collab.auto_suggest.or(old_fork.auto_suggest);
}
}
pub fn load_config_file() -> Result<ConfigFile> {
let path = config_file_path();
let mut config: ConfigFile = if path.exists() {
let file = std::fs::File::open(&path).map_err(|e| {
AgentError::Config(format!("Failed to open config: {} - {}", path.display(), e))
})?;
file.lock_shared().map_err(|e| {
AgentError::Config(format!("Failed to lock config: {} - {}", path.display(), e))
})?;
let content = std::fs::read_to_string(&path).map_err(|e| {
AgentError::Config(format!("Failed to read config: {} - {}", path.display(), e))
})?;
file.unlock().map_err(|e| {
AgentError::Config(format!(
"Failed to unlock config: {} - {}",
path.display(),
e
))
})?;
toml::from_str(&content).map_err(|e| {
AgentError::Config(format!(
"Failed to parse config: {} - {}",
path.display(),
e
))
})?
} else {
ConfigFile::default()
};
if path.exists() && !secrets_file_path().exists() {
let _ = migrate_secrets_from_config(&path, &config);
}
let secrets = load_secrets();
super::secrets::merge_secrets(&mut config, &secrets);
Ok(config)
}
fn migrate_secrets_from_config(path: &Path, cf: &ConfigFile) -> Result<()> {
let machine_id = load_secrets().machine_id;
let extracted = extract_secrets(cf, machine_id);
let has_secrets = extracted.api.api_key_enc.is_some()
|| !extracted.providers.is_empty()
|| extracted.web.password_enc.is_some()
|| extracted.telegram.token_enc.is_some()
|| extracted.slack.bot_token_enc.is_some()
|| extracted.slack.app_token_enc.is_some()
|| extracted.discord.token_enc.is_some();
if !has_secrets {
return Ok(());
}
save_secrets(&extracted)?;
update_gitignore_for_secrets();
let raw = toml::to_string_pretty(cf)
.map_err(|e| AgentError::Config(format!("Failed to serialize config: {e}")))?;
let mut clean: ConfigFile = toml::from_str(&raw)
.map_err(|e| AgentError::Config(format!("Failed to re-parse config: {e}")))?;
clean.api.api_key_enc = None;
clean.api.api_key = None;
clean.api.base_url = None; clean.web.password_enc = None;
clean.telegram.token_enc = None;
clean.telegram.token = None;
clean.slack.bot_token_enc = None;
clean.slack.bot_token = None;
clean.slack.app_token_enc = None;
clean.slack.app_token = None;
clean.discord.token_enc = None;
clean.discord.token = None;
for p in clean.providers.iter_mut() {
p.api_key_enc = None;
}
let toml_str = toml::to_string_pretty(&clean)
.map_err(|e| AgentError::Config(format!("Failed to serialize config: {e}")))?;
let lock_file = std::fs::OpenOptions::new()
.write(true)
.open(path)
.map_err(|e| AgentError::Config(format!("Failed to open config for migration: {e}")))?;
lock_file
.lock_exclusive()
.map_err(|e| AgentError::Config(format!("Failed to lock config for migration: {e}")))?;
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, toml_str).map_err(|e| {
AgentError::Config(format!(
"Failed to write temp config {}: {e}",
tmp.display()
))
})?;
std::fs::rename(&tmp, path).map_err(|e| {
AgentError::Config(format!("Failed to replace config {}: {e}", path.display()))
})?;
lock_file
.unlock()
.map_err(|e| AgentError::Config(format!("Failed to unlock config after migration: {e}")))?;
Ok(())
}
pub fn resolve_default_provider() -> Option<(String, ProviderEntry, String, String)> {
let file = load_config_file().ok()?;
let (provider_name, model) = if let Some(ref p) = file.api.provider {
(p.clone(), file.api.model.clone().unwrap_or_default())
} else if let Some(ref chain) = file.api.providers {
let first = chain
.split(',')
.next()
.map(str::trim)
.filter(|s| !s.is_empty())?;
let slash = first.find('/')?;
(first[..slash].to_string(), first[slash + 1..].to_string())
} else {
return None;
};
let (entry, key) = resolve_provider(&provider_name)?;
Some((provider_name, entry, key, model))
}
pub fn resolve_provider(name: &str) -> Option<(ProviderEntry, String)> {
let provider_name = name.split('/').next().unwrap_or(name);
let file = load_config_file().ok()?;
let entry = file
.providers
.iter()
.find(|p| p.name.eq_ignore_ascii_case(provider_name))?
.clone();
let api_key = entry
.api_key_enc
.as_ref()
.filter(|enc| !enc.is_empty())
.and_then(|enc| decrypt_key(enc).ok())
.unwrap_or_default();
Some((entry, api_key))
}
pub fn save_ui_theme(name: &str) -> Result<()> {
let path = config_file_path();
if !path.exists() {
ensure_config_dir()?;
std::fs::write(&path, format!("[ui]\ntheme = \"{name}\"\n"))
.map_err(|e| AgentError::Config(format!("Failed to write config: {e}")))?;
return Ok(());
}
let content = std::fs::read_to_string(&path)
.map_err(|e| AgentError::Config(format!("Failed to read config: {e}")))?;
let mut in_ui = false;
let mut replaced = false;
let mut lines: Vec<String> = content
.lines()
.map(|line| {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_ui = trimmed == "[ui]";
}
if in_ui && !replaced {
let stripped = trimmed.trim_start_matches('#').trim();
if stripped.starts_with("theme") && stripped.contains('=') {
replaced = true;
return format!("theme = \"{name}\"");
}
}
line.to_string()
})
.collect();
if !replaced {
if let Some(pos) = lines.iter().position(|l| l.trim() == "[ui]") {
lines.insert(pos + 1, format!("theme = \"{name}\""));
} else {
lines.push(String::new());
lines.push("[ui]".to_string());
lines.push(format!("theme = \"{name}\""));
}
}
let mut result = lines.join("\n");
if content.ends_with('\n') {
result.push('\n');
}
std::fs::write(&path, result)
.map_err(|e| AgentError::Config(format!("Failed to write config: {e}")))?;
Ok(())
}