#!/bin/sh
set -e

# Hivemind installer — https://deeplake.ai/hivemind.sh
#
# Installs the @deeplake/hivemind CLI globally, then wires Hivemind into every
# AI assistant it detects on your machine (Claude Code, OpenClaw, Codex, Cursor,
# Hermes, pi) and opens your browser once for SSO.
#
# Usage:
#   curl -fsSL https://deeplake.ai/hivemind.sh | sh
#
#   # Install for a single assistant (forwarded to the CLI):
#   curl -fsSL https://deeplake.ai/hivemind.sh | sh -s -- claude install
#
#   # Headless / CI — pass the token to the script side of the pipe:
#   curl -fsSL https://deeplake.ai/hivemind.sh | HIVEMIND_TOKEN=<your-token> sh
#
#   # Scope captures to a workspace:
#   curl -fsSL https://deeplake.ai/hivemind.sh | HIVEMIND_WORKSPACE_ID=payments-service sh
#
#   # Unattended: install Node too, if it is missing. Without a terminal the
#   # installer will not download a runtime unless you ask for it here.
#   curl -fsSL https://deeplake.ai/hivemind.sh | sh -s -- install --install-node
#
# Telemetry:
#   This installer reports which stage it reached — and which one it died on —
#   so failures that happen before the CLI exists are visible to us at all. It
#   sends the Node/npm version, OS, architecture, shell name and a numeric exit
#   code. It never sends command output, paths, your username, hostname, or
#   anything from your environment.
#
#   Opt out:  curl -fsSL https://deeplake.ai/hivemind.sh | HIVEMIND_NO_TELEMETRY=1 sh

PKG="@deeplake/hivemind"
SCRIPT_VERSION="4"

HELP_URL="https://deeplake.ai/hivemind/help"

# The Node the installer downloads when the machine has none. Pinned by version
# AND by hash: fetching nodejs.org's SHASUMS256.txt at install time would prove
# only that the download was not truncated, since the same server serves both and
# anyone able to swap one can swap the other. These constants are the trust
# anchor, and they are only as good as how they were produced.
#
# BUMP PROCEDURE — a deliberate release step, not a chore to automate away:
#   1. curl -fsSLO https://nodejs.org/dist/vX.Y.Z/SHASUMS256.txt
#   2. curl -fsSLO https://nodejs.org/dist/vX.Y.Z/SHASUMS256.txt.sig
#   3. gpgv --keyring ./nodejs-keyring.kbx SHASUMS256.txt.sig SHASUMS256.txt
#      (build the keyring from the release keys listed in the Node README)
#   4. copy the five lines below FROM THE VERIFIED FILE, never from a mirror
#   5. bump NODE_BOOTSTRAP_VERSION in hivemind.sh AND hivemind.ps1 together
# Step 3 is the whole point: it is what makes these hashes worth more than the
# fetch they replace.
NODE_BOOTSTRAP_VERSION="22.20.0"
NODE_SHA256_darwin_arm64="cc04a76a09f79290194c0646f48fec40354d88969bec467789a5d55dd097f949"
NODE_SHA256_darwin_x64="00df9c5df3e4ec6848c26b70fb47bf96492f342f4bed6b17f12d99b3a45eeecc"
NODE_SHA256_linux_arm64="4181609e03dcb9880e7e5bf956061ecc0503c77a480c6631d868cb1f65a2c7dd"
NODE_SHA256_linux_x64="eeaccb0378b79406f2208e8b37a62479c70595e20be6b659125eb77dd1ab2a29"

# Node version the published package declares in "engines". Below this npm only
# prints an EBADENGINE warning and installs anyway, so the run is reported and
# then allowed to continue — the histogram of what people actually run on is
# the point, and a hard stop here would block installs that work today.
MIN_NODE_MAJOR="22"

BEACON_URL="${HIVEMIND_BEACON_URL:-https://api.deeplake.ai/telemetry/install-beacon}"

# Colors (disabled when stdout is not a terminal)
if [ -t 1 ]; then
    GREEN='\033[0;32m'; YELLOW='\033[0;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'; NC='\033[0m'
else
    GREEN=''; YELLOW=''; RED=''; BLUE=''; NC=''
fi

say() { printf '%b\n' "$*"; }
err() { printf '%b\n' "${RED}error:${NC} $*" >&2; }

# ---------------------------------------------------------------------------
# Telemetry
#
# Every call is bounded at 2 seconds and swallowed with `|| true`: an installer
# that is slower, or that fails, because our analytics are down would cost more
# than the numbers are worth. curl is required for the beacon and for nothing
# else — you got this script through curl, so it is here.
# ---------------------------------------------------------------------------

START_EPOCH=$(date +%s 2>/dev/null || echo 0)

# npm's own machine-readable failure code, filled in by install_pkg. Empty until
# npm actually fails.
NPM_CODE=""

# Set when this run downloaded its own Node. It decides whether a launcher is
# written afterwards: an install that used the machine's existing Node needs no
# launcher, and writing one would be a file we left behind for no reason.
NODE_WAS_BOOTSTRAPPED=0

# When we bootstrapped, the package MUST land beside the runtime we downloaded.
# Putting the private node first on PATH is not enough to guarantee that: npm
# resolves its global prefix from config, so a `prefix=` line in the user's
# ~/.npmrc or an exported npm_config_prefix wins over the runtime's own default.
# Measured, with a private node on PATH:
#     default prefix of the private npm: /tmp/node-v22.20.0-linux-x64
#     with npm_config_prefix set:        /tmp/elsewhere
#     with prefix= in ~/.npmrc:          /tmp/from-npmrc
# The package would install to the user's configured prefix while the launcher
# looks beside the runtime, find nothing, and return silently -- an install that
# reports success and leaves no working command. --prefix on the command line
# outranks both config sources, which is why it is passed explicitly.
NPM_PREFIX_ARGS=

# Filled in by the argument scan in main(), read by require_node.
INSTALL_NODE=0

# Distinguishes two genuine attempts on one machine from one attempt retried by
# the transport: the server namespaces this per install_id and per step, so a
# re-run counts and a curl retry does not.
# A pipeline reports only the last command's status, so `od | tr || fallback`
# never reaches the fallback: tr succeeds on the empty input a failed od left it.
# Check the value instead of the exit code.
RUN_TOKEN=$(od -An -tx1 -N4 /dev/urandom 2>/dev/null | tr -d ' \n')
[ -n "$RUN_TOKEN" ] || RUN_TOKEN="$$"

# Each source is checked for a well-formed result rather than for a zero exit:
# these are pipelines, and a pipeline reports only its last command, so a failed
# producer would hand back an empty string that reads as success. An empty or
# malformed ID is rejected by the backend and would lose the run entirely.
is_uuid() {
    case "$1" in
        [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
            return 0 ;;
    esac
    return 1
}

uuid4() {
    if [ -r /proc/sys/kernel/random/uuid ]; then
        candidate=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || true)
        if is_uuid "$candidate"; then
            printf '%s' "$candidate"
            return 0
        fi
    fi
    if command -v uuidgen >/dev/null 2>&1; then
        # macOS uuidgen prints uppercase; the CLI and the backend both require
        # lowercase and treat anything else as corrupt.
        candidate=$(uuidgen 2>/dev/null | tr 'ABCDEF' 'abcdef')
        if is_uuid "$candidate"; then
            printf '%s' "$candidate"
            return 0
        fi
    fi
    candidate=$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n' | awk '{
        printf "%s-%s-4%s-a%s-%s", substr($0,1,8), substr($0,9,4),
               substr($0,14,3), substr($0,18,3), substr($0,21,12)
    }')
    if is_uuid "$candidate"; then
        printf '%s' "$candidate"
    fi
}

# The CLI reads ~/.deeplake/install-id and adopts any valid UUID it finds there
# (src/commands/install-id.ts). Writing it here first is what makes one key span
# download → install → login, so the shell-phase events and signup_intent land
# on the same person instead of two halves that cannot be joined.
resolve_install_id() {
    id_file="$HOME/.deeplake/install-id"
    if [ -r "$id_file" ]; then
        existing=$(tr -d '\n\r \t' < "$id_file" 2>/dev/null || true)
        case "$existing" in
            [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f]-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
                printf '%s' "$existing"
                return 0
                ;;
        esac
    fi
    new_id=$(uuid4)
    # Same modes the CLI applies: 0700 on the directory, 0600 on the file.
    (umask 077; mkdir -p "$HOME/.deeplake" >/dev/null 2>&1 || true
     printf '%s' "$new_id" > "$id_file" 2>/dev/null || true)

    # An ID we could not persist is worse than no ID: the CLI would mint its own
    # on the next run, so this one identifies a single beacon burst and nothing
    # else — a PostHog person that can never be joined to a signup and inflates
    # the denominator forever. install-id.ts returns an empty string for the
    # same reason. Read it back rather than trusting the write.
    written=$(tr -d '\n\r \t' < "$id_file" 2>/dev/null || true)
    if [ "$written" = "$new_id" ]; then
        printf '%s' "$new_id"
    fi
}

# INSTALL_ID is empty when the user opted out, so the opt-out is enforced in one
# place instead of being re-checked at every call site.
telemetry_enabled() {
    [ -n "$INSTALL_ID" ] && command -v curl >/dev/null 2>&1
}

elapsed_ms() {
    now=$(date +%s 2>/dev/null || echo 0)
    if [ "$START_EPOCH" -gt 0 ] 2>/dev/null; then
        echo $(( (now - START_EPOCH) * 1000 ))
    else
        echo 0
    fi
}

# beacon <step> [exit_code]
#
# --data-urlencode does the escaping, so a version string or a --ref code with
# an unexpected character cannot corrupt the body and silently drop the event
# we are trying to record.
beacon() {
    telemetry_enabled || return 0
    b_step="$1"
    b_code="${2:-}"
    curl -sS -o /dev/null -m 2 -X POST "$BEACON_URL" \
        -H "X-Hivemind-Install-Id: $INSTALL_ID" \
        -H "X-Deeplake-Client: hivemind-installer/$SCRIPT_VERSION" \
        --data-urlencode "step=$b_step" \
        --data-urlencode "script_version=$SCRIPT_VERSION" \
        --data-urlencode "node=$ENV_NODE" \
        --data-urlencode "npm=$ENV_NPM" \
        --data-urlencode "os=$ENV_OS" \
        --data-urlencode "arch=$ENV_ARCH" \
        --data-urlencode "shell=$ENV_SHELL" \
        --data-urlencode "ci=$ENV_CI" \
        --data-urlencode "tty=$ENV_TTY" \
        --data-urlencode "prefix_writable=$ENV_PREFIX_WRITABLE" \
        --data-urlencode "ref=$REF_CODE" \
        --data-urlencode "code=$b_code" \
        --data-urlencode "npm_code=$NPM_CODE" \
        --data-urlencode "ms=$(elapsed_ms)" \
        --data-urlencode "idem=$RUN_TOKEN" \
        >/dev/null 2>&1 || true
}

# Everything here is a bounded enumeration or a number. Nothing derived from a
# path, a username, a hostname, or command output goes into these.
collect_env() {
    ENV_NODE=$(command -v node >/dev/null 2>&1 && node -v 2>/dev/null || echo none)
    ENV_NPM=$(command -v npm >/dev/null 2>&1 && npm -v 2>/dev/null || echo none)
    ENV_OS=$(uname -s 2>/dev/null || echo unknown)
    ENV_ARCH=$(uname -m 2>/dev/null || echo unknown)
    # basename only: $SHELL is a path, and on a Nix or home-prefixed install it
    # carries the username.
    ENV_SHELL=$(basename "${SHELL:-unknown}" 2>/dev/null || echo unknown)
    [ -n "$CI" ] && ENV_CI=1 || ENV_CI=0
    # A run with no terminal cannot show the SSO prompt, so the CLI skips auth
    # and never reaches signup_intent. Without this flag that legitimate outcome
    # is indistinguishable from a user who dropped out.
    #
    # `[ -e /dev/tty ]` is NOT that test: the device node exists on every normal
    # macOS and Linux system, including inside a container where the process has
    # no controlling terminal and opening it fails with ENXIO. Opening it is the
    # only honest check — and getting this wrong sends an unattended run down the
    # interactive sudo branch, straight into the password prompt that `sudo -n`
    # exists to avoid.
    if (exec 3<>/dev/tty) 2>/dev/null; then
        ENV_TTY=1
    else
        ENV_TTY=0
    fi
    ENV_PREFIX_WRITABLE=0
    if command -v npm >/dev/null 2>&1; then
        npm_prefix=$(npm config get prefix 2>/dev/null || echo "")
        # An `a && b && c=1` chain returns non-zero when the prefix is NOT
        # writable, and as the last command in this function that ends the whole
        # script under `set -e` — silently, before a single beacon is sent, in
        # exactly the EACCES case this instrumentation exists to measure. The
        # else branch is what keeps the status zero.
        if [ -n "$npm_prefix" ] && [ -w "$npm_prefix" ]; then
            ENV_PREFIX_WRITABLE=1
        else
            ENV_PREFIX_WRITABLE=0
        fi
    fi
}

# ---------------------------------------------------------------------------

# Where a private Node lives. Under the XDG data dir, versioned, owned entirely
# by us: removing it is one directory and the machine is exactly as it was.
# Nothing here touches a system path or needs elevation.
node_prefix() {
    printf '%s/hivemind/node/%s' "${XDG_DATA_HOME:-$HOME/.local/share}" "$NODE_BOOTSTRAP_VERSION"
}

# The tarball nodejs.org publishes for this machine, or "" for a platform we have
# no pinned hash for. An unknown architecture falls through to the instructions
# rather than failing opaquely on a 404 halfway through a download.
node_tarball_name() {
    n_os=""
    case "$(uname -s 2>/dev/null)" in
        Darwin) n_os="darwin" ;;
        Linux)  n_os="linux" ;;
    esac
    n_arch=""
    case "$(uname -m 2>/dev/null)" in
        x86_64|amd64)  n_arch="x64" ;;
        arm64|aarch64) n_arch="arm64" ;;
    esac
    [ -n "$n_os" ] && [ -n "$n_arch" ] || return 0
    printf 'node-v%s-%s-%s.tar.gz' "$NODE_BOOTSTRAP_VERSION" "$n_os" "$n_arch"
}

# The pinned hash for a tarball name, or "" when we have none.
node_expected_sha() {
    case "$1" in
        *-darwin-arm64.tar.gz) printf '%s' "$NODE_SHA256_darwin_arm64" ;;
        *-darwin-x64.tar.gz)   printf '%s' "$NODE_SHA256_darwin_x64" ;;
        *-linux-arm64.tar.gz)  printf '%s' "$NODE_SHA256_linux_arm64" ;;
        *-linux-x64.tar.gz)    printf '%s' "$NODE_SHA256_linux_x64" ;;
    esac
}

# Emits its argument as a single-quoted POSIX shell literal, safe to paste into
# generated shell text. Single quotes suppress every expansion, so the only
# character needing care is a single quote itself: close the quoted run, emit an
# escaped quote, reopen. This is the standard POSIX idiom and it has no
# dependencies, which matters in a script that runs before Node exists.
sh_quote() {
    printf '%s' "'$(printf '%s' "$1" | sed "s/'/'\\\\''/g")'"
}

sha256_of() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" 2>/dev/null | cut -d" " -f1
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256 "$1" 2>/dev/null | cut -d" " -f1
    fi
}

# Every failure path in install_node ends here, so the half-finished directory is
# never left for a later run to mistake for a good runtime.
discard_node_staging() {
    [ -n "${NODE_STAGING:-}" ] || return 0
    case "$NODE_STAGING" in
        # Symmetric with the prefix guard in install_node. A dot segment cannot
        # satisfy the .incoming. pattern below, so this is belt and braces rather
        # than a live hole -- but the two guards should not disagree about what a
        # safe path looks like, because that disagreement is how the last three
        # defects in this work got in.
        */hivemind/node/. | */hivemind/node/..) ;;
        */hivemind/node/*.incoming.*) rm -rf "$NODE_STAGING" 2>/dev/null || true ;;
    esac
    NODE_STAGING=""
}

say_node_instructions() {
    say ""
    say "Install Node.js ${MIN_NODE_MAJOR}+ and re-run this installer:"
    say "  macOS:  ${BLUE}brew install node${NC}"
    say "  Linux:  https://nodejs.org  (or your distro's package manager)"
    say ""
    say "Or let this installer do it, without touching anything system-wide:"
    say "  ${BLUE}curl -fsSL https://deeplake.ai/hivemind.sh | sh -s -- install --install-node${NC}"
    say ""
    say "More help: ${BLUE}${HELP_URL}#troubleshooting-node${NC}"
}

# Downloads Node into the private prefix and puts it on PATH for this run.
# Returns non-zero and leaves nothing behind on any failure.
install_node() {
    tarball=$(node_tarball_name)
    expected=$(node_expected_sha "$tarball")
    if [ -z "$tarball" ] || [ -z "$expected" ]; then
        # No pinned hash for this platform. Downloading something we cannot
        # verify would be worse than printing the instructions.
        return 1
    fi
    command -v tar >/dev/null 2>&1 || return 1

    prefix=$(node_prefix)
    beacon node_install_started
    say "${BLUE}Downloading Node ${NODE_BOOTSTRAP_VERSION} (about 55 MB) ...${NC}"

    # Staged beside the prefix and moved into place only once verified, so an
    # interrupted download cannot leave a partial runtime behind.
    NODE_STAGING="${prefix}.incoming.$$"
    ( umask 077; mkdir -p "$NODE_STAGING" ) 2>/dev/null || { NODE_STAGING=""; return 1; }

    if ! curl -fsSL --retry 2 -o "$NODE_STAGING/$tarball" \
        "https://nodejs.org/dist/v${NODE_BOOTSTRAP_VERSION}/$tarball"; then
        discard_node_staging
        return 1
    fi

    actual=$(sha256_of "$NODE_STAGING/$tarball")
    if [ -z "$actual" ] || [ "$actual" != "$expected" ]; then
        # No checksum tool, or the wrong bytes. Both mean the same thing: we
        # cannot say this is Node, so we do not run it.
        discard_node_staging
        return 1
    fi

    if ! tar -xzf "$NODE_STAGING/$tarball" -C "$NODE_STAGING" 2>/dev/null; then
        discard_node_staging
        return 1
    fi
    extracted="$NODE_STAGING/${tarball%.tar.gz}"
    if [ ! -x "$extracted/bin/node" ]; then
        discard_node_staging
        return 1
    fi

    ( umask 077; mkdir -p "$(dirname "$prefix")" ) 2>/dev/null || true
    # The same shape guard discard_node_staging applies, and for the same reason:
    # this is a RECURSIVE delete of a path assembled from $XDG_DATA_HOME or
    # $HOME, neither of which this script chose. The guard costs one line and
    # means a surprising environment cannot turn a reinstall into a recursive
    # delete of somewhere else. discard_node_staging had this and its sibling
    # here did not, which is the same code-right-in-one-place inconsistency that
    # produced two other defects in this work.
    case "$prefix" in
        # A final segment of . or .. satisfies the shape below and then resolves
        # UPWARD -- .../hivemind/node/.. IS .../hivemind -- so the recursive
        # delete would take the parent tree. Not reachable today (the segment is
        # a hardcoded version), rejected anyway: this guard's whole job is to be
        # paranoid, and one that accepts .. is not.
        */hivemind/node/. | */hivemind/node/..)
            discard_node_staging
            return 1
            ;;
        */hivemind/node/?*)
            if [ -d "$prefix" ]; then
                rm -rf "$prefix" 2>/dev/null || true
            fi
            ;;
        *)
            # Refuse rather than guess. A prefix that does not look like ours is
            # not ours to delete.
            discard_node_staging
            return 1
            ;;
    esac
    if ! mv "$extracted" "$prefix" 2>/dev/null; then
        discard_node_staging
        return 1
    fi
    discard_node_staging

    # For the rest of this run. The launcher written after a successful install
    # is what makes it work in the user's NEXT terminal.
    PATH="$prefix/bin:$PATH"
    export PATH
    NODE_WAS_BOOTSTRAPPED=1
    return 0
}

# Why a launcher and not a symlink: the published `hivemind` bin begins with
# `#!/usr/bin/env node`, so a symlink into a directory on PATH finds the program
# and then fails to find its interpreter — `env node` searches the user's PATH,
# which is exactly the PATH with no Node on it. Measured, that combination gives
# "/usr/bin/env: 'node': No such file or directory". This puts the private
# runtime on PATH first, so both halves resolve.
write_launcher() {
    launcher_dir="$HOME/.local/bin"
    launcher="$launcher_dir/hivemind"
    prefix=$(node_prefix)
    [ -x "$prefix/bin/hivemind" ] || return 0

    mkdir -p "$launcher_dir" 2>/dev/null || return 0
    if [ -e "$launcher" ] && ! grep -q "hivemind private-runtime launcher" "$launcher" 2>/dev/null; then
        say "${YELLOW}note:${NC} ${launcher} already exists and was left alone."
        return 0
    fi
    # The prefix is derived from $HOME or $XDG_DATA_HOME, which are the user's
    # to choose and can contain ANY byte a directory name can hold. Interpolating
    # it into generated shell text is code injection, not a quoting nit: a home
    # directory containing $(...) executed on every launch. Reproduced with
    # $(printf INJECTED), which ran twice.
    #
    # sh_quote emits a single-quoted shell literal, which has exactly one escape
    # to handle, so nothing inside it is ever expanded or executed.
    q_prefix=$(sh_quote "$prefix")
    # A quoted heredoc delimiter: the shell performs NO substitution inside this
    # body, so the only values that reach the file are the ones printf puts there.
    # An unquoted delimiter is what allowed the injection.
    {
        printf '%s\n' '#!/bin/sh'
        printf '%s\n' '# hivemind private-runtime launcher — written by the hivemind installer.'
        printf '%s\n' '# Remove this file and the runtime directory below to undo it.'
        printf 'PATH=%s/bin:$PATH\n' "$q_prefix"
        printf '%s\n' 'export PATH'
        printf 'exec %s/bin/hivemind "$@"\n' "$q_prefix"
    } > "$launcher" 2>/dev/null || return 0
    chmod 755 "$launcher" 2>/dev/null || true

    case ":$PATH:" in
        *":$launcher_dir:"*)
            say "${GREEN}hivemind${NC} is on your PATH via ${launcher}"
            ;;
        *)
            # The launcher exists either way, so this is one line of work rather
            # than a reinstall.
            say ""
            say "${YELLOW}One more step.${NC} hivemind was installed to:"
            say "  ${BLUE}${launcher}${NC}"
            say "which is not on your PATH, so the command will not be found yet. Add it:"
            say "  ${BLUE}export PATH=\"\$HOME/.local/bin:\$PATH\"${NC}"
            say "(put that line in your shell profile to make it permanent)"
            ;;
    esac
}

require_node() {
    if ! command -v node >/dev/null 2>&1; then
        beacon node_missing
        # A terminal separates the person this installer exists for — someone who
        # pasted a command and is watching — from a Dockerfile or a CI step. We
        # ask the first and never surprise the second with a 55 MB download it
        # did not request. --install-node is how the second one asks.
        if [ "$ENV_TTY" = "1" ] || [ "$INSTALL_NODE" = "1" ]; then
            want_node="$INSTALL_NODE"
            if [ "$want_node" != "1" ]; then
                beacon node_install_prompted
                say ""
                say "Hivemind needs Node.js ${MIN_NODE_MAJOR}+, which this machine does not have."
                say "I can install a private copy just for hivemind — no sudo, nothing"
                say "system-wide, removable by deleting one directory:"
                say "  ${BLUE}$(node_prefix)${NC}"
                say ""
                printf 'Press Enter to install, or type n to skip: '
                # /dev/tty, not stdin: under `curl | sh` stdin is the remaining
                # text of this script, so a bare `read` would consume it and then
                # hand the shell whatever was left to execute.
                node_answer=""
                read node_answer < /dev/tty || node_answer=""
                case "$node_answer" in
                    ""|y|Y|yes|Yes|YES) want_node=1 ;;
                    *) want_node=0 ;;
                esac
            fi
            if [ "$want_node" = "1" ]; then
                if install_node; then
                    # Pin npm's global prefix to the runtime we just downloaded.
                    NPM_PREFIX_ARGS="--prefix=$(node_prefix)"
                    beacon node_install_ok
                    say "${GREEN}Node ${NODE_BOOTSTRAP_VERSION} installed.${NC}"
                else
                    beacon node_install_failed
                    err "Could not install Node.js automatically."
                    say_node_instructions
                    exit 1
                fi
            else
                beacon node_install_declined
                say_node_instructions
                exit 1
            fi
        else
            err "Node.js is required but was not found."
            say_node_instructions
            exit 1
        fi
    fi
    if ! command -v npm >/dev/null 2>&1; then
        beacon node_missing
        err "npm is required but was not found."
        say ""
        # Debian and Ubuntu ship npm as its own package, so "it comes with
        # Node.js" sends exactly the users who hit this down the wrong path.
        say "On Debian/Ubuntu npm is a separate package:"
        say "  ${BLUE}sudo apt install npm${NC}"
        say "Elsewhere it ships with Node.js — reinstall Node from https://nodejs.org"
        exit 1
    fi

    # `node -v` prints e.g. v20.20.2; take the major.
    node_major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
    case "$node_major" in
        ''|*[!0-9]*) return 0 ;;  # unparseable — do not guess, let npm decide
    esac
    if [ "$node_major" -lt "$MIN_NODE_MAJOR" ]; then
        beacon node_too_old
        say "${YELLOW}note:${NC} ${PKG} declares Node ${MIN_NODE_MAJOR}+, you are on $(node -v)."
        say "      Not a blocker on its own — the installer carries on below and"
        say "      npm normally installs it anyway. Upgrade Node if the CLI then"
        say "      misbehaves:  ${BLUE}${HELP_URL}#troubleshooting-node${NC}"
    fi
}

# npm prints its failure reason as `npm error code EACCES` on a line of its own.
# That line carries no path, unlike the `npm error path ...` and `npm error
# Error: ...` lines around it, so it is the one piece of npm's output that can be
# reported without leaking a username or a registry URL. Only the bare code is
# kept, and only if it looks like one.
npm_error_code() {
    code=$(sed -n 's/^npm \(error\|ERR!\) code \([A-Z0-9]\{2,20\}\)$/\2/p' "$1" 2>/dev/null | head -1)
    case "$code" in
        E*) printf '%s' "$code" ;;
    esac
}

# npm refuses a global install outright when its own config turns workspaces on
# — `workspaces=true` or `workspace=<name>` in ~/.npmrc, or an npm_config_*
# variable in the environment. It reports that with a bare English sentence and
# no `npm error code` line, so npm_error_code finds nothing and the failure
# lands in the permissions advice below, which sends the user to a Node version
# manager that cannot fix an npm setting. This is the one npm failure sudo
# makes no difference to, so it gets its own branch.
# The second message is matched without its opening words on purpose: npm 10.9.8
# prints "Can not use --no-workspaces ...", npm 11.11.0 prints "Cannot use ...",
# and a match on either spelling alone silently misses half the installs.
npm_workspaces_error() {
    grep -q "Workspaces not supported for global packages" "$1" 2>/dev/null ||
        grep -q "no-workspaces and --workspace at the same time" "$1" 2>/dev/null
}

# The two refusals need different remediation, so they are counted apart.
# `workspaces=true` is what --workspaces=false already neutralises, and anything
# still reaching us with that message has it set somewhere the flag cannot reach.
# `workspace=<name>` is the case npm rejects ALONGSIDE the flag, and the only
# thing that clears it is deleting the setting.
npm_workspaces_named_error() {
    grep -q "no-workspaces and --workspace at the same time" "$1" 2>/dev/null
}

# Every remediation here is a file edit rather than an `npm config` command:
# with `workspaces=true` set, `npm config` itself refuses to run ("This command
# does not support workspaces"), and with `workspace=<name>` set the flag that
# would clear that refusal is rejected as a conflict. Editing the file is the
# one instruction that works for both.
# One line and a link, not a lecture. The full remediation — every npmrc
# location, the fact that `npm config` cannot remove the setting while it is set,
# and the npm_config_* environment case — lives on the help page, which can be
# corrected without redeploying a script people have already piped into a shell.
say_workspaces_fix() {
    err "npm is configured to use workspaces, and npm cannot install a global package that way."
    say "This is a setting in your npm config, not a problem with ${PKG}."
    say ""
    # The remediation is a FILE EDIT, never an `npm config` command. With this
    # setting in place npm config itself refuses to run -- "This command does not
    # support workspaces" -- so `npm config delete workspace` fails with
    # ENOWORKSPACES and leaves the user exactly where they started. An earlier
    # revision of this branch printed that command; the comment above
    # say_workspaces_fix on origin/main had already recorded why it cannot work,
    # and collapsing this message deleted the warning along with the guidance.
    if [ "$1" = "named" ]; then
        say "Your npm config names a workspace. Delete that line and re-run this"
        say "installer -- ${BLUE}npm config${NC} cannot remove it while it is set:"
    else
        say "Find the ${BLUE}workspaces${NC} setting in your npm config, delete that line,"
        say "then re-run. ${BLUE}npm config${NC} cannot remove it while it is set:"
    fi
    say "  ${BLUE}~/.npmrc${NC}        your own config, the usual one"
    say "  ${BLUE}./.npmrc${NC}        in the directory you ran this from"
    say "  ${BLUE}etc/npmrc${NC}       next to your Node install, machine-wide"
    say "If it comes from the environment instead, unset ${BLUE}npm_config_workspaces${NC}"
    say "and ${BLUE}npm_config_workspace${NC} and re-run."
    say ""
    say "Where it lives and how to clear it in every case:"
    say "  ${BLUE}${HELP_URL}#troubleshooting-npm-workspaces${NC}"
}

# Where one `npm install -g` attempt's output is left for the failure branches
# to read. Empty when no log file could be created at all.
NPM_ATTEMPT_LOG=""

# mktemp is missing on some minimal images, and without a log the workspaces
# refusal cannot be told apart from a permissions failure — which is the wrong
# advice this change exists to stop printing. So fall back to a path this
# script already owns rather than to no log. /tmp is deliberately not that
# fallback: a predictable name in a world-writable directory is a file anyone
# on the machine can pre-create as a symlink.
# npm_log_path <role> — a file this script can safely write, or "" if none.
npm_log_path() {
    path=$(mktemp 2>/dev/null || echo "")
    if [ -n "$path" ]; then
        printf '%s' "$path"
        return 0
    fi
    (umask 077; mkdir -p "$HOME/.deeplake") 2>/dev/null || return 0
    # $1 names the role, because this is called twice per attempt and two calls
    # in the same second would otherwise ask for the same name — which noclobber
    # then refuses, losing the capture on exactly the machines this fallback
    # exists for. A counter cannot do it: the callers are command substitutions,
    # so each runs in a subshell and any increment dies with it.
    path="$HOME/.deeplake/npm-install-$$-$(date +%s 2>/dev/null || echo 0)-${1:-out}.log"
    # noclobber, so this creates the file or fails: it will not write through an
    # existing file, or through a symlink someone left in its place. Set inside
    # a subshell so the rest of the script keeps its own redirection rules.
    ( set -C; : > "$path" ) 2>/dev/null || return 0
    printf '%s' "$path"
}

# npm_install_attempt <prefix> [prefix args...] — runs one global install with
# the given command prefix (`command` for the plain run, `sudo` for the retry),
# shows npm's output live, leaves it in NPM_ATTEMPT_LOG, and returns npm's own
# status.
#
# Both attempts go through here so both are readable afterwards. The sudo retry
# used to run bare, which left the elevated failure unexplained: a workspaces
# setting in root's npmrc reads as an ordinary permissions failure and gets the
# nvm advice that cannot fix it.
#
# Under `curl | sh` stdin is the remaining text of this script. npm, or any
# lifecycle script it runs, that reads stdin would consume it and the shell
# would then execute whatever was left. </dev/null closes that hole. sudo is
# unaffected: its password prompt reads /dev/tty, not stdin.
#
# --workspaces=false is what makes the install survive an npm config with
# workspaces turned on. It is a command-line flag rather than an exported
# npm_config_workspaces because sudo resets the environment, so a variable
# would be dropped on exactly the retry that needs it most.
npm_install_attempt() {
    NPM_ATTEMPT_LOG=$(npm_log_path out)
    attempt_code=0
    # The status file is created the same guarded way as the log, not as
    # "$NPM_ATTEMPT_LOG.rc": a name derived from one this script just printed is
    # predictable, and `echo $? > name` follows a symlink left in its place,
    # which would let a local user pick a file for this script — or for root, on
    # the sudo retry — to overwrite.
    attempt_rc=""
    if [ -n "$NPM_ATTEMPT_LOG" ]; then
        attempt_rc=$(npm_log_path rc)
    fi
    if [ -n "$attempt_rc" ]; then
        # npm's output is teed rather than swallowed: the user still sees the
        # install progress live, which can take minutes when the package
        # compiles native bindings, and a blank screen for that long reads as a
        # hang.
        #
        # A pipeline reports only its LAST command, so `npm ... | tee` would
        # hand back tee's status and every npm failure would look like a
        # success. POSIX sh has no PIPESTATUS; the status goes through a file.
        #
        # `set +e` inside the group, or `set -e` ends it the moment npm fails
        # and the status line never runs — leaving the real exit code lost. The
        # group is the left side of a pipe, so it is a subshell and this does
        # not leak to the rest of the script.
        { set +e; "$@" npm install -g --workspaces=false ${NPM_PREFIX_ARGS:+"$NPM_PREFIX_ARGS"} "$PKG" </dev/null 2>&1; echo $? > "$attempt_rc"; } | tee "$NPM_ATTEMPT_LOG"
        attempt_code=$(cat "$attempt_rc" 2>/dev/null || echo 1)
        rm -f "$attempt_rc" 2>/dev/null || true
    else
        # No usable pair of files: run without capture. Nothing can be read back
        # afterwards, so leave no half-written log claiming otherwise.
        discard_npm_log
        # `|| attempt_code=$?` is what keeps `set -e` from ending the script
        # here on a bare failing command, which is the whole failure this
        # instrumentation exists to make visible.
        "$@" npm install -g --workspaces=false ${NPM_PREFIX_ARGS:+"$NPM_PREFIX_ARGS"} "$PKG" </dev/null || attempt_code=$?
    fi
    case "$attempt_code" in
        ''|*[!0-9]*) attempt_code=1 ;;
    esac
    return "$attempt_code"
}

# GNU and busybox `rm -f ""` exit zero, but that is not guaranteed elsewhere —
# and a non-zero status here ends the whole script under `set -e`, on the
# success path, on the machines that could not create a log in the first place.
discard_npm_log() {
    [ -n "$NPM_ATTEMPT_LOG" ] || return 0
    # Cleanup is best-effort: a log this script cannot delete is not a reason to
    # end an install that worked.
    rm -f "$NPM_ATTEMPT_LOG" 2>/dev/null || true
    NPM_ATTEMPT_LOG=""
}

# Set once any attempt has been refused over workspaces. It decides which advice
# the final failure prints, and nothing else: an attempt refused this way is
# still allowed to fall through to the sudo retry, because root has its own
# config and the setting that stopped the user's run may simply not be in it.
NPM_WORKSPACES_REFUSED=0
# "named" once any attempt was refused over a config that NAMES a workspace,
# which is the case --workspaces=false cannot override. Decides the beacon step
# and which of the two remediations is printed.
NPM_WORKSPACES_KIND=""

note_workspaces_refusal() {
    [ -n "$NPM_ATTEMPT_LOG" ] || return 0
    npm_workspaces_error "$NPM_ATTEMPT_LOG" || return 0
    NPM_WORKSPACES_REFUSED=1
    if npm_workspaces_named_error "$NPM_ATTEMPT_LOG"; then
        NPM_WORKSPACES_KIND="named"
    fi
}

# The beacon step for whichever refusal we saw. Two steps rather than one because
# the remediation differs, and because the named case is the one whose count
# decides whether the filtered-npmrc option — validated and deliberately not
# built — ever needs reopening. It has fired zero times since 2026-09-03.
workspaces_step() {
    if [ "$NPM_WORKSPACES_KIND" = "named" ]; then
        printf 'npm_workspaces_named'
    else
        printf 'npm_workspaces_config'
    fi
}

install_pkg() {
    say "${BLUE}Installing ${PKG} ...${NC}"
    # `$?` after a bare `fi` is the status of the whole if-statement, which is
    # zero when the condition failed and there is no else — so reading it there
    # reports every npm failure as "code 0". `|| npm_code=$?` is where the real
    # exit status survives.
    npm_code=0
    npm_install_attempt command || npm_code=$?
    if [ "$npm_code" -eq 0 ]; then
        discard_npm_log
        beacon npm_ok
        return 0
    fi
    if [ -n "$NPM_ATTEMPT_LOG" ]; then
        NPM_CODE=$(npm_error_code "$NPM_ATTEMPT_LOG")
    fi
    # --workspaces=false already neutralises the common case (`workspaces=true`).
    # What still reaches here is a config that names a workspace
    # (`workspace=a`), which npm rejects alongside the flag.
    note_workspaces_refusal
    discard_npm_log

    # Global installs often fail with EACCES when the npm prefix is root-owned.
    if command -v sudo >/dev/null 2>&1; then
        beacon npm_sudo_retry "$npm_code"
        say "${YELLOW}Global install failed — retrying with sudo ...${NC}"
        # Without `|| { ... }` this is where `set -e` used to end the script:
        # the second failure exited with no beacon and no message at all, which
        # is precisely the silent disappearance this instrumentation is for.
        # With no terminal — `curl | sh` from a cron job, a Dockerfile, a
        # remote shell — a password prompt has nowhere to appear and the
        # install hangs there indefinitely, which is indistinguishable from a
        # user who walked away. -n makes it fail immediately instead, so the
        # beacon below actually fires.
        sudo_code=0
        if [ "$ENV_TTY" = "1" ]; then
            npm_install_attempt sudo || sudo_code=$?
        else
            npm_install_attempt sudo -n || sudo_code=$?
        fi
        if [ "$sudo_code" -ne 0 ]; then
            # root has its own npmrc, so the elevated run can hit the workspaces
            # refusal even when the user's run did not.
            note_workspaces_refusal
            discard_npm_log
            if [ "$NPM_WORKSPACES_REFUSED" -eq 1 ]; then
                beacon "$(workspaces_step)" "$sudo_code"
                say_workspaces_fix "$NPM_WORKSPACES_KIND"
                exit 1
            fi
            beacon npm_failed "$sudo_code"
            err "Failed to install ${PKG} even with sudo."
            say "Try a Node version manager instead, which needs no elevation:"
            say "  ${BLUE}https://github.com/nvm-sh/nvm${NC}"
            say ""
            say "More help: ${BLUE}${HELP_URL}#troubleshooting-npm-permissions${NC}"
            exit 1
        fi
        discard_npm_log
    else
        if [ "$NPM_WORKSPACES_REFUSED" -eq 1 ]; then
            beacon "$(workspaces_step)" "$npm_code"
            say_workspaces_fix "$NPM_WORKSPACES_KIND"
            exit 1
        fi
        beacon npm_failed "$npm_code"
        err "Failed to install ${PKG}."
        say "You may need elevated permissions:"
        say "  ${BLUE}sudo npm install -g ${PKG}${NC}"
        say ""
        say "More help: ${BLUE}${HELP_URL}#troubleshooting-npm-permissions${NC}"
        exit 1
    fi
}

run_hivemind() {
    if ! command -v hivemind >/dev/null 2>&1; then
        beacon path_missing
        err "hivemind was installed but is not on your PATH."
        say "Open a new terminal (or add your npm global bin dir to PATH), then run:"
        say "  ${BLUE}hivemind $*${NC}"
        say ""
        say "More help: ${BLUE}${HELP_URL}#troubleshooting-path${NC}"
        exit 1
    fi
    beacon cli_launched
    say "${BLUE}Running: hivemind $*${NC}"
    # Give the CLI a real terminal so the SSO prompt works under `curl | sh`.
    # ENV_TTY is the open-it-for-real check, not `[ -e /dev/tty ]`: in a headless
    # container the node exists but the open fails, and under `set -e` that ended
    # the install before the CLI ever ran.
    #
    # The CLI's own exit status is deliberately left to propagate: the script's
    # last act is to hand over to it, and swallowing a real failure here would
    # report success for an install that did not finish.
    # The CLI's exit status is captured rather than left to `set -e`, so a crash
    # produces a beacon instead of the script vanishing. It is still propagated
    # at the end: the script's last act is to hand over, and reporting success
    # for an install that did not finish would be a lie.
    #
    # Without cli_exited, "cli_launched and then no signup_intent" collapses
    # three different stories into one: the CLI crashed, it ran headless and
    # skipped auth by design, or the user declined the sign-in prompt. The tty
    # field separates the second; this separates the first.
    cli_code=0
    if [ "$ENV_TTY" = "1" ]; then
        hivemind "$@" < /dev/tty || cli_code=$?
    else
        hivemind "$@" || cli_code=$?
    fi
    beacon cli_exited "$cli_code"
    if [ "$cli_code" -ne 0 ]; then
        exit "$cli_code"
    fi
}

main() {
    # Opting out means opting out of the file too: someone who sets this and
    # then finds a freshly minted UUID on their disk has been told one thing and
    # given another. The CLI still writes its own on first login, which is its
    # decision to explain, not this script's.
    if [ -n "$HIVEMIND_NO_TELEMETRY" ]; then
        INSTALL_ID=""
    else
        INSTALL_ID=$(resolve_install_id)
    fi
    collect_env

    # The affiliate code is forwarded to the CLI as-is; it is read here only so
    # the shell-phase events carry the same campaign attribution the device
    # flow parks, instead of two breakdowns that have to be unioned.
    REF_CODE=""
    prev_arg=""
    for arg in "$@"; do
        case "$prev_arg" in --ref) REF_CODE="$arg" ;; esac
        case "$arg" in --ref=*) REF_CODE="${arg#--ref=}" ;; esac
        prev_arg="$arg"
    done
    # A campaign code is a short slug. Anything else here is a mistake or a path
    # someone pasted, and it would be stored verbatim next to a person record —
    # so drop it rather than forward it. The CLI still receives the original
    # argument untouched; only the telemetry copy is filtered.
    case "$REF_CODE" in
        "") ;;
        *[!A-Za-z0-9._-]*) REF_CODE="" ;;
        ????????????????????????????????????????????????????????????????*) REF_CODE="" ;;
    esac

    # The installer's own flags are REMOVED from what the CLI is handed, and this
    # is the only place that can do it: POSIX sh has no way for a function to
    # rewrite its caller's "$@", so the rebuild is inline.
    #
    # `for arg in "$@"` expands the list once, up front, so shifting and
    # re-appending inside the loop is safe. Every kept argument is re-appended
    # with `set -- "$@" "$arg"` — never reconstructed into a string and never
    # passed through eval — so boundaries, order, empty strings, embedded
    # whitespace and literal glob characters all survive exactly.
    #
    # Why the flag must not simply be forwarded: the script hands its remaining
    # arguments to the CLI, and the CLI reads argv[0] as the command name. A
    # stray `--install-node` would arrive as an unknown command and exit 1 AFTER
    # npm had already installed successfully — the funnel would read a working
    # install as a CLI crash. Stripping also restores the zero-argument case
    # below, so `sh -s -- --install-node` still ends up running `hivemind install`.
    INSTALL_NODE=0
    past_ddash=0
    for arg in "$@"; do
        shift
        if [ "$past_ddash" -eq 1 ]; then
            set -- "$@" "$arg"
            continue
        fi
        case "$arg" in
            # Everything after a literal `--` belongs to the CLI verbatim.
            --) past_ddash=1; set -- "$@" "$arg"; continue ;;
            --install-node) INSTALL_NODE=1; continue ;;
        esac
        set -- "$@" "$arg"
    done

    beacon started

    require_node
    install_pkg

    # Only meaningful when this run brought its own Node: the package landed in
    # the private prefix, which is on PATH for this process and nowhere else.
    # Without this the install would look like it worked and then `hivemind`
    # would not exist in the user's next terminal.
    if [ "$NODE_WAS_BOOTSTRAPPED" = "1" ]; then
        write_launcher
    fi

    # Default to a full install across all detected assistants. Any args after
    # `sh -s --` (e.g. `claude install`, `install --ref <code>`) are forwarded
    # verbatim to the CLI.
    if [ "$#" -eq 0 ]; then
        set -- install
    fi

    run_hivemind "$@"

    say ""
    say "${GREEN}Hivemind is ready.${NC} Restart your assistants and they all share the same brain."
}

main "$@"
