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 #[serde(default)]
27 pub docs: DocsConfig,
28}
29
30impl CgxConfig {
31 pub fn load(repo_path: &Path) -> anyhow::Result<Self> {
34 let repo_config = repo_path.join(".cgx").join("config.toml");
35 let global_config = dirs::home_dir()
36 .unwrap_or_else(|| PathBuf::from("."))
37 .join(".cgx")
38 .join("config.toml");
39
40 let mut config = Self::default();
42
43 if global_config.exists() {
45 let content = std::fs::read_to_string(&global_config)?;
46 config = toml::from_str(&content)?;
47 }
48
49 if repo_config.exists() {
52 let content = std::fs::read_to_string(&repo_config)?;
53 config = toml::from_str(&content)?;
54 }
55
56 Ok(config)
57 }
58
59 pub fn save(&self, repo_path: &Path) -> anyhow::Result<()> {
61 let config_dir = repo_path.join(".cgx");
62 std::fs::create_dir_all(&config_dir)?;
63 let config_path = config_dir.join("config.toml");
64 let content = toml::to_string_pretty(self)?;
65 std::fs::write(&config_path, content)?;
66 Ok(())
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ProjectConfig {
72 #[serde(default = "default_project_name")]
73 pub name: String,
74}
75
76impl Default for ProjectConfig {
77 fn default() -> Self {
78 Self {
79 name: default_project_name(),
80 }
81 }
82}
83
84fn default_project_name() -> String {
85 std::env::current_dir()
86 .ok()
87 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
88 .unwrap_or_else(|| "cgx-project".to_string())
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct AnalyzeConfig {
93 #[serde(default = "default_languages")]
94 pub languages: Vec<String>,
95 #[serde(default = "default_exclude")]
96 pub exclude: Vec<String>,
97 #[serde(default = "default_churn_window")]
98 pub churn_window_days: u32,
99 #[serde(default = "default_co_change_threshold")]
100 pub co_change_threshold: u32,
101 #[serde(default = "default_max_file_size")]
102 pub max_file_size: u64,
103}
104
105impl Default for AnalyzeConfig {
106 fn default() -> Self {
107 Self {
108 languages: default_languages(),
109 exclude: default_exclude(),
110 churn_window_days: default_churn_window(),
111 co_change_threshold: default_co_change_threshold(),
112 max_file_size: default_max_file_size(),
113 }
114 }
115}
116
117fn default_languages() -> Vec<String> {
118 vec![
119 "typescript".to_string(),
120 "javascript".to_string(),
121 "python".to_string(),
122 "rust".to_string(),
123 "go".to_string(),
124 "java".to_string(),
125 "php".to_string(),
126 ]
127}
128
129fn default_exclude() -> Vec<String> {
130 vec![
131 "vendor/".to_string(),
132 "generated/".to_string(),
133 "*.pb.go".to_string(),
134 "third_party/".to_string(),
135 "build/".to_string(),
136 "out/".to_string(),
137 ]
138}
139
140fn default_churn_window() -> u32 {
141 90
142}
143
144fn default_co_change_threshold() -> u32 {
145 2
146}
147
148fn default_max_file_size() -> u64 {
149 2 * 1024 * 1024
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct IndexConfig {
154 #[serde(default = "default_true")]
155 pub incremental: bool,
156 #[serde(default = "default_true")]
157 pub store_hashes: bool,
158}
159
160impl Default for IndexConfig {
161 fn default() -> Self {
162 Self {
163 incremental: true,
164 store_hashes: true,
165 }
166 }
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct WatchConfig {
171 #[serde(default = "default_watch_include")]
172 pub include: Vec<String>,
173 #[serde(default = "default_watch_ignore")]
174 pub ignore: Vec<String>,
175 #[serde(default = "default_debounce")]
176 pub debounce_ms: u64,
177}
178
179impl Default for WatchConfig {
180 fn default() -> Self {
181 Self {
182 include: default_watch_include(),
183 ignore: default_watch_ignore(),
184 debounce_ms: default_debounce(),
185 }
186 }
187}
188
189fn default_watch_include() -> Vec<String> {
190 vec![
191 "*.ts".to_string(),
192 "*.js".to_string(),
193 "*.py".to_string(),
194 "*.rs".to_string(),
195 "*.go".to_string(),
196 "*.java".to_string(),
197 "*.php".to_string(),
198 ]
199}
200
201fn default_watch_ignore() -> Vec<String> {
202 vec![
203 ".git".to_string(),
204 "node_modules".to_string(),
205 "target".to_string(),
206 "dist".to_string(),
207 "__pycache__".to_string(),
208 ".cgx".to_string(),
209 ]
210}
211
212fn default_debounce() -> u64 {
213 500
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ChatConfig {
218 #[serde(default = "default_chat_provider")]
219 pub provider: String,
220 #[serde(default = "default_chat_model")]
221 pub model: String,
222 #[serde(default = "default_ollama_host")]
223 pub ollama_host: String,
224 #[serde(default = "default_chat_timeout")]
225 pub timeout: u32,
226}
227
228impl Default for ChatConfig {
229 fn default() -> Self {
230 Self {
231 provider: default_chat_provider(),
232 model: default_chat_model(),
233 ollama_host: default_ollama_host(),
234 timeout: default_chat_timeout(),
235 }
236 }
237}
238
239fn default_chat_provider() -> String {
240 "ollama".to_string()
241}
242
243fn default_chat_model() -> String {
244 "codellama".to_string()
245}
246
247fn default_ollama_host() -> String {
248 "http://localhost:11434".to_string()
249}
250
251fn default_chat_timeout() -> u32 {
252 30
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ServeConfig {
257 #[serde(default = "default_port")]
258 pub port: u16,
259 #[serde(default = "default_true")]
260 pub auto_open: bool,
261}
262
263impl Default for ServeConfig {
264 fn default() -> Self {
265 Self {
266 port: default_port(),
267 auto_open: true,
268 }
269 }
270}
271
272fn default_port() -> u16 {
273 7373
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct McpConfig {
278 #[serde(default = "default_true")]
279 pub enabled: bool,
280 #[serde(default = "default_mcp_timeout")]
281 pub timeout: u32,
282}
283
284impl Default for McpConfig {
285 fn default() -> Self {
286 Self {
287 enabled: true,
288 timeout: default_mcp_timeout(),
289 }
290 }
291}
292
293fn default_mcp_timeout() -> u32 {
294 30
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct SkillConfig {
299 #[serde(default = "default_true")]
300 pub auto_generate: bool,
301 #[serde(default = "default_true")]
302 pub include_token_budget: bool,
303 #[serde(default = "default_true")]
304 pub include_architecture: bool,
305}
306
307impl Default for SkillConfig {
308 fn default() -> Self {
309 Self {
310 auto_generate: true,
311 include_token_budget: true,
312 include_architecture: true,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ExportConfig {
319 #[serde(default = "default_export_format")]
320 pub default_format: String,
321 #[serde(default = "default_max_nodes")]
322 pub max_nodes: usize,
323}
324
325impl Default for ExportConfig {
326 fn default() -> Self {
327 Self {
328 default_format: default_export_format(),
329 max_nodes: default_max_nodes(),
330 }
331 }
332}
333
334fn default_export_format() -> String {
335 "json".to_string()
336}
337
338fn default_max_nodes() -> usize {
339 80
340}
341
342fn default_true() -> bool {
343 true
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct DocsConfig {
348 #[serde(default = "default_docs_output_dir")]
349 pub output_dir: String,
350 #[serde(default)]
351 pub vault_path: String,
352 #[serde(default = "default_docs_layout")]
353 pub layout: String,
354 #[serde(default = "default_wiki_links_style")]
355 pub wiki_links: String,
356 #[serde(default = "default_true")]
357 pub prompt_packets: bool,
358 #[serde(default = "default_true")]
359 pub frontmatter: bool,
360 #[serde(default = "default_true")]
361 pub include_dead_code: bool,
362 #[serde(default = "default_true")]
363 pub include_duplicates: bool,
364}
365
366impl Default for DocsConfig {
367 fn default() -> Self {
368 Self {
369 output_dir: default_docs_output_dir(),
370 vault_path: String::new(),
371 layout: default_docs_layout(),
372 wiki_links: default_wiki_links_style(),
373 prompt_packets: true,
374 frontmatter: true,
375 include_dead_code: true,
376 include_duplicates: true,
377 }
378 }
379}
380
381fn default_docs_output_dir() -> String {
382 "./cgx-docs".to_string()
383}
384
385fn default_docs_layout() -> String {
386 "layered".to_string()
387}
388
389fn default_wiki_links_style() -> String {
390 "obsidian".to_string()
391}