#!/bin/sh # Raven one-line installer (macOS / Linux). # # Remote (website): curl -fsSL https://raven.evermind.ai/install.sh | sh # Local (dev clone): git clone ... && cd raven && ./install.sh # # A piped run always installs the published release wheel, even from inside a # clone. Set RAVEN_LOCAL_SRC= to force an editable install of a checkout. # Set RAVEN_MINIMAL=1 to skip the chromium download and the LibreOffice offer; # the wheel install itself is unchanged. # # Goal: a clean machine ends up able to run `raven` / `raven tui` from any # directory with no manual steps. The script is idempotent -- it detects what # is already present and only fills the gaps: # 1. uv (Python toolchain + package manager) # 2. Node.js >= 22 (TUI runtime; installed privately if the system lacks it) # 3. raven (installed as a global uv tool -> ~/.local/bin/raven) # 4. chromium (browser-tool runtime; downloaded by playwright) # 5. LibreOffice (deck preview; installed on macOS, offered on Linux) # # POSIX sh on purpose (runs under dash/ash, not just bash). set -eu # --- config --------------------------------------------------------------- MIN_NODE_MAJOR=22 RAVEN_HOME="${RAVEN_HOME:-${HOME:?HOME is required, or set RAVEN_HOME explicitly}/.raven}" NODE_RUNTIME_DIR="$RAVEN_HOME/runtime" # --- pretty output --------------------------------------------------------- info() { printf '\033[1;34m>\033[0m %s\n' "$1"; } ok() { printf '\033[1;32m+\033[0m %s\n' "$1"; } warn() { printf '\033[1;33m!\033[0m %s\n' "$1" >&2; } die() { printf '\033[1;31mx\033[0m %s\n' "$1" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } # --- 0. platform detection ------------------------------------------------- detect_platform() { os="$(uname -s)" arch="$(uname -m)" case "$os" in Darwin) NODE_OS="darwin" ;; Linux) NODE_OS="linux" ;; *) die "Unsupported OS: $os (only macOS / Linux; on Windows use install.ps1)" ;; esac case "$arch" in arm64|aarch64) NODE_ARCH="arm64" ;; x86_64|amd64) NODE_ARCH="x64" ;; *) die "Unsupported architecture: $arch" ;; esac } # --- 1. ensure uv ---------------------------------------------------------- ensure_uv() { if have uv; then ok "uv already installed ($(uv --version))" return fi info "uv not found, installing..." curl -fsSL https://astral.sh/uv/install.sh | sh # uv installs to ~/.local/bin (or $XDG_BIN_HOME) -- make it visible for the # rest of this script even before the shell profile is re-sourced. export PATH="$HOME/.local/bin:$PATH" have uv || die "uv still unavailable after install; check PATH (expected in ~/.local/bin)" ok "uv installed" } # --- 2. ensure Node >= 22 -------------------------------------------------- # Returns 0 if a usable system node is found. system_node_ok() { have node || return 1 v="$(node --version 2>/dev/null | sed 's/^v//; s/\..*//')" [ -n "$v" ] && [ "$v" -ge "$MIN_NODE_MAJOR" ] 2>/dev/null } # Resolve the latest v22 LTS version string (e.g. v22.20.0) from nodejs.org, # without requiring jq/python. Falls back to a pinned version if the index # can't be reached. latest_node_v22() { idx="$(curl -fsSL https://nodejs.org/dist/index.json 2>/dev/null || true)" ver="$(printf '%s' "$idx" | tr ',' '\n' | grep -o '"version":"v22\.[0-9.]*"' \ | head -n1 | sed 's/.*"v/v/; s/"$//')" [ -n "$ver" ] && printf '%s' "$ver" || printf 'v22.20.0' } # Print the path to a Raven-provisioned private node binary (first match), or # return non-zero if none. Iterating the glob avoids passing multiple words to # `[ -x ... ]` (which errors) when several versioned dirs linger. private_node_bin() { for n in "$NODE_RUNTIME_DIR"/node-v22*/bin/node; do [ -x "$n" ] || continue # Actually run it -- a half-extracted / corrupt binary is +x but won't run, # and must NOT be mistaken for a ready runtime (else we'd never re-download). "$n" --version >/dev/null 2>&1 && { printf '%s' "$n"; return 0; } done return 1 } ensure_node() { if system_node_ok; then ok "Node.js already meets requirement ($(node --version))" return fi # Already provisioned privately by a previous run? if pn="$(private_node_bin)"; then ok "Raven private Node already present ($pn)" return fi info "Node.js >= $MIN_NODE_MAJOR not found; downloading a private runtime (does not touch the system)..." ver="$(latest_node_v22)" pkg="node-${ver}-${NODE_OS}-${NODE_ARCH}" url="https://nodejs.org/dist/${ver}/${pkg}.tar.gz" mkdir -p "$NODE_RUNTIME_DIR" tmp="$(mktemp -d)" info " $url" curl -fsSL "$url" -o "$tmp/node.tar.gz" || die "Node download failed: $url" # Supply-chain integrity: verify the tarball against the official # SHASUMS256.txt before extracting/executing it. Node publishes this file # next to every release. if curl -fsSL "https://nodejs.org/dist/${ver}/SHASUMS256.txt" -o "$tmp/SHASUMS256.txt" 2>/dev/null; then expected="$(awk -v f="${pkg}.tar.gz" '$2==f {print $1}' "$tmp/SHASUMS256.txt")" if [ -n "$expected" ]; then if have shasum; then actual="$(shasum -a 256 "$tmp/node.tar.gz" | awk '{print $1}')" elif have sha256sum; then actual="$(sha256sum "$tmp/node.tar.gz" | awk '{print $1}')" else actual=""; warn "shasum/sha256sum not found; skipping verification" fi if [ -n "$actual" ] && [ "$actual" != "$expected" ]; then rm -rf "$tmp" die "Node checksum mismatch (expected $expected, got $actual)" fi [ -n "$actual" ] && ok "Node tarball SHA256 verified" else warn "SHASUMS256.txt did not list ${pkg}.tar.gz; skipping verification" fi else warn "Could not fetch SHASUMS256.txt; skipping integrity check" fi tar -xzf "$tmp/node.tar.gz" -C "$NODE_RUNTIME_DIR" rm -rf "$tmp" [ -x "$NODE_RUNTIME_DIR/$pkg/bin/node" ] || die "Node executable not found after extraction" # Run it once now: catches a libc mismatch (e.g. glibc tarball on Alpine/musl) # at install time instead of letting `raven tui` fail later on the user's box. "$NODE_RUNTIME_DIR/$pkg/bin/node" --version >/dev/null 2>&1 \ || die "Downloaded Node cannot run on this machine (possible libc mismatch, e.g. Alpine/musl). Install Node >= ${MIN_NODE_MAJOR} via your system package manager." ok "Node private runtime ready: $NODE_RUNTIME_DIR/$pkg" # raven's find_node() globs ~/.raven/runtime/node-*/bin/node automatically, # so no PATH change is needed for `raven tui` to find it. } # --- 2b. build the web assets a source checkout does not carry ------------- # `ui-tui/dist/entry.js` (the TUI bundle) and `ui-web/dist/index.html` (the page # `raven web` serves) are both gitignored build artifacts. A release wheel # carries them; an editable install of a checkout gets neither, so without this # a clone install has no TUI and no page. Both must exist before first run. build_web_assets() { src="$1" need_tui=0 need_page=0 [ -f "$src/ui-tui/dist/entry.js" ] || need_tui=1 [ -f "$src/ui-web/dist/index.html" ] || need_page=1 [ "$need_tui" = 1 ] || [ "$need_page" = 1 ] || return 0 # One probe for both builds. ensure_node may have provisioned a private # runtime that never reaches PATH, so look there before giving up. node_bin="$(command -v node || true)" [ -n "$node_bin" ] || node_bin="$(private_node_bin || true)" node_dir="" blocker="" if [ -z "$node_bin" ] || [ ! -x "$node_bin" ]; then blocker="No usable node found" elif ! PATH="$(dirname "$node_bin"):$PATH" command -v npm >/dev/null 2>&1; then # npm ships alongside node, but verify explicitly before relying on it. blocker="Found node but not npm" else node_dir="$(dirname "$node_bin")" fi if [ "$need_tui" = 1 ]; then if [ -n "$blocker" ]; then warn "$blocker; skipping TUI build; raven tui may not work" else info "Building the TUI bundle (ui-tui/dist/entry.js)..." ( cd "$src/ui-tui" && PATH="$node_dir:$PATH" npm ci && PATH="$node_dir:$PATH" npm run build ) fi fi if [ "$need_page" = 1 ]; then if [ -n "$blocker" ]; then warn "$blocker; skipping the served-page build; raven web will not start" else info "Building the served page (ui-web/dist/index.html)..." # Warned rather than propagated, unlike the bundle above: bare `raven` # opens the TUI, so a machine that cannot build the page still gets the # surface this script exists to deliver. build_page "$src" "$node_dir" \ || warn "The served page did not build (see above); raven web will not start" fi fi } # Vite emits ui-web/.modern/modern.iife.js, then ui-web/build.py inlines it with the # page sources and the shared i18n catalogue into the single-file dist. # # Kept to one `&&` chain on purpose: `set -e` does not apply inside a function # whose caller guards it with `||`, so a second statement would run even after # npm ci had failed. Python comes from uv, which is already a hard requirement # here, rather than from a bare `python3` -- on a machine without the Command # Line Tools that name is a stub macOS answers with an install prompt. build_page() { ( cd "$1/ui-web" && PATH="$2:$PATH" npm ci && PATH="$2:$PATH" npm run build ) \ && uv run --no-project python "$1/ui-web/build.py" } # --- 3. install raven ------------------------------------------------------ # True when $1 holds a raven source checkout (its own pyproject, not a dep's). is_raven_source() { [ -f "$1/pyproject.toml" ] && grep -q '^name = "raven"' "$1/pyproject.toml" 2>/dev/null } install_raven() { # Local mode: run from a raven source checkout -> editable install of the # working tree (what a developer wants). Otherwise install the release wheel. # # "$0" names a real file only when this script runs as a file # (./install.sh). Piped through `curl ... | sh` the script arrives on stdin, # "$0" is "sh" and dirname "$0" is "." -- taking that as the source dir turns # a one-line install started from inside a clone into a silent editable # install of that working tree, whatever it happens to contain. So local mode # requires "$0" to be a file; RAVEN_LOCAL_SRC is the explicit opt-in for a # piped run. script_dir="" if [ -n "${RAVEN_LOCAL_SRC:-}" ]; then script_dir="$(CDPATH= cd -- "$RAVEN_LOCAL_SRC" 2>/dev/null && pwd || true)" [ -n "$script_dir" ] || die "RAVEN_LOCAL_SRC is not a directory: $RAVEN_LOCAL_SRC" is_raven_source "$script_dir" || die "RAVEN_LOCAL_SRC is not a raven source checkout: $script_dir" elif [ -f "$0" ]; then script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" is_raven_source "$script_dir" || script_dir="" fi if [ -n "$script_dir" ]; then info "Local raven source detected; editable install: $script_dir" build_web_assets "$script_dir" # Pin to the locked dependency set so an install matches what we test. constraints="$(mktemp)" uv export --directory "$script_dir" --frozen --all-extras --no-hashes --no-emit-workspace -o "$constraints" # Install all channel adapters by default. If the umbrella extra fails to # resolve/build on this platform, fall back to base raven so one broken # channel SDK cannot block the whole install. # The default config names the everos memory backend, which ships as its # own distribution beside the wheel -- carry it, and degrade loudly (the # host boots memoryless and `raven doctor` says why) if it cannot build. memory_dir="$script_dir/plugins-dist/everos-memory" # Raven-Design and Raven-PPT keep their harness in a distribution too, and # the roster gates on it: discovery reads the `engine` block in each # agents//subagent.json and disables the row when that package is # not importable where raven runs. Installing raven without them therefore # yields two agents that are listed and cannot be dispatched to, with the # remedy stated nowhere the installer ran. design_dir="$script_dir/plugins-dist/design-engine" ppt_dir="$script_dir/plugins-dist/ppt-engine" # tool_install [...]. The directories ride in the # positional parameters rather than in one string: sh has no arrays, and a # checkout path holding a space must stay one argv entry -- encoding the # option/path pairs into a scalar splits every one of them and ends the # ladder at bare raven with no plugin installed. The loop rotates the list # in place, taking each directory off the front and appending its option # pair to the back, so what remains is exactly the pairs. tool_install() { spec="$1" shift remaining=$# while [ "$remaining" -gt 0 ]; do set -- "$@" --with-editable "$1" shift remaining=$((remaining - 1)) done uv tool install --force -c "$constraints" "$@" -e "$spec" } # Four rungs, dropping one capability each: the engines carry native # builds (PDF, raster, plotting) that a platform can refuse on its own, so # they fall before the memory plugin rather than taking it down with them. if ! tool_install "$script_dir[channels]" "$memory_dir" "$design_dir" "$ppt_dir"; then warn "Channel dependencies failed to install; retrying with base raven. Some channels stay unavailable (see: raven channels list)." if ! tool_install "$script_dir" "$memory_dir" "$design_dir" "$ppt_dir"; then warn "A product engine failed to build; Raven-Design and Raven-PPT stay disabled (raven doctor explains)." if ! tool_install "$script_dir" "$memory_dir"; then warn "EverOS memory plugin failed to install; long-term memory stays off (raven doctor explains)." tool_install "$script_dir" fi fi fi else # Remote mode: install the latest published release wheel, which bundles # the prebuilt ui-tui/dist/entry.js (built by CI). We deliberately do NOT # install from git here -- the TUI bundle is a gitignored build artifact, # so a git install would yield a raven whose `raven tui` cannot start. # Override RAVEN_WHEEL_URL to pin a specific wheel. wheel_url="${RAVEN_WHEEL_URL:-}" if [ -z "$wheel_url" ]; then info "Resolving the latest raven release from GitHub..." release_json="$(curl -fsSL "https://api.github.com/repos/EverMind-AI/Raven/releases/latest" 2>/dev/null)" wheel_url="$(printf '%s' "$release_json" | grep -oE 'https://[^"]*/raven-[^"]*\.whl' | head -n1)" # The everos memory plugin ships as a sibling wheel from the same # release; older releases carry none, and its absence only means the # default memory backend degrades loudly at boot. everos_url="$(printf '%s' "$release_json" | grep -oE 'https://[^"]*/everos_memory-[^"]*\.whl' | head -n1)" design_url="$(printf '%s' "$release_json" | grep -oE 'https://[^"]*/design_engine-[^"]*\.whl' | head -n1)" ppt_url="$(printf '%s' "$release_json" | grep -oE 'https://[^"]*/ppt_engine-[^"]*\.whl' | head -n1)" fi if [ -z "$wheel_url" ]; then # The GitHub API caps unauthenticated callers at 60 requests/hour per IP, so a # shared egress can exhaust it. The release page carries no API quota: its # redirect names the latest stable tag, and the wheel URL is derived from it. warn "GitHub API returned no release wheel; falling back to the release page." tag="$(curl -fsS -o /dev/null -w '%{redirect_url}' \ "https://github.com/EverMind-AI/Raven/releases/latest")" || tag="" # Same shape the CLI and install.ps1 enforce: the redirect must land on this # repository's tag page, and the version must be exactly three numeric fields # with no leading zeros. case "$tag" in https://github.com/EverMind-AI/Raven/releases/tag/v*) version="${tag##*/}" ;; *) version="" ;; esac version="${version#v}" case "$version" in *.*.*.*) version="" ;; *.*.*) ;; *) version="" ;; esac if [ -n "$version" ]; then v_rest="${version#*.}" for field in "${version%%.*}" "${v_rest%%.*}" "${v_rest#*.}"; do case "$field" in ""|*[!0-9]*|0[0-9]*) version="" ;; esac done fi if [ -n "$version" ]; then wheel_url="https://github.com/EverMind-AI/Raven/releases/download/v${version}/raven-${version}-py3-none-any.whl" fi fi [ -n "$wheel_url" ] || die "Could not resolve the latest raven release wheel from GitHub. Retry later, or set RAVEN_WHEEL_URL to a wheel URL." # Derive the locked-constraints URL from the wheel URL (same release dir) so # the constraints always match the wheel being installed, including when # RAVEN_WHEEL_URL pins an older wheel. Missing asset / download failure -> # install without pinning rather than fail. constraints_url="${RAVEN_CONSTRAINTS_URL:-}" if [ -z "$constraints_url" ]; then case "$wheel_url" in *.whl) constraints_url="${wheel_url%/*}/raven-constraints.txt" ;; esac fi c_args="" if [ -n "$constraints_url" ]; then constraints="$(mktemp)" if curl -fsSL "$constraints_url" -o "$constraints" 2>/dev/null; then c_args="-c $constraints" else warn "Could not download locked constraints; installing without version pinning." fi fi info " installing $wheel_url" # shellcheck disable=SC2086 # $c_args is an intentional word-split option pair. p_args="" if [ -n "${everos_url:-}" ]; then # No spaces in the requirement: unquoted expansion must yield exactly # "--with" plus one argument, or uv rejects the extra words. p_args="--with everos-memory@$everos_url" info " with memory plugin $everos_url" else warn "This release carries no EverOS memory plugin wheel; long-term memory stays off (raven doctor explains)." fi # Same shape for the product engines the roster gates Raven-Design and # Raven-PPT on. Absent wheels are a warning, not a failure: the release # still installs, and the two rows stay disabled the way discovery # already reports them. # Kept in their own variable, not appended to the memory plugin's: the # ladder below drops the engines one rung before the memory plugin, and a # shared scalar would take everos-memory down with the first engine that # cannot resolve or build. e_args="" if [ -n "${design_url:-}" ]; then e_args="$e_args --with design-engine@$design_url" info " with design engine $design_url" else warn "This release carries no design-engine wheel; Raven-Design stays disabled (raven doctor explains)." fi if [ -n "${ppt_url:-}" ]; then e_args="$e_args --with ppt-engine@$ppt_url" info " with deck engine $ppt_url" else warn "This release carries no ppt-engine wheel; Raven-PPT stays disabled (raven doctor explains)." fi # shellcheck disable=SC2086 # $c_args / $p_args / $e_args are intentional word-split option pairs. if ! uv tool install --force $c_args $p_args $e_args "raven[channels] @ $wheel_url"; then warn "Channel dependencies failed to install; retrying with base raven. Some channels stay unavailable (see: raven channels list)." if ! uv tool install --force $c_args $p_args $e_args "$wheel_url"; then warn "A product engine failed to install; Raven-Design and Raven-PPT stay disabled (raven doctor explains)." if ! uv tool install --force $c_args $p_args "$wheel_url"; then warn "EverOS memory plugin failed to install; long-term memory stays off (raven doctor explains)." uv tool install --force $c_args "$wheel_url" fi fi fi fi # Ensure ~/.local/bin (uv tool bin dir) is on PATH for future shells. uv tool update-shell || true ok "raven installed" } # --- 4. optional capabilities: browser + LibreOffice ------------------------- # Both installs are best-effort: raven itself is already installed by the time # they run, so a failed download or a declined offer must never abort a # completed install. RAVEN_MINIMAL skips both. install_browser() { # The browser tool drives chromium through the playwright library inside the # raven tool venv, so both the probe and the download must use that venv's # python -- the system python knows nothing about this install. py="$(uv tool dir 2>/dev/null || true)/raven/bin/python" if [ ! -x "$py" ]; then warn "raven tool venv python not found; skipping the chromium download." return 0 fi # A pinned RAVEN_WHEEL_URL and the release-page fallback install no engine # wheels, so playwright can be absent even after a green install. if ! "$py" -c "import playwright" 2>/dev/null; then warn "This install carries no browser library (a pinned wheel URL or the release-page fallback installs no engines); the browser tool stays off." return 0 fi info "Downloading chromium for the browser tool..." # On Linux chromium may additionally need system libraries; playwright prints # the exact sudo command for them (--with-deps). We never run sudo ourselves. "$py" -m playwright install chromium \ || warn "Chromium download failed; the browser tool stays off. Retry later with: $py -m playwright install chromium" } install_office() { # soffice and libreoffice are the two launcher names the runtime resolves # (raven/utils/office.py); either one means deck preview already works. have soffice && return 0 have libreoffice && return 0 case "$NODE_OS" in darwin) if have brew; then info "Installing LibreOffice (deck preview)..." # A cask needs no sudo, so install directly rather than prompting. brew install --cask libreoffice \ || warn "LibreOffice install failed; deck preview stays off. Retry later with: brew install --cask libreoffice" else warn "LibreOffice not found; deck preview stays off. Install it later with: brew install --cask libreoffice" fi ;; linux) if ! have apt-get; then warn "LibreOffice not found; deck preview stays off. Install it with your system package manager (package: libreoffice)." return 0 fi # Installing needs sudo, so ask first -- and under `curl | sh` stdin is # the script itself, so the answer must come from the terminal. The # /dev/tty node can exist yet be unopenable (CI, cron, docker -t # without -i), so probe by opening it rather than stat-ing it; no # openable terminal means skip cleanly, never hang on the read. if ! { : < /dev/tty; } 2>/dev/null || ! have sudo; then warn "LibreOffice not found; deck preview stays off. Install it later with: sudo apt-get install -y libreoffice" return 0 fi # Default yes: for the deck lane this is the one dependency that matters # (the whole render-truth capability is soffice being present), and the # macOS path already installs it without asking. sudo's own password # prompt still stands between Enter and any change. printf 'Install LibreOffice for deck preview (needs sudo)? Without it a deck still builds, but no page is ever rendered, measured or checked. [Y/n] ' # A failed read is not an Enter: Ctrl-D, or a tty that closed after the # gate passed, must decline -- only a deliberate empty Enter accepts. answer="" read -r answer < /dev/tty || { warn "Skipping LibreOffice (no answer read); deck preview stays off. Install it later with: sudo apt-get install -y libreoffice" return 0 } case "$answer" in n|N|[nN][oO]) warn "Skipping LibreOffice; deck preview stays off. Install it later with: sudo apt-get install -y libreoffice" ;; *) # sudo's password prompt also reads stdin: give it the tty too. # shellcheck disable=SC2024 # input redirect on purpose; opening /dev/tty needs no elevation. sudo apt-get install -y libreoffice < /dev/tty \ || warn "LibreOffice install failed; deck preview stays off. Retry later with: sudo apt-get install -y libreoffice" ;; esac ;; esac } # --- 5. capability summary --------------------------------------------------- # One mouth for what actually landed: `raven doctor --install-summary` reads # only what is importable/installed, needs no config, and always exits 0. The # raven shim lands in `uv tool dir --bin`, which this shell's PATH may not # carry yet, so invoke it by absolute path. Purely informational -- the caller # guards the whole call so it can never fail a completed install. print_capability_summary() { bin="$(uv tool dir --bin 2>/dev/null || true)/raven" [ -x "$bin" ] || bin="$HOME/.local/bin/raven" [ -x "$bin" ] || return 0 printf '\n' info "Capabilities:" "$bin" doctor --install-summary \ || warn "capability summary unavailable (raven doctor failed)" } # --- main ------------------------------------------------------------------ main() { have curl || die "curl is required; please install it first" # Read before installing so the closing hint can tell a first run from an # upgrade; the install itself never writes config.json (the wizard does). if [ -f "$RAVEN_HOME/config.json" ]; then had_config=1 else had_config=0 fi detect_platform ensure_uv ensure_node install_raven # The summary is not gated: a minimal install still sees what it skipped. [ -n "${RAVEN_MINIMAL:-}" ] || install_browser [ -n "${RAVEN_MINIMAL:-}" ] || install_office print_capability_summary || true printf '\n' if [ "$had_config" = 1 ]; then ok "Raven updated. Your config in $RAVEN_HOME is unchanged." printf '\n \033[1mraven\033[0m # continue where you left off\n\n' printf ' tip: next time you can upgrade in place with \033[1mraven upgrade\033[0m\n\n' else ok "All set! Open a new terminal (or source your shell profile), then run:" printf '\n \033[1mraven\033[0m # sets you up on first run, then opens the TUI\n\n' fi if ! printf '%s' "$PATH" | grep -q "$HOME/.local/bin"; then warn "Your current PATH does not include ~/.local/bin yet -- open a new terminal, or run: export PATH=\"\$HOME/.local/bin:\$PATH\"" fi } main "$@"