use std::path::PathBuf;
use std::sync::Mutex;
use std::time::SystemTime;
use anyhow::{Context, Result};
use serde_json::{Map, Value};
use crate::profile::{atomic_write, clauth_dir, home_dir};
const PER_PROFILE_FIELDS: &[&str] = &[
"oauthAccount",
"overageCreditGrantCache",
"passesEligibilityCache",
"passesLastSeenRemaining",
"cachedExtraUsageDisabledReason",
];
static LAST_SYNCED: Mutex<Option<SystemTime>> = Mutex::new(None);
fn is_per_profile(key: &str) -> bool {
PER_PROFILE_FIELDS.contains(&key)
}
fn known_paths() -> Result<Vec<PathBuf>> {
let mut paths = vec![home_dir()?.join(".claude.json")];
let profiles = clauth_dir()?.join("profiles");
if let Ok(entries) = std::fs::read_dir(&profiles) {
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|t| t.is_dir()) {
paths.push(entry.path().join("runtime").join(".claude.json"));
}
}
}
Ok(paths)
}
pub(crate) fn sync_once() -> Result<()> {
let paths = known_paths()?;
let newest = paths
.iter()
.filter_map(|p| std::fs::metadata(p).ok()?.modified().ok())
.max();
let Some(newest) = newest else {
return Ok(());
};
{
let last = LAST_SYNCED.lock().unwrap_or_else(|p| p.into_inner());
if last.is_some_and(|l| newest <= l) {
return Ok(());
}
}
sync_paths(&paths)?;
*LAST_SYNCED.lock().unwrap_or_else(|p| p.into_inner()) = Some(newest);
Ok(())
}
struct Member {
path: PathBuf,
mtime: SystemTime,
obj: Map<String, Value>,
}
fn sync_paths(paths: &[PathBuf]) -> Result<()> {
let mut members: Vec<Member> = Vec::new();
for path in paths {
let Ok(meta) = std::fs::metadata(path) else {
continue;
};
let Ok(mtime) = meta.modified() else {
continue;
};
let Ok(bytes) = std::fs::read(path) else {
continue;
};
let Ok(Value::Object(obj)) = serde_json::from_slice::<Value>(&bytes) else {
continue;
};
members.push(Member {
path: path.clone(),
mtime,
obj,
});
}
if members.len() < 2 {
return Ok(());
}
let winner = members
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.mtime.cmp(&b.mtime).then_with(|| a.path.cmp(&b.path)))
.map(|(i, _)| i)
.expect("members is non-empty");
let shared: Map<String, Value> = members[winner]
.obj
.iter()
.filter(|(k, _)| !is_per_profile(k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for (i, member) in members.iter().enumerate() {
if i == winner {
continue;
}
let mut merged = member.obj.clone();
merged.retain(|k, _| is_per_profile(k) || shared.contains_key(k));
for (k, v) in &shared {
merged.insert(k.clone(), v.clone());
}
if merged == member.obj {
continue;
}
let bytes = serde_json::to_vec_pretty(&Value::Object(merged))
.context("failed to serialize merged .claude.json")?;
atomic_write(&member.path, &bytes)
.with_context(|| format!("failed to write {}", member.path.display()))?;
}
Ok(())
}
#[cfg(test)]
#[path = "../tests/inline/claude_json.rs"]
mod tests;