Files
autofirmer-expanded/setup-linux.sh
T
Brandon LiandClaude Opus 5 c5c946d3cc Add Linux/Raspberry Pi setup and port the clicker to X11
The clicker had no Linux support at all: pygetwindow has no X11 backend, so
_other_activate returned "window management is unsupported" and every step
failed before it could click. focus.py now has a third backend that raises the
browser with xdotool and reads the focused window's WM_CLASS to verify it came
forward - the same activate-then-confirm shape as the macOS and Windows paths.

setup-linux.sh mirrors setup-windows.bat with apt instead of winget. Three
things are specific to this platform rather than incidental:

- Node comes from NodeSource. Pi OS ships one too old for Next 16, and the LTS
  line is also what better-sqlite3 publishes prebuilt arm64 binaries for.
- Python dependencies go in a virtualenv. Pi OS Bookworm enforces PEP 668, so
  pip into the system interpreter fails with externally-managed-environment.
- Autostart uses an XDG ~/.config/autostart entry, the direct analogue of the
  Windows Startup folder: no sudo, and it runs inside the graphical session,
  which the clicker needs for DISPLAY.

Wayland is called out in four places - the session guard, diagnose.py, the
installer and both READMEs - because Pi OS on a Pi 5 defaults to it and the
clicker simply cannot work there. Wayland does not let one client synthesise
input into another, so this is a switch-to-X11 situation, not a bug to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:53:25 -05:00

171 lines
8.3 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# AutoFirmer instance setup for Raspberry Pi OS / Debian / Ubuntu.
#
# Standalone: download this one file and run it. It clones the repo into a
# subfolder next to itself, installs, builds, and points the instance at the
# master dashboard. Safe to re-run - it pulls and rebuilds instead of cloning.
#
# curl -fsSLO https://git.juicerroom.com/senofy/autofirmer-expanded/raw/branch/master/setup-linux.sh
# bash setup-linux.sh
set -euo pipefail
REPO_URL="https://git.juicerroom.com/senofy/autofirmer-expanded.git"
DEFAULT_MASTER="https://master.juicerroom.com"
HERE="$(cd "$(dirname "$0")" && pwd)"
TARGET="$HERE/autofirmer"
NODE_MAJOR_MIN=20
ok() { printf ' [ok] %s\n' "$1"; }
info() { printf ' %s\n' "$1"; }
warn() { printf ' [!] %s\n' "$1" >&2; }
die() { printf '\n [X] %s\n\n' "$1" >&2; exit 1; }
echo
echo " ============================================"
echo " AutoFirmer - Linux setup"
echo " ============================================"
echo
# ── sudo ────────────────────────────────────────────────────────────────────
command -v apt-get >/dev/null 2>&1 || die "this script expects apt (Raspberry Pi OS, Debian, Ubuntu)"
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
command -v sudo >/dev/null 2>&1 || die "not root and sudo is not installed"
SUDO="sudo"
info "some steps need sudo; you may be prompted"
fi
# ── system packages ─────────────────────────────────────────────────────────
# xdotool raises the browser window (pygetwindow has no X11 backend, so the
# clicker drives xdotool instead). scrot backs pyautogui's screen reads.
# build-essential/python3-dev are only the fallback path for better-sqlite3 if
# no arm64 prebuilt binary matches this Node ABI.
info "installing system packages..."
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq \
git curl ca-certificates \
python3 python3-venv python3-dev \
xdotool scrot \
build-essential
ok "system packages"
# ── node ────────────────────────────────────────────────────────────────────
node_major() { command -v node >/dev/null 2>&1 && node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
if [ "$(node_major)" -lt "$NODE_MAJOR_MIN" ]; then
# Raspberry Pi OS ships a Node too old for Next 16, so take the LTS line
# from NodeSource. LTS also matters for better-sqlite3, which publishes
# prebuilt arm64 binaries only for released ABIs.
info "installing Node 22 LTS (found major $(node_major))..."
curl -fsSL https://deb.nodesource.com/setup_22.x | $SUDO -E bash - >/dev/null
$SUDO apt-get install -y -qq nodejs
fi
[ "$(node_major)" -ge "$NODE_MAJOR_MIN" ] || die "Node $NODE_MAJOR_MIN+ required, found $(node -v 2>/dev/null || echo none)"
ok "node $(node -v) npm $(npm -v)"
ok "python $(python3 -V 2>&1 | awk '{print $2}')"
# ── memory ──────────────────────────────────────────────────────────────────
TOTAL_MB=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
SWAP_MB=$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo)
if [ "$TOTAL_MB" -lt 3500 ] && [ "$SWAP_MB" -lt 1024 ]; then
warn "${TOTAL_MB}MB RAM and only ${SWAP_MB}MB swap - 'next build' may be OOM-killed."
warn "Consider raising CONF_SWAPSIZE in /etc/dphys-swapfile to 2048 and rebooting."
fi
# ── prompts ─────────────────────────────────────────────────────────────────
echo
read -r -p " Master dashboard URL [$DEFAULT_MASTER]: " MASTER_URL
MASTER_URL="${MASTER_URL:-$DEFAULT_MASTER}"
read -r -p " Instance name [$(hostname)]: " INSTANCE
INSTANCE="${INSTANCE:-$(hostname)}"
echo
info "installing to: $TARGET"
echo
# ── 1. clone / pull ─────────────────────────────────────────────────────────
# The branch is named explicitly: a bare clone follows the remote's default
# branch, which is not necessarily master.
if [ -d "$TARGET/.git" ]; then
info "[1/6] existing checkout - switching to master and pulling..."
git -C "$TARGET" fetch origin
git -C "$TARGET" checkout master
git -C "$TARGET" pull --ff-only origin master
else
info "[1/6] cloning $REPO_URL ..."
git clone --branch master "$REPO_URL" "$TARGET"
fi
cd "$TARGET"
# ── 2. npm ──────────────────────────────────────────────────────────────────
info "[2/6] installing npm dependencies (a few minutes on a Pi)..."
npm install
# ── 3. build ────────────────────────────────────────────────────────────────
info "[3/6] building..."
npm run build
# ── 4. settings ─────────────────────────────────────────────────────────────
info "[4/6] writing instance settings..."
node scripts/seed-settings.js "$MASTER_URL" "$INSTANCE"
# ── 5. python ───────────────────────────────────────────────────────────────
# A virtualenv rather than a system pip install: Raspberry Pi OS Bookworm
# enforces PEP 668, so pip into the system interpreter fails outright with
# "externally-managed-environment".
info "[5/6] installing clicker dependencies into a virtualenv..."
VENV="$TARGET/clicker/.venv"
[ -d "$VENV" ] || python3 -m venv "$VENV"
"$VENV/bin/python" -m pip install --quiet --upgrade pip
if "$VENV/bin/python" -m pip install --quiet -r clicker/requirements.txt; then
echo "$VENV/bin/python" > scripts/.python-cmd
ok "clicker dependencies ready"
else
warn "pip install failed - the dashboard still works, the clicker will not"
fi
# ── 6. autostart ────────────────────────────────────────────────────────────
echo
info "[6/6] Auto-start and auto-update"
info " Starts AutoFirmer and the clicker at login, and checks master every"
info " 5 minutes - rebuilding and restarting when it moves. A failed build"
info " is never deployed."
read -r -p " Set this up now? [Y/n] " DOAUTO
AUTOSTART=0
if [ "${DOAUTO,,}" != "n" ]; then
if bash scripts/install-autostart.sh; then AUTOSTART=1; else
warn "auto-start setup failed - start by hand with: node scripts/start-all.mjs"
fi
else
info " Skipped. Run scripts/install-autostart.sh later to enable it."
fi
# ── summary ─────────────────────────────────────────────────────────────────
echo
echo " ============================================"
echo " Done."
echo " ============================================"
echo
info "Instance name : $INSTANCE"
info "Reporting to : $MASTER_URL"
info "Installed in : $TARGET"
info "Dashboard at : http://localhost:3000"
echo
if [ "$AUTOSTART" -eq 1 ]; then
info "Running now, and again at every login (PM2)."
info "Handy: pm2 list | pm2 logs autofirmer | pm2 logs clicker"
else
info "Start it with : node scripts/start-all.mjs"
fi
echo
info "Still manual:"
info " - Load the Chrome extension: chrome://extensions -> Developer mode"
info " -> Load unpacked -> $TARGET/extension"
info " - Add your firm credentials on the dashboard's Settings page"
if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -z "${DISPLAY:-}" ]; then
echo
warn "This is a Wayland session - the clicker needs X11."
warn "sudo raspi-config -> Advanced Options -> Wayland -> X11, then reboot."
fi
echo