[New] nvm install --offline: install from cache without network access

Add `--offline` flag to `nvm install` that resolves versions using only locally installed versions and cached downloads. No network calls are made.

New helper functions `nvm_ls_cached` and `nvm_offline_version` scan `$NVM_DIR/.cache/bin/` for previously downloaded tarballs.
In offline mode, `nvm_download_artifact` returns cached tarballs directly without checksum verification or download attempts.
The curl/wget requirement is skipped when `--offline` is set.
Supports `--lts` via locally stored LTS alias files.
This commit is contained in:
Jordan Harband
2026-03-13 16:13:19 -04:00
parent 14d01c6877
commit 59bd32be6b
4 changed files with 208 additions and 17 deletions

View File

@@ -482,6 +482,22 @@ nvm install-latest-npm
If you've already gotten an error to the effect of "npm does not support Node.js", you'll need to (1) revert to a previous node version (`nvm ls` & `nvm use <your latest _working_ version from the ls>`), (2) delete the newly created node version (`nvm uninstall <your _broken_ version of node from the ls>`), then (3) rerun your `nvm install` with the `--latest-npm` flag. If you've already gotten an error to the effect of "npm does not support Node.js", you'll need to (1) revert to a previous node version (`nvm ls` & `nvm use <your latest _working_ version from the ls>`), (2) delete the newly created node version (`nvm uninstall <your _broken_ version of node from the ls>`), then (3) rerun your `nvm install` with the `--latest-npm` flag.
### Offline Install
If you've previously downloaded a node version (or it's still in the cache), you can install it without any network access using the `--offline` flag:
```sh
nvm install --offline 14.7.0
```
This resolves versions using only locally installed versions and cached downloads. It will not attempt to download anything. This is useful in air-gapped environments, on planes, or when you want to avoid network latency.
You can combine `--offline` with `--lts` to install the latest cached LTS version (as long as LTS aliases have been populated by a prior `nvm ls-remote --lts`):
```sh
nvm install --offline --lts
```
### Default Global Packages From File While Installing ### Default Global Packages From File While Installing
If you have a list of default packages you want installed every time you install a new version, we support that too -- just add the package names, one per line, to the file `$NVM_DIR/default-packages`. You can add anything npm would accept as a package argument on the command line. If you have a list of default packages you want installed every time you install a new version, we support that too -- just add the package names, one per line, to the file `$NVM_DIR/default-packages`. You can add anything npm would accept as a package argument on the command line.

132
nvm.sh
View File

@@ -2490,22 +2490,35 @@ nvm_download_artifact() {
local COMPRESSION local COMPRESSION
COMPRESSION="$(nvm_get_artifact_compression "${VERSION}")" COMPRESSION="$(nvm_get_artifact_compression "${VERSION}")"
local CHECKSUM
CHECKSUM="$(nvm_get_checksum "${FLAVOR}" "${TYPE}" "${VERSION}" "${SLUG}" "${COMPRESSION}")"
local tmpdir local tmpdir
if [ "${KIND}" = 'binary' ]; then if [ "${KIND}" = 'binary' ]; then
tmpdir="$(nvm_cache_dir)/bin/${SLUG}" tmpdir="$(nvm_cache_dir)/bin/${SLUG}"
else else
tmpdir="$(nvm_cache_dir)/src/${SLUG}" tmpdir="$(nvm_cache_dir)/src/${SLUG}"
fi fi
local TARBALL
TARBALL="${tmpdir}/${SLUG}.${COMPRESSION}"
if [ "${NVM_OFFLINE-}" = 1 ]; then
# In offline mode, use cached tarball without checksum or download
if [ -r "${TARBALL}" ]; then
nvm_err "Offline: using cached archive $(nvm_sanitize_path "${TARBALL}")"
nvm_echo "${TARBALL}"
return 0
fi
nvm_err "Offline: no cached archive found for ${SLUG}"
return 4
fi
local CHECKSUM
CHECKSUM="$(nvm_get_checksum "${FLAVOR}" "${TYPE}" "${VERSION}" "${SLUG}" "${COMPRESSION}")"
command mkdir -p "${tmpdir}/files" || ( command mkdir -p "${tmpdir}/files" || (
nvm_err "creating directory ${tmpdir}/files failed" nvm_err "creating directory ${tmpdir}/files failed"
return 3 return 3
) )
local TARBALL
TARBALL="${tmpdir}/${SLUG}.${COMPRESSION}"
local TARBALL_URL local TARBALL_URL
if nvm_version_greater_than_or_equal_to "${VERSION}" 0.1.14; then if nvm_version_greater_than_or_equal_to "${VERSION}" 0.1.14; then
TARBALL_URL="${MIRROR}/${VERSION}/${SLUG}.${COMPRESSION}" TARBALL_URL="${MIRROR}/${VERSION}/${SLUG}.${COMPRESSION}"
@@ -3050,6 +3063,57 @@ nvm_cache_dir() {
nvm_echo "${NVM_DIR}/.cache" nvm_echo "${NVM_DIR}/.cache"
} }
# args: pattern
# Lists versions available in the local cache (not yet installed).
# Returns version numbers like "v18.20.4", one per line, sorted.
nvm_ls_cached() {
local PATTERN
PATTERN="${1-}"
local CACHE_BIN_DIR
CACHE_BIN_DIR="$(nvm_cache_dir)/bin"
if [ ! -d "${CACHE_BIN_DIR}" ]; then
return
fi
local NVM_OS
NVM_OS="$(nvm_get_os)"
local NVM_ARCH
NVM_ARCH="$(nvm_get_arch)"
local SUFFIX
SUFFIX="${NVM_OS}-${NVM_ARCH}"
# shellcheck disable=SC2010
command ls -1 "${CACHE_BIN_DIR}" \
| nvm_grep "^node-v.*-${SUFFIX}\$" \
| command sed "s/^node-\\(v[0-9][0-9.]*\\)-${SUFFIX}\$/\\1/" \
| nvm_grep "$(nvm_ensure_version_prefix "${PATTERN}")" \
| command sort -t. -u -k 1.2,1n -k 2,2n -k 3,3n
}
# args: pattern
# Resolves a version pattern to a single version using only locally
# installed versions and cached downloads. No network access.
nvm_offline_version() {
local PATTERN
PATTERN="${1-}"
# First try locally installed versions
local VERSION
VERSION="$(nvm_version "${PATTERN}")"
if [ "_${VERSION}" != '_N/A' ]; then
nvm_echo "${VERSION}"
return 0
fi
# Then try cached downloads
VERSION="$(nvm_ls_cached "${PATTERN}" | command tail -1)"
if [ -n "${VERSION}" ]; then
nvm_echo "${VERSION}"
return 0
fi
nvm_echo 'N/A'
return 3
}
nvm() { nvm() {
if [ "$#" -lt 1 ]; then if [ "$#" -lt 1 ]; then
nvm --help nvm --help
@@ -3130,6 +3194,7 @@ nvm() {
nvm_echo ' --skip-default-packages When installing, skip the default-packages file if it exists' nvm_echo ' --skip-default-packages When installing, skip the default-packages file if it exists'
nvm_echo ' --latest-npm After installing, attempt to upgrade to the latest working npm on the given node version' nvm_echo ' --latest-npm After installing, attempt to upgrade to the latest working npm on the given node version'
nvm_echo ' --no-progress Disable the progress bar on any downloads' nvm_echo ' --no-progress Disable the progress bar on any downloads'
nvm_echo ' --offline Install from cache only, without downloading anything'
nvm_echo ' --alias=<name> After installing, set the alias specified to the version specified. (same as: nvm alias <name> <version>)' nvm_echo ' --alias=<name> After installing, set the alias specified to the version specified. (same as: nvm alias <name> <version>)'
nvm_echo ' --default After installing, set default alias to the version specified. (same as: nvm alias default <version>)' nvm_echo ' --default After installing, set default alias to the version specified. (same as: nvm alias default <version>)'
nvm_echo ' --save After installing, write the specified version to .nvmrc' nvm_echo ' --save After installing, write the specified version to .nvmrc'
@@ -3335,11 +3400,6 @@ nvm() {
local NVM_OS local NVM_OS
NVM_OS="$(nvm_get_os)" NVM_OS="$(nvm_get_os)"
if ! nvm_has "curl" && ! nvm_has "wget"; then
nvm_err 'nvm needs curl or wget to proceed.'
return 1
fi
if [ $# -lt 1 ]; then if [ $# -lt 1 ]; then
version_not_provided=1 version_not_provided=1
fi fi
@@ -3347,9 +3407,11 @@ nvm() {
local nobinary local nobinary
local nosource local nosource
local noprogress local noprogress
local NVM_OFFLINE
nobinary=0 nobinary=0
noprogress=0 noprogress=0
nosource=0 nosource=0
NVM_OFFLINE=0
local LTS local LTS
local ALIAS local ALIAS
local NVM_UPGRADE_NPM local NVM_UPGRADE_NPM
@@ -3392,6 +3454,10 @@ nvm() {
noprogress=1 noprogress=1
shift shift
;; ;;
--offline)
NVM_OFFLINE=1
shift
;;
--lts) --lts)
LTS='*' LTS='*'
shift shift
@@ -3468,6 +3534,11 @@ nvm() {
esac esac
done done
if [ "${NVM_OFFLINE}" != 1 ] && ! nvm_has "curl" && ! nvm_has "wget"; then
nvm_err 'nvm needs curl or wget to proceed.'
return 1
fi
local provided_version local provided_version
provided_version="${1-}" provided_version="${1-}"
@@ -3508,8 +3579,27 @@ nvm() {
esac esac
local EXIT_CODE local EXIT_CODE
VERSION="$(NVM_VERSION_ONLY=true NVM_LTS="${LTS-}" nvm_remote_version "${provided_version}")"
EXIT_CODE="$?" if [ "${NVM_OFFLINE}" = 1 ]; then
local OFFLINE_PATTERN
OFFLINE_PATTERN="${provided_version}"
if [ -n "${LTS-}" ]; then
if [ "${LTS}" = '*' ]; then
OFFLINE_PATTERN="$(nvm_resolve_alias 'lts/*' 2>/dev/null || nvm_echo)"
else
OFFLINE_PATTERN="$(nvm_resolve_alias "lts/${LTS}" 2>/dev/null || nvm_echo)"
fi
if [ -z "${OFFLINE_PATTERN}" ]; then
nvm_err "LTS alias '${LTS}' not found locally. Run \`nvm ls-remote --lts\` first to populate LTS aliases."
return 3
fi
fi
VERSION="$(nvm_offline_version "${OFFLINE_PATTERN}")"
EXIT_CODE="$?"
else
VERSION="$(NVM_VERSION_ONLY=true NVM_LTS="${LTS-}" nvm_remote_version "${provided_version}")"
EXIT_CODE="$?"
fi
if [ "${VERSION}" = 'N/A' ] || [ $EXIT_CODE -ne 0 ]; then if [ "${VERSION}" = 'N/A' ] || [ $EXIT_CODE -ne 0 ]; then
local LTS_MSG local LTS_MSG
@@ -3525,9 +3615,17 @@ nvm() {
return 3 return 3
fi fi
else else
REMOTE_CMD='nvm ls-remote' if [ "${NVM_OFFLINE}" = 1 ]; then
REMOTE_CMD='nvm ls'
else
REMOTE_CMD='nvm ls-remote'
fi
fi
if [ "${NVM_OFFLINE}" = 1 ]; then
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found locally or in cache - try \`${REMOTE_CMD}\` to browse available versions."
else
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found - try \`${REMOTE_CMD}\` to browse available versions."
fi fi
nvm_err "Version '${provided_version}' ${LTS_MSG-}not found - try \`${REMOTE_CMD}\` to browse available versions."
return 3 return 3
fi fi
@@ -3667,7 +3765,7 @@ nvm() {
# skip binary install if "nobinary" option specified. # skip binary install if "nobinary" option specified.
if [ $nobinary -ne 1 ] && nvm_binary_available "${VERSION}"; then if [ $nobinary -ne 1 ] && nvm_binary_available "${VERSION}"; then
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" nvm_install_binary "${FLAVOR}" std "${VERSION}" "${nosource}" NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" NVM_OFFLINE="${NVM_OFFLINE}" nvm_install_binary "${FLAVOR}" std "${VERSION}" "${nosource}"
EXIT_CODE=$? EXIT_CODE=$?
else else
EXIT_CODE=-1 EXIT_CODE=-1
@@ -3686,7 +3784,7 @@ nvm() {
nvm_err 'Installing from source on non-WSL Windows is not supported' nvm_err 'Installing from source on non-WSL Windows is not supported'
EXIT_CODE=87 EXIT_CODE=87
else else
NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" nvm_install_source "${FLAVOR}" std "${VERSION}" "${NVM_MAKE_JOBS}" "${ADDITIONAL_PARAMETERS}" NVM_NO_PROGRESS="${NVM_NO_PROGRESS:-${noprogress}}" NVM_OFFLINE="${NVM_OFFLINE}" nvm_install_source "${FLAVOR}" std "${VERSION}" "${NVM_MAKE_JOBS}" "${ADDITIONAL_PARAMETERS}"
EXIT_CODE=$? EXIT_CODE=$?
fi fi
fi fi
@@ -4520,7 +4618,7 @@ nvm() {
nvm_binary_available nvm_change_path nvm_strip_path \ nvm_binary_available nvm_change_path nvm_strip_path \
nvm_num_version_groups nvm_format_version nvm_ensure_version_prefix \ nvm_num_version_groups nvm_format_version nvm_ensure_version_prefix \
nvm_normalize_version nvm_is_valid_version nvm_normalize_lts \ nvm_normalize_version nvm_is_valid_version nvm_normalize_lts \
nvm_ensure_version_installed nvm_cache_dir \ nvm_ensure_version_installed nvm_cache_dir nvm_ls_cached nvm_offline_version \
nvm_version_path nvm_alias_path nvm_version_dir \ nvm_version_path nvm_alias_path nvm_version_dir \
nvm_find_nvmrc nvm_find_up nvm_find_project_dir nvm_tree_contains_path \ nvm_find_nvmrc nvm_find_up nvm_find_project_dir nvm_tree_contains_path \
nvm_version_greater nvm_version_greater_than_or_equal_to \ nvm_version_greater nvm_version_greater_than_or_equal_to \

View File

@@ -0,0 +1,38 @@
#!/bin/sh
die () { echo "$@" ; exit 1; }
\. ../../../nvm.sh
\. ../../common.sh
# Mock nvm_download to ensure no network access
nvm_download() {
die "nvm_download should not be called in offline mode"
}
# --offline with an already-installed version should succeed
INSTALLED_VERSION="$(nvm ls | command tail -1 | command awk '{print $1}' | command sed 's/\x1b\[[0-9;]*m//g')"
if [ -n "${INSTALLED_VERSION}" ] && [ "_${INSTALLED_VERSION}" != '_N/A' ] && [ "_${INSTALLED_VERSION}" != '_system' ]; then
try nvm install --offline "${INSTALLED_VERSION}"
[ "_$CAPTURED_EXIT_CODE" = "_0" ] \
|| die "nvm install --offline with installed version '${INSTALLED_VERSION}' should succeed, got exit code $CAPTURED_EXIT_CODE"
fi
# --offline with a nonexistent version should fail
try_err nvm install --offline 999.999.999
[ "_$CAPTURED_EXIT_CODE" != "_0" ] \
|| die "nvm install --offline with nonexistent version should fail"
EXPECTED_ERR="not found locally or in cache"
nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "${EXPECTED_ERR}" \
|| die "nvm install --offline error should mention 'not found locally or in cache'; got '$CAPTURED_STDERR'"
# --offline should not require curl or wget
nvm_has() { return 1; }
try_err nvm install --offline 999.999.999
# Should fail with "not found" not "nvm needs curl or wget"
nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "curl or wget" \
&& die "nvm install --offline should not require curl or wget"
alias nvm_has='\nvm_has'
unset -f nvm_has

View File

@@ -0,0 +1,39 @@
#!/bin/sh
die () { echo "$@" ; cleanup ; exit 1; }
\. ../../../nvm.sh
\. ../../common.sh
TEST_DIR="$(pwd)/nvm_offline_version_tmp"
cleanup() {
rm -rf "${TEST_DIR}"
}
[ ! -e "${TEST_DIR}" ] && mkdir -p "${TEST_DIR}"
# nvm_offline_version should find installed versions
INSTALLED_VERSION="$(nvm_version node)"
if [ "_${INSTALLED_VERSION}" != '_N/A' ] && [ "_${INSTALLED_VERSION}" != '_system' ]; then
try nvm_offline_version "${INSTALLED_VERSION}"
[ "_$CAPTURED_STDOUT" = "_${INSTALLED_VERSION}" ] \
|| die "nvm_offline_version '${INSTALLED_VERSION}' should return '${INSTALLED_VERSION}'; got '$CAPTURED_STDOUT'"
[ "_$CAPTURED_EXIT_CODE" = "_0" ] \
|| die "nvm_offline_version '${INSTALLED_VERSION}' should exit 0; got '$CAPTURED_EXIT_CODE'"
fi
# nvm_offline_version with nonexistent version should return N/A
try nvm_offline_version "999.999.999"
[ "_$CAPTURED_STDOUT" = "_N/A" ] \
|| die "nvm_offline_version '999.999.999' should return 'N/A'; got '$CAPTURED_STDOUT'"
[ "_$CAPTURED_EXIT_CODE" = "_3" ] \
|| die "nvm_offline_version '999.999.999' should exit 3; got '$CAPTURED_EXIT_CODE'"
# nvm_ls_cached with nonexistent pattern should return nothing
try nvm_ls_cached "999.999"
[ -z "$CAPTURED_STDOUT" ] \
|| die "nvm_ls_cached '999.999' should return empty; got '$CAPTURED_STDOUT'"
cleanup