diff --git a/docs/.vitepress/cli_commands.ts b/docs/.vitepress/cli_commands.ts index 7e06f53ae3..3fbd10f334 100644 --- a/docs/.vitepress/cli_commands.ts +++ b/docs/.vitepress/cli_commands.ts @@ -282,6 +282,9 @@ export const commands: { [key: string]: Command } = { unset: { hide: false, }, + unuse: { + hide: false, + }, upgrade: { hide: false, }, diff --git a/docs/cli/index.md b/docs/cli/index.md index 1c38d278a9..dcecc3461e 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -101,7 +101,7 @@ Can also use `MISE_NO_CONFIG=1` - [`mise plugins ls-remote [-u --urls] [--only-names]`](/cli/plugins/ls-remote.md) - [`mise plugins uninstall [-p --purge] [-a --all] [PLUGIN]...`](/cli/plugins/uninstall.md) - [`mise plugins update [-j --jobs ] [PLUGIN]...`](/cli/plugins/update.md) -- [`mise prune [FLAGS] [PLUGIN]...`](/cli/prune.md) +- [`mise prune [FLAGS] [INSTALLED_TOOL]...`](/cli/prune.md) - [`mise registry [-b --backend ] [--hide-aliased] [NAME]`](/cli/registry.md) - [`mise reshim [-f --force]`](/cli/reshim.md) - [`mise run [FLAGS]`](/cli/run.md) @@ -129,6 +129,7 @@ Can also use `MISE_NO_CONFIG=1` - [`mise trust [FLAGS] [CONFIG_FILE]`](/cli/trust.md) - [`mise uninstall [-a --all] [-n --dry-run] [INSTALLED_TOOL@VERSION]...`](/cli/uninstall.md) - [`mise unset [-f --file ] [-g --global] [KEYS]...`](/cli/unset.md) +- [`mise unuse [--no-prune] [--global] ...`](/cli/unuse.md) - [`mise upgrade [FLAGS] [TOOL@VERSION]...`](/cli/upgrade.md) - [`mise use [FLAGS] [TOOL@VERSION]...`](/cli/use.md) - [`mise version`](/cli/version.md) diff --git a/docs/cli/prune.md b/docs/cli/prune.md index 5e5ddd9c68..48c37a4531 100644 --- a/docs/cli/prune.md +++ b/docs/cli/prune.md @@ -1,6 +1,6 @@ # `mise prune` -- **Usage**: `mise prune [FLAGS] [PLUGIN]...` +- **Usage**: `mise prune [FLAGS] [INSTALLED_TOOL]...` - **Source code**: [`src/cli/prune.rs`](https://github.com/jdx/mise/blob/main/src/cli/prune.rs) Delete unused versions of tools @@ -12,9 +12,9 @@ as will versions only referenced on the command line `mise exec @...` +- **Aliases**: `rm`, `remove` +- **Source code**: [`src/cli/unuse.rs`](https://github.com/jdx/mise/blob/main/src/cli/unuse.rs) + +Removes installed tool versions from mise.toml + +Will also prune the installed version if no other configurations are using it. + +## Arguments + +### `...` + +Tool(s) to remove + +## Flags + +### `--no-prune` + +Do not also prune the installed version + +### `--global` + +Remove tool from global config + +Examples: + +``` +# will uninstall specific version +$ mise remove node@18.0.0 +``` diff --git a/e2e/cli/test_ls b/e2e/cli/test_ls index 0d5d7e265f..fa840148e0 100644 --- a/e2e/cli/test_ls +++ b/e2e/cli/test_ls @@ -23,7 +23,7 @@ assert "cat json | jq -r '.tiny[1].active'" "true" assert "cat json | jq -r '.tiny[1].version'" "3.1.0" assert "cat json | jq -r '.tiny[1].install_path'" "$MISE_DATA_DIR/installs/tiny/3.1.0" -assert "mise rm tiny@3.1.0" +assert "mise uninstall tiny@3.1.0" assert_contains "mise ls tiny" "tiny 3.1.0 (missing) ~/workdir/mise.toml 3" assert_contains "mise ls --missing tiny" "tiny 3.1.0 (missing) ~/workdir/mise.toml 3" assert_not_contains "mise ls --missing tiny" "2.0.0" diff --git a/e2e/cli/test_uninstall b/e2e/cli/test_uninstall index 9f3740d7b6..a9070f30cc 100644 --- a/e2e/cli/test_uninstall +++ b/e2e/cli/test_uninstall @@ -11,12 +11,12 @@ assert_contains "mise ls dummy" "1.0.0" assert_contains "mise ls dummy" "1.1.0" assert_contains "mise ls dummy" "2.0.0" -mise rm -a dummy@1 +mise uninstall -a dummy@1 assert_not_contains "mise ls dummy" "1.0.0" assert_not_contains "mise ls dummy" "1.1.0" assert_contains "mise ls dummy" "2.0.0" -mise rm -a dummy +mise uninstall -a dummy assert_not_contains "mise ls dummy" "1.1.0" assert_not_contains "mise ls dummy" "2.0.1" assert_not_contains "mise ls dummy" "2.1.0" diff --git a/e2e/cli/test_unuse b/e2e/cli/test_unuse new file mode 100644 index 0000000000..f5a4479302 --- /dev/null +++ b/e2e/cli/test_unuse @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +assert "mise use dummy@1.0.0" +assert_contains "mise ls dummy" "1.0.0" +assert "mise unuse dummy" +assert_empty "mise ls dummy" + +assert "mise use dummy@1.0.0" +mkdir subdir +cd subdir || exit 1 +assert "mise use -p mise.toml dummy@1.0.0" +assert "mise ls dummy" "dummy 1.0.0 ~/workdir/subdir/mise.toml 1.0.0" +assert "mise unuse dummy" +# version is not pruned because it's in ~/workdir/mise.toml +assert "mise ls dummy" "dummy 1.0.0 ~/workdir/mise.toml 1.0.0" +assert "mise unuse dummy" +assert_empty "mise ls dummy" + +assert "mise use -g dummy@1.0.0" +assert "mise ls dummy" "dummy 1.0.0 ~/.config/mise/config.toml 1.0.0" +assert "mise unuse dummy" +assert_empty "mise ls dummy" diff --git a/man/man1/mise.1 b/man/man1/mise.1 index c950a0fbe1..3ff874ed08 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -169,6 +169,9 @@ Removes installed tool versions mise\-unset(1) Remove environment variable(s) from the config file. .TP +mise\-unuse(1) +Removes installed tool versions from mise.toml +.TP mise\-upgrade(1) Upgrades outdated tools .TP diff --git a/mise.usage.kdl b/mise.usage.kdl index 1139481ce0..84615fecf5 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -922,7 +922,7 @@ as will versions only referenced on the command line `mise exec @..." help="Tool(s) to remove" var=true +} cmd "upgrade" help="Upgrades outdated tools" { alias "up" long_help r"Upgrades outdated tools diff --git a/src/cli/local.rs b/src/cli/local.rs index e4827efd10..2e189428ee 100644 --- a/src/cli/local.rs +++ b/src/cli/local.rs @@ -115,7 +115,7 @@ pub fn local( if let Some(plugins) = &remove { for plugin in plugins { - cf.remove_plugin(plugin)?; + cf.remove_tool(plugin)?; } let tools = plugins .iter() diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 42b41a2fed..dc2a56d4e5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -58,6 +58,7 @@ mod tool; mod trust; mod uninstall; mod unset; +mod unuse; mod upgrade; mod usage; mod r#use; @@ -222,6 +223,7 @@ pub enum Commands { Trust(trust::Trust), Uninstall(uninstall::Uninstall), Unset(unset::Unset), + Unuse(unuse::Unuse), Upgrade(upgrade::Upgrade), Usage(usage::Usage), Use(r#use::Use), @@ -284,6 +286,7 @@ impl Commands { Self::Trust(cmd) => cmd.run(), Self::Uninstall(cmd) => cmd.run(), Self::Unset(cmd) => cmd.run(), + Self::Unuse(cmd) => cmd.run(), Self::Upgrade(cmd) => cmd.run(), Self::Usage(cmd) => cmd.run(), Self::Use(cmd) => cmd.run(), diff --git a/src/cli/prune.rs b/src/cli/prune.rs index 83573b4201..7b9a2dd7fa 100644 --- a/src/cli/prune.rs +++ b/src/cli/prune.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::sync::Arc; use crate::backend::Backend; -use crate::cli::args::BackendArg; +use crate::cli::args::{BackendArg, ToolArg}; use crate::config::tracking::Tracker; use crate::config::{Config, SETTINGS}; use crate::toolset::{ToolVersion, Toolset, ToolsetBuilder}; @@ -22,9 +22,9 @@ use super::trust::Trust; #[derive(Debug, clap::Args)] #[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] pub struct Prune { - /// Prune only versions from this plugin(s) + /// Prune only these tools #[clap()] - pub plugin: Option>, + pub installed_tool: Option>, /// Do not actually delete anything #[clap(long, short = 'n')] @@ -45,7 +45,11 @@ impl Prune { self.prune_configs()?; } if self.tools || !self.configs { - self.prune_tools()?; + let backends = self + .installed_tool + .as_ref() + .map(|it| it.iter().map(|ta| &ta.ba).collect()); + prune(backends.unwrap_or_default(), self.dry_run)?; } Ok(()) } @@ -60,49 +64,46 @@ impl Prune { } Ok(()) } +} - fn prune_tools(&self) -> Result<()> { - let config = Config::try_get()?; - let ts = ToolsetBuilder::new().build(&config)?; - let mut to_delete = ts - .list_installed_versions()? - .into_iter() - .map(|(p, tv)| ((tv.ba().short.to_string(), tv.tv_pathname()), (p, tv))) - .collect::, ToolVersion)>>(); +pub fn prune(tools: Vec<&BackendArg>, dry_run: bool) -> Result<()> { + let config = Config::try_get()?; + let ts = ToolsetBuilder::new().build(&config)?; + let mut to_delete = ts + .list_installed_versions()? + .into_iter() + .map(|(p, tv)| ((tv.ba().short.to_string(), tv.tv_pathname()), (p, tv))) + .collect::, ToolVersion)>>(); - if let Some(backends) = &self.plugin { - to_delete.retain(|_, (_, tv)| backends.contains(tv.ba())); - } + if !tools.is_empty() { + to_delete.retain(|_, (_, tv)| tools.contains(&tv.ba())); + } - for cf in config.get_tracked_config_files()?.values() { - let mut ts = Toolset::from(cf.to_tool_request_set()?); - ts.resolve()?; - for (_, tv) in ts.list_current_versions() { - to_delete.remove(&(tv.ba().short.to_string(), tv.tv_pathname())); - } + for cf in config.get_tracked_config_files()?.values() { + let mut ts = Toolset::from(cf.to_tool_request_set()?); + ts.resolve()?; + for (_, tv) in ts.list_current_versions() { + to_delete.remove(&(tv.ba().short.to_string(), tv.tv_pathname())); } - - self.delete(to_delete.into_values().collect()) } - fn delete(&self, to_delete: Vec<(Arc, ToolVersion)>) -> Result<()> { - let mpr = MultiProgressReport::get(); - for (p, tv) in to_delete { - let mut prefix = tv.style(); - if self.dry_run { - prefix = format!("{} {} ", prefix, style("[dryrun]").bold()); - } - let pr = mpr.add(&prefix); - if self.dry_run - || SETTINGS.yes - || prompt::confirm_with_all(format!("remove {} ?", &tv))? - { - p.uninstall_version(&tv, pr.as_ref(), self.dry_run)?; - pr.finish(); - } + delete(dry_run, to_delete.into_values().collect()) +} + +fn delete(dry_run: bool, to_delete: Vec<(Arc, ToolVersion)>) -> Result<()> { + let mpr = MultiProgressReport::get(); + for (p, tv) in to_delete { + let mut prefix = tv.style(); + if dry_run { + prefix = format!("{} {} ", prefix, style("[dryrun]").bold()); + } + let pr = mpr.add(&prefix); + if dry_run || SETTINGS.yes || prompt::confirm_with_all(format!("remove {} ?", &tv))? { + p.uninstall_version(&tv, pr.as_ref(), dry_run)?; + pr.finish(); } - Ok(()) } + Ok(()) } static AFTER_LONG_HELP: &str = color_print::cstr!( diff --git a/src/cli/uninstall.rs b/src/cli/uninstall.rs index f9502d3e41..7534dbc395 100644 --- a/src/cli/uninstall.rs +++ b/src/cli/uninstall.rs @@ -16,7 +16,7 @@ use crate::{config, dirs, file}; /// /// This only removes the installed version, it does not modify mise.toml. #[derive(Debug, clap::Args)] -#[clap(verbatim_doc_comment, visible_aliases = ["remove", "rm"], after_long_help = AFTER_LONG_HELP)] +#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] pub struct Uninstall { /// Tool(s) to remove #[clap(value_name = "INSTALLED_TOOL@VERSION", required_unless_present = "all")] diff --git a/src/cli/unuse.rs b/src/cli/unuse.rs new file mode 100644 index 0000000000..0b906bbea0 --- /dev/null +++ b/src/cli/unuse.rs @@ -0,0 +1,84 @@ +use eyre::Result; + +use crate::cli::args::ToolArg; +use crate::cli::prune::prune; +use crate::config; +use crate::config::config_file::ConfigFile; +use crate::config::{config_file, Config}; +use crate::file::display_path; + +/// Removes installed tool versions from mise.toml +/// +/// Will also prune the installed version if no other configurations are using it. +#[derive(Debug, clap::Args)] +#[clap(verbatim_doc_comment, visible_aliases = ["rm", "remove"], after_long_help = AFTER_LONG_HELP)] +pub struct Unuse { + /// Tool(s) to remove + #[clap(value_name = "INSTALLED_TOOL@VERSION", required = true)] + installed_tool: Vec, + + /// Do not also prune the installed version + #[clap(long)] + no_prune: bool, + + /// Remove tool from global config + #[clap(long)] + global: bool, +} + +impl Unuse { + pub fn run(self) -> Result<()> { + let config = Config::get(); + let mut cf = self.get_config_file(&config)?; + let tools = cf.to_tool_request_set()?.tools; + let mut removed = vec![]; + for ta in &self.installed_tool { + if tools.contains_key(&ta.ba) { + removed.push(ta); + } + cf.remove_tool(&ta.ba)?; + } + if removed.is_empty() { + debug!("no tools to remove"); + return Ok(()); + } + cf.save()?; + info!( + "removed: {} from {}", + removed + .iter() + .map(|ta| ta.to_string()) + .collect::>() + .join(", "), + display_path(cf.get_path()) + ); + + if !self.no_prune { + prune(self.installed_tool.iter().map(|ta| &ta.ba).collect(), false)?; + } + + Ok(()) + } + + pub fn get_config_file(&self, config: &Config) -> Result> { + for cf in config.config_files.values() { + if cf + .to_tool_request_set()? + .tools + .keys() + .any(|ba| self.installed_tool.iter().any(|ta| ta.ba == *ba)) + { + return config_file::parse(cf.get_path()); + } + } + config_file::parse_or_init(&config::local_toml_config_path()) + } +} + +static AFTER_LONG_HELP: &str = color_print::cstr!( + r#"Examples: + + # will uninstall specific version + $ mise remove node@18.0.0 +"# +); diff --git a/src/cli/use.rs b/src/cli/use.rs index 61d14f2228..b938f95e8f 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -177,7 +177,7 @@ impl Use { self.warn_if_hidden(&config, cf.get_path()); } for plugin_name in &self.remove { - cf.remove_plugin(plugin_name)?; + cf.remove_tool(plugin_name)?; } cf.save()?; diff --git a/src/config/config_file/idiomatic_version.rs b/src/config/config_file/idiomatic_version.rs index bbde740365..f5770a1921 100644 --- a/src/config/config_file/idiomatic_version.rs +++ b/src/config/config_file/idiomatic_version.rs @@ -56,7 +56,7 @@ impl ConfigFile for IdiomaticVersionFile { } #[cfg_attr(coverage_nightly, coverage(off))] - fn remove_plugin(&mut self, _fa: &BackendArg) -> Result<()> { + fn remove_tool(&mut self, _fa: &BackendArg) -> Result<()> { unimplemented!() } diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 6f9100939f..e047e01cf8 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -306,7 +306,7 @@ impl ConfigFile for MiseToml { self.tasks.0.values().collect() } - fn remove_plugin(&mut self, fa: &BackendArg) -> eyre::Result<()> { + fn remove_tool(&mut self, fa: &BackendArg) -> eyre::Result<()> { self.tools.shift_remove(fa); let doc = self.doc_mut()?; if let Some(tools) = doc.get_mut("tools") { @@ -1744,7 +1744,7 @@ mod tests { ) .unwrap(); let mut cf = MiseToml::from_file(&p).unwrap(); - cf.remove_plugin(&"node".into()).unwrap(); + cf.remove_tool(&"node".into()).unwrap(); assert_debug_snapshot!(cf.to_toolset().unwrap()); let cf: Box = Box::new(cf); diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 03babfd452..47e6c25aae 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -80,7 +80,7 @@ pub trait ConfigFile: Debug + Send + Sync { fn tasks(&self) -> Vec<&Task> { Default::default() } - fn remove_plugin(&mut self, ba: &BackendArg) -> eyre::Result<()>; + fn remove_tool(&mut self, ba: &BackendArg) -> eyre::Result<()>; fn replace_versions(&mut self, ba: &BackendArg, versions: Vec) -> eyre::Result<()>; fn save(&self) -> eyre::Result<()>; @@ -214,9 +214,14 @@ fn init(path: &Path) -> Box { } pub fn parse_or_init(path: &Path) -> eyre::Result> { + let path = if path.is_dir() { + path.join("mise.toml") + } else { + path.into() + }; let cf = match path.exists() { - true => parse(path)?, - false => init(path), + true => parse(&path)?, + false => init(&path), }; Ok(cf) } @@ -499,7 +504,11 @@ fn trust_file_hash(path: &Path) -> eyre::Result { } fn detect_config_file_type(path: &Path) -> Option { - match path.file_name().unwrap().to_str().unwrap() { + match path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or("mise.toml") + { f if f.ends_with(".toml") => Some(ConfigFileType::MiseToml), f if env::MISE_OVERRIDE_CONFIG_FILENAMES.contains(f) => Some(ConfigFileType::MiseToml), f if env::MISE_DEFAULT_CONFIG_FILENAME.as_str() == f => Some(ConfigFileType::MiseToml), diff --git a/src/config/config_file/tool_versions.rs b/src/config/config_file/tool_versions.rs index 591245bbdf..1de5779dc7 100644 --- a/src/config/config_file/tool_versions.rs +++ b/src/config/config_file/tool_versions.rs @@ -150,7 +150,7 @@ impl ConfigFile for ToolVersions { self.path.as_path() } - fn remove_plugin(&mut self, fa: &BackendArg) -> Result<()> { + fn remove_tool(&mut self, fa: &BackendArg) -> Result<()> { self.plugins.shift_remove(fa); Ok(()) } diff --git a/xtasks/fig/src/mise.ts b/xtasks/fig/src/mise.ts index ddc293e8bd..42b61def07 100644 --- a/xtasks/fig/src/mise.ts +++ b/xtasks/fig/src/mise.ts @@ -1754,11 +1754,10 @@ const completionSpec: Fig.Spec = { ], "args": [ { - "name": "plugin", - "description": "Prune only versions from this plugin(s)", + "name": "installed_tool", + "description": "Prune only these tools", "isOptional": true, - "isVariadic": true, - "generators": pluginGenerator + "isVariadic": true } ] }, @@ -3048,9 +3047,7 @@ const completionSpec: Fig.Spec = { }, { "name": [ - "uninstall", - "remove", - "rm" + "uninstall" ], "description": "Removes installed tool versions", "options": [ @@ -3119,6 +3116,39 @@ const completionSpec: Fig.Spec = { } ] }, + { + "name": [ + "unuse", + "rm", + "remove" + ], + "description": "Removes installed tool versions from mise.toml", + "options": [ + { + "name": [ + "--no-prune" + ], + "description": "Do not also prune the installed version", + "isRepeatable": false + }, + { + "name": [ + "--global" + ], + "description": "Remove tool from global config", + "isRepeatable": false + } + ], + "args": [ + { + "name": "installed_tool@version", + "description": "Tool(s) to remove", + "isOptional": false, + "isVariadic": true, + "generators": installedToolVersionGenerator + } + ] + }, { "name": [ "upgrade",