#!/bin/sh # Keep Mac Awake installer for the command line, served at https://keepmacawake.com/install.sh. # # curl -fsSL https://keepmacawake.com/install.sh | sh # curl -fsSL https://keepmacawake.com/install.sh | sh -s -- --uninstall # # It downloads the package from the GitHub release, checks its SHA-256, the Developer ID # signature of the pinned team and Apple's notarization, then runs macOS Installer. macOS asks # for an administrator once: in the terminal through sudo, or, without a terminal (an AI agent), # in a system dialog. It refuses while a Keep Mac Awake session runs, because installing or # removing ends the session. Run it as the logged-in user, never with sudo. # # Exit codes: 0 done, 1 failed, 2 invalid arguments, 3 this Mac is not supported, # 4 action needed from the user, 5 a session is running. set -eu RELEASES_URL='https://github.com/bartdudzic/keepmacawake/releases' # The Apple Developer Team ID that signs the packages. TEAM_ID='3X35AKH8DH' MIN_MACOS_MAJOR=26 APP_PATH='/Applications/Keep Mac Awake.app' CLI='/usr/local/bin/keepmacawake' RECEIPT='com.keepmacawake' say() { printf '%s\n' "$*"; } fail() { code=$1 shift printf 'keepmacawake install: %s\n' "$*" >&2 exit "$code" } usage() { cat <<'EOF' Usage: install.sh [--uninstall] [--version X.Y.Z] [--pkg PATH] [--dry-run] (no option) Install or update to the latest release. --uninstall Remove Keep Mac Awake with its uninstaller package. --version X.Y.Z Use this release instead of the latest. --pkg PATH Use a package already on this Mac instead of downloading one. --dry-run Download and check the package, but do not install it. Run as the logged-in user, never with sudo. macOS asks for an administrator once. EOF } check_mac() { [ "$(/usr/bin/uname -s)" = Darwin ] || fail 3 'Keep Mac Awake runs only on macOS.' [ "$(/usr/sbin/sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = 1 ] || fail 3 'Keep Mac Awake needs a Mac with Apple silicon.' major=$(/usr/bin/sw_vers -productVersion | /usr/bin/cut -d. -f1) [ "$major" -ge "$MIN_MACOS_MAJOR" ] 2>/dev/null || fail 3 "Keep Mac Awake needs macOS $MIN_MACOS_MAJOR or later." [ "$(/usr/bin/id -u)" != 0 ] || fail 4 'Run this as the logged-in user without sudo; it asks for the administrator password itself.' } is_installed() { [ -e "$APP_PATH" ] || /usr/sbin/pkgutil --pkg-info "$RECEIPT" >/dev/null 2>&1 } # Installing or removing stops the services, which ends a running session. Never do that. check_no_session() { [ -x "$CLI" ] || return 0 status_code=0 status=$("$CLI" status --json --no-input 2>/dev/null) || status_code=$? case $status_code in 0) ;; 6) return 0 ;; # The background service is not running, so no session runs. *) fail 5 'Could not read the Keep Mac Awake session state. Run `keepmacawake status`, then try again.' ;; esac case $status in *'"live":true'*) fail 5 'A Keep Mac Awake session is running. End it with `keepmacawake stop`, then run this again.' ;; esac } fetch() { /usr/bin/curl --proto '=https' --tlsv1.2 -fsSL --retry 2 -o "$2" "$1" || fail 1 "Download failed: $1" } latest_version() { url=$(/usr/bin/curl --proto '=https' --tlsv1.2 -fsSL -o /dev/null -w '%{url_effective}' "$RELEASES_URL/latest") || fail 1 "Could not find the latest release at $RELEASES_URL." printf '%s\n' "${url##*/tag/v}" } valid_version() { printf '%s\n' "$1" | /usr/bin/grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' } # A downloaded package must match its .sha256 file, carry the pinned team's Developer ID # Installer signature and pass Gatekeeper, which requires Apple's notarization. verify_download() { pkg=$1 name=$(/usr/bin/basename "$pkg") expected=$(/usr/bin/awk -v name="$name" 'NF == 2 && $2 == name && $1 ~ /^[0-9a-f]+$/ && length($1) == 64 { print $1 }' "$pkg.sha256") [ -n "$expected" ] || fail 1 "The checksum file of $name is not valid." actual=$(/usr/bin/shasum -a 256 "$pkg" | /usr/bin/awk '{ print $1 }') [ "$actual" = "$expected" ] || fail 1 "The SHA-256 of $name does not match its checksum file." signer=$(/usr/sbin/pkgutil --check-signature "$pkg" 2>/dev/null | /usr/bin/awk '/^ *1\. / { sub(/^ *1\. /, ""); print; exit }') case $signer in "Developer ID Installer: "*"($TEAM_ID)") ;; *) fail 1 "$name is not signed by the Keep Mac Awake developer." ;; esac /usr/sbin/spctl --assess --type install "$pkg" >/dev/null 2>&1 || fail 1 "Gatekeeper does not accept $name; it is not notarized by Apple." say "Checked $name: SHA-256, Developer ID ($TEAM_ID) and notarization." } # Runs Installer as root: sudo when there is a terminal or a cached sudo ticket, # otherwise the system's administrator dialog. run_installer() { pkg=$1 if /usr/bin/sudo -n true 2>/dev/null; then /usr/bin/sudo -n /usr/sbin/installer -pkg "$pkg" -target / || fail 1 'Installer failed; see its messages above.' elif (: /dev/null; then say 'macOS needs an administrator password to install the system services.' /usr/bin/sudo /usr/sbin/installer -pkg "$pkg" -target / &1" with prompt (item 2 of argv) with administrator privileges' \ -e 'end run' \ "$pkg" "$2" 2>&1) || result=$? [ -z "$output" ] || say "$output" if [ "$result" != 0 ]; then case $output in *'(-128)'*) fail 4 'The administrator dialog was cancelled.' ;; *'(-1743)'* | *'(-1712)'* | *'(-10810)'*) fail 4 'macOS could not show the administrator dialog. Run this command in Terminal.' ;; *) fail 1 'Installer failed; see its messages above.' ;; esac fi fi } main() { mode=install version='' local_pkg='' dry_run=0 while [ $# -gt 0 ]; do case $1 in --uninstall) mode=uninstall ;; --version) [ $# -ge 2 ] || fail 2 '--version needs a value, for example 1.0.0.' version=${2#v} shift ;; --pkg) [ $# -ge 2 ] || fail 2 '--pkg needs a path.' local_pkg=$2 shift ;; --dry-run) dry_run=1 ;; -h | --help) usage; exit 0 ;; *) usage >&2; fail 2 "Unknown option: $1" ;; esac shift done [ -z "$version" ] || [ -z "$local_pkg" ] || fail 2 '--version and --pkg cannot be used together.' [ -z "$version" ] || valid_version "$version" || fail 2 "Not a version number: $version" check_mac # A dry run changes nothing, so it also works during a session or without an installation. if [ "$dry_run" = 0 ]; then if [ "$mode" = uninstall ] && ! is_installed; then say 'Keep Mac Awake is not installed.' exit 0 fi check_no_session fi if [ -n "$local_pkg" ]; then [ -f "$local_pkg" ] || fail 2 "No such package: $local_pkg" case $local_pkg in /*) pkg=$local_pkg ;; *) pkg=$(pwd)/$local_pkg ;; esac if /usr/sbin/spctl --assess --type install "$pkg" >/dev/null 2>&1; then say "Using $pkg (accepted by Gatekeeper)." else say "Using $pkg. It is not notarized; it is used because you chose this file." fi else [ -n "$TEAM_ID" ] || fail 1 'No signed release is published yet. Use --pkg with a package you have.' [ -n "$version" ] || version=$(latest_version) valid_version "$version" || fail 1 "Could not read the latest release version from $RELEASES_URL." if [ "$mode" = uninstall ]; then name="KeepMacAwake-Uninstall-$version-arm64.pkg" else name="KeepMacAwake-$version-arm64.pkg" fi work=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/keepmacawake-install.XXXXXX") trap '/bin/rm -rf "$work"' EXIT trap 'exit 130' HUP INT TERM pkg="$work/$name" say "Downloading $name..." fetch "$RELEASES_URL/download/v$version/$name" "$pkg" fetch "$RELEASES_URL/download/v$version/$name.sha256" "$pkg.sha256" verify_download "$pkg" fi if [ "$dry_run" = 1 ]; then say "Dry run: $pkg is ready; nothing was installed." exit 0 fi if [ "$mode" = uninstall ]; then run_installer "$pkg" 'Keep Mac Awake wants to remove its app and system services.' say 'Keep Mac Awake is removed and the sleep setting is restored.' return 0 fi run_installer "$pkg" 'Keep Mac Awake wants to install its app and system services.' [ -x "$CLI" ] || fail 1 "Installer finished, but $CLI is missing." status_code=0 "$CLI" status --no-input >/dev/null 2>&1 || status_code=$? [ "$status_code" = 0 ] || fail 1 "Installed, but the background service does not answer (keepmacawake status exited with $status_code)." installed=$("$CLI" --version 2>/dev/null || true) say "Keep Mac Awake ${installed##* } is installed. Nothing is turned on." say 'Start a session with `keepmacawake start --hours N` or the menu bar icon; `keepmacawake capabilities --json` describes the commands.' # Most installs are run by an agent, which reads this and can offer its own plugin. say 'AI agents: with the Keep Mac Awake plugin, your agent turns a session on before long tasks and off after.' say ' Claude Code: claude plugin install keep-mac-awake --marketplace bartdudzic/keepmacawake' say ' Codex, Copilot CLI, Cursor and others: "Teach your agent once" in https://keepmacawake.com/agents.md' } # Everything runs from here, so a download cut short by the network executes nothing. main "$@"