Skip to main content

aether_cli/settings/
mod.rs

1use crate::init::{HarnessIntegration, InitRequest, InitTargetRequest, OverwriteMode, Preset};
2use clap::{Args, Subcommand};
3use llm::catalog::Provider;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Subcommand)]
7pub enum SettingsCommand {
8    /// Initialize Aether settings
9    Init(SettingsInitArgs),
10}
11
12#[derive(Debug, Clone, Args)]
13#[command(group(
14    clap::ArgGroup::new("scope")
15        .required(true)
16        .args(["user", "project"])
17))]
18pub struct SettingsInitArgs {
19    /// Initialize user-level settings in `$AETHER_HOME` or `~/.aether`.
20    #[arg(long, conflicts_with = "project")]
21    pub user: bool,
22
23    /// Initialize project-level settings in `<path>/.aether`.
24    #[arg(long, conflicts_with = "user")]
25    pub project: bool,
26
27    /// Project directory to initialize when --project is used.
28    #[arg(long, default_value = ".", requires = "project", conflicts_with = "user")]
29    pub path: PathBuf,
30
31    /// Provider to use (e.g. `anthropic`, `codex`, `bedrock`). Skips the prompt when set.
32    #[arg(long)]
33    pub provider: Option<Provider>,
34
35    /// Preset to write (`minimal` or `batteries-included`). Skips the prompt when set.
36    #[arg(long)]
37    pub preset: Option<Preset>,
38
39    /// Harness conventions to auto-load (only with `batteries-included` preset).
40    #[arg(long = "harness", value_enum)]
41    pub harnesses: Vec<HarnessIntegration>,
42
43    /// Overwrite an existing settings file at the selected target.
44    #[arg(long)]
45    pub force: bool,
46}
47
48impl From<SettingsInitArgs> for InitRequest {
49    fn from(args: SettingsInitArgs) -> Self {
50        let target = if args.user { InitTargetRequest::User } else { InitTargetRequest::Project { path: args.path } };
51        Self {
52            target,
53            provider: args.provider,
54            preset: args.preset,
55            harnesses: args.harnesses,
56            overwrite: if args.force { OverwriteMode::OverwriteExisting } else { OverwriteMode::PreserveExisting },
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use clap::Parser;
65
66    #[derive(Debug, Parser)]
67    struct TestCli {
68        #[command(subcommand)]
69        command: SettingsCommand,
70    }
71
72    fn parse(args: &[&str]) -> Result<TestCli, clap::Error> {
73        TestCli::try_parse_from(std::iter::once("test").chain(args.iter().copied()))
74    }
75
76    #[test]
77    fn accepts_user_scope() {
78        let cli = parse(&["init", "--user"]).unwrap();
79        let SettingsCommand::Init(args) = cli.command;
80        assert!(args.user);
81        assert!(!args.project);
82    }
83
84    #[test]
85    fn accepts_project_scope_and_path() {
86        let cli = parse(&["init", "--project", "--path", "/tmp/repo"]).unwrap();
87        let SettingsCommand::Init(args) = cli.command;
88        assert!(args.project);
89        assert_eq!(args.path, PathBuf::from("/tmp/repo"));
90    }
91
92    #[test]
93    fn accepts_provider_preset_and_force() {
94        let cli = parse(&["init", "--user", "--provider", "codex", "--preset", "minimal", "--force"]).unwrap();
95        let SettingsCommand::Init(args) = cli.command;
96        assert_eq!(args.provider, Some(Provider::Codex));
97        assert_eq!(args.preset, Some(Preset::Minimal));
98        assert!(args.force);
99    }
100
101    #[test]
102    fn rejects_missing_scope() {
103        assert!(parse(&["init"]).is_err());
104    }
105
106    #[test]
107    fn rejects_both_scopes() {
108        assert!(parse(&["init", "--user", "--project"]).is_err());
109    }
110
111    #[test]
112    fn rejects_user_path() {
113        assert!(parse(&["init", "--user", "--path", "/tmp/repo"]).is_err());
114    }
115
116    #[test]
117    fn accepts_repeated_harness_flags() {
118        let cli =
119            parse(&["init", "--user", "--preset", "batteries-included", "--harness", "claude", "--harness", "agents"])
120                .unwrap();
121        let SettingsCommand::Init(args) = cli.command;
122        assert_eq!(args.harnesses, vec![HarnessIntegration::Claude, HarnessIntegration::Agents]);
123    }
124}