Skip to main content

cgx_engine/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3
4/// cgx configuration — loaded from `.cgx/config.toml` in the repo root
5/// or `~/.cgx/config.toml` for global defaults.
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct CgxConfig {
8    #[serde(default)]
9    pub project: ProjectConfig,
10    #[serde(default)]
11    pub analyze: AnalyzeConfig,
12    #[serde(default)]
13    pub index: IndexConfig,
14    #[serde(default)]
15    pub watch: WatchConfig,
16    #[serde(default)]
17    pub chat: ChatConfig,
18    #[serde(default)]
19    pub serve: ServeConfig,
20    #[serde(default)]
21    pub mcp: McpConfig,
22    #[serde(default)]
23    pub skill: SkillConfig,
24    #[serde(default)]
25    pub export: ExportConfig,
26}
27
28impl CgxConfig {
29    /// Load config from `.cgx/config.toml` in the given directory,
30    /// falling back to `~/.cgx/config.toml` for missing fields.
31    pub fn load(repo_path: &Path) -> anyhow::Result<Self> {
32        let repo_config = repo_path.join(".cgx").join("config.toml");
33        let global_config = dirs::home_dir()
34            .unwrap_or_else(|| PathBuf::from("."))
35            .join(".cgx")
36            .join("config.toml");
37
38        // Start with defaults
39        let mut config = Self::default();
40
41        // Load global defaults first (if no repo config exists, this is the primary config)
42        if global_config.exists() {
43            let content = std::fs::read_to_string(&global_config)?;
44            config = toml::from_str(&content)?;
45        }
46
47        // Repo-local config fully overrides global (no partial merge to avoid
48        // default-value clobbering)
49        if repo_config.exists() {
50            let content = std::fs::read_to_string(&repo_config)?;
51            config = toml::from_str(&content)?;
52        }
53
54        Ok(config)
55    }
56
57    /// Save config to `.cgx/config.toml` in the given directory.
58    pub fn save(&self, repo_path: &Path) -> anyhow::Result<()> {
59        let config_dir = repo_path.join(".cgx");
60        std::fs::create_dir_all(&config_dir)?;
61        let config_path = config_dir.join("config.toml");
62        let content = toml::to_string_pretty(self)?;
63        std::fs::write(&config_path, content)?;
64        Ok(())
65    }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ProjectConfig {
70    #[serde(default = "default_project_name")]
71    pub name: String,
72}
73
74impl Default for ProjectConfig {
75    fn default() -> Self {
76        Self {
77            name: default_project_name(),
78        }
79    }
80}
81
82fn default_project_name() -> String {
83    std::env::current_dir()
84        .ok()
85        .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
86        .unwrap_or_else(|| "cgx-project".to_string())
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct AnalyzeConfig {
91    #[serde(default = "default_languages")]
92    pub languages: Vec<String>,
93    #[serde(default = "default_exclude")]
94    pub exclude: Vec<String>,
95    #[serde(default = "default_churn_window")]
96    pub churn_window_days: u32,
97    #[serde(default = "default_co_change_threshold")]
98    pub co_change_threshold: u32,
99    #[serde(default = "default_max_file_size")]
100    pub max_file_size: u64,
101}
102
103impl Default for AnalyzeConfig {
104    fn default() -> Self {
105        Self {
106            languages: default_languages(),
107            exclude: default_exclude(),
108            churn_window_days: default_churn_window(),
109            co_change_threshold: default_co_change_threshold(),
110            max_file_size: default_max_file_size(),
111        }
112    }
113}
114
115fn default_languages() -> Vec<String> {
116    vec![
117        "typescript".to_string(),
118        "javascript".to_string(),
119        "python".to_string(),
120        "rust".to_string(),
121        "go".to_string(),
122        "java".to_string(),
123        "php".to_string(),
124    ]
125}
126
127fn default_exclude() -> Vec<String> {
128    vec![
129        "vendor/".to_string(),
130        "generated/".to_string(),
131        "*.pb.go".to_string(),
132        "third_party/".to_string(),
133        "build/".to_string(),
134        "out/".to_string(),
135    ]
136}
137
138fn default_churn_window() -> u32 {
139    90
140}
141
142fn default_co_change_threshold() -> u32 {
143    2
144}
145
146fn default_max_file_size() -> u64 {
147    2 * 1024 * 1024
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct IndexConfig {
152    #[serde(default = "default_true")]
153    pub incremental: bool,
154    #[serde(default = "default_true")]
155    pub store_hashes: bool,
156}
157
158impl Default for IndexConfig {
159    fn default() -> Self {
160        Self {
161            incremental: true,
162            store_hashes: true,
163        }
164    }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct WatchConfig {
169    #[serde(default = "default_watch_include")]
170    pub include: Vec<String>,
171    #[serde(default = "default_watch_ignore")]
172    pub ignore: Vec<String>,
173    #[serde(default = "default_debounce")]
174    pub debounce_ms: u64,
175}
176
177impl Default for WatchConfig {
178    fn default() -> Self {
179        Self {
180            include: default_watch_include(),
181            ignore: default_watch_ignore(),
182            debounce_ms: default_debounce(),
183        }
184    }
185}
186
187fn default_watch_include() -> Vec<String> {
188    vec![
189        "*.ts".to_string(),
190        "*.js".to_string(),
191        "*.py".to_string(),
192        "*.rs".to_string(),
193        "*.go".to_string(),
194        "*.java".to_string(),
195        "*.php".to_string(),
196    ]
197}
198
199fn default_watch_ignore() -> Vec<String> {
200    vec![
201        ".git".to_string(),
202        "node_modules".to_string(),
203        "target".to_string(),
204        "dist".to_string(),
205        "__pycache__".to_string(),
206        ".cgx".to_string(),
207    ]
208}
209
210fn default_debounce() -> u64 {
211    500
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ChatConfig {
216    #[serde(default = "default_chat_provider")]
217    pub provider: String,
218    #[serde(default = "default_chat_model")]
219    pub model: String,
220    #[serde(default = "default_ollama_host")]
221    pub ollama_host: String,
222    #[serde(default = "default_chat_timeout")]
223    pub timeout: u32,
224}
225
226impl Default for ChatConfig {
227    fn default() -> Self {
228        Self {
229            provider: default_chat_provider(),
230            model: default_chat_model(),
231            ollama_host: default_ollama_host(),
232            timeout: default_chat_timeout(),
233        }
234    }
235}
236
237fn default_chat_provider() -> String {
238    "ollama".to_string()
239}
240
241fn default_chat_model() -> String {
242    "codellama".to_string()
243}
244
245fn default_ollama_host() -> String {
246    "http://localhost:11434".to_string()
247}
248
249fn default_chat_timeout() -> u32 {
250    30
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct ServeConfig {
255    #[serde(default = "default_port")]
256    pub port: u16,
257    #[serde(default = "default_true")]
258    pub auto_open: bool,
259}
260
261impl Default for ServeConfig {
262    fn default() -> Self {
263        Self {
264            port: default_port(),
265            auto_open: true,
266        }
267    }
268}
269
270fn default_port() -> u16 {
271    7373
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct McpConfig {
276    #[serde(default = "default_true")]
277    pub enabled: bool,
278    #[serde(default = "default_mcp_timeout")]
279    pub timeout: u32,
280}
281
282impl Default for McpConfig {
283    fn default() -> Self {
284        Self {
285            enabled: true,
286            timeout: default_mcp_timeout(),
287        }
288    }
289}
290
291fn default_mcp_timeout() -> u32 {
292    30
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct SkillConfig {
297    #[serde(default = "default_true")]
298    pub auto_generate: bool,
299    #[serde(default = "default_true")]
300    pub include_token_budget: bool,
301    #[serde(default = "default_true")]
302    pub include_architecture: bool,
303}
304
305impl Default for SkillConfig {
306    fn default() -> Self {
307        Self {
308            auto_generate: true,
309            include_token_budget: true,
310            include_architecture: true,
311        }
312    }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ExportConfig {
317    #[serde(default = "default_export_format")]
318    pub default_format: String,
319    #[serde(default = "default_max_nodes")]
320    pub max_nodes: usize,
321}
322
323impl Default for ExportConfig {
324    fn default() -> Self {
325        Self {
326            default_format: default_export_format(),
327            max_nodes: default_max_nodes(),
328        }
329    }
330}
331
332fn default_export_format() -> String {
333    "json".to_string()
334}
335
336fn default_max_nodes() -> usize {
337    80
338}
339
340fn default_true() -> bool {
341    true
342}