use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use crate::paths::claude_json_path;
use crate::utils::atomic::atomic_write;
pub fn prune_project(workbench: &Path) -> io::Result<bool> {
prune_project_at(claude_json_path(), workbench)
}
fn prune_project_at(claude_json: Option<PathBuf>, workbench: &Path) -> io::Result<bool> {
let Some(claude_json) = claude_json else {
return Ok(false);
};
if !claude_json.exists() {
return Ok(false);
}
let lock_path = lock_path_for(&claude_json);
let lock_file = File::create(&lock_path)?;
lock_exclusive(&lock_file)?;
let removed = prune_locked(&claude_json, workbench);
let _ = unlock(&lock_file);
removed
}
fn prune_locked(claude_json: &Path, workbench: &Path) -> io::Result<bool> {
let raw = fs::read_to_string(claude_json)?;
let mut document: serde_json::Value = serde_json::from_str(&raw)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
let key = workbench.to_string_lossy().into_owned();
let removed = document
.get_mut("projects")
.and_then(serde_json::Value::as_object_mut)
.is_some_and(|projects| projects.remove(&key).is_some());
if removed {
let bytes = serialize_document(&document)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
atomic_write(claude_json, &bytes)?;
}
Ok(removed)
}
#[cfg(test)]
const SERIALIZE_FAIL_ENV: &str = "MOADIM_CLAUDE_JSON_SERIALIZE_FAIL_FOR_TEST";
fn serialize_document(document: &serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
#[cfg(test)]
if std::env::var_os(SERIALIZE_FAIL_ENV).is_some() {
use serde::ser::Error as _;
return Err(serde_json::Error::custom(
"forced serialize failure for test",
));
}
serde_json::to_vec(document)
}
fn lock_path_for(claude_json: &Path) -> PathBuf {
let mut lock = claude_json.as_os_str().to_owned();
lock.push(".lock");
PathBuf::from(lock)
}
#[cfg(unix)]
fn lock_exclusive(file: &File) -> io::Result<()> {
use std::os::fd::AsRawFd;
let outcome = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if outcome == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(unix))]
fn lock_exclusive(_file: &File) -> io::Result<()> {
Ok(())
}
#[cfg(unix)]
fn unlock(file: &File) -> io::Result<()> {
use std::os::fd::AsRawFd;
let outcome = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
if outcome == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(unix))]
fn unlock(_file: &File) -> io::Result<()> {
Ok(())
}
#[cfg(test)]
#[path = "claude_json_tests.rs"]
mod claude_json_tests;