| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| HERMES_NODE_MIN_VERSION="${HERMES_NODE_MIN_VERSION:-20}" |
| HERMES_NODE_TARGET_MAJOR="${HERMES_NODE_TARGET_MAJOR:-22}" |
| HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" |
| HERMES_NODE_AVAILABLE=false |
|
|
| |
| |
| |
|
|
| _nb_log() { declare -F log_info >/dev/null 2>&1 && log_info "$*" || printf '→ %s\n' "$*" >&2; } |
| _nb_ok() { declare -F log_success >/dev/null 2>&1 && log_success "$*" || printf '✓ %s\n' "$*" >&2; } |
| _nb_warn() { declare -F log_warn >/dev/null 2>&1 && log_warn "$*" || printf '⚠ %s\n' "$*" >&2; } |
|
|
| |
| |
| |
|
|
| _nb_is_termux() { |
| [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]] |
| } |
|
|
| |
| |
| |
| _nb_get_link_dir() { |
| if _nb_is_termux && [ -n "${PREFIX:-}" ]; then |
| echo "$PREFIX/bin" |
| elif [ "$(id -u)" = 0 ] && [ "$(uname -s)" = "Linux" ]; then |
| echo "/usr/local/bin" |
| else |
| echo "$HOME/.local/bin" |
| fi |
| } |
|
|
| |
| |
| |
| |
| |
| _nb_configure_npm_prefix() { |
| [ -x "$HERMES_HOME/node/bin/npm" ] || return 0 |
| local _link_dir |
| _link_dir="$(_nb_get_link_dir)" |
| mkdir -p "$HERMES_HOME/node/etc" |
| printf 'prefix=%s\n' "$(dirname "$_link_dir")" > "$HERMES_HOME/node/etc/npmrc" |
| } |
|
|
| _nb_node_major() { |
| local v |
| v=$(node --version 2>/dev/null | sed 's/^v//' | cut -d. -f1) |
| [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0 |
| } |
|
|
| |
| |
| |
| |
| _nb_npm_range() { |
| if [ -n "${HERMES_NPM_TARGET_RANGE:-}" ]; then |
| printf '%s\n' "$HERMES_NPM_TARGET_RANGE" |
| return 0 |
| fi |
| local repo_root manifest range |
| repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd)" |
| manifest="$repo_root/package.json" |
| if [ -r "$manifest" ]; then |
| |
| range=$(sed -n '/"engines"/,/}/p' "$manifest" \ |
| | sed -n 's/.*"npm"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ |
| | head -1) |
| if [ -n "$range" ]; then |
| printf '%s\n' "$range" |
| return 0 |
| fi |
| fi |
| printf '>=12.0.0\n' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _nb_ensure_bundled_npm_range() { |
| local npm_bin="$HERMES_HOME/node/bin/npm" |
| [ -x "$npm_bin" ] || return 0 |
|
|
| local range have want |
| range="$(_nb_npm_range)" |
| [ -n "$range" ] || return 0 |
|
|
| |
| |
| |
| if [[ "$range" =~ ^\>=([0-9]+) ]]; then |
| want="${BASH_REMATCH[1]}" |
| have=$("$npm_bin" --version 2>/dev/null | cut -d. -f1) |
| if [[ "$have" =~ ^[0-9]+$ ]] && [ "$have" -ge "$want" ]; then |
| return 0 |
| fi |
| fi |
|
|
| _nb_log "Upgrading bundled npm to satisfy $range..." |
| local tmp_cwd |
| tmp_cwd=$(mktemp -d) |
| if ( |
| cd "$tmp_cwd" || exit 1 |
| CI=1 npm_config_min_release_age=0 \ |
| "$npm_bin" install --global \ |
| --prefix "$HERMES_HOME/node" \ |
| "npm@$range" \ |
| --no-fund --no-audit --progress=false >/dev/null 2>&1 |
| ); then |
| rm -rf "$tmp_cwd" |
| _nb_ok "npm $("$npm_bin" --version 2>/dev/null) installed" |
| return 0 |
| fi |
|
|
| rm -rf "$tmp_cwd" |
| _nb_warn "Could not upgrade bundled npm to $range — \`npm ci\` may fail with EBADENGINE." |
| _nb_warn "Fix manually: npm install -g --prefix \"$HERMES_HOME/node\" npm@\"$range\"" |
| return 1 |
| } |
|
|
| |
| |
| |
| |
| _nb_node_is_prerelease() { |
| case "$(node --version 2>/dev/null)" in |
| *-*) return 0 ;; |
| *) return 1 ;; |
| esac |
| } |
|
|
| _nb_have_modern_node() { |
| command -v node >/dev/null 2>&1 || return 1 |
| _nb_node_is_prerelease && return 1 |
| [ "$(_nb_node_major)" -ge "$HERMES_NODE_MIN_VERSION" ] |
| } |
|
|
| |
| |
| |
|
|
| _nb_try_fnm() { |
| command -v fnm >/dev/null 2>&1 || return 1 |
| _nb_log "fnm detected — installing Node $HERMES_NODE_TARGET_MAJOR..." |
| eval "$(fnm env 2>/dev/null)" || true |
| fnm install "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1 |
| fnm use "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1 |
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) activated via fnm" |
| return 0 |
| } |
|
|
| _nb_try_proto() { |
| command -v proto >/dev/null 2>&1 || return 1 |
| _nb_log "proto detected — installing Node $HERMES_NODE_TARGET_MAJOR..." |
| proto install node "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1 |
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) activated via proto" |
| return 0 |
| } |
|
|
| _nb_try_nvm() { |
| local nvm_sh="${NVM_DIR:-$HOME/.nvm}/nvm.sh" |
| [ -s "$nvm_sh" ] || return 1 |
| |
| \. "$nvm_sh" >/dev/null 2>&1 || return 1 |
| _nb_log "nvm detected — installing Node $HERMES_NODE_TARGET_MAJOR..." |
| nvm install "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1 |
| nvm use "$HERMES_NODE_TARGET_MAJOR" >/dev/null 2>&1 || return 1 |
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) activated via nvm" |
| return 0 |
| } |
|
|
| |
| |
| |
|
|
| _nb_try_termux_pkg() { |
| _nb_is_termux || return 1 |
| _nb_log "Installing Node.js via pkg..." |
| pkg install -y nodejs >/dev/null 2>&1 || return 1 |
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) installed via pkg" |
| return 0 |
| } |
|
|
| _nb_try_brew() { |
| [ "$(uname -s)" = "Darwin" ] || return 1 |
| command -v brew >/dev/null 2>&1 || return 1 |
| _nb_log "Installing Node via Homebrew..." |
| brew install "node@${HERMES_NODE_TARGET_MAJOR}" >/dev/null 2>&1 \ |
| || brew install node >/dev/null 2>&1 \ |
| || return 1 |
| brew link --overwrite --force "node@${HERMES_NODE_TARGET_MAJOR}" >/dev/null 2>&1 || true |
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) installed via Homebrew" |
| return 0 |
| } |
|
|
| |
| |
| |
|
|
| _nb_install_bundled_node() { |
| local arch node_arch os_name node_os |
| arch=$(uname -m) |
| case "$arch" in |
| x86_64) node_arch="x64" ;; |
| aarch64|arm64) node_arch="arm64" ;; |
| armv7l) node_arch="armv7l" ;; |
| *) |
| _nb_warn "Unsupported arch ($arch) — install Node.js manually: https://nodejs.org/" |
| return 1 |
| ;; |
| esac |
|
|
| os_name=$(uname -s) |
| case "$os_name" in |
| Linux*) node_os="linux" ;; |
| Darwin*) node_os="darwin" ;; |
| *) |
| _nb_warn "Unsupported OS ($os_name) — install Node.js manually: https://nodejs.org/" |
| return 1 |
| ;; |
| esac |
|
|
| local index_url="https://nodejs.org/dist/latest-v${HERMES_NODE_TARGET_MAJOR}.x/" |
| local tarball="" |
| |
| if command -v xz >/dev/null 2>&1; then |
| tarball=$(curl -fsSL "$index_url" \ |
| | grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \ |
| | head -1) |
| fi |
| if [ -z "$tarball" ]; then |
| tarball=$(curl -fsSL "$index_url" \ |
| | grep -oE "node-v${HERMES_NODE_TARGET_MAJOR}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \ |
| | head -1) |
| fi |
| if [ -z "$tarball" ]; then |
| _nb_warn "Could not resolve Node $HERMES_NODE_TARGET_MAJOR binary for $node_os-$node_arch" |
| return 1 |
| fi |
|
|
| local tmp |
| tmp=$(mktemp -d) |
| _nb_log "Downloading $tarball..." |
| curl -fsSL "${index_url}${tarball}" -o "$tmp/$tarball" || { |
| _nb_warn "Download failed"; rm -rf "$tmp"; return 1 |
| } |
|
|
| _nb_log "Extracting to $HERMES_HOME/node/..." |
| if [[ "$tarball" == *.tar.xz ]]; then |
| tar xf "$tmp/$tarball" -C "$tmp" || { rm -rf "$tmp"; return 1; } |
| else |
| tar xzf "$tmp/$tarball" -C "$tmp" || { rm -rf "$tmp"; return 1; } |
| fi |
|
|
| local extracted |
| extracted=$(find "$tmp" -maxdepth 1 -type d -name 'node-v*' 2>/dev/null | head -1) |
| if [ ! -d "$extracted" ]; then |
| _nb_warn "Extraction produced no node-v* directory" |
| rm -rf "$tmp" |
| return 1 |
| fi |
|
|
| |
| |
| |
| |
| case "$("$extracted/bin/node" --version 2>/dev/null)" in |
| *-*) |
| _nb_warn "Node $("$extracted/bin/node" --version 2>/dev/null) is a pre-release build — native modules cannot be built against it" |
| rm -rf "$tmp" |
| return 1 |
| ;; |
| esac |
|
|
| mkdir -p "$HERMES_HOME" |
| rm -rf "$HERMES_HOME/node" |
| mv "$extracted" "$HERMES_HOME/node" |
| rm -rf "$tmp" |
|
|
| local _link_dir |
| _link_dir="$(_nb_get_link_dir)" |
| |
| |
| |
| |
| if [ "${HERMES_NODE_SKIP_LINKS:-0}" != "1" ]; then |
| mkdir -p "$_link_dir" |
| ln -sf "$HERMES_HOME/node/bin/node" "$_link_dir/node" |
| ln -sf "$HERMES_HOME/node/bin/npm" "$_link_dir/npm" |
| ln -sf "$HERMES_HOME/node/bin/npx" "$_link_dir/npx" |
| fi |
|
|
| _nb_configure_npm_prefix |
|
|
| export PATH="$HERMES_HOME/node/bin:$PATH" |
|
|
| _nb_have_modern_node || return 1 |
| _nb_ok "Node $(node --version) installed to $HERMES_HOME/node/" |
| |
| |
| _nb_ensure_bundled_npm_range || true |
| return 0 |
| } |
|
|
| |
| |
| |
|
|
| _nb_managed_tool_broken() { |
| local tool="$1" |
| local probe |
| for probe in \ |
| "$HERMES_HOME/node/bin/$tool" \ |
| "$HERMES_HOME/node/${tool}.exe" \ |
| "$HERMES_HOME/node/$tool"; do |
| if [ -x "$probe" ] || [ -f "$probe" ]; then |
| if ! "$probe" --version >/dev/null 2>&1; then |
| return 0 |
| fi |
| fi |
| done |
| return 1 |
| } |
|
|
| |
| |
| |
| |
| |
| _nb_managed_node_outdated() { |
| local probe ver major |
| for probe in "$HERMES_HOME/node/bin/node" "$HERMES_HOME/node/node"; do |
| [ -x "$probe" ] || continue |
| ver="$("$probe" --version 2>/dev/null)" || return 1 |
| major="${ver#v}"; major="${major%%.*}" |
| case "$major" in ''|*[!0-9]*) return 1 ;; esac |
| [ "$major" -lt "$HERMES_NODE_TARGET_MAJOR" ] && return 0 |
| return 1 |
| done |
| return 1 |
| } |
|
|
| _nb_managed_node_needs_heal() { |
| local tool |
| for tool in node npm npx; do |
| if _nb_managed_tool_broken "$tool"; then |
| return 0 |
| fi |
| done |
| _nb_managed_node_outdated |
| } |
|
|
| |
| |
| |
| |
| heal_managed_node() { |
| [ -d "$HERMES_HOME/node" ] || return 1 |
| if ! _nb_managed_node_needs_heal; then |
| return 0 |
| fi |
| _nb_log "Hermes-managed Node is broken — redownloading to $HERMES_HOME/node/..." |
| _nb_install_bundled_node |
| } |
|
|
| |
| |
| |
|
|
| ensure_node() { |
| HERMES_NODE_AVAILABLE=false |
|
|
| |
| |
| _nb_configure_npm_prefix |
|
|
| if _nb_have_modern_node; then |
| _nb_ok "Node $(node --version) found" |
| HERMES_NODE_AVAILABLE=true |
| return 0 |
| fi |
|
|
| if [ -x "$HERMES_HOME/node/bin/node" ]; then |
| export PATH="$HERMES_HOME/node/bin:$PATH" |
| if _nb_have_modern_node; then |
| _nb_ok "Node $(node --version) found (Hermes-managed)" |
| HERMES_NODE_AVAILABLE=true |
| |
| |
| |
| |
| |
| |
| _nb_ensure_bundled_npm_range || true |
| return 0 |
| fi |
| fi |
|
|
| |
| _nb_try_fnm && { HERMES_NODE_AVAILABLE=true; return 0; } |
| _nb_try_proto && { HERMES_NODE_AVAILABLE=true; return 0; } |
| _nb_try_nvm && { HERMES_NODE_AVAILABLE=true; return 0; } |
|
|
| |
| _nb_try_termux_pkg && { HERMES_NODE_AVAILABLE=true; return 0; } |
| _nb_try_brew && { HERMES_NODE_AVAILABLE=true; return 0; } |
|
|
| |
| _nb_install_bundled_node && { HERMES_NODE_AVAILABLE=true; return 0; } |
|
|
| _nb_warn "Node.js install failed — TUI and browser tools will be unavailable." |
| _nb_warn "Install manually: https://nodejs.org/en/download/ (or: \`brew install node\`, \`fnm install $HERMES_NODE_TARGET_MAJOR\`, etc.)" |
| return 1 |
| } |
|
|