#!/bin/sh # OAR — Open Agent Relay · installer · https://oar.sh # # One line, one confirmation: finds or fetches Node.js, installs @oar-sh/oar, # writes the config, and starts the relay. Everything lands in your home # directory; nothing needs root. # # curl -fsSL oar.sh/install | sh # curl -fsSL oar.sh/install | sh -s -- --yes # no question (scripts, CI) # # Env: OAR_VERSION=x.y.z (pin) · OAR_CHANNEL=beta · OAR_PORT=n (relay port) # OAR_YES=1 (same as --yes) · OAR_DRY_RUN=1 (print the plan only) # OAR_NODE=private (use OAR's own Node even when yours would do) set -eu # The whole script is one function called on the last line, so a download cut # short runs nothing. main() { NODE_FLOOR_MAJOR=22 NODE_FLOOR_MINOR=13 NODE_LINE=24 # the Node.js line fetched when yours is missing or too old PACKAGE="@oar-sh/oar" NPM_ALLOW_SCRIPTS="--allow-scripts=@oar-sh/oar,better-sqlite3,koffi" # keep in sync with the tool's NPM_INSTALL_SCRIPT_PACKAGES ASSUME_YES="${OAR_YES:-}" for arg in "$@"; do case "$arg" in -y|--yes) ASSUME_YES=1 ;; *) fail "unknown option: $arg" ;; esac done [ "$(id -u)" -eq 0 ] && fail "refusing to run as root — OAR installs per user; re-run without sudo." [ -n "${HOME:-}" ] && [ -d "$HOME" ] || fail "HOME is not set to a directory." OAR_ROOT="${OAR_STATE_ROOT:-$HOME/.oar}" RUNTIME_DIR="$OAR_ROOT/runtime/node" PREFIX_DIR="$OAR_ROOT/npm" BIN_DIR="$HOME/.local/bin" if [ -n "${OAR_PACKAGE_SPEC:-}" ]; then SPEC="$OAR_PACKAGE_SPEC" # a tarball or folder, for testing an unreleased build elif [ -n "${OAR_VERSION:-}" ]; then SPEC="${PACKAGE}@${OAR_VERSION}" elif [ "${OAR_CHANNEL:-stable}" = "beta" ]; then SPEC="${PACKAGE}@beta" else SPEC="${PACKAGE}@latest" fi # --- What is here already ------------------------------------------------- # MODE: system = your Node and its global npm folder, as `npm i -g` does # prefix = your Node, package in OAR's own folder (yours is not writable) # private = OAR's own Node and folder (yours is missing or too old) NODE_BIN="" NODE_NOTE="" NODE_FETCH="" if [ "${OAR_NODE:-}" = "private" ]; then NODE_NOTE="OAR_NODE=private" elif command -v node >/dev/null 2>&1; then if ! node_is_new_enough "$(node -v)"; then NODE_NOTE="yours is $(node -v), OAR needs >= ${NODE_FLOOR_MAJOR}.${NODE_FLOOR_MINOR}" elif ! command -v npm >/dev/null 2>&1; then NODE_NOTE="your Node.js $(node -v) comes without npm" else NODE_BIN="$(command -v node)" if npm_prefix_writable; then MODE=system; else MODE=prefix; fi fi fi if [ -z "$NODE_BIN" ]; then MODE=private if [ -x "$RUNTIME_DIR/bin/node" ] && node_is_new_enough "$("$RUNTIME_DIR/bin/node" -v)"; then : else NODE_FETCH=1 resolve_node_download fi fi # --- The plan --------------------------------------------------------------- say "OAR — Open Agent Relay" say "" say "This will:" case "$MODE" in system) say " - use your Node.js $(node -v) ($NODE_BIN)" say " - install $SPEC with npm into $(npm prefix -g)" ;; prefix) say " - use your Node.js $(node -v) ($NODE_BIN)" say " - install $SPEC into $PREFIX_DIR" say " (npm's global folder $(npm prefix -g) needs root; it stays untouched)" say " - put the 'oar' command into $BIN_DIR" ;; private) if [ -n "$NODE_FETCH" ]; then say " - download Node.js $NODE_VERSION from nodejs.org into $RUNTIME_DIR" say " (${NODE_NOTE:-no Node.js found}; only OAR uses this copy, about 30 MB)" else say " - use OAR's own Node.js in $RUNTIME_DIR" fi say " - install $SPEC into $PREFIX_DIR" say " - put the 'oar' command into $BIN_DIR" ;; esac PROFILE_FILE="" if [ "$MODE" != system ] && ! on_path "$BIN_DIR"; then PROFILE_FILE="$(profile_file)" say " - add $BIN_DIR to your PATH in $PROFILE_FILE" fi say " - write the config to $OAR_ROOT/config.json${OAR_PORT:+ (port $OAR_PORT)}" if [ "$(uname -s)" = Linux ] && systemctl --user show-environment >/dev/null 2>&1; then say " - start the relay as a systemd user service, so it is back after a reboot" else say " - start the relay in the background" fi say "" if [ -n "${OAR_DRY_RUN:-}" ]; then say "dry run — nothing installed." return 0 fi if [ -z "$ASSUME_YES" ]; then # Piped from curl, stdin is the script itself: ask on the terminal. if ! (exec /dev/null; then fail "no terminal to ask on. To install without the question: curl -fsSL oar.sh/install | sh -s -- --yes" fi printf 'Continue? [Y/n] ' read -r answer /dev/null; then # shellcheck disable=SC2086 "$OAR_CMD" setup --defaults --start ${OAR_PORT:+--port "$OAR_PORT"} \ || fail "OAR $OAR_INSTALLED is installed, but the relay did not start (see above). Try again with: oar start" say "" say "OAR $OAR_INSTALLED is installed and running. Open the Relay URL above in your browser." else "$OAR_CMD" setup --defaults say "" say "OAR $OAR_INSTALLED is installed." fi say " oar open the menu: start, stop, restart, status, URL and QR code, service" say " oar setup change the settings (LAN access for your phone, port, token)" say " oar doctor show what is installed and where" if [ -n "$PROFILE_FILE" ]; then say "New terminals find the 'oar' command; in this one, first run: export PATH=\"$BIN_DIR:\$PATH\"" fi } say() { printf '%s\n' "$*"; } fail() { printf 'oar install: %s\n' "$*" >&2; exit 1; } fail_npm() { say "" >&2 fail "npm install failed (see the log above; if it mentions node-gyp or prebuild, this platform needs a C/C++ toolchain and Python). The relay was not set up." } # npm without its chatter (funding, audit, update notice, warnings): an error # still prints. --allow-scripts names the packages whose install scripts must # run: OAR's own copies the data an earlier version kept inside the package, # the others fetch OAR's native binaries; npm 12 runs none it was not told to # allow. With OAR's own Node, npm runs with that Node first on PATH for # this one command, because npm and the packages' install scripts are Node # scripts; the PATH the relay is set up with stays yours. run_npm() { if [ "$MODE" = private ]; then PATH="$RUNTIME_DIR/bin:$PATH" "$RUNTIME_DIR/bin/npm" --no-fund --no-audit --no-update-notifier --loglevel=error "$NPM_ALLOW_SCRIPTS" "$@" else npm --no-fund --no-audit --no-update-notifier --loglevel=error "$NPM_ALLOW_SCRIPTS" "$@" fi } # node_is_new_enough v24.1.0 node_is_new_enough() { version="${1#v}" major="${version%%.*}" rest="${version#*.}" minor="${rest%%.*}" case "$major$minor" in ''|*[!0-9]*) return 1 ;; esac [ "$major" -gt "$NODE_FLOOR_MAJOR" ] || { [ "$major" -eq "$NODE_FLOOR_MAJOR" ] && [ "$minor" -ge "$NODE_FLOOR_MINOR" ]; } } # Can `npm install -g` write where it wants to, as this user? npm_prefix_writable() { prefix="$(npm prefix -g 2>/dev/null)" || return 1 [ -n "$prefix" ] || return 1 modules="$prefix/lib/node_modules" if [ -d "$modules" ]; then [ -w "$modules" ] || return 1 elif [ -d "$prefix/lib" ]; then [ -w "$prefix/lib" ] || return 1 else [ -w "$prefix" ] || return 1 fi if [ -d "$prefix/bin" ]; then [ -w "$prefix/bin" ]; else [ -w "$prefix" ]; fi } on_path() { case ":$PATH:" in *":$1:"*) return 0 ;; *) return 1 ;; esac } # The file a new terminal of this user's shell reads. profile_file() { case "$(basename "${SHELL:-sh}")" in zsh) printf '%s\n' "${ZDOTDIR:-$HOME}/.zshrc" ;; bash) if [ "$(uname -s)" = Darwin ]; then printf '%s\n' "$HOME/.bash_profile"; else printf '%s\n' "$HOME/.bashrc"; fi ;; *) printf '%s\n' "$HOME/.profile" ;; esac } add_to_path() { if [ -f "$PROFILE_FILE" ] && grep -q '# added by the OAR installer' "$PROFILE_FILE"; then return 0; fi # shellcheck disable=SC2016 printf '\n# added by the OAR installer\nexport PATH="%s:$PATH"\n' "$BIN_DIR" >> "$PROFILE_FILE" } # The `oar` command for an install outside npm's own global folder. write_launcher() { entry="$OAR_ENTRY" [ -f "$entry" ] || fail "npm reported success, but $entry is missing." rm -f "$BIN_DIR/oar" if [ "$MODE" = private ]; then # OAR's Node goes last on PATH: the relay's sessions find `node` and `npm` # at all, and a Node of yours still wins in them. cat > "$BIN_DIR/oar" < "$BIN_DIR/oar" < if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" elif command -v wget >/dev/null 2>&1; then wget -q "$1" -O "$2" else fail "neither curl nor wget is here to download with." fi } sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 else fail "neither sha256sum nor shasum is here to check the Node.js download with." fi } # Sets NODE_VERSION, NODE_FILE, NODE_SHA and NODE_BASE_URL for this machine. resolve_node_download() { manual="install Node.js >= ${NODE_FLOOR_MAJOR}.${NODE_FLOOR_MINOR} yourself (https://nodejs.org), then run this again." case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) fail "no Node.js download for $(uname -s) — $manual" ;; esac case "$(uname -m)" in x86_64|amd64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; *) fail "no Node.js download for $(uname -m) — $manual" ;; esac if [ "$os" = linux ] && { [ -f /etc/alpine-release ] || (ldd --version 2>&1 | grep -qi musl); }; then fail "this system uses musl libc, which nodejs.org has no build for — $manual" fi command -v tar >/dev/null 2>&1 || fail "tar is needed to unpack Node.js." NODE_BASE_URL="https://nodejs.org/dist/latest-v${NODE_LINE}.x" WORK_DIR="$(mktemp -d)" trap 'rm -rf "$WORK_DIR"' EXIT download "$NODE_BASE_URL/SHASUMS256.txt" "$WORK_DIR/SHASUMS256.txt" \ || fail "could not reach nodejs.org — check the connection, or $manual" line="$(grep " node-v.*-${os}-${arch}\.tar\.gz\$" "$WORK_DIR/SHASUMS256.txt" | head -n 1)" [ -n "$line" ] || fail "nodejs.org lists no Node.js $NODE_LINE build for ${os}-${arch} — $manual" NODE_SHA="${line%% *}" NODE_FILE="${line##* }" NODE_VERSION="${NODE_FILE#node-}" NODE_VERSION="${NODE_VERSION%%-*}" } fetch_node() { say "Downloading Node.js $NODE_VERSION ..." download "$NODE_BASE_URL/$NODE_FILE" "$WORK_DIR/$NODE_FILE" || fail "the Node.js download failed. Nothing was installed." got="$(sha256_of "$WORK_DIR/$NODE_FILE")" [ "$got" = "$NODE_SHA" ] || fail "the Node.js download does not match its checksum (got $got). Nothing was installed." mkdir -p "$WORK_DIR/unpacked" tar -xzf "$WORK_DIR/$NODE_FILE" -C "$WORK_DIR/unpacked" --strip-components=1 "$WORK_DIR/unpacked/bin/node" -v >/dev/null 2>&1 || fail "the downloaded Node.js does not run on this system. Nothing was installed." mkdir -p "$(dirname "$RUNTIME_DIR")" rm -rf "$RUNTIME_DIR" mv "$WORK_DIR/unpacked" "$RUNTIME_DIR" } main "$@"