diff --git a/lib/manifests/rokit.rs b/lib/manifests/rokit.rs index c602d55..1d0bb6f 100644 --- a/lib/manifests/rokit.rs +++ b/lib/manifests/rokit.rs @@ -132,6 +132,27 @@ impl RokitManifest { } } + /** + Removes a tool from the manifest. + + If the tool did not 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 let Some(tools) = doc.get_mut("tools") { + let tools = tools.as_table_mut().unwrap(); + if tools.contains_key(alias.name()) { + tools.remove(alias.name()); + true + } else { + false + } + } 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..1002009 100644 --- a/lib/storage/tool_storage.rs +++ b/lib/storage/tool_storage.rs @@ -137,6 +137,22 @@ impl ToolStorage { Ok(()) } + /** + Removes a link for a given tool. + + # Errors + + - If the link could not be removed. + */ + pub async fn remove_tool_link(&self, alias: &ToolAlias) -> RokitResult<()> { + let path = self.alias_path(alias); + if path_exists(&path).await { + remove_file(&path).await?; + } + + Ok(()) + } + /** Reads all currently known link paths for tool aliases in the binary directory. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5959537..d382089 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use clap::{ArgAction, CommandFactory, Parser}; +use remove::RemoveSubcommand; use tokio::time::Instant; use tracing::level_filters::LevelFilter; @@ -13,6 +14,7 @@ mod authenticate; mod init; mod install; mod list; +mod remove; mod self_install; mod self_update; mod system_info; @@ -123,6 +125,7 @@ pub enum Subcommand { SystemInfo(SystemInfoSubcommand), Trust(TrustSubcommand), Update(UpdateSubcommand), + Remove(RemoveSubcommand), } impl Subcommand { @@ -138,6 +141,7 @@ impl Subcommand { Self::SystemInfo(cmd) => cmd.run(home).await, Self::Trust(cmd) => cmd.run(home).await, Self::Update(cmd) => cmd.run(home).await, + Self::Remove(cmd) => cmd.run(home).await, } } } diff --git a/src/cli/remove.rs b/src/cli/remove.rs new file mode 100644 index 0000000..bca4141 --- /dev/null +++ b/src/cli/remove.rs @@ -0,0 +1,84 @@ +use anyhow::{bail, Context, Result}; +use clap::Parser; +use console::style; + +use rokit::{ + discovery::discover_all_manifests, manifests::RokitManifest, storage::Home, tool::ToolAlias, +}; + +use crate::util::CliProgressTracker; + +/// Removes a tool from Rokit. +#[derive(Debug, Parser)] +pub struct RemoveSubcommand { + /// The alias of the tool to remove. + pub alias: ToolAlias, + /// Remove this tool globally instead of removing + /// it from the nearest manifest file. + #[clap(long)] + pub global: bool, +} + +impl RemoveSubcommand { + pub async fn run(self, home: &Home) -> Result<()> { + let tool_cache = home.tool_cache(); + let tool_storage = home.tool_storage(); + + // 1. Load the manifest and check whether the tool + // to be removed is present in the manifest + let manifest_path = if self.global { + home.path().to_path_buf() + } else { + let non_global_manifests = discover_all_manifests(true, true).await; + non_global_manifests + .first() + .map(|m| m.path.parent().unwrap().to_path_buf()) + .context( + "No manifest was found for the current directory.\ + \nRun `rokit init` in your project root to create one.", + )? + }; + + let mut manifest = if self.global { + RokitManifest::load_or_create(&manifest_path).await? + } else { + RokitManifest::load(&manifest_path).await? + }; + if !manifest.has_tool(&self.alias) { + bail!("Tool does not exist and can't be removed: {}", self.alias); + } + + // 2. Remove the tool from the manifest + let spec = manifest.get_tool(&self.alias).unwrap(); + let pt = CliProgressTracker::new_with_message("Removing", 2); + + manifest.remove_tool(&self.alias); + manifest.save(manifest_path).await?; + pt.task_completed(); + + // 3. Uninstall the tool link + if tool_cache.is_installed(&spec) { + pt.update_message("Uninstalling"); + tool_storage.remove_tool_link(&self.alias).await?; + } + pt.task_completed(); + + // 3. Finally, display a nice message to the user + pt.finish_with_message(format!( + "Removed version {} of tool {}{} {}", + style(spec.version()).bold().yellow(), + style(spec.name()).bold().magenta(), + if self.alias.name() == spec.id().name() { + String::new() + } else { + format!( + " with alias {}", + style(self.alias.to_string()).bold().cyan() + ) + }, + pt.formatted_elapsed(), + )); + + Ok(()) + } +}