#!/bin/sh # Install the `getpay` CLI — GetPay accounting from the command line. # # curl -fsSL https://getpay.cc/cli/install.sh | sh # # What it does (nothing else; no sudo, nothing outside your home directory): # 1. checks node >= 20 is on PATH (the CLI is ONE node ESM file, zero deps); # 2. downloads https://getpay.cc/cli/getpay.mjs and getpay.mjs.sha256 into a # private temp dir and verifies the sha256 (a truncated or corrupted # download is never installed). Pin a hash you obtained out of band with # GETPAY_CLI_SHA256= for protection beyond a same-origin checksum; # 3. installs the bundle to ~/.local/share/getpay/getpay.mjs and a small # node-shebang launcher to ~/.local/bin/getpay; # 4. prints a PATH hint when ~/.local/bin is not on your PATH. # # Options (environment variables): # GETPAY_INSTALL_BIN_DIR launcher directory (default ~/.local/bin) # GETPAY_INSTALL_LIB_DIR bundle directory (default ~/.local/share/getpay) # GETPAY_CLI_SHA256 expected sha256 of getpay.mjs (optional pin) # GETPAY_CLI_BASE_URL download base (default https://getpay.cc/cli; # https only, http only for 127.0.0.1 — testing) # # Uninstall: rm ~/.local/bin/getpay && rm -r ~/.local/share/getpay # (sign out first with `getpay logout`; credentials live in ~/.config/getpay). set -eu BASE_URL="${GETPAY_CLI_BASE_URL:-https://getpay.cc/cli}" BIN_DIR="${GETPAY_INSTALL_BIN_DIR:-$HOME/.local/bin}" LIB_DIR="${GETPAY_INSTALL_LIB_DIR:-$HOME/.local/share/getpay}" say() { printf '%s\n' "$*" >&2; } die() { say "getpay install: $*"; exit 1; } case "$BASE_URL" in https://*) ;; http://127.0.0.1:*|http://127.0.0.1/*) ;; *) die "GETPAY_CLI_BASE_URL must be an https:// URL (got: $BASE_URL)" ;; esac # --- 1. node >= 20 ----------------------------------------------------------- command -v node >/dev/null 2>&1 || die "node was not found on PATH. Install Node.js 20 or newer (https://nodejs.org), then rerun." NODE_BIN="$(command -v node)" NODE_MAJOR="$("$NODE_BIN" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)" case "$NODE_MAJOR" in ''|*[!0-9]*) NODE_MAJOR=0 ;; esac [ "$NODE_MAJOR" -ge 20 ] || die "getpay needs node >= 20 (found $("$NODE_BIN" --version 2>/dev/null || echo unknown)). Upgrade Node.js, then rerun." # --- 2. download + verify ---------------------------------------------------- if command -v curl >/dev/null 2>&1; then fetch() { curl -fsSL --proto '=https,http' --proto-redir '=https' --retry 2 -o "$2" "$1"; } elif command -v wget >/dev/null 2>&1; then fetch() { wget -q -O "$2" "$1"; } else die "need curl or wget to download the CLI." fi umask 077 TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t getpay)" trap 'rm -rf "$TMP_DIR"' EXIT trap 'exit 130' INT TERM say "Downloading getpay from $BASE_URL ..." fetch "$BASE_URL/getpay.mjs" "$TMP_DIR/getpay.mjs" || die "download of getpay.mjs failed." fetch "$BASE_URL/getpay.mjs.sha256" "$TMP_DIR/getpay.mjs.sha256" || die "download of getpay.mjs.sha256 failed." # sha256 via node (always present here; no dependency on sha256sum/shasum). ACTUAL="$("$NODE_BIN" -e ' const fs = require("fs"); const c = require("crypto"); process.stdout.write(c.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex")); ' "$TMP_DIR/getpay.mjs")" || die "could not hash the download." PUBLISHED="$(sed -n '1s/^\([0-9a-f]\{64\}\) getpay\.mjs$/\1/p' "$TMP_DIR/getpay.mjs.sha256")" [ -n "$PUBLISHED" ] || die "getpay.mjs.sha256 is not a sha256 line; refusing to install." [ "$ACTUAL" = "$PUBLISHED" ] || die "sha256 mismatch (expected $PUBLISHED, got $ACTUAL); the download is corrupted or tampered with. Nothing was installed." if [ -n "${GETPAY_CLI_SHA256:-}" ]; then PIN="$(printf '%s' "$GETPAY_CLI_SHA256" | tr 'A-F' 'a-f')" [ "$ACTUAL" = "$PIN" ] || die "sha256 $ACTUAL does not match GETPAY_CLI_SHA256 ($PIN). Nothing was installed." fi # The bundle must run on this node before it replaces anything. "$NODE_BIN" "$TMP_DIR/getpay.mjs" version >/dev/null 2>&1 || die "the downloaded CLI does not run on this node ($("$NODE_BIN" --version)). Nothing was installed." # --- 3. install ---------------------------------------------------------------- umask 022 mkdir -p "$BIN_DIR" "$LIB_DIR" || die "could not create $BIN_DIR / $LIB_DIR." # Bundle: atomic replace (a running getpay keeps its old file). cp "$TMP_DIR/getpay.mjs" "$LIB_DIR/.getpay.mjs.new" chmod 0644 "$LIB_DIR/.getpay.mjs.new" mv -f "$LIB_DIR/.getpay.mjs.new" "$LIB_DIR/getpay.mjs" # Launcher: a node-shebang file that dynamic-imports the ESM bundle by its # file:// URL. It uses nothing but import() and process, so it runs whether # node loads it as CommonJS or as ESM (an extensionless file is ESM inside a # "type":"module" package scope, e.g. a ~/package.json from `npm init`). The # URL is computed here by node itself and embedded as a JSON string, so any # character in the path is quoted safely. BUNDLE_JSON="$("$NODE_BIN" -e 'process.stdout.write(JSON.stringify(require("url").pathToFileURL(require("path").resolve(process.argv[1])).href))' "$LIB_DIR/getpay.mjs")" { printf '%s\n' '#!/usr/bin/env node' printf '%s\n' '// getpay launcher — installed by https://getpay.cc/cli/install.sh (do not edit).' printf '%s\n' "const bundle = $BUNDLE_JSON" printf '%s\n' 'import(bundle).catch((e) => {' printf '%s\n' ' process.stderr.write(`getpay: cannot load ${bundle}: ${e && e.message ? e.message : e}\nReinstall: curl -fsSL https://getpay.cc/cli/install.sh | sh\n`)' printf '%s\n' ' process.exitCode = 1' printf '%s\n' '})' } > "$BIN_DIR/.getpay.new" chmod 0755 "$BIN_DIR/.getpay.new" mv -f "$BIN_DIR/.getpay.new" "$BIN_DIR/getpay" VERSION="$("$BIN_DIR/getpay" version --json 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')" || VERSION="" [ -n "$VERSION" ] || die "the installed launcher $BIN_DIR/getpay does not run." say "Installed getpay $VERSION to $BIN_DIR/getpay (sha256 $ACTUAL)." # --- 4. PATH hint -------------------------------------------------------------- case ":$PATH:" in *":$BIN_DIR:"*) say "Next: getpay login (help: getpay help)" ;; *) say "" say "$BIN_DIR is not on your PATH. Add it, e.g.:" say " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.profile (or ~/.zshrc / ~/.bashrc)" say "then open a new terminal and run: getpay login" ;; esac