Skip to main content

aether_cli/settings/
mod.rs

1use crate::init::{InitRequest, InitTargetRequest, 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    /// Overwrite an existing settings file at the selected target.
40    #[arg(long)]
41    pub force: bool,
42}
43
44impl From<SettingsInitArgs> for InitRequest {
45    fn from(args: SettingsInitArgs) -> Self {
46        let target = if args.user { InitTargetRequest::User } else { InitTargetRequest::Project { path: args.path } };
47        Self { target, provider: args.provider, preset: args.preset, force: args.force }
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use clap::Parser;
55
56    #[derive(Debug, Parser)]
57    struct TestCli {
58        #[command(subcommand)]
59        command: SettingsCommand,
60    }
61
62    fn parse(args: &[&str]) -> Result<TestCli, clap::Error> {
63        TestCli::try_parse_from(std::iter::once("test").chain(args.iter().copied()))
64    }
65
66    #[test]
67    fn accepts_user_scope() {
68        let cli = parse(&["init", "--user"]).unwrap();
69        let SettingsCommand::Init(args) = cli.command;
70        assert!(args.user);
71        assert!(!args.project);
72    }
73
74    #[test]
75    fn accepts_project_scope_and_path() {
76        let cli = parse(&["init", "--project", "--path", "/tmp/repo"]).unwrap();
77        let SettingsCommand::Init(args) = cli.command;
78        assert!(args.project);
79        assert_eq!(args.path, PathBuf::from("/tmp/repo"));
80    }
81
82    #[test]
83    fn accepts_provider_preset_and_force() {
84        let cli = parse(&["init", "--user", "--provider", "codex", "--preset", "minimal", "--force"]).unwrap();
85        let SettingsCommand::Init(args) = cli.command;
86        assert_eq!(args.provider, Some(Provider::Codex));
87        assert_eq!(args.preset, Some(Preset::Minimal));
88        assert!(args.force);
89    }
90
91    #[test]
92    fn rejects_missing_scope() {
93        assert!(parse(&["init"]).is_err());
94    }
95
96    #[test]
97    fn rejects_both_scopes() {
98        assert!(parse(&["init", "--user", "--project"]).is_err());
99    }
100
101    #[test]
102    fn rejects_user_path() {
103        assert!(parse(&["init", "--user", "--path", "/tmp/repo"]).is_err());
104    }
105}