Skip to main content

chub_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4const DEFAULT_CDN_URL: &str = "https://cdn.aichub.org/v1";
5const DEFAULT_TELEMETRY_URL: &str = "https://api.aichub.org/v1";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SourceConfig {
9    pub name: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub url: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub path: Option<String>,
14}
15
16#[derive(Debug, Clone)]
17pub struct Config {
18    pub sources: Vec<SourceConfig>,
19    pub output_dir: String,
20    pub refresh_interval: u64,
21    pub output_format: String,
22    pub source: String,
23    pub telemetry: bool,
24    pub feedback: bool,
25    pub telemetry_url: String,
26}
27
28impl Default for Config {
29    fn default() -> Self {
30        Self {
31            sources: vec![SourceConfig {
32                name: "default".to_string(),
33                url: Some(DEFAULT_CDN_URL.to_string()),
34                path: None,
35            }],
36            output_dir: ".context".to_string(),
37            refresh_interval: 21600,
38            output_format: "human".to_string(),
39            source: "official,maintainer,community".to_string(),
40            telemetry: true,
41            feedback: true,
42            telemetry_url: DEFAULT_TELEMETRY_URL.to_string(),
43        }
44    }
45}
46
47/// Raw YAML config file structure.
48#[derive(Debug, Deserialize, Default)]
49struct FileConfig {
50    #[serde(default)]
51    sources: Option<Vec<SourceConfig>>,
52    #[serde(default)]
53    cdn_url: Option<String>,
54    #[serde(default)]
55    output_dir: Option<String>,
56    #[serde(default)]
57    refresh_interval: Option<u64>,
58    #[serde(default)]
59    output_format: Option<String>,
60    #[serde(default)]
61    source: Option<String>,
62    #[serde(default)]
63    telemetry: Option<bool>,
64    #[serde(default)]
65    feedback: Option<bool>,
66    #[serde(default)]
67    telemetry_url: Option<String>,
68}
69
70/// Get the chub data directory (~/.chub or CHUB_DIR env var).
71pub fn chub_dir() -> PathBuf {
72    if let Ok(dir) = std::env::var("CHUB_DIR") {
73        PathBuf::from(dir)
74    } else {
75        dirs::home_dir()
76            .unwrap_or_else(|| PathBuf::from("."))
77            .join(".chub")
78    }
79}
80
81/// Load config with three-tier merge:
82/// Tier 1: ~/.chub/config.yaml (personal defaults)
83/// Tier 2: .chub/config.yaml (project, overrides personal)
84/// Tier 3: Active profile (additive — doesn't override config fields)
85pub fn load_config() -> Config {
86    let defaults = Config::default();
87    let config_path = chub_dir().join("config.yaml");
88
89    // Tier 1: personal config
90    let file_config: FileConfig = std::fs::read_to_string(&config_path)
91        .ok()
92        .and_then(|raw| serde_yaml::from_str(&raw).ok())
93        .unwrap_or_default();
94
95    // Tier 2: project config (override personal)
96    let project_config: FileConfig = find_project_chub_dir()
97        .map(|d| d.join("config.yaml"))
98        .and_then(|p| std::fs::read_to_string(&p).ok())
99        .and_then(|raw| serde_yaml::from_str(&raw).ok())
100        .unwrap_or_default();
101
102    // Merge: project overrides personal, personal overrides defaults
103    let sources = project_config
104        .sources
105        .or(file_config.sources)
106        .unwrap_or_else(|| {
107            let url = std::env::var("CHUB_BUNDLE_URL")
108                .ok()
109                .or(project_config.cdn_url)
110                .or(file_config.cdn_url)
111                .unwrap_or_else(|| DEFAULT_CDN_URL.to_string());
112            vec![SourceConfig {
113                name: "default".to_string(),
114                url: Some(url),
115                path: None,
116            }]
117        });
118
119    Config {
120        sources,
121        output_dir: project_config
122            .output_dir
123            .or(file_config.output_dir)
124            .unwrap_or(defaults.output_dir),
125        refresh_interval: project_config
126            .refresh_interval
127            .or(file_config.refresh_interval)
128            .unwrap_or(defaults.refresh_interval),
129        output_format: project_config
130            .output_format
131            .or(file_config.output_format)
132            .unwrap_or(defaults.output_format),
133        source: project_config
134            .source
135            .or(file_config.source)
136            .unwrap_or(defaults.source),
137        telemetry: project_config
138            .telemetry
139            .or(file_config.telemetry)
140            .unwrap_or(defaults.telemetry),
141        feedback: project_config
142            .feedback
143            .or(file_config.feedback)
144            .unwrap_or(defaults.feedback),
145        telemetry_url: project_config
146            .telemetry_url
147            .or(file_config.telemetry_url)
148            .unwrap_or(defaults.telemetry_url),
149    }
150}
151
152/// Search upward from CWD for a `.chub/` directory (project-level).
153fn find_project_chub_dir() -> Option<PathBuf> {
154    let start = std::env::current_dir().ok()?;
155    let mut current = start.as_path();
156    loop {
157        let candidate = current.join(".chub");
158        if candidate.is_dir() {
159            // Don't use ~/.chub as a project dir
160            let home_chub = chub_dir();
161            if candidate != home_chub {
162                return Some(candidate);
163            }
164        }
165        current = current.parent()?;
166    }
167}