use std::path::PathBuf;
use anyhow::Result;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct NukePlan {
pub paths: Vec<PathBuf>,
pub mcp_files: Vec<PathBuf>,
pub purge_binary: bool,
}
impl NukePlan {
pub fn compute(purge: bool) -> Result<Self> {
let mut paths = Vec::new();
for p in [
crate::config::config_dir().ok(),
crate::config::state_dir().ok(),
crate::session::sessions_root().ok(),
dirs::cache_dir().map(|c| c.join("wire")),
]
.into_iter()
.flatten()
{
if p.exists() && !paths.contains(&p) {
paths.push(p);
}
}
let mut mcp_files = Vec::new();
for adapter in crate::adapters::harness::HARNESS_ADAPTERS {
for path in (adapter.paths_fn)() {
if path.exists() && !mcp_files.contains(&path) {
mcp_files.push(path);
}
}
}
Ok(NukePlan {
paths,
mcp_files,
purge_binary: purge,
})
}
pub fn execute(&self) -> Result<NukeReport> {
self.execute_with(|kind| crate::service::uninstall_kind(kind).map(|rep| rep.platform))
}
fn execute_with<U>(&self, uninstall_unit: U) -> Result<NukeReport>
where
U: Fn(crate::service::ServiceKind) -> Result<String>,
{
let mut r = NukeReport::default();
for kind in [
crate::service::ServiceKind::Daemon,
crate::service::ServiceKind::LocalRelay,
] {
match uninstall_unit(kind) {
Ok(platform) => r.removed_units.push(format!("{kind:?}: {platform}")),
Err(e) => r.warnings.push(format!("uninstall {kind:?}: {e:#}")),
}
}
'files: for path in &self.mcp_files {
for adapter in crate::adapters::harness::HARNESS_ADAPTERS {
match (adapter.remove_fn)(path, "wire") {
Ok(true) => {
r.removed_mcp_entries.push(path.clone());
continue 'files;
}
Ok(false) => {}
Err(e) => {
r.warnings
.push(format!("mcp de-register {}: {e:#}", path.display()));
continue 'files;
}
}
}
}
for p in &self.paths {
match std::fs::remove_dir_all(p) {
Ok(()) => r.removed_paths.push(p.clone()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => r.warnings.push(format!("rm {}: {e:#}", p.display())),
}
}
Ok(r)
}
}
pub fn should_proceed(force: bool, is_tty: bool, read_line: impl FnOnce() -> String) -> bool {
if force {
return true;
}
if !is_tty {
return false;
}
read_line().trim() == "nuke"
}
pub fn parse_registry_bindings(bytes: &[u8]) -> Vec<(String, String)> {
let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) else {
return Vec::new();
};
let Some(by_cwd) = v.get("by_cwd").and_then(|m| m.as_object()) else {
return Vec::new();
};
by_cwd
.iter()
.filter_map(|(cwd, name)| name.as_str().map(|n| (cwd.clone(), n.to_string())))
.collect()
}
pub fn default_registry_bindings() -> Vec<(String, String)> {
let Ok(root) = crate::session::default_sessions_root() else {
return Vec::new();
};
match std::fs::read(root.join("registry.json")) {
Ok(bytes) => parse_registry_bindings(&bytes),
Err(_) => Vec::new(),
}
}
pub fn host_guard_refusal(bound: &[(String, String)], really: bool) -> Option<String> {
if really || bound.is_empty() {
return None;
}
let mut msg = format!(
"refusing to nuke: this machine has a live wire install ({} registry-bound session(s)):\n",
bound.len()
);
for (cwd, name) in bound {
msg.push_str(&format!(" {name} ← {cwd}\n"));
}
msg.push_str(
"nuke removes launchd/systemd units, MCP registrations, and kills every wire daemon \
machine-wide — even when WIRE_HOME points elsewhere.\n\
If you really mean this machine, re-run with --really-this-machine.",
);
Some(msg)
}
#[derive(Debug, Default, Serialize)]
pub struct NukeReport {
pub removed_paths: Vec<PathBuf>,
pub removed_mcp_entries: Vec<PathBuf>,
pub removed_units: Vec<String>,
pub killed_pids: Vec<u32>,
pub binary_removed: bool,
pub warnings: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_lists_existing_wire_dirs_only() {
crate::config::test_support::with_temp_home(|| {
crate::config::ensure_dirs().unwrap();
let plan = NukePlan::compute(false).unwrap();
assert!(
plan.paths.iter().any(|p| p.ends_with("wire")),
"expected a wire dir in {:?}",
plan.paths
);
assert!(
!plan.purge_binary,
"default plan does not remove the binary"
);
});
}
#[test]
fn purge_plan_sets_binary_removal() {
crate::config::test_support::with_temp_home(|| {
let plan = NukePlan::compute(true).unwrap();
assert!(plan.purge_binary);
});
}
#[test]
fn confirm_logic() {
assert!(should_proceed(
true,
false,
|| unreachable!()
));
assert!(!should_proceed(false, false, String::new));
assert!(should_proceed(false, true, || "nuke".to_string()));
assert!(!should_proceed(false, true, || "no".to_string()));
assert!(!should_proceed(false, true, || "NUKE".to_string()));
}
#[test]
fn execute_removes_dirs_and_mcp_entry() {
crate::config::test_support::with_temp_home(|| {
crate::config::ensure_dirs().unwrap();
let state = crate::config::state_dir().unwrap();
assert!(state.exists());
let mcp =
std::path::PathBuf::from(std::env::var("WIRE_HOME").unwrap()).join("mcp.json");
std::fs::write(&mcp, r#"{"mcpServers":{"wire":{"command":"wire"}}}"#).unwrap();
let plan = NukePlan {
paths: vec![state.clone()],
mcp_files: vec![mcp.clone()],
purge_binary: false,
};
let report = plan.execute_with(|_kind| Ok("stub".to_string())).unwrap();
assert_eq!(report.removed_units.len(), 2, "both unit kinds attempted");
assert!(!state.exists(), "state dir deleted");
let v: serde_json::Value =
serde_json::from_slice(&std::fs::read(&mcp).unwrap()).unwrap();
assert!(v["mcpServers"].get("wire").is_none(), "wire de-registered");
assert!(report.removed_paths.contains(&state));
assert!(report.removed_mcp_entries.contains(&mcp));
});
}
#[test]
fn host_guard_silent_with_no_bindings() {
assert_eq!(host_guard_refusal(&[], false), None);
assert_eq!(host_guard_refusal(&[], true), None);
}
#[test]
fn host_guard_refuses_bound_machine_without_flag() {
let bound = vec![(
"/Users/op/Source/wire".to_string(),
"slancha-wire".to_string(),
)];
let msg = host_guard_refusal(&bound, false).expect("guard must refuse");
assert!(msg.contains("slancha-wire"));
assert!(msg.contains("/Users/op/Source/wire"));
assert!(msg.contains("--really-this-machine"));
}
#[test]
fn host_guard_passes_with_explicit_flag() {
let bound = vec![("/x".to_string(), "s".to_string())];
assert_eq!(host_guard_refusal(&bound, true), None);
}
#[test]
fn registry_bindings_parse_shapes() {
let bytes = br#"{"by_cwd":{"/a":"one","/b":"two"}}"#;
let mut got = parse_registry_bindings(bytes);
got.sort();
assert_eq!(
got,
vec![
("/a".to_string(), "one".to_string()),
("/b".to_string(), "two".to_string())
]
);
assert!(parse_registry_bindings(br#"{"by_cwd":{}}"#).is_empty());
assert!(parse_registry_bindings(br"{}").is_empty());
assert!(parse_registry_bindings(br#"{"by_cwd":42}"#).is_empty());
assert!(parse_registry_bindings(b"not json").is_empty());
assert_eq!(
parse_registry_bindings(br#"{"by_cwd":{"/a":1,"/b":"two"}}"#),
vec![("/b".to_string(), "two".to_string())]
);
}
}