auths_cli/commands/
config.rs1use crate::commands::executable::ExecutableCommand;
4use crate::config::CliConfig;
5use anyhow::{Context, Result, bail};
6use auths_sdk::core_config::{AuthsConfig, PassphraseCachePolicy};
7use auths_sdk::ports::ConfigStore;
8
9use crate::adapters::config_store::FileConfigStore;
10use clap::{Parser, Subcommand};
11use std::path::{Path, PathBuf};
12
13#[derive(Parser, Debug, Clone)]
15#[command(
16 name = "config",
17 about = "View and modify Auths configuration",
18 after_help = "Configuration file: ~/.auths/config.toml
19
20Examples:
21 auths config show # View all settings
22 auths config get passphrase.cache # Check caching status
23 auths config set passphrase.cache always # Cache passphrases
24
25Valid Keys:
26 passphrase.cache — 'never', 'session', 'always'
27 passphrase.duration — seconds until cache expires
28 passphrase.biometric — 'enabled', 'disabled' (macOS)
29
30Related:
31 auths doctor — Check system configuration"
32)]
33pub struct ConfigCommand {
34 #[command(subcommand)]
35 pub action: ConfigAction,
36}
37
38#[derive(Subcommand, Debug, Clone)]
40pub enum ConfigAction {
41 Set {
43 key: String,
45 value: String,
47 },
48 Get {
50 key: String,
52 },
53 Show,
55}
56
57impl ExecutableCommand for ConfigCommand {
58 fn execute(&self, ctx: &CliConfig) -> Result<()> {
59 let path = config_path(ctx.repo_path.clone())?;
60 match &self.action {
61 ConfigAction::Set { key, value } => execute_set(key, value, &path),
62 ConfigAction::Get { key } => execute_get(key, &path),
63 ConfigAction::Show => execute_show(&path),
64 }
65 }
66}
67
68fn config_path(repo: Option<PathBuf>) -> Result<PathBuf> {
79 let dir = match repo {
80 Some(_) => auths_sdk::storage_layout::resolve_repo_path(repo)
81 .context("Failed to resolve the repository path for the config file")?,
82 None => auths_sdk::paths::auths_home()
83 .map_err(|e| anyhow::anyhow!("Failed to resolve the Auths home directory: {e}"))?,
84 };
85 Ok(dir.join("config.toml"))
86}
87
88fn read_config(path: &Path) -> AuthsConfig {
90 match FileConfigStore.read(path) {
91 Ok(Some(contents)) => toml::from_str(&contents).unwrap_or_default(),
92 _ => AuthsConfig::default(),
93 }
94}
95
96fn write_config(config: &AuthsConfig, path: &Path) -> Result<()> {
98 let contents = toml::to_string_pretty(config)
99 .map_err(|e| anyhow::anyhow!("Failed to serialize config: {e}"))?;
100 FileConfigStore
101 .write(path, &contents)
102 .map_err(|e| anyhow::anyhow!("{e}"))
103}
104
105fn execute_set(key: &str, value: &str, path: &Path) -> Result<()> {
106 let mut config = read_config(path);
107
108 match key {
109 "passphrase.cache" => {
110 config.passphrase.cache = parse_cache_policy(value)?;
111 }
112 "passphrase.duration" => {
113 auths_sdk::keychain::parse_duration_str(value).ok_or_else(|| {
114 anyhow::anyhow!(
115 "Invalid duration '{}'. Use formats like '7d', '24h', '30m', '3600s'.",
116 value
117 )
118 })?;
119 config.passphrase.duration = Some(value.to_string());
120 }
121 "passphrase.biometric" => {
122 config.passphrase.biometric = parse_bool(value)?;
123 }
124 _ => bail!(
125 "Unknown config key '{}'. Valid keys: passphrase.cache, passphrase.duration, passphrase.biometric",
126 key
127 ),
128 }
129
130 write_config(&config, path)?;
131 println!("Set {} = {}", key, value);
132 Ok(())
133}
134
135fn execute_get(key: &str, path: &Path) -> Result<()> {
136 let config = read_config(path);
137
138 match key {
139 "passphrase.cache" => {
140 let label = policy_label(&config.passphrase.cache);
141 println!("{}", label);
142 }
143 "passphrase.duration" => {
144 println!(
145 "{}",
146 config.passphrase.duration.as_deref().unwrap_or("(not set)")
147 );
148 }
149 "passphrase.biometric" => {
150 println!("{}", config.passphrase.biometric);
151 }
152 _ => bail!(
153 "Unknown config key '{}'. Valid keys: passphrase.cache, passphrase.duration, passphrase.biometric",
154 key
155 ),
156 }
157
158 Ok(())
159}
160
161fn execute_show(path: &Path) -> Result<()> {
162 let config = read_config(path);
163 if crate::ux::format::is_json_mode() {
164 let json = serde_json::to_string_pretty(&config)
165 .map_err(|e| anyhow::anyhow!("Failed to serialize config as JSON: {}", e))?;
166 println!("{}", json);
167 } else {
168 let toml_str = toml::to_string_pretty(&config)
169 .map_err(|e| anyhow::anyhow!("Failed to serialize config: {}", e))?;
170 println!("{}", toml_str);
171 }
172 Ok(())
173}
174
175fn parse_cache_policy(s: &str) -> Result<PassphraseCachePolicy> {
176 match s.to_lowercase().as_str() {
177 "always" => Ok(PassphraseCachePolicy::Always),
178 "session" => Ok(PassphraseCachePolicy::Session),
179 "duration" => Ok(PassphraseCachePolicy::Duration),
180 "never" => Ok(PassphraseCachePolicy::Never),
181 _ => bail!(
182 "Invalid cache policy '{}'. Valid values: always, session, duration, never",
183 s
184 ),
185 }
186}
187
188fn policy_label(policy: &PassphraseCachePolicy) -> &'static str {
189 match policy {
190 PassphraseCachePolicy::Always => "always",
191 PassphraseCachePolicy::Session => "session",
192 PassphraseCachePolicy::Duration => "duration",
193 PassphraseCachePolicy::Never => "never",
194 }
195}
196
197fn parse_bool(s: &str) -> Result<bool> {
198 match s.to_lowercase().as_str() {
199 "true" | "1" | "yes" => Ok(true),
200 "false" | "0" | "no" => Ok(false),
201 _ => bail!("Invalid boolean '{}'. Use true/false, yes/no, or 1/0", s),
202 }
203}