Skip to main content

aether_cli/agent/new_agent_wizard/
draft_agent_entry.rs

1use aether_project::{AetherSettings, AgentConfig, McpSourceSpec, PromptSource};
2use std::{
3    fs::{create_dir_all, read_to_string, write},
4    path::{Path, PathBuf},
5};
6
7use super::new_agent_step::{NewAgentMode, PromptFile};
8use crate::error::CliError;
9
10pub struct DraftAgentEntry {
11    pub entry: AgentConfig,
12    pub system_md_content: String,
13    pub system_md_edited: bool,
14    pub selected_mcp_servers: Vec<String>,
15    pub workspace_mcp_configs: Vec<String>,
16}
17
18impl DraftAgentEntry {
19    pub fn slug(&self) -> String {
20        self.entry.name.to_lowercase().replace(' ', "-")
21    }
22
23    pub fn generated_paths(&self, mode: &NewAgentMode) -> GeneratedPaths {
24        let filename = format!("{}.md", self.slug().to_uppercase());
25        match mode {
26            NewAgentMode::ScaffoldProject => GeneratedPaths {
27                system_md: PathBuf::from(format!(".aether/{filename}")),
28                mcp_json: PathBuf::from(".aether/mcp.json"),
29            },
30            NewAgentMode::AddAgentToExistingProject => {
31                let slug = self.slug();
32                GeneratedPaths {
33                    system_md: PathBuf::from(format!(".aether/agents/{slug}/{filename}")),
34                    mcp_json: PathBuf::from(format!(".aether/agents/{slug}/mcp.json")),
35                }
36            }
37        }
38    }
39
40    pub fn to_agent_config(&self, mode: &NewAgentMode, inherited_prompts: &[String]) -> AgentConfig {
41        let paths = self.generated_paths(mode);
42
43        let mut prompts = vec![PromptSource::file(paths.system_md.to_string_lossy())];
44        match mode {
45            NewAgentMode::ScaffoldProject => {
46                prompts.extend(self.entry.prompts.iter().cloned());
47            }
48            NewAgentMode::AddAgentToExistingProject => {
49                for prompt in &self.entry.prompts {
50                    if let Some(path) = prompt.path()
51                        && !inherited_prompts.iter().any(|d| d == path)
52                    {
53                        prompts.push(prompt.clone());
54                    }
55                }
56            }
57        }
58
59        let mut mcp = if self.selected_mcp_servers.is_empty() {
60            vec![]
61        } else {
62            vec![McpSourceSpec::file(paths.mcp_json.to_string_lossy())]
63        };
64
65        mcp.extend(self.workspace_mcp_configs.iter().map(McpSourceSpec::file));
66
67        AgentConfig { prompts, mcps: mcp, ..self.entry.clone() }
68    }
69
70    pub fn to_settings(&self, mode: &NewAgentMode, existing: Option<&str>) -> AetherSettings {
71        match mode {
72            NewAgentMode::ScaffoldProject => {
73                let entry = self.to_agent_config(mode, &[]);
74                AetherSettings { agent: Some(entry.name.clone()), agents: vec![entry], ..AetherSettings::default() }
75            }
76            NewAgentMode::AddAgentToExistingProject => {
77                let inherited = inherited_prompts_from_existing(existing);
78                let entry = self.to_agent_config(mode, &inherited);
79
80                let mut config: AetherSettings =
81                    existing.and_then(|s| serde_json::from_str(s).ok()).unwrap_or_default();
82                config.agents.push(entry);
83                config
84            }
85        }
86    }
87
88    pub fn to_mcp_json(&self) -> String {
89        use mcp_utils::client::config::{RawMcpConfig, RawMcpServerConfig};
90        use std::collections::BTreeMap;
91
92        let servers = self
93            .selected_mcp_servers
94            .iter()
95            .map(|entry| {
96                let name = entry.as_str();
97                let args = match name {
98                    "coding" => vec!["--rules-dir".into(), ".aether/skills".into()],
99                    "skills" => {
100                        vec!["--dir".into(), ".aether/skills".into(), "--notes-dir".into(), ".aether/notes".into()]
101                    }
102                    _ => vec![],
103                };
104                (name.to_string(), RawMcpServerConfig::InMemory { args, input: None })
105            })
106            .collect::<BTreeMap<_, _>>();
107
108        let config = RawMcpConfig { servers };
109        serde_json::to_string_pretty(&config).expect("mcp serialization cannot fail")
110    }
111}
112
113pub struct GeneratedPaths {
114    pub system_md: PathBuf,
115    pub mcp_json: PathBuf,
116}
117
118fn inherited_prompts_from_existing(existing: Option<&str>) -> Vec<String> {
119    existing
120        .and_then(|s| serde_json::from_str::<AetherSettings>(s).ok())
121        .map(|s| {
122            let prompts = if s.prompts.is_empty() {
123                s.agents.first().map(|agent| agent.prompts.clone()).unwrap_or_default()
124            } else {
125                s.prompts
126            };
127
128            prompts
129                .iter()
130                .filter_map(|p| p.path().map(str::to_string))
131                .filter(|p| PromptFile::all().iter().any(|d| d.filename() == p))
132                .collect()
133        })
134        .unwrap_or_default()
135}
136
137pub fn build_system_md(draft: &DraftAgentEntry) -> String {
138    format!(
139        "# {name}
140
141{description}
142
143## System Env
144
145Working directory: !`pwd`\\
146Platform: !`uname -s`\\
147Today's date: !`date +%Y-%m-%d`\\
148Git branch: !`git rev-parse --abbrev-ref HEAD`
149",
150        name = draft.entry.name,
151        description = draft.entry.description,
152    )
153}
154
155pub fn build_agents_md(draft: &DraftAgentEntry) -> String {
156    format!("# {}\n\n{}\n\nYou are an expert coding assistant.\n", draft.entry.name, draft.entry.description)
157}
158
159pub fn scaffold(project_root: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
160    create_dir_all(project_root).map_err(CliError::IoError)?;
161
162    let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
163    write_if_absent(&project_root.join(&paths.system_md), &draft.system_md_content)?;
164    write_if_absent(&project_root.join(".aether/mcp.json"), &draft.to_mcp_json())?;
165    if draft.entry.prompts.iter().any(|n| n.path() == Some(PromptFile::Agents.filename())) {
166        write_if_absent(&project_root.join("AGENTS.md"), &build_agents_md(draft))?;
167    }
168    let config = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
169    let json = serde_json::to_string_pretty(&config).expect("settings serialization cannot fail");
170    write_if_absent(&project_root.join(".aether/settings.json"), &json)?;
171
172    Ok(())
173}
174
175pub fn add_agent(settings_path: &Path, draft: &DraftAgentEntry) -> Result<(), CliError> {
176    let content = read_to_string(settings_path).map_err(CliError::IoError)?;
177    let slug_dir = settings_path.parent().unwrap().join("agents").join(draft.slug());
178    create_dir_all(&slug_dir).map_err(CliError::IoError)?;
179
180    let filename = format!("{}.md", draft.slug().to_uppercase());
181    write(slug_dir.join(filename), &draft.system_md_content).map_err(CliError::IoError)?;
182
183    if !draft.selected_mcp_servers.is_empty() {
184        write(slug_dir.join("mcp.json"), draft.to_mcp_json()).map_err(CliError::IoError)?;
185    }
186
187    let config = draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&content));
188    let json = serde_json::to_string_pretty(&config).expect("settings serialization cannot fail");
189    write(settings_path, json).map_err(CliError::IoError)?;
190
191    Ok(())
192}
193
194fn write_if_absent(path: &Path, content: &str) -> Result<(), CliError> {
195    if path.exists() {
196        return Ok(());
197    }
198    if let Some(parent) = path.parent() {
199        std::fs::create_dir_all(parent).map_err(CliError::IoError)?;
200    }
201    std::fs::write(path, content).map_err(CliError::IoError)?;
202    Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use aether_project::{AetherSettingsSource, AgentCatalog, SettingsFileSource};
209    use llm::ReasoningEffort;
210    use mcp_utils::client::config::RawMcpConfig;
211
212    fn has_prompt(agent: &AgentConfig, path: &str) -> bool {
213        agent.prompts.iter().any(|prompt| prompt.path() == Some(path))
214    }
215
216    fn has_mcp(agent: &AgentConfig, path: &str) -> bool {
217        agent.mcps.iter().any(|mcp| mcp.path() == Some(path))
218    }
219
220    fn default_draft() -> DraftAgentEntry {
221        let mut draft = DraftAgentEntry {
222            entry: AgentConfig {
223                name: "Default".to_string(),
224                description: "Default coding agent".to_string(),
225                user_invocable: true,
226                agent_invocable: true,
227                model: "anthropic:claude-sonnet-4-5".to_string(),
228                prompts: vec![PromptSource::file("AGENTS.md")],
229                ..AgentConfig::default()
230            },
231            system_md_content: String::new(),
232            system_md_edited: false,
233            selected_mcp_servers: vec!["coding".into(), "skills".into(), "tasks".into()],
234            workspace_mcp_configs: vec![],
235        };
236        draft.system_md_content = build_system_md(&draft);
237        draft
238    }
239
240    fn researcher_draft() -> DraftAgentEntry {
241        let mut draft = default_draft();
242        draft.entry.name = "Researcher".to_string();
243        draft.entry.description = "Research agent".to_string();
244        draft.selected_mcp_servers = vec![];
245        draft.workspace_mcp_configs = vec![];
246        draft.system_md_content = build_system_md(&draft);
247        draft
248    }
249
250    #[test]
251    fn scaffold_writes_all_files() {
252        let dir = tempfile::tempdir().unwrap();
253        scaffold(dir.path(), &default_draft()).unwrap();
254
255        assert!(dir.path().join(".aether/settings.json").exists());
256        assert!(dir.path().join(".aether/mcp.json").exists());
257        assert!(dir.path().join(".aether/DEFAULT.md").exists());
258        assert!(dir.path().join("AGENTS.md").exists());
259    }
260
261    #[test]
262    fn scaffold_settings_json_is_valid() {
263        let dir = tempfile::tempdir().unwrap();
264        scaffold(dir.path(), &default_draft()).unwrap();
265
266        let config = load_project_settings(dir.path());
267        let catalog = AgentCatalog::from_settings(dir.path(), config).unwrap();
268        assert_eq!(catalog.all().len(), 1);
269        assert_eq!(catalog.all()[0].name, "Default");
270    }
271
272    #[test]
273    fn scaffold_mcp_json_is_valid() {
274        let dir = tempfile::tempdir().unwrap();
275        scaffold(dir.path(), &default_draft()).unwrap();
276
277        let raw = RawMcpConfig::from_json_file(dir.path().join(".aether/mcp.json")).unwrap();
278        assert_eq!(raw.servers.len(), 3);
279        assert!(raw.servers.contains_key("coding"));
280        assert!(raw.servers.contains_key("skills"));
281        assert!(raw.servers.contains_key("tasks"));
282    }
283
284    #[test]
285    fn scaffold_skips_existing_files() {
286        let dir = tempfile::tempdir().unwrap();
287        let agents_path = dir.path().join("AGENTS.md");
288        std::fs::write(&agents_path, "My custom prompt").unwrap();
289
290        scaffold(dir.path(), &default_draft()).unwrap();
291
292        let content = std::fs::read_to_string(&agents_path).unwrap();
293        assert_eq!(content, "My custom prompt");
294    }
295
296    #[test]
297    fn scaffold_creates_parent_dirs() {
298        let dir = tempfile::tempdir().unwrap();
299        let nested = dir.path().join("deep/nested/project");
300        scaffold(&nested, &default_draft()).unwrap();
301
302        assert!(nested.join(".aether/settings.json").exists());
303        assert!(nested.join(".aether/mcp.json").exists());
304        assert!(nested.join(".aether/DEFAULT.md").exists());
305        assert!(nested.join("AGENTS.md").exists());
306    }
307
308    #[test]
309    fn scaffold_is_idempotent() {
310        let dir = tempfile::tempdir().unwrap();
311        let draft = default_draft();
312        scaffold(dir.path(), &draft).unwrap();
313        scaffold(dir.path(), &draft).unwrap();
314        assert!(dir.path().join(".aether/settings.json").exists());
315    }
316
317    #[test]
318    fn generated_settings_reference_aether_paths() {
319        let dir = tempfile::tempdir().unwrap();
320        scaffold(dir.path(), &default_draft()).unwrap();
321
322        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
323        let config: AetherSettings = serde_json::from_str(&content).unwrap();
324        let agent = &config.agents[0];
325
326        assert_eq!(config.agents.len(), 1);
327        assert!(has_prompt(agent, ".aether/DEFAULT.md"));
328        assert!(has_prompt(agent, "AGENTS.md"));
329        assert!(has_mcp(agent, ".aether/mcp.json"));
330    }
331
332    #[test]
333    fn scaffold_without_agents_md() {
334        let dir = tempfile::tempdir().unwrap();
335        let mut draft = default_draft();
336        draft.entry.prompts = vec![];
337        scaffold(dir.path(), &draft).unwrap();
338
339        assert!(!dir.path().join("AGENTS.md").exists());
340
341        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
342        let config: AetherSettings = serde_json::from_str(&content).unwrap();
343        assert!(!has_prompt(&config.agents[0], "AGENTS.md"));
344    }
345
346    #[test]
347    fn scaffold_includes_reasoning_effort() {
348        let dir = tempfile::tempdir().unwrap();
349        let mut draft = default_draft();
350        draft.entry.reasoning_effort = Some(ReasoningEffort::High);
351        scaffold(dir.path(), &draft).unwrap();
352
353        let config = load_project_settings(dir.path());
354        let catalog = AgentCatalog::from_settings(dir.path(), config).unwrap();
355        assert_eq!(catalog.all()[0].reasoning_effort, Some(ReasoningEffort::High));
356    }
357
358    #[test]
359    fn scaffold_omits_reasoning_effort_when_none() {
360        let dir = tempfile::tempdir().unwrap();
361        scaffold(dir.path(), &default_draft()).unwrap();
362
363        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
364        assert!(!content.contains("reasoningEffort"));
365    }
366
367    #[test]
368    fn scaffold_custom_servers() {
369        let dir = tempfile::tempdir().unwrap();
370        let mut draft = default_draft();
371        draft.selected_mcp_servers = vec!["coding".into(), "lsp".into()];
372        scaffold(dir.path(), &draft).unwrap();
373
374        let raw = RawMcpConfig::from_json_file(dir.path().join(".aether/mcp.json")).unwrap();
375        assert_eq!(raw.servers.len(), 2);
376        assert!(raw.servers.contains_key("coding"));
377        assert!(raw.servers.contains_key("lsp"));
378        assert!(!raw.servers.contains_key("tasks"));
379    }
380
381    #[test]
382    fn scaffold_no_servers_no_mcp_json_ref() {
383        let dir = tempfile::tempdir().unwrap();
384        let mut draft = default_draft();
385        draft.selected_mcp_servers = vec![];
386        scaffold(dir.path(), &draft).unwrap();
387
388        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
389        let config: AetherSettings = serde_json::from_str(&content).unwrap();
390        assert!(config.agents[0].mcps.is_empty());
391    }
392
393    #[test]
394    fn add_agent_appends_to_existing_settings() {
395        let dir = tempfile::tempdir().unwrap();
396        scaffold(dir.path(), &default_draft()).unwrap();
397
398        let settings_path = dir.path().join(".aether/settings.json");
399        add_agent(&settings_path, &researcher_draft()).unwrap();
400
401        let config = load_project_settings(dir.path());
402        let catalog = AgentCatalog::from_settings(dir.path(), config).unwrap();
403        assert_eq!(catalog.all().len(), 2);
404        assert_eq!(catalog.all()[0].name, "Default");
405        assert_eq!(catalog.all()[1].name, "Researcher");
406    }
407
408    #[test]
409    fn add_agent_writes_per_agent_system_md() {
410        let dir = tempfile::tempdir().unwrap();
411        scaffold(dir.path(), &default_draft()).unwrap();
412
413        let settings_path = dir.path().join(".aether/settings.json");
414        let mut new_draft = researcher_draft();
415        new_draft.entry.prompts = vec![];
416        let expected_per_agent = new_draft.system_md_content.clone();
417        add_agent(&settings_path, &new_draft).unwrap();
418
419        let agent_md = dir.path().join(".aether/agents/researcher/RESEARCHER.md");
420        assert!(agent_md.exists());
421        assert_eq!(std::fs::read_to_string(agent_md).unwrap(), expected_per_agent);
422    }
423
424    #[test]
425    fn add_agent_writes_per_agent_mcp_json() {
426        let dir = tempfile::tempdir().unwrap();
427        scaffold(dir.path(), &default_draft()).unwrap();
428
429        let settings_path = dir.path().join(".aether/settings.json");
430        let mut new_draft = researcher_draft();
431        new_draft.entry.prompts = vec![];
432        new_draft.selected_mcp_servers = vec!["coding".into(), "lsp".into()];
433        add_agent(&settings_path, &new_draft).unwrap();
434
435        let agent_mcp = dir.path().join(".aether/agents/researcher/mcp.json");
436        assert!(agent_mcp.exists());
437
438        let raw = RawMcpConfig::from_json_file(&agent_mcp).unwrap();
439        assert_eq!(raw.servers.len(), 2);
440        assert!(raw.servers.contains_key("coding"));
441        assert!(raw.servers.contains_key("lsp"));
442    }
443
444    #[test]
445    fn add_agent_config_references_local_assets() {
446        let dir = tempfile::tempdir().unwrap();
447        scaffold(dir.path(), &default_draft()).unwrap();
448
449        let settings_path = dir.path().join(".aether/settings.json");
450        let mut new_draft = researcher_draft();
451        new_draft.entry.user_invocable = false;
452        new_draft.entry.prompts = vec![];
453        new_draft.selected_mcp_servers = vec!["coding".into()];
454        add_agent(&settings_path, &new_draft).unwrap();
455
456        let content = std::fs::read_to_string(&settings_path).unwrap();
457        let config: AetherSettings = serde_json::from_str(&content).unwrap();
458        let researcher = &config.agents[1];
459
460        assert_eq!(researcher.name, "Researcher");
461        assert!(!researcher.user_invocable);
462        assert!(researcher.agent_invocable);
463        assert!(has_prompt(researcher, ".aether/agents/researcher/RESEARCHER.md"));
464        assert!(has_mcp(researcher, ".aether/agents/researcher/mcp.json"));
465    }
466
467    #[test]
468    fn generated_paths_scaffold() {
469        let draft = default_draft();
470        let paths = draft.generated_paths(&NewAgentMode::ScaffoldProject);
471        assert_eq!(paths.system_md, PathBuf::from(".aether/DEFAULT.md"));
472        assert_eq!(paths.mcp_json, PathBuf::from(".aether/mcp.json"));
473    }
474
475    #[test]
476    fn generated_paths_add_agent() {
477        let draft = default_draft();
478        let paths = draft.generated_paths(&NewAgentMode::AddAgentToExistingProject);
479        assert_eq!(paths.system_md, PathBuf::from(".aether/agents/default/DEFAULT.md"));
480        assert_eq!(paths.mcp_json, PathBuf::from(".aether/agents/default/mcp.json"));
481    }
482
483    #[test]
484    fn slug_from_name() {
485        let mut draft = default_draft();
486        draft.entry.name = "Codebase Explorer".to_string();
487        assert_eq!(draft.slug(), "codebase-explorer");
488    }
489
490    #[test]
491    fn build_system_md_uses_name_description_and_bash_block() {
492        let mut draft = default_draft();
493        draft.entry.name = "Researcher".to_string();
494        draft.entry.description = "Digs through the codebase".to_string();
495        let body = build_system_md(&draft);
496        assert!(body.starts_with("# Researcher\n"));
497        assert!(body.contains("Digs through the codebase"));
498        assert!(body.contains("## System Env"));
499        assert!(body.contains("Working directory: !`pwd`\\"));
500        assert!(body.contains("Platform: !`uname -s`\\"));
501        assert!(body.contains("Today's date: !`date +%Y-%m-%d`\\"));
502        assert!(body.contains("Git branch: !`git rev-parse --abbrev-ref HEAD`"));
503    }
504
505    #[test]
506    fn build_settings_scaffold_emits_all_selected_prompts() {
507        let mut draft = default_draft();
508        draft.entry.prompts = vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md")];
509        let config = draft.to_settings(&NewAgentMode::ScaffoldProject, None);
510        let agent = &config.agents[0];
511
512        assert!(has_prompt(agent, ".aether/DEFAULT.md"));
513        assert!(has_prompt(agent, "AGENTS.md"));
514        assert!(has_prompt(agent, "CLAUDE.md"));
515    }
516
517    #[test]
518    fn build_settings_add_agent_skips_shared_prompts() {
519        let existing = serde_json::to_string_pretty(&AetherSettings {
520            agent: Some("Default".to_string()),
521            prompts: vec![PromptSource::file("AGENTS.md")],
522            agents: vec![AgentConfig {
523                prompts: vec![],
524                ..default_draft().to_agent_config(&NewAgentMode::ScaffoldProject, &[])
525            }],
526            ..AetherSettings::default()
527        })
528        .unwrap();
529
530        let mut new_draft = researcher_draft();
531        new_draft.entry.prompts = vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md")];
532        let config = new_draft.to_settings(&NewAgentMode::AddAgentToExistingProject, Some(&existing));
533
534        let researcher = &config.agents[1];
535        assert_eq!(researcher.name, "Researcher");
536        assert!(!has_prompt(researcher, "AGENTS.md"));
537        assert!(has_prompt(researcher, "CLAUDE.md"));
538    }
539
540    #[test]
541    fn scaffold_writes_agents_md_when_selected() {
542        let dir = tempfile::tempdir().unwrap();
543        let mut draft = default_draft();
544        draft.entry.prompts = vec![PromptSource::file("AGENTS.md")];
545        scaffold(dir.path(), &draft).unwrap();
546        assert!(dir.path().join("AGENTS.md").exists());
547    }
548
549    #[test]
550    fn scaffold_includes_workspace_mcp_configs() {
551        let dir = tempfile::tempdir().unwrap();
552        std::fs::write(dir.path().join("mcp.json"), r#"{"servers":{}}"#).unwrap();
553
554        let mut draft = default_draft();
555        draft.workspace_mcp_configs = vec!["mcp.json".to_string()];
556        scaffold(dir.path(), &draft).unwrap();
557
558        let content = std::fs::read_to_string(dir.path().join(".aether/settings.json")).unwrap();
559        let config: AetherSettings = serde_json::from_str(&content).unwrap();
560
561        assert!(has_mcp(&config.agents[0], "mcp.json"));
562    }
563
564    #[test]
565    fn add_agent_includes_workspace_mcp_configs() {
566        let dir = tempfile::tempdir().unwrap();
567        scaffold(dir.path(), &default_draft()).unwrap();
568
569        let settings_path = dir.path().join(".aether/settings.json");
570        let mut new_draft = researcher_draft();
571        new_draft.selected_mcp_servers = vec!["coding".into()];
572        new_draft.workspace_mcp_configs = vec![".mcp.json".to_string()];
573        add_agent(&settings_path, &new_draft).unwrap();
574
575        let content = std::fs::read_to_string(&settings_path).unwrap();
576        let config: AetherSettings = serde_json::from_str(&content).unwrap();
577        let researcher = &config.agents[1];
578
579        assert!(has_mcp(researcher, ".mcp.json"));
580    }
581
582    #[test]
583    fn scaffold_never_writes_claude_or_gemini_md() {
584        let dir = tempfile::tempdir().unwrap();
585        let mut draft = default_draft();
586        draft.entry.prompts =
587            vec![PromptSource::file("AGENTS.md"), PromptSource::file("CLAUDE.md"), PromptSource::file("GEMINI.md")];
588        scaffold(dir.path(), &draft).unwrap();
589
590        assert!(dir.path().join("AGENTS.md").exists());
591        assert!(!dir.path().join("CLAUDE.md").exists());
592        assert!(!dir.path().join("GEMINI.md").exists());
593    }
594
595    fn load_project_settings(dir: &Path) -> AetherSettings {
596        AetherSettings::load(dir, [AetherSettingsSource::File(SettingsFileSource::new(".aether/settings.json", dir))])
597            .unwrap()
598    }
599}