claude_code_switcher/
snapshots.rs

1use crate::settings::ClaudeSettings;
2use anyhow::{Result, anyhow};
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7use uuid::Uuid;
8
9/// Scope for snapshots
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub enum SnapshotScope {
12    /// Only environment variables
13    Env,
14    /// Common settings (exclude environment)
15    Common,
16    /// All settings
17    All,
18}
19
20impl std::str::FromStr for SnapshotScope {
21    type Err = anyhow::Error;
22
23    fn from_str(s: &str) -> Result<Self> {
24        match s.to_lowercase().as_str() {
25            "env" => Ok(SnapshotScope::Env),
26            "common" => Ok(SnapshotScope::Common),
27            "all" => Ok(SnapshotScope::All),
28            _ => Err(anyhow!(
29                "Invalid scope '{}'. Must be one of: env, common, all",
30                s
31            )),
32        }
33    }
34}
35
36impl std::fmt::Display for SnapshotScope {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            SnapshotScope::Env => write!(f, "env"),
40            SnapshotScope::Common => write!(f, "common"),
41            SnapshotScope::All => write!(f, "all"),
42        }
43    }
44}
45
46/// A snapshot of Claude Code settings
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Snapshot {
49    /// Unique identifier
50    pub id: String,
51
52    /// User-friendly name
53    pub name: String,
54
55    /// Optional description
56    pub description: Option<String>,
57
58    /// The settings data
59    pub settings: ClaudeSettings,
60
61    /// When this snapshot was created
62    pub created_at: String,
63
64    /// When this snapshot was last modified
65    pub updated_at: String,
66
67    /// Scope of this snapshot
68    pub scope: SnapshotScope,
69
70    /// Version for future compatibility
71    pub version: u32,
72}
73
74impl Snapshot {
75    /// Create a new snapshot
76    pub fn new(
77        name: String,
78        settings: ClaudeSettings,
79        scope: SnapshotScope,
80        description: Option<String>,
81    ) -> Self {
82        let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
83
84        Self {
85            id: Uuid::new_v4().to_string(),
86            name,
87            description,
88            settings,
89            created_at: now.clone(),
90            updated_at: now,
91            scope,
92            version: 1,
93        }
94    }
95
96    /// Update the timestamp
97    pub fn touch(&mut self) {
98        let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
99        self.updated_at = now;
100    }
101}
102
103/// Store for managing snapshots
104#[derive(Debug, Clone)]
105pub struct SnapshotStore {
106    /// Directory where snapshots are stored
107    pub snapshots_dir: PathBuf,
108}
109
110impl SnapshotStore {
111    /// Create a new snapshot store
112    pub fn new(snapshots_dir: PathBuf) -> Self {
113        Self { snapshots_dir }
114    }
115
116    /// Ensure the snapshots directory exists
117    pub fn ensure_dir(&self) -> Result<()> {
118        if !self.snapshots_dir.exists() {
119            fs::create_dir_all(&self.snapshots_dir).map_err(|e| {
120                anyhow!(
121                    "Failed to create snapshots directory {}: {}",
122                    self.snapshots_dir.display(),
123                    e
124                )
125            })?;
126        }
127        Ok(())
128    }
129
130    /// Get the path for a snapshot file
131    pub fn snapshot_path(&self, snapshot_id: &str) -> PathBuf {
132        self.snapshots_dir.join(format!("{}.json", snapshot_id))
133    }
134
135    /// Save a snapshot
136    pub fn save(&self, snapshot: &Snapshot) -> Result<()> {
137        self.ensure_dir()?;
138
139        let path = self.snapshot_path(&snapshot.id);
140        let content = serde_json::to_string_pretty(snapshot)
141            .map_err(|e| anyhow!("Failed to serialize snapshot: {}", e))?;
142
143        fs::write(&path, content)
144            .map_err(|e| anyhow!("Failed to write snapshot file {}: {}", path.display(), e))?;
145
146        Ok(())
147    }
148
149    /// Load a snapshot by ID
150    pub fn load(&self, snapshot_id: &str) -> Result<Snapshot> {
151        let path = self.snapshot_path(snapshot_id);
152
153        if !path.exists() {
154            return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
155        }
156
157        let content = fs::read_to_string(&path)
158            .map_err(|e| anyhow!("Failed to read snapshot file {}: {}", path.display(), e))?;
159
160        let snapshot: Snapshot = serde_json::from_str(&content)
161            .map_err(|e| anyhow!("Failed to parse snapshot file {}: {}", path.display(), e))?;
162
163        Ok(snapshot)
164    }
165
166    /// Load a snapshot by name
167    pub fn load_by_name(&self, name: &str) -> Result<Snapshot> {
168        let snapshots = self.list()?;
169
170        for snapshot in snapshots {
171            if snapshot.name == name {
172                return Ok(snapshot);
173            }
174        }
175
176        Err(anyhow!("Snapshot '{}' not found", name))
177    }
178
179    /// List all snapshots
180    pub fn list(&self) -> Result<Vec<Snapshot>> {
181        if !self.snapshots_dir.exists() {
182            return Ok(Vec::new());
183        }
184
185        let mut snapshots = Vec::new();
186
187        for entry in fs::read_dir(&self.snapshots_dir)? {
188            let entry = entry?;
189            let path = entry.path();
190
191            if path.extension().and_then(|s| s.to_str()) == Some("json") {
192                match self.load(path.file_stem().and_then(|s| s.to_str()).unwrap_or("")) {
193                    Ok(snapshot) => snapshots.push(snapshot),
194                    Err(_) => {
195                        // Skip invalid snapshot files
196                        continue;
197                    }
198                }
199            }
200        }
201
202        // Sort by creation date (newest first)
203        snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
204
205        Ok(snapshots)
206    }
207
208    /// Delete a snapshot
209    pub fn delete(&self, snapshot_id: &str) -> Result<()> {
210        let path = self.snapshot_path(snapshot_id);
211
212        if !path.exists() {
213            return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
214        }
215
216        fs::remove_file(&path)
217            .map_err(|e| anyhow!("Failed to delete snapshot file {}: {}", path.display(), e))?;
218
219        Ok(())
220    }
221
222    /// Delete a snapshot by name
223    pub fn delete_by_name(&self, name: &str) -> Result<()> {
224        let snapshots = self.list()?;
225
226        for snapshot in snapshots {
227            if snapshot.name == name {
228                return self.delete(&snapshot.id);
229            }
230        }
231
232        Err(anyhow!("Snapshot '{}' not found", name))
233    }
234
235    /// Check if a snapshot exists
236    pub fn exists(&self, snapshot_id: &str) -> bool {
237        self.snapshot_path(snapshot_id).exists()
238    }
239
240    /// Check if a snapshot with the given name exists
241    pub fn exists_by_name(&self, name: &str) -> bool {
242        self.list()
243            .map(|snapshots| snapshots.iter().any(|s| s.name == name))
244            .unwrap_or(false)
245    }
246
247    /// Get all snapshot names
248    pub fn list_names(&self) -> Result<Vec<String>> {
249        let snapshots = self.list()?;
250        Ok(snapshots.into_iter().map(|s| s.name).collect())
251    }
252}
253
254/// Filter settings by scope
255pub fn filter_settings_by_scope(settings: ClaudeSettings, scope: &SnapshotScope) -> ClaudeSettings {
256    match scope {
257        SnapshotScope::Env => ClaudeSettings {
258            environment: settings.environment,
259            ..Default::default()
260        },
261        SnapshotScope::Common => ClaudeSettings {
262            provider: settings.provider,
263            model: settings.model,
264            endpoint: settings.endpoint,
265            http: settings.http,
266            permissions: settings.permissions,
267            hooks: settings.hooks,
268            status_line: settings.status_line,
269            environment: None,
270        },
271        SnapshotScope::All => settings,
272    }
273}
274
275impl Default for SnapshotScope {
276    fn default() -> Self {
277        Self::Common
278    }
279}