Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- [io.js](#iojs)
- [System Version of Node](#system-version-of-node)
- [Listing Versions](#listing-versions)
- [Pruning old versions](#pruning-old-versions)
- [Setting Custom Colors](#setting-custom-colors)
- [Persisting custom colors](#persisting-custom-colors)
- [Suppressing colorized output](#suppressing-colorized-output)
Expand Down Expand Up @@ -553,6 +554,21 @@ If you want to see what versions are available to install:
nvm ls-remote
```

### Pruning old versions

To clean up old, unused Node.js versions, use `nvm prune`:

```sh
nvm prune
```

This will uninstall all installed versions except for the latest one within each major version group (e.g., if you have `v16.1.0` and `v16.2.0`, it will keep `v16.2.0`).

- **Safety**: The currently active version is **never** removed, even if it is not the latest in its group.
- **Global Modules**: `nvm prune` will warn you if it's about to uninstall a version that contains global npm modules (other than `npm` itself).
- **Preview**: Use `nvm prune --dry-run` to see which versions would be removed without actually uninstalling them.
- **Minor Version Retention**: Use `nvm prune --minor` to keep the latest version of each **minor** release group instead of major.

### Setting Custom Colors

You can set five colors that will be used to display version and alias information. These colors replace the default colors.
Expand Down
177 changes: 177 additions & 0 deletions nvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,176 @@ nvm_ls_remote() {
NVM_LTS="${NVM_LTS-}" nvm_ls_remote_index_tab node std "${PATTERN}"
}

nvm_prune() {
local NVM_DRY_RUN
NVM_DRY_RUN=0
local KEEP_MINOR
KEEP_MINOR=0
local ARG
for ARG in "$@"; do
case "${ARG}" in
--dry-run)
NVM_DRY_RUN=1
;;
--minor)
KEEP_MINOR=1
;;
*)
nvm_err "Unknown argument: ${ARG}"
return 127
;;
esac
done

if [ "${NVM_DEBUG-}" = 1 ]; then
nvm_echo "Pruning old installed versions..."
if [ "${NVM_DRY_RUN}" -eq 1 ]; then
nvm_echo "Dry run mode: no versions will be removed."
fi
fi

local HAS_SORT_V
HAS_SORT_V=0
if command sort -V </dev/null >/dev/null 2>&1; then
HAS_SORT_V=1
fi

local AWK_CMD
AWK_CMD='{
for(i=1;i<=NF;i++) {
if ($i ~ /^(iojs-)?v[0-9]+\.[0-9]+\.[0-9]+$/) {
sub(/^iojs-/, "", $i)
print $i
}
}
}'

local VERSIONS
if [ "${HAS_SORT_V}" -eq 1 ]; then
VERSIONS="$(nvm_ls | command awk "${AWK_CMD}" | command sort -V)"
else
# Fallback to numeric sort on fields roughly for POSIX compliance
# v1.2.3 -> field 1 (1.2n ignores 'v'), field 2, field 3
VERSIONS="$(nvm_ls | command awk "${AWK_CMD}" | command sort -t. -k 1.2,1n -k 2,2n -k 3,3n)"
fi

if [ -z "${VERSIONS}" ]; then
nvm_echo "No versions installed to prune."
return 0
fi

# Ensure the current version is stripped of iojs prefix to match our clean list
local CURRENT_VERSION
CURRENT_VERSION="$(nvm_strip_iojs_prefix "$(nvm_ls_current)")"
if [ "${CURRENT_VERSION}" = "system" ] || [ "${CURRENT_VERSION}" = "none" ]; then
CURRENT_VERSION=""
fi

local PREV_MAJOR
PREV_MAJOR=""
local GROUP_VERSIONS
GROUP_VERSIONS=""

# Append a dummy line to flush the last group
VERSIONS="${VERSIONS}
END"

nvm_is_zsh && setopt local_options shwordsplit
local IFS
IFS='
'
# shellcheck disable=SC2013
for VERSION in ${VERSIONS}; do
unset IFS
local MAJOR
MAJOR=""
if [ "${VERSION}" != "END" ] && [ -n "${VERSION}" ]; then
if [ "${KEEP_MINOR}" -eq 1 ]; then
MAJOR="$(nvm_echo "${VERSION}" | command cut -d. -f1,2)"
else
MAJOR="$(nvm_echo "${VERSION}" | command cut -d. -f1)"
fi
fi

if [ "${MAJOR}" != "${PREV_MAJOR}" ] && [ -n "${PREV_MAJOR}" ]; then
# Process previous group
local LATEST_IN_GROUP
# Get the last word in GROUP_VERSIONS
LATEST_IN_GROUP=""
for V in ${GROUP_VERSIONS}; do
LATEST_IN_GROUP="${V}"
done

for V in ${GROUP_VERSIONS}; do
if [ "${V}" = "${LATEST_IN_GROUP}" ]; then
# It's the latest, keep it
# nvm_echo "Keeping latest: ${V}"
continue
fi
if [ "${V}" = "${CURRENT_VERSION}" ]; then
if [ "${NVM_DEBUG-}" = 1 ]; then
nvm_echo "Keeping current version: ${V}"
fi
continue
fi

# Re-resolve if the version is actually iojs
local UNINSTALL_V
UNINSTALL_V="${V}"
if ! nvm_is_version_installed "${UNINSTALL_V}" >/dev/null 2>&1; then
local IOJS_V
IOJS_V="$(nvm_add_iojs_prefix "${V}")"
if nvm_is_version_installed "${IOJS_V}" >/dev/null 2>&1; then
UNINSTALL_V="${IOJS_V}"
fi
fi

if ! nvm_is_version_installed "${UNINSTALL_V}" >/dev/null 2>&1; then
continue
fi

# Check for global npm modules explicitly
local NODE_MODULES_DIR
NODE_MODULES_DIR="$(nvm_version_path "${UNINSTALL_V}")/lib/node_modules"
local HAS_GLOBALS
HAS_GLOBALS=0
if [ -d "${NODE_MODULES_DIR}" ]; then
local NUM_GLOBALS
NUM_GLOBALS="$(command ls -1 "${NODE_MODULES_DIR}" 2>/dev/null | nvm_grep -v '^npm$' | command wc -l | command awk '{print $1}')"
if [ "${NUM_GLOBALS}" -gt 0 ]; then
HAS_GLOBALS="${NUM_GLOBALS}"
fi
fi

local MSG_SUFFIX
MSG_SUFFIX=""
if [ "${HAS_GLOBALS}" -gt 0 ]; then
MSG_SUFFIX=" (Warning: replacing ${HAS_GLOBALS} global module(s))"
fi

# Prune V
if [ "${NVM_DRY_RUN}" -eq 1 ]; then
nvm_echo "[Dry Run] Uninstalling ${UNINSTALL_V}...${MSG_SUFFIX}"
else
nvm_echo "Uninstalling ${UNINSTALL_V}...${MSG_SUFFIX}"
nvm uninstall "${UNINSTALL_V}" >/dev/null
fi
done

GROUP_VERSIONS=""
fi

if [ "${VERSION}" = "END" ]; then
break
fi

if [ -n "${VERSION}" ]; then
PREV_MAJOR="${MAJOR}"
GROUP_VERSIONS="${GROUP_VERSIONS} ${VERSION}"
fi
done
}

nvm_ls_remote_iojs() {
NVM_LTS="${NVM_LTS-}" nvm_ls_remote_index_tab iojs std "${1-}"
}
Expand Down Expand Up @@ -3217,6 +3387,9 @@ nvm() {
nvm_echo ' nvm uninstall <version> Uninstall a version'
nvm_echo ' nvm uninstall --lts Uninstall using automatic LTS (long-term support) alias `lts/*`, if available.'
nvm_echo ' nvm uninstall --lts=<LTS name> Uninstall using automatic alias for provided LTS line, if available.'
nvm_echo ' nvm prune Uninstall all versions except the latest one for each major version.'
nvm_echo ' --dry-run Preview deletions without executing them.'
nvm_echo ' --minor Uninstall all versions except the latest one for each minor version.'
nvm_echo ' nvm use [<version>] Modify PATH to use <version>. Uses .nvmrc if available and version is omitted.'
nvm_echo ' The following optional arguments, if provided, must appear directly after `nvm use`:'
nvm_echo ' --silent Silences stdout/stderr output'
Expand Down Expand Up @@ -3914,6 +4087,9 @@ nvm() {
nvm unalias "$(command basename "${ALIAS}")"
done
;;
"prune")
nvm_prune "$@"
;;
"deactivate")
local NVM_SILENT
while [ $# -ne 0 ]; do
Expand Down Expand Up @@ -4648,6 +4824,7 @@ nvm() {
nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \
nvm_process_nvmrc nvm_nvmrc_invalid_msg \
nvm_write_nvmrc \
nvm_prune \
>/dev/null 2>&1
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
NVM_CD_FLAGS NVM_BIN NVM_INC NVM_MAKE_JOBS \
Expand Down
97 changes: 97 additions & 0 deletions test/fast/Running 'nvm prune' should remove old versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/bin/sh

# Test: Running 'nvm prune' should remove old versions

die () { echo "$@" ; exit 1; }

# Mocking nvm_ls output logic relies on nvm_ls calling directory listing.
# Instead of mocking filesystem which is complex, we will override nvm_ls and nvm_ls_current.

. ../../nvm.sh

# Mock nvm_ls to return specific versions as if they were installed
# The implementation of nvm_prune calls: nvm_ls --no-colors --no-alias
# We must mock nvm_ls to respond to that or just return the list.
# Note: nvm_prune strips output except vX.Y.Z.
nvm_ls() {
echo " v16.20.2"
echo " v18.20.5"
echo " v18.20.7"
echo " v20.17.0"
echo "-> v22.22.0"
echo " v24.13.0"
}

# Mock nvm_ls_current
nvm_ls_current() {
echo "v22.22.0"
}

# Mock nvm_is_version_installed
nvm_is_version_installed() {
return 0
}

# Mock nvm function to intercept uninstall calls
# nvm_prune calls `nvm uninstall`.
UNINSTALLED_VERSIONS=""
nvm() {
if [ "$1" = "uninstall" ]; then
UNINSTALLED_VERSIONS="${UNINSTALLED_VERSIONS} $2"
return 0
fi
echo "nvm called with unexpected args: $*"
}

# Run prune
nvm_prune

# Expected:
# v16.20.2 (Keep: Only one v16)
# v18.20.5 (Delete: v18.20.7 is newer) -> WAIT. v18.20.7 is newer. So keep 18.20.7. Delete 18.20.5.
# v18.20.7 (Keep: Latest v18)
# v20.17.0 (Keep: Only one v20)
# v22.22.0 (Keep: Current)
# v24.13.0 (Keep: Only one v24)

# Wait, my logic for nvm_prune was to keep latest per major.
# v18.20.5 and v18.20.7 are Major 18.
# Latest is v18.20.7.
# So v18.20.5 should be pruned.

# Check UNINSTALLED_VERSIONS
EXPECTED=" v18.20.5"
if [ "${UNINSTALLED_VERSIONS}" != "${EXPECTED}" ]; then
die "Expected uninstalled: '${EXPECTED}', got: '${UNINSTALLED_VERSIONS}'"
fi

# New Test Case: Current version is NOT the latest.
# v18.1.0 (Current)
# v18.2.0 (Latest)
# v18.0.0 (Old)

nvm_ls() {
echo " v18.0.0"
echo "-> v18.1.0"
echo " v18.2.0"
}

nvm_ls_current() {
echo "v18.1.0"
}

UNINSTALLED_VERSIONS=""
nvm_prune

# Expected:
# v18.0.0 (Prune)
# v18.1.0 (Keep: Current)
# v18.2.0 (Keep: Latest)

EXPECTED=" v18.0.0"

if [ "${UNINSTALLED_VERSIONS}" != "${EXPECTED}" ]; then
die "TestCase 2 Failed. Expected uninstalled: '${EXPECTED}', got: '${UNINSTALLED_VERSIONS}'"
fi

echo "All tests passed"
Loading