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, }
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}