diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 0000000..53fcdf2 --- /dev/null +++ b/install/install.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +## +# toes installer +# Usage: curl -sSL https://toes.space/install | bash +# Must be run as the 'toes' user. + +DEST=~/toes +REPO="https://git.nose.space/defunkt/toes" + +quiet() { "$@" > /dev/null 2>&1; } + +echo "" +echo " ╔══════════════════════════════════╗" +echo " ║ 🐾 toes - personal web appliance ║" +echo " ╚══════════════════════════════════╝" +echo "" + +# Must be running as toes +if [ "$(whoami)" != "toes" ]; then + echo "ERROR: This script must be run as the 'toes' user." + echo "Create the user during Raspberry Pi OS setup." + exit 1 +fi + +# Must have passwordless sudo (can't prompt when piped from curl) +if ! sudo -n true 2>/dev/null; then + echo "ERROR: This script requires passwordless sudo." + echo "On Raspberry Pi OS, the default user has this already." + exit 1 +fi + +# -- System packages -- + +echo ">> Updating system packages" +quiet sudo apt-get update +quiet sudo apt-get install -y git libcap2-bin avahi-utils fish unzip + +echo ">> Setting fish as default shell" +if [ "$(getent passwd toes | cut -d: -f7)" != "/usr/bin/fish" ]; then + quiet sudo chsh -s /usr/bin/fish toes +fi + +# -- Bun -- + +BUN_REAL="$HOME/.bun/bin/bun" +BUN_SYMLINK="/usr/local/bin/bun" + +if [ ! -x "$BUN_REAL" ]; then + echo ">> Installing bun" + curl -fsSL https://bun.sh/install | bash > /dev/null 2>&1 + if [ ! -x "$BUN_REAL" ]; then + echo "ERROR: bun installation failed" + exit 1 + fi +fi + +if [ ! -x "$BUN_SYMLINK" ]; then + sudo ln -sf "$BUN_REAL" "$BUN_SYMLINK" +fi + +echo ">> Setting CAP_NET_BIND_SERVICE on bun" +sudo setcap 'cap_net_bind_service=+ep' "$BUN_REAL" + +# -- Clone -- + +if [ ! -d "$DEST" ]; then + echo ">> Cloning toes" + git clone "$REPO" "$DEST" +else + echo ">> Updating toes" + cd "$DEST" && git pull origin main +fi + +# -- Directories -- + +mkdir -p ~/data ~/apps + +# -- Dependencies & build -- + +echo ">> Installing dependencies" +cd "$DEST" && bun install + +echo ">> Building client" +cd "$DEST" && bun run build + +# -- Bundled apps -- + +echo ">> Installing bundled apps" +BUNDLED_APPS="clock code cron env stats versions" +for app in $BUNDLED_APPS; do + if [ -d "$DEST/apps/$app" ]; then + if [ -d ~/apps/"$app" ]; then + echo " $app (exists, skipping)" + continue + fi + echo " $app" + cp -r "$DEST/apps/$app" ~/apps/ + version_dir=$(ls -1 ~/apps/"$app" | grep -E '^[0-9]{8}-[0-9]{6}$' | sort -r | head -1) + if [ -n "$version_dir" ]; then + ln -sfn "$version_dir" ~/apps/"$app"/current + (cd ~/apps/"$app"/current && bun install --frozen-lockfile) > /dev/null 2>&1 || true + fi + fi +done + +# -- Systemd -- + +echo ">> Installing toes service" +sudo install -m 644 -o root -g root "$DEST/scripts/toes.service" /etc/systemd/system/toes.service +sudo systemctl daemon-reload +sudo systemctl enable toes + +echo ">> Starting toes" +sudo systemctl restart toes + +# -- Done -- + +echo "" +echo " toes is installed and running!" +echo " Visit: http://$(hostname).local" +echo "" diff --git a/install/server.ts b/install/server.ts new file mode 100644 index 0000000..45db375 --- /dev/null +++ b/install/server.ts @@ -0,0 +1,17 @@ +import { resolve } from "path" + +const script = await Bun.file(resolve(import.meta.dir, "install.sh")).text() + +Bun.serve({ + port: parseInt(process.env.PORT || "3000"), + fetch(req) { + if (new URL(req.url).pathname === "/install") { + return new Response(script, { + headers: { "content-type": "text/plain" }, + }) + } + return new Response("404 Not Found", { status: 404 }) + }, +}) + +console.log(`Serving /install on :${Bun.env.PORT || 3000}`)