Skip to main content

aether_cli/
settings_args.rs

1use aether_project::{AetherSettings, AetherSettingsSource, AgentCatalog, SettingsError, SettingsFileSource};
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Clone, Debug, Default, clap::Args)]
6pub struct SettingsSourceArgs {
7    #[arg(long = "settings-json", conflicts_with = "settings_file")]
8    pub settings_json: Option<String>,
9
10    #[arg(long = "settings-file", conflicts_with = "settings_json")]
11    pub settings_file: Option<PathBuf>,
12}
13
14/// A JSON options object supplied both an inline `settings` object and a `settingsFile`.
15#[derive(Debug, Error)]
16#[error("settings and settingsFile cannot both be supplied")]
17pub struct ConflictingSettingsSources;
18
19impl SettingsSourceArgs {
20    pub fn from_json_options(
21        settings: Option<AetherSettings>,
22        settings_file: Option<PathBuf>,
23    ) -> Result<Self, ConflictingSettingsSources> {
24        if settings.is_some() && settings_file.is_some() {
25            return Err(ConflictingSettingsSources);
26        }
27        Ok(Self {
28            settings_json: settings.map(|settings| serde_json::to_string(&settings).expect("settings serialize")),
29            settings_file,
30        })
31    }
32
33    pub fn source(&self, root: &Path) -> Option<AetherSettingsSource> {
34        if let Some(json) = &self.settings_json {
35            Some(AetherSettingsSource::Json(json.clone()))
36        } else {
37            self.settings_file
38                .as_ref()
39                .map(|path| AetherSettingsSource::File(SettingsFileSource::new(path.clone(), root)))
40        }
41    }
42
43    pub fn load_settings(&self, cwd: &Path) -> Result<AetherSettings, SettingsError> {
44        match self.source(cwd) {
45            Some(source) => AetherSettings::load(cwd, [source]),
46            None => AetherSettings::load_default(cwd),
47        }
48    }
49
50    pub fn load_agent_catalog(&self, cwd: &Path) -> Result<AgentCatalog, SettingsError> {
51        let settings = self.load_settings(cwd)?;
52        AgentCatalog::from_settings_or_empty(cwd, settings)
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn settings_json_maps_to_json_source() {
62        let args = SettingsSourceArgs { settings_json: Some("{\"agents\":[]}".to_string()), settings_file: None };
63
64        let Some(AetherSettingsSource::Json(json)) = args.source(Path::new(".")) else {
65            panic!("expected JSON settings source");
66        };
67        assert_eq!(json, "{\"agents\":[]}");
68    }
69
70    #[test]
71    fn from_json_options_serializes_inline_settings() {
72        let args = SettingsSourceArgs::from_json_options(Some(AetherSettings::default()), None).unwrap();
73
74        assert!(args.settings_json.is_some());
75        assert!(args.settings_file.is_none());
76    }
77
78    #[test]
79    fn from_json_options_rejects_both_settings_and_file() {
80        let error = SettingsSourceArgs::from_json_options(
81            Some(AetherSettings::default()),
82            Some(PathBuf::from("settings.json")),
83        );
84
85        assert!(matches!(error, Err(ConflictingSettingsSources)));
86    }
87
88    #[test]
89    fn settings_file_maps_to_file_source() {
90        let args = SettingsSourceArgs { settings_json: None, settings_file: Some(PathBuf::from("settings.json")) };
91
92        let Some(AetherSettingsSource::File(source)) = args.source(Path::new("/workspace")) else {
93            panic!("expected file settings source");
94        };
95        assert_eq!(source, SettingsFileSource::new("settings.json", "/workspace"));
96    }
97}