use anyhow::{Context, Result};
use std::path::Path;
use wcore::paths::{AGENTS_DIR, CONFIG_FILE, LOCAL_DIR, PACKAGES_DIR, SKILLS_DIR};
pub const DEFAULT_CONFIG: &str = include_str!("../../config.toml");
pub fn scaffold_config_dir(config_dir: &Path) -> Result<()> {
migrate_layout(config_dir);
std::fs::create_dir_all(config_dir.join(AGENTS_DIR))
.context("failed to create agents directory")?;
std::fs::create_dir_all(config_dir.join(SKILLS_DIR))
.context("failed to create skills directory")?;
std::fs::create_dir_all(config_dir.join(PACKAGES_DIR))
.context("failed to create packages directory")?;
let config_toml = config_dir.join(CONFIG_FILE);
if !config_toml.exists() {
std::fs::write(&config_toml, DEFAULT_CONFIG)
.with_context(|| format!("failed to write {}", config_toml.display()))?;
}
Ok(())
}
fn migrate_layout(config_dir: &Path) {
let old_config = config_dir.join("crab.toml");
let new_config = config_dir.join(CONFIG_FILE);
if old_config.exists() && !new_config.exists() {
if let Err(e) = std::fs::rename(&old_config, &new_config) {
tracing::warn!("failed to rename crab.toml → config.toml: {e}");
} else {
tracing::info!("migrated crab.toml → config.toml");
}
}
let local_dir = config_dir.join(LOCAL_DIR);
let _ = std::fs::create_dir_all(&local_dir);
let old_skills = config_dir.join("skills");
let new_skills = config_dir.join(SKILLS_DIR);
if old_skills.exists() && old_skills.is_dir() && !new_skills.exists() {
if let Err(e) = std::fs::rename(&old_skills, &new_skills) {
tracing::warn!("failed to move skills/ → local/skills/: {e}");
} else {
tracing::info!("migrated skills/ → local/skills/");
}
}
let old_agents = config_dir.join("agents");
let new_agents = config_dir.join(AGENTS_DIR);
if old_agents.exists() && old_agents.is_dir() && !new_agents.exists() {
if let Err(e) = std::fs::rename(&old_agents, &new_agents) {
tracing::warn!("failed to move agents/ → local/agents/: {e}");
} else {
tracing::info!("migrated agents/ → local/agents/");
}
}
let config_path = config_dir.join(CONFIG_FILE);
if config_path.exists() {
migrate_mcps_agents(&config_path, &local_dir.join("CrabTalk.toml"));
}
}
fn migrate_mcps_agents(config_path: &Path, manifest_path: &Path) {
use toml_edit::DocumentMut;
let Ok(content) = std::fs::read_to_string(config_path) else {
return;
};
let Ok(mut doc) = content.parse::<DocumentMut>() else {
return;
};
let has_mcps = doc
.get("mcps")
.and_then(|v| v.as_table())
.is_some_and(|t| !t.is_empty());
let has_agents = doc
.get("agents")
.and_then(|v| v.as_table())
.is_some_and(|t| !t.is_empty());
if !has_mcps && !has_agents {
return;
}
let mut manifest_doc = if manifest_path.exists() {
std::fs::read_to_string(manifest_path)
.ok()
.and_then(|s| s.parse::<DocumentMut>().ok())
.unwrap_or_default()
} else {
DocumentMut::default()
};
if has_mcps
&& manifest_doc
.get("mcps")
.and_then(|v| v.as_table())
.is_none_or(|t| t.is_empty())
&& let Some(mcps) = doc.remove("mcps")
{
manifest_doc.insert("mcps", mcps);
tracing::info!("migrated [mcps] from config.toml → local/CrabTalk.toml");
}
if has_agents
&& manifest_doc
.get("agents")
.and_then(|v| v.as_table())
.is_none_or(|t| t.is_empty())
&& let Some(agents) = doc.remove("agents")
{
manifest_doc.insert("agents", agents);
tracing::info!("migrated [agents] from config.toml → local/CrabTalk.toml");
}
if let Err(e) = std::fs::write(manifest_path, manifest_doc.to_string()) {
tracing::warn!("failed to write local/CrabTalk.toml: {e}");
return;
}
if let Err(e) = std::fs::write(config_path, doc.to_string()) {
tracing::warn!("failed to update config.toml after migration: {e}");
}
}