Skip to main content

hh_cli/config/
loader.rs

1use crate::agent::{AgentLoader, AgentRegistry};
2use crate::config::settings::Settings;
3use anyhow::Context;
4use std::{env, fs, path::PathBuf};
5
6pub fn global_config_path() -> PathBuf {
7    let base = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
8    base.join("hh/config.json")
9}
10
11pub fn project_config_path(cwd: &std::path::Path) -> PathBuf {
12    cwd.join(".hh/config.json")
13}
14
15pub fn load_settings(
16    cwd: &std::path::Path,
17    agent_name: Option<String>,
18) -> anyhow::Result<Settings> {
19    let mut settings = Settings::default();
20
21    merge_settings_file(&mut settings, &global_config_path())?;
22    merge_settings_file(&mut settings, &project_config_path(cwd))?;
23
24    settings.normalize_models();
25    override_from_env(&mut settings.models.default, "HH_MODEL");
26    settings.normalize_models();
27    override_selected_provider_field(&mut settings, "HH_BASE_URL", |provider, value| {
28        provider.base_url = value;
29    });
30    override_selected_provider_field(&mut settings, "HH_API_KEY_ENV", |provider, value| {
31        provider.api_key_env = value;
32    });
33    override_optional_from_env(&mut settings.agent.system_prompt, "HH_SYSTEM_PROMPT");
34
35    // Apply agent settings if specified
36    if let Some(name) = agent_name {
37        settings.selected_agent = Some(name.clone());
38        let loader = AgentLoader::new()?;
39        let agents = loader.load_agents()?;
40        let registry = AgentRegistry::new(agents);
41
42        if let Some(agent) = registry.get_agent(&name) {
43            settings.apply_agent_settings(agent);
44        }
45    }
46
47    Ok(settings)
48}
49
50fn merge_settings(base: &mut Settings, override_with: Settings) {
51    base.models = override_with.models;
52    base.providers = override_with.providers;
53    base.agent = override_with.agent;
54    base.tools = override_with.tools;
55    base.permission = override_with.permission;
56    base.session.root = expand_path(&override_with.session.root);
57}
58
59fn merge_settings_file(settings: &mut Settings, path: &std::path::Path) -> anyhow::Result<()> {
60    if !path.exists() {
61        return Ok(());
62    }
63
64    let content =
65        fs::read_to_string(path).with_context(|| format!("failed reading {}", path.display()))?;
66    let value: Settings = serde_json::from_str(&content)
67        .with_context(|| format!("failed parsing {}", path.display()))?;
68    merge_settings(settings, value);
69
70    Ok(())
71}
72
73fn override_from_env(target: &mut String, key: &str) {
74    if let Ok(value) = env::var(key) {
75        *target = value;
76    }
77}
78
79fn override_optional_from_env(target: &mut Option<String>, key: &str) {
80    if let Ok(value) = env::var(key) {
81        *target = Some(value);
82    }
83}
84
85fn override_selected_provider_field(
86    settings: &mut Settings,
87    key: &str,
88    mut apply: impl FnMut(&mut crate::config::settings::ProviderConfig, String),
89) {
90    let Ok(value) = env::var(key) else {
91        return;
92    };
93
94    let Some((provider_id, _)) = settings.models.default.split_once('/') else {
95        return;
96    };
97
98    if let Some(provider) = settings.providers.get_mut(provider_id) {
99        apply(provider, value);
100    }
101}
102
103fn expand_path(path: &std::path::Path) -> PathBuf {
104    let path_str = path.to_string_lossy();
105
106    if let Some(home) = dirs::home_dir() {
107        if path_str == "~" {
108            return home;
109        }
110        if path_str.starts_with('~') {
111            return home.join(path_str[2..].trim_start_matches('/'));
112        }
113        if let Some(rest) = path_str.strip_prefix("$HOME") {
114            return home.join(rest.trim_start_matches('/'));
115        }
116        if let Some(rest) = path_str.strip_prefix("${HOME}") {
117            return home.join(rest.trim_start_matches('/'));
118        }
119    }
120
121    path.to_path_buf()
122}
123
124pub fn write_default_project_config(cwd: &std::path::Path) -> anyhow::Result<PathBuf> {
125    let config_dir = cwd.join(".hh");
126    std::fs::create_dir_all(&config_dir)?;
127    let config_path = config_dir.join("config.json");
128    let mut default = Settings::default();
129    default.normalize_models();
130    let text = serde_json::to_string_pretty(&default)?;
131    std::fs::write(&config_path, text)?;
132    Ok(config_path)
133}