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