mai_cli/cli/
update_cmd.rs1use crate::core::{InstallScope, MaiConfig, Pack, Version};
2use crate::error::{Error, Result};
3use crate::storage::{Registry, XdgPaths};
4use clap::Args;
5
6#[derive(Debug, Args)]
7pub struct UpdateCommand {
8 pub pack: String,
10
11 #[arg(short, long)]
13 pub tool: Option<String>,
14
15 #[arg(long)]
17 pub latest: bool,
18
19 #[arg(short = 'g', long, default_value = "false")]
21 pub global: bool,
22}
23
24impl UpdateCommand {
25 pub fn run(&self) -> Result<()> {
26 let scope = if self.global {
27 InstallScope::Global
28 } else {
29 InstallScope::Local
30 };
31
32 let paths = if scope == InstallScope::Global {
34 XdgPaths::new()
35 } else {
36 let project_root = find_project_root()?;
38 XdgPaths::local(&project_root)
39 };
40
41 let config_path = paths.config_file();
42
43 let mut config = if config_path.exists() {
44 let content = std::fs::read_to_string(&config_path)?;
45 toml::from_str::<MaiConfig>(&content).unwrap_or_default()
46 } else {
47 MaiConfig::new()
48 };
49
50 let tool = self
51 .tool
52 .clone()
53 .or_else(|| config.active_tool().map(String::from))
54 .unwrap_or_else(|| "default".to_string());
55
56 let (pack_name, target_version, pack_type) = self.parse_pack()?;
57
58 let registry = Registry::new(&paths);
59
60 let current_pack = registry
62 .find_pack(&tool, &pack_name, pack_type)
63 .filter(|p| p.scope == scope);
64
65 let current_pack = match current_pack {
66 Some(p) => p,
67 None => {
68 return Err(Error::pack_not_found(format!(
69 "Pack '{}' not found for tool '{}' in {} scope. Use 'mai install' first.",
70 pack_name, tool, scope
71 )));
72 }
73 };
74
75 let target_version = if self.latest {
77 Version::new(
80 current_pack.version.major,
81 current_pack.version.minor,
82 current_pack.version.patch + 1,
83 )
84 } else {
85 target_version.unwrap_or_else(|| {
86 Version::new(
87 current_pack.version.major,
88 current_pack.version.minor,
89 current_pack.version.patch + 1,
90 )
91 })
92 };
93
94 registry.remove_pack(&tool, ¤t_pack)?;
96
97 let new_pack = Pack::new(&pack_name, pack_type, target_version.clone())
99 .with_tool(&tool)
100 .with_scope(scope);
101 registry.install_pack(&tool, &new_pack)?;
102
103 if let Some(tool_config) = config.tools.get_mut(&tool) {
105 tool_config
106 .installed_packs
107 .retain(|p| !(p.name == pack_name && p.pack_type == pack_type && p.scope == scope));
108 tool_config.installed_packs.push(new_pack.clone());
109 }
110
111 let content = toml::to_string_pretty(&config)?;
112 std::fs::write(&config_path, content)?;
113
114 println!(
115 "✓ Updated: {} -> {} ({})",
116 current_pack.version, target_version, scope
117 );
118 Ok(())
119 }
120
121 fn parse_pack(&self) -> Result<(String, Option<Version>, crate::core::PackType)> {
122 let mut parts = self.pack.splitn(2, '@');
123 let name_part = parts.next().unwrap();
124 let version_str = parts.next();
125
126 let (pack_type, name) = if let Some((t, n)) = name_part.split_once('/') {
127 let pack_type = match t.to_lowercase().as_str() {
128 "skill" => crate::core::PackType::Skill,
129 "command" => crate::core::PackType::Command,
130 "mcp" => crate::core::PackType::Mcp,
131 _ => crate::core::PackType::Skill,
132 };
133 (pack_type, n.to_string())
134 } else {
135 (crate::core::PackType::Skill, name_part.to_string())
136 };
137
138 let version = if let Some(v) = version_str {
139 Some(
140 Version::parse(v)
141 .map_err(|e| Error::version_error(format!("Invalid version: {}", e)))?,
142 )
143 } else {
144 None
145 };
146
147 Ok((name, version, pack_type))
148 }
149}
150
151fn find_project_root() -> Result<std::path::PathBuf> {
153 let current = std::env::current_dir()?;
154
155 let mut current = current.as_path();
156 while let Some(parent) = current.parent() {
157 if current.join("mai.toml").exists() || current.join(".git").exists() {
158 return Ok(current.to_path_buf());
159 }
160 current = parent;
161 }
162
163 Ok(std::env::current_dir()?)
165}