vtcode_core/config/loader/
mod.rs

1use crate::config::context::ContextFeaturesConfig;
2use crate::config::core::{
3    AgentConfig, AutomationConfig, CommandsConfig, PromptCachingConfig, SecurityConfig, ToolsConfig,
4};
5use crate::config::mcp::McpClientConfig;
6use crate::config::router::RouterConfig;
7use crate::config::telemetry::TelemetryConfig;
8use crate::config::{PtyConfig, UiConfig};
9use crate::project::SimpleProjectManager;
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12use std::fs;
13use std::path::{Path, PathBuf};
14
15/// Syntax highlighting configuration
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct SyntaxHighlightingConfig {
18    /// Enable syntax highlighting for tool output
19    #[serde(default = "default_true")]
20    pub enabled: bool,
21
22    /// Theme to use for syntax highlighting
23    #[serde(default = "default_theme")]
24    pub theme: String,
25
26    /// Enable theme caching for better performance
27    #[serde(default = "default_true")]
28    pub cache_themes: bool,
29
30    /// Maximum file size for syntax highlighting (in MB)
31    #[serde(default = "default_max_file_size")]
32    pub max_file_size_mb: usize,
33
34    /// Languages to enable syntax highlighting for
35    #[serde(default = "default_enabled_languages")]
36    pub enabled_languages: Vec<String>,
37
38    /// Performance settings - highlight timeout in milliseconds
39    #[serde(default = "default_highlight_timeout")]
40    pub highlight_timeout_ms: u64,
41}
42
43fn default_true() -> bool {
44    true
45}
46fn default_theme() -> String {
47    "base16-ocean.dark".to_string()
48}
49fn default_max_file_size() -> usize {
50    10
51}
52fn default_enabled_languages() -> Vec<String> {
53    vec![
54        "rust".to_string(),
55        "python".to_string(),
56        "javascript".to_string(),
57        "typescript".to_string(),
58        "go".to_string(),
59        "java".to_string(),
60        "cpp".to_string(),
61        "c".to_string(),
62        "php".to_string(),
63        "html".to_string(),
64        "css".to_string(),
65        "sql".to_string(),
66        "csharp".to_string(),
67        "bash".to_string(),
68    ]
69}
70fn default_highlight_timeout() -> u64 {
71    5000
72}
73
74impl Default for SyntaxHighlightingConfig {
75    fn default() -> Self {
76        Self {
77            enabled: default_true(),
78            theme: default_theme(),
79            cache_themes: default_true(),
80            max_file_size_mb: default_max_file_size(),
81            enabled_languages: default_enabled_languages(),
82            highlight_timeout_ms: default_highlight_timeout(),
83        }
84    }
85}
86
87/// Main configuration structure for VTCode
88#[derive(Debug, Clone, Deserialize, Serialize)]
89pub struct VTCodeConfig {
90    /// Agent-wide settings
91    #[serde(default)]
92    pub agent: AgentConfig,
93
94    /// Tool execution policies
95    #[serde(default)]
96    pub tools: ToolsConfig,
97
98    /// Unix command permissions
99    #[serde(default)]
100    pub commands: CommandsConfig,
101
102    /// Security settings
103    #[serde(default)]
104    pub security: SecurityConfig,
105
106    /// UI settings
107    #[serde(default)]
108    pub ui: UiConfig,
109
110    /// PTY settings
111    #[serde(default)]
112    pub pty: PtyConfig,
113
114    /// Context features (e.g., Decision Ledger)
115    #[serde(default)]
116    pub context: ContextFeaturesConfig,
117
118    /// Router configuration (dynamic model + engine selection)
119    #[serde(default)]
120    pub router: RouterConfig,
121
122    /// Telemetry configuration (logging, trajectory)
123    #[serde(default)]
124    pub telemetry: TelemetryConfig,
125
126    /// Syntax highlighting configuration
127    #[serde(default)]
128    pub syntax_highlighting: SyntaxHighlightingConfig,
129
130    /// Automation configuration
131    #[serde(default)]
132    pub automation: AutomationConfig,
133
134    /// Prompt cache configuration (local + provider integration)
135    #[serde(default)]
136    pub prompt_cache: PromptCachingConfig,
137
138    /// Model Context Protocol configuration
139    #[serde(default)]
140    pub mcp: McpClientConfig,
141}
142
143impl Default for VTCodeConfig {
144    fn default() -> Self {
145        Self {
146            agent: AgentConfig::default(),
147            tools: ToolsConfig::default(),
148            commands: CommandsConfig::default(),
149            security: SecurityConfig::default(),
150            ui: UiConfig::default(),
151            pty: PtyConfig::default(),
152            context: ContextFeaturesConfig::default(),
153            router: RouterConfig::default(),
154            telemetry: TelemetryConfig::default(),
155            syntax_highlighting: SyntaxHighlightingConfig::default(),
156            automation: AutomationConfig::default(),
157            prompt_cache: PromptCachingConfig::default(),
158            mcp: McpClientConfig::default(),
159        }
160    }
161}
162
163impl VTCodeConfig {
164    /// Bootstrap project with config + gitignore
165    pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
166        Self::bootstrap_project_with_options(workspace, force, false)
167    }
168
169    /// Bootstrap project with config + gitignore, with option to create in home directory
170    pub fn bootstrap_project_with_options<P: AsRef<Path>>(
171        workspace: P,
172        force: bool,
173        use_home_dir: bool,
174    ) -> Result<Vec<String>> {
175        let workspace = workspace.as_ref();
176        let mut created_files = Vec::new();
177
178        // Determine where to create the config file
179        let (config_path, gitignore_path) = if use_home_dir {
180            // Create in user's home directory
181            if let Some(home_dir) = ConfigManager::get_home_dir() {
182                let vtcode_dir = home_dir.join(".vtcode");
183                // Create .vtcode directory if it doesn't exist
184                if !vtcode_dir.exists() {
185                    fs::create_dir_all(&vtcode_dir).with_context(|| {
186                        format!("Failed to create directory: {}", vtcode_dir.display())
187                    })?;
188                }
189                (
190                    vtcode_dir.join("vtcode.toml"),
191                    vtcode_dir.join(".vtcodegitignore"),
192                )
193            } else {
194                // Fallback to workspace if home directory cannot be determined
195                let config_path = workspace.join("vtcode.toml");
196                let gitignore_path = workspace.join(".vtcodegitignore");
197                (config_path, gitignore_path)
198            }
199        } else {
200            // Create in workspace
201            let config_path = workspace.join("vtcode.toml");
202            let gitignore_path = workspace.join(".vtcodegitignore");
203            (config_path, gitignore_path)
204        };
205
206        // Create vtcode.toml
207        if !config_path.exists() || force {
208            let default_config = VTCodeConfig::default();
209            let config_content = toml::to_string_pretty(&default_config)
210                .context("Failed to serialize default configuration")?;
211
212            fs::write(&config_path, config_content).with_context(|| {
213                format!("Failed to write config file: {}", config_path.display())
214            })?;
215
216            created_files.push("vtcode.toml".to_string());
217        }
218
219        // Create .vtcodegitignore
220        if !gitignore_path.exists() || force {
221            let gitignore_content = Self::default_vtcode_gitignore();
222            fs::write(&gitignore_path, gitignore_content).with_context(|| {
223                format!(
224                    "Failed to write gitignore file: {}",
225                    gitignore_path.display()
226                )
227            })?;
228
229            created_files.push(".vtcodegitignore".to_string());
230        }
231
232        Ok(created_files)
233    }
234
235    /// Generate default .vtcodegitignore content
236    fn default_vtcode_gitignore() -> String {
237        r#"# Security-focused exclusions
238.env, .env.local, secrets/, .aws/, .ssh/
239
240# Development artifacts
241target/, build/, dist/, node_modules/, vendor/
242
243# Database files
244*.db, *.sqlite, *.sqlite3
245
246# Binary files
247*.exe, *.dll, *.so, *.dylib, *.bin
248
249# IDE files (comprehensive)
250.vscode/, .idea/, *.swp, *.swo
251"#
252        .to_string()
253    }
254
255    /// Create sample configuration file
256    pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
257        let output = output.as_ref();
258        let default_config = VTCodeConfig::default();
259        let config_content = toml::to_string_pretty(&default_config)
260            .context("Failed to serialize default configuration")?;
261
262        fs::write(output, config_content)
263            .with_context(|| format!("Failed to write config file: {}", output.display()))?;
264
265        Ok(())
266    }
267}
268
269/// Configuration manager for loading and validating configurations
270#[derive(Clone)]
271pub struct ConfigManager {
272    config: VTCodeConfig,
273    config_path: Option<PathBuf>,
274    project_manager: Option<SimpleProjectManager>,
275    project_name: Option<String>,
276}
277
278impl ConfigManager {
279    /// Load configuration from the default locations
280    pub fn load() -> Result<Self> {
281        Self::load_from_workspace(std::env::current_dir()?)
282    }
283
284    /// Get the user's home directory path
285    fn get_home_dir() -> Option<PathBuf> {
286        // Try standard environment variables
287        if let Ok(home) = std::env::var("HOME") {
288            return Some(PathBuf::from(home));
289        }
290
291        // Try USERPROFILE on Windows
292        if let Ok(userprofile) = std::env::var("USERPROFILE") {
293            return Some(PathBuf::from(userprofile));
294        }
295
296        // Fallback to dirs crate approach
297        dirs::home_dir()
298    }
299
300    /// Load configuration from a specific workspace
301    pub fn load_from_workspace(workspace: impl AsRef<Path>) -> Result<Self> {
302        let workspace = workspace.as_ref();
303
304        // Initialize project manager
305        let project_manager = Some(SimpleProjectManager::new(workspace.to_path_buf()));
306        let project_name = project_manager
307            .as_ref()
308            .and_then(|pm| pm.identify_current_project().ok());
309
310        // Try vtcode.toml in workspace root first
311        let config_path = workspace.join("vtcode.toml");
312        if config_path.exists() {
313            let config = Self::load_from_file(&config_path)?;
314            return Ok(Self {
315                config: config.config,
316                config_path: config.config_path,
317                project_manager,
318                project_name,
319            });
320        }
321
322        // Try .vtcode/vtcode.toml in workspace
323        let fallback_path = workspace.join(".vtcode").join("vtcode.toml");
324        if fallback_path.exists() {
325            let config = Self::load_from_file(&fallback_path)?;
326            return Ok(Self {
327                config: config.config,
328                config_path: config.config_path,
329                project_manager,
330                project_name,
331            });
332        }
333
334        // Try ~/.vtcode/vtcode.toml in user home directory
335        if let Some(home_dir) = Self::get_home_dir() {
336            let home_config_path = home_dir.join(".vtcode").join("vtcode.toml");
337            if home_config_path.exists() {
338                let config = Self::load_from_file(&home_config_path)?;
339                return Ok(Self {
340                    config: config.config,
341                    config_path: config.config_path,
342                    project_manager,
343                    project_name,
344                });
345            }
346        }
347
348        // Try project-specific configuration
349        if let (Some(pm), Some(pname)) = (&project_manager, &project_name) {
350            let project_config_path = pm.config_dir(pname).join("vtcode.toml");
351            if project_config_path.exists() {
352                let config = Self::load_from_file(&project_config_path)?;
353                return Ok(Self {
354                    config: config.config,
355                    config_path: config.config_path,
356                    project_manager: Some(pm.clone()),
357                    project_name: Some(pname.clone()),
358                });
359            }
360        }
361
362        // Use default configuration if no file found
363        Ok(Self {
364            config: VTCodeConfig::default(),
365            config_path: None,
366            project_manager,
367            project_name,
368        })
369    }
370
371    /// Load configuration from a specific file
372    pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
373        let path = path.as_ref();
374        let content = std::fs::read_to_string(path)
375            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
376
377        let config: VTCodeConfig = toml::from_str(&content)
378            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
379
380        // Initialize project manager but don't set project name since we're loading from file
381        // Use current directory as workspace root for file-based loading
382        let project_manager = std::env::current_dir()
383            .ok()
384            .map(|cwd| SimpleProjectManager::new(cwd));
385
386        Ok(Self {
387            config,
388            config_path: Some(path.to_path_buf()),
389            project_manager,
390            project_name: None,
391        })
392    }
393
394    /// Get the loaded configuration
395    pub fn config(&self) -> &VTCodeConfig {
396        &self.config
397    }
398
399    /// Get the configuration file path (if loaded from file)
400    pub fn config_path(&self) -> Option<&Path> {
401        self.config_path.as_deref()
402    }
403
404    /// Get session duration from agent config
405    pub fn session_duration(&self) -> std::time::Duration {
406        std::time::Duration::from_secs(60 * 60) // Default 1 hour
407    }
408
409    /// Get the project manager (if available)
410    pub fn project_manager(&self) -> Option<&SimpleProjectManager> {
411        self.project_manager.as_ref()
412    }
413
414    /// Get the project name (if identified)
415    pub fn project_name(&self) -> Option<&str> {
416        self.project_name.as_deref()
417    }
418}