aether_cli/settings/
mod.rs1use crate::init::{HarnessIntegration, 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 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 { target, provider: args.provider, preset: args.preset, harnesses: args.harnesses, force: args.force }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use clap::Parser;
59
60 #[derive(Debug, Parser)]
61 struct TestCli {
62 #[command(subcommand)]
63 command: SettingsCommand,
64 }
65
66 fn parse(args: &[&str]) -> Result<TestCli, clap::Error> {
67 TestCli::try_parse_from(std::iter::once("test").chain(args.iter().copied()))
68 }
69
70 #[test]
71 fn accepts_user_scope() {
72 let cli = parse(&["init", "--user"]).unwrap();
73 let SettingsCommand::Init(args) = cli.command;
74 assert!(args.user);
75 assert!(!args.project);
76 }
77
78 #[test]
79 fn accepts_project_scope_and_path() {
80 let cli = parse(&["init", "--project", "--path", "/tmp/repo"]).unwrap();
81 let SettingsCommand::Init(args) = cli.command;
82 assert!(args.project);
83 assert_eq!(args.path, PathBuf::from("/tmp/repo"));
84 }
85
86 #[test]
87 fn accepts_provider_preset_and_force() {
88 let cli = parse(&["init", "--user", "--provider", "codex", "--preset", "minimal", "--force"]).unwrap();
89 let SettingsCommand::Init(args) = cli.command;
90 assert_eq!(args.provider, Some(Provider::Codex));
91 assert_eq!(args.preset, Some(Preset::Minimal));
92 assert!(args.force);
93 }
94
95 #[test]
96 fn rejects_missing_scope() {
97 assert!(parse(&["init"]).is_err());
98 }
99
100 #[test]
101 fn rejects_both_scopes() {
102 assert!(parse(&["init", "--user", "--project"]).is_err());
103 }
104
105 #[test]
106 fn rejects_user_path() {
107 assert!(parse(&["init", "--user", "--path", "/tmp/repo"]).is_err());
108 }
109
110 #[test]
111 fn accepts_repeated_harness_flags() {
112 let cli =
113 parse(&["init", "--user", "--preset", "batteries-included", "--harness", "claude", "--harness", "agents"])
114 .unwrap();
115 let SettingsCommand::Init(args) = cli.command;
116 assert_eq!(args.harnesses, vec![HarnessIntegration::Claude, HarnessIntegration::Agents]);
117 }
118}