Skip to main content

zenith_cli/commands/plugin/
uninstall.rs

1//! Remove a previously installed skill for an agent.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use super::agent::{Agent, Scope, SkillFormat};
7use super::assets::COMMAND_FILES;
8use super::paths::{SkillTarget, command_dir, skill_target};
9
10/// Outcome of removing one item (file or directory).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RemoveOutcome {
13    /// The item existed and was removed.
14    Removed,
15    /// The item was not present — nothing to do.
16    Absent,
17}
18
19/// Per-item removal result.
20#[derive(Debug, Clone)]
21pub struct RemoveResult {
22    pub path: PathBuf,
23    pub outcome: Result<RemoveOutcome, String>,
24}
25
26/// Per-agent uninstall report.
27#[derive(Debug, Clone)]
28pub struct AgentUninstall {
29    pub agent: Agent,
30    pub items: Vec<RemoveResult>,
31    pub unsupported: Option<String>,
32}
33
34/// Uninstall (or, when `dry_run`, plan removal of) the skill for one agent.
35pub fn uninstall_agent(
36    agent: Agent,
37    scope: Scope,
38    project_root: &Path,
39    dry_run: bool,
40) -> AgentUninstall {
41    let Some(target) = skill_target(agent, scope, project_root) else {
42        return AgentUninstall {
43            agent,
44            items: Vec::new(),
45            unsupported: Some(format!("{} has no automatic target", agent.display())),
46        };
47    };
48
49    let mut items = Vec::new();
50    match (agent.format(), &target) {
51        (SkillFormat::Folder, SkillTarget::Folder(dir)) => {
52            items.push(remove(dir, true, dry_run));
53            if let Some(cmd_dir) = command_dir(agent, scope, project_root) {
54                for (name, _) in COMMAND_FILES {
55                    items.push(remove(&cmd_dir.join(name), false, dry_run));
56                }
57            }
58        }
59        (SkillFormat::Rule, SkillTarget::Rule(path)) => {
60            items.push(remove(path, false, dry_run));
61        }
62        (SkillFormat::Folder, SkillTarget::Rule(_))
63        | (SkillFormat::Rule, SkillTarget::Folder(_)) => {}
64    }
65
66    AgentUninstall {
67        agent,
68        items,
69        unsupported: None,
70    }
71}
72
73/// Remove a file or directory, reporting whether it existed.
74fn remove(path: &Path, is_dir: bool, dry_run: bool) -> RemoveResult {
75    let exists = path.exists();
76    let outcome = if !exists {
77        Ok(RemoveOutcome::Absent)
78    } else if dry_run {
79        Ok(RemoveOutcome::Removed)
80    } else {
81        let res = if is_dir {
82            fs::remove_dir_all(path)
83        } else {
84            fs::remove_file(path)
85        };
86        res.map(|()| RemoveOutcome::Removed)
87            .map_err(|e| e.to_string())
88    };
89    RemoveResult {
90        path: path.to_path_buf(),
91        outcome,
92    }
93}