#!/bin/sh set -eu label="app.pocketty.daemon" bundle_id="app.pocketty.daemon" releases="${POCKETTY_RELEASES:-https://dl.pocketty.app}" bin_dir="$HOME/.local/bin" bin="$bin_dir/pocketty" app="${XDG_DATA_HOME:-$HOME/.local/share}/pocketty/pocketty.app" exe="$app/Contents/MacOS/pocketty" state="${XDG_STATE_HOME:-$HOME/.local/state}/pocketty" plist="$HOME/Library/LaunchAgents/$label.plist" log="$HOME/Library/Logs/pocketty.log" unit="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/pocketty.service" say() { printf '%s\n' "$*" } fail() { printf 'pocketty: %s\n' "$*" >&2 exit 1 } usage() { say "Usage: install.sh [--uninstall]" say "Installs the pocketty daemon to $bin and starts it as a service." } target() { arch=$(uname -m) case "$(uname -s)" in Darwin) if [ "$arch" = x86_64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then arch=arm64 fi case "$arch" in arm64) say aarch64-apple-darwin ;; x86_64) say x86_64-apple-darwin ;; *) fail "this Mac has a $arch CPU, which pocketty does not support" ;; esac ;; Linux) case "$arch" in aarch64 | arm64) say aarch64-unknown-linux-musl ;; x86_64 | amd64) say x86_64-unknown-linux-musl ;; *) fail "this computer has a $arch CPU, which pocketty does not support" ;; esac ;; *) fail "pocketty runs on macOS and Linux, not $(uname -s)" ;; esac } fetch() { curl -fsSL --proto-redir =https "$1" -o "$2" || fail "could not download $1" } digest() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 else fail "this computer needs sha256sum or shasum to check the download" fi } service_path() { extra="" if [ "$(uname -s)" = Darwin ]; then extra=":/opt/homebrew/bin:/usr/local/bin" fi printf '%s' "$bin_dir:$PATH$extra:/usr/bin:/bin:/usr/sbin:/sbin" | awk -v RS=: '/^\// && !seen[$0]++ { printf "%s%s", sep, $0; sep = ":" }' } xml() { printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g' } has_agent_host() { if command -v herdr >/dev/null 2>&1 || command -v tailscale >/dev/null 2>&1; then return 0 fi for path in "$bin_dir/herdr" /opt/homebrew/bin/herdr /usr/local/bin/herdr; do if [ -x "$path" ]; then return 0 fi done [ "$(uname -s)" = Darwin ] && [ -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ] } linked() { case "$(uname -s)" in Darwin) [ "$(readlink "$bin" 2>/dev/null)" = "$exe" ] ;; *) [ ! -L "$bin" ] ;; esac } place_app() { [ -d "$tmp/pocketty.app" ] || fail "the download has no pocketty.app" dir=$(dirname "$app") mkdir -p "$dir" rm -rf "$dir/.pocketty.app.new" "$dir/.pocketty.app.old" mv "$tmp/pocketty.app" "$dir/.pocketty.app.new" if [ -e "$app" ]; then mv "$app" "$dir/.pocketty.app.old" fi mv "$dir/.pocketty.app.new" "$app" rm -rf "$dir/.pocketty.app.old" ln -sfn "$exe" "$bin_dir/.pocketty.new" mv -f "$bin_dir/.pocketty.new" "$bin" say "Installed pocketty $version to $app" } place_binary() { cp "$tmp/pocketty" "$bin_dir/.pocketty.new" mv -f "$bin_dir/.pocketty.new" "$bin" say "Installed pocketty $version to $bin" } download() { latest="$tmp/latest" fetch "$releases/latest" "$latest" version=$(tr -d ' \r\n' <"$latest") case "$version" in "" | *[!0-9A-Za-z.+-]*) fail "the release server sent a bad version: $version" ;; esac current=$("$bin" --version 2>/dev/null | cut -d' ' -f2 || true) if [ "$current" = "$version" ] && linked; then say "pocketty $version is already installed." return fi file="pocketty-$platform.tar.gz" say "Downloading pocketty $version for $platform..." fetch "$releases/$version/$file" "$tmp/$file" fetch "$releases/$version/SHA256SUMS" "$tmp/SHA256SUMS" want=$(awk -v file="$file" '$2 == file || $2 == "*" file { print $1 }' "$tmp/SHA256SUMS") if [ -z "$want" ] || [ "$want" != "$(digest "$tmp/$file")" ]; then fail "the download does not match its checksum. Run the install command again." fi tar -xzf "$tmp/$file" -C "$tmp" [ -f "$tmp/pocketty" ] || fail "the download has no pocketty binary" chmod 755 "$tmp/pocketty" "$tmp/pocketty" --version >/dev/null 2>&1 || fail "the downloaded binary does not run on this computer" mkdir -p "$bin_dir" case "$(uname -s)" in Darwin) place_app ;; *) place_binary ;; esac } start_launchd() { domain="gui/$(id -u)" program=$(xml "$exe") path=$(xml "$(service_path)") output=$(xml "$log") kept="" if [ -f "$plist" ]; then kept=$(plutil -extract EnvironmentVariables xml1 -o - "$plist" 2>/dev/null | sed -e '1,//,$d' || true) fi mkdir -p "$(dirname "$plist")" "$(dirname "$log")" launchctl bootout "$domain/$label" >/dev/null 2>&1 || true cat >"$plist" < Label $label AssociatedBundleIdentifiers $bundle_id ProgramArguments $program daemon EnvironmentVariables PATH $path RunAtLoad KeepAlive StandardOutPath $output StandardErrorPath $output PLIST if [ -n "$kept" ]; then plutil -replace EnvironmentVariables -xml "$kept" "$plist" plutil -replace EnvironmentVariables.PATH -string "$(service_path)" "$plist" fi if ! launchctl print "$domain" >/dev/null 2>&1; then say "pocketty starts the next time you log in to this Mac." say "Log: $log" return fi launchctl enable "$domain/$label" tries=0 until error=$(launchctl bootstrap "$domain" "$plist" 2>&1); do tries=$((tries + 1)) [ "$tries" -lt 5 ] || fail "launchd did not start pocketty: $error" sleep 1 done say "Started pocketty as the launchd agent $label." say "Log: $log" } user_systemd() { [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 || return 1 if [ -z "${XDG_RUNTIME_DIR:-}" ] && [ -d "/run/user/$(id -u)" ]; then XDG_RUNTIME_DIR="/run/user/$(id -u)" export XDG_RUNTIME_DIR fi systemctl --user show-environment >/dev/null 2>&1 } start_systemd() { if ! user_systemd; then say "pocketty is not running, because you have no systemd user session here." if command -v loginctl >/dev/null 2>&1; then say "To start one that stays up, run once, then run the install command again:" say " loginctl enable-linger \"\$USER\"" fi say "Or start pocketty in a terminal: pocketty daemon" return fi path=$(service_path | sed -e 's/%/%%/g' -e 's/["\\]/\\&/g') mkdir -p "$(dirname "$unit")" cat >"$unit" </dev/null || true)" = no ]; then say "" say "pocketty stops when you log out. To keep it running, run once:" say " loginctl enable-linger \"\$USER\"" fi } check_path() { found=$(command -v pocketty 2>/dev/null || true) [ "$found" = "$bin" ] && return rc=.profile case "${SHELL##*/}" in zsh) rc=.zshrc ;; bash) if [ "$(uname -s)" = Darwin ]; then rc=.bash_profile; else rc=.bashrc; fi ;; esac fix="echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> \"\$HOME/$rc\"" if [ "${SHELL##*/}" = fish ]; then fix="fish_add_path ~/.local/bin" fi say "" if [ -n "$found" ]; then say "Your shell runs $found when you type pocketty." else say "Your shell does not find pocketty yet." fi say "To run commands like pocketty approve, put $bin_dir first on your PATH:" say " $fix" } install() { platform=$(target) for prefix in /opt/homebrew /usr/local /home/linuxbrew/.linuxbrew "$HOME/.linuxbrew"; do if [ -e "$prefix/opt/pocketty" ]; then fail "pocketty is installed with Homebrew. Update it with: brew upgrade pocketty" fi done tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM download say "" if ! has_agent_host; then say "pocketty needs herdr or Tailscale on this computer, and it found neither." say "Install one of them, then run the install command again to start pocketty." else case "$(uname -s)" in Darwin) start_launchd ;; Linux) start_systemd ;; esac fi check_path say "" say "Next: open pocketty on your phone and connect to this computer." } uninstall() { case "$(uname -s)" in Darwin) launchctl bootout "gui/$(id -u)/$label" >/dev/null 2>&1 || true rm -f "$plist" rm -rf "$app" rmdir "$(dirname "$app")" 2>/dev/null || true ;; Linux) user_systemd || true systemctl --user disable --now pocketty >/dev/null 2>&1 || true rm -f "$unit" systemctl --user daemon-reload >/dev/null 2>&1 || true ;; esac rm -f "$bin" say "Removed pocketty and its service." say "Your paired phones stay in $state. Delete that folder to forget them too." } main() { if [ "$(id -u)" = 0 ] && [ -n "${SUDO_USER:-}" ]; then fail "run this as your own user, without sudo" fi case "${1:-}" in "") install ;; --uninstall) uninstall ;; -h | --help) usage ;; *) usage >&2; exit 2 ;; esac } main "$@"