199 lines
9.2 KiB
Bash
Executable File
199 lines
9.2 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
PACKAGE="rea-agents"
|
|
REPOSITORY="morluto/rea"
|
|
NODE_FORMULA="node@24"
|
|
NODE_VERSION="24.18.0"
|
|
temporary_directory=""
|
|
node_destination=""
|
|
node_stage=""
|
|
node_backup=""
|
|
node_version=""
|
|
|
|
cleanup() {
|
|
if [[ -n "$temporary_directory" && -d "$temporary_directory" ]]; then
|
|
rm -rf -- "$temporary_directory"
|
|
fi
|
|
if [[ -n "$node_stage" && -e "$node_stage" ]]; then
|
|
rm -rf -- "$node_stage"
|
|
fi
|
|
if [[ -n "$node_backup" && -e "$node_backup" && ! -e "$node_destination" ]]; then
|
|
mv -- "$node_backup" "$node_destination"
|
|
fi
|
|
}
|
|
trap cleanup EXIT HUP INT TERM
|
|
|
|
fail() {
|
|
printf 'REA installation failed: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
version_at_least() {
|
|
local actual="$1" required_major="$2" required_minor="$3"
|
|
local major minor
|
|
major="${actual%%.*}"
|
|
minor="${actual#*.}"
|
|
minor="${minor%%.*}"
|
|
[[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]] || return 1
|
|
((major > required_major || (major == required_major && minor >= required_minor)))
|
|
}
|
|
|
|
platform="$(uname -s)"
|
|
if [[ "$platform" == "Darwin" ]]; then
|
|
macos_version="$(sw_vers -productVersion 2>/dev/null)" || fail "could not determine the macOS version."
|
|
macos_major="${macos_version%%.*}"
|
|
[[ "$macos_major" =~ ^[0-9]+$ && "$macos_major" -ge 12 ]] || fail "macOS 12 or newer is required."
|
|
elif [[ "$platform" == "Linux" ]]; then
|
|
[[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "aarch64" ]] || fail "64-bit x86 or ARM Linux is required."
|
|
[[ -r /etc/os-release ]] || fail "could not identify the Linux distribution."
|
|
linux_id="$(sed -n 's/^ID=//p' /etc/os-release | tr -d '"')"
|
|
linux_version="$(sed -n 's/^VERSION_ID=//p' /etc/os-release | tr -d '"')"
|
|
case "$linux_id" in
|
|
ubuntu) [[ "${linux_version%%.*}" -ge 24 ]] || fail "Ubuntu 24.04 or newer is required." ;;
|
|
fedora) [[ "${linux_version%%.*}" -ge 41 ]] || fail "Fedora 41 or newer is required." ;;
|
|
arch) ;;
|
|
*) fail "supported Linux distributions are Ubuntu 24.04+, Fedora 41+, and Arch Linux." ;;
|
|
esac
|
|
else
|
|
fail "REA supports macOS and selected 64-bit Linux distributions."
|
|
fi
|
|
command -v curl >/dev/null 2>&1 || fail "curl is required."
|
|
command -v shasum >/dev/null 2>&1 || command -v sha256sum >/dev/null 2>&1 || fail "shasum or sha256sum is required."
|
|
|
|
if [[ "$platform" == "Darwin" ]] && ! command -v brew >/dev/null 2>&1; then
|
|
printf 'Installing Homebrew prerequisite...\n'
|
|
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || fail "Homebrew installation did not complete."
|
|
if [[ -x /opt/homebrew/bin/brew ]]; then
|
|
PATH="/opt/homebrew/bin:$PATH"
|
|
elif [[ -x /usr/local/bin/brew ]]; then
|
|
PATH="/usr/local/bin:$PATH"
|
|
fi
|
|
fi
|
|
if [[ "$platform" == "Darwin" ]]; then
|
|
command -v brew >/dev/null 2>&1 || fail "Homebrew is not available after installation."
|
|
fi
|
|
|
|
node_compatible=false
|
|
if command -v node >/dev/null 2>&1; then
|
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
fi
|
|
if version_at_least "$node_version" 24 18 && [[ "$node_version" =~ ^24\. ]]; then
|
|
node_compatible=true
|
|
fi
|
|
if [[ "$node_compatible" != true ]]; then
|
|
if [[ "$platform" == "Darwin" ]]; then
|
|
brew install "$NODE_FORMULA" || fail "Node.js 24 installation failed."
|
|
node_prefix="$(brew --prefix "$NODE_FORMULA")" || fail "could not locate Node.js 24."
|
|
PATH="$node_prefix/bin:$PATH"
|
|
else
|
|
machine="$(uname -m)"
|
|
node_arch="$([[ "$machine" == "x86_64" ]] && printf 'x64' || printf 'arm64')"
|
|
node_archive="node-v${NODE_VERSION}-linux-${node_arch}.tar.xz"
|
|
node_base="https://nodejs.org/dist/v${NODE_VERSION}"
|
|
temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/rea-install.XXXXXX")" || fail "could not create a temporary directory."
|
|
curl -fsSL "$node_base/$node_archive" -o "$temporary_directory/$node_archive" || fail "Node.js download failed."
|
|
curl -fsSL "$node_base/SHASUMS256.txt" -o "$temporary_directory/SHASUMS256.txt" || fail "Node.js checksum download failed."
|
|
expected_checksum="$(awk -v archive="$node_archive" '$2 == archive { print $1 }' "$temporary_directory/SHASUMS256.txt")"
|
|
[[ "$expected_checksum" =~ ^[a-f0-9]{64}$ ]] || fail "Node.js checksum metadata was malformed."
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
actual_checksum="$(shasum -a 256 "$temporary_directory/$node_archive" | awk '{ print $1 }')"
|
|
else
|
|
actual_checksum="$(sha256sum "$temporary_directory/$node_archive" | awk '{ print $1 }')"
|
|
fi
|
|
[[ "$actual_checksum" == "$expected_checksum" ]] || fail "Node.js checksum verification failed."
|
|
node_destination="$HOME/.local/share/rea/node"
|
|
node_stage="$HOME/.local/share/rea/node.new.$$"
|
|
node_backup="$HOME/.local/share/rea/node.backup.$$"
|
|
[[ ! -L "$node_destination" ]] || fail "refusing to replace a symlinked Node.js installation."
|
|
mkdir -p "$HOME/.local/share/rea"
|
|
rm -rf -- "$node_stage" "$node_backup"
|
|
mkdir "$node_stage"
|
|
tar -xJf "$temporary_directory/$node_archive" --strip-components=1 -C "$node_stage" || fail "Node.js extraction failed."
|
|
if [[ -e "$node_destination" ]]; then
|
|
mv -- "$node_destination" "$node_backup"
|
|
fi
|
|
if ! mv -- "$node_stage" "$node_destination"; then
|
|
[[ ! -e "$node_backup" ]] || mv -- "$node_backup" "$node_destination"
|
|
fail "Node.js installation replacement failed."
|
|
fi
|
|
node_stage=""
|
|
rm -rf -- "$node_backup"
|
|
node_backup=""
|
|
PATH="$node_destination/bin:$PATH"
|
|
fi
|
|
fi
|
|
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
|
if ! command -v node >/dev/null 2>&1 || ! version_at_least "$node_version" 24 18 || [[ ! "$node_version" =~ ^24\. ]]; then
|
|
fail "Node.js 24.18 or newer is not active on PATH."
|
|
fi
|
|
command -v npm >/dev/null 2>&1 || fail "npm is missing from the Node.js installation."
|
|
npm_version="$(npm --version)"
|
|
if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
|
|
if [[ "$platform" == "Linux" ]]; then
|
|
npm install --global --prefix "$HOME/.local" npm@11 || fail "npm 11 installation failed."
|
|
else
|
|
npm install --global npm@11 || fail "npm 11 installation failed."
|
|
fi
|
|
fi
|
|
npm_version="$(npm --version)"
|
|
if [[ ! "$npm_version" =~ ^11\. ]] || ! version_at_least "$npm_version" 11 16; then
|
|
fail "npm 11.16 or newer is not active on PATH."
|
|
fi
|
|
if [[ -n "$temporary_directory" ]]; then
|
|
rm -rf -- "$temporary_directory"
|
|
temporary_directory=""
|
|
fi
|
|
|
|
if [[ -n "${REA_VERSION:-}" ]]; then
|
|
version="$REA_VERSION"
|
|
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "REA_VERSION must be an exact semantic version."
|
|
else
|
|
release_json="$(curl -fsSL -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "could not resolve the latest GitHub release."
|
|
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})')" || fail "GitHub returned malformed release metadata."
|
|
[[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "latest release tag is not a valid rea-agents release."
|
|
version="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
|
if [[ "$platform" == "Linux" ]]; then
|
|
mkdir -p "$HOME/.local/bin"
|
|
PATH="$HOME/.local/bin:$PATH"
|
|
npm install --global --prefix "$HOME/.local" "$PACKAGE@$version" || fail "npm package installation failed."
|
|
else
|
|
npm install --global "$PACKAGE@$version" || fail "npm package installation failed."
|
|
fi
|
|
command -v rea >/dev/null 2>&1 || fail "the rea command is not available after installation."
|
|
installed_version="$(rea --version 2>/dev/null | tr -d '[:space:]')" || fail "could not read the installed REA version."
|
|
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
|
|
|
|
temporary_directory="$(mktemp -d "${TMPDIR:-/tmp}/rea-install.XXXXXX")" || fail "could not create a temporary directory."
|
|
setup_output="$temporary_directory/setup.json"
|
|
doctor_output="$temporary_directory/doctor.json"
|
|
rea setup --yes --json >"$setup_output" || fail "REA setup command failed."
|
|
rea doctor --json >"$doctor_output" || fail "REA doctor command failed."
|
|
result="$(node - "$setup_output" "$doctor_output" <<'NODE'
|
|
const fs = require("node:fs");
|
|
const [setupPath, doctorPath] = process.argv.slice(2);
|
|
try {
|
|
const setup = JSON.parse(fs.readFileSync(setupPath, "utf8"));
|
|
const doctor = JSON.parse(fs.readFileSync(doctorPath, "utf8"));
|
|
if (!Array.isArray(doctor.checks) || typeof doctor.healthy !== "boolean") process.exit(2);
|
|
if (setup.status !== "ready" && setup.status !== "needs_human") process.exit(2);
|
|
const activation = setup.actions.includes("installed_hopper");
|
|
if (!doctor.healthy && !activation) process.exit(3);
|
|
process.stdout.write(activation ? "activation" : "ready");
|
|
} catch { process.exit(2); }
|
|
NODE
|
|
)" || fail "setup or doctor reported an unhealthy required component or malformed output."
|
|
|
|
if [[ "$result" == "activation" ]]; then
|
|
printf 'REA %s is installed and coding agents are configured.\n' "$version"
|
|
printf 'One user action remains: open Hopper, complete its activation, then run: rea doctor --json\n'
|
|
else
|
|
printf 'REA %s is installed and ready. Run rea doctor --json at any time.\n' "$version"
|
|
fi
|
|
if [[ "$platform" == "Linux" ]]; then
|
|
printf 'Add %s to PATH to run rea from future shells.\n' "$HOME/.local/bin"
|
|
fi
|