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, Default)]
11pub enum SnapshotScope {
12 Env,
14 #[default]
16 Common,
17 All,
19}
20
21impl std::str::FromStr for SnapshotScope {
22 type Err = anyhow::Error;
23
24 fn from_str(s: &str) -> Result<Self> {
25 match s.to_lowercase().as_str() {
26 "env" => Ok(SnapshotScope::Env),
27 "common" => Ok(SnapshotScope::Common),
28 "all" => Ok(SnapshotScope::All),
29 _ => Err(anyhow!(
30 "Invalid scope '{}'. Must be one of: env, common, all",
31 s
32 )),
33 }
34 }
35}
36
37impl std::fmt::Display for SnapshotScope {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 SnapshotScope::Env => write!(f, "env"),
41 SnapshotScope::Common => write!(f, "common"),
42 SnapshotScope::All => write!(f, "all"),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Snapshot {
50 pub id: String,
52
53 pub name: String,
55
56 pub description: Option<String>,
58
59 pub settings: ClaudeSettings,
61
62 pub created_at: String,
64
65 pub updated_at: String,
67
68 pub scope: SnapshotScope,
70
71 pub version: u32,
73}
74
75impl Snapshot {
76 pub fn new(
78 name: String,
79 settings: ClaudeSettings,
80 scope: SnapshotScope,
81 description: Option<String>,
82 ) -> Self {
83 let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
84
85 Self {
86 id: Uuid::new_v4().to_string(),
87 name,
88 description,
89 settings,
90 created_at: now.clone(),
91 updated_at: now,
92 scope,
93 version: 1,
94 }
95 }
96
97 pub fn touch(&mut self) {
99 let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
100 self.updated_at = now;
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct SnapshotStore {
107 pub snapshots_dir: PathBuf,
109}
110
111impl SnapshotStore {
112 pub fn new(snapshots_dir: PathBuf) -> Self {
114 Self { snapshots_dir }
115 }
116
117 pub fn ensure_dir(&self) -> Result<()> {
119 if !self.snapshots_dir.exists() {
120 fs::create_dir_all(&self.snapshots_dir).map_err(|e| {
121 anyhow!(
122 "Failed to create snapshots directory {}: {}",
123 self.snapshots_dir.display(),
124 e
125 )
126 })?;
127 }
128 Ok(())
129 }
130
131 pub fn snapshot_path(&self, snapshot_id: &str) -> PathBuf {
133 self.snapshots_dir.join(format!("{}.json", snapshot_id))
134 }
135
136 pub fn save(&self, snapshot: &Snapshot) -> Result<()> {
138 self.ensure_dir()?;
139
140 let path = self.snapshot_path(&snapshot.id);
141 let content = serde_json::to_string_pretty(snapshot)
142 .map_err(|e| anyhow!("Failed to serialize snapshot: {}", e))?;
143
144 fs::write(&path, content)
145 .map_err(|e| anyhow!("Failed to write snapshot file {}: {}", path.display(), e))?;
146
147 Ok(())
148 }
149
150 pub fn load(&self, snapshot_id: &str) -> Result<Snapshot> {
152 let path = self.snapshot_path(snapshot_id);
153
154 if !path.exists() {
155 return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
156 }
157
158 let content = fs::read_to_string(&path)
159 .map_err(|e| anyhow!("Failed to read snapshot file {}: {}", path.display(), e))?;
160
161 let snapshot: Snapshot = serde_json::from_str(&content)
162 .map_err(|e| anyhow!("Failed to parse snapshot file {}: {}", path.display(), e))?;
163
164 Ok(snapshot)
165 }
166
167 pub fn load_by_name(&self, name: &str) -> Result<Snapshot> {
169 let snapshots = self.list()?;
170
171 for snapshot in snapshots {
172 if snapshot.name == name {
173 return Ok(snapshot);
174 }
175 }
176
177 Err(anyhow!("Snapshot '{}' not found", name))
178 }
179
180 pub fn list(&self) -> Result<Vec<Snapshot>> {
182 if !self.snapshots_dir.exists() {
183 return Ok(Vec::new());
184 }
185
186 let mut snapshots = Vec::new();
187
188 for entry in fs::read_dir(&self.snapshots_dir)? {
189 let entry = entry?;
190 let path = entry.path();
191
192 if path.extension().and_then(|s| s.to_str()) == Some("json") {
193 match self.load(path.file_stem().and_then(|s| s.to_str()).unwrap_or("")) {
194 Ok(snapshot) => snapshots.push(snapshot),
195 Err(_) => {
196 continue;
198 }
199 }
200 }
201 }
202
203 snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
205
206 Ok(snapshots)
207 }
208
209 pub fn delete(&self, snapshot_id: &str) -> Result<()> {
211 let path = self.snapshot_path(snapshot_id);
212
213 if !path.exists() {
214 return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
215 }
216
217 fs::remove_file(&path)
218 .map_err(|e| anyhow!("Failed to delete snapshot file {}: {}", path.display(), e))?;
219
220 Ok(())
221 }
222
223 pub fn delete_by_name(&self, name: &str) -> Result<()> {
225 let snapshots = self.list()?;
226
227 for snapshot in snapshots {
228 if snapshot.name == name {
229 return self.delete(&snapshot.id);
230 }
231 }
232
233 Err(anyhow!("Snapshot '{}' not found", name))
234 }
235
236 pub fn exists(&self, snapshot_id: &str) -> bool {
238 self.snapshot_path(snapshot_id).exists()
239 }
240
241 pub fn exists_by_name(&self, name: &str) -> bool {
243 self.list()
244 .map(|snapshots| snapshots.iter().any(|s| s.name == name))
245 .unwrap_or(false)
246 }
247
248 pub fn list_names(&self) -> Result<Vec<String>> {
250 let snapshots = self.list()?;
251 Ok(snapshots.into_iter().map(|s| s.name).collect())
252 }
253}
254
255pub fn filter_settings_by_scope(settings: ClaudeSettings, scope: &SnapshotScope) -> ClaudeSettings {
257 match scope {
258 SnapshotScope::Env => ClaudeSettings {
259 env: settings.env,
260 ..Default::default()
261 },
262 SnapshotScope::Common => ClaudeSettings {
263 env: settings.env,
264 model: settings.model,
265 output_style: settings.output_style,
266 include_co_authored_by: settings.include_co_authored_by,
267 permissions: settings.permissions,
268 hooks: settings.hooks,
269 status_line: settings.status_line,
270 subagent_model: settings.subagent_model,
271 ..Default::default()
272 },
273 SnapshotScope::All => settings,
274 }
275}