#!/bin/sh # gabeforge installer. # # curl -fsSL https://get.gabeforge.com | sh # # Signs you in by email, downloads the release for this OS, checks its sha256, and # installs it for your user only: no sudo, nothing outside your home directory. # # ~/.local/share/gabeforge/ the release (binary, pi's files, harness, companion) # ~/.local/bin/gabeforge a link to the binary # ~/.gabeforge/ settings, sessions, and your sign-in token (0600) # # The token never appears in a URL, a command line or your shell history: it goes # from the sign-in reply straight into ~/.gabeforge/token, and curl reads the # Authorization header from a private config file. set -eu ACCOUNTS="${GABEFORGE_ACCOUNTS_URL:-https://accounts.gabeforge.com}" PREFIX="${GABEFORGE_PREFIX:-$HOME/.local/share/gabeforge}" BIN_DIR="${GABEFORGE_BIN_DIR:-$HOME/.local/bin}" CONF="${GABEFORGE_HOME:-$HOME/.gabeforge}" say() { printf '%s\n' "$*"; } die() { printf 'gabeforge install: %s\n' "$*" >&2; exit 1; } for tool in curl tar uname sed mktemp; do command -v "$tool" >/dev/null 2>&1 || die "needs $tool" done if command -v sha256sum >/dev/null 2>&1; then SHA="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256" else die "needs sha256sum or shasum"; fi # Piped into sh, stdin is this script: questions go to the terminal instead. [ -r /dev/tty ] || die "run it in a terminal (it asks for your email)" case "$(uname -s)" in Linux) os=linux ;; Darwin) os=darwin ;; *) die "unsupported OS $(uname -s); Linux and macOS for now" ;; esac case "$(uname -m)" in x86_64|amd64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; *) die "unsupported CPU $(uname -m)" ;; esac if [ "$os" = linux ] && ldd --version 2>&1 | grep -qi musl; then die "musl-based Linux (Alpine) is not supported yet" fi if [ "$os" = darwin ] && [ -z "${GABEFORGE_PREFIX:-}" ]; then PREFIX="$HOME/Library/Application Support/gabeforge" fi TARGET="$os-$arch" umask 077 TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT INT TERM mkdir -p "$CONF" # json_get KEY FILE: the string value of a top-level key in a one-line JSON reply. json_get() { sed -n "s/.*\"$1\":\"\\([^\"]*\\)\".*/\\1/p" "$2"; } json_num() { sed -n "s/.*\"$1\":\\([0-9]*\\).*/\\1/p" "$2"; } auth_conf() { # curl reads the header from here, so the token is never in curl's argv. printf 'header = "Authorization: Bearer %s"\n' "$(cat "$CONF/token")" > "$TMP/auth.conf" } signed_in() { [ -s "$CONF/token" ] || return 1 auth_conf curl -fsS -K "$TMP/auth.conf" "$ACCOUNTS/v1/me" -o "$TMP/me.json" 2>/dev/null } sign_in() { printf 'Email you paid with on Ko-fi (or will sign in with): ' > /dev/tty read -r email < /dev/tty [ -n "$email" ] || die "no email given" printf '{"email":"%s","client":"cli","label":"%s"}' \ "$(printf '%s' "$email" | sed 's/["\\]//g')" "$(uname -n | sed 's/["\\]//g')" > "$TMP/start.req" curl -fsS -H 'Content-Type: application/json' --data @"$TMP/start.req" "$ACCOUNTS/device/start" -o "$TMP/start.json" \ || die "could not start sign-in (check the address, or try again in a few minutes)" code="$(json_get user_code "$TMP/start.json")" interval="$(json_num interval "$TMP/start.json")"; interval="${interval:-3}" expires="$(json_num expires_in "$TMP/start.json")"; expires="${expires:-900}" # The device code is a secret too: it goes in a request body read from a file. printf '{"device_code":"%s"}' "$(json_get device_code "$TMP/start.json")" > "$TMP/poll.req" say "" say "Sent a sign-in link to $email." say "Open it and check it shows this code: $code" say "" printf 'Waiting for you to confirm' waited=0 while [ "$waited" -lt "$expires" ]; do sleep "$interval"; waited=$((waited + interval)) curl -fsS -H 'Content-Type: application/json' --data @"$TMP/poll.req" "$ACCOUNTS/device/poll" -o "$TMP/poll.json" || continue case "$(json_get status "$TMP/poll.json")" in ok) json_get token "$TMP/poll.json" > "$CONF/token.new" chmod 600 "$CONF/token.new" && mv "$CONF/token.new" "$CONF/token" rm -f "$TMP/poll.json" say "" return 0 ;; denied) say ""; die "sign-in was denied on the link page" ;; expired) break ;; esac printf '.' done say "" die "the sign-in link expired; run the installer again" } if signed_in; then say "Signed in as $(json_get email "$TMP/me.json")." else sign_in auth_conf fi say "Fetching the $TARGET release…" status="$(curl -sS -K "$TMP/auth.conf" -H 'Content-Type: application/json' --data "{\"target\":\"$TARGET\"}" \ "$ACCOUNTS/v1/download" -o "$TMP/release.json" -w '%{http_code}')" || die "cannot reach $ACCOUNTS" [ "$status" = 200 ] || die "$(json_get error "$TMP/release.json")" url="$(json_get url "$TMP/release.json")" want="$(json_get sha256 "$TMP/release.json")" version="$(json_get version "$TMP/release.json")" [ -n "$url" ] && [ -n "$want" ] || die "no release for $TARGET: $(json_get error "$TMP/release.json")" curl -fL --progress-bar "$url" -o "$TMP/gabeforge.tar.gz" || die "download failed" got="$($SHA "$TMP/gabeforge.tar.gz" | cut -d' ' -f1)" [ "$got" = "$want" ] || die "checksum mismatch: got $got, expected $want" # Unpack beside the install dir, then swap whole trees. parent="$(dirname "$PREFIX")" mkdir -p "$parent" stage="$(mktemp -d "$parent/.gabeforge-new-XXXXXX")" tar -xzf "$TMP/gabeforge.tar.gz" -C "$stage" [ -x "$stage/gabeforge/gabeforge" ] || die "release is missing its binary" if [ -e "$PREFIX" ]; then mv "$PREFIX" "$stage/old" fi mv "$stage/gabeforge" "$PREFIX" rm -rf "$stage" chmod 755 "$PREFIX" mkdir -p "$BIN_DIR" ln -sf "$PREFIX/gabeforge" "$BIN_DIR/gabeforge" say "Installed gabeforge $version in $PREFIX." # First run: which model it talks to, and whether the phone can start sessions here. "$PREFIX/gabeforge" setup < /dev/tty || true if [ "$os" = linux ] && command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then printf '\nLet your phone start sessions on this machine (runs in the background)? [Y/n] ' > /dev/tty read -r ans < /dev/tty || ans=n case "$ans" in n|N|no|No) ;; *) "$PREFIX/gabeforge" host --install || true ;; esac fi case ":$PATH:" in *":$BIN_DIR:"*) say ""; say "Run: gabeforge" ;; *) say ""; say "Add $BIN_DIR to your PATH, then run: gabeforge" ;; esac