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