claude_code_switcher/
settings.rs

1use anyhow::{Result, anyhow};
2use console::style;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8use crate::{Configurable, SnapshotScope, TemplateType};
9
10/// Main Claude Code settings structure
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "camelCase")]
13pub struct ClaudeSettings {
14    /// Provider configuration
15    pub provider: Option<ProviderConfig>,
16
17    /// Model settings
18    pub model: Option<ModelConfig>,
19
20    /// API endpoint configuration
21    pub endpoint: Option<EndpointConfig>,
22
23    /// HTTP client settings
24    pub http: Option<HTTPConfig>,
25
26    /// Permissions configuration
27    pub permissions: Option<Permissions>,
28
29    /// Hook configurations
30    pub hooks: Option<Hooks>,
31
32    /// Status line configuration
33    pub status_line: Option<StatusLine>,
34
35    /// Environment variables captured at snapshot time
36    pub environment: Option<HashMap<String, String>>,
37}
38
39/// Provider configuration
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct ProviderConfig {
42    pub id: String,
43    pub metadata: Option<HashMap<String, String>>,
44}
45
46/// Model configuration
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ModelConfig {
49    pub name: String,
50    pub metadata: Option<HashMap<String, String>>,
51}
52
53/// Endpoint configuration
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct EndpointConfig {
56    pub id: String,
57    pub api_base: String,
58    pub api_key: Option<String>,
59    pub endpoint_id: Option<String>,
60    pub metadata: Option<HashMap<String, String>>,
61}
62
63/// HTTP client configuration
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub struct HTTPConfig {
66    pub timeout_ms: Option<u64>,
67    pub max_retries: Option<u32>,
68    pub retry_backoff_factor: Option<f64>,
69}
70
71/// Permissions configuration
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct Permissions {
74    pub allow_network_access: Option<bool>,
75    pub allow_filesystem_access: Option<bool>,
76    pub allow_command_execution: Option<bool>,
77}
78
79/// Hooks configuration
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct Hooks {
82    pub on_start: Option<Vec<String>>,
83    pub on_save: Option<Vec<String>>,
84    pub on_send_message: Option<Vec<String>>,
85    pub on_receive_message: Option<Vec<String>>,
86}
87
88/// Status line configuration
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct StatusLine {
91    pub enabled: Option<bool>,
92    pub format: Option<String>,
93    pub style: Option<String>,
94}
95
96impl ClaudeSettings {
97    /// Create empty settings
98    pub fn new() -> Self {
99        Self {
100            provider: None,
101            model: None,
102            endpoint: None,
103            http: None,
104            permissions: None,
105            hooks: None,
106            status_line: None,
107            environment: None,
108        }
109    }
110
111    /// Read settings from file
112    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
113        let path = path.as_ref();
114        if !path.exists() {
115            return Ok(Self::new());
116        }
117
118        let content = fs::read_to_string(path)
119            .map_err(|e| anyhow!("Failed to read settings file {}: {}", path.display(), e))?;
120
121        if content.trim().is_empty() {
122            return Ok(Self::new());
123        }
124
125        serde_json::from_str(&content)
126            .map_err(|e| anyhow!("Failed to parse settings file {}: {}", path.display(), e))
127    }
128
129    /// Write settings to file
130    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
131        let path = path.as_ref();
132        let parent = path.parent().ok_or_else(|| {
133            anyhow!(
134                "Settings file path {} has no parent directory",
135                path.display()
136            )
137        })?;
138
139        fs::create_dir_all(parent).map_err(|e| {
140            anyhow!(
141                "Failed to create settings directory {}: {}",
142                parent.display(),
143                e
144            )
145        })?;
146
147        let content = serde_json::to_string_pretty(self)
148            .map_err(|e| anyhow!("Failed to serialize settings: {}", e))?;
149
150        fs::write(path, content)
151            .map_err(|e| anyhow!("Failed to write settings file {}: {}", path.display(), e))
152    }
153
154    /// Capture environment variables relevant to Claude Code
155    pub fn capture_environment() -> HashMap<String, String> {
156        let mut env = HashMap::new();
157
158        // Claude Code specific environment variables
159        if let Ok(value) = std::env::var("CLAUDE_CODE_API_KEY") {
160            env.insert("CLAUDE_CODE_API_KEY".to_string(), value);
161        }
162        if let Ok(value) = std::env::var("ANTHROPIC_API_KEY") {
163            env.insert("ANTHROPIC_API_KEY".to_string(), value);
164        }
165
166        env
167    }
168
169    /// Capture environment variables for a specific template type
170    pub fn capture_template_environment(template_type: &TemplateType) -> HashMap<String, String> {
171        let mut env = HashMap::new();
172
173        match template_type {
174            TemplateType::DeepSeek => {
175                if let Ok(value) = std::env::var("DEEPSEEK_API_KEY") {
176                    env.insert("DEEPSEEK_API_KEY".to_string(), value);
177                }
178            }
179            TemplateType::Zai => {
180                if let Ok(value) = std::env::var("Z_AI_API_KEY") {
181                    env.insert("Z_AI_API_KEY".to_string(), value);
182                }
183            }
184            TemplateType::K2 | TemplateType::K2Thinking => {
185                if let Ok(value) = std::env::var("MOONSHOT_API_KEY") {
186                    env.insert("MOONSHOT_API_KEY".to_string(), value);
187                }
188            }
189            TemplateType::KatCoder | TemplateType::KatCoderPro | TemplateType::KatCoderAir => {
190                if let Ok(value) = std::env::var("KAT_CODER_API_KEY") {
191                    env.insert("KAT_CODER_API_KEY".to_string(), value);
192                }
193            }
194            TemplateType::Kimi => {
195                if let Ok(value) = std::env::var("KIMI_API_KEY") {
196                    env.insert("KIMI_API_KEY".to_string(), value);
197                }
198            }
199            TemplateType::Longcat => {
200                if let Ok(value) = std::env::var("LONGCAT_API_KEY") {
201                    env.insert("LONGCAT_API_KEY".to_string(), value);
202                }
203            }
204            TemplateType::MiniMax => {
205                if let Ok(value) = std::env::var("MINIMAX_API_KEY") {
206                    env.insert("MINIMAX_API_KEY".to_string(), value);
207                }
208            }
209        }
210
211        env
212    }
213
214    /// Mask API keys in settings for display
215    pub fn mask_api_keys(&self) -> Self {
216        let mut masked = self.clone();
217
218        if let Some(ref mut endpoint) = masked.endpoint {
219            if endpoint.api_key.is_some() {
220                endpoint.api_key = Some("••••••••".to_string());
221            }
222        }
223
224        masked
225    }
226
227    /// Get API key from settings or environment
228    pub fn get_api_key(&self) -> Option<String> {
229        // First try from settings
230        if let Some(ref endpoint) = self.endpoint {
231            if let Some(ref api_key) = endpoint.api_key {
232                return Some(api_key.clone());
233            }
234        }
235
236        // Then try environment variables
237        if let Ok(key) = std::env::var("CLAUDE_CODE_API_KEY") {
238            return Some(key);
239        }
240        if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
241            return Some(key);
242        }
243
244        None
245    }
246}
247
248impl crate::Configurable for ClaudeSettings {
249    fn merge_with(mut self, other: Self) -> Self {
250        // Merge in priority order: self (higher priority) overrides other (lower priority)
251
252        if other.provider.is_some() && self.provider.is_none() {
253            self.provider = other.provider;
254        }
255
256        if other.model.is_some() && self.model.is_none() {
257            self.model = other.model;
258        }
259
260        if other.endpoint.is_some() && self.endpoint.is_none() {
261            self.endpoint = other.endpoint;
262        }
263
264        if other.http.is_some() && self.http.is_none() {
265            self.http = other.http;
266        }
267
268        if other.permissions.is_some() && self.permissions.is_none() {
269            self.permissions = other.permissions;
270        }
271
272        if other.hooks.is_some() && self.hooks.is_none() {
273            self.hooks = other.hooks;
274        }
275
276        if other.status_line.is_some() && self.status_line.is_none() {
277            self.status_line = other.status_line;
278        }
279
280        // Merge environment variables
281        if let Some(other_env) = other.environment {
282            let mut env = self.environment.unwrap_or_default();
283            env.extend(other_env);
284            self.environment = Some(env);
285        }
286
287        self
288    }
289
290    fn filter_by_scope(self, scope: &SnapshotScope) -> Self {
291        match scope {
292            SnapshotScope::Env => {
293                // Only environment variables
294                ClaudeSettings {
295                    environment: self.environment,
296                    ..Default::default()
297                }
298            }
299            SnapshotScope::Common => {
300                // Common settings (exclude environment)
301                ClaudeSettings {
302                    provider: self.provider,
303                    model: self.model,
304                    endpoint: self.endpoint,
305                    http: self.http,
306                    permissions: self.permissions,
307                    hooks: self.hooks,
308                    status_line: self.status_line,
309                    environment: None,
310                }
311            }
312            SnapshotScope::All => self, // Include everything
313        }
314    }
315
316    fn mask_sensitive_data(self) -> Self {
317        self.mask_api_keys()
318    }
319}
320
321impl Default for ClaudeSettings {
322    fn default() -> Self {
323        Self::new()
324    }
325}
326
327/// Merge multiple settings with priority
328pub fn merge_settings(settings: Vec<ClaudeSettings>) -> ClaudeSettings {
329    settings
330        .into_iter()
331        .fold(ClaudeSettings::new(), |acc, settings| {
332            settings.merge_with(acc)
333        })
334}
335
336/// Get display formatting for settings
337pub fn format_settings_for_display(settings: &ClaudeSettings, verbose: bool) -> String {
338    let mut output = String::new();
339
340    if verbose {
341        output.push_str(&format!("{} Settings\n", style("Current").bold().cyan()));
342        output.push_str(&format!(
343            "{} {}\n",
344            style("Provider:").bold(),
345            settings
346                .provider
347                .as_ref()
348                .map(|p| &p.id)
349                .unwrap_or(&"None".to_string())
350        ));
351        output.push_str(&format!(
352            "{} {}\n",
353            style("Model:").bold(),
354            settings
355                .model
356                .as_ref()
357                .map(|m| &m.name)
358                .unwrap_or(&"None".to_string())
359        ));
360
361        if let Some(ref endpoint) = settings.endpoint {
362            output.push_str(&format!(
363                "{} {} ({})\n",
364                style("Endpoint:").bold(),
365                endpoint.id,
366                endpoint.api_base
367            ));
368            if let Some(ref api_key) = endpoint.api_key {
369                output.push_str(&format!(
370                    "{} {}\n",
371                    style("API Key:").bold(),
372                    if api_key.len() > 8 {
373                        format!("{}••••••••", &api_key[..8])
374                    } else {
375                        "••••••••".to_string()
376                    }
377                ));
378            }
379        }
380
381        if let Some(ref http) = settings.http {
382            if let Some(timeout) = http.timeout_ms {
383                output.push_str(&format!("{} {}ms\n", style("Timeout:").bold(), timeout));
384            }
385        }
386    } else {
387        let provider = settings
388            .provider
389            .as_ref()
390            .map(|p| p.id.as_str())
391            .unwrap_or("None");
392        let model = settings
393            .model
394            .as_ref()
395            .map(|m| m.name.as_str())
396            .unwrap_or("None");
397
398        output.push_str(&format!(
399            "{}: {} | {}: {}\n",
400            style("Provider").bold(),
401            provider,
402            style("Model").bold(),
403            model
404        ));
405    }
406
407    output
408}