1use std::path::PathBuf;
9
10use anyhow::Result;
11use serde::Serialize;
12
13#[derive(Debug, Serialize)]
16pub struct NukePlan {
17 pub paths: Vec<PathBuf>,
19 pub mcp_files: Vec<PathBuf>,
22 pub purge_binary: bool,
24}
25
26impl NukePlan {
27 pub fn compute(purge: bool) -> Result<Self> {
29 let mut paths = Vec::new();
30 for p in [
32 crate::config::config_dir().ok(),
33 crate::config::state_dir().ok(),
34 crate::session::sessions_root().ok(),
35 dirs::cache_dir().map(|c| c.join("wire")),
36 ]
37 .into_iter()
38 .flatten()
39 {
40 if p.exists() && !paths.contains(&p) {
41 paths.push(p);
42 }
43 }
44 let mut mcp_files = Vec::new();
46 for adapter in crate::adapters::harness::HARNESS_ADAPTERS {
47 for path in (adapter.paths_fn)() {
48 if path.exists() && !mcp_files.contains(&path) {
49 mcp_files.push(path);
50 }
51 }
52 }
53 Ok(NukePlan {
54 paths,
55 mcp_files,
56 purge_binary: purge,
57 })
58 }
59
60 pub fn execute(&self) -> Result<NukeReport> {
65 let mut r = NukeReport::default();
66
67 for kind in [
69 crate::service::ServiceKind::Daemon,
70 crate::service::ServiceKind::LocalRelay,
71 ] {
72 match crate::service::uninstall_kind(kind) {
73 Ok(rep) => r.removed_units.push(format!("{kind:?}: {}", rep.platform)),
74 Err(e) => r.warnings.push(format!("uninstall {kind:?}: {e:#}")),
75 }
76 }
77
78 'files: for path in &self.mcp_files {
83 for adapter in crate::adapters::harness::HARNESS_ADAPTERS {
84 match (adapter.remove_fn)(path, "wire") {
85 Ok(true) => {
86 r.removed_mcp_entries.push(path.clone());
87 continue 'files;
88 }
89 Ok(false) => {}
90 Err(e) => {
91 r.warnings
92 .push(format!("mcp de-register {}: {e:#}", path.display()));
93 continue 'files;
94 }
95 }
96 }
97 }
98
99 for p in &self.paths {
101 match std::fs::remove_dir_all(p) {
102 Ok(()) => r.removed_paths.push(p.clone()),
103 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
104 Err(e) => r.warnings.push(format!("rm {}: {e:#}", p.display())),
105 }
106 }
107
108 Ok(r)
109 }
110}
111
112pub fn should_proceed(force: bool, is_tty: bool, read_line: impl FnOnce() -> String) -> bool {
116 if force {
117 return true;
118 }
119 if !is_tty {
120 return false;
121 }
122 read_line().trim() == "nuke"
123}
124
125#[derive(Debug, Default, Serialize)]
127pub struct NukeReport {
128 pub removed_paths: Vec<PathBuf>,
129 pub removed_mcp_entries: Vec<PathBuf>,
130 pub removed_units: Vec<String>,
131 pub killed_pids: Vec<u32>,
132 pub binary_removed: bool,
133 pub warnings: Vec<String>,
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn plan_lists_existing_wire_dirs_only() {
143 crate::config::test_support::with_temp_home(|| {
144 crate::config::ensure_dirs().unwrap();
146 let plan = NukePlan::compute(false).unwrap();
147 assert!(
150 plan.paths.iter().any(|p| p.ends_with("wire")),
151 "expected a wire dir in {:?}",
152 plan.paths
153 );
154 assert!(
155 !plan.purge_binary,
156 "default plan does not remove the binary"
157 );
158 });
159 }
160
161 #[test]
162 fn purge_plan_sets_binary_removal() {
163 crate::config::test_support::with_temp_home(|| {
164 let plan = NukePlan::compute(true).unwrap();
165 assert!(plan.purge_binary);
166 });
167 }
168
169 #[test]
170 fn confirm_logic() {
171 assert!(should_proceed(
173 true,
174 false,
175 || unreachable!()
176 ));
177 assert!(!should_proceed(false, false, String::new));
179 assert!(should_proceed(false, true, || "nuke".to_string()));
181 assert!(!should_proceed(false, true, || "no".to_string()));
182 assert!(!should_proceed(false, true, || "NUKE".to_string()));
183 }
184
185 #[test]
186 fn execute_removes_dirs_and_mcp_entry() {
187 crate::config::test_support::with_temp_home(|| {
188 crate::config::ensure_dirs().unwrap();
189 let state = crate::config::state_dir().unwrap();
190 assert!(state.exists());
191 let mcp =
193 std::path::PathBuf::from(std::env::var("WIRE_HOME").unwrap()).join("mcp.json");
194 std::fs::write(&mcp, r#"{"mcpServers":{"wire":{"command":"wire"}}}"#).unwrap();
195 let plan = NukePlan {
196 paths: vec![state.clone()],
197 mcp_files: vec![mcp.clone()],
198 purge_binary: false,
199 };
200 let report = plan.execute().unwrap();
201 assert!(!state.exists(), "state dir deleted");
202 let v: serde_json::Value =
203 serde_json::from_slice(&std::fs::read(&mcp).unwrap()).unwrap();
204 assert!(v["mcpServers"].get("wire").is_none(), "wire de-registered");
205 assert!(report.removed_paths.contains(&state));
206 assert!(report.removed_mcp_entries.contains(&mcp));
207 });
208 }
209}