#!/bin/bash
# ArtBrowser installer: makes a private, local copy of the Arc browser you already have.
#
# It copies the Arc app and your Arc data on THIS Mac into a separate app called ArtBrowser,
# adds a small launcher that keeps Arc's cloud features switched off, and signs the result
# locally. It never changes Arc or your Arc data. It does not download Arc and uploads nothing.
#
#   bash install-artbrowser.sh               install
#   bash install-artbrowser.sh --check       look at your Mac and report, change nothing
#   bash install-artbrowser.sh --uninstall   remove ArtBrowser (your Arc and backups stay)
#
# The launcher and local-services library it installs come from a small payload file
# (checked against the SHA-256 below). They are built from native/Launcher.m and
# native/LocalServices.m, which are published next to this script.
#
# Everything below the option parsing lives in main(), so a truncated download (for example
# a dropped `curl | bash`) fails to parse and changes nothing.

set -euo pipefail
umask 077

SUPPORTED_VERSIONS="1.169.0 1.170.0"
PAYLOAD_URL="https://arcreborn.cool.computer/artbrowser-payload.tgz"
PAYLOAD_SHA256="3d57ae01c9060c7b0e1ff7a1686856027c26c2012d68c67ada30874f2dc89fc9"
PAYLOAD_FILE=""
ARC_APP="/Applications/Arc.app"
SUPPORT="$HOME/Library/Application Support"
ARC_DATA="$SUPPORT/Arc"
APPS_DIR="$HOME/Applications"
DEST_APP="$APPS_DIR/ArtBrowser.app"
DEST_DATA_ROOT="$SUPPORT/ArtBrowser.noindex"
DEST_ALIAS="$SUPPORT/ArtBrowser"
BACKUP_ROOT="$SUPPORT/ArtBrowserBackup"
LOCK_DIR="$SUPPORT/.artbrowser-install.lock"
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
MODE="install"
ASSUME_YES=0
STAGE_APP=""; STAGE_DATA=""; CREATED=(); DONE=0; LOCKED=0

say()  { printf '%s\n' "$*"; }
step() { printf '\n▸ %s\n' "$*"; }
die()  { printf '\n✗ %s\n' "$*" >&2; exit 1; }

usage() { sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }

while [ $# -gt 0 ]; do
  case "$1" in
    --check) MODE="check" ;;
    --uninstall) MODE="uninstall" ;;
    --yes|-y) ASSUME_YES=1 ;;
    --arc-app) ARC_APP="${2:?--arc-app needs a path}"; shift ;;
    --arc-data) ARC_DATA="${2:?--arc-data needs a path}"; shift ;;
    --payload) PAYLOAD_FILE="${2:?--payload needs a path}"; shift ;;
    -h|--help) usage ;;
    *) die "Unknown option: $1 (try --help)" ;;
  esac
  shift
done

confirm() {
  [ "$ASSUME_YES" = 1 ] && return 0
  printf '%s [y/N] ' "$1"; local a=""; read -r a < /dev/tty 2>/dev/null || a=""
  case "$a" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}

json_ok() {
  /usr/bin/osascript -l JavaScript -e 'ObjC.import("Foundation"); function run(a){ var d=$.NSData.dataWithContentsOfFile(a[0]); if(!d) throw new Error("unreadable"); var o=$.NSJSONSerialization.JSONObjectWithDataOptionsError(d,0,$()); if(!o || o.isNil()) throw new Error("not JSON"); return "ok"; }' "$1" >/dev/null 2>&1
}

# Is a process running whose command line contains this exact text? (literal, not a pattern)
# The needle goes through the environment, not awk's arguments, so awk cannot match its own command line.
proc_running() { ps -axo command= | NEEDLE="$1" awk 'index($0, ENVIRON["NEEDLE"]){f=1} END{exit !f}'; }
# Is an app with this bundle identifier running? This does not depend on what the app folder is called.
# True when the app is running, and also when we cannot tell: destructive steps must fail closed.
# (Set ARTBROWSER_SKIP_BUNDLE_CHECK=1 only if this check cannot work in your session.)
bundle_running() {
  local n
  if ! n="$(/usr/bin/osascript -l JavaScript -e 'ObjC.import("AppKit"); function run(a){ return String($.NSRunningApplication.runningApplicationsWithBundleIdentifier(a[0]).count) }' "$1" 2>/dev/null)"; then
    [ "${ARTBROWSER_SKIP_BUNDLE_CHECK:-}" = "1" ] && return 1; return 0
  fi
  case "$n" in 0) return 1 ;; ''|*[!0-9]*) [ "${ARTBROWSER_SKIP_BUNDLE_CHECK:-}" = "1" ] && return 1; return 0 ;; *) return 0 ;; esac
}
arc_running() {
  [ "${ARTBROWSER_INSTALLER_TEST:-}" = "1" ] && return 1
  proc_running "$ARC_APP/Contents/" || { [ -n "${ARC_APP_ORIG:-}" ] && proc_running "$ARC_APP_ORIG/Contents/"; } || bundle_running company.thebrowser.Browser
}
art_running() {
  [ "${ARTBROWSER_INSTALLER_TEST:-}" = "1" ] && return 1
  proc_running "/ArtBrowser.app/Contents/" || bundle_running local.artbrowser.app
}

resolve_dir() { ( cd -P "$1" 2>/dev/null && pwd -P ); }
existing_parent() { local p="$1"; while [ ! -e "$p" ]; do p="$(dirname "$p")"; done; [ -d "$p" ] && p="$(cd -P "$p" && pwd -P)"; printf '%s' "$p"; }
dev_of()  { stat -L -f %d "$(existing_parent "$1")"; }
free_on() { df -k "$(existing_parent "$1")" | awk 'NR==2{print $4}'; }
is_apfs() { diskutil info "$(existing_parent "$1")" 2>/dev/null | grep -qi "APFS"; }

# Every link under ROOT whose real destination (after following any chain of links) is not
# inside ROOT, or that goes nowhere. Chromium's three disposable Singleton links in
# "User Data" are the only exception. Copying such a link would let ArtBrowser write into Arc's files.
unsafe_links() {
  /usr/bin/perl -MFile::Find -MCwd=abs_path -e '
    my $root = abs_path($ARGV[0]) or exit 0;
    my %ok = map { ("$root/User Data/$_" => 1) } qw(SingletonLock SingletonCookie SingletonSocket);
    find({ no_chdir => 1, wanted => sub {
      return unless -l $_;
      return if $ok{$_};
      my $dest = abs_path($_);
      print "$_ -> ", readlink($_), "\n" unless defined $dest && ($dest eq $root || index($dest, "$root/") == 0);
    } }, $root);' "$1"
}

run() { local out; if ! out="$("$@" 2>&1)"; then printf '%s\n' "$out" >&2; return 1; fi; }
pset() { local f="$1" k="$2" t="$3" v="$4"; /usr/libexec/PlistBuddy -c "Set :$k $v" "$f" >/dev/null 2>&1 || /usr/libexec/PlistBuddy -c "Add :$k $t $v" "$f"; }

acquire_lock() {
  mkdir -p "$SUPPORT"
  if ! mkdir "$LOCK_DIR" 2>/dev/null; then
    local old; old="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
    if [ -n "$old" ] && kill -0 "$old" 2>/dev/null; then die "Another ArtBrowser install or uninstall is already running (process $old). Wait for it to finish."; fi
    die "A previous install or uninstall did not finish cleanly and left a lock behind. If no installer is running, remove the lock and run this again:  rm -rf '$LOCK_DIR'"
  fi
  echo "$$" > "$LOCK_DIR/pid"; LOCKED=1
}

# Run cleanup on Ctrl-C, hang-up, termination and a closed output pipe too, not only on a normal exit.
arm_traps() { trap cleanup EXIT; trap 'exit 130' INT; trap 'exit 143' TERM; trap 'exit 129' HUP; trap 'exit 141' PIPE; }

cleanup() {
  local rc=$?
  if [ "$DONE" != 1 ]; then
    local p
    for p in "${CREATED[@]:-}"; do [ -n "$p" ] && { if [ -L "$p" ]; then rm -f "$p"; else rm -rf "$p"; fi; }; done
    [ $rc -ne 0 ] && [ ${#CREATED[@]} -gt 0 ] && printf '\n✗ Install did not finish, so I removed everything I had created. Arc and its data were not touched.\n' >&2
  fi
  [ -n "$STAGE_APP" ] && rm -rf "$STAGE_APP"
  [ -n "$STAGE_DATA" ] && rm -rf "$STAGE_DATA"
  if [ "$LOCKED" = 1 ] && [ "$(cat "$LOCK_DIR/pid" 2>/dev/null || true)" = "$$" ]; then rm -rf "$LOCK_DIR"; fi
  return 0
}

main() {
  # ------------------------------------------------------------ uninstall
  if [ "$MODE" = "uninstall" ]; then
    step "Uninstalling ArtBrowser"
    acquire_lock; arm_traps
    art_running && die "ArtBrowser is open. Quit it (⌘Q) and run this again."
    local apps=() a
    for a in "$DEST_APP" "/Applications/ArtBrowser.app"; do
      [ -d "$a" ] && [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$a/Contents/Info.plist" 2>/dev/null || true)" = "local.artbrowser.app" ] && apps+=("$a")
    done
    say "This removes: ${apps[*]:-(no ArtBrowser app found)}"
    say "              $DEST_DATA_ROOT  (ArtBrowser's own copy of your data)"
    say "It also removes ArtBrowser's logs and caches. It keeps your original Arc, your Arc data and the backup in $BACKUP_ROOT (delete that folder yourself if you no longer want it)."
    confirm "Remove ArtBrowser now?" || die "Cancelled. Nothing was changed."
    art_running && die "ArtBrowser was opened while you were deciding. Quit it (⌘Q) and run this again."
    [ -L "$DEST_ALIAS" ] && rm "$DEST_ALIAS"
    for a in "${apps[@]:-}"; do [ -n "$a" ] && rm -rf "$a"; done
    rm -rf "$DEST_DATA_ROOT"
    defaults delete local.artbrowser.app >/dev/null 2>&1 || true
    rm -rf "$HOME/Library/Caches/local.artbrowser.app" "$HOME/Library/HTTPStorages/local.artbrowser.app" \
           "$HOME/Library/Saved Application State/local.artbrowser.app.savedState" \
           "$SUPPORT/ArtBrowserPreservation.noindex" "$SUPPORT/firestore/ArtBrowser" \
           "$HOME/Library/Caches/ArtBrowser" "$HOME/Library/Caches/SentryCrash/ArtBrowser"
    say "Done. ArtBrowser is removed. Arc was not touched."
    DONE=1; exit 0
  fi

  # ------------------------------------------------------------ preflight
  step "Checking your Mac"
  [ "$(uname -s)" = "Darwin" ] || die "This installer is for macOS."
  [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = "1" ] || die "This build has only been tested on Apple Silicon Macs. Your Mac looks like an Intel Mac, so I stopped before changing anything."
  local macos_major; macos_major="$(sw_vers -productVersion | cut -d. -f1)"
  [ "$macos_major" -ge 12 ] || die "macOS 12 or newer is required (you have $(sw_vers -productVersion))."
  say "  ✓ Apple Silicon, macOS $(sw_vers -productVersion)"

  [ -d "$ARC_APP" ] || die "Arc was not found at $ARC_APP. Install Arc first, or point me at it with --arc-app /path/to/Arc.app"
  ARC_APP_ORIG="$ARC_APP"
  ARC_APP="$(resolve_dir "$ARC_APP")" || die "Could not read $ARC_APP."
  local arc_id; arc_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$ARC_APP/Contents/Info.plist" 2>/dev/null || true)"
  [ "$arc_id" = "company.thebrowser.Browser" ] || die "That does not look like Arc (bundle id: ${arc_id:-unknown})."
  local arc_version arc_build
  arc_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$ARC_APP/Contents/Info.plist")"
  arc_build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$ARC_APP/Contents/Info.plist")"
  case " $SUPPORTED_VERSIONS " in
    *" $arc_version "*) say "  ✓ Arc $arc_version ($arc_build)" ;;
    *) die "Your Arc is version $arc_version. This installer supports Arc versions: $SUPPORTED_VERSIONS. I stopped before changing anything, because an untested version could end up broken." ;;
  esac
  local links; links="$(unsafe_links "$ARC_APP")"
  if [ -n "$links" ]; then printf '%s\n' "$links" | head -5 >&2; die "The Arc app contains links that point outside it (shown above). Copying them could let the build change files outside the copy, so I stopped before changing anything."; fi

  [ -d "$ARC_DATA" ] || die "Arc's data folder was not found at $ARC_DATA. Open Arc once and sign in, then try again."
  ARC_DATA="$(resolve_dir "$ARC_DATA")" || die "Could not read Arc's data folder."
  local f
  for f in StorableSidebar.json StorableAuthState.json; do
    [ -s "$ARC_DATA/$f" ] || die "Missing $f in Arc's data folder. Open Arc once and sign in, then try again."
    json_ok "$ARC_DATA/$f" || die "$f in Arc's data folder could not be read. Nothing was changed."
  done
  local uid; uid="$(plutil -extract session.user.id raw -o - "$ARC_DATA/StorableAuthState.json" 2>/dev/null || true)"
  [ -n "$uid" ] || die "Arc has no saved sign-in on this Mac. ArtBrowser needs the saved session, so sign in to Arc once, quit it, and try again."
  say "  ✓ Arc data found (saved sidebar and session)"

  links="$(unsafe_links "$ARC_DATA")"
  if [ -n "$links" ]; then
    printf '%s\n' "$links" | head -5 >&2
    die "Arc's data folder contains links that point outside it (shown above). Copying them would let ArtBrowser write into Arc's data, so I stopped before changing anything."
  fi
  say "  ✓ Arc's app and data are self-contained"

  if [ -e "$DEST_APP" ] || [ -L "$DEST_APP" ] || [ -e "/Applications/ArtBrowser.app" ] || [ -e "$DEST_DATA_ROOT" ] || [ -e "$DEST_ALIAS" ] || [ -L "$DEST_ALIAS" ]; then
    die "ArtBrowser is already installed. To start over, run this installer again with --uninstall (your Arc and backups are untouched)."
  fi
  say "  ✓ ArtBrowser is not installed yet"

  arc_running && die "Arc is still running, or I could not tell. Quit it completely with ⌘Q (closing the window is not enough), then run this again. I need Arc closed so your data is copied cleanly."
  say "  ✓ Arc is not running"

  # Space: measured on the volume each copy really lands on, and clones only where source and destination share an APFS volume.
  local data_kb app_kb need_apps need_support free_apps free_support
  data_kb="$(du -sk "$ARC_DATA" | cut -f1)"; app_kb="$(du -sk "$ARC_APP" | cut -f1)"
  if [ "$(dev_of "$ARC_APP")" = "$(dev_of "$APPS_DIR")" ] && is_apfs "$APPS_DIR"; then need_apps=$((512*1024)); else need_apps=$((app_kb + 512*1024)); fi
  if [ "$(dev_of "$ARC_DATA")" = "$(dev_of "$SUPPORT")" ] && is_apfs "$SUPPORT"; then need_support=$((1024*1024)); else need_support=$((2*data_kb + 1024*1024)); fi
  free_apps="$(free_on "$APPS_DIR")"; free_support="$(free_on "$SUPPORT")"
  if [ "$(dev_of "$APPS_DIR")" = "$(dev_of "$SUPPORT")" ]; then
    [ "$free_apps" -ge $((need_apps + need_support)) ] || die "Not enough free disk space (need about $(( (need_apps+need_support)/1024/1024 )) GB, have $((free_apps/1024/1024)) GB)."
  else
    [ "$free_apps" -ge "$need_apps" ] || die "Not enough free space for the app in $APPS_DIR (need about $((need_apps/1024/1024)) GB)."
    [ "$free_support" -ge "$need_support" ] || die "Not enough free space for your data in $SUPPORT (need about $((need_support/1024/1024)) GB, have $((free_support/1024/1024)) GB)."
  fi
  say "  ✓ Enough free space"

  if [ "$MODE" = "check" ]; then
    say ""; say "All checks passed. Nothing was changed. Run without --check to install."
    exit 0
  fi

  acquire_lock; arm_traps
  step "Installing (Arc itself is never changed)"
  say "  1. Back up your Arc data to $BACKUP_ROOT"
  say "  2. Build $DEST_APP from a copy of your Arc"
  say "  3. Give it its own copy of your data"

  mkdir -p "$APPS_DIR"
  STAGE_APP="$(mktemp -d "$APPS_DIR/.artbrowser-build.XXXXXX")"
  STAGE_DATA="$(mktemp -d "$SUPPORT/.artbrowser-stage-XXXXXX")"

  step "1/5 Backing up your Arc data"
  arc_running && die "Arc was opened again. Quit it (⌘Q) and run this again."
  local stamp backup; stamp="$(date +%Y-%m-%d_%H-%M-%S)"; backup="$BACKUP_ROOT/$stamp"
  mkdir -p "$BACKUP_ROOT"; mkdir "$backup" || die "A backup folder for this second already exists. Try again."
  CREATED+=("$backup")
  run cp -cR "$ARC_DATA" "$backup/Arc-data"
  say "  ✓ $backup"

  step "2/5 Getting the launcher"
  mkdir -p "$STAGE_APP/payload"
  if [ -n "$PAYLOAD_FILE" ]; then cp "$PAYLOAD_FILE" "$STAGE_APP/payload.tgz"
  else curl -fsSL --max-time 180 "$PAYLOAD_URL" -o "$STAGE_APP/payload.tgz" || die "Could not download the launcher from $PAYLOAD_URL. Check your internet connection and try again."; fi
  local got; got="$(shasum -a 256 "$STAGE_APP/payload.tgz" | cut -d' ' -f1)"
  [ "$got" = "$PAYLOAD_SHA256" ] || die "The launcher file does not match its checksum, so I did not use it. Download the installer again."
  tar -xzf "$STAGE_APP/payload.tgz" -C "$STAGE_APP/payload"
  [ -x "$STAGE_APP/payload/ArtBrowser" ] && [ -f "$STAGE_APP/payload/ArtBrowserLocalServices.dylib" ] && [ -f "$STAGE_APP/payload/ArtBrowser.icns" ] || die "The launcher file is incomplete. Download the installer again."
  say "  ✓ Launcher verified"

  step "3/5 Building ArtBrowser.app (this takes a minute)"
  local APP="$STAGE_APP/ArtBrowser.app"
  run cp -cR "$ARC_APP" "$APP"
  [ -d "$APP" ] && [ ! -L "$APP" ] || die "Copying Arc produced something unexpected. Nothing was changed."
  links="$(unsafe_links "$APP")"
  [ -z "$links" ] || die "The copy of the Arc app contains links that point outside it, so I stopped before changing it."
  local INFO="$APP/Contents/Info.plist"
  pset "$INFO" CFBundleIdentifier string local.artbrowser.app
  pset "$INFO" CFBundleName string ArtBrowser
  pset "$INFO" CFBundleDisplayName string ArtBrowser
  pset "$INFO" CFBundleExecutable string ArtBrowser
  pset "$INFO" CFBundleIconFile string ArtBrowser
  /usr/libexec/PlistBuddy -c "Delete :CFBundleIconName" "$INFO" >/dev/null 2>&1 || true
  # Arc draws its Dock icon with a plug-in that the Dock itself loads; without it the Dock uses our icon.
  /usr/libexec/PlistBuddy -c "Delete :NSDockTilePlugIn" "$INFO" >/dev/null 2>&1 || true
  rm -rf "$APP/Contents/PlugIns/DockTilePlugIn.plugin"; rmdir "$APP/Contents/PlugIns" 2>/dev/null || true
  pset "$INFO" SUEnableAutomaticChecks bool false
  pset "$INFO" SUAutomaticallyUpdate bool false
  /usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$INFO" >/dev/null 2>&1 || true
  mv "$APP/Contents/MacOS/Arc" "$APP/Contents/MacOS/ArcBinary"
  cp "$STAGE_APP/payload/ArtBrowser" "$APP/Contents/MacOS/ArtBrowser"; chmod 755 "$APP/Contents/MacOS/ArtBrowser"
  cp "$STAGE_APP/payload/ArtBrowserLocalServices.dylib" "$APP/Contents/Frameworks/ArtBrowserLocalServices.dylib"
  cp "$STAGE_APP/payload/ArtBrowser.icns" "$APP/Contents/Resources/ArtBrowser.icns"
  printf '{\n  "source_version": "%s",\n  "source_build": "%s",\n  "architecture": "arm64",\n  "built_at": "%s",\n  "installer": "friends-3"\n}\n' \
    "$arc_version" "$arc_build" "$(date +%Y-%m-%dT%H:%M:%S)" > "$APP/Contents/Resources/ArtBrowser-build.json"
  cat > "$STAGE_APP/entitlements.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
<key>com.apple.security.device.audio-input</key><true/>
<key>com.apple.security.device.camera</key><true/>
</dict></plist>
PLIST
  xattr -cr "$APP"
  run codesign --force --sign - --identifier local.artbrowser.app --entitlements "$STAGE_APP/entitlements.plist" "$APP/Contents/MacOS/ArcBinary"
  run codesign --force --sign - "$APP/Contents/Frameworks/ArtBrowserLocalServices.dylib"
  run codesign --force --deep --sign - --entitlements "$STAGE_APP/entitlements.plist" "$APP"
  run codesign --verify --deep --strict "$APP"
  say "  ✓ Built and signed on this Mac"

  step "4/5 Giving ArtBrowser its own copy of your data"
  run cp -cR "$ARC_DATA" "$STAGE_DATA/Profile"
  links="$(unsafe_links "$STAGE_DATA/Profile")"
  [ -z "$links" ] || die "The copy of Arc's data contains links that point outside it, so I stopped. Arc's data was not changed."
  [ ! -L "$STAGE_DATA/Profile/User Data" ] || die "Arc's browser data folder is a link, so I stopped. Arc's data was not changed."
  local l
  for l in SingletonLock SingletonCookie SingletonSocket; do rm -f "$STAGE_DATA/Profile/User Data/$l"; done
  local ARC_PREFS="$HOME/Library/Preferences/company.thebrowser.Browser.plist"
  if [ -f "$ARC_PREFS" ]; then cp "$ARC_PREFS" "$STAGE_DATA/prefs.plist"; else printf '<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict/></plist>' > "$STAGE_DATA/prefs.plist"; fi
  local k
  for k in cloudKitSyncEnabled firebaseSyncEnabled SUEnableAutomaticChecks SUAutomaticallyUpdate; do pset "$STAGE_DATA/prefs.plist" "$k" bool false; done

  step "5/5 Putting it in place"
  arc_running && die "Arc was opened again. Quit it (⌘Q) and run this again."
  mkdir "$DEST_DATA_ROOT" 2>/dev/null || die "ArtBrowser's data folder appeared while installing (another installer?). Nothing of yours was changed."
  CREATED+=("$DEST_DATA_ROOT")
  mv "$STAGE_DATA/Profile" "$DEST_DATA_ROOT/Profile"
  local target="$DEST_DATA_ROOT/Profile"
  if [ ! -e "$DEST_ALIAS" ] && [ ! -L "$DEST_ALIAS" ] && ln -s "$target" "$DEST_ALIAS" 2>/dev/null && [ -L "$DEST_ALIAS" ] && [ "$(readlink "$DEST_ALIAS")" = "$target" ]; then
    CREATED+=("$DEST_ALIAS")
  else
    # Something appeared at that path. If our link landed inside it, remove just that link, never the folder.
    [ -L "$DEST_ALIAS/Profile" ] && [ "$(readlink "$DEST_ALIAS/Profile")" = "$target" ] && rm -f "$DEST_ALIAS/Profile"
    die "$DEST_ALIAS appeared while installing, so I stopped. Nothing of yours was changed."
  fi
  [ ! -e "$DEST_APP" ] && [ ! -L "$DEST_APP" ] || die "ArtBrowser.app appeared while installing. Nothing of yours was changed."
  mv "$APP" "$DEST_APP" || die "Could not place the app."
  if [ -x "$DEST_APP/Contents/MacOS/ArtBrowser" ] && [ ! -e "$DEST_APP/ArtBrowser.app" ]; then
    CREATED+=("$DEST_APP")
  else
    rm -rf "$DEST_APP/ArtBrowser.app"      # our copy landed inside a folder that appeared meanwhile: remove only our copy
    die "A folder appeared at $DEST_APP while installing, so I stopped. Nothing of yours was changed."
  fi
  # Preferences go in last, so a failure earlier never leaves a preferences domain behind.
  if [ "${ARTBROWSER_INSTALLER_TEST:-}" != "1" ]; then
    local prefs_existed=0; defaults read local.artbrowser.app >/dev/null 2>&1 && prefs_existed=1
    if ! run defaults import local.artbrowser.app "$STAGE_DATA/prefs.plist"; then
      [ "$prefs_existed" = 0 ] && defaults delete local.artbrowser.app >/dev/null 2>&1
      die "Could not save ArtBrowser's preferences, so I stopped and removed what I had created."
    fi
  fi
  [ -x "$LSREGISTER" ] && "$LSREGISTER" -f "$DEST_APP" >/dev/null 2>&1 || true
  DONE=1

  say ""
  say "✓ ArtBrowser is installed:  $DEST_APP"
  say ""
  say "Next:"
  say "  1. Open it:  open \"$DEST_APP\""
  say "  2. macOS will ask to use \"Arc Safe Storage\". Enter your Mac password (or use Touch ID) and click Always Allow. That unlocks your saved logins."
  say "  3. Optional: make it your default browser in System Settings → Desktop & Dock → Default web browser."
  say "  4. Keep Arc installed, and do not delete the \"Arc Safe Storage\" Keychain entry."
  say ""
  say "ArtBrowser is a one-time copy: changes in Arc and ArtBrowser do not sync."
  say "Your Arc data was backed up to: $backup"
  say "To remove ArtBrowser later, run this installer again with --uninstall"
  exit 0
}

main
