Skip to main content

mai_cli/cli/
remove_cmd.rs

1use crate::core::{InstallScope, MaiConfig};
2use crate::error::{Error, Result};
3use crate::storage::{Registry, XdgPaths};
4use clap::Args;
5
6#[derive(Debug, Args)]
7pub struct RemoveCommand {
8    /// Pack to remove (format: name or type/name)
9    pub pack: String,
10
11    /// Tool to remove from (uses active tool if not specified)
12    #[arg(short, long)]
13    pub tool: Option<String>,
14
15    /// Remove from global installation (user-wide) instead of local (project-specific)
16    #[arg(short = 'g', long, default_value = "false")]
17    pub global: bool,
18}
19
20impl RemoveCommand {
21    pub fn run(&self) -> Result<()> {
22        let scope = if self.global {
23            InstallScope::Global
24        } else {
25            InstallScope::Local
26        };
27
28        // Determine paths based on scope
29        let paths = if scope == InstallScope::Global {
30            XdgPaths::new()
31        } else {
32            // For local installation, find project root
33            let project_root = find_project_root()?;
34            XdgPaths::local(&project_root)
35        };
36
37        let config_path = paths.config_file();
38
39        let mut config = if config_path.exists() {
40            let content = std::fs::read_to_string(&config_path)?;
41            toml::from_str::<MaiConfig>(&content).unwrap_or_default()
42        } else {
43            MaiConfig::new()
44        };
45
46        let tool = self
47            .tool
48            .clone()
49            .or_else(|| config.active_tool().map(String::from))
50            .unwrap_or_else(|| "default".to_string());
51
52        let (pack_name, pack_type) = self.parse_pack()?;
53
54        let registry = Registry::new(&paths);
55
56        // Find the pack with the specified scope
57        let pack_to_remove = registry
58            .find_pack(&tool, &pack_name, pack_type)
59            .filter(|p| p.scope == scope);
60
61        if let Some(pack) = pack_to_remove {
62            registry.remove_pack(&tool, &pack)?;
63
64            // Remove from config
65            if let Some(tool_config) = config.tools.get_mut(&tool) {
66                tool_config.installed_packs.retain(|p| {
67                    !(p.name == pack_name && p.pack_type == pack_type && p.scope == scope)
68                });
69            }
70
71            let content = toml::to_string_pretty(&config)?;
72            std::fs::write(&config_path, content)?;
73
74            println!("✓ Removed: {}/{} ({})", tool, pack_name, scope);
75        } else {
76            return Err(Error::pack_not_found(format!(
77                "Pack '{}' not found for tool '{}' in {} scope",
78                pack_name, tool, scope
79            )));
80        }
81
82        Ok(())
83    }
84
85    fn parse_pack(&self) -> Result<(String, crate::core::PackType)> {
86        let (pack_type, name) = if let Some((t, n)) = self.pack.split_once('/') {
87            let pack_type = match t.to_lowercase().as_str() {
88                "skill" => crate::core::PackType::Skill,
89                "command" => crate::core::PackType::Command,
90                "mcp" => crate::core::PackType::Mcp,
91                _ => crate::core::PackType::Skill,
92            };
93            (pack_type, n.to_string())
94        } else {
95            (crate::core::PackType::Skill, self.pack.to_string())
96        };
97
98        Ok((name, pack_type))
99    }
100}
101
102/// Find the project root directory (directory containing mai.toml or .git)
103fn find_project_root() -> Result<std::path::PathBuf> {
104    let current = std::env::current_dir()?;
105
106    let mut current = current.as_path();
107    while let Some(parent) = current.parent() {
108        if current.join("mai.toml").exists() || current.join(".git").exists() {
109            return Ok(current.to_path_buf());
110        }
111        current = parent;
112    }
113
114    // If no project root found, use current directory
115    Ok(std::env::current_dir()?)
116}