#!/bin/bash # One-command installer for Vigil, using the prebuilt universal app from the # latest GitHub release — no Xcode toolchain needed. # # curl -fsSL https://raw.githubusercontent.com/sachanayush47/vigil/master/get.sh | bash # # The same command updates an existing install: it replaces the app in place, # leaves your preferences alone, and reinstalls unconditionally so a half-broken # install repairs itself rather than being skipped as "already current". # # Env overrides go on `bash`, not on `curl` (which would only set them for the # download itself): # # curl -fsSL .../get.sh | VIGIL_VERSION=v1.0 bash # # VIGIL_VERSION pin a specific release tag instead of the latest # VIGIL_REPO owner/name, to install from a fork # # Deliberately avoids heredocs: this script is usually executed from a pipe, and # printf keeps it robust regardless of how bash buffers stdin. set -euo pipefail REPO="${VIGIL_REPO:-sachanayush47/vigil}" APP_DEST="$HOME/Applications/Vigil.app" HOOK_DIR="$HOME/.claude/vigil" HOOK_DEST="$HOOK_DIR/vigil-hook.py" LABEL="app.vigil.menubar" AGENT="$HOME/Library/LaunchAgents/$LABEL.plist" die() { printf 'error: %s\n' "$1" >&2; exit 1; } # ---- preflight ------------------------------------------------------------ [[ "$(uname -s)" == "Darwin" ]] || die "Vigil is macOS only (found $(uname -s))." major="$(sw_vers -productVersion | cut -d. -f1)" [[ "$major" -ge 13 ]] || die "macOS 13+ required (found $(sw_vers -productVersion))." command -v curl >/dev/null 2>&1 || die "curl not found." command -v python3 >/dev/null 2>&1 || \ die "python3 not found. Install the Xcode Command Line Tools: xcode-select --install" # ---- resolve the release -------------------------------------------------- if [[ -n "${VIGIL_VERSION:-}" ]]; then api="https://api.github.com/repos/$REPO/releases/tags/$VIGIL_VERSION" else api="https://api.github.com/repos/$REPO/releases/latest" fi printf 'resolving release from %s\n' "$REPO" release="$(curl -fsSL -H 'Accept: application/vnd.github+json' "$api")" || \ die "could not reach the GitHub releases API for $REPO." # Print " ", picking the universal zip out of the asset list. read -r TAG ASSET_URL < <(printf '%s' "$release" | python3 -c ' import json, sys r = json.load(sys.stdin) tag = r.get("tag_name") or "" url = "" for a in r.get("assets", []): if a.get("name", "").endswith("-universal.zip"): url = a.get("browser_download_url", "") break print(tag, url) ') [[ -n "$TAG" ]] || die "no release tag found for $REPO." [[ -n "$ASSET_URL" ]] || die "release $TAG has no *-universal.zip asset attached." # The installed bundle is the only record of what someone is running, so read # the version straight out of it rather than guessing from a marker file. installed="" if [[ -f "$APP_DEST/Contents/Info.plist" ]]; then installed="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \ "$APP_DEST/Contents/Info.plist" 2>/dev/null || true)" fi if [[ -z "$installed" ]]; then printf 'installing Vigil %s\n' "$TAG" elif [[ "$installed" == "${TAG#v}" ]]; then printf 'reinstalling Vigil %s (already current)\n' "$TAG" else # VIGIL_VERSION can pin an older release, so don't call every change an # update. sort -V orders version strings properly. newest="$(printf '%s\n%s\n' "$installed" "${TAG#v}" | sort -V | tail -1)" if [[ "$newest" == "$installed" ]]; then printf 'downgrading Vigil %s -> %s\n' "$installed" "${TAG#v}" else printf 'updating Vigil %s -> %s\n' "$installed" "${TAG#v}" fi fi TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT # ---- download ------------------------------------------------------------- printf 'downloading app\n' curl -fsSL -o "$TMP/vigil.zip" "$ASSET_URL" || die "download failed: $ASSET_URL" ditto -x -k "$TMP/vigil.zip" "$TMP/unpacked" || die "could not unpack the release zip." [[ -d "$TMP/unpacked/Vigil.app" ]] || die "release zip did not contain Vigil.app." raw="https://raw.githubusercontent.com/$REPO/$TAG" printf 'downloading hook\n' curl -fsSL -o "$TMP/vigil-hook.py" "$raw/hooks/vigil-hook.py" || die "could not fetch the hook." curl -fsSL -o "$TMP/register-hooks.py" "$raw/scripts/register-hooks.py" || \ die "could not fetch the hook registration script." # ---- retire the pre-rename install --------------------------------------- LEGACY_LABEL="local.claude.menubar" if launchctl print "gui/$UID/$LEGACY_LABEL" >/dev/null 2>&1; then printf 'removing previous install (%s)\n' "$LEGACY_LABEL" launchctl bootout "gui/$UID/$LEGACY_LABEL" >/dev/null 2>&1 || true fi rm -f "$HOME/Library/LaunchAgents/$LEGACY_LABEL.plist" rm -rf "$HOME/Applications/ClaudeSessions.app" "$HOME/.claude/menubar" # ---- install the app ------------------------------------------------------ printf 'installing app -> %s\n' "$APP_DEST" mkdir -p "$HOME/Applications" osascript -e "quit app id \"$LABEL\"" >/dev/null 2>&1 || true rm -rf "$APP_DEST" ditto "$TMP/unpacked/Vigil.app" "$APP_DEST" # The app is ad-hoc signed, not notarized, so a downloaded copy carries the # quarantine flag and Gatekeeper would refuse to launch it. Clearing it here is # what makes this a one-command install rather than a right-click-Open dance. xattr -dr com.apple.quarantine "$APP_DEST" 2>/dev/null || true # ---- install the hook ----------------------------------------------------- printf 'installing hook -> %s\n' "$HOOK_DEST" mkdir -p "$HOOK_DIR/state" cp "$TMP/vigil-hook.py" "$HOOK_DEST" chmod +x "$HOOK_DEST" printf 'registering hooks in ~/.claude/settings.json\n' python3 "$TMP/register-hooks.py" "$HOOK_DEST" # ---- login agent ---------------------------------------------------------- printf 'installing login agent -> %s\n' "$AGENT" mkdir -p "$HOME/Library/LaunchAgents" printf '%s\n' \ '' \ '' \ '' \ '' \ " Label$LABEL" \ ' ProgramArguments' \ ' ' \ " $APP_DEST/Contents/MacOS/Vigil" \ ' ' \ ' RunAtLoad' \ ' KeepAlive' \ '' \ '' > "$AGENT" launchctl bootout "gui/$UID/$LABEL" >/dev/null 2>&1 || true launchctl bootstrap "gui/$UID" "$AGENT" printf '\ndone — Vigil %s is running. Look for the coloured dots in your menu bar.\n' "$TAG" printf 'Open a new Claude session (or run /hooks in an existing one) to pick up the hooks.\n' printf '\nTo update later, run this same command again.\n' printf '\nTo uninstall:\n' printf ' curl -fsSL https://raw.githubusercontent.com/%s/master/uninstall.sh | bash\n' "$REPO"