#!/usr/bin/env bash # Check (and optionally install) OS + Node + NATS packages needed to run # Verae Time × Zapier modules on this host. # # Debian/Ubuntu and Alpine are first-class. Other Linux: print the plan. # macOS: Homebrew when present. # # Usage: # bash scripts/host-deps.sh # detect modules, install missing # bash scripts/host-deps.sh --check # report only # bash scripts/host-deps.sh --type ns1-all-in-one # bash scripts/host-deps.sh --module zappier-edge # bash scripts/host-deps.sh --nats # also install nats-server # bash scripts/host-deps.sh --docs # pandoc / weasyprint / pypdf set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" MANIFEST="$ROOT/host-deps.tsv" NODE_VER="${NODE_VER:-22.14.0}" NATS_VER="${NATS_VER:-2.10.26}" PREFIX="${PREFIX:-}" CHECK_ONLY=0 WANT_NATS=0 WANT_DOCS=0 WANT_CADDY=0 TYPE="" MODULES=() usage() { sed -n '2,16p' "$0" | tr -d '#'; exit "${1:-0}"; } while [[ $# -gt 0 ]]; do case "$1" in --check) CHECK_ONLY=1 ;; --install) CHECK_ONLY=0 ;; --nats) WANT_NATS=1 ;; --docs) WANT_DOCS=1 ;; --caddy) WANT_CADDY=1 ;; --type) TYPE="${2:-}"; shift ;; --module) MODULES+=("${2:-}"); shift ;; --prefix) PREFIX="${2:-}"; shift ;; -h|--help) usage 0 ;; *) echo "unknown arg $1" >&2; usage 1 ;; esac shift done # --- OS / arch --- OS_ID="unknown" OS_FAMILY="unknown" if [[ "$(uname -s)" == Darwin ]]; then OS_ID=macos OS_FAMILY=macos elif [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-unknown}" case "$OS_ID" in debian|ubuntu|raspbian|linuxmint) OS_FAMILY=debian ;; alpine) OS_FAMILY=alpine ;; *) OS_FAMILY=linux ;; esac fi ARCH="$(uname -m)" case "$ARCH" in x86_64|amd64) NODE_ARCH=x64; NATS_ARCH=amd64; DEB_ARCH=amd64 ;; aarch64|arm64) NODE_ARCH=arm64; NATS_ARCH=arm64; DEB_ARCH=arm64 ;; armv7l) NODE_ARCH=armv7l; NATS_ARCH=arm7; DEB_ARCH=armhf ;; *) NODE_ARCH=$ARCH; NATS_ARCH=$ARCH; DEB_ARCH=$ARCH ;; esac MUSL=0 if [[ "$OS_FAMILY" == alpine ]]; then MUSL=1; fi is_root() { [[ "$(id -u)" -eq 0 ]]; } run_root() { if is_root; then "$@" elif command -v sudo >/dev/null; then sudo "$@" else echo "need root (or sudo) to run: $*" >&2 return 1 fi } if [[ -z "$PREFIX" ]]; then if is_root; then PREFIX=/usr/local; else PREFIX="${HOME}/.local"; fi fi mkdir -p "$PREFIX/bin" export PATH="$PREFIX/bin:$PATH" # --- module list --- PKG_TO_REPO="$(cat <<'EOF' zappier zappier-edge verae-zapier-middleware verae-middleware verae-zapier verae-zapier-app docs-master zapier-docs-master ui-docs UI-Docs EOF )" map_name() { local n="$1" local line while read -r from to; do [[ "$n" == "$from" ]] && { echo "$to"; return; } done <<<"$PKG_TO_REPO" echo "$n" } collect_from_list() { local f="$1" while read -r name; do [[ -z "$name" || "$name" == \#* ]] && continue MODULES+=("$name") done < "$f" } if [[ ${#MODULES[@]} -eq 0 ]]; then if [[ -n "$TYPE" ]]; then collect_from_list "$ROOT/types/$TYPE/repos.txt" elif [[ -n "${VERAE_SRC:-}" && -d "${VERAE_SRC}" ]]; then for d in "$VERAE_SRC"/*/; do [[ -d "$d" ]] || continue MODULES+=("$(basename "$d")") done elif [[ -d "$PWD/packages" ]]; then for d in "$PWD"/packages/*/; do MODULES+=("$(map_name "$(basename "$d")")") done MODULES+=(master-zapier-plan-draft) elif [[ -f "$PWD/package.json" ]]; then n="$(basename "$PWD")" MODULES+=("$(map_name "$n")") else collect_from_list "$ROOT/repos.txt" fi fi # types that run nats-server on this box case "$TYPE" in ns1-all-in-one|ns1-archive) WANT_NATS=1 ;; esac # --- union flags --- MIN_NODE=0 NEED_NPM=0 NEED_NATIVE=0 NEED_NATS_CLIENT=0 NEED_SSH=0 NEED_DOCS=$WANT_DOCS while IFS='|' read -r name min flags; do [[ -z "$name" || "$name" == \#* ]] && continue wanted=0 for m in "${MODULES[@]}"; do [[ "$m" == "$name" ]] && wanted=1 done [[ $wanted -eq 1 ]] || continue if [[ "$min" != 0 && "$min" -gt "$MIN_NODE" ]]; then MIN_NODE=$min; fi [[ ",$flags," == *",npm,"* ]] && NEED_NPM=1 [[ ",$flags," == *",native,"* ]] && NEED_NATIVE=1 [[ ",$flags," == *",nats,"* ]] && NEED_NATS_CLIENT=1 [[ ",$flags," == *",nats_server,"* ]] && WANT_NATS=1 [[ ",$flags," == *",ssh,"* ]] && NEED_SSH=1 [[ ",$flags," == *",docs,"* ]] && NEED_DOCS=1 done < "$MANIFEST" echo "os=$OS_ID family=$OS_FAMILY arch=$ARCH musl=$MUSL prefix=$PREFIX" echo "modules: ${MODULES[*]}" echo "need: node>=${MIN_NODE:-0} npm=$NEED_NPM native=$NEED_NATIVE nats_client=$NEED_NATS_CLIENT nats_server=$WANT_NATS ssh=$NEED_SSH docs=$NEED_DOCS" MISSING=() have() { command -v "$1" >/dev/null 2>&1; } need_cmd() { local c="$1" if have "$c"; then echo "ok $c $($c --version 2>/dev/null | head -1 || true)" else echo "NO $c" MISSING+=("$c") fi } node_ok() { local want="$1" have node || return 1 node -e "process.exit(parseInt(process.versions.node,10)>=$want?0:1)" 2>/dev/null } # --- package installers --- apt_install() { run_root apt-get update -qq run_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$@" } apk_install() { run_root apk add --no-cache "$@"; } brew_install() { brew install "$@"; } install_pkgs() { local pkgs=("$@") [[ ${#pkgs[@]} -eq 0 ]] && return 0 echo "install OS packages: ${pkgs[*]}" [[ $CHECK_ONLY -eq 1 ]] && return 0 case "$OS_FAMILY" in debian) apt_install "${pkgs[@]}" ;; alpine) apk_install "${pkgs[@]}" ;; macos) brew_install "${pkgs[@]}" ;; *) echo "install these packages yourself: ${pkgs[*]}" >&2; return 1 ;; esac } install_node() { local want="${1:-20}" if node_ok "$want"; then echo "ok node $(node -v) (>=$want)" return 0 fi echo "NO node (>=$want) have=$(node -v 2>/dev/null || echo none)" MISSING+=("node>=$want") [[ $CHECK_ONLY -eq 1 ]] && return 0 if [[ "$OS_FAMILY" == alpine && $MUSL -eq 1 ]]; then apk_install nodejs npm python3 make g++ linux-headers || true if node_ok "$want"; then echo "ok node $(node -v) (apk)"; return 0; fi fi if [[ "$OS_FAMILY" == debian ]]; then apt_install ca-certificates curl xz-utils elif [[ "$OS_FAMILY" == alpine ]]; then apk_install curl tar xz fi local dist="linux-${NODE_ARCH}" [[ $MUSL -eq 1 ]] && dist="linux-${NODE_ARCH}-musl" local base="https://nodejs.org/dist/v${NODE_VER}" [[ $MUSL -eq 1 ]] && base="https://unofficial-builds.nodejs.org/download/release/v${NODE_VER}" local tar="node-v${NODE_VER}-${dist}.tar.xz" local tmp tmp="$(mktemp -d)" echo "download $base/$tar" curl -fsSL "$base/$tar" -o "$tmp/$tar" tar -xJf "$tmp/$tar" -C "$tmp" mkdir -p "$PREFIX" run_root cp -a "$tmp/node-v${NODE_VER}-${dist}/." "$PREFIX/" rm -rf "$tmp" hash -r node_ok "$want" || { echo "node install failed" >&2; return 1; } echo "ok node $(node -v) → $PREFIX" } install_nats() { if have nats-server; then echo "ok nats-server $(nats-server -v 2>/dev/null | head -1 || true)" return 0 fi echo "NO nats-server" MISSING+=("nats-server") [[ $CHECK_ONLY -eq 1 ]] && return 0 if [[ "$OS_FAMILY" == alpine ]]; then apk_install nats-server 2>/dev/null && { echo "ok nats-server (apk)"; return 0; } || true fi local tar="nats-server-v${NATS_VER}-linux-${NATS_ARCH}.tar.gz" local url="https://github.com/nats-io/nats-server/releases/download/v${NATS_VER}/${tar}" local tmp tmp="$(mktemp -d)" echo "download $url" curl -fsSL "$url" -o "$tmp/$tar" tar -xzf "$tmp/$tar" -C "$tmp" run_root install -m 0755 "$tmp/nats-server-v${NATS_VER}-linux-${NATS_ARCH}/nats-server" "$PREFIX/bin/nats-server" rm -rf "$tmp" have nats-server || { echo "nats-server install failed" >&2; return 1; } echo "ok nats-server → $PREFIX/bin" } # --- plan OS packages --- PKGS=() add_pkg() { PKGS+=("$1"); } case "$OS_FAMILY" in debian) add_pkg ca-certificates; add_pkg curl; add_pkg git; add_pkg bash; add_pkg tar; add_pkg xz-utils [[ $NEED_NATIVE -eq 1 || $NEED_NPM -eq 1 ]] && { add_pkg python3; add_pkg make; add_pkg g++; add_pkg python3-dev; } [[ $NEED_SSH -eq 1 ]] && add_pkg openssh-client [[ $NEED_DOCS -eq 1 ]] && { add_pkg pandoc; add_pkg weasyprint; add_pkg python3-pip; } [[ $WANT_CADDY -eq 1 ]] && add_pkg debian-keyring ;; alpine) add_pkg ca-certificates; add_pkg curl; add_pkg git; add_pkg bash; add_pkg tar; add_pkg xz [[ $NEED_NATIVE -eq 1 || $NEED_NPM -eq 1 ]] && { add_pkg python3; add_pkg make; add_pkg g++; add_pkg linux-headers; add_pkg libstdc++; } [[ $NEED_SSH -eq 1 ]] && add_pkg openssh-client [[ $NEED_DOCS -eq 1 ]] && { add_pkg pandoc; add_pkg py3-pip; add_pkg weasyprint; } ;; macos) add_pkg git; add_pkg curl [[ $NEED_DOCS -eq 1 ]] && { add_pkg pandoc; add_pkg weasyprint; } ;; esac echo "--- check ---" need_cmd git need_cmd curl [[ $NEED_SSH -eq 1 ]] && need_cmd ssh if [[ "$MIN_NODE" != 0 ]]; then if node_ok "$MIN_NODE"; then echo "ok node $(node -v)"; else echo "NO node (>=$MIN_NODE)"; MISSING+=("node>=$MIN_NODE"); fi need_cmd npm fi [[ $WANT_NATS -eq 1 ]] && { have nats-server && echo "ok nats-server" || { echo "NO nats-server"; MISSING+=("nats-server"); }; } [[ $NEED_DOCS -eq 1 ]] && { need_cmd pandoc; have weasyprint || have weasyprint3 || echo "NO weasyprint"; } if [[ $CHECK_ONLY -eq 1 ]]; then if [[ ${#MISSING[@]} -eq 0 ]]; then echo "host deps OK" exit 0 fi echo "missing: ${MISSING[*]}" exit 2 fi echo "--- install ---" if [[ ${#PKGS[@]} -gt 0 ]]; then UNIQ=() while IFS= read -r p; do [[ -n "$p" ]] && UNIQ+=("$p") done < <(printf '%s\n' "${PKGS[@]}" | sort -u) install_pkgs "${UNIQ[@]}" fi if [[ "$MIN_NODE" != 0 ]]; then install_node "$MIN_NODE" fi if [[ $WANT_NATS -eq 1 ]]; then install_nats fi if [[ $NEED_DOCS -eq 1 ]] && have python3; then python3 -m pip install --user pypdf >/dev/null 2>&1 || true fi MISSING=() need_cmd git need_cmd curl if [[ "$MIN_NODE" != 0 ]]; then node_ok "$MIN_NODE" || MISSING+=("node>=$MIN_NODE") have npm || MISSING+=("npm") fi [[ $WANT_NATS -eq 1 ]] && { have nats-server || MISSING+=("nats-server"); } if [[ ${#MISSING[@]} -eq 0 ]]; then echo "host deps OK (PATH should include $PREFIX/bin)" exit 0 fi echo "still missing: ${MISSING[*]}" >&2 exit 1