#!/bin/bash # MacKit public beta installer # Usage: curl -fsSL https://mackit.nodetools.org/install.sh | bash set -euo pipefail BASE_URL="https://mackit.nodetools.org" APP_NAME="MacKit" VERSION="0.1.0" MIN_MACOS=13 DEFAULT_INSTALL_DIR="/Applications" INSTALL_DIR="${MACKIT_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" OPEN_AFTER=1 KEEP_QUARANTINE=0 DRY_RUN=0 EXPECTED_VERSION="" usage() { cat <<'USAGE' MacKit installer (public beta) curl -fsSL https://mackit.nodetools.org/install.sh | bash Options: --install-dir Install location (default: /Applications) --version Require this installed version (default: latest beta) --keep-quarantine Do not remove the macOS quarantine flag --no-open Do not launch after install --dry-run Print actions without changing anything -h, --help Show this help Environment: MACKIT_INSTALL_DIR USAGE } while [[ $# -gt 0 ]]; do case "$1" in --install-dir) INSTALL_DIR="${2:?}"; shift 2 ;; --version) EXPECTED_VERSION="${2:?}"; shift 2 ;; --keep-quarantine) KEEP_QUARANTINE=1; shift ;; --no-open) OPEN_AFTER=0; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage; exit 1 ;; esac done say() { printf '\033[1;36m›\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m!\033[0m %s\n' "$*"; } fail() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; } [[ "$(uname -s)" == "Darwin" ]] || fail "MacKit requires macOS." MACOS_MAJOR=$(sw_vers -productVersion | cut -d. -f1) [[ "$MACOS_MAJOR" -ge "$MIN_MACOS" ]] || fail "macOS $MIN_MACOS or newer is required (found $(sw_vers -productVersion))." ARCH=$(uname -m) [[ "$ARCH" == "arm64" || "$ARCH" == "x86_64" ]] || fail "Unsupported architecture: $ARCH" say "MacKit $VERSION installer · macOS $(sw_vers -productVersion) · $ARCH" warn "This is a free beta. It is ad-hoc signed and not notarized yet, so the installer removes the quarantine flag after verifying the checksum." TMP=$(mktemp -d "${TMPDIR:-/tmp}/mackit-install.XXXXXX") MNT="$TMP/mnt" cleanup() { hdiutil detach "$MNT" -quiet >/dev/null 2>&1 || true rm -rf "$TMP" } trap cleanup EXIT DMG_URL="$BASE_URL/download/MacKit.dmg" SHA_URL="$BASE_URL/download/MacKit.dmg.sha256" say "Downloading $DMG_URL" curl -fL --proto '=https' --tlsv1.2 --retry 3 --progress-bar "$DMG_URL" -o "$TMP/MacKit.dmg" \ || fail "Download failed: $DMG_URL" curl -fsSL --proto '=https' --tlsv1.2 --retry 3 "$SHA_URL" -o "$TMP/MacKit.dmg.sha256" \ || fail "Could not download the checksum." EXPECTED=$(tr -d '[:space:]' < "$TMP/MacKit.dmg.sha256") ACTUAL=$(shasum -a 256 "$TMP/MacKit.dmg" | awk '{print $1}') [[ -n "$EXPECTED" && "$EXPECTED" == "$ACTUAL" ]] || fail "Checksum mismatch. The download may be corrupted." say "Checksum verified" if [[ "$DRY_RUN" == "1" ]]; then say "Dry run: would install to $INSTALL_DIR/$APP_NAME.app" exit 0 fi mkdir -p "$MNT" hdiutil attach "$TMP/MacKit.dmg" -nobrowse -readonly -mountpoint "$MNT" -quiet [[ -d "$MNT/$APP_NAME.app" ]] || fail "The disk image does not contain $APP_NAME.app." osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true TARGET_DIR="$INSTALL_DIR" if [[ ! -d "$TARGET_DIR" ]]; then mkdir -p "$TARGET_DIR" 2>/dev/null || true fi if [[ ! -w "$TARGET_DIR" ]]; then TARGET_DIR="$HOME/Applications" warn "$INSTALL_DIR is not writable; installing to $TARGET_DIR instead." mkdir -p "$TARGET_DIR" fi say "Installing to $TARGET_DIR/$APP_NAME.app" rm -rf "$TARGET_DIR/$APP_NAME.app" ditto "$MNT/$APP_NAME.app" "$TARGET_DIR/$APP_NAME.app" if [[ "$KEEP_QUARANTINE" == "0" ]]; then xattr -dr com.apple.quarantine "$TARGET_DIR/$APP_NAME.app" >/dev/null 2>&1 || true fi INSTALLED_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$TARGET_DIR/$APP_NAME.app/Contents/Info.plist" 2>/dev/null || echo "?") say "Installed $APP_NAME $INSTALLED_VERSION" if [[ -n "$EXPECTED_VERSION" && "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then fail "Expected version $EXPECTED_VERSION but installed $INSTALLED_VERSION." fi if [[ "$OPEN_AFTER" == "1" ]]; then open "$TARGET_DIR/$APP_NAME.app" fi cat <