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)]
19    pub vector: VectorConfig,
20    #[serde(default)]
21    pub output: OutputConfig,
22    #[serde(default)]
23    pub install: InstallConfig,
24}
25
26pub const OFFICIAL_PUBLIC_KEY: [u8; 32] = [
27    25, 127, 107, 35, 225, 108, 133, 50, 198, 171, 200, 56, 250, 205, 94, 167, 137, 190, 12, 118,
28    178, 146, 3, 52, 3, 155, 250, 139, 61, 54, 141, 97,
29];
30
31impl Default for Config {
32    fn default() -> Self {
33        Self {
34            api_url: "https://api.cmdhub.io/v1".to_string(),
35            public_key: OFFICIAL_PUBLIC_KEY
36                .iter()
37                .map(|b| format!("{:02x}", b))
38                .collect(),
39            timeout_seconds: 30,
40            vector: VectorConfig::default(),
41            output: OutputConfig::default(),
42            install: InstallConfig::default(),
43        }
44    }
45}
46
47pub fn get_config_dir() -> PathBuf {
48    if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
49        if !xdg.is_empty() {
50            return PathBuf::from(xdg).join("cmdhub");
51        }
52    }
53    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
54    PathBuf::from(home).join(".config").join("cmdhub")
55}
56
57pub fn get_data_dir() -> PathBuf {
58    if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
59        if !xdg.is_empty() {
60            return PathBuf::from(xdg).join("cmdhub");
61        }
62    }
63    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
64    PathBuf::from(home)
65        .join(".local")
66        .join("share")
67        .join("cmdhub")
68}
69
70pub fn get_cache_dir() -> PathBuf {
71    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
72        if !xdg.is_empty() {
73            return PathBuf::from(xdg).join("cmdhub");
74        }
75    }
76    let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
77    PathBuf::from(home).join(".cache").join("cmdhub")
78}
79
80pub fn resolve_config_path(custom_path: Option<PathBuf>) -> PathBuf {
81    if let Some(path) = custom_path {
82        path
83    } else if let Ok(env_path) = std::env::var("CMDH_CONFIG") {
84        if !env_path.is_empty() {
85            PathBuf::from(env_path)
86        } else {
87            get_config_dir().join("config.toml")
88        }
89    } else {
90        get_config_dir().join("config.toml")
91    }
92}
93
94pub fn load_or_create_config(custom_path: Option<PathBuf>) -> Result<Config> {
95    let config_path = resolve_config_path(custom_path);
96    let default_xdg_path = get_config_dir().join("config.toml");
97
98    if !config_path.exists() {
99        if config_path != default_xdg_path {
100            anyhow::bail!(
101                "Custom configuration file does not exist at {:?}",
102                config_path
103            );
104        }
105        if let Some(parent) = config_path.parent() {
106            fs::create_dir_all(parent).context("Failed to create config directory")?;
107        }
108        let default_config = Config::default();
109        let toml_str = toml::to_string_pretty(&default_config)
110            .context("Failed to serialize default config")?;
111        fs::write(&config_path, toml_str).context("Failed to write default config file")?;
112        eprintln!(
113            "[INFO] Created default configuration file at: {}",
114            config_path.display()
115        );
116        Ok(default_config)
117    } else {
118        let toml_str = fs::read_to_string(&config_path).context("Failed to read config file")?;
119        let config: Config = toml::from_str(&toml_str).context("Failed to parse config TOML")?;
120        Ok(config)
121    }
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone)]
125pub struct OutputConfig {
126    #[serde(default = "default_output_mode")]
127    pub mode: String, // "full", "usage", "minimal"
128}
129
130impl Default for OutputConfig {
131    fn default() -> Self {
132        Self {
133            mode: default_output_mode(),
134        }
135    }
136}
137
138fn default_output_mode() -> String {
139    "full".to_string()
140}
141
142#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct InstallConfig {
144    pub os: Option<String>,
145    #[serde(default = "default_package_managers")]
146    pub package_managers: Vec<String>,
147}
148
149impl Default for InstallConfig {
150    fn default() -> Self {
151        Self {
152            os: None,
153            package_managers: default_package_managers(),
154        }
155    }
156}
157
158fn default_package_managers() -> Vec<String> {
159    vec![
160        "uv".to_string(),
161        "npm".to_string(),
162        "cargo".to_string(),
163        "go".to_string(),
164    ]
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_config_parsing_defaults() {
173        let toml_str = r#"
174            api_url = "https://api.cmdhub.xyz"
175            public_key = "01020304"
176            timeout_seconds = 30
177        "#;
178        let config: Config = toml::from_str(toml_str).unwrap();
179        assert_eq!(config.output.mode, "full");
180        assert_eq!(config.install.os, None);
181        assert_eq!(
182            config.install.package_managers,
183            vec![
184                "uv".to_string(),
185                "npm".to_string(),
186                "cargo".to_string(),
187                "go".to_string()
188            ]
189        );
190    }
191}