use crate::settings::ClaudeSettings;
use anyhow::{Result, anyhow};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum SnapshotScope {
Env,
#[default]
Common,
All,
}
impl std::str::FromStr for SnapshotScope {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"env" => Ok(SnapshotScope::Env),
"common" => Ok(SnapshotScope::Common),
"all" => Ok(SnapshotScope::All),
_ => Err(anyhow!(
"Invalid scope '{}'. Must be one of: env, common, all",
s
)),
}
}
}
impl std::fmt::Display for SnapshotScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SnapshotScope::Env => write!(f, "env"),
SnapshotScope::Common => write!(f, "common"),
SnapshotScope::All => write!(f, "all"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub id: String,
pub name: String,
pub description: Option<String>,
pub settings: ClaudeSettings,
pub created_at: String,
pub updated_at: String,
pub scope: SnapshotScope,
pub version: u32,
}
impl Snapshot {
pub fn new(
name: String,
settings: ClaudeSettings,
scope: SnapshotScope,
description: Option<String>,
) -> Self {
let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
Self {
id: Uuid::new_v4().to_string(),
name,
description,
settings,
created_at: now.clone(),
updated_at: now,
scope,
version: 1,
}
}
pub fn touch(&mut self) {
let now = Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string();
self.updated_at = now;
}
}
#[derive(Debug, Clone)]
pub struct SnapshotStore {
pub snapshots_dir: PathBuf,
}
impl SnapshotStore {
pub fn new(snapshots_dir: PathBuf) -> Self {
Self { snapshots_dir }
}
pub fn ensure_dir(&self) -> Result<()> {
if !self.snapshots_dir.exists() {
fs::create_dir_all(&self.snapshots_dir).map_err(|e| {
anyhow!(
"Failed to create snapshots directory {}: {}",
self.snapshots_dir.display(),
e
)
})?;
}
Ok(())
}
pub fn snapshot_path(&self, snapshot_id: &str) -> PathBuf {
self.snapshots_dir.join(format!("{}.json", snapshot_id))
}
pub fn save(&self, snapshot: &Snapshot) -> Result<()> {
self.ensure_dir()?;
let path = self.snapshot_path(&snapshot.id);
let content = serde_json::to_string_pretty(snapshot)
.map_err(|e| anyhow!("Failed to serialize snapshot: {}", e))?;
fs::write(&path, content)
.map_err(|e| anyhow!("Failed to write snapshot file {}: {}", path.display(), e))?;
Ok(())
}
pub fn load(&self, snapshot_id: &str) -> Result<Snapshot> {
let path = self.snapshot_path(snapshot_id);
if !path.exists() {
return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
}
let content = fs::read_to_string(&path)
.map_err(|e| anyhow!("Failed to read snapshot file {}: {}", path.display(), e))?;
let snapshot: Snapshot = serde_json::from_str(&content)
.map_err(|e| anyhow!("Failed to parse snapshot file {}: {}", path.display(), e))?;
Ok(snapshot)
}
pub fn load_by_name(&self, name: &str) -> Result<Snapshot> {
let snapshots = self.list()?;
for snapshot in snapshots {
if snapshot.name == name {
return Ok(snapshot);
}
}
Err(anyhow!("Snapshot '{}' not found", name))
}
pub fn list(&self) -> Result<Vec<Snapshot>> {
if !self.snapshots_dir.exists() {
return Ok(Vec::new());
}
let mut snapshots = Vec::new();
for entry in fs::read_dir(&self.snapshots_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
match self.load(path.file_stem().and_then(|s| s.to_str()).unwrap_or("")) {
Ok(snapshot) => snapshots.push(snapshot),
Err(_) => {
continue;
}
}
}
}
snapshots.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(snapshots)
}
pub fn delete(&self, snapshot_id: &str) -> Result<()> {
let path = self.snapshot_path(snapshot_id);
if !path.exists() {
return Err(anyhow!("Snapshot '{}' not found", snapshot_id));
}
fs::remove_file(&path)
.map_err(|e| anyhow!("Failed to delete snapshot file {}: {}", path.display(), e))?;
Ok(())
}
pub fn delete_by_name(&self, name: &str) -> Result<()> {
let snapshots = self.list()?;
for snapshot in snapshots {
if snapshot.name == name {
return self.delete(&snapshot.id);
}
}
Err(anyhow!("Snapshot '{}' not found", name))
}
pub fn exists(&self, snapshot_id: &str) -> bool {
self.snapshot_path(snapshot_id).exists()
}
pub fn exists_by_name(&self, name: &str) -> bool {
self.list()
.map(|snapshots| snapshots.iter().any(|s| s.name == name))
.unwrap_or(false)
}
pub fn list_names(&self) -> Result<Vec<String>> {
let snapshots = self.list()?;
Ok(snapshots.into_iter().map(|s| s.name).collect())
}
}
pub fn filter_settings_by_scope(settings: ClaudeSettings, scope: &SnapshotScope) -> ClaudeSettings {
match scope {
SnapshotScope::Env => ClaudeSettings {
env: settings.env,
..Default::default()
},
SnapshotScope::Common => ClaudeSettings {
env: settings.env,
model: settings.model,
output_style: settings.output_style,
attribution: settings.attribution,
permissions: settings.permissions,
hooks: settings.hooks,
status_line: settings.status_line,
subagent_model: settings.subagent_model,
..Default::default()
},
SnapshotScope::All => settings,
}
}