#!/bin/sh # Ping installer for macOS and Linux. Members run the one line shown on their install page: # curl -fsSL https://pondletter.com/ping-install.sh | sh -s -- # Read this first if you like; it is short. What it does, and nothing else: # 1. installs uv (a Python package manager) into ~/.local/bin if you do not have it; # 2. downloads Ping and its checksum list with your token, checks the SHA-256, and installs it with its own Python # into uv's tool folder (nothing touches your system Python); # 3. fetches your member key (tied to your membership email) into Ping's folder, where Ping picks it up once; # 4. adds a "Ping" launcher (Applications on macOS, the app menu on Linux) and opens Ping. # No sudo. Everything lives in your home folder. # Uninstall: uv tool uninstall ping; rm -rf ~/Applications/Ping.app ~/.ping ~/.local/share/applications/ping.desktop # Your token is personal and expires in 30 minutes; the member key it fetches is tied to your account. set -eu TOKEN="${1:-${PING_TOKEN:-}}" BASE="${PING_BASE:-https://pondletter.com}" WHEEL_FILE="${PING_WHEEL:-}" # developers: a local .whl to install instead of downloading DATA_DIR="${PING_DATA_DIR:-$HOME/.ping}" WHEEL_NAME="ping-latest-py3-none-any.whl" say() { printf '\033[1m%s\033[0m\n' "$*"; } die() { echo "Ping installer: $*" >&2; exit 1; } OS="$(uname -s)" case "$OS" in Darwin|Linux) ;; *) die "this installer is for macOS and Linux. On Windows use ping-install.ps1." ;; esac command -v curl >/dev/null 2>&1 || die "curl is needed and was not found." if [ -z "$WHEEL_FILE" ] && [ -z "$TOKEN" ]; then die "the download token is missing. Copy the whole command from your install page: $BASE/install-ping/" fi TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT INT TERM fetch() { # fetch code="$(curl -sS -w '%{http_code}' -o "$2" "$BASE/ping-dl/$TOKEN/$1")" || die "could not reach $BASE." case "$code" in 200) ;; 410) die "this install link has expired (they last 30 minutes). Open $BASE/install-ping/ for a fresh one." ;; *) die "download of $1 failed (HTTP $code). Open $BASE/install-ping/ and copy the command again." ;; esac } sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 else die "neither shasum nor sha256sum is available to verify the download."; fi } # 1. uv (brings its own Python; no system Python needed) if ! command -v uv >/dev/null 2>&1 && [ ! -x "$HOME/.local/bin/uv" ]; then say "Installing uv (Python manager) into ~/.local/bin" curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 INSTALLER_NO_MODIFY_PATH=1 sh >/dev/null fi UV="$(command -v uv 2>/dev/null || echo "$HOME/.local/bin/uv")" [ -x "$UV" ] || die "uv did not install. See https://docs.astral.sh/uv/ and run this again." # 2. Ping, verified against the published checksum if [ -z "$WHEEL_FILE" ]; then say "Downloading Ping" fetch "SHA256SUMS" "$TMP/SHA256SUMS" # The list names the versioned file first; uv needs the version in the filename, so save it under that name. REAL_NAME="$(grep ' ping-[0-9][^ ]*\.whl$' "$TMP/SHA256SUMS" | head -1 | awk '{print $2}')" [ -n "$REAL_NAME" ] || die "the checksum list did not name a versioned wheel." want="$(grep " $WHEEL_NAME\$" "$TMP/SHA256SUMS" | head -1 | cut -d' ' -f1)" [ -n "$want" ] || die "the checksum list did not mention $WHEEL_NAME." fetch "$WHEEL_NAME" "$TMP/$REAL_NAME" have="$(sha256 "$TMP/$REAL_NAME")" [ "$want" = "$have" ] || die "checksum mismatch for $REAL_NAME (expected $want, got $have). Nothing was installed." say "Checksum verified: $have ($REAL_NAME)" WHEEL_FILE="$TMP/$REAL_NAME" fi say "Installing Ping with its own Python 3.12" "$UV" tool install --quiet --python 3.12 --force --reinstall "ping[app] @ $WHEEL_FILE" BIN="$("$UV" tool dir --bin 2>/dev/null || echo "$HOME/.local/bin")" PING_APP="$BIN/ping-app" [ -x "$PING_APP" ] || die "install finished but the launcher was not found at $PING_APP" # 3. Your member key, handed to Ping once (it moves it into your keychain and deletes the file) if [ -n "$TOKEN" ]; then mkdir -p "$DATA_DIR"; chmod 700 "$DATA_DIR" 2>/dev/null || true umask 077 fetch "member-key" "$DATA_DIR/member-key.bootstrap" umask 022 say "Member key fetched for this account" fi # 4. A launcher where you expect one if [ "$OS" = "Darwin" ]; then APP="$HOME/Applications/Ping.app" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" cat > "$APP/Contents/MacOS/Ping" < "$APP/Contents/Info.plist" <<'EOF' CFBundleNamePing CFBundleDisplayNamePing CFBundleIdentifiercom.pondletter.ping.launcher CFBundleExecutablePing CFBundleIconFileicon CFBundlePackageTypeAPPL CFBundleShortVersionString1.0 LSUIElement EOF [ -n "$TOKEN" ] && curl -fsSL "$BASE/ping-dl/$TOKEN/icon.icns" -o "$APP/Contents/Resources/icon.icns" 2>/dev/null || true say "Added Ping to $HOME/Applications" elif [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && command -v systemctl >/dev/null 2>&1; then # a headless box (a small server or a Raspberry Pi): a user service keeps Ping running; reach it over an SSH tunnel mkdir -p "$HOME/.config/systemd/user" cat > "$HOME/.config/systemd/user/ping.service" </dev/null || echo "To keep it running after you log out: loginctl enable-linger $USER" systemctl --user daemon-reload 2>/dev/null || true # enable, then restart: a rerun (update or repair) must swap the running copy for the one just installed if systemctl --user enable ping 2>/dev/null && systemctl --user restart ping 2>/dev/null; then sleep 2 systemctl --user is-active --quiet ping && say "Ping is running as a user service (systemctl --user status ping)" || say "Service enabled but not active yet; check: journalctl --user -u ping -n 30" else say "Service file written; start it with: systemctl --user enable --now ping" fi IP="$(curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}')" echo "Open it from your own computer: ssh -L 7420:127.0.0.1:7420 $USER@${IP:-$(hostname)} then http://127.0.0.1:7420" HEADLESS=1 else mkdir -p "$HOME/.local/share/applications" "$HOME/.local/share/icons" [ -n "$TOKEN" ] && curl -fsSL "$BASE/ping-dl/$TOKEN/icon-1024.png" -o "$HOME/.local/share/icons/ping.png" 2>/dev/null || true cat > "$HOME/.local/share/applications/ping.desktop" </dev/null 2>&1 & echo echo "Done. To run Ping later: open Ping from Applications, or run: $PING_APP" echo "To update: copy a fresh command from $BASE/install-ping/ and run it again. To uninstall: $UV tool uninstall ping" echo "Where the code lives (read it any time): $("$UV" tool dir 2>/dev/null || echo "$HOME/.local/share/uv/tools")/ping"