From ab9984bb15de2f9827a3183e558c874f393781eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 18 Jun 2026 17:54:29 +0200 Subject: [PATCH] feat(cli): support `deno remove --global` as alias for `deno uninstall --global` `deno remove --global ` now removes a globally installed executable script, behaving exactly like `deno uninstall --global `. The flag is parsed in `remove_parse` and routed to the same `Uninstall` subcommand the uninstaller uses, so the two share one code path. `--root` is supported (and requires `--global`), and a global removal targets a single executable, so passing multiple names with `--global` is rejected, matching `uninstall`. Adds flag-parse unit tests and a hermetic spec test that installs a local script globally and removes it via `deno remove --global`. --- cli/args/flags.rs | 123 +++++++++++++++++- .../global/remove_global_alias/__test__.jsonc | 17 +++ .../remove_global_alias/assert_removed.js | 10 ++ .../global/remove_global_alias/install.out | 2 + .../global/remove_global_alias/main.js | 1 + .../global/remove_global_alias/remove.out | 1 + 6 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 tests/specs/install/global/remove_global_alias/__test__.jsonc create mode 100644 tests/specs/install/global/remove_global_alias/assert_removed.js create mode 100644 tests/specs/install/global/remove_global_alias/install.out create mode 100644 tests/specs/install/global/remove_global_alias/main.js create mode 100644 tests/specs/install/global/remove_global_alias/remove.out diff --git a/cli/args/flags.rs b/cli/args/flags.rs index ce83449e5dab7a..947b37d9020ac2 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -2242,7 +2242,7 @@ pub fn flags_from_vec_with_initial_cwd( "add" => add_parse(&mut flags, &mut m)?, "audit" => audit_parse(&mut flags, &mut m)?, "approve-scripts" => approve_scripts_parse(&mut flags, &mut m)?, - "remove" => remove_parse(&mut flags, &mut m), + "remove" => remove_parse(&mut flags, &mut m)?, "bench" => bench_parse(&mut flags, &mut m)?, "bundle" => bundle_parse(&mut flags, &mut m)?, "cache" => cache_parse(&mut flags, &mut m)?, @@ -2801,6 +2801,10 @@ fn remove_subcommand() -> Command { You can remove multiple dependencies at once: deno remove @std/path @std/assert + +With the --global flag, this is an alias for deno uninstall --global and +removes a globally installed executable script: + deno remove --global file_server " ), UnstableArgsConfig::None, @@ -2814,9 +2818,23 @@ You can remove multiple dependencies at once: .num_args(1..) .action(ArgAction::Append), ) + .arg( + Arg::new("root") + .long("root") + .help("Installation root") + .requires("global") + .value_hint(ValueHint::DirPath), + ) + .arg( + Arg::new("global") + .long("global") + .short('g') + .help("Remove globally installed package or module") + .action(ArgAction::SetTrue), + ) .args(lock_args()) - .arg(lockfile_only_arg()) - .arg(package_json_arg()) + .arg(lockfile_only_arg().conflicts_with("global")) + .arg(package_json_arg().conflicts_with("global")) }) } @@ -7291,13 +7309,40 @@ fn add_parse_inner( } } -fn remove_parse(flags: &mut Flags, matches: &mut ArgMatches) { +fn remove_parse( + flags: &mut Flags, + matches: &mut ArgMatches, +) -> clap::error::Result<()> { lock_args_parse(flags, matches); + let mut packages = matches.remove_many::("packages").unwrap(); + + // `deno remove --global ` is an alias for `deno uninstall --global + // `: it removes a globally installed executable rather than a + // dependency from the configuration file, so route it through the same + // subcommand the uninstaller uses. + if matches.get_flag("global") { + let name = packages.next().unwrap(); + // A global removal targets a single executable, matching + // `deno uninstall --global`, so reject any extra names. + if packages.next().is_some() { + return Err(clap::Error::raw( + clap::error::ErrorKind::ArgumentConflict, + "--global only removes a single executable, but multiple packages were provided\n", + )); + } + let root = matches.remove_one::("root"); + flags.subcommand = DenoSubcommand::Uninstall(UninstallFlags { + kind: UninstallKind::Global(UninstallFlagsGlobal { name, root }), + }); + return Ok(()); + } + flags.subcommand = DenoSubcommand::Remove(RemoveFlags { - packages: matches.remove_many::("packages").unwrap().collect(), + packages: packages.collect(), lockfile_only: matches.get_flag("lockfile-only"), package_json: matches.get_flag("package-json"), }); + Ok(()) } fn link_parse( @@ -16136,6 +16181,74 @@ mod tests { ); } + #[test] + fn remove_global_alias_for_uninstall() { + // `deno remove --global ` is an alias for `deno uninstall --global + // ` and produces the exact same subcommand. + for global_flag in ["--global", "-g"] { + let r = + flags_from_vec(svec!["deno", "remove", global_flag, "file_server"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Uninstall(UninstallFlags { + kind: UninstallKind::Global(UninstallFlagsGlobal { + name: "file_server".to_string(), + root: None, + }), + }), + ..Flags::default() + } + ); + } + + // `--root` is honored, just like `deno uninstall --global --root`. + let r = flags_from_vec(svec![ + "deno", + "remove", + "-g", + "--root", + "/user/foo/bar", + "file_server" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Uninstall(UninstallFlags { + kind: UninstallKind::Global(UninstallFlagsGlobal { + name: "file_server".to_string(), + root: Some("/user/foo/bar".to_string()), + }), + }), + ..Flags::default() + } + ); + + // A global removal targets a single executable; extra names are rejected. + let r = + flags_from_vec(svec!["deno", "remove", "-g", "file_server", "chalk"]); + assert!(r.is_err()); + + // `--root` requires `--global`. + let r = + flags_from_vec(svec!["deno", "remove", "--root", "/tmp", "@std/path"]); + assert!(r.is_err()); + + // Without `--global`, removal stays a config-file dependency removal. + let r = flags_from_vec(svec!["deno", "remove", "@david/which"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Remove(RemoveFlags { + packages: svec!["@david/which"], + lockfile_only: false, + package_json: false, + }), + ..Flags::default() + } + ); + } + #[test] fn run_with_frozen_lockfile() { let cases = [ diff --git a/tests/specs/install/global/remove_global_alias/__test__.jsonc b/tests/specs/install/global/remove_global_alias/__test__.jsonc new file mode 100644 index 00000000000000..f9b2671a2a7a53 --- /dev/null +++ b/tests/specs/install/global/remove_global_alias/__test__.jsonc @@ -0,0 +1,17 @@ +{ + "tempDir": true, + "steps": [ + { + "args": "install --global --root ./bins --name deno-test-bin ./main.js", + "output": "install.out" + }, + { + "args": "remove --global --root ./bins deno-test-bin", + "output": "remove.out" + }, + { + "args": "run -A assert_removed.js", + "output": "" + } + ] +} diff --git a/tests/specs/install/global/remove_global_alias/assert_removed.js b/tests/specs/install/global/remove_global_alias/assert_removed.js new file mode 100644 index 00000000000000..42e720d8c014df --- /dev/null +++ b/tests/specs/install/global/remove_global_alias/assert_removed.js @@ -0,0 +1,10 @@ +// `deno remove --global` is an alias for `deno uninstall --global`, so after +// it runs the globally installed executable should be gone from the bin dir. +const binsDir = "./bins/bin"; +for await (const entry of Deno.readDir(binsDir)) { + if (entry.name === "deno-test-bin" || entry.name === "deno-test-bin.cmd") { + throw new Error( + `Expected ${entry.name} to be removed by 'deno remove --global', but it still exists`, + ); + } +} diff --git a/tests/specs/install/global/remove_global_alias/install.out b/tests/specs/install/global/remove_global_alias/install.out new file mode 100644 index 00000000000000..a069bae1000e72 --- /dev/null +++ b/tests/specs/install/global/remove_global_alias/install.out @@ -0,0 +1,2 @@ +✅ Successfully installed deno-test-bin +[WILDCARD] diff --git a/tests/specs/install/global/remove_global_alias/main.js b/tests/specs/install/global/remove_global_alias/main.js new file mode 100644 index 00000000000000..4e5ecc9eaccb20 --- /dev/null +++ b/tests/specs/install/global/remove_global_alias/main.js @@ -0,0 +1 @@ +console.log("hi from deno-test-bin"); diff --git a/tests/specs/install/global/remove_global_alias/remove.out b/tests/specs/install/global/remove_global_alias/remove.out new file mode 100644 index 00000000000000..7fd8afbb3c5b67 --- /dev/null +++ b/tests/specs/install/global/remove_global_alias/remove.out @@ -0,0 +1 @@ +[WILDCARD]✅ Successfully uninstalled deno-test-bin