vtcode_core/config/loader/
mod.rs

1use crate::config::PtyConfig;
2use crate::config::context::ContextFeaturesConfig;
3use crate::config::core::{
4    AgentConfig, AutomationConfig, CommandsConfig, SecurityConfig, ToolsConfig,
5};
6use crate::config::router::RouterConfig;
7use crate::config::telemetry::TelemetryConfig;
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    /// PTY settings
106    #[serde(default)]
107    pub pty: PtyConfig,
108
109    /// Context features (e.g., Decision Ledger)
110    #[serde(default)]
111    pub context: ContextFeaturesConfig,
112
113    /// Router configuration (dynamic model + engine selection)
114    #[serde(default)]
115    pub router: RouterConfig,
116
117    /// Telemetry configuration (logging, trajectory)
118    #[serde(default)]
119    pub telemetry: TelemetryConfig,
120
121    /// Syntax highlighting configuration
122    #[serde(default)]
123    pub syntax_highlighting: SyntaxHighlightingConfig,
124
125    /// Automation configuration
126    #[serde(default)]
127    pub automation: AutomationConfig,
128}
129
130impl Default for VTCodeConfig {
131    fn default() -> Self {
132        Self {
133            agent: AgentConfig::default(),
134            tools: ToolsConfig::default(),
135            commands: CommandsConfig::default(),
136            security: SecurityConfig::default(),
137            pty: PtyConfig::default(),
138            context: ContextFeaturesConfig::default(),
139            router: RouterConfig::default(),
140            telemetry: TelemetryConfig::default(),
141            syntax_highlighting: SyntaxHighlightingConfig::default(),
142            automation: AutomationConfig::default(),
143        }
144    }
145}
146
147impl VTCodeConfig {
148    /// Bootstrap project with config + gitignore
149    pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
150        Self::bootstrap_project_with_options(workspace, force, false)
151    }
152
153    /// Bootstrap project with config + gitignore, with option to create in home directory
154    pub fn bootstrap_project_with_options<P: AsRef<Path>>(
155        workspace: P,
156        force: bool,
157        use_home_dir: bool,
158    ) -> Result<Vec<String>> {
159        let workspace = workspace.as_ref();
160        let mut created_files = Vec::new();
161
162        // Determine where to create the config file
163        let (config_path, gitignore_path) = if use_home_dir {
164            // Create in user's home directory
165            if let Some(home_dir) = ConfigManager::get_home_dir() {
166                let vtcode_dir = home_dir.join(".vtcode");
167                // Create .vtcode directory if it doesn't exist
168                if !vtcode_dir.exists() {
169                    fs::create_dir_all(&vtcode_dir).with_context(|| {
170                        format!("Failed to create directory: {}", vtcode_dir.display())
171                    })?;
172                }
173                (
174                    vtcode_dir.join("vtcode.toml"),
175                    vtcode_dir.join(".vtcodegitignore"),
176                )
177            } else {
178                // Fallback to workspace if home directory cannot be determined
179                let config_path = workspace.join("vtcode.toml");
180                let gitignore_path = workspace.join(".vtcodegitignore");
181                (config_path, gitignore_path)
182            }
183        } else {
184            // Create in workspace
185            let config_path = workspace.join("vtcode.toml");
186            let gitignore_path = workspace.join(".vtcodegitignore");
187            (config_path, gitignore_path)
188        };
189
190        // Create vtcode.toml
191        if !config_path.exists() || force {
192            let default_config = VTCodeConfig::default();
193            let config_content = toml::to_string_pretty(&default_config)
194                .context("Failed to serialize default configuration")?;
195
196            fs::write(&config_path, config_content).with_context(|| {
197                format!("Failed to write config file: {}", config_path.display())
198            })?;
199
200            created_files.push("vtcode.toml".to_string());
201        }
202
203        // Create .vtcodegitignore
204        if !gitignore_path.exists() || force {
205            let gitignore_content = Self::default_vtcode_gitignore();
206            fs::write(&gitignore_path, gitignore_content).with_context(|| {
207                format!(
208                    "Failed to write gitignore file: {}",
209                    gitignore_path.display()
210                )
211            })?;
212
213            created_files.push(".vtcodegitignore".to_string());
214        }
215
216        Ok(created_files)
217    }
218
219    /// Generate default .vtcodegitignore content
220    fn default_vtcode_gitignore() -> String {
221        r#"# Security-focused exclusions
222.env, .env.local, secrets/, .aws/, .ssh/
223
224# Development artifacts
225target/, build/, dist/, node_modules/, vendor/
226
227# Database files
228*.db, *.sqlite, *.sqlite3
229
230# Binary files
231*.exe, *.dll, *.so, *.dylib, *.bin
232
233# IDE files (comprehensive)
234.vscode/, .idea/, *.swp, *.swo
235"#
236        .to_string()
237    }
238
239    /// Create sample configuration file
240    pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
241        let output = output.as_ref();
242        let default_config = VTCodeConfig::default();
243        let config_content = toml::to_string_pretty(&default_config)
244            .context("Failed to serialize default configuration")?;
245
246        fs::write(output, config_content)
247            .with_context(|| format!("Failed to write config file: {}", output.display()))?;
248
249        Ok(())
250    }
251}
252
253/// Configuration manager for loading and validating configurations
254#[derive(Clone)]
255pub struct ConfigManager {
256    config: VTCodeConfig,
257    config_path: Option<PathBuf>,
258    project_manager: Option<SimpleProjectManager>,
259    project_name: Option<String>,
260}
261
262impl ConfigManager {
263    /// Load configuration from the default locations
264    pub fn load() -> Result<Self> {
265        Self::load_from_workspace(std::env::current_dir()?)
266    }
267
268    /// Get the user's home directory path
269    fn get_home_dir() -> Option<PathBuf> {
270        // Try standard environment variables
271        if let Ok(home) = std::env::var("HOME") {
272            return Some(PathBuf::from(home));
273        }
274
275        // Try USERPROFILE on Windows
276        if let Ok(userprofile) = std::env::var("USERPROFILE") {
277            return Some(PathBuf::from(userprofile));
278        }
279
280        // Fallback to dirs crate approach
281        dirs::home_dir()
282    }
283
284    /// Load configuration from a specific workspace
285    pub fn load_from_workspace(workspace: impl AsRef<Path>) -> Result<Self> {
286        let workspace = workspace.as_ref();
287
288        // Initialize project manager
289        let project_manager = Some(SimpleProjectManager::new(workspace.to_path_buf()));
290        let project_name = project_manager
291            .as_ref()
292            .and_then(|pm| pm.identify_current_project().ok());
293
294        // Try vtcode.toml in workspace root first
295        let config_path = workspace.join("vtcode.toml");
296        if config_path.exists() {
297            let config = Self::load_from_file(&config_path)?;
298            return Ok(Self {
299                config: config.config,
300                config_path: config.config_path,
301                project_manager,
302                project_name,
303            });
304        }
305
306        // Try .vtcode/vtcode.toml in workspace
307        let fallback_path = workspace.join(".vtcode").join("vtcode.toml");
308        if fallback_path.exists() {
309            let config = Self::load_from_file(&fallback_path)?;
310            return Ok(Self {
311                config: config.config,
312                config_path: config.config_path,
313                project_manager,
314                project_name,
315            });
316        }
317
318        // Try ~/.vtcode/vtcode.toml in user home directory
319        if let Some(home_dir) = Self::get_home_dir() {
320            let home_config_path = home_dir.join(".vtcode").join("vtcode.toml");
321            if home_config_path.exists() {
322                let config = Self::load_from_file(&home_config_path)?;
323                return Ok(Self {
324                    config: config.config,
325                    config_path: config.config_path,
326                    project_manager,
327                    project_name,
328                });
329            }
330        }
331
332        // Try project-specific configuration
333        if let (Some(pm), Some(pname)) = (&project_manager, &project_name) {
334            let project_config_path = pm.config_dir(pname).join("vtcode.toml");
335            if project_config_path.exists() {
336                let config = Self::load_from_file(&project_config_path)?;
337                return Ok(Self {
338                    config: config.config,
339                    config_path: config.config_path,
340                    project_manager: Some(pm.clone()),
341                    project_name: Some(pname.clone()),
342                });
343            }
344        }
345
346        // Use default configuration if no file found
347        Ok(Self {
348            config: VTCodeConfig::default(),
349            config_path: None,
350            project_manager,
351            project_name,
352        })
353    }
354
355    /// Load configuration from a specific file
356    pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
357        let path = path.as_ref();
358        let content = std::fs::read_to_string(path)
359            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
360
361        let config: VTCodeConfig = toml::from_str(&content)
362            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
363
364        // Initialize project manager but don't set project name since we're loading from file
365        // Use current directory as workspace root for file-based loading
366        let project_manager = std::env::current_dir()
367            .ok()
368            .map(|cwd| SimpleProjectManager::new(cwd));
369
370        Ok(Self {
371            config,
372            config_path: Some(path.to_path_buf()),
373            project_manager,
374            project_name: None,
375        })
376    }
377
378    /// Get the loaded configuration
379    pub fn config(&self) -> &VTCodeConfig {
380        &self.config
381    }
382
383    /// Get the configuration file path (if loaded from file)
384    pub fn config_path(&self) -> Option<&Path> {
385        self.config_path.as_deref()
386    }
387
388    /// Get session duration from agent config
389    pub fn session_duration(&self) -> std::time::Duration {
390        std::time::Duration::from_secs(60 * 60) // Default 1 hour
391    }
392
393    /// Get the project manager (if available)
394    pub fn project_manager(&self) -> Option<&SimpleProjectManager> {
395        self.project_manager.as_ref()
396    }
397
398    /// Get the project name (if identified)
399    pub fn project_name(&self) -> Option<&str> {
400        self.project_name.as_deref()
401    }
402}