#!/bin/sh # nerdit installer — https://get.nerdit.ai # # Installs the nerdit node (CLI + daemon + bundled Caddy) from a signed # release tarball and registers a service-manager unit (systemd on Linux, # launchd on macOS). Usage: # # curl -fsSL https://get.nerdit.ai | sh # Linux (as root) / macOS # curl -fsSL https://get.nerdit.ai | sudo sh # Linux # NERDIT_INSTALL_MODE=user sh install.sh # Linux, per-user install # NERDIT_VERSION=0.5.0 sh install.sh # pin a version # # This script is the SOURCE OF TRUTH, versioned in lwi00/nerdit_private at # packaging/install.sh and deployed to get.nerdit.ai (see # packaging/hosting/README.md). It is NEVER hand-edited on the live host: # edit it here, run the deploy one-liner. The same file is shipped inside # every release tarball and is what `nerdit update` re-executes. # # It is also its own updater: re-running it over an existing install stops the # service, swaps the version directory, flips `current`, and restarts. The # data directory (~/.nerdit) and config.toml are never touched. set -eu # -------------------------------------------------------------------------- # Constants # -------------------------------------------------------------------------- REPO="${NERDIT_RELEASES_REPO:-nerdit-ai/releases}" REQ_VERSION="${NERDIT_VERSION:-}" REQ_VERSION="${REQ_VERSION#v}" DAEMON_PORT=9321 CONSOLE_URL="https://app.nerdit.ai" MIN_FREE_KB=1048576 # 1 GiB # --- release signing public key (ECDSA P-256 — D-P30-11 rev 1.2). P-256 and # --- not Ed25519 because stock macOS ships LibreSSL, which cannot parse an # --- Ed25519 key at all; `dgst -sha256 -verify` works on every LibreSSL and # --- OpenSSL build we support, so macOS onboarding stays two commands. NERDIT_RELEASE_PUBKEY_PEM="-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErD99ocEK39T60IgnOpitYeNoHD+V qwskFP+Z5xTkEFhI+zNwUBr06bBcVTzBH71xc472T0ZHRrr5aK6evs6oSg== -----END PUBLIC KEY-----" # -------------------------------------------------------------------------- # Helpers # -------------------------------------------------------------------------- say() { echo "$*" } warn() { echo "warning: $*" >&2 } die() { echo "error: $*" >&2 exit 1 } # Free space on the filesystem holding $1 (walking up to the first existing # directory), against MIN_FREE_KB. Both the install root AND the staging temp # dir go through this: they are frequently different filesystems (/opt vs a # tmpfs /tmp), and a full /tmp used to abort the install *after* the service # had already been stopped. check_free_space() { _ck_target="$1" while [ ! -d "$_ck_target" ]; do _ck_target="$(dirname "$_ck_target")" done _ck_free="$(df -Pk "$_ck_target" 2>/dev/null | awk 'NR>1 {print $4; exit}')" case "${_ck_free:-}" in '' | *[!0-9]*) warn "could not determine free space on $_ck_target; continuing." ;; *) if [ "$_ck_free" -lt "$MIN_FREE_KB" ]; then die "not enough free space on $_ck_target: $((_ck_free / 1024)) MiB available, $((MIN_FREE_KB / 1024)) MiB required." fi ;; esac } # True when version $1 sorts strictly before version $2. Dot-separated numeric # comparison in awk (`sort -V` is not POSIX and BSD sort lacks it); a # non-numeric component compares as 0, which is enough for the one thing this # gates: refusing a silent downgrade. version_lt() { awk -v a="$1" -v b="$2" ' BEGIN { na = split(a, x, ".") nb = split(b, y, ".") n = (na > nb) ? na : nb for (i = 1; i <= n; i++) { av = (i <= na) ? x[i] + 0 : 0 bv = (i <= nb) ? y[i] + 0 : 0 if (av < bv) { exit 0 } if (av > bv) { exit 1 } } exit 1 }' } # -------------------------------------------------------------------------- # 1. Platform detection # -------------------------------------------------------------------------- case "$(uname -s)" in Linux) OS=linux ;; Darwin) OS=macos ;; *) die "nerdit supports Linux and macOS only." ;; esac case "$(uname -m)" in x86_64 | amd64) ARCH=x86_64 ;; aarch64 | arm64) ARCH=arm64 ;; *) die "nerdit does not ship a build for this CPU architecture ($(uname -m))." ;; esac if [ "$OS" = macos ] && [ "$ARCH" = x86_64 ]; then die "nerdit ships for Apple Silicon only; Intel Macs are not supported." fi if [ "$OS" = linux ] && [ ! -d /run/systemd/system ]; then die "nerdit v1 requires systemd to manage the daemon (see docs)." fi # -------------------------------------------------------------------------- # 2. Mode selection and layout # -------------------------------------------------------------------------- EUID_NOW="$(id -u)" if [ "$OS" = macos ]; then [ "$EUID_NOW" != 0 ] || die "do not install nerdit on macOS with sudo; the LaunchAgent is per-user, so rerun this as your normal user." MODE=user elif [ "$EUID_NOW" = 0 ]; then if [ "${NERDIT_INSTALL_MODE:-}" = user ]; then die "NERDIT_INSTALL_MODE=user is a non-root install; rerun it without sudo." fi MODE=system elif [ "${NERDIT_INSTALL_MODE:-}" = user ]; then MODE=user else die "installing nerdit on Linux needs root: rerun 'curl -fsSL https://get.nerdit.ai | sudo sh', or set NERDIT_INSTALL_MODE=user for a per-user install under \$HOME/.nerdit." fi PLIST_DST="" if [ "$MODE" = system ]; then ROOT=/opt/nerdit VERSIONS_DIR=/opt/nerdit SHIM=/usr/local/bin/nerdit SHIM_TARGET=/opt/nerdit/current/nerdit UNIT_SRC=units/nerdit.service UNIT_DST=/etc/systemd/system/nerdit.service UNIT_USER="${SUDO_USER:-root}" else [ -n "${HOME:-}" ] || die "HOME is not set; a per-user install needs it." ROOT="$HOME/.nerdit" VERSIONS_DIR="$HOME/.nerdit/versions" SHIM="$HOME/.nerdit/bin/nerdit" SHIM_TARGET="../current/nerdit" if [ "$OS" = macos ]; then UNIT_SRC=units/ai.nerdit.daemon.plist UNIT_DST="$HOME/Library/LaunchAgents/ai.nerdit.daemon.plist" PLIST_DST="$UNIT_DST" else UNIT_SRC=units/nerdit-user.service UNIT_DST="$HOME/.config/systemd/user/nerdit.service" fi UNIT_USER="$(id -un)" fi case "$UNIT_USER" in '' | *[!A-Za-z0-9._-]*) die "refusing to build a service unit for the unusual user name '$UNIT_USER'." ;; esac # An install that already exists OWNS its service identity, and this script # must not re-derive it. The daemon's data dir is $HOME of the unit user, so # rewriting User=/Environment=HOME= from whoever happens to run *this* # invocation (SUDO_USER is set under `sudo nerdit update`, unset in a root # shell) would silently move a live node onto an empty data dir — services, # secrets, the internal CA and the node identity all still on disk, none of # them visible to the daemon. Read what the unit records and keep it. RECORDED_HOME="" if [ "$MODE" = system ] && [ -f "$UNIT_DST" ]; then RECORDED_USER="$(sed -n 's/^ *User *= *\([^ ]*\).*/\1/p' "$UNIT_DST" | head -n 1)" RECORDED_HOME="$(sed -n 's/^ *Environment=HOME=\([^ "]*\).*/\1/p' "$UNIT_DST" | head -n 1)" case "${RECORDED_USER:-}" in '' | *[!A-Za-z0-9._-]*) RECORDED_USER="" ;; esac if [ -n "${RECORDED_USER:-}" ] && [ "$RECORDED_USER" != "$UNIT_USER" ]; then say "keeping the service user recorded in $UNIT_DST: '$RECORDED_USER' (this run would have used '$UNIT_USER'; the recorded one owns the data dir)" UNIT_USER="$RECORDED_USER" fi case "$RECORDED_HOME" in /*) ;; *) RECORDED_HOME="" ;; esac fi if [ "$MODE" = system ]; then UNIT_HOME="$RECORDED_HOME" if [ -z "$UNIT_HOME" ] && command -v getent >/dev/null 2>&1; then UNIT_HOME="$(getent passwd "$UNIT_USER" | cut -d: -f6)" fi if [ -z "${UNIT_HOME:-}" ]; then UNIT_HOME="$(eval "echo ~$UNIT_USER")" fi case "$UNIT_HOME" in /*) ;; *) UNIT_HOME=/root ;; esac else UNIT_HOME="$HOME" fi CURRENT_LINK="$ROOT/current" NERDITD_PATH="$ROOT/current/nerditd" # -------------------------------------------------------------------------- # 3. Preflights — every refusal happens here, before the first mutation. # A failed preflight leaves `current` and the running service untouched. # -------------------------------------------------------------------------- # (a) required tools for _tool in curl tar; do command -v "$_tool" >/dev/null 2>&1 || die "$_tool is required and was not found; install it and rerun." done # Any openssl will do. The release signature is ECDSA P-256 verified through # `dgst -sha256 -verify`, which stock macOS LibreSSL supports (D-P30-11 # rev 1.2) — so there is no Homebrew path to hunt for and no prerequisite. OPENSSL="" command -v openssl >/dev/null 2>&1 && OPENSSL=openssl [ -n "$OPENSSL" ] || die "openssl is required to verify the release signature; install it and rerun." if command -v sha256sum >/dev/null 2>&1; then sha256() { sha256sum "$1" | cut -d' ' -f1; } elif command -v shasum >/dev/null 2>&1; then sha256() { shasum -a 256 "$1" | cut -d' ' -f1; } else die "sha256sum or shasum is required to verify the download; install one and rerun." fi # (b) the installer must be release-keyed case "$NERDIT_RELEASE_PUBKEY_PEM" in *PLACEHOLDER*) die "this installer is not yet release-keyed — see packaging/RELEASING.md." ;; esac # (c) Docker command -v docker >/dev/null 2>&1 || die "Docker is required and was not found — install it first: https://docs.docker.com/engine/install/" DOCKER_PROBE_USER="$(id -un)" DOCKER_OK=0 if [ "$MODE" = system ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then DOCKER_PROBE_USER="$SUDO_USER" if sudo -n -u "$SUDO_USER" docker info >/dev/null 2>&1; then DOCKER_OK=1 fi fi if [ "$DOCKER_OK" -eq 0 ]; then if docker info >/dev/null 2>&1; then DOCKER_OK=1 fi fi [ "$DOCKER_OK" -eq 1 ] || die "Docker is installed but its socket is not reachable by $DOCKER_PROBE_USER — is the daemon running, and is the user in the docker group?" # (d) disk space on the install filesystem (the staging temp dir is checked # the same way once mktemp has picked it — they are often different # filesystems, and only one of them was ever measured before) check_free_space "$ROOT" # (e) on update, the invoking user must own the existing install IS_UPDATE=0 INSTALLED_VERSION="" if [ -e "$CURRENT_LINK" ] || [ -L "$CURRENT_LINK" ]; then IS_UPDATE=1 if [ -f "$CURRENT_LINK/VERSION" ]; then INSTALLED_VERSION="$(tr -d '\r\n' <"$CURRENT_LINK/VERSION" 2>/dev/null || true)" fi [ -L "$CURRENT_LINK" ] || die "$CURRENT_LINK exists but is not a symlink; move it aside and rerun." # shellcheck disable=SC2012 # fixed path; `stat` uid flags differ GNU vs BSD OWNER_UID="$(ls -ldn "$CURRENT_LINK" | awk 'NR==1 {print $3; exit}')" case "${OWNER_UID:-}" in '' | *[!0-9]*) warn "could not determine the owner of $CURRENT_LINK; continuing." ;; *) if [ "$OWNER_UID" != "$EUID_NOW" ]; then die "the existing install at $ROOT is owned by uid $OWNER_UID; run the update as that user." fi ;; esac fi # -------------------------------------------------------------------------- # 4. Resolve the version # -------------------------------------------------------------------------- if [ -n "$REQ_VERSION" ]; then VERSION="$REQ_VERSION" say "version pinned by NERDIT_VERSION: $VERSION" else VERSION="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name"[^"]*"v\{0,1\}\([^"]*\)".*/\1/p' | head -n 1)" say "resolved latest release from $REPO: ${VERSION:-}" if [ -n "${SUDO_COMMAND:-}" ] && [ "$IS_UPDATE" = 0 ]; then # The documented pin is `| sudo NERDIT_VERSION=x sh`; writing the # assignment BEFORE sudo puts it in sudo's own environment, where # env_reset drops it — a silently ignored pin. say "(if you meant to pin a version under sudo, the assignment goes AFTER sudo: curl … | sudo NERDIT_VERSION=x.y.z sh)" fi fi [ -n "${VERSION:-}" ] || die "could not resolve the latest nerdit release from $REPO; set NERDIT_VERSION=x.y.z to pin one." # A releases-repo compromise cannot forge a signature, but it CAN re-point # `latest` at an older, still-validly-signed release and roll every unpinned # install back onto known-vulnerable code. Going backwards therefore requires # an explicit pin (`nerdit update --version x.y.z` supplies one). if [ -z "$REQ_VERSION" ] && [ -n "$INSTALLED_VERSION" ] && version_lt "$VERSION" "$INSTALLED_VERSION"; then die "$REPO offers $VERSION but $INSTALLED_VERSION is installed — refusing to downgrade silently. Pin it explicitly if you mean it: nerdit update --version $VERSION" fi ASSET="nerdit-$VERSION-$OS-$ARCH.tar.gz" BASE_URL="https://github.com/$REPO/releases/download/v$VERSION" say "installing nerdit $VERSION ($OS/$ARCH, $MODE install) into $ROOT" # -------------------------------------------------------------------------- # 5. Download # -------------------------------------------------------------------------- # Service-manager verbs live in exactly one place each: the installer, the # rollback handler below and `nerdit link` must never drift into two dialects # of "restart the daemon" (they are mirrored in utils/install_layout.py). stop_unit() { if [ "$OS" = macos ]; then launchctl bootout "gui/$EUID_NOW/ai.nerdit.daemon" >/dev/null 2>&1 || true elif [ "$MODE" = system ]; then systemctl stop nerdit.service >/dev/null 2>&1 || true else systemctl --user stop nerdit.service >/dev/null 2>&1 || true fi } start_unit() { if [ "$OS" = macos ]; then launchctl bootstrap "gui/$EUID_NOW" "$PLIST_DST" >/dev/null 2>&1 || true launchctl kickstart -k "gui/$EUID_NOW/ai.nerdit.daemon" >/dev/null 2>&1 || true elif [ "$MODE" = system ]; then systemctl daemon-reload systemctl enable --now nerdit.service else systemctl --user daemon-reload systemctl --user enable --now nerdit.service fi } # Rollback state. Declared here — after the last preflight refusal, so the # "nothing is mutated before a refusal" contract still holds. TMP="" STAGING="" VDIR="" OLD_VDIR="" SERVICE_STOPPED=0 SWAP_DONE=0 cleanup() { _rc=$? # The dangerous window is: previous tree moved aside, replacement not yet # in place. Failing there used to leave a dangling `current`, a broken # shim, a stopped service and the only good tree under a name nobody has # seen. Put it back, say so, and bring the service up again. if [ "$_rc" -ne 0 ] && [ "$SWAP_DONE" -eq 0 ] && [ -n "$OLD_VDIR" ]; then if [ -d "$OLD_VDIR" ] && [ ! -e "$VDIR" ] && mv "$OLD_VDIR" "$VDIR" 2>/dev/null; then OLD_VDIR="" echo "error: install failed — restored the previous version at $VDIR" >&2 else echo "error: install failed and $VDIR could not be restored; the previous tree is at $OLD_VDIR — move it back by hand." >&2 fi fi if [ "$_rc" -ne 0 ] && [ "$SERVICE_STOPPED" -eq 1 ] && [ "$SWAP_DONE" -eq 0 ]; then echo "error: restarting the nerdit service that was stopped for this install" >&2 start_unit || true fi if [ -n "$STAGING" ]; then rm -rf "$STAGING" fi if [ -n "$TMP" ]; then rm -rf "$TMP" fi exit "$_rc" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM TMP="$(mktemp -d)" # The download and the extracted tree live HERE, not on the install # filesystem — a tmpfs /tmp on a small VM is the common case, and running out # of room mid-extract used to abort with the service already stopped. check_free_space "$TMP" for _f in "$ASSET" SHA256SUMS SHA256SUMS.sig; do curl -fsSL -o "$TMP/$_f" "$BASE_URL/$_f" || die "download failed: $BASE_URL/$_f" done # -------------------------------------------------------------------------- # 6. Verify — signature first, then checksum. Nothing is extracted before # both pass. # -------------------------------------------------------------------------- printf '%s\n' "$NERDIT_RELEASE_PUBKEY_PEM" >"$TMP/pub.pem" "$OPENSSL" dgst -sha256 -verify "$TMP/pub.pem" \ -signature "$TMP/SHA256SUMS.sig" "$TMP/SHA256SUMS" >/dev/null 2>&1 || die "release signature verification FAILED — refusing to install." say "release signature verified." EXPECTED="$(awk -v f="$ASSET" '$2 == f || $2 == "*" f {print $1; exit}' "$TMP/SHA256SUMS")" [ -n "${EXPECTED:-}" ] || die "SHA256SUMS does not list $ASSET — refusing to install." ACTUAL="$(sha256 "$TMP/$ASSET")" [ "$EXPECTED" = "$ACTUAL" ] || die "checksum mismatch for $ASSET — refusing to install." say "checksum verified." # -------------------------------------------------------------------------- # 7. Extract and stage — BEFORE the service is stopped, and onto the # destination filesystem. Everything that can still fail (a full /tmp, a # truncated tarball, a full /opt) happens with the daemon still running. # -------------------------------------------------------------------------- tar -xzf "$TMP/$ASSET" -C "$TMP" SRC_DIR="$TMP/nerdit-$VERSION" [ -d "$SRC_DIR" ] || die "the release tarball does not contain the expected nerdit-$VERSION directory." VDIR="$VERSIONS_DIR/$VERSION" # Explicit modes, never the inherited umask: sudo unions the invoking umask, # so a hardened 027/077 login would create /opt/nerdit unreadable by the # non-root user the system unit runs as — the daemon would fail with 203/EXEC # and the shim would be permission-denied for everyone. umask 022 if [ "$MODE" = system ]; then mkdir -p "$VERSIONS_DIR" chmod 0755 "$VERSIONS_DIR" else if [ ! -d "$ROOT" ]; then # $ROOT is also the data dir on a user install; 0700 is what the # daemon's own data_dir_perms check wants. mkdir -p "$ROOT" chmod 0700 "$ROOT" fi mkdir -p "$VERSIONS_DIR" fi # Stage on the destination filesystem so the swap below is a rename, not a # cross-filesystem copy that can die halfway with `current` already dangling. STAGING="$VERSIONS_DIR/.staging.$$" rm -rf "$STAGING" mv "$SRC_DIR" "$STAGING" if [ "$MODE" = system ]; then # tar run as root preserves the build machine's uid/gid; a system install # must be root-owned, not owned by whatever uid built the tarball. chown -R 0:0 "$STAGING" fi # -------------------------------------------------------------------------- # 8. Stop the running service (update path only) # -------------------------------------------------------------------------- if [ -f "$UNIT_DST" ]; then say "stopping the running nerdit service" stop_unit SERVICE_STOPPED=1 fi # -------------------------------------------------------------------------- # 9. Flip — two same-filesystem renames, then the symlink # -------------------------------------------------------------------------- if [ -e "$VDIR" ]; then # Reinstall of the same version: move the old tree aside rather than # deleting it in place, so a `nerdit update` running from inside it keeps # resolving its own files until the new tree is in position. OLD_VDIR="$VDIR.old.$$" rm -rf "$OLD_VDIR" mv "$VDIR" "$OLD_VDIR" fi mv "$STAGING" "$VDIR" STAGING="" SWAP_DONE=1 ln -sfn "$VDIR" "$CURRENT_LINK" if [ "$MODE" = system ]; then mkdir -p /usr/local/bin ln -sfn "$SHIM_TARGET" "$SHIM" else mkdir -p "$ROOT/bin" ln -sfn "$SHIM_TARGET" "$SHIM" case ":$PATH:" in *":$ROOT/bin:"*) ;; *) say "add nerdit to your PATH: export PATH=\"\$HOME/.nerdit/bin:\$PATH\"" ;; esac fi # -------------------------------------------------------------------------- # 10. Install / refresh the service unit # -------------------------------------------------------------------------- UNIT_TEMPLATE="$VDIR/$UNIT_SRC" [ -f "$UNIT_TEMPLATE" ] || die "the release tarball is missing $UNIT_SRC — refusing to leave the daemon unmanaged." mkdir -p "$(dirname "$UNIT_DST")" sed -e "s|__NERDITD__|$NERDITD_PATH|g" \ -e "s|__USER__|$UNIT_USER|g" \ -e "s|__HOME__|$UNIT_HOME|g" \ "$UNIT_TEMPLATE" >"$UNIT_DST" chmod 644 "$UNIT_DST" # -------------------------------------------------------------------------- # 10b. Mint the daemon auth token — BEFORE the unit is first started. # A tokenless daemon attaches the LOCAL *admin* principal to every # loopback request, so on a multi-user box every user on it is admin. # Idempotent (`--auth-token-only` never overwrites an existing # config.toml) and fresh-install only: minting one under an existing node # would break whatever already talks to it unauthenticated. # # It MUST run as the unit user. The daemon's data dir is $HOME of that # user, and `nerdit link` later reads the same config.toml as the same # user — a token written into root's home would be invisible to both. # -------------------------------------------------------------------------- if [ "$IS_UPDATE" = 0 ]; then if [ "$MODE" = system ] && [ "$UNIT_USER" != "$(id -un)" ]; then if command -v sudo >/dev/null 2>&1 && sudo -n -u "$UNIT_USER" env HOME="$UNIT_HOME" "$SHIM" init --auth-token-only; then : else warn "could not mint the daemon auth token as '$UNIT_USER'; the daemon will treat every user on this box as admin. Fix with: sudo -u $UNIT_USER $SHIM init --auth-token-only && " fi elif ! "$SHIM" init --auth-token-only; then warn "could not mint the daemon auth token; the daemon will treat every user on this box as admin. Fix with: $SHIM init --auth-token-only && " fi fi start_unit if [ "$MODE" != system ] && [ "$OS" != macos ]; then say "run 'loginctl enable-linger $UNIT_USER' so the daemon keeps running after you log out." # A --user unit cannot carry AmbientCapabilities (an unprivileged user # manager cannot raise one; the directive fails the unit with # 218/CAPABILITIES), so unlike the system install this mode cannot bind the # default [proxy].https_port = 443. Say so once, here, rather than letting # the operator discover it as a Caddy respawn loop. say "note: a per-user install cannot bind :443. If you enable the URL layer, either set an unprivileged port ($SHIM config set proxy https_port=8443) or run 'sudo setcap cap_net_bind_service=+ep $CURRENT_LINK/caddy' after every update." fi # -------------------------------------------------------------------------- # 11. Prune superseded version directories # Keep the one just installed and the one it replaced (that is what # `nerdit update --version ` rolls back to). A frozen bundle plus a # static Caddy is ~150-200 MiB per release; unbounded accumulation # eventually fails the disk preflight with an unrelated-looking message. # -------------------------------------------------------------------------- if [ -n "$OLD_VDIR" ]; then rm -rf "$OLD_VDIR" OLD_VDIR="" fi for _entry in "$VERSIONS_DIR"/*; do [ -d "$_entry" ] || continue if [ -L "$_entry" ]; then continue fi # A version dir is one the installer made: it carries the release stamp. [ -f "$_entry/VERSION" ] || continue _name="$(basename "$_entry")" if [ "$_name" = "$VERSION" ]; then continue fi if [ -n "$INSTALLED_VERSION" ] && [ "$_name" = "$INSTALLED_VERSION" ]; then continue fi say "removing superseded version $_name" rm -rf "$_entry" done # -------------------------------------------------------------------------- # 12. Post-install # -------------------------------------------------------------------------- HEALTHY=0 _i=0 while [ "$_i" -lt 30 ]; do if curl -fs "http://127.0.0.1:$DAEMON_PORT/health" >/dev/null 2>&1; then HEALTHY=1 break fi _i=$((_i + 1)) sleep 1 done if [ "$HEALTHY" -eq 0 ]; then if [ "$IS_UPDATE" = 0 ]; then # A node that never answered /health is not installed, whatever the # filesystem looks like. Say so with a non-zero exit rather than # ending on "Next step: nerdit link". die "the daemon did not answer http://127.0.0.1:$DAEMON_PORT/health within 30s. The files are in place at $ROOT; check the service logs (systemctl status nerdit.service / journalctl -u nerdit.service, or ~/.nerdit/nerditd-launchd.log on macOS) and rerun once the cause is fixed." fi warn "daemon did not report healthy within 30s; check the service logs" fi # The closing doctor MUST run as the unit user. D-P30-12 mints the auth token # into that user's ~/.nerdit/config.toml, and the CLI reads the token from the # config of whoever runs it — so a doctor run as the invoking user (root, under # `sudo sh install.sh`) authenticates with nothing, gets 401 from the daemon it # just installed, and ends a perfectly good install on a red # "daemon | fail | unreachable: 401 Unauthorized" table. Same `sudo -n -u` shape # as the mint in 10b. if [ "$MODE" = system ] && [ "$UNIT_USER" != "$(id -un)" ] && command -v sudo >/dev/null 2>&1; then sudo -n -u "$UNIT_USER" env HOME="$UNIT_HOME" "$SHIM" doctor || true else "$SHIM" doctor || true fi say "nerdit $VERSION is installed." say "Next step: nerdit link (get your code at $CONSOLE_URL)"