project_examer/
config.rs

1use serde::{Deserialize, Serialize};
2use std::{env, path::PathBuf};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Config {
6    pub target_directory: PathBuf,
7    pub ignore_patterns: Vec<String>,
8    pub file_extensions: Vec<String>,
9    pub max_file_size: usize,
10    pub llm: LLMConfig,
11    pub analysis: AnalysisConfig,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct LLMConfig {
16    pub provider: LLMProvider,
17    pub api_key: Option<String>,
18    pub base_url: Option<String>,
19    pub model: String,
20    pub max_tokens: usize,
21    pub temperature: f32,
22    pub timeout_seconds: u64,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum LLMProvider {
27    OpenAI,
28    Ollama,
29    Anthropic,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AnalysisConfig {
34    pub include_dependencies: bool,
35    pub include_function_calls: bool,
36    pub include_architecture_patterns: bool,
37    pub include_security_analysis: bool,
38    pub max_depth: usize,
39}
40
41impl Default for Config {
42    fn default() -> Self {
43        Self {
44            target_directory: PathBuf::from("."),
45            ignore_patterns: vec![
46                "node_modules".to_string(),
47                ".git".to_string(),
48                "target".to_string(),
49                "build".to_string(),
50                "dist".to_string(),
51                "*.log".to_string(),
52                ".env".to_string(),
53                ".env.*".to_string(),
54                "*.min.js".to_string(),
55                "*.map".to_string(),
56                "test-*".to_string(),
57                "test_*".to_string(),
58            ],
59            file_extensions: vec![
60                "rs".to_string(),
61                "js".to_string(),
62                "ts".to_string(),
63                "tsx".to_string(),
64                "jsx".to_string(),
65                "py".to_string(),
66                "java".to_string(),
67                "go".to_string(),
68                "cpp".to_string(),
69                "c".to_string(),
70                "h".to_string(),
71                "md".to_string(),
72                "txt".to_string(),
73                "toml".to_string(),
74                "yaml".to_string(),
75                "yml".to_string(),
76                "json".to_string(),
77                "html".to_string(),
78                "css".to_string(),
79            ],
80            max_file_size: 1024 * 1024, // 1MB
81            llm: LLMConfig {
82                provider: LLMProvider::OpenAI,
83                api_key: None,
84                base_url: None,
85                model: "gpt-4".to_string(),
86                max_tokens: 4000,
87                temperature: 0.1,
88                timeout_seconds: 300,
89            },
90            analysis: AnalysisConfig {
91                include_dependencies: true,
92                include_function_calls: true,
93                include_architecture_patterns: true,
94                include_security_analysis: false,
95                max_depth: 10,
96            },
97        }
98    }
99}
100
101impl Config {
102    /// Get the default config file path (~/.project-examer.toml)
103    pub fn default_config_path() -> crate::Result<PathBuf> {
104        let home_dir = env::var("HOME")
105            .or_else(|_| env::var("USERPROFILE"))
106            .map_err(|_| anyhow::anyhow!("Could not determine home directory"))?;
107        Ok(PathBuf::from(home_dir).join(".project-examer.toml"))
108    }
109
110    /// Load config from file, falling back to defaults if file doesn't exist
111    pub fn load() -> crate::Result<Self> {
112        let config_path = Self::default_config_path()?;
113        
114        let mut config = if config_path.exists() {
115            println!("📝 Loading configuration from: {}", config_path.display());
116            Self::from_file(&config_path)?
117        } else {
118            println!("â„šī¸  No config file found at {}, using defaults", config_path.display());
119            println!("💡 Run 'project-examer config' to create a default configuration file");
120            Self::default()
121        };
122        
123        // Override API key from environment variables if not set in config
124        if config.llm.api_key.is_none() {
125            config.llm.api_key = match config.llm.provider {
126                LLMProvider::OpenAI => env::var("OPENAI_API_KEY").ok(),
127                LLMProvider::Anthropic => env::var("ANTHROPIC_API_KEY").ok(),
128                LLMProvider::Ollama => None, // Ollama typically doesn't need API keys
129            };
130        }
131        
132        Ok(config)
133    }
134
135    /// Load config from a specific file path
136    pub fn from_file(path: &PathBuf) -> crate::Result<Self> {
137        let content = std::fs::read_to_string(path)?;
138        let config: Config = toml::from_str(&content)?;
139        Ok(config)
140    }
141
142    /// Save config to a file
143    pub fn to_file(&self, path: &PathBuf) -> crate::Result<()> {
144        // Create parent directories if they don't exist
145        if let Some(parent) = path.parent() {
146            std::fs::create_dir_all(parent)?;
147        }
148        
149        let content = toml::to_string_pretty(self)?;
150        std::fs::write(path, content)?;
151        Ok(())
152    }
153
154    /// Save config to the default location
155    pub fn save_default(&self) -> crate::Result<()> {
156        let config_path = Self::default_config_path()?;
157        self.to_file(&config_path)
158    }
159
160    /// Create a config file with all available options documented
161    pub fn create_documented_config() -> String {
162        format!(r#"# Project Examer Configuration File
163# This file configures how project-examer analyzes your codebase
164
165# Target directory to analyze (defaults to current directory)
166target_directory = "."
167
168# Patterns to ignore during file discovery
169ignore_patterns = [
170    "node_modules",
171    ".git", 
172    "target",
173    "build",
174    "dist",
175    "*.log",
176    ".env",
177    ".env.*",
178    "*.min.js",
179    "*.map"
180]
181
182# File extensions to include in analysis
183file_extensions = [
184    "rs", "js", "ts", "tsx", "jsx", "py", "java", "go", 
185    "cpp", "c", "h", "php", "rb", "cs", "swift", "kt",
186    "scala", "clj", "hs", "ml", "elm", "ex", "erl", "dart",
187    "lua", "r", "pl", "sh", "sql", "html", "css", "scss"
188]
189
190# Maximum file size to analyze (in bytes, default 1MB)
191max_file_size = 1048576
192
193[llm]
194# LLM Provider: "OpenAI", "Ollama", or "Anthropic"
195provider = "OpenAI"
196
197# API key for the provider (can also be set via environment variables)
198# OpenAI: OPENAI_API_KEY
199# Anthropic: ANTHROPIC_API_KEY  
200# api_key = "your-api-key-here"
201
202# Base URL (mainly for Ollama local instances)
203# base_url = "http://localhost:11434"
204
205# Model to use
206model = "gpt-4"
207
208# Maximum tokens for LLM responses
209max_tokens = 4000
210
211# Temperature for LLM responses (0.0 = deterministic, 1.0 = creative)
212temperature = 0.1
213
214# Request timeout in seconds (default: 300 seconds / 5 minutes)
215timeout_seconds = 300
216
217[analysis]
218# Include dependency analysis
219include_dependencies = true
220
221# Include function call analysis
222include_function_calls = true
223
224# Include architecture pattern detection
225include_architecture_patterns = true
226
227# Include security vulnerability analysis
228include_security_analysis = false
229
230# Maximum depth for dependency traversal
231max_depth = 10
232"#)
233    }
234}