Skip to main content

mai_cli/cli/
update_cmd.rs

1use 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    /// Pack to update (format: name[@version] or type/name[@version])
9    pub pack: String,
10
11    /// Tool to update for (uses active tool if not specified)
12    #[arg(short, long)]
13    pub tool: Option<String>,
14
15    /// Update to latest version
16    #[arg(long)]
17    pub latest: bool,
18
19    /// Update global installation (user-wide) instead of local (project-specific)
20    #[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        // Determine paths based on scope
33        let paths = if scope == InstallScope::Global {
34            XdgPaths::new()
35        } else {
36            // For local installation, find project root
37            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        // Find the current pack with the specified scope
61        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        // Determine target version
76        let target_version = if self.latest {
77            // For now, just bump patch version as a placeholder
78            // In Phase 3, this would query a registry
79            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        // Remove old version
95        registry.remove_pack(&tool, &current_pack)?;
96
97        // Install new version
98        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        // Update config
104        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
151/// Find the project root directory (directory containing mai.toml or .git)
152fn 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    // If no project root found, use current directory
164    Ok(std::env::current_dir()?)
165}