diff --git a/rust/crates/cli/src/main.rs b/rust/crates/cli/src/main.rs index 66f4381eba..4021d74c16 100644 --- a/rust/crates/cli/src/main.rs +++ b/rust/crates/cli/src/main.rs @@ -164,6 +164,11 @@ enum CliPkgAction { Delete { id: String }, #[command(about = "Enable a package")] Enable { id: String }, + #[command(about = "Update installed modules")] + Update { + #[arg(help = "Module identifier to update (updates all if omitted)")] + id: Option, + }, } impl From for Command { @@ -224,6 +229,7 @@ impl From for PkgAction { CliPkgAction::Install { id, url } => PkgAction::Install { id, url }, CliPkgAction::Delete { id } => PkgAction::Delete { id }, CliPkgAction::Enable { id } => PkgAction::Enable { id }, + CliPkgAction::Update { id } => PkgAction::Update { id }, } } } diff --git a/rust/crates/spicetify/src/commands/mod.rs b/rust/crates/spicetify/src/commands/mod.rs index 9b0a1f9517..241de541f1 100644 --- a/rust/crates/spicetify/src/commands/mod.rs +++ b/rust/crates/spicetify/src/commands/mod.rs @@ -65,6 +65,7 @@ pub enum PkgAction { Install { id: String, url: Option }, Delete { id: String }, Enable { id: String }, + Update { id: Option }, } pub fn dispatch(cmd: &Command, ctx: &AppContext) -> Result<()> { @@ -112,6 +113,7 @@ pub fn dispatch(cmd: &Command, ctx: &AppContext) -> Result<()> { ), PkgAction::Delete { id } => crate::module::delete_module(&ctx.config_root, id), PkgAction::Enable { id } => crate::module::enable_module(&ctx.config_root, id), + PkgAction::Update { id } => pkg::update(ctx, id.as_deref()), } } Command::Protocol(uri) => protocol::run(ctx, uri), @@ -155,6 +157,7 @@ mod tests { PkgAction::Install { id: "module@1".to_string(), url: None }, PkgAction::Delete { id: "module@1".to_string() }, PkgAction::Enable { id: "module@1".to_string() }, + PkgAction::Update { id: Some("module@1".to_string()) }, ] { let error = dispatch(&Command::Pkg(action), &ctx).expect_err("competing mutation"); assert!(error.to_string().contains("already in progress"), "{error}"); diff --git a/rust/crates/spicetify/src/commands/pkg.rs b/rust/crates/spicetify/src/commands/pkg.rs index d1b9bbd0f6..d685399de7 100644 --- a/rust/crates/spicetify/src/commands/pkg.rs +++ b/rust/crates/spicetify/src/commands/pkg.rs @@ -356,6 +356,100 @@ pub(crate) fn install(ctx: &AppContext, identifier: &str) -> Result<()> { ) } +pub(crate) fn update(ctx: &AppContext, target_id: Option<&str>) -> Result<()> { + let vault = cached_vault(&ctx.config_root)?; + let paths = crate::module::ModulePaths::from_config_root(&ctx.config_root); + let installed_mods = installed(&ctx.config_root); + + if installed_mods.is_empty() { + tracing::info!("no modules installed"); + return Ok(()); + } + + if let Some(target) = target_id { + let Some((_, current_version)) = installed_mods.iter().find(|(id, _)| id == target) else { + anyhow::bail!("module '{target}' is not installed"); + }; + + let Some(module) = vault.modules.get(target) else { + anyhow::bail!("module '{target}' is not in the vault"); + }; + + let target_version = resolve_version(module)?; + if !should_stage(true, Some(current_version), &target_version, !module.enabled.is_empty()) { + tracing::info!("{target} is already up to date ({current_version})"); + return Ok(()); + } + + let Some(entry) = module.v.get(&target_version) else { + anyhow::bail!("{target}@{target_version} is not in the vault"); + }; + + update_single_module(ctx, &paths, target, current_version, &target_version, entry)?; + } else { + let mut updated_count = 0; + for (id, current_version) in &installed_mods { + let Some(module) = vault.modules.get(id) else { + continue; + }; + + let Ok(target_version) = resolve_version(module) else { + continue; + }; + + if should_stage(true, Some(current_version), &target_version, !module.enabled.is_empty()) + && let Some(entry) = module.v.get(&target_version) + { + tracing::info!("updating {id}: {current_version} -> {target_version}"); + if let Err(e) = update_single_module(ctx, &paths, id, current_version, &target_version, entry) { + tracing::warn!("failed to update {id}: {e}"); + } else { + updated_count += 1; + } + } + } + + if updated_count == 0 { + tracing::info!("all modules are up to date"); + } else { + tracing::info!("successfully updated {updated_count} module(s)"); + } + } + + Ok(()) +} + +fn update_single_module( + ctx: &AppContext, + paths: &crate::module::ModulePaths, + id: &str, + old_version: &str, + new_version: &str, + entry: &VaultVersion, +) -> Result<()> { + if entry.artifacts.is_empty() { + anyhow::bail!("{id}@{new_version} has no artifacts"); + } + + let tag = format!("{id}@{new_version}"); + crate::module::install_from_vault( + &ctx.config_root, + &tag, + entry.artifacts.clone(), + entry.checksum.clone(), + )?; + crate::module::enable_module(&ctx.config_root, &tag)?; + tracing::info!("updated {id} to {new_version}"); + + if old_version != new_version + && let Ok(superseded) = crate::module::vault::StoreIdentifier::parse(&format!("{id}@{old_version}")) + { + let _ = crate::module::delete(paths, &superseded); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*;