Skip to main content

cmdhub_cli/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Serialize, Deserialize, Clone, Default)]
7pub struct VectorConfig {
8    pub model_url: Option<String>,
9    pub model_path: Option<String>,
10    pub model_sha256: Option<String>,
11}
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Config {
15    pub api_url: String,
16    pub public_key: String,
17    pub timeout_seconds: u64,
18    #[serde(default = "default_risk_guard_level")]
19    pub risk_guard_level: String,
20    #[serde(default)]
21    pub vector: VectorConfig,
22    #[serde(default)]
23    pub output: OutputConfig,
24    #[serde(default)]
25    pub install: InstallConfig,
26}
27
28// Ed25519 public key for verifying the official offline database. The matching private
29// key lives outside the repo (~/.config/cmdhub/keys/ed25519_private.bin) and signs each
30// release's SHA-256(db.zst). Generated 2026-06-11; rotating it requires a client release.
31pub const OFFICIAL_PUBLIC_KEY: [u8; 32] = [
32    97, 228, 162, 92, 153, 11, 201, 252, 71, 48, 104, 125, 199, 128, 20, 60, 250, 189, 150, 94,
33    170, 212, 223, 133, 120, 182, 137, 88, 220, 130, 171, 194,
34];
35
36impl Default for Config {
37    fn default() -> Self {
38        Self {
39            api_url: "https://cdn.cmdhub.org".to_string(),
40            public_key: OFFICIAL_PUBLIC_KEY
41                .iter()
42                .map(|b| format!("{:02x}", b))
43                .collect(),
44            timeout_seconds: 30,
45            risk_guard_level: default_risk_guard_level(),
46            vector: VectorConfig::default(),
47            output: OutputConfig::default(),
48            install: InstallConfig::default(),
49        }
50    }
51}
52
53pub fn get_config_dir() -> PathBuf {
54    if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
55        if !xdg.is_empty() {
56            return PathBuf::from(xdg).join("cmdhub");
57        }
58    }
59    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
60    PathBuf::from(home).join(".config").join("cmdhub")
61}
62
63pub fn get_data_dir() -> PathBuf {
64    if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
65        if !xdg.is_empty() {
66            return PathBuf::from(xdg).join("cmdhub");
67        }
68    }
69    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
70    PathBuf::from(home)
71        .join(".local")
72        .join("share")
73        .join("cmdhub")
74}
75
76pub fn get_cache_dir() -> PathBuf {
77    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
78        if !xdg.is_empty() {
79            return PathBuf::from(xdg).join("cmdhub");
80        }
81    }
82    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
83    PathBuf::from(home).join(".cache").join("cmdhub")
84}
85
86pub fn resolve_config_path(custom_path: Option<PathBuf>) -> PathBuf {
87    if let Some(path) = custom_path {
88        path
89    } else if let Ok(env_path) = std::env::var("CMDH_CONFIG") {
90        if !env_path.is_empty() {
91            PathBuf::from(env_path)
92        } else {
93            get_config_dir().join("config.toml")
94        }
95    } else {
96        get_config_dir().join("config.toml")
97    }
98}
99
100pub fn load_or_create_config(custom_path: Option<PathBuf>) -> Result<Config> {
101    let config_path = resolve_config_path(custom_path);
102    let default_xdg_path = get_config_dir().join("config.toml");
103
104    if !config_path.exists() {
105        if config_path != default_xdg_path {
106            anyhow::bail!(
107                "Custom configuration file does not exist at {:?}",
108                config_path
109            );
110        }
111        if let Some(parent) = config_path.parent() {
112            fs::create_dir_all(parent).context("Failed to create config directory")?;
113        }
114        let default_config = Config::default();
115        let toml_str = toml::to_string_pretty(&default_config)
116            .context("Failed to serialize default config")?;
117        fs::write(&config_path, toml_str).context("Failed to write default config file")?;
118        eprintln!(
119            "[INFO] Created default configuration file at: {}",
120            config_path.display()
121        );
122        Ok(default_config)
123    } else {
124        let toml_str = fs::read_to_string(&config_path).context("Failed to read config file")?;
125        let mut config: Config =
126            toml::from_str(&toml_str).context("Failed to parse config TOML")?;
127
128        // Cloud-native overrides via Environment Variables
129        if let Ok(api_url) = std::env::var("CMDH_API_URL") {
130            if !api_url.is_empty() {
131                config.api_url = api_url;
132            }
133        }
134        if let Ok(model_url) = std::env::var("CMDH_MODEL_URL") {
135            if !model_url.is_empty() {
136                config.vector.model_url = Some(model_url);
137            }
138        }
139
140        Ok(config)
141    }
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct OutputConfig {
146    #[serde(default = "default_output_mode")]
147    pub mode: String, // "full", "usage", "minimal"
148}
149
150impl Default for OutputConfig {
151    fn default() -> Self {
152        Self {
153            mode: default_output_mode(),
154        }
155    }
156}
157
158fn default_output_mode() -> String {
159    "full".to_string()
160}
161
162fn default_risk_guard_level() -> String {
163    "ask".to_string()
164}
165
166#[derive(Debug, Serialize, Deserialize, Clone)]
167pub struct InstallConfig {
168    pub os: Option<String>,
169    #[serde(default = "default_package_managers")]
170    pub package_managers: Vec<String>,
171}
172
173impl Default for InstallConfig {
174    fn default() -> Self {
175        Self {
176            os: None,
177            package_managers: default_package_managers(),
178        }
179    }
180}
181
182fn default_package_managers() -> Vec<String> {
183    vec![
184        "uv".to_string(),
185        "npm".to_string(),
186        "cargo".to_string(),
187        "go".to_string(),
188    ]
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_config_parsing_defaults() {
197        let toml_str = r#"
198            api_url = "https://cdn.cmdhub.org"
199            public_key = "01020304"
200            timeout_seconds = 30
201        "#;
202        let config: Config = toml::from_str(toml_str).unwrap();
203        assert_eq!(config.risk_guard_level, "ask");
204        assert_eq!(config.output.mode, "full");
205        assert_eq!(config.install.os, None);
206        assert_eq!(
207            config.install.package_managers,
208            vec![
209                "uv".to_string(),
210                "npm".to_string(),
211                "cargo".to_string(),
212                "go".to_string()
213            ]
214        );
215    }
216}