vtcode_core/config/loader/
mod.rs

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