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}