diff --git a/lib/manifests/rokit.rs b/lib/manifests/rokit.rs index c602d55..49621c6 100644 --- a/lib/manifests/rokit.rs +++ b/lib/manifests/rokit.rs @@ -132,6 +132,25 @@ impl RokitManifest { } } + /** + Remove a tool from the manifest. + + If the tool doesn't exist, this will return `false` and do nothing. + */ + pub fn remove_tool(&mut self, alias: &ToolAlias) -> bool { + let doc = self.document.as_table_mut(); + if !doc.contains_table("tools") { + return false; + } + let tools = doc["tools"].as_table_mut().unwrap(); + if tools.contains_value(alias.name()) { + tools.remove(alias.name()); + true + } else { + false + } + } + /** Updates a tool in the manifest with a new tool specification. diff --git a/lib/storage/tool_storage.rs b/lib/storage/tool_storage.rs index aae1528..3c150a8 100644 --- a/lib/storage/tool_storage.rs +++ b/lib/storage/tool_storage.rs @@ -50,7 +50,8 @@ impl ToolStorage { (tool_dir, tool_file) } - fn alias_path(&self, alias: &ToolAlias) -> PathBuf { + #[must_use] + pub fn alias_path(&self, alias: &ToolAlias) -> PathBuf { let alias_file_name = format!("{}{EXE_SUFFIX}", alias.name.uncased_str()); self.aliases_dir.join(alias_file_name) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5959537..9b2bdc5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -5,6 +5,7 @@ use tracing::level_filters::LevelFilter; use rokit::storage::Home; use rokit::system::ProcessParent; +use uninstall::UninstallSubcommand; use crate::util::init_tracing; @@ -17,6 +18,7 @@ mod self_install; mod self_update; mod system_info; mod trust; +mod uninstall; mod update; use self::add::AddSubcommand; @@ -122,6 +124,7 @@ pub enum Subcommand { SelfUpdate(SelfUpdateSubcommand), SystemInfo(SystemInfoSubcommand), Trust(TrustSubcommand), + Uninstall(UninstallSubcommand), Update(UpdateSubcommand), } @@ -137,6 +140,7 @@ impl Subcommand { Self::SelfUpdate(cmd) => cmd.run(home).await, Self::SystemInfo(cmd) => cmd.run(home).await, Self::Trust(cmd) => cmd.run(home).await, + Self::Uninstall(cmd) => cmd.run(home).await, Self::Update(cmd) => cmd.run(home).await, } } diff --git a/src/cli/uninstall.rs b/src/cli/uninstall.rs new file mode 100644 index 0000000..4de0ce0 --- /dev/null +++ b/src/cli/uninstall.rs @@ -0,0 +1,68 @@ +use anyhow::{bail, Result}; +use clap::Parser; +use console::style; +use rokit::{ + discovery::{discover_all_manifests, discover_tool_spec}, + manifests::RokitManifest, + storage::Home, + tool::ToolAlias, +}; +use tokio::fs::{read_dir, remove_dir, remove_dir_all, remove_file}; + +use crate::util::{CliProgressTracker, ToolAliasOrId}; + +/// Removes a tool from Rokit and uninstalls it. +#[derive(Debug, Parser)] +pub struct UninstallSubcommand { + /// The tool alias or identifier to uninstall. + pub tool: ToolAliasOrId, +} + +impl UninstallSubcommand { + pub async fn run(self, home: &Home) -> Result<()> { + let tool_storage = home.tool_storage(); + let tool_cache = home.tool_cache(); + + let alias: ToolAlias = match self.tool { + ToolAliasOrId::Alias(alias) => alias, + ToolAliasOrId::Id(id) => id.into(), + }; + let Some(spec) = discover_tool_spec(&alias, true, false).await else { + bail!("Failed to find tool '{alias}' in any project manifest file.") + }; + + // 1. Remove the tool from all manifests that contain it + let pt = CliProgressTracker::new_with_message("Uninstalling", 1); + let manifests = discover_all_manifests(true, false).await; + for manifest in manifests { + let manifest_path = manifest.path.parent().unwrap(); + let mut manifest = RokitManifest::load(&manifest_path).await?; + if manifest.has_tool(&alias) { + manifest.remove_tool(&alias); + manifest.save(&manifest_path).await?; + } + } + + // 2. Uninstall the tool binary and remove it from the install cache + let tool_path = tool_storage.tool_path(&spec); + let tool_dir = tool_path.ancestors().nth(2).unwrap(); + let author_dir = tool_dir.parent().unwrap(); + + remove_file(tool_storage.alias_path(&alias)).await?; + remove_dir_all(tool_dir).await?; + if read_dir(&author_dir).await?.next_entry().await?.is_none() { + remove_dir(author_dir).await?; + } + + let _ = tool_cache.remove_installed(&spec); + + // 3. Finally, display a nice message to the user + pt.finish_with_message(format!( + "Uninstalled tool {} {}", + style(spec.name()).bold().magenta(), + pt.formatted_elapsed() + )); + + Ok(()) + } +} diff --git a/src/util/alias_or_id.rs b/src/util/alias_or_id.rs new file mode 100644 index 0000000..8837057 --- /dev/null +++ b/src/util/alias_or_id.rs @@ -0,0 +1,49 @@ +use std::str::FromStr; + +use serde_with::DeserializeFromStr; + +use rokit::tool::{ToolAlias, ToolId}; + +/** + A tool alias *or* identifier. + + See [`ToolAlias`] and [`ToolId`] for more information. +*/ +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, DeserializeFromStr)] +pub enum ToolAliasOrId { + Alias(ToolAlias), + Id(ToolId), +} + +impl FromStr for ToolAliasOrId { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + if s.contains('/') { + Ok(Self::Id(s.parse()?)) + } else { + Ok(Self::Alias(s.parse()?)) + } + } +} + +impl From for ToolAliasOrId { + fn from(alias: ToolAlias) -> Self { + Self::Alias(alias) + } +} + +impl From for ToolAliasOrId { + fn from(id: ToolId) -> Self { + Self::Id(id) + } +} + +impl From for ToolAlias { + fn from(alias_or_id: ToolAliasOrId) -> Self { + let name = match alias_or_id { + ToolAliasOrId::Alias(alias) => alias.name().to_string(), + ToolAliasOrId::Id(id) => id.name().to_string(), + }; + Self::from_str(&name).expect("Derived alias is always valid") + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 1e0a9b1..16ce60f 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ +mod alias_or_id; mod alias_or_id_or_spec; mod artifacts; mod constants; @@ -6,6 +7,7 @@ mod progress; mod prompts; mod tracing; +pub use self::alias_or_id::ToolAliasOrId; pub use self::alias_or_id_or_spec::ToolAliasOrIdOrSpec; pub use self::artifacts::find_most_compatible_artifact; pub use self::id_or_spec::ToolIdOrSpec;