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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub enum SnapshotScope {
12 Env,
14 Common,
16 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#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Snapshot {
49 pub id: String,
51
52 pub name: String,
54
55 pub description: Option<String>,
57
58 pub settings: ClaudeSettings,
60
61 pub created_at: String,
63
64 pub updated_at: String,
66
67 pub scope: SnapshotScope,
69
70 pub version: u32,
72}
73
74impl Snapshot {
75 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 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#[derive(Debug, Clone)]
105pub struct SnapshotStore {
106 pub snapshots_dir: PathBuf,
108}
109
110impl SnapshotStore {
111 pub fn new(snapshots_dir: PathBuf) -> Self {
113 Self { snapshots_dir }
114 }
115
116 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 pub fn snapshot_path(&self, snapshot_id: &str) -> PathBuf {
132 self.snapshots_dir.join(format!("{}.json", snapshot_id))
133 }
134
135 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 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 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 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 continue;
197 }
198 }
199 }
200 }
201
202 snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
204
205 Ok(snapshots)
206 }
207
208 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 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 pub fn exists(&self, snapshot_id: &str) -> bool {
237 self.snapshot_path(snapshot_id).exists()
238 }
239
240 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 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
254pub 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}