1use serde::{Deserialize, Serialize};
2use std::path::{Path, PathBuf};
3
4#[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 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 let mut config = Self::default();
40
41 if global_config.exists() {
43 let content = std::fs::read_to_string(&global_config)?;
44 config = toml::from_str(&content)?;
45 }
46
47 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 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}