use crate::prompt::inbox::INBOX_DIR;
use crate::prompt::step::STEPS_DIR;
use crate::template::GitRunner;
use crate::workspace::{self, AGENTS_DIR};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
mod subtree;
use subtree::{mark_refs, pending, require_quiescent, subtree};
const DIRS: [&str; 3] = [AGENTS_DIR, STEPS_DIR, INBOX_DIR];
#[derive(Debug)]
pub struct DeleteReport {
pub agent: String,
pub descendants: Vec<String>,
pub pending_deposits: usize,
pub removed: bool,
}
impl std::fmt::Display for DeleteReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mood = match self.removed {
true => "deleted",
false => "would delete",
};
let named = match self.descendants.is_empty() {
true => String::new(),
false => format!(" ({})", self.descendants.join(", ")),
};
write!(
f,
"{mood} {}; descendants: {}{named}; pending deposits: {}",
self.agent,
self.descendants.len(),
self.pending_deposits
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum DeleteError {
#[error(transparent)]
Layout(#[from] workspace::LayoutError),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("git {op}: {source}")]
Git {
op: &'static str,
#[source]
source: io::Error,
},
#[error(
"agent {id:?} has {} descendant(s) — {}; deleting them is not implied, \
pass --children to remove the whole subtree (ARCH §2.3, §9.2)",
.descendants.len(),
crate::name::pool(.descendants)
)]
HasDescendants {
id: String,
descendants: Vec<String>,
},
#[error(
"agent {id:?} is being driven — an executor holds its lock at {lock} \
(ARCH §2.11); stop it first (`lernie stop`) and delete once it is quiescent"
)]
Driven { id: String, lock: PathBuf },
#[error("probe executor lock at {path}: {source}")]
Probe {
path: PathBuf,
#[source]
source: io::Error,
},
}
pub fn delete(
ws: &Path,
agent_id: &str,
children: bool,
dry_run: bool,
git: &dyn GitRunner,
) -> Result<DeleteReport, DeleteError> {
workspace::require(ws)?;
let repo = workspace::repo_git(ws);
let marks = mark_refs(&repo, git)?;
let targets = subtree(ws, agent_id, &marks, git)?;
let descendants: Vec<String> = targets
.iter()
.filter(|id| *id != agent_id)
.cloned()
.collect();
if !children && !descendants.is_empty() {
return Err(DeleteError::HasDescendants {
id: agent_id.to_owned(),
descendants,
});
}
let mut pending_deposits = 0;
for id in &targets {
require_quiescent(ws, id)?;
pending_deposits += pending(ws, id)?;
}
let report = DeleteReport {
agent: agent_id.to_owned(),
descendants,
pending_deposits,
removed: !dry_run,
};
if dry_run {
return Ok(report);
}
for id in &targets {
for dir in DIRS {
remove_dir(&ws.join(dir).join(id))?;
}
}
run(git, &repo, &["worktree", "prune"], "worktree prune")?;
for spec in ref_specs(&targets, &marks) {
run(git, &repo, &["update-ref", "-d", &spec], "update-ref -d")?;
}
Ok(report)
}
fn ref_specs(targets: &[String], marks: &[String]) -> Vec<String> {
let mut specs: Vec<String> = targets
.iter()
.map(|id| format!("refs/heads/{}", workspace::agent_ref(id)))
.collect();
specs.extend(
marks
.iter()
.filter(|r| targets.iter().any(|id| r.ends_with(&format!("/{id}"))))
.cloned(),
);
specs
}
fn remove_dir(path: &Path) -> io::Result<()> {
match fs::remove_dir_all(path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
other => other,
}
}
fn run(
git: &dyn GitRunner,
repo: &Path,
args: &[&str],
op: &'static str,
) -> Result<(), DeleteError> {
git.run(repo, args)
.map_err(|source| DeleteError::Git { op, source })
}