ras-config 2.4.0

Env and logger bootstrap
Documentation
use std::env;
use std::path::PathBuf;
use std::time::Duration;

use ras_errors::AppError;
use thiserror::Error;

#[derive(Debug, Clone)]
pub struct Config {
    pub home_dir: PathBuf,
    pub config_dir: PathBuf,
    pub extensions_dir: PathBuf,
    pub profiles_dir: PathBuf,
    pub anthropic_api_key: Option<String>,
    pub claude_code_oauth_enabled: bool,
    pub cdp_timeout: Duration,
    pub action_timeout: Duration,
    pub anonymized_telemetry: bool,
    pub cloud_sync: bool,
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("missing required env var: {0}")]
    MissingEnv(String),
    #[error("invalid value for {0}: {1}")]
    InvalidValue(String, String),
}

impl From<ConfigError> for AppError {
    fn from(e: ConfigError) -> Self {
        Self::ValidationError(e.to_string())
    }
}

impl Config {
    pub fn from_env() -> Result<Self, ConfigError> {
        let home_dir = env::var("RAS_HOME")
            .ok()
            .map(PathBuf::from)
            .or_else(|| dirs_home())
            .ok_or_else(|| ConfigError::MissingEnv("RAS_HOME".into()))?;
        let config_dir = env::var("RAS_CONFIG_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|_| home_dir.join(".config").join("rust-ai-surfer"));
        let extensions_dir = env::var("RAS_EXTENSIONS_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|_| config_dir.join("extensions"));
        let profiles_dir = env::var("RAS_PROFILES_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|_| config_dir.join("profiles"));
        Ok(Self {
            home_dir,
            config_dir,
            extensions_dir,
            profiles_dir,
            anthropic_api_key: env::var("ANTHROPIC_API_KEY").ok(),
            claude_code_oauth_enabled: env::var("RAS_CLAUDE_CODE_OAUTH").ok().as_deref()
                != Some("0"),
            cdp_timeout: parse_duration("RAS_CDP_TIMEOUT_S", 60)?,
            action_timeout: parse_duration("RAS_ACTION_TIMEOUT_S", 180)?,
            anonymized_telemetry: env::var("RAS_ANONYMIZED_TELEMETRY").ok().as_deref() != Some("0"),
            cloud_sync: env::var("RAS_CLOUD_SYNC").ok().as_deref() == Some("1"),
        })
    }
}

fn parse_duration(key: &str, default_secs: u64) -> Result<Duration, ConfigError> {
    match env::var(key) {
        Ok(v) => v
            .parse::<u64>()
            .map(Duration::from_secs)
            .map_err(|_| ConfigError::InvalidValue(key.into(), v)),
        Err(_) => Ok(Duration::from_secs(default_secs)),
    }
}

fn dirs_home() -> Option<PathBuf> {
    env::var_os("HOME").map(PathBuf::from)
}