1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Config {
7 pub api: ApiConfig,
8 pub cache: CacheConfig,
9 pub output: OutputConfig,
10 pub processing: ProcessingConfig,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ApiConfig {
15 pub openai_api_key: Option<String>,
16 pub model: String,
17 pub temperature: f32,
18 pub max_retries: u32,
19 pub retry_delay_ms: u64,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CacheConfig {
24 pub enabled: bool,
25 pub cache_dir: PathBuf,
26 pub ttl_hours: u64,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct OutputConfig {
31 pub format: OutputFormat,
32 pub language: String,
33 pub show_token_usage: bool,
34 pub quiet: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum OutputFormat {
40 Markdown,
41 Json,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ProcessingConfig {
46 pub parallel_requests: usize,
47 pub log_dir: PathBuf,
48}
49
50impl Default for Config {
51 fn default() -> Self {
52 Self {
53 api: ApiConfig {
54 openai_api_key: None,
55 model: "gpt-4o".to_string(),
56 temperature: 0.3,
57 max_retries: 3,
58 retry_delay_ms: 1000,
59 },
60 cache: CacheConfig {
61 enabled: true,
62 cache_dir: default_cache_dir(),
63 ttl_hours: 24,
64 },
65 output: OutputConfig {
66 format: OutputFormat::Markdown,
67 language: detect_system_language(),
68 show_token_usage: false,
69 quiet: false,
70 },
71 processing: ProcessingConfig {
72 parallel_requests: 1,
73 log_dir: default_log_dir(),
74 },
75 }
76 }
77}
78
79impl Config {
80 pub fn from_args(matches: &clap::ArgMatches) -> Result<Self, Box<dyn std::error::Error>> {
82 let mut config = Self::default();
83
84 if let Some(key) = matches.get_one::<String>("api-key") {
86 config.api.openai_api_key = Some(key.clone());
87 } else if let Ok(key) = std::env::var("OPENAI_API_KEY") {
88 config.api.openai_api_key = Some(key);
89 }
90
91 if let Some(model) = matches.get_one::<String>("model") {
92 config.api.model = model.clone();
93 }
94
95 if let Some(format) = matches.get_one::<String>("format") {
100 config.output.format = match format.as_str() {
101 "json" => OutputFormat::Json,
102 _ => OutputFormat::Markdown,
103 };
104 }
105
106 if let Some(lang) = matches.get_one::<String>("lang") {
107 config.output.language = lang.clone();
108 }
109
110 config.output.show_token_usage = matches.get_flag("show-token-usage");
111 config.output.quiet = matches.get_flag("quiet");
112
113 if let Some(parallel) = matches.get_one::<usize>("parallel") {
115 config.processing.parallel_requests = (*parallel).min(10);
116 }
117
118 if let Some(log_dir) = matches.get_one::<String>("log-dir") {
119 config.processing.log_dir = PathBuf::from(log_dir);
120 }
121
122 Ok(config)
123 }
124
125 pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
127 if self.api.openai_api_key.is_none() {
128 return Err("OpenAI API key is required. Set OPENAI_API_KEY environment variable or use --api-key option.".into());
129 }
130
131 if !self.processing.log_dir.exists() {
132 return Err(format!(
133 "Log directory does not exist: {}",
134 self.processing.log_dir.display()
135 )
136 .into());
137 }
138
139 Ok(())
140 }
141}
142
143fn default_cache_dir() -> PathBuf {
144 if let Ok(home) = std::env::var("HOME") {
145 PathBuf::from(home).join(".cache").join("cc2report")
146 } else {
147 PathBuf::from(".cc2report-cache")
148 }
149}
150
151fn default_log_dir() -> PathBuf {
152 if let Ok(home) = std::env::var("HOME") {
153 PathBuf::from(home).join(".claude").join("projects")
154 } else {
155 PathBuf::from(".")
156 }
157}
158
159fn detect_system_language() -> String {
160 if let Ok(lang) = std::env::var("LANG") {
161 let lang_code = lang.split('.').next().unwrap_or(&lang);
162 let primary_lang = lang_code.split('_').next().unwrap_or(lang_code);
163
164 match primary_lang {
165 "ja" => "ja",
166 "zh" => "zh",
167 "ko" => "ko",
168 "es" => "es",
169 "fr" => "fr",
170 "de" => "de",
171 "pt" => "pt",
172 "ru" => "ru",
173 "it" => "it",
174 "nl" => "nl",
175 "pl" => "pl",
176 "tr" => "tr",
177 "ar" => "ar",
178 "hi" => "hi",
179 "th" => "th",
180 "vi" => "vi",
181 "id" => "id",
182 "ms" => "ms",
183 _ => "en",
184 }
185 .to_string()
186 } else {
187 "en".to_string()
188 }
189}