use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct VectorConfig {
pub model_url: Option<String>,
pub model_path: Option<String>,
pub model_sha256: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub api_url: String,
pub public_key: String,
pub timeout_seconds: u64,
#[serde(default)]
pub vector: VectorConfig,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub install: InstallConfig,
}
pub const OFFICIAL_PUBLIC_KEY: [u8; 32] = [
25, 127, 107, 35, 225, 108, 133, 50, 198, 171, 200, 56, 250, 205, 94, 167, 137, 190, 12, 118,
178, 146, 3, 52, 3, 155, 250, 139, 61, 54, 141, 97,
];
impl Default for Config {
fn default() -> Self {
Self {
api_url: "https://api.cmdhub.io/v1".to_string(),
public_key: OFFICIAL_PUBLIC_KEY
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
timeout_seconds: 30,
vector: VectorConfig::default(),
output: OutputConfig::default(),
install: InstallConfig::default(),
}
}
}
pub fn get_config_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("cmdhub");
}
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
PathBuf::from(home).join(".config").join("cmdhub")
}
pub fn get_data_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("cmdhub");
}
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
PathBuf::from(home)
.join(".local")
.join("share")
.join("cmdhub")
}
pub fn get_cache_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("cmdhub");
}
}
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/fuyu".to_string());
PathBuf::from(home).join(".cache").join("cmdhub")
}
pub fn resolve_config_path(custom_path: Option<PathBuf>) -> PathBuf {
if let Some(path) = custom_path {
path
} else if let Ok(env_path) = std::env::var("CMDH_CONFIG") {
if !env_path.is_empty() {
PathBuf::from(env_path)
} else {
get_config_dir().join("config.toml")
}
} else {
get_config_dir().join("config.toml")
}
}
pub fn load_or_create_config(custom_path: Option<PathBuf>) -> Result<Config> {
let config_path = resolve_config_path(custom_path);
let default_xdg_path = get_config_dir().join("config.toml");
if !config_path.exists() {
if config_path != default_xdg_path {
anyhow::bail!(
"Custom configuration file does not exist at {:?}",
config_path
);
}
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context("Failed to create config directory")?;
}
let default_config = Config::default();
let toml_str = toml::to_string_pretty(&default_config)
.context("Failed to serialize default config")?;
fs::write(&config_path, toml_str).context("Failed to write default config file")?;
eprintln!(
"[INFO] Created default configuration file at: {}",
config_path.display()
);
Ok(default_config)
} else {
let toml_str = fs::read_to_string(&config_path).context("Failed to read config file")?;
let config: Config = toml::from_str(&toml_str).context("Failed to parse config TOML")?;
Ok(config)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OutputConfig {
#[serde(default = "default_output_mode")]
pub mode: String, }
impl Default for OutputConfig {
fn default() -> Self {
Self {
mode: default_output_mode(),
}
}
}
fn default_output_mode() -> String {
"full".to_string()
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InstallConfig {
pub os: Option<String>,
#[serde(default = "default_package_managers")]
pub package_managers: Vec<String>,
}
impl Default for InstallConfig {
fn default() -> Self {
Self {
os: None,
package_managers: default_package_managers(),
}
}
}
fn default_package_managers() -> Vec<String> {
vec![
"uv".to_string(),
"npm".to_string(),
"cargo".to_string(),
"go".to_string(),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_parsing_defaults() {
let toml_str = r#"
api_url = "https://api.cmdhub.xyz"
public_key = "01020304"
timeout_seconds = 30
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.output.mode, "full");
assert_eq!(config.install.os, None);
assert_eq!(
config.install.package_managers,
vec![
"uv".to_string(),
"npm".to_string(),
"cargo".to_string(),
"go".to_string()
]
);
}
}