aether_cli/settings/
mod.rs1use 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 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 #[arg(long, conflicts_with = "project")]
21 pub user: bool,
22
23 #[arg(long, conflicts_with = "user")]
25 pub project: bool,
26
27 #[arg(long, default_value = ".", requires = "project", conflicts_with = "user")]
29 pub path: PathBuf,
30
31 #[arg(long)]
33 pub provider: Option<Provider>,
34
35 #[arg(long)]
37 pub preset: Option<Preset>,
38
39 #[arg(long = "harness", value_enum)]
41 pub harnesses: Vec<HarnessIntegration>,
42
43 #[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}