1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
use anyhow::{Result, anyhow};
use console::style;
use crate::agent_setup::{self, Agent};
use crate::skills;
use crate::xdg;
/// Run the uninstall process.
///
/// Removes status tracking hooks, skills, and XDG cache/state dirs for
/// all known agents, regardless of detection. This is a hidden
/// implementation detail for the uninstall shell script -- not meant for
/// end users directly.
pub fn run(dry_run: bool) -> Result<()> {
if dry_run {
println!(" {} Dry run mode", style("●").dim());
}
println!();
let mut errors: Vec<String> = Vec::new();
// Phase 1: Status tracking hooks (try ALL agents, not just detected ones)
println!(" {} Status tracking hooks", style("●").dim());
for agent in Agent::all() {
if dry_run {
println!(
" {} {:12} Would remove hooks for {}",
style("~").dim(),
agent.name(),
agent.name()
);
} else {
let result = agent_setup::uninstall_one(agent);
match result {
Ok(msg) => println!(" {} {:12} {}", style("~").dim(), agent.name(), msg),
Err(e) => {
println!(" {} {:12} {}", style("~").yellow(), agent.name(), e);
errors.push(format!("{} hooks: {e}", agent.name()));
}
}
}
}
println!();
// Phase 2: Skills
println!(" {} Skills", style("●").dim());
for agent in Agent::all() {
if skills::skills_dir(agent).is_some() {
if dry_run {
println!(
" {} {:12} Would remove skills for {}",
style("~").dim(),
agent.name(),
agent.name()
);
} else {
let result = skills::remove_skills(agent);
match result {
Ok(msg) => println!(" {} {:12} {}", style("~").dim(), agent.name(), msg),
Err(e) => {
println!(" {} {:12} {}", style("~").yellow(), agent.name(), e);
errors.push(format!("{} skills: {e}", agent.name()));
}
}
}
}
}
println!();
// Phase 3: XDG dirs (only cache + state, NOT config)
println!(" {} Data directories", style("●").dim());
let dirs = [("Cache", xdg::cache_dir()), ("State", xdg::state_dir())];
for (label, path_result) in &dirs {
match path_result {
Ok(path) if path.exists() => {
if dry_run {
println!(
" {} Would remove {} ({})",
style("~").dim(),
label.to_lowercase(),
path.display()
);
} else {
match std::fs::remove_dir_all(path) {
Ok(()) => println!(
" {} Removed {} ({})",
style("~").dim(),
label.to_lowercase(),
path.display()
),
Err(e) => {
println!(
" {} Failed to remove {}: {}",
style("~").yellow(),
label.to_lowercase(),
e
);
errors.push(format!("{label} directory: {e}"));
}
}
}
}
Ok(_path) => println!(
" {} No {} directory",
style("~").dim(),
label.to_lowercase()
),
Err(e) => println!(" {} Could not resolve: {}", style("~").yellow(), e),
}
}
if !errors.is_empty() {
return Err(anyhow!(
"uninstall completed with {} error(s): {}",
errors.len(),
errors.join("; ")
));
}
Ok(())
}