#!/usr/bin/env bash # Tabelaro installer — downloads the DMG with curl (no quarantine), # verifies SHA-256, and installs to /Applications. set -euo pipefail VERSION="0.15.27" SHA256="98c9e00a4105f5ab923bc9c94dc9f2c5df4bc90e419312be31a092e9a7645baa" DMG_URL="https://tabelaro.com/download/Tabelaro-latest.dmg" APP_NAME="Tabelaro.app" INSTALL_DIR="/Applications" die() { printf 'Tabelaro install failed: %s\n' "$*" >&2 exit 1 } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } # --- platform checks --- [[ "$(uname -s)" == "Darwin" ]] || die "Tabelaro is a macOS app (detected $(uname -s))." need_cmd curl need_cmd shasum need_cmd hdiutil need_cmd ditto need_cmd sw_vers macos_ver="$(sw_vers -productVersion)" macos_major="${macos_ver%%.*}" [[ "$macos_major" =~ ^[0-9]+$ ]] || die "could not parse macOS version: ${macos_ver}" (( macos_major >= 11 )) || die "macOS 11.0 Big Sur or later required (found ${macos_ver})." # --- temp workspace + cleanup --- TMPDIR_INSTALL="$(mktemp -d "${TMPDIR:-/tmp}/tabelaro-install.XXXXXX")" MOUNTPOINT="" DMG_PATH="${TMPDIR_INSTALL}/Tabelaro-v${VERSION}.dmg" cleanup() { local ec=$? if [[ -n "${MOUNTPOINT}" && -d "${MOUNTPOINT}" ]]; then hdiutil detach "${MOUNTPOINT}" -quiet -force >/dev/null 2>&1 || true fi if [[ -n "${TMPDIR_INSTALL}" && -d "${TMPDIR_INSTALL}" ]]; then rm -rf "${TMPDIR_INSTALL}" fi exit "${ec}" } trap cleanup EXIT INT TERM printf 'Downloading Tabelaro v%s…\n' "${VERSION}" if ! curl -fL --progress-bar --retry 3 --retry-delay 2 \ -o "${DMG_PATH}" "${DMG_URL}"; then die "download failed from ${DMG_URL}" fi printf 'Verifying SHA-256…\n' actual="$(shasum -a 256 "${DMG_PATH}" | awk '{print $1}')" [[ "${actual}" == "${SHA256}" ]] || die "checksum mismatch expected: ${SHA256} got: ${actual} Refusing to install." MOUNTPOINT="${TMPDIR_INSTALL}/mnt" mkdir -p "${MOUNTPOINT}" printf 'Mounting disk image…\n' hdiutil attach "${DMG_PATH}" -nobrowse -quiet -mountpoint "${MOUNTPOINT}" \ || die "could not mount the disk image" SRC_APP="${MOUNTPOINT}/${APP_NAME}" [[ -d "${SRC_APP}" ]] || die "expected ${APP_NAME} inside the DMG (not found at ${SRC_APP})" # Quit running app if open (ignore errors — may not be running) osascript -e 'tell application "Tabelaro" to quit' >/dev/null 2>&1 || true sleep 1 DEST="${INSTALL_DIR}/${APP_NAME}" printf 'Installing to %s…\n' "${DEST}" copy_app() { if [[ -e "${DEST}" ]]; then rm -rf "${DEST}" fi ditto "${SRC_APP}" "${DEST}" } if [[ -w "${INSTALL_DIR}" ]] && { [[ ! -e "${DEST}" ]] || [[ -w "${DEST}" ]]; }; then copy_app || die "could not copy ${APP_NAME} to ${INSTALL_DIR}" else printf 'Need administrator access to write to %s.\n' "${INSTALL_DIR}" # Export paths for the elevated shell (avoids nested-quote bugs) export TABELARO_SRC_APP="${SRC_APP}" export TABELARO_DEST="${DEST}" sudo -E bash -c ' set -euo pipefail if [[ -e "${TABELARO_DEST}" ]]; then rm -rf "${TABELARO_DEST}"; fi ditto "${TABELARO_SRC_APP}" "${TABELARO_DEST}" ' || die "install with sudo failed" fi # Clear quarantine if present (curl downloads usually have none) if command -v xattr >/dev/null 2>&1; then if [[ -w "${DEST}" ]]; then xattr -dr com.apple.quarantine "${DEST}" 2>/dev/null || true else sudo xattr -dr com.apple.quarantine "${DEST}" 2>/dev/null || true fi fi hdiutil detach "${MOUNTPOINT}" -quiet -force >/dev/null 2>&1 || true MOUNTPOINT="" printf '\nTabelaro v%s is installed in %s.\n' "${VERSION}" "${DEST}" printf 'Open it with: open -a Tabelaro\n' printf 'Or find Tabelaro in Applications / Launchpad.\n'