Skip to main content

auths_cli/commands/
config.rs

1//! `auths config` command — view and modify `~/.auths/config.toml`.
2
3use 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/// Manage Auths configuration.
14#[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/// Config subcommands.
39#[derive(Subcommand, Debug, Clone)]
40pub enum ConfigAction {
41    /// Set a configuration value (e.g. `auths config set passphrase.cache always`).
42    Set {
43        /// Dotted key path (e.g. `passphrase.cache`, `passphrase.duration`).
44        key: String,
45        /// Value to assign.
46        value: String,
47    },
48    /// Get a configuration value (e.g. `auths config get passphrase.cache`).
49    Get {
50        /// Dotted key path.
51        key: String,
52    },
53    /// Show the full configuration.
54    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
68/// Resolves the `config.toml` path for the active registry, honoring `--repo`.
69///
70/// Args:
71/// * `repo`: The optional `--repo` override; `None` selects the default
72///   `~/.auths` registry, matching the path used when `--repo` is absent.
73///
74/// Usage:
75/// ```ignore
76/// let path = config_path(ctx.repo_path.clone())?;
77/// ```
78fn 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
88/// Reads the config at `path`, returning defaults when the file is absent.
89fn 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
96/// Writes the config to `path`, creating parent directories as needed.
97fn 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}