#!/bin/sh # wandlet installer for macOS. # # curl -fsSL https://wandlet.app/install.sh | sh # # The macOS sibling of install.ps1. Downloads the current .dmg from the stable # channel URL, so this script never carries a version number of its own — it # reports the version it actually installed, read from the installed bundle. # (install.ps1 learns the version from /updates/latest.json instead; the stable # URL is updated by the same publish step, so both scripts track the same # release.) # # No signature check here on purpose: this script and any key it could fetch # come from the same host, so verifying one against the other would prove # nothing. HTTPS to wandlet.app is the trust boundary. This is FIRST-INSTALL trust # only — from here on the app updates itself through /updates/latest.json, and # those payloads are minisign-verified against the pubkey compiled into the # binary this script just installed. # # A curl-downloaded file carries no quarantine xattr, so this install never # meets Gatekeeper — no "unidentified developer" dialog, no right-click→Open # dance. That is the main reason this script exists next to the browser # download. set -eu DMG_URL="https://wandlet.app/download/wandlet.dmg" DEST="/Applications/Wandlet.app" # State for honest failure reporting, mirroring install.ps1: losing the # download AND the running app with no explanation is the worst outcome. TMPDIR_MQ="" MNT="" KEEP_DMG=0 CLOSED_APP=0 say() { printf ' %s\n' "$*"; } cleanup() { if [ -n "$MNT" ]; then # Only blank MNT if the detach succeeded: rm -rf below must never run # against a still-mounted volume. A leaked temp dir is the lesser evil. /usr/bin/hdiutil detach "$MNT" -quiet 2>/dev/null && MNT="" || true fi if [ -z "$MNT" ] && [ -n "$TMPDIR_MQ" ] && [ "$KEEP_DMG" -eq 0 ]; then rm -rf "$TMPDIR_MQ" fi } fail() { cleanup printf '\n install failed: %s\n' "$*" >&2 if [ "$KEEP_DMG" -eq 1 ] && [ -n "$TMPDIR_MQ" ]; then printf ' The downloaded image was kept at: %s/wandlet.dmg\n' "$TMPDIR_MQ" >&2 printf ' Open it and drag wandlet to Applications by hand.\n' >&2 fi if [ "$CLOSED_APP" -eq 1 ]; then printf ' wandlet was closed for this install and is NOT running now.\n' >&2 fi printf '\n' >&2 exit 1 } main() { printf '\n wandlet installer\n\n' # --- preconditions ---------------------------------------------------- [ "$(uname -s)" = "Darwin" ] || fail "this installer is for macOS only." # hw.optional.arm64 is 1 on Apple Silicon even inside a Rosetta shell, # where uname -m would lie and report x86_64. if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" != "1" ]; then fail "current wandlet builds are Apple Silicon only — this Mac is Intel." fi if [ ! -w /Applications ]; then fail "/Applications is not writable by this user. Install from an administrator account, or download the dmg from https://wandlet.app and drag the app somewhere you can write." fi # --- download ----------------------------------------------------------- say "downloading the latest wandlet..." TMPDIR_MQ="$(mktemp -d)" || fail "could not create a temporary directory." DMG="$TMPDIR_MQ/wandlet.dmg" curl -fSL --proto '=https' --connect-timeout 30 -o "$DMG" "$DMG_URL" \ || fail "download failed from $DMG_URL — is the network up?" # The download worked. From here on, any failure must leave the dmg # behind — it is the only thing the user can retry with. KEEP_DMG=1 # --- mount + inspect ------------------------------------------------------ MNT="$TMPDIR_MQ/mnt" mkdir -p "$MNT" /usr/bin/hdiutil attach "$DMG" -nobrowse -readonly -quiet -mountpoint "$MNT" \ || { MNT=""; fail "the downloaded image would not mount (corrupt download?)."; } APP="$MNT/Wandlet.app" [ -d "$APP" ] || fail "the image mounted but contains no Wandlet.app." VERSION="$(defaults read "$APP/Contents/Info" CFBundleShortVersionString 2>/dev/null || echo "unknown")" say "found v$VERSION" # --- close a running copy ------------------------------------------------- # wandlet sits in the menu bar and owns a global event tap; closing it # without a word would be a surprise. Say it out loud (install.ps1 and # install-local-mac.sh do the same). if pgrep -xq wandlet; then say "wandlet is running -- closing it, and starting it again after the install" osascript -e 'tell application "Wandlet" to quit' >/dev/null 2>&1 || true for _ in 1 2 3 4 5; do pgrep -xq wandlet || break; sleep 1; done if pgrep -xq wandlet; then pkill -x wandlet || true; sleep 1; fi CLOSED_APP=1 fi # --- install --------------------------------------------------------------- say "installing to $DEST ..." rm -rf "$DEST" || fail "could not remove the previous copy at $DEST." /usr/bin/ditto "$APP" "$DEST" || fail "copying the app into /Applications failed." # curl applies no quarantine flag, but clearing it costs nothing and covers # a dmg that ever passed through a browser or AirDrop. xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true # Installed cleanly; the image is redundant now. KEEP_DMG=0 cleanup # --- start it ---------------------------------------------------------------- open "$DEST" || fail "installed v$VERSION, but it would not start. Open it from /Applications." sleep 2 if pgrep -xq wandlet; then CLOSED_APP=0 printf '\n installed wandlet v%s\n' "$VERSION" say "It lives in the menu bar. Select text anywhere, then double-tap Shift." printf '\n' say "First install only: grant Accessibility when the app asks (it opens the" say "right Settings pane). On current macOS that grant also covers input" say "monitoring — wandlet not appearing in the Input Monitoring pane is" say "normal. Upgrades keep their permissions, including the in-app ones:" say "from now on wandlet offers new versions from its menu bar icon." printf '\n' else fail "installed v$VERSION, but the app did not stay running — open it from /Applications and check the diagnostics window (menu bar icon → Open diagnostics…)." fi } # Wrapped in main() and invoked on the last line so a partially downloaded # script does nothing at all — the `curl | sh` equivalent of install.ps1's # never-call-exit rule. main