use anyhow::{Context, Result, anyhow};
use serde_json::{Map, Value};
use std::io::Write;
use std::path::Path;
pub struct ClaudeJson {
pub raw: String,
pub data: Value,
}
impl ClaudeJson {
pub fn load(path: &Path) -> Result<Self> {
let raw =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let data: Value =
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
Ok(Self { raw, data })
}
pub fn projects(&self) -> Option<&Map<String, Value>> {
self.data.get("projects").and_then(Value::as_object)
}
pub fn projects_mut(&mut self) -> Option<&mut Map<String, Value>> {
self.data.get_mut("projects").and_then(Value::as_object_mut)
}
}
pub fn render(data: &Value) -> Result<String> {
serde_json::to_string_pretty(data).map_err(|e| anyhow!("serialize: {e}"))
}
pub fn write_atomic(path: &Path, contents: &str) -> Result<()> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("claude.json");
let tmp = dir.join(format!(".{name}.tmp-{}", std::process::id()));
{
let mut f = std::fs::File::create(&tmp)
.with_context(|| format!("create temp {}", tmp.display()))?;
f.write_all(contents.as_bytes())
.with_context(|| format!("write temp {}", tmp.display()))?;
f.sync_all()
.with_context(|| format!("sync temp {}", tmp.display()))?;
}
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(anyhow!(
"rename {} -> {}: {e}",
tmp.display(),
path.display()
));
}
Ok(())
}