From 029e349c5b0ca28e702f9d2e59a5064bf8037511 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 25 Feb 2026 20:38:47 -0800 Subject: [PATCH 1/4] add toes installer and install server --- install/install.sh | 155 +++++++++++++++++++++++++++++++++++++++++++++ install/server.ts | 17 +++++ 2 files changed, 172 insertions(+) create mode 100644 install/install.sh create mode 100644 install/server.ts diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 0000000..ddd0e31 --- /dev/null +++ b/install/install.sh @@ -0,0 +1,155 @@ +#!/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 + +# -- Hostname -- + +CURRENT_HOSTNAME=$(hostname) +if [ "$CURRENT_HOSTNAME" != "toes" ]; then + echo ">> Setting hostname to 'toes' (was '$CURRENT_HOSTNAME')" + sudo hostnamectl set-hostname toes + sudo sed -i "s/$CURRENT_HOSTNAME/toes/g" /etc/hosts +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 + 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 + +# -- Kiosk mode -- + +echo ">> Enabling kiosk mode" +sudo raspi-config nonint do_boot_behaviour B4 2>/dev/null || true + +mkdir -p ~/.config/labwc +cat > ~/.config/labwc/autostart <<'EOF' +chromium --noerrdialogs --disable-infobars --kiosk http://localhost +EOF + +WAYFIRE_CONFIG="$HOME/.config/wayfire.ini" +if [ -f "$WAYFIRE_CONFIG" ]; then + sed -i '/^chromium = /d' "$WAYFIRE_CONFIG" + if grep -q '^\[autostart\]' "$WAYFIRE_CONFIG"; then + sed -i '/^\[autostart\]/a chromium = chromium --noerrdialogs --disable-infobars --kiosk http://localhost' "$WAYFIRE_CONFIG" + else + cat >> "$WAYFIRE_CONFIG" <<'EOF' + +[autostart] +chromium = chromium --noerrdialogs --disable-infobars --kiosk http://localhost +EOF + fi +fi + +# -- Done -- + +echo "" +echo " toes is installed! Rebooting in 5 seconds..." +echo " After reboot, visit: http://toes.local" +echo "" +sleep 5 +sudo nohup reboot >/dev/null 2>&1 & +exit 0 diff --git a/install/server.ts b/install/server.ts new file mode 100644 index 0000000..8e62548 --- /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("toes", { status: 404 }) + }, +}) + +console.log(`Serving /install on :${Bun.env.PORT || 3000}`) From b0c5a11cde67d899429a0a28062085a067569a41 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 25 Feb 2026 20:44:05 -0800 Subject: [PATCH 2/4] Add hostname setup and kiosk mode to install --- install/install.sh | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/install/install.sh b/install/install.sh index ddd0e31..830f30f 100644 --- a/install/install.sh +++ b/install/install.sh @@ -31,15 +31,6 @@ if ! sudo -n true 2>/dev/null; then exit 1 fi -# -- Hostname -- - -CURRENT_HOSTNAME=$(hostname) -if [ "$CURRENT_HOSTNAME" != "toes" ]; then - echo ">> Setting hostname to 'toes' (was '$CURRENT_HOSTNAME')" - sudo hostnamectl set-hostname toes - sudo sed -i "s/$CURRENT_HOSTNAME/toes/g" /etc/hosts -fi - # -- System packages -- echo ">> Updating system packages" @@ -120,36 +111,9 @@ sudo systemctl enable toes echo ">> Starting toes" sudo systemctl restart toes -# -- Kiosk mode -- - -echo ">> Enabling kiosk mode" -sudo raspi-config nonint do_boot_behaviour B4 2>/dev/null || true - -mkdir -p ~/.config/labwc -cat > ~/.config/labwc/autostart <<'EOF' -chromium --noerrdialogs --disable-infobars --kiosk http://localhost -EOF - -WAYFIRE_CONFIG="$HOME/.config/wayfire.ini" -if [ -f "$WAYFIRE_CONFIG" ]; then - sed -i '/^chromium = /d' "$WAYFIRE_CONFIG" - if grep -q '^\[autostart\]' "$WAYFIRE_CONFIG"; then - sed -i '/^\[autostart\]/a chromium = chromium --noerrdialogs --disable-infobars --kiosk http://localhost' "$WAYFIRE_CONFIG" - else - cat >> "$WAYFIRE_CONFIG" <<'EOF' - -[autostart] -chromium = chromium --noerrdialogs --disable-infobars --kiosk http://localhost -EOF - fi -fi - # -- Done -- echo "" -echo " toes is installed! Rebooting in 5 seconds..." -echo " After reboot, visit: http://toes.local" +echo " toes is installed and running!" +echo " Visit: http://$(hostname).local" echo "" -sleep 5 -sudo nohup reboot >/dev/null 2>&1 & -exit 0 From 604ac96b3018f6e5455526a74af9bdc8b091c657 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 26 Feb 2026 20:28:07 -0800 Subject: [PATCH 3/4] Add paw print emoji to install banner --- install/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/install.sh b/install/install.sh index 830f30f..8c6860e 100644 --- a/install/install.sh +++ b/install/install.sh @@ -13,7 +13,7 @@ quiet() { "$@" > /dev/null 2>&1; } echo "" echo " ╔══════════════════════════════════╗" -echo " ║ toes - personal web appliance ║" +echo " ║ 🐾 toes - personal web appliance ║" echo " ╚══════════════════════════════════╝" echo "" From 4853ee4f7a9307d6099bd3f6744c1fc708c20f16 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 27 Feb 2026 19:35:06 -0800 Subject: [PATCH 4/4] Skip bundled app install if already exists --- install/install.sh | 10 +++++++--- install/server.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/install/install.sh b/install/install.sh index 8c6860e..53fcdf2 100644 --- a/install/install.sh +++ b/install/install.sh @@ -91,12 +91,16 @@ 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) + 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 + ln -sfn "$version_dir" ~/apps/"$app"/current + (cd ~/apps/"$app"/current && bun install --frozen-lockfile) > /dev/null 2>&1 || true fi fi done diff --git a/install/server.ts b/install/server.ts index 8e62548..45db375 100644 --- a/install/server.ts +++ b/install/server.ts @@ -10,7 +10,7 @@ Bun.serve({ headers: { "content-type": "text/plain" }, }) } - return new Response("toes", { status: 404 }) + return new Response("404 Not Found", { status: 404 }) }, })