#!/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
