#!/usr/bin/env bash # Vloopex installer for macOS and Linux. It provides Node.js, then runs # setup.mjs, which installs the rest: # curl -fsSL https://vloopex.com/install.sh | bash # curl -fsSL https://vloopex.com/install.sh | bash -s -- --yes --harnesses claude # # The whole script is one function: bash reads all of it before it runs, so a # command that reads input cannot consume the rest of a piped script. main() { set -euo pipefail local node_min="24.17.0" node_line=24 local runtime_dir="$HOME/.vloopex/runtime" local setup_url="${VLOOPEX_SETUP_URL:-https://vloopex.com/setup.mjs}" local assume_yes=0 check_only=0 dry_run=0 arg for arg in "$@"; do case "$arg" in --yes|-y) assume_yes=1 ;; --check) check_only=1 ;; --dry-run) dry_run=1 ;; esac done local tmp tmp="$(mktemp -d)" # Expanded now: the local variable is gone when the shell exits. trap "rm -rf '$tmp'" EXIT fail() { printf 'Vloopex install: %s\n' "$*" >&2; exit 1; } have_tty() { { : /dev/null; } confirm() { [ "$assume_yes" = 1 ] && return 0 have_tty || fail "$1 Run again with --yes to agree." local reply printf '%s [Y/n] ' "$1" >/dev/tty read -r reply /dev/null | sed 's/^v//' || true } version_ok() { local IFS=. have want i read -r -a have <<<"$1" read -r -a want <<<"$node_min" for i in 0 1 2; do ((10#${have[i]:-0} > 10#${want[i]})) && return 0 ((10#${have[i]:-0} < 10#${want[i]})) && return 1 done return 0 } sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f 1 else shasum -a 256 "$1" | cut -d ' ' -f 1 fi } # Downloads the official Node.js build into the runtime folder and prints # its bin folder. It needs no admin rights and changes no other Node.js. install_node() { local os arch case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) fail "This installer supports macOS and Linux. On Windows, run install.ps1." ;; esac case "$(uname -m)" in x86_64|amd64) arch=x64 ;; arm64|aarch64) arch=arm64 ;; *) fail "nodejs.org has no Node.js build for $(uname -m). Install Node.js ${node_min} or newer, then run this again." ;; esac if [ "$os" = linux ] && ldd --version 2>&1 | grep -qi musl; then fail "The nodejs.org builds need glibc. Install Node.js ${node_min} or newer with your package manager (Alpine: apk add nodejs npm), then run this again." fi local base="https://nodejs.org/dist/latest-v${node_line}.x" sums file hash sums="$(curl -fsSL "$base/SHASUMS256.txt")" || fail "Could not reach nodejs.org." file="$(printf '%s\n' "$sums" | awk -v pattern="^node-v[0-9.]+-${os}-${arch}[.]tar[.]gz$" '$2 ~ pattern { print $2; exit }')" hash="$(printf '%s\n' "$sums" | awk -v name="$file" '$2 == name { print $1; exit }')" [ -n "$file" ] && [ -n "$hash" ] || fail "nodejs.org lists no Node.js ${node_line} build for ${os}-${arch}." printf 'Downloading %s from nodejs.org\n' "$file" >&2 curl -fsSL "$base/$file" -o "$tmp/$file" || fail "Could not download $base/$file" [ "$(sha256 "$tmp/$file")" = "$hash" ] || fail "The Node.js download does not match its checksum. Nothing was installed." tar -xzf "$tmp/$file" -C "$tmp" local dir="${file%.tar.gz}" mkdir -p "$runtime_dir" rm -rf "${runtime_dir:?}/$dir" mv "$tmp/$dir" "$runtime_dir/$dir" printf '%s\n' "$runtime_dir/$dir/bin" } # Node.js on PATH first, then a copy that setup downloaded before. local node_bin="" candidate current="" if command -v node >/dev/null 2>&1; then current="$(node_version node)"; fi if [ -z "$current" ] || ! version_ok "$current"; then for candidate in "$runtime_dir"/node-v*/bin/node; do if [ -x "$candidate" ] && version_ok "$(node_version "$candidate")"; then node_bin="${candidate%/node}" break fi done if [ -z "$node_bin" ]; then local found="Node.js is not installed" [ -n "$current" ] && found="Node.js $current is too old" [ "$check_only" = 1 ] && fail "$found. Vloopex needs ${node_min} or newer." if [ "$dry_run" = 1 ]; then printf 'Dry run: %s. Setup would download Node.js %s from nodejs.org into %s. The other steps need Node.js, so the dry run stops here.\n' "$found" "$node_line" "$runtime_dir" exit 0 fi confirm "$found. Vloopex needs ${node_min} or newer. Download Node.js ${node_line} from nodejs.org into ${runtime_dir}? It needs no admin rights and does not change other Node.js installs." \ || fail "Vloopex needs Node.js ${node_min} or newer." node_bin="$(install_node)" version_ok "$(node_version "$node_bin/node")" || fail "The downloaded Node.js does not run on this computer." fi PATH="$node_bin:$PATH" export PATH fi local setup="" script_dir="" if [ -n "${BASH_SOURCE[0]:-}" ] && [ -f "${BASH_SOURCE[0]}" ]; then script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fi if [ -n "$script_dir" ] && [ -f "$script_dir/setup.mjs" ]; then setup="$script_dir/setup.mjs" else curl -fsSL "$setup_url" -o "$tmp/setup.mjs" || fail "Could not download $setup_url" setup="$tmp/setup.mjs" fi # A piped script has the pipe as input, so the questions read the terminal. if [ "$assume_yes" = 0 ] && [ "$check_only" = 0 ] && have_tty; then node "$setup" "$@"