use anyhow::{Result, bail};
use crate::claude::{mcp, settings};
use crate::config::Paths;
use crate::diagnostics;
use crate::persona::{self, ClaudeMdConfig, McpConfig, Persona, SkillsConfig};
pub fn run(paths: &Paths, name: Option<String>) -> Result<()> {
let persona_name = name.unwrap_or_else(|| {
chrono::Local::now()
.format("snap-%Y%m%d-%H%M%S")
.to_string()
});
let persona_file = persona::persona_path(&paths.personas, &persona_name);
if persona_file.exists() {
bail!("Persona '{}' already exists.", persona_name);
}
let current_settings = settings::read_settings(&paths.claude_settings)?;
let managed = diagnostics::list_managed_links(paths)?;
let mut active_skills: Vec<String> = managed.into_iter().map(|(name, _)| name).collect();
active_skills.sort();
let untracked = diagnostics::list_untracked_skills(paths)?;
if !untracked.is_empty() {
eprintln!(
" ⚠ {} untracked skill(s) not captured (wild directories): {}",
untracked.len(),
untracked.join(", ")
);
eprintln!(" Run `cc-persona adopt` first if you want them in this persona.");
}
let mcp_servers = mcp::list_mcp_servers(&paths.claude_json)?;
let enabled_mcp: Vec<String> = mcp_servers
.iter()
.filter(|(_, disabled)| !disabled)
.map(|(name, _)| name.clone())
.collect();
let disabled_mcp: Vec<String> = mcp_servers
.iter()
.filter(|(_, disabled)| *disabled)
.map(|(name, _)| name.clone())
.collect();
let persona = Persona {
name: persona_name.clone(),
description: format!(
"Snapshot taken at {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
),
base: None,
settings: Some(current_settings),
skills: Some(SkillsConfig {
active: active_skills,
}),
mcp: Some(McpConfig {
enable: enabled_mcp,
disable: disabled_mcp,
}),
claude_md: Some(ClaudeMdConfig {
file: Some(format!("{}.md", persona_name)),
}),
};
paths.ensure_dirs()?;
persona.save(&persona_file)?;
let md_target = paths.claude_md.join(format!("{}.md", persona_name));
if paths.claude_md_file.exists() {
let content = std::fs::read_to_string(&paths.claude_md_file).unwrap_or_default();
std::fs::write(&md_target, content)?;
} else {
std::fs::write(&md_target, "")?;
}
eprintln!("✓ Snapped current config as persona '{}'", persona_name);
eprintln!(" File: {}", persona_file.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::TestEnv;
use serde_json::json;
#[cfg(unix)]
#[test]
fn run_captures_current_configuration_into_persona_assets() {
let env = TestEnv::new();
env.paths.ensure_dirs().unwrap();
env.write_file(
&env.paths.claude_settings,
&json!({
"model": "claude-sonnet",
"ui": {
"theme": "light"
}
})
.to_string(),
);
env.write_file(
&env.paths.claude_json,
&json!({
"mcpServers": {
"GitHub": {
"command": "github"
},
"Figma": {
"command": "figma",
"disabled": true
}
}
})
.to_string(),
);
env.write_file(&env.paths.claude_md_file, "current claude instructions");
std::fs::create_dir_all(&env.paths.claude_skills).unwrap();
env.create_store_skill("alpha", "---\nname: alpha\n---\n");
env.link_into_claude_skills("alpha");
env.create_skill(&env.paths.claude_skills, "wild", "---\nname: wild\n---\n");
run(&env.paths, Some("engineer".to_string())).unwrap();
let persona_file = persona::persona_path(&env.paths.personas, "engineer");
assert!(persona_file.exists());
let snapped = Persona::load(&persona_file).unwrap();
assert_eq!(snapped.name, "engineer");
assert_eq!(
snapped.settings,
Some(json!({
"model": "claude-sonnet",
"ui": {
"theme": "light"
}
}))
);
assert_eq!(snapped.skills.unwrap().active, vec!["alpha"]);
let mcp = snapped.mcp.unwrap();
assert_eq!(mcp.enable, vec!["GitHub"]);
assert_eq!(mcp.disable, vec!["Figma"]);
assert_eq!(
snapped.claude_md.unwrap().file.as_deref(),
Some("engineer.md")
);
assert_eq!(
env.read_file(&env.paths.claude_md.join("engineer.md")),
"current claude instructions"
);
}
}