Skip to content

Commit 4712ed8

Browse files
authored
Merge pull request #137 from stephenleo/story-9-2-align-windows-installer-and-uninstaller-paths
fix(uninstall): align Windows uninstaller paths with installer
2 parents 3f6158f + a99f123 commit 4712ed8

File tree

1 file changed

+100
-5
lines changed

1 file changed

+100
-5
lines changed

src/uninstall.rs

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ pub fn run() {
1616

1717
fn remove_binary(home: &std::path::Path) {
1818
#[cfg(not(target_os = "windows"))]
19-
let candidates = [home.join(".local/bin/cship"), home.join(".cargo/bin/cship")];
19+
let candidates = vec![home.join(".local/bin/cship"), home.join(".cargo/bin/cship")];
2020
#[cfg(target_os = "windows")]
21-
let candidates = [
22-
home.join(".cargo/bin/cship.exe"),
23-
home.join(r".local\bin\cship.exe"),
24-
];
21+
let candidates = {
22+
let mut v: Vec<std::path::PathBuf> = Vec::new();
23+
match std::env::var("LOCALAPPDATA") {
24+
Ok(local_app_data) => {
25+
v.push(
26+
std::path::Path::new(&local_app_data)
27+
.join("Programs")
28+
.join("cship")
29+
.join("cship.exe"),
30+
);
31+
}
32+
Err(_) => {
33+
tracing::warn!(
34+
"LOCALAPPDATA env var not set; skipping %LOCALAPPDATA%\\Programs\\cship\\cship.exe candidate"
35+
);
36+
}
37+
}
38+
v.push(home.join(".cargo/bin/cship.exe"));
39+
v.push(home.join(r".local\bin\cship.exe"));
40+
v
41+
};
2542
for bin in candidates {
2643
if bin.exists() {
2744
match std::fs::remove_file(&bin) {
@@ -35,7 +52,21 @@ fn remove_binary(home: &std::path::Path) {
3552
}
3653

3754
fn remove_statusline_from_settings(home: &std::path::Path) {
55+
#[cfg(target_os = "windows")]
56+
let path = match std::env::var("APPDATA") {
57+
Ok(app_data) => std::path::Path::new(&app_data)
58+
.join("Claude")
59+
.join("settings.json"),
60+
Err(_) => {
61+
tracing::warn!(
62+
"APPDATA env var not set; falling back to ~/.claude/settings.json for settings path"
63+
);
64+
home.join(".claude/settings.json")
65+
}
66+
};
67+
#[cfg(not(target_os = "windows"))]
3868
let path = home.join(".claude/settings.json");
69+
3970
if !path.exists() {
4071
println!("settings.json not found — skipping.");
4172
return;
@@ -131,9 +162,35 @@ mod tests {
131162
let cargo_path = cargo_bin.join(bin_name);
132163
std::fs::write(&cargo_path, b"fake binary").unwrap();
133164

165+
// On Windows, also test the LOCALAPPDATA candidate path
166+
#[cfg(target_os = "windows")]
167+
let (tmp_local, localappdata_bin_path) = {
168+
let tmp = tempfile::tempdir().unwrap();
169+
let programs_dir = tmp.path().join("Programs").join("cship");
170+
std::fs::create_dir_all(&programs_dir).unwrap();
171+
let bin_path = programs_dir.join("cship.exe");
172+
std::fs::write(&bin_path, b"fake binary").unwrap();
173+
// Point LOCALAPPDATA to our temp dir so remove_binary finds it
174+
// SAFETY: guarded by HOME_MUTEX; no other threads read LOCALAPPDATA concurrently.
175+
unsafe { std::env::set_var("LOCALAPPDATA", tmp.path()) };
176+
(tmp, bin_path)
177+
};
178+
134179
remove_binary(home);
180+
135181
assert!(!local_path.exists());
136182
assert!(!cargo_path.exists());
183+
184+
#[cfg(target_os = "windows")]
185+
{
186+
assert!(
187+
!localappdata_bin_path.exists(),
188+
"LOCALAPPDATA binary should be removed on Windows"
189+
);
190+
// SAFETY: guarded by HOME_MUTEX; no other threads read LOCALAPPDATA concurrently.
191+
unsafe { std::env::remove_var("LOCALAPPDATA") };
192+
drop(tmp_local);
193+
}
137194
});
138195
}
139196

@@ -171,6 +228,43 @@ mod tests {
171228
});
172229
}
173230

231+
#[test]
232+
#[cfg(target_os = "windows")]
233+
fn test_remove_statusline_uses_appdata_on_windows() {
234+
with_tempdir(|home| {
235+
// Create a temp APPDATA directory with Claude/settings.json
236+
let tmp_appdata = tempfile::tempdir().unwrap();
237+
let claude_dir = tmp_appdata.path().join("Claude");
238+
std::fs::create_dir_all(&claude_dir).unwrap();
239+
let settings_path = claude_dir.join("settings.json");
240+
std::fs::write(
241+
&settings_path,
242+
r#"{"statusline":"cship","otherKey":"value"}"#,
243+
)
244+
.unwrap();
245+
246+
// SAFETY: guarded by HOME_MUTEX; no other threads read APPDATA concurrently.
247+
unsafe { std::env::set_var("APPDATA", tmp_appdata.path()) };
248+
249+
remove_statusline_from_settings(home);
250+
251+
let content = std::fs::read_to_string(&settings_path).unwrap();
252+
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
253+
assert!(
254+
parsed.get("statusline").is_none(),
255+
"statusline key should be removed from APPDATA path on Windows"
256+
);
257+
assert_eq!(
258+
parsed.get("otherKey").and_then(|v| v.as_str()),
259+
Some("value"),
260+
"other keys should be preserved"
261+
);
262+
263+
// SAFETY: guarded by HOME_MUTEX; no other threads read APPDATA concurrently.
264+
unsafe { std::env::remove_var("APPDATA") };
265+
});
266+
}
267+
174268
#[test]
175269
fn test_remove_statusline_absent_key() {
176270
with_tempdir(|home| {
@@ -244,6 +338,7 @@ mod tests {
244338
// Should print message and return, not panic or touch root paths
245339
run();
246340
// Restore to avoid poisoning other tests
341+
// SAFETY: guarded by HOME_MUTEX; no other threads read CLAUDE_HOME concurrently.
247342
unsafe { std::env::remove_var("CLAUDE_HOME") };
248343
}
249344
}

0 commit comments

Comments
 (0)