| 1 | #!/usr/bin/env bash |
| 2 | # Installs the Diffinery CLI on Linux and macOS: |
| 3 | # |
| 4 | # curl -fsSL https://diffinery.com/install | bash |
| 5 | # |
| 6 | # The CLI goes in ~/.diffinery (set DIFFINERY_INSTALL to change that), and ~/.diffinery/bin is |
| 7 | # added to PATH in your shell's startup file (DIFFINERY_NO_MODIFY_PATH=1 skips that). It runs on |
| 8 | # Node.js 22 or later: the one on your PATH when it's new enough, otherwise a private copy of the |
| 9 | # Node.js LTS from nodejs.org, checked against its published SHA-256, in ~/.diffinery/node. |
| 10 | # Run it again, or `diffinery upgrade`, to update. Served by Diffinery from apps/cli/install/. |
| 11 | set -euo pipefail |
| 12 | |
| 13 | server=${DIFFINERY_URL:-__DIFFINERY_URL__} |
| 14 | case $server in __*) server=https://diffinery.com ;; esac |
| 15 | server=${server%/} |
| 16 | root=${DIFFINERY_INSTALL:-$HOME/.diffinery} |
| 17 | bin=$root/bin |
| 18 | node_line=24 |
| 19 | |
| 20 | if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then |
| 21 | bold=$'\033[1m' dim=$'\033[2m' green=$'\033[32m' red=$'\033[31m' reset=$'\033[0m' |
| 22 | else |
| 23 | bold='' dim='' green='' red='' reset='' |
| 24 | fi |
| 25 | say() { printf '%s\n' "$*" >&2; } |
| 26 | die() { |
| 27 | say "${red}error${reset}: $*" |
| 28 | exit 1 |
| 29 | } |
| 30 | tilde() { case $1 in "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;; *) printf '%s' "$1" ;; esac; } |
| 31 | |
| 32 | command -v curl >/dev/null || die "curl is required" |
| 33 | case $(uname -s) in |
| 34 | Linux) os=linux ;; |
| 35 | Darwin) os=darwin ;; |
| 36 | MINGW* | MSYS* | CYGWIN*) die "on Windows, run this in PowerShell instead: irm $server/install.ps1 | iex" ;; |
| 37 | *) die "$(uname -s) isn't supported by this script; install Node.js 22 or later and download $server/-/cli/diffinery.mjs" ;; |
| 38 | esac |
| 39 | case $(uname -m) in |
| 40 | x86_64 | amd64) arch=x64 ;; |
| 41 | arm64 | aarch64) arch=arm64 ;; |
| 42 | *) arch='' ;; |
| 43 | esac |
| 44 | # A shell under Rosetta on Apple silicon still gets the arm64 Node.js. |
| 45 | if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64; fi |
| 46 | |
| 47 | tmp=$(mktemp -d) |
| 48 | trap 'rm -rf "$tmp"' EXIT |
| 49 | |
| 50 | node_ok() { "$1" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' >/dev/null 2>&1; } |
| 51 | |
| 52 | # 1. Node.js for the CLI to run on. |
| 53 | private=$root/node/bin/node |
| 54 | if [ -x "$private" ] && node_ok "$private"; then |
| 55 | node=$private |
| 56 | elif command -v node >/dev/null && node_ok node; then |
| 57 | node='' |
| 58 | else |
| 59 | [ -n "$arch" ] || die "the CLI needs Node.js 22 or later, and nodejs.org has no build for $(uname -m); install Node.js and run this again" |
| 60 | if [ "$os" = linux ] && ldd --version 2>&1 | grep -qi musl; then die "the CLI needs Node.js 22 or later; on Alpine and other musl systems install it with your package manager (apk add nodejs) and run this again"; fi |
| 61 | say "Installing Node.js $node_line LTS for the CLI, from nodejs.org…" |
| 62 | dist=https://nodejs.org/dist/latest-v$node_line.x |
| 63 | sums=$(curl -fsSL "$dist/SHASUMS256.txt") || die "couldn't reach nodejs.org" |
| 64 | line=$(printf '%s\n' "$sums" | grep -E " node-v[0-9.]+-$os-$arch\.tar\.gz$" | head -n 1 || true) |
| 65 | [ -n "$line" ] || die "nodejs.org has no Node.js build for $os-$arch" |
| 66 | want=${line%% *} |
| 67 | file=${line##* } |
| 68 | curl -fsSL "$dist/$file" -o "$tmp/node.tar.gz" || die "couldn't download $file" |
| 69 | if command -v sha256sum >/dev/null; then got=$(sha256sum "$tmp/node.tar.gz"); else got=$(shasum -a 256 "$tmp/node.tar.gz"); fi |
| 70 | [ "${got%% *}" = "$want" ] || die "$file doesn't match its published SHA-256, so it wasn't installed" |
| 71 | mkdir -p "$tmp/node" "$root" |
| 72 | tar -xzf "$tmp/node.tar.gz" -C "$tmp/node" --strip-components 1 |
| 73 | rm -rf "$root/node" |
| 74 | mv "$tmp/node" "$root/node" |
| 75 | node=$private |
| 76 | fi |
| 77 | |
| 78 | # 2. The CLI itself: one JavaScript file. |
| 79 | curl -fsSL "$server/-/cli/diffinery.mjs" -o "$tmp/diffinery.mjs" || die "couldn't download the CLI from $server" |
| 80 | head -n 1 "$tmp/diffinery.mjs" | grep -q '^#!/usr/bin/env node' || die "$server/-/cli/diffinery.mjs isn't the CLI" |
| 81 | mkdir -p "$root/lib" "$bin" |
| 82 | chmod 755 "$tmp/diffinery.mjs" |
| 83 | mv -f "$tmp/diffinery.mjs" "$root/lib/diffinery.mjs" |
| 84 | |
| 85 | # 3. The diffinery command: the private Node.js when there is one, else the node on PATH. |
| 86 | q=$(printf '%q' "$root") |
| 87 | cat >"$tmp/diffinery" <<EOF |
| 88 | #!/bin/sh |
| 89 | # The Diffinery CLI, installed from $server/install. Update it with: diffinery upgrade |
| 90 | root=$q |
| 91 | node=\$root/node/bin/node |
| 92 | [ -x "\$node" ] || node=node |
| 93 | exec "\$node" "\$root/lib/diffinery.mjs" "\$@" |
| 94 | EOF |
| 95 | chmod 755 "$tmp/diffinery" |
| 96 | mv -f "$tmp/diffinery" "$bin/diffinery" |
| 97 | version=$("$bin/diffinery" --version) || die "the CLI was installed in $(tilde "$root") but doesn't start; run $bin/diffinery --version to see why" |
| 98 | |
| 99 | # 4. PATH. |
| 100 | note='' |
| 101 | case ":$PATH:" in |
| 102 | *":$bin:"*) ;; |
| 103 | *) |
| 104 | if [ -z "${DIFFINERY_NO_MODIFY_PATH:-}" ]; then |
| 105 | line="export PATH=\"$bin:\$PATH\"" |
| 106 | case $(basename "${SHELL:-sh}") in |
| 107 | zsh) rc=${ZDOTDIR:-$HOME}/.zshrc ;; |
| 108 | bash) if [ "$os" = darwin ]; then rc=$HOME/.bash_profile; else rc=$HOME/.bashrc; fi ;; |
| 109 | fish) |
| 110 | rc=$HOME/.config/fish/conf.d/diffinery.fish |
| 111 | line="fish_add_path \"$bin\"" |
| 112 | ;; |
| 113 | *) rc=$HOME/.profile ;; |
| 114 | esac |
| 115 | if grep -qsF "$bin" "$rc"; then |
| 116 | did="$(tilde "$rc") already adds $(tilde "$bin") to PATH." |
| 117 | else |
| 118 | mkdir -p "$(dirname "$rc")" |
| 119 | printf '\n# Diffinery CLI\n%s\n' "$line" >>"$rc" |
| 120 | did="Added $(tilde "$bin") to PATH in $(tilde "$rc")." |
| 121 | fi |
| 122 | note="$did Open a new terminal, or run: ${bold}export PATH=\"$bin:\$PATH\"${reset}" |
| 123 | else |
| 124 | note="Add $(tilde "$bin") to your PATH to run diffinery." |
| 125 | fi |
| 126 | ;; |
| 127 | esac |
| 128 | |
| 129 | say "${green}✓${reset} ${version} installed to $(tilde "$bin/diffinery")${node:+ ${dim}(with its own Node.js)${reset}}" |
| 130 | [ -z "$note" ] || say "$note" |
| 131 | other=$(command -v diffinery 2>/dev/null || true) |
| 132 | if [ -n "$other" ] && [ "$other" != "$bin/diffinery" ]; then say "${dim}Another diffinery at $(tilde "$other") comes first on your PATH; remove it to use this one.${reset}"; fi |
| 133 | login="diffinery login" |
| 134 | [ "$server" = https://diffinery.com ] || login="diffinery login --url $server" |
| 135 | say "" |
| 136 | say "Next, sign in through your browser:" |
| 137 | say " ${bold}$login${reset}" |