Skip to main content

aether_cli/agent/
remove.rs

1use aether_project::{AetherSettings, AgentConfig};
2use crossterm::style::Stylize;
3use std::fs;
4use std::path::Path;
5
6use crate::agent::RemoveArgs;
7use crate::error::CliError;
8
9pub fn run_remove(args: RemoveArgs) -> Result<(), CliError> {
10    let project_root = args.path.canonicalize().unwrap_or(args.path);
11    let settings_path = project_root.join(".aether/settings.json");
12
13    let content = fs::read_to_string(&settings_path).map_err(CliError::IoError)?;
14    let mut config: AetherSettings =
15        serde_json::from_str(&content).map_err(|e| CliError::AgentError(format!("Failed to parse settings: {e}")))?;
16
17    let index = config
18        .agents
19        .iter()
20        .position(|a| a.name == args.name)
21        .ok_or_else(|| CliError::AgentError(format!("Agent '{}' not found", args.name)))?;
22
23    let entry = config.agents.remove(index);
24    let slug = entry.name.to_lowercase().replace(' ', "-");
25
26    cleanup_agent_files(&project_root, &slug, &entry);
27
28    let json = serde_json::to_string_pretty(&config).expect("settings serialization cannot fail");
29    fs::write(&settings_path, json).map_err(CliError::IoError)?;
30
31    println!("{} Removed agent '{}'", "✓".green().bold(), entry.name);
32    Ok(())
33}
34
35fn cleanup_agent_files(project_root: &Path, slug: &str, entry: &AgentConfig) {
36    let per_agent_dir = project_root.join(".aether/agents").join(slug);
37    if per_agent_dir.is_dir() {
38        let _ = fs::remove_dir_all(&per_agent_dir);
39    }
40
41    for prompt in &entry.prompts {
42        let Some(prompt_path) = prompt.path() else { continue };
43        let path = project_root.join(prompt_path);
44        if path.starts_with(project_root.join(".aether")) {
45            let _ = fs::remove_file(&path);
46        }
47    }
48
49    for mcp in &entry.mcps {
50        let Some(mcp_path) = mcp.path() else { continue };
51        let path = project_root.join(mcp_path);
52        if path.starts_with(project_root.join(".aether")) {
53            let _ = fs::remove_file(&path);
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::agent::new_agent_wizard::{DraftAgentEntry, add_agent, build_system_md, scaffold};
62
63    #[test]
64    fn remove_only_agent() {
65        let dir = tempfile::tempdir().unwrap();
66        scaffold(dir.path(), &default_draft()).unwrap();
67
68        let args = super::super::RemoveArgs { name: "Default".to_string(), path: dir.path().to_path_buf() };
69        run_remove(args).unwrap();
70
71        let content = fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
72        let config: AetherSettings = serde_json::from_str(&content).unwrap();
73        assert!(config.agents.is_empty());
74
75        assert!(!dir.path().join(".aether/DEFAULT.md").exists());
76    }
77
78    #[test]
79    fn remove_second_agent_keeps_first() {
80        let dir = tempfile::tempdir().unwrap();
81        scaffold(dir.path(), &default_draft()).unwrap();
82
83        let settings_path = dir.path().join(".aether/settings.json");
84        add_agent(&settings_path, &researcher_draft()).unwrap();
85
86        let args = super::super::RemoveArgs { name: "Researcher".to_string(), path: dir.path().to_path_buf() };
87        run_remove(args).unwrap();
88
89        let content = fs::read_to_string(&settings_path).unwrap();
90        let config: AetherSettings = serde_json::from_str(&content).unwrap();
91        assert_eq!(config.agents.len(), 1);
92        assert_eq!(config.agents[0].name, "Default");
93
94        assert!(!dir.path().join(".aether/agents/researcher").exists());
95        assert!(dir.path().join(".aether/DEFAULT.md").exists());
96    }
97
98    #[test]
99    fn remove_nonexistent_agent_returns_error() {
100        let dir = tempfile::tempdir().unwrap();
101        scaffold(dir.path(), &default_draft()).unwrap();
102
103        let args = super::super::RemoveArgs { name: "Ghost".to_string(), path: dir.path().to_path_buf() };
104        let result = run_remove(args);
105        assert!(result.is_err());
106    }
107
108    #[test]
109    fn remove_no_settings_file_returns_error() {
110        let dir = tempfile::tempdir().unwrap();
111        let args = super::super::RemoveArgs { name: "Default".to_string(), path: dir.path().to_path_buf() };
112        let result = run_remove(args);
113        assert!(result.is_err());
114    }
115
116    fn default_draft() -> DraftAgentEntry {
117        let mut draft = DraftAgentEntry {
118            entry: AgentConfig {
119                name: "Default".to_string(),
120                description: "Default coding agent".to_string(),
121                user_invocable: true,
122                agent_invocable: true,
123                model: "anthropic:claude-sonnet-4-5".to_string(),
124                prompts: vec![aether_project::PromptSource::file("AGENTS.md")],
125                ..AgentConfig::default()
126            },
127            system_md_content: String::new(),
128            system_md_edited: false,
129            selected_mcp_servers: vec!["coding".into()],
130            workspace_mcp_configs: vec![],
131        };
132        draft.system_md_content = build_system_md(&draft);
133        draft
134    }
135
136    fn researcher_draft() -> DraftAgentEntry {
137        let mut draft = default_draft();
138        draft.entry.name = "Researcher".to_string();
139        draft.entry.description = "Research agent".to_string();
140        draft.selected_mcp_servers = vec![];
141        draft.workspace_mcp_configs = vec![];
142        draft.system_md_content = build_system_md(&draft);
143        draft
144    }
145}