aether_cli/agent/new_agent_wizard/
new_agent_step.rs1use std::path::Path;
2
3use tui::SelectOption;
4
5pub enum NewAgentMode {
6 ScaffoldProject,
7 AddAgentToExistingProject,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PromptFile {
12 Agents,
13 Claude,
14 Gemini,
15}
16
17impl PromptFile {
18 pub fn all() -> &'static [PromptFile] {
19 &[PromptFile::Agents, PromptFile::Claude, PromptFile::Gemini]
20 }
21
22 pub fn filename(self) -> &'static str {
23 match self {
24 PromptFile::Agents => "AGENTS.md",
25 PromptFile::Claude => "CLAUDE.md",
26 PromptFile::Gemini => "GEMINI.md",
27 }
28 }
29
30 pub fn description(self) -> &'static str {
31 match self {
32 PromptFile::Agents => "Project-level instructions shared across agents",
33 PromptFile::Claude => "Claude Code prompt file",
34 PromptFile::Gemini => "Gemini CLI prompt file",
35 }
36 }
37}
38
39pub fn detect_prompt_files(project_root: &Path) -> Vec<PromptFile> {
40 PromptFile::all().iter().copied().filter(|d| project_root.join(d.filename()).is_file()).collect()
41}
42
43pub fn available_prompt_files(mode: &NewAgentMode, project_root: &Path) -> Vec<PromptFile> {
50 let mut detected = detect_prompt_files(project_root);
51 if matches!(mode, NewAgentMode::ScaffoldProject) && !detected.contains(&PromptFile::Agents) {
52 detected.insert(0, PromptFile::Agents);
53 }
54 detected
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum NewAgentOutcome {
59 Applied,
60 Cancelled,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum NewAgentStep {
65 Identity,
66 Model,
67 Prompts,
68 Tools,
69}
70
71impl NewAgentStep {
72 pub fn all() -> &'static [NewAgentStep] {
73 &[NewAgentStep::Identity, NewAgentStep::Model, NewAgentStep::Prompts, NewAgentStep::Tools]
74 }
75
76 pub fn title(self) -> &'static str {
77 match self {
78 NewAgentStep::Identity => "Identity",
79 NewAgentStep::Model => "Model",
80 NewAgentStep::Prompts => "Prompts",
81 NewAgentStep::Tools => "Tools",
82 }
83 }
84
85 pub fn heading(self) -> &'static str {
86 match self {
87 NewAgentStep::Identity => "Name your agent",
88 NewAgentStep::Model => "Select one or more models",
89 NewAgentStep::Prompts => "Select System Prompt Files",
90 NewAgentStep::Tools => "Select Tools",
91 }
92 }
93
94 pub fn next(self) -> Option<NewAgentStep> {
95 match self {
96 NewAgentStep::Identity => Some(NewAgentStep::Model),
97 NewAgentStep::Model => Some(NewAgentStep::Prompts),
98 NewAgentStep::Prompts => Some(NewAgentStep::Tools),
99 NewAgentStep::Tools => None,
100 }
101 }
102
103 pub fn prev(self) -> Option<NewAgentStep> {
104 match self {
105 NewAgentStep::Identity => None,
106 NewAgentStep::Model => Some(NewAgentStep::Identity),
107 NewAgentStep::Prompts => Some(NewAgentStep::Model),
108 NewAgentStep::Tools => Some(NewAgentStep::Prompts),
109 }
110 }
111}
112
113pub fn should_run_onboarding(dir: &Path) -> bool {
114 !dir.join(".aether/settings.json").exists()
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum McpConfigFile {
119 McpJson,
120 DotMcpJson,
121}
122
123impl McpConfigFile {
124 pub fn all() -> &'static [McpConfigFile] {
125 &[McpConfigFile::McpJson, McpConfigFile::DotMcpJson]
126 }
127
128 pub fn filename(self) -> &'static str {
129 match self {
130 McpConfigFile::McpJson => "mcp.json",
131 McpConfigFile::DotMcpJson => ".mcp.json",
132 }
133 }
134
135 pub fn description(self) -> &'static str {
136 match self {
137 McpConfigFile::McpJson => "MCP server configuration",
138 McpConfigFile::DotMcpJson => "MCP server configuration (dotfile)",
139 }
140 }
141}
142
143pub fn detect_mcp_configs(project_root: &Path) -> Vec<McpConfigFile> {
144 McpConfigFile::all().iter().copied().filter(|c| project_root.join(c.filename()).is_file()).collect()
145}
146
147pub fn server_options() -> Vec<tui::SelectOption> {
148 vec![
149 tui::SelectOption {
150 value: "coding".to_string(),
151 title: "Coding".to_string(),
152 description: Some("Filesystem, search, and bash tools".to_string()),
153 },
154 tui::SelectOption {
155 value: "lsp".to_string(),
156 title: "Lsp".to_string(),
157 description: Some("Language Server Protocol integration".to_string()),
158 },
159 tui::SelectOption {
160 value: "skills".to_string(),
161 title: "Skills".to_string(),
162 description: Some("Skills and slash-commands".to_string()),
163 },
164 tui::SelectOption {
165 value: "subagents".to_string(),
166 title: "Subagents".to_string(),
167 description: Some("Spawn sub-agents in parallel".to_string()),
168 },
169 tui::SelectOption {
170 value: "tasks".to_string(),
171 title: "Tasks".to_string(),
172 description: Some("Task management tools, backed by JSONL files".to_string()),
173 },
174 tui::SelectOption {
175 value: "survey".to_string(),
176 title: "Survey".to_string(),
177 description: Some("Allow your agent to ask you structured questions".to_string()),
178 },
179 SelectOption {
180 value: "plan".to_string(),
181 title: "Plan".to_string(),
182 description: Some("Plan-mode prompt and plan review via elicitation".to_string()),
183 },
184 ]
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 #[test]
191 fn should_run_onboarding_when_settings_missing() {
192 let dir = tempfile::tempdir().unwrap();
193 assert!(should_run_onboarding(dir.path()));
194 }
195
196 #[test]
197 fn does_not_run_onboarding_when_settings_exists() {
198 let dir = tempfile::tempdir().unwrap();
199 std::fs::create_dir_all(dir.path().join(".aether")).unwrap();
200 std::fs::write(dir.path().join(".aether/settings.json"), "{}").unwrap();
201 assert!(!should_run_onboarding(dir.path()));
202 }
203
204 #[test]
205 fn wizard_step_ordering() {
206 assert_eq!(NewAgentStep::Identity.next(), Some(NewAgentStep::Model));
207 assert_eq!(NewAgentStep::Model.next(), Some(NewAgentStep::Prompts));
208 assert_eq!(NewAgentStep::Prompts.next(), Some(NewAgentStep::Tools));
209 assert_eq!(NewAgentStep::Tools.next(), None);
210
211 assert_eq!(NewAgentStep::Identity.prev(), None);
212 assert_eq!(NewAgentStep::Model.prev(), Some(NewAgentStep::Identity));
213 assert_eq!(NewAgentStep::Tools.prev(), Some(NewAgentStep::Prompts));
214 }
215
216 #[test]
217 fn detect_prompt_files_returns_only_existing() {
218 let dir = tempfile::tempdir().unwrap();
219 std::fs::write(dir.path().join("AGENTS.md"), "a").unwrap();
220 std::fs::write(dir.path().join("GEMINI.md"), "g").unwrap();
221
222 let detected = detect_prompt_files(dir.path());
223 assert_eq!(detected, vec![PromptFile::Agents, PromptFile::Gemini]);
224 }
225
226 #[test]
227 fn available_prompt_files_scaffold_always_includes_agents_md() {
228 let dir = tempfile::tempdir().unwrap();
229 let prompt_files = available_prompt_files(&NewAgentMode::ScaffoldProject, dir.path());
230 assert_eq!(prompt_files, vec![PromptFile::Agents]);
231 }
232
233 #[test]
234 fn available_prompt_files_scaffold_preserves_order_when_all_present() {
235 let dir = tempfile::tempdir().unwrap();
236 std::fs::write(dir.path().join("AGENTS.md"), "a").unwrap();
237 std::fs::write(dir.path().join("CLAUDE.md"), "c").unwrap();
238 std::fs::write(dir.path().join("GEMINI.md"), "g").unwrap();
239
240 let prompt_files = available_prompt_files(&NewAgentMode::ScaffoldProject, dir.path());
241 assert_eq!(prompt_files, vec![PromptFile::Agents, PromptFile::Claude, PromptFile::Gemini]);
242 }
243
244 #[test]
245 fn available_prompt_files_scaffold_prepends_agents_md_when_only_others_exist() {
246 let dir = tempfile::tempdir().unwrap();
247 std::fs::write(dir.path().join("CLAUDE.md"), "c").unwrap();
248
249 let prompt_files = available_prompt_files(&NewAgentMode::ScaffoldProject, dir.path());
250 assert_eq!(prompt_files, vec![PromptFile::Agents, PromptFile::Claude]);
251 }
252
253 #[test]
254 fn detect_mcp_configs_returns_only_existing() {
255 let dir = tempfile::tempdir().unwrap();
256 std::fs::write(dir.path().join("mcp.json"), r#"{"servers":{}}"#).unwrap();
257
258 let detected = detect_mcp_configs(dir.path());
259 assert_eq!(detected, vec![McpConfigFile::McpJson]);
260 }
261
262 #[test]
263 fn detect_mcp_configs_finds_both() {
264 let dir = tempfile::tempdir().unwrap();
265 std::fs::write(dir.path().join("mcp.json"), r#"{"servers":{}}"#).unwrap();
266 std::fs::write(dir.path().join(".mcp.json"), r#"{"servers":{}}"#).unwrap();
267
268 let detected = detect_mcp_configs(dir.path());
269 assert_eq!(detected, vec![McpConfigFile::McpJson, McpConfigFile::DotMcpJson]);
270 }
271
272 #[test]
273 fn detect_mcp_configs_returns_empty_when_none() {
274 let dir = tempfile::tempdir().unwrap();
275 let detected = detect_mcp_configs(dir.path());
276 assert!(detected.is_empty());
277 }
278
279 #[test]
280 fn available_prompt_files_add_agent_is_detection_only() {
281 let dir = tempfile::tempdir().unwrap();
282 let prompt_files = available_prompt_files(&NewAgentMode::AddAgentToExistingProject, dir.path());
283 assert!(prompt_files.is_empty());
284 }
285}