#!/usr/bin/env bash # Update the Pi and Claude Code, then move the Claude sessions onto the new # version: the panel updater, the reboot and six terminals, in one go. # # update.sh the Update button: apt + Claude, restart every idle # session now, and offer the reboot (60 s countdown) # if an update needs one # update.sh --nightly the 04:45 timer: the same, but a session must be idle # NIGHTLY_MIN_IDLE minutes, and it reboots only if # update.conf says AUTO_REBOOT=1 # # Log: ~/.cache/claude-sessions/update-*.log (last 30 kept) set -uo pipefail HERE=$(cd "$(dirname "$(readlink -f "$0")")" && pwd) # shellcheck source=update.conf . "$HERE/update.conf" CS="$HERE/claude-sessions.py" MODE=button; [ "${1:-}" = --nightly ] && MODE=nightly # Pressed from inside a Claude session, these would leak into claude update. unset $(compgen -e | grep '^CLAUDE') 2>/dev/null export PATH="$HOME/.local/bin:$PATH" LOGDIR=~/.cache/claude-sessions mkdir -p "$LOGDIR" LOG="$LOGDIR/update-$(date +%F-%H%M%S).log" exec > >(tee -a "$LOG") 2>&1 ls -1t "$LOGDIR"/update-*.log 2>/dev/null | tail -n +31 | xargs -r rm -f step() { printf '\n== %s\n' "$*"; } finish() { echo; echo "Log: $LOG" if [ $MODE = button ] && [ -t 0 ]; then read -rp "Done. Press Enter to close. " _; fi exit "${1:-0}" } echo "$(date '+%F %T') update.sh ($MODE)" # ── the OS (apt) ──────────────────────────────────────────────────────────────────── if [ "${APT_UPDATES:-1}" = 1 ]; then APT=(sudo -n env DEBIAN_FRONTEND=noninteractive apt-get -q -o DPkg::Lock::Timeout=600) step "Pi OS: refreshing package lists" "${APT[@]}" update | tail -2 mapfile -t PKGS < <("${APT[@]}" -s full-upgrade | awk '/^Inst /{print $2}') if [ ${#PKGS[@]} -eq 0 ]; then echo "up to date" else step "Pi OS: upgrading ${#PKGS[@]} package(s)" printf '%s\n' "${PKGS[@]}" | column -c 100 # Keep our edited config files if a package ships a new one; never stop to ask. "${APT[@]}" -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold \ full-upgrade > "$LOGDIR/apt-last.log" 2>&1 \ && echo "done" || echo "⚠ apt full-upgrade FAILED — see $LOGDIR/apt-last.log" # A new kernel is caught by claude-sessions.py itself; these need a reboot too. if printf '%s\n' "${PKGS[@]}" | grep -qE '^(raspi-firmware|libc6|systemd|dbus|dbus-daemon)$'; then touch "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pi-update-reboot-required" fi fi fi # ── Claude Code ────────────────────────────────────────────────────────────── step "Claude Code" before=$(basename "$(readlink ~/.local/bin/claude)") claude update < /dev/null 2>&1 | grep -v '^\s*$' | tail -3 after=$(basename "$(readlink ~/.local/bin/claude)") [ "$before" = "$after" ] && echo "installed: $after" || echo "installed: $before → $after" # ── reboot, or restart the sessions ────────────────────────────────────────── if "$CS" reboot-pending > /dev/null; then step "An update needs a reboot" if [ $MODE = button ] && [ -t 0 ]; then "$CS" status echo echo "The Claude sessions come back by themselves at login." echo "Rebooting in 60 s. Enter = reboot now · Ctrl-C = not now." skip=0; trap 'skip=1' INT read -rt 60 _; rc=$? trap - INT if [ $skip = 0 ] && { [ $rc = 0 ] || [ $rc -gt 128 ]; }; then sudo -n systemctl reboot; exit 0; fi echo; echo "not rebooting now" elif [ $MODE = nightly ] && [ "${AUTO_REBOOT:-0}" = 1 ]; then if "$CS" all-idle --min-idle "${NIGHTLY_MIN_IDLE:-30}"; then echo "every session idle ${NIGHTLY_MIN_IDLE:-30}+ min: rebooting" sudo -n systemctl reboot; exit 0 fi echo "a session is in use: no reboot tonight" else echo "not rebooting (AUTO_REBOOT=0); the tmux tab bar shows ⟳ until someone does" fi fi step "Claude sessions" if [ $MODE = nightly ]; then "$CS" restart --outdated --adopt --min-idle "${NIGHTLY_MIN_IDLE:-30}" else "$CS" restart --outdated --adopt fi "$CS" start --open --if-unattached echo "$CS" status finish