Sid Gifari From Gifari Industries - BD Cyber Security Team
Home
/
usr
/
share
/
l.v.e-manager
/
utils
/
✏️
Editing: npm_wrapper
#!/bin/bash if [[ $EUID -eq 0 ]]; then echo "This program is not intended to be run as root." 1>&2 exit 1 fi error_msg="Cloudlinux NodeJS Selector demands to store node modules for application in separate folder \ (virtual environment) pointed by symlink called \"node_modules\". That's why application should not contain \ folder/file with such name in application root" CWD=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) source "${CWD}/activate" eval $(${CWD}/set_env_vars.py nodejs) venv_node_modules="${CL_VIRTUAL_ENV}/lib/node_modules" nodejs_npm="$CL_NODEHOME/usr/bin/npm" # set_env_vars did not resolve this invocation to an application (its # diagnostics go to stderr, so eval saw nothing): there is no venv to deliver # into and the paths below would be nonsense like "$HOME//node_modules". Run # plain npm and get out, before any bun-delivery logic. if [[ -z "${CL_APP_ROOT:-}" ]]; then exec "${nodejs_npm}" "$@" fi app_node_modules="${HOME}/${CL_APP_ROOT}/node_modules" invalidate_shared_node_modules_marker() { # A failed plain npm can leave a conservative false-negative; never claim # the delivered tree is still intact once npm was allowed to mutate it. rm -f "${venv_node_modules}/.cl-shared-node-modules.json" } # bun install-time delivery (docs/design/nodejs-bun-delivery.md). # # This wrapper is where an account's own `npm install` lands -- a person # over SSH, or an agent in a shell, which is the workload a shared cache # exists for: an agent redeploys dozens of times an afternoon and every one # of those installs re-downloads what the server already has. # # The account cannot link out of the cache itself (the kernel refuses a hard # link to a file it does not own), so the middle phase is asked of a root # daemon over a unix socket. The account keeps the phases that must stay # unprivileged: resolution runs with its own registry configuration, and the # native builds run as itself. # # resolve (account) -> deliver (root, over the socket) -> rebuild (account) # # If any step fails -- daemon stopped, a git dependency, a busy application -- # the install fails and says why, rather than quietly becoming a plain npm # install. Explicit administrator disable/exclusion policy selects classic npm; # every other refusal remains loud. Silently # choosing a different installer is what let two delivery defects # sit behind healthy-looking numbers while this feature was built; an install that looks like it # succeeded is worse than one that stops. BUN_USER_PHASE="/usr/share/l.v.e-manager/utils/bun_user_phase" BUN_DELIVERY_CONF="/opt/cl-bun/etc/node-modules-storage.json" NODE_MODULES_STORAGE_PYTHON="/opt/cloudlinux/venv/bin/python3" NODE_MODULES_STORAGE_READER="${NODE_MODULES_STORAGE_PYTHON}" DELIVERY_CLIENT="/usr/share/l.v.e-manager/utils/cloudlinux_bun_client.py" close_selector_lock_copy_for_npm() { # Selector itself keeps the same open-file-description locked until the # npm operation and its restart/health phases finish. Drop only this # wrapper child's descriptor before npm starts, otherwise a lifecycle # script that backgrounds a process could inherit the application lock # indefinitely after Selector returns. local selector_fd="${CL_SELECTOR_APP_LOCK_FD:-}" local lock_file="$(dirname "${CL_VIRTUAL_ENV}")/.lock" [[ "${selector_fd}" =~ ^[0-9]+$ ]] || return 0 [ -e "/proc/self/fd/${selector_fd}" ] || return 0 [ "$(stat -Lc '%d:%i' "/proc/self/fd/${selector_fd}" 2>/dev/null)" \ = "$(stat -Lc '%d:%i' "${lock_file}" 2>/dev/null)" ] || return 0 eval "exec ${selector_fd}>&-" } discard_restored_private_marker() { # Account backups materialize hardlinks as ordinary private files but may # copy the delivery marker. With no server configuration, discard only a # regular account-owned marker after a bounded walk proves the whole tree # contains no shared regular inode or unsafe node type. A restored # root-owned single-link file is private and npm can replace its directory # entry without gaining write access to the inode. # Any ambiguity leaves the marker in place for privileged fail-closed # unshare handling below. local modules="${venv_node_modules}" local marker="${modules}/.cl-shared-node-modules.json" local account_uid unsafe scan_status [ ! -e "${BUN_DELIVERY_CONF}" ] || return 1 [ -d "${modules}" ] && [ ! -L "${modules}" ] || return 1 [ -f "${marker}" ] && [ ! -L "${marker}" ] || return 1 account_uid=$(id -u) || return 1 [ "$(stat -c '%u' -- "${marker}" 2>/dev/null)" = "${account_uid}" ] \ || return 1 command -v timeout >/dev/null 2>&1 || return 1 # Only a foreign-owned linked inode can be a store inode: with # fs.protected_hardlinks=1 the account cannot link a file it does not # own, so its own linked bytes (`cp -al`, `rsync -aH` restores) are # private however many links they carry. unsafe=$(timeout 5 find "${modules}" -xdev \ \( \( -type f -links +1 ! -uid "${account_uid}" \) \ -o \( ! -type d ! -type f ! -type l \) \) \ -print -quit 2>/dev/null) scan_status=$? [ "${scan_status}" -eq 0 ] && [ -z "${unsafe}" ] || return 1 rm -f -- "${marker}" } # A self-contained account restore has private bytes even when its copied # telemetry marker says the source application used the Store. Retire that # stale marker only after the private-tree proof above; a failed proof keeps # the Shared Store-aware recovery path. if [ ! -e "${BUN_DELIVERY_CONF}" ] \ && [ -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then discard_restored_private_marker || true fi unverified_sharing_scan() { # The bounded scan could not answer and there is no marker. Failing # closed here would put every npm call of a markerless application on # the heavy path -- including root unshare and its budget -- on every # disabled-config host, and under LVE IO a large tree loses that race # routinely. Only a configuration that may enable delivery keeps the # conservative verdict. config_may_enable_delivery && return 0 echo "Shared node_modules Store: could not verify node_modules sharing;" \ "using classic npm" 1>&2 return 1 } scan_shared_tree() { local modules="${CL_VIRTUAL_ENV}/lib/node_modules" local account_uid shared_file scan_status [ -e "${modules}/.cl-shared-node-modules.json" ] && return 0 [ -d "${modules}" ] || return 1 command -v timeout >/dev/null 2>&1 || { unverified_sharing_scan; return $?; } account_uid=$(id -u) || { unverified_sharing_scan; return $?; } # pnpm, `cp -l` and backup tools may create account-owned hardlinks. # Shared Store cache inodes stay foreign (normally root-owned), so only # those ambiguous links need privileged fail-closed recovery. shared_file=$(timeout 5 find "${modules}" -xdev -type f -links +1 \ ! -uid "${account_uid}" \ -print -quit 2>/dev/null) scan_status=$? [ "${scan_status}" -eq 0 ] || { unverified_sharing_scan; return $?; } [ -n "${shared_file}" ] } shared_tree_verdict='' shared_tree_may_exist() { # One scan per process: the compatibility gate and the pre-unshare check # ask the same question, and the answer is a bounded walk of the whole # tree. Nothing between them changes this tree -- the app lock is held # before the second caller runs. if [ -z "${shared_tree_verdict}" ]; then scan_shared_tree shared_tree_verdict=$? fi return "${shared_tree_verdict}" } config_may_enable_delivery() { [ -e "${BUN_DELIVERY_CONF}" ] || return 1 [ -r "${BUN_DELIVERY_CONF}" ] || return 0 grep -q '"enabled"[[:space:]]*:[[:space:]]*true' "${BUN_DELIVERY_CONF}" 2>/dev/null || return 1 return 0 } run_legacy_npm() { # This is the exact pre-Shared-Store wrapper contract: only the historical # setup/list spellings target the Selector venv; everything else reaches # npm unchanged. Keep it in one helper so an absent and a disabled config # cannot drift apart again. if [[ "$*" =~ ^(install|i|add|list|la|ll)$ \ || "$*" =~ ^(install|i|add|list|la|ll)[[:space:]].*$ ]]; then if [[ -L "${app_node_modules}" ]]; then rm -f "${app_node_modules}" \ || { echo "Can't remove symlink ${app_node_modules}" >&2; exit 1; } elif [[ -d "${app_node_modules}" || -f "${app_node_modules}" ]]; then echo "${error_msg}" >&2 exit 1 fi mkdir -p "${venv_node_modules}" || exit 1 ln -fs "${venv_node_modules}" "${app_node_modules}" || exit 1 ln -sf "${HOME}/${CL_APP_ROOT}/package.json" \ "${CL_VIRTUAL_ENV}/lib/package.json" || exit 1 close_selector_lock_copy_for_npm exec "${nodejs_npm}" "$@" --prefix="${CL_VIRTUAL_ENV}/lib" fi close_selector_lock_copy_for_npm exec "${nodejs_npm}" "$@" } # Default-off compatibility is an explicit early branch. On a server where # delivery is not enabled and the application has no marker or shared inode, # preserve the wrapper shipped before this feature: only setup/list commands # target the Selector venv; every other npm command is an untouched exec. A # config written only to change limits must not permanently wrap every app. # Ambiguous or actually shared trees remain on the fail-closed recovery path. if [ ! -e "${BUN_DELIVERY_CONF}" ] \ && [ ! -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then run_legacy_npm "$@" fi # An administrator may publish a disabled config merely to tune limits. That # must not opt every untouched application into the new lock/prepare path. # Only a delivery marker or a foreign-owned linked inode needs the # compatibility/recovery path below. Canonical Selector links and lockfiles # predate Shared Store and must not opt an ordinary application into it. if [ -e "${BUN_DELIVERY_CONF}" ] \ && ! config_may_enable_delivery \ && ! shared_tree_may_exist; then run_legacy_npm "$@" fi bun_delivery_state() { # 0 means ask the authenticated daemon for this account's policy, 1 means # the plain npm path because the global feature is off, and 2 means the # public configuration reader could not run. Account names are never # present in this CageFS-readable file. # # Refuse-on-error exists so a delivery that should happen is never # silently skipped -- which is a reason only where the config plausibly # says enabled. The file is written by cl-node-modules-storage alone, # so absent, or with no plain `"enabled": true` in it, is the fleet # that never turned the feature on: that is decided right here in # bash, and a broken interpreter or venv cannot turn every account's # `npm install` into a refusal there. Anything that *looks* enabled # goes to the authoritative reader (a nested "enabled" in an unknown # key must not enable the feature by grep), and only there does a # reader that cannot run refuse. [ -e "$1" ] || return 1 grep -q '"enabled"[[:space:]]*:[[:space:]]*true' "$1" 2>/dev/null || return 1 # Isolated mode keeps the account's environment and working directory # out of Python imports. "${NODE_MODULES_STORAGE_READER}" -Ibb -c ' import sys try: from clselector import node_modules_storage config = node_modules_storage.read_public_config(sys.argv[1]) applies = config["enabled"] and node_modules_storage.nodejs_selector_supported() except Exception as error: sys.stderr.write("Node modules storage configuration error: %s\\n" % error) sys.exit(2) sys.exit(0 if applies else 1) ' "$1" } bun_delivery_run() ( lock_classic || return $? # A freshly created application has only <venv>/<major>/bin, and the # daemon finds the venv by its library directory: without this the very # first `npm install` over SSH is refused before anything is installed. # The classic and resolve paths create the same account-owned directory. mkdir -p "${CL_VIRTUAL_ENV}/lib" || return 1 "${NODE_MODULES_STORAGE_PYTHON}" "${DELIVERY_CLIENT}" \ policy "${CL_APP_ROOT}" policy_status=$? [ "${policy_status}" -eq 0 ] || return "${policy_status}" # The phase keeps stdout clean for exactly these sentinels; bun's own # diagnosis goes to stderr and reaches the account unchanged. resolve_sentinel="$("${BUN_USER_PHASE}" resolve "${CL_APP_ROOT}")" resolve_status=$? # Exit 3 is the classic-npm route, but the phase also propagates bun's # own status: without the sentinel a future bun exit 3 would send every # account to classic npm silently. The code alone is not the contract. if [ "${resolve_status}" -eq 3 ] \ && { [ "${resolve_sentinel}" = 'cl-bun:legacy-package-lock-v1' ] \ || [ "${resolve_sentinel}" = 'cl-bun:unsupported-npm-shrinkwrap' ] \ || [ "${resolve_sentinel}" = 'cl-bun:no-dependencies' ] \ || case "${resolve_sentinel}" in 'cl-bun:unsupported-manifest '*) true ;; *) false ;; esac; }; then return 3 fi if [ "${resolve_status}" -ne 0 ]; then # The phase has already printed bun's own diagnosis; name the phase # so the account knows npm never ran and the tree is unchanged. echo "Shared node_modules Store: resolve phase failed;" \ "npm install was not run" 1>&2 return 1 fi # Delivery now keeps the old tree in its privileged transaction while it # invokes the account-side rebuild; only a successful rebuild commits. "${BUN_USER_PHASE}" deliver "${CL_APP_ROOT}" || return 1 return 0 ) bun_delivery_needs_unshare() { # A marker or linked inode proves Shared Store may have created this tree. # A bounded-scan failure stays shared/fail-closed only while the # configuration may enable delivery; see unverified_sharing_scan. shared_tree_may_exist } lock_classic() { # All setup and classic mutation paths take this descriptor before they # inspect or alter the app-root node_modules link. Direct SSH npm inherits # this wrapper-owned descriptor; a Selector child drops only its inherited # duplicate before npm because the Selector parent keeps the same OFD. local lock_file lock_file="$(dirname "${CL_VIRTUAL_ENV}")/.lock" if [[ "${CL_SELECTOR_APP_LOCK_FD:-}" =~ ^[0-9]+$ ]] \ && [ -e "/proc/self/fd/${CL_SELECTOR_APP_LOCK_FD}" ] \ && [ "$(stat -Lc '%d:%i' "/proc/self/fd/${CL_SELECTOR_APP_LOCK_FD}" 2>/dev/null)" \ = "$(stat -Lc '%d:%i' "${lock_file}" 2>/dev/null)" ]; then bun_lock_fd="${CL_SELECTOR_APP_LOCK_FD}" flock -n "${bun_lock_fd}" \ || { echo "application is locked by another operation" >&2; return 1; } export CL_BUN_LOCK_FD="${bun_lock_fd}" return 0 fi exec {bun_lock_fd}>"${lock_file}" || return 1 flock -n "${bun_lock_fd}" || { echo "application is locked by another operation" >&2; return 1; } export CL_BUN_LOCK_FD="${bun_lock_fd}" } lock_and_prepare_classic() { lock_classic || return $? # The common user phase canonicalizes unambiguous legacy prefix lockfiles # while this app lock is held. Divergent regular copies are preserved for # classic npm. Do this before scanning or asking root to unshare. classic_canonical_locks="" classic_prepare_needed=false if [ ! -e "${BUN_DELIVERY_CONF}" ] \ && [ ! -e "${venv_node_modules}/.cl-shared-node-modules.json" ]; then return 0 fi classic_prepare_needed=true "${BUN_USER_PHASE}" prepare-classic "${CL_APP_ROOT}" || return 1 local lockfile app_lock venv_lock for lockfile in package-lock.json npm-shrinkwrap.json yarn.lock; do app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}" venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}" if [[ -L "${venv_lock}" && "$(readlink "${venv_lock}")" == "${app_lock}" ]]; then classic_canonical_locks="${classic_canonical_locks} ${lockfile}" fi done } finish_classic_lockfiles() { # npm writes prefix lockfiles with rename(2), replacing a canonical # symlink with a regular file. Only aliases observed after the locked # preflight are authoritative here; a historical divergent pair was not # an alias and remains untouched for classic compatibility. local lockfile app_lock venv_lock [ "${classic_prepare_needed:-false}" = true ] || return 0 # Validate every observed alias before moving any npm output. A malformed # later lock must not leave an earlier app-root lock partly updated. for lockfile in ${classic_canonical_locks:-}; do app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}" venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}" if [[ -L "${venv_lock}" ]]; then [[ "$(readlink "${venv_lock}")" == "${app_lock}" ]] || return 1 elif [[ -f "${venv_lock}" ]]; then [[ ! -L "${app_lock}" && ( ! -e "${app_lock}" || -f "${app_lock}" ) ]] || return 1 elif [[ -e "${venv_lock}" ]]; then return 1 elif [[ -e "${app_lock}" || -L "${app_lock}" ]]; then [[ -f "${app_lock}" && ! -L "${app_lock}" ]] || return 1 fi done for lockfile in ${classic_canonical_locks:-}; do app_lock="${HOME}/${CL_APP_ROOT}/${lockfile}" venv_lock="${CL_VIRTUAL_ENV}/lib/${lockfile}" if [[ -L "${venv_lock}" ]]; then [[ "$(readlink "${venv_lock}")" == "${app_lock}" ]] || return 1 continue fi if [[ -f "${venv_lock}" ]]; then [[ ! -L "${app_lock}" && ( ! -e "${app_lock}" || -f "${app_lock}" ) ]] || return 1 mv -f -- "${venv_lock}" "${app_lock}" || return 1 ln -s -- "${app_lock}" "${venv_lock}" || return 1 elif [[ -e "${venv_lock}" ]]; then return 1 elif [[ -f "${app_lock}" && ! -L "${app_lock}" ]]; then ln -s -- "${app_lock}" "${venv_lock}" || return 1 elif [[ -e "${app_lock}" || -L "${app_lock}" ]]; then return 1 fi done "${BUN_USER_PHASE}" prepare-classic "${CL_APP_ROOT}" } bun_unshare_before_classic() { # A classic npm mutation and the optional unshare are one transaction: # take the same app lock before even scanning, then let exec npm inherit # this descriptor. This keeps Selector/delivery from swapping the tree # while npm changes it. lock_and_prepare_classic || return $? unshare_after_classic_lock } unshare_after_classic_lock() { bun_delivery_needs_unshare || return 0 "${NODE_MODULES_STORAGE_PYTHON}" "${DELIVERY_CLIENT}" unshare "${CL_APP_ROOT}" } validate_setup_node_modules_link() { if [[ -L "${app_node_modules}" ]]; then if [[ "$(readlink "${app_node_modules}")" != "${venv_node_modules}" ]]; then echo "${error_msg}" >&2 return 1 fi elif [[ -e "${app_node_modules}" ]]; then echo "${error_msg}" >&2 return 1 fi } ensure_setup_node_modules_link() { validate_setup_node_modules_link || return $? if [[ ! -e "${app_node_modules}" && ! -L "${app_node_modules}" ]]; then mkdir -p "${venv_node_modules}" || return 1 ln -s "${venv_node_modules}" "${app_node_modules}" || return 1 fi ln -sf "${HOME}/${CL_APP_ROOT}/package.json" "${CL_VIRTUAL_ENV}/lib/package.json" } is_npm_command_name() { # npm accepts configuration flags before its command. Keep this list # broad enough to distinguish an option value from the command that # follows it; only the mutation classifiers below change behavior. case "${1:-}" in access|add|adduser|audit|bugs|cache|ci|cit|clean-install|clean-install-test|completion|config|ddp|dedupe|deprecate|diff|dist-tag|docs|doctor|edit|exec|explain|explore|find-dupes|fund|get|help|help-search|hook|i|ic|in|init|ins|inst|insta|instal|install|install-ci-test|install-clean|install-test|isnt|isnta|isntal|isntall|isntall-clean|it|la|link|list|ll|ln|login|logout|ls|org|outdated|owner|pack|ping|pkg|prefix|profile|prune|publish|query|rb|r|rebuild|remove|repo|restart|rm|root|run|run-script|s|se|search|set|shrinkwrap|sit|star|stars|start|stop|t|team|test|token|tst|un|uninstall|unlink|unpublish|unstar|up|update|upgrade|udpate|version|view|whoami|x) return 0 ;; esac return 1 } npm_option_takes_value() { # Options whose separate value may itself look like an npm command need # an explicit entry. Other unknown leading options are handled below by # consuming a following non-command token as their probable value. case "${1:-}" in -C|-w|--access|--before|--cache|--cafile|--cert|--cpu|--fetch-retries|--fetch-retry-factor|--fetch-retry-maxtimeout|--fetch-retry-mintimeout|--fetch-timeout|--https-proxy|--include|--install-strategy|--key|--libc|--location|--loglevel|--maxsockets|--node-options|--omit|--otp|--os|--prefix|--proxy|--registry|--scope|--script-shell|--tag|--userconfig|--workspace) return 0 ;; esac return 1 } classify_npm_command_args() { # Preserve the original argv for npm. Strip only leading global options # from the copy used to decide whether the invocation mutates the tree. local option while [ "$#" -gt 0 ]; do case "$1" in --) shift break ;; --*=*|-?=*) shift ;; -*) option=$1 shift if npm_option_takes_value "${option}"; then [ "$#" -gt 0 ] && shift elif [ "$#" -gt 0 ] && [[ "$1" != -* ]] \ && ! is_npm_command_name "$1"; then # npm configuration keys may be supplied as # `--key value`. An unknown non-command token is the # value; continue looking for the actual command. shift fi ;; *) break ;; esac done npm_classification_args=("$@") } inspect_npm_mode_options() { # These options change whether npm can touch this application's # node_modules at all. Inspect the original argv up to `--`; unlike the # command classifier, this does not need to remove or reorder anything. npm_global_mode=false npm_preview_mode=false npm_lockfile_only=false npm_explicit_prefix=false # npm takes the last prefix it is given; so do we, or an account could # name this venv first and something else last and be handled as this # application while npm installed elsewhere (or the reverse). npm_prefix_value= while [ "$#" -gt 0 ]; do case "$1" in --) break ;; -g|--global|--global=true|--location=global) npm_global_mode=true ;; --location) shift if [ "${1:-}" = global ]; then npm_global_mode=true fi ;; --prefix|-C) npm_explicit_prefix=true shift npm_prefix_value="${1:-}" ;; --prefix=*|-C=*) npm_explicit_prefix=true npm_prefix_value="${1#*=}" ;; --dry-run|--dry-run=true|--help|--help=true|-h|-\?) npm_preview_mode=true ;; --package-lock-only|--package-lock-only=true) npm_lockfile_only=true ;; esac [ "$#" -gt 0 ] && shift done } npm_prefix_targets_application() { # An explicit prefix that resolves to this application's venv IS this # application, whatever npm calls it: `npm install --prefix # ~/nodevenv/app/22/lib pkg` mutates exactly the tree a bare install # does. Treating every explicit prefix as "somewhere else" let that # through with no application lock, no unshare of a still-shared tree, # and a marker left claiming the tree was intact. local prefix="${1:-}" resolved venv_resolved [ -n "${prefix}" ] || return 1 [ -n "${CL_VIRTUAL_ENV:-}" ] || return 1 # `realpath` is coreutils and is in every cage. If it is not, we cannot # tell the two apart, and the safe answer is the locked application path # rather than an unprotected mutation of a possibly shared tree. resolved="$(realpath -m -- "${prefix}" 2>/dev/null)" || return 0 venv_resolved="$(realpath -m -- "${CL_VIRTUAL_ENV}" 2>/dev/null)" || return 0 [ -n "${resolved}" ] && [ -n "${venv_resolved}" ] || return 0 case "${resolved}" in "${venv_resolved}"|"${venv_resolved}"/*) return 0 ;; esac return 1 } is_setup_command() { case "${1:-}" in install|i|in|ins|inst|insta|instal|isnt|isnta|isntal|isntall|add|ci|clean-install|ic|install-clean|isntall-clean|install-test|it|install-ci-test|cit|clean-install-test|sit) return 0 ;; esac return 1 } is_read_only_setup_command() { case "${1:-}" in list|la|ll) return 0 ;; esac return 1 } is_shared_node_modules_install() { [ "$#" -eq 1 ] || return 1 case "$1" in install|i) return 0 ;; esac return 1 } is_classic_npm_mutation() { case "${1:-}" in install|i|in|ins|inst|insta|instal|isnt|isnta|isntal|isntall|add|ci|clean-install|ic|install-clean|isntall-clean|install-test|it|install-ci-test|cit|clean-install-test|sit|update|upgrade|up|udpate|uninstall|unlink|remove|rm|r|un|prune|dedupe|ddp|link|ln|rebuild|rb) return 0 ;; audit) local audit_arg shift for audit_arg in "$@"; do case "$audit_arg" in fix|--fix) return 0 ;; esac done return 1 ;; esac return 1 } classify_npm_command_args "$@" inspect_npm_mode_options "$@" # Global npm and a prefix outside this venv have a different target; help/dry-run # does not change this application tree. Preserve npm's original argv and avoid # any application lock, unshare, marker, link, or prefix side effect. npm_explicit_application_prefix=false if [ "${npm_explicit_prefix}" = true ] \ && npm_prefix_targets_application "${npm_prefix_value}"; then npm_explicit_application_prefix=true fi if [ "${npm_global_mode}" = true ] \ || [ "${npm_preview_mode}" = true ] \ || { [ "${npm_explicit_prefix}" = true ] \ && [ "${npm_explicit_application_prefix}" = false ]; }; then close_selector_lock_copy_for_npm exec "${nodejs_npm}" "$@" fi # Everything below runs npm against this application. It normally names the # venv library as the prefix itself; when the account already named a prefix # inside this venv, npm keeps the argv it was given -- appending a second # --prefix would silently retarget theirs. npm_prefix_args=(--prefix="${CL_VIRTUAL_ENV}/lib") if [ "${npm_explicit_application_prefix}" = true ]; then npm_prefix_args=() fi # package-lock-only changes dependency metadata but not node_modules. Keep it # serialized with delivery and canonicalize unambiguous legacy lock inputs, # but do not detach a shared tree or invalidate its marker. if [ "${npm_lockfile_only}" = true ] \ && is_classic_npm_mutation "${npm_classification_args[@]}"; then lock_and_prepare_classic || exit $? # npm resolves the selected prefix from venv lib, so expose the same # application manifest as the ordinary setup path without creating or # detaching node_modules. ln -sf "${HOME}/${CL_APP_ROOT}/package.json" \ "${CL_VIRTUAL_ENV}/lib/package.json" || exit 1 close_selector_lock_copy_for_npm "${nodejs_npm}" "$@" "${npm_prefix_args[@]}" npm_status=$? finish_classic_lockfiles prepare_status=$? if [ "${npm_status}" -ne 0 ]; then exit "${npm_status}" fi exit "${prepare_status}" fi # Commands that populate the venv have a common setup path. Only the two # canonical bare install spellings can use Bun delivery; every alias remains # a classic npm mutation under the app lock. if is_read_only_setup_command "${npm_classification_args[@]}"; then # Metadata queries can run while Selector owns the application flock. # They must not canonicalize inputs, alter links, or detach a tree. close_selector_lock_copy_for_npm exec "${nodejs_npm}" "$@" "${npm_prefix_args[@]}" elif is_setup_command "${npm_classification_args[@]}"; then # A bare `install`/`i` is the whole tree, which is what the cache can # serve. Named packages, uninstall and update change package.json first # and stay on the plain path. if is_shared_node_modules_install "$@"; then bun_delivery_state "${BUN_DELIVERY_CONF}" delivery_state=$? case "${delivery_state}" in 0) bun_delivery_run delivery_status=$? [ "${delivery_status}" -eq 3 ] || exit "${delivery_status}" ;; 1) ;; *) echo "Node modules storage could not be checked; refusing to run npm." 1>&2 exit "${delivery_state}" ;; esac fi if is_classic_npm_mutation "${npm_classification_args[@]}"; then lock_and_prepare_classic || exit $? validate_setup_node_modules_link || exit $? unshare_after_classic_lock || exit $? invalidate_shared_node_modules_marker else lock_classic || exit $? fi ensure_setup_node_modules_link || exit $? # With no existing lock input npm must create a regular prefix lock: # npm 10 rejects a dangling package-lock symlink. Keep this shell (and # therefore the application lock) around long enough to move the new # lock to the canonical app root and replace it with the venv alias. close_selector_lock_copy_for_npm "${nodejs_npm}" "$@" "${npm_prefix_args[@]}" npm_status=$? finish_classic_lockfiles prepare_status=$? if [ "${npm_status}" -ne 0 ]; then exit "${npm_status}" fi exit "${prepare_status}" else if is_classic_npm_mutation "${npm_classification_args[@]}"; then bun_unshare_before_classic || exit $? invalidate_shared_node_modules_marker fi # npm invoked from the application root may replace its node_modules # symlink while updating a now-private delivered tree. Preserve only the # selector's exact link by making the venv library the npm prefix; a # user-created real directory (or another link) remains untouched. if is_classic_npm_mutation "${npm_classification_args[@]}" \ && [[ -L "${app_node_modules}" \ && "$(readlink "${app_node_modules}")" == "${venv_node_modules}" ]]; then close_selector_lock_copy_for_npm "${nodejs_npm}" "$@" "${npm_prefix_args[@]}" npm_status=$? finish_classic_lockfiles prepare_status=$? if [ "${npm_status}" -ne 0 ]; then exit "${npm_status}" fi exit "${prepare_status}" fi close_selector_lock_copy_for_npm exec "${nodejs_npm}" "$@" fi
💾 Save
❌ Cancel