fix(installer): make a greenfield install actually work — node bootstrap, PATH, wizard profile #1229
@@ -565,8 +565,151 @@ install_next_cli_from_registry() {
|
||||
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
||||
}
|
||||
|
||||
# ─── node bootstrap ───────────────────────────────────────────────────────────
|
||||
#
|
||||
# Nothing on a greenfield host installs Node.js, yet this installer and the CLI
|
||||
# it installs both hard-require it. Measured on a clean Debian 13 image: the
|
||||
# installer stopped at `require_cmd node` with "Required command not found" and
|
||||
# nothing was installed, with no hint of how to proceed.
|
||||
#
|
||||
# Inlined rather than factored into a sibling file on purpose: this script is
|
||||
# fetched standalone by curl and has nothing to source.
|
||||
#
|
||||
# No-op when a suitable node is already on PATH, so it never fights an
|
||||
# operator's nvm/fnm/distro node.
|
||||
|
||||
NODE_ROOT="${MOSAIC_NODE_ROOT:-$HOME/.mosaic/node}"
|
||||
NODE_BOOTSTRAP_VERSION="${MOSAIC_NODE_VERSION:-v22.23.2}"
|
||||
NODE_MIN_MAJOR="${MOSAIC_NODE_MIN_MAJOR:-20}"
|
||||
NODE_DIST_BASE="${MOSAIC_NODE_DIST_BASE:-https://nodejs.org/dist}"
|
||||
|
||||
# Major version of the node at $1, or empty if it will not run.
|
||||
node_major_of() {
|
||||
local candidate="$1" version
|
||||
version="$("$candidate" -e 'process.stdout.write(process.versions.node)' 2>/dev/null)" || return 0
|
||||
printf '%s' "${version%%.*}"
|
||||
}
|
||||
|
||||
node_is_suitable() {
|
||||
local major
|
||||
major="$(node_major_of "$1")"
|
||||
[[ -n "$major" ]] && [[ "$major" -ge "$NODE_MIN_MAJOR" ]]
|
||||
}
|
||||
|
||||
install_node() {
|
||||
local node_os node_arch tarball release_url work_dir extracted target node_bin
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux) node_os="linux" ;;
|
||||
Darwin) node_os="darwin" ;;
|
||||
*) fail "Unsupported OS '$(uname -s)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||
esac
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) node_arch="x64" ;;
|
||||
aarch64|arm64) node_arch="arm64" ;;
|
||||
armv7l) node_arch="armv7l" ;;
|
||||
*) fail "Unsupported architecture '$(uname -m)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||
esac
|
||||
|
||||
# .tar.gz rather than the smaller .tar.xz: gzip is universally present, xz is
|
||||
# not, and a minimal image is exactly the case this exists to handle.
|
||||
tarball="node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}.tar.gz"
|
||||
release_url="${NODE_DIST_BASE}/${NODE_BOOTSTRAP_VERSION}"
|
||||
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-node-XXXXXX")"
|
||||
|
||||
info "Installing Node.js $NODE_BOOTSTRAP_VERSION ($node_os-$node_arch) to $NODE_ROOT…"
|
||||
|
||||
if ! curl -fsSL "${release_url}/${tarball}" -o "$work_dir/$tarball"; then
|
||||
fail "Download failed: ${release_url}/${tarball}"
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
if ! curl -fsSL "${release_url}/SHASUMS256.txt" -o "$work_dir/SHASUMS256.txt"; then
|
||||
fail "Could not fetch SHASUMS256.txt; refusing to install an unverified runtime."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
# Keep only our artifact's line, so a missing entry is an error not a pass.
|
||||
if ! grep " ${tarball}\$" "$work_dir/SHASUMS256.txt" >"$work_dir/expected.sha256"; then
|
||||
fail "$tarball has no entry in SHASUMS256.txt; refusing to install."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
if ! (cd "$work_dir" && verify_sha256 expected.sha256); then
|
||||
fail "Checksum mismatch for $tarball; refusing to install."
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
ok "Checksum verified"
|
||||
|
||||
tar xzf "$work_dir/$tarball" -C "$work_dir"
|
||||
extracted="$work_dir/node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}"
|
||||
if [[ ! -x "$extracted/bin/node" ]]; then
|
||||
fail "Extracted archive has no bin/node"
|
||||
rm -rf "$work_dir"; return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$NODE_ROOT"
|
||||
target="$NODE_ROOT/$NODE_BOOTSTRAP_VERSION"
|
||||
rm -rf "$target.incoming"
|
||||
mv "$extracted" "$target.incoming"
|
||||
rm -rf "$target"
|
||||
mv "$target.incoming" "$target"
|
||||
ln -sfn "$NODE_BOOTSTRAP_VERSION" "$NODE_ROOT/current"
|
||||
rm -rf "$work_dir"
|
||||
|
||||
node_bin="$NODE_ROOT/current/bin"
|
||||
if ! node_is_suitable "$node_bin/node"; then
|
||||
fail "Installed node at $node_bin/node did not run"
|
||||
return 1
|
||||
fi
|
||||
|
||||
export PATH="$node_bin:$PATH"
|
||||
ok "Node.js $(node -v) installed with npm $(npm -v 2>/dev/null || echo '?')"
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_node() {
|
||||
if command -v node &>/dev/null && node_is_suitable node; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# A previous run may have installed one that is not on this shell's PATH.
|
||||
if node_is_suitable "$NODE_ROOT/current/bin/node"; then
|
||||
export PATH="$NODE_ROOT/current/bin:$PATH"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${MOSAIC_SKIP_NODE_BOOTSTRAP:-0}" == "1" ]]; then
|
||||
fail "No suitable Node.js and MOSAIC_SKIP_NODE_BOOTSTRAP=1; refusing to download."
|
||||
echo " Install Node.js >= $NODE_MIN_MAJOR yourself, then re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
|
||||
# sha256sum on Linux, shasum on macOS. Verification is not optional: without a
|
||||
# checksum this would install an unauthenticated runtime.
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
verify_sha256() { sha256sum -c --status "$1"; }
|
||||
elif command -v shasum &>/dev/null; then
|
||||
verify_sha256() { shasum -a 256 -c --status "$1"; }
|
||||
else
|
||||
fail "sha256sum or shasum required to verify the Node.js download"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! install_node; then
|
||||
fail "Could not bootstrap Node.js. Install Node.js >= $NODE_MIN_MAJOR and re-run."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||
|
||||
ensure_node
|
||||
require_cmd node
|
||||
require_cmd npm
|
||||
|
||||
|
||||
Reference in New Issue
Block a user