#!/bin/sh # DiviDen — put the divi CLI on this machine, in one line. # # curl -fsSL https://dividen.ai/install | sh # install, then sign in # curl -fsSL https://dividen.ai/install | sh -s -- --machine # install, then attach this computer # curl -fsSL https://dividen.ai/pair | sh -s -- # install, then answer a channel # # Installs for the current user under ~/.dividen. Nothing here needs root. set -eu HUB="https://dividen.ai" BUNDLE="$HUB/pair/cli.mjs" DAEMON_URL="$HUB/pair/vault-agent.js" DAEMON_MANIFEST_URL="$HUB/pair/vault-agent.json" DEST="${DIVI_HOME:-$HOME/.dividen}/bin" BIN="$DEST/divi" # THE DAEMON GOES ONE LEVEL UP FROM bin/, which is exactly where the CLI's own # resolver looks (`/../vault-agent`). The location is not a # preference: put it anywhere else and `machine serve` cannot find it without # being told, which is the failure this whole change exists to end. AGENT_DIR="${DIVI_HOME:-$HOME/.dividen}/vault-agent" # What to do once the CLI is on disk: login | machine | pair | none. MODE=login CODE="" say() { printf '%s\n' "$*" >&2; } die() { say "✗ $*"; exit 1; } usage() { say "divi installer" say " (no arguments) install, then sign in with \`divi login\`" say " --machine install, then attach this computer to your Vault" say " install, then answer the channel that minted " say " --install-only install and stop — for CI and scripted agents" } # ── What was pasted ────────────────────────────────────────────────────────── # A BARE POSITIONAL IS A CHANNEL CODE AND MUST STAY ONE. That line is already # printed in channels; re-reading it as anything else would break paste-and-go # for every one of them. while [ $# -gt 0 ]; do case "$1" in --machine=*) MODE=machine; CODE="${1#--machine=}" ;; --machine) MODE=machine; CODE="${2:-}"; if [ $# -ge 2 ]; then shift; fi ;; --pair=*) MODE=pair; CODE="${1#--pair=}" ;; --pair) MODE=pair; CODE="${2:-}"; if [ $# -ge 2 ]; then shift; fi ;; --install-only|--no-run) MODE=none ;; --login) MODE=login ;; -h|--help) usage; exit 0 ;; -*) usage; die "unknown option: $1" ;; *) MODE=pair; CODE="$1" ;; esac shift done if [ "$MODE" = machine ] && [ -z "$CODE" ]; then die "--machine needs the code from Settings → Shared Divi. Copy the WHOLE line." fi if [ "$MODE" = pair ] && [ -z "$CODE" ]; then die "no pairing code. Copy the WHOLE line from the channel — the code is the last part of it." fi command -v node >/dev/null 2>&1 || die "node 18+ is required and was not found. Install it from https://nodejs.org, then paste the line again." NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" [ "$NODE_MAJOR" -ge 18 ] 2>/dev/null || die "node 18+ is required (found $(node -v 2>/dev/null || echo none))." fetch() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1" else die "neither curl nor wget is available on this machine." fi } mkdir -p "$DEST" TMP="$(mktemp "${TMPDIR:-/tmp}/divi.XXXXXX")" || die "could not create a temporary file." trap 'rm -f "$TMP"' EXIT INT TERM say "· downloading the divi CLI…" fetch "$BUNDLE" "$TMP" || die "could not download the CLI from $BUNDLE." # A HALF-WRITTEN DOWNLOAD MUST NEVER LAND ON THE PATH. A proxy error page is a # few hundred bytes and would otherwise be installed as `divi` and fail later, # somewhere far from here. SIZE="$(wc -c < "$TMP" | tr -d ' ')" [ "$SIZE" -ge 300000 ] 2>/dev/null || die "that download looks truncated ($SIZE bytes) — check your connection and paste the line again." head -n 1 "$TMP" | grep -q '^#!' || die "that did not look like the CLI. Nothing was installed." chmod +x "$TMP" mv -f "$TMP" "$BIN" trap - EXIT INT TERM say "✓ installed $BIN" # PROVE IT RUNS before handing the terminal over. A home directory mounted noexec # — or a node that cannot load the bundle — otherwise surfaces as a bare # "exec: Permission denied" from the shell, on the line AFTER a success message, # which reads like the paste was wrong rather than the filesystem. # …and SAY WHICH VERSION, FROM WHICH CHANNEL, off the SAME invocation. The CLI # ships two ways — this platform bundle and npm — and BOTH move at a RELEASE # CUT, never at a deploy; they can still disagree; the founder # installed a platform 0.1.38 while npm served 0.1.37 with nothing anywhere # saying the channels had diverged. One `--version` call is both the exec # proof and the provenance statement, so the two can never disagree. # ── THE DAEMON, the other half of a machine ────────────────────────────────── # A CLI WITHOUT A DAEMON CANNOT SERVE. Fetched into $AGENT_DIR — the one place # `divi machine serve` looks without being told — because before this the # installer laid down only the CLI and `machine serve --install` then wrote a # unit that could not start, which the service manager restarted every few # seconds forever (measured on the founder's droplet, 2026-09-03). # # ⚠ LOUD, BUT NOT FATAL. An earlier draft died here. That was wrong: a CLI with # no daemon still logs in, pairs and adopts — it only cannot SERVE — so aborting # would take away the working half to punish the missing one, and would make an # older deployment that has no daemon route hand back nothing at all. The unit # install is where this is really caught: `machine serve --install` refuses when # no daemon is present, and now verifies the unit is RUNNING seconds later # rather than that a file was written. say "· downloading the vault-agent daemon…" AGENT_OK=no if mkdir -p "$AGENT_DIR/dist" 2>/dev/null; then TMPD="$(mktemp "${TMPDIR:-/tmp}/divi-agent.XXXXXX")" || TMPD="" TMPM="$(mktemp "${TMPDIR:-/tmp}/divi-agent-json.XXXXXX")" || TMPM="" if [ -n "$TMPD" ] && [ -n "$TMPM" ] && fetch "$DAEMON_URL" "$TMPD" && fetch "$DAEMON_MANIFEST_URL" "$TMPM"; then DSIZE="$(wc -c < "$TMPD" | tr -d ' ')" # The manifest declares "type": "module"; without it the daemon entry does # not load at all. Checking it is how a proxy error page is caught here # rather than at first boot. if [ "$DSIZE" -ge 50000 ] 2>/dev/null && grep -q '"type": *"module"' "$TMPM"; then mv -f "$TMPD" "$AGENT_DIR/dist/index.js" mv -f "$TMPM" "$AGENT_DIR/package.json" AGENT_OK=yes fi fi rm -f "$TMPD" "$TMPM" 2>/dev/null || true fi if [ "$AGENT_OK" = yes ]; then say "✓ installed $AGENT_DIR" else # NAME WHAT IS MISSING AND WHAT IT COSTS. Silence here is how the droplet # ended up with a unit nobody could start and no clue why. say "⚠ the vault-agent daemon could not be downloaded from $DAEMON_URL" say " The CLI is installed and you can sign in and pair. This machine cannot" say " SERVE until the daemon is present: `divi machine serve --install` will" say " refuse and say so rather than install a unit that cannot start." fi VER="$("$BIN" --version 2>/dev/null)" || die "installed $BIN but could not run it. If that directory is on a noexec filesystem, set DIVI_HOME to one that allows execution and paste the line again." say "· divi $VER — installed from this platform's own bundle. Both channels move at a RELEASE, not at a deploy: npm's @denominator/dividen may serve a different version, and neither is refreshed by deploying the app." # ── Make `divi` survive this terminal ──────────────────────────────────────── # ADVICE IS NOT INSTALLATION. The old script only PRINTED an export line, so the # next window had no `divi` and the person who followed the one-liner exactly # was the person most likely to conclude it had not worked. PATH="$DEST:$PATH" export PATH persist_path() { PROFILE="" case "${SHELL:-}" in */zsh) PROFILE="$HOME/.zshrc" ;; */bash) if [ -f "$HOME/.bashrc" ]; then PROFILE="$HOME/.bashrc"; else PROFILE="$HOME/.bash_profile"; fi ;; # fish's syntax is not sh's, and writing an `export` line into config.fish # would leave a broken profile behind. Say so instead of guessing. */fish) say "· fish detected — add it yourself: fish_add_path $DEST" ; return 0 ;; *) PROFILE="$HOME/.profile" ;; esac # Idempotent: the marker, not the export line, is what is matched — so a person # who reruns the installer (or pastes both one-liners) gets ONE entry. if [ -f "$PROFILE" ] && grep -q 'added by the DiviDen installer' "$PROFILE" 2>/dev/null; then return 0 fi if printf '\n# added by the DiviDen installer\nexport PATH="%s:$PATH"\n' "$DEST" >> "$PROFILE" 2>/dev/null; then say "· added $DEST to your PATH in $PROFILE (open a new terminal, or: export PATH=\"$DEST:\$PATH\")" else say "· could not write $PROFILE — add this yourself: export PATH=\"$DEST:\$PATH\"" fi } persist_path if [ "$MODE" = none ]; then say "· done. \`divi login\` signs this machine in; \`divi --help\` lists everything else." exit 0 fi # Hand the terminal to the CLI, with a REAL STDIN. This script arrives through a # pipe (`curl … | sh`), so the CLI would inherit an exhausted one and every # question it asks would read as an immediate EOF — the failure looks like the # CLI refusing to talk. /dev/tty is the terminal the person is actually sitting # at; when there is none (CI, a scripted agent) the pipe is correct and kept. # # ⚠ THE TEST IS AN OPEN IN A SUBSHELL, and both halves of that are load-bearing. # Measured, twice: # 1. `[ -r /dev/tty ]` is TRUE with no controlling terminal — the device node # exists and is mode-readable; only the OPEN fails. So the readable test # answers a different question than the one being asked, and the installer # died at `exec … < /dev/tty` AFTER it had already installed. # 2. `{ : < /dev/tty; } 2>/dev/null` does not survive it either: in dash a # failed redirection on a grouped command aborts the SCRIPT, and the # `2>/dev/null` then swallows the reason — an exit 1 with no output at all. # A subshell contains the failure, so the else branch can be reached. run() { if ( : < /dev/tty ) 2>/dev/null; then exec "$BIN" "$@" --hub "$HUB" < /dev/tty else exec "$BIN" "$@" --hub "$HUB" fi } case "$MODE" in # Attaching a computer to the Vault returns the prompt when it is done. machine) run machine pair "$CODE" ;; # `divi pair` redeems and then STAYS attached — this window is the one # answering the channel, so exec rather than call: the process the operator # interrupts is the one they started. pair) run pair "$CODE" ;; *) run login ;; esac