mai_cli/cli/
remove_cmd.rs1use 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 pub pack: String,
10
11 #[arg(short, long)]
13 pub tool: Option<String>,
14
15 #[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 let paths = if scope == InstallScope::Global {
30 XdgPaths::new()
31 } else {
32 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 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 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
102fn 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 Ok(std::env::current_dir()?)
116}