use std::fs;
use std::io::Write as _;
use std::path::PathBuf;
use anyhow::{Context, Result};
pub async fn run(workspace: PathBuf) -> Result<()> {
let pitboss_dir = workspace.join(".pitboss");
if !pitboss_dir.is_dir() {
anyhow::bail!(
"nothing to nuke: {:?} has no `.pitboss/` directory.",
workspace
);
}
println!();
println!(" About to delete:");
println!(" {}", pitboss_dir.display());
if let Ok(entries) = fs::read_dir(&pitboss_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let suffix = if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
"/"
} else {
""
};
println!(" .pitboss/{}{}", name, suffix);
}
}
println!();
println!(" This removes all config, plan content, deferred items, run state,");
println!(" agent logs, and grind state. The action cannot be undone.");
println!();
if !confirm()? {
eprintln!("nuke cancelled — nothing removed");
return Ok(());
}
fs::remove_dir_all(&pitboss_dir)
.with_context(|| format!("nuke: removing {:?}", pitboss_dir))?;
println!();
println!(" Pitboss deleted from project.");
println!(" Run `pitboss config` to add it again.");
println!();
Ok(())
}
fn confirm() -> Result<bool> {
if !is_interactive() {
anyhow::bail!("pitboss nuke requires an interactive terminal to confirm the deletion.");
}
print!(" Are you sure you want to delete? (y/N): ");
std::io::stdout().flush().ok();
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.context("nuke: reading confirmation")?;
let answer = line.trim().to_lowercase();
Ok(answer.starts_with('y'))
}
#[cfg(not(test))]
fn is_interactive() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal()
}
#[cfg(test)]
fn is_interactive() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn bails_when_no_pitboss_dir() {
let dir = tempdir().unwrap();
let err = run(dir.path().to_path_buf()).await.unwrap_err();
assert!(
err.to_string().contains("nothing to nuke"),
"expected 'nothing to nuke' message, got: {err}"
);
}
#[tokio::test]
async fn refuses_non_interactive_when_pitboss_exists() {
let dir = tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".pitboss/play")).unwrap();
let err = run(dir.path().to_path_buf()).await.unwrap_err();
assert!(
err.to_string().contains("interactive terminal"),
"expected interactive-terminal bail, got: {err}"
);
assert!(dir.path().join(".pitboss").is_dir());
}
}