Files
autofirmer-expanded/setup-linux.sh
T
Brandon LiandClaude Opus 5 d14bcdeed3 A Pi gets the unpacked path, not a dead end
Chrome is x86_64 only on Linux and Chromium cannot be enrolled, so on ARM the
enrolment step can never succeed. Removing load-unpacked from the installer
left those machines being told to run a script that would refuse - setup now
detects the architecture, explains why, and the summary points at the unpacked
install, which on that hardware is not a fallback but the only thing that
works. It still self-updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:18:14 -05:00

215 lines
11 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/7] 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/7] cloning $REPO_URL ..."
git clone --branch master "$REPO_URL" "$TARGET"
fi
cd "$TARGET"
# ── 2. npm ──────────────────────────────────────────────────────────────────
info "[2/7] installing npm dependencies (a few minutes on a Pi)..."
npm install
# ── 3. build ────────────────────────────────────────────────────────────────
info "[3/7] building..."
npm run build
# ── 4. settings ─────────────────────────────────────────────────────────────
info "[4/7] 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/7] 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/7] 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
# ── 7. extension ────────────────────────────────────────────────────────────
# Chrome will not update an unpacked folder, and only force-installs an off-store
# extension on a managed browser. Enrolling in Chrome Enterprise Core is what
# makes this machine's Chrome managed; the extension then arrives from the Admin
# console already set to force install. The token file lives under /etc, hence
# sudo.
echo
EXTPOLICY=0
if [ "$(uname -m)" != "x86_64" ]; then
# A Pi, or anything else ARM. Google publishes Chrome for x86_64 only, and
# Chromium - the browser that is available here - has no cloud management,
# so it cannot be enrolled and can never force-install an off-store
# extension. Unpacked is not a downgrade to be apologised for on this
# machine; it is the only thing that works, and it still updates itself.
info "[7/7] Chrome extension"
info " Chrome is not published for $(uname -m), and Chromium cannot be"
info " enrolled - so on this machine the extension is loaded unpacked."
info " It still reloads itself from disk when the dashboard reports a"
info " newer version, so updates arrive without clicking anything."
EXTPOLICY=2
else
info "[7/7] Chrome extension"
info " Enrols this machine's Chrome, after which the extension installs"
info " itself from the Admin console and stays updated on its own."
info " Installs Chrome first if it is not here yet. Needs sudo."
read -r -p " Set this up now? [Y/n] " DOEXT
if [ "${DOEXT,,}" != "n" ]; then
if sudo bash scripts/enroll-chrome-management.sh; then EXTPOLICY=1; else
warn "enrolment failed - re-run: sudo scripts/enroll-chrome-management.sh"
fi
else
info " Skipped. Run: sudo scripts/enroll-chrome-management.sh"
fi
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:"
if [ "$EXTPOLICY" -eq 1 ]; then
info " - Restart Chrome. chrome://management should name your organisation,"
info " and the extension installs itself within a minute or two."
elif [ "$EXTPOLICY" -eq 2 ]; then
info " - Load the extension in Chromium: chrome://extensions ->"
info " Developer mode -> Load unpacked -> $TARGET/extension"
info " (after that it reloads itself when an update lands)"
else
info " - Enrol Chrome so it can install the extension:"
info " sudo scripts/enroll-chrome-management.sh"
fi
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