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