#!/bin/bash # Squirrelly skill installer. macOS Bash 3.2 and bundled tools only. set -euo pipefail umask 077 installer_work='' installer_stage='' installer_lock='' installer_backup='' installer_target='' installer_done=0 fail() { printf 'Squirrelly: %s\n' "$*" >&2; exit 1; } cleanup() { installer_status=$? set +e if [ "$installer_done" -eq 0 ] && [ -n "$installer_backup" ] && [ ! -e "$installer_target" ] && [ ! -L "$installer_target" ]; then /bin/mv "$installer_backup" "$installer_target" || printf 'Previous skill is preserved at: %s\n' "$installer_backup" >&2 fi [ -z "$installer_stage" ] || /bin/rm -rf "$installer_stage" [ -z "$installer_work" ] || /bin/rm -rf "$installer_work" [ -z "$installer_lock" ] || /bin/rmdir "$installer_lock" 2>/dev/null exit "$installer_status" } trap cleanup EXIT main() { local client='codex' skills_dir='' update=0 archive_path='' supplied_hash='' while [ "$#" -gt 0 ]; do case "$1" in --client|--skills-dir|--archive|--sha256) [ "$#" -ge 2 ] || fail 'Missing option value.' case "$1" in --client) client=$2;; --skills-dir) skills_dir=$2;; --archive) archive_path=$2;; --sha256) supplied_hash=$2;; esac shift 2;; --update) update=1; shift;; --help|-h) printf '%s\n' 'Usage: install-macos.sh [--client codex|claude-code] [--skills-dir DIR] [--update]' 'Offline: also supply --archive FILE --sha256 HEX'; return;; *) fail "Unknown option: $1";; esac done case "$client" in codex|claude-code) ;; *) fail 'Client must be codex or claude-code.';; esac if [ -z "$skills_dir" ]; then if [ "$client" = 'claude-code' ]; then skills_dir="${HOME:?No user home}/.claude/skills" elif [ -n "${CODEX_HOME:-}" ]; then skills_dir="${CODEX_HOME%/}/skills" else skills_dir="${HOME:?No user home}/.agents/skills"; fi fi [ -z "$supplied_hash" ] || [ -n "$archive_path" ] || fail '--sha256 requires --archive.' [ -z "$archive_path" ] || [ -n "$supplied_hash" ] || fail '--archive requires --sha256.' for tool in /usr/bin/unzip /usr/bin/ditto /usr/bin/shasum /usr/bin/mktemp /usr/bin/sort /usr/bin/diff; do [ -x "$tool" ] || fail "Required macOS tool missing: $tool"; done installer_work=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/squirrelly-installer.XXXXXX") local bundle="$installer_work/squirrelly-skill.zip" expected_hash='' manifest_name='' manifest_extra='' actual_hash='' if [ -n "$archive_path" ]; then [ -f "$archive_path" ] || fail 'Archive does not exist.' /bin/cp "$archive_path" "$bundle"; expected_hash=$supplied_hash else command -v curl >/dev/null 2>&1 || fail 'curl is required.' curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 90 --max-filesize 5242880 --output "$bundle" 'https://mcp.squirrelly.ai/downloads/squirrelly-skill.zip' curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 30 --max-filesize 512 --output "$installer_work/checksum" 'https://mcp.squirrelly.ai/downloads/squirrelly-skill.zip.sha256' read -r expected_hash manifest_name manifest_extra < "$installer_work/checksum" || fail 'Invalid checksum manifest.' [ "$manifest_name" = 'squirrelly-skill.zip' ] && [ -z "$manifest_extra" ] || fail 'Invalid checksum manifest.' fi [[ "$expected_hash" =~ ^[[:xdigit:]]{64}$ ]] || fail 'Invalid SHA256 value.' [ "$(/usr/bin/wc -c < "$bundle")" -le 5242880 ] || fail 'Oversized archive.' actual_hash=$(/usr/bin/shasum -a 256 "$bundle" | /usr/bin/awk '{print $1}') expected_hash=$(printf '%s' "$expected_hash" | /usr/bin/tr 'A-F' 'a-f') [ "$actual_hash" = "$expected_hash" ] || fail 'SHA256 mismatch; nothing installed.' printf '%s\n' 'squirrelly/SKILL.md' 'squirrelly/INSTALL.md' 'squirrelly/agents/openai.yaml' 'squirrelly/assets/avatar.jpg' 'squirrelly/references/tool-contract.md' 'squirrelly/scripts/amount.mjs' | /usr/bin/sort > "$installer_work/expected" /usr/bin/unzip -Z -1 "$bundle" | /usr/bin/sort > "$installer_work/actual" /usr/bin/diff -q "$installer_work/expected" "$installer_work/actual" >/dev/null || fail 'Unsafe archive entries or duplicates.' /usr/bin/unzip -Z -l "$bundle" | /usr/bin/awk '/ squirrelly\// {n++;if(substr($0,1,1)!="-")bad=1} END {exit (bad||n!=6)}' || fail 'Unsafe archive file type.' local size_summary='' entry_count='' expanded_size='' size_summary=$(/usr/bin/unzip -Z -t "$bundle" | /usr/bin/awk '$2=="files,"&&$4=="bytes"&&$5=="uncompressed," {print $1,$3}') read -r entry_count expanded_size <<< "$size_summary" [[ "$entry_count" = '6' && "$expanded_size" =~ ^[0-9]+$ ]] || fail 'Invalid archive size metadata.' [ "$expanded_size" -le 8388608 ] || fail 'Oversized expanded archive.' # Central-directory sizes can lie. Bound the actual decompressed stream before # writing any extracted files; pipefail also rejects an early-stopped stream. local actual_expanded='' actual_expanded=$(/usr/bin/unzip -p "$bundle" | /usr/bin/head -c 8388609 | /usr/bin/wc -c) || fail 'Oversized or invalid expanded archive.' [ "$actual_expanded" -le 8388608 ] || fail 'Oversized expanded archive.' /usr/bin/unzip -tq "$bundle" >/dev/null || fail 'Archive integrity check failed.' /bin/mkdir -p "$skills_dir" skills_dir=$(cd "$skills_dir" && /bin/pwd -P) installer_target="$skills_dir/squirrelly" if ! /bin/mkdir "$skills_dir/.squirrelly-install.lock" 2>/dev/null; then fail 'Another installation is running, or an installation lock already exists.'; fi installer_lock="$skills_dir/.squirrelly-install.lock" [ ! -L "$installer_target" ] || fail 'Linked destination preserved; no replacement performed.' if [ -e "$installer_target" ] && [ ! -d "$installer_target" ]; then fail 'Existing destination is not a directory; unchanged.'; fi installer_stage=$(/usr/bin/mktemp -d "$skills_dir/.squirrelly-stage.XXXXXX") /usr/bin/ditto -x -k "$bundle" "$installer_stage" while IFS= read -r file; do [ ! -L "$installer_stage/$file" ] && [ -f "$installer_stage/$file" ] || fail 'Unsafe extracted file.' /bin/chmod 644 "$installer_stage/$file" done < "$installer_work/expected" /usr/bin/grep -q '^name: squirrelly$' "$installer_stage/squirrelly/SKILL.md" || fail 'Unexpected skill identity.' /usr/bin/find "$installer_stage/squirrelly" -type d -exec /bin/chmod 755 {} + if [ -d "$installer_target" ]; then if /usr/bin/diff -qr "$installer_target" "$installer_stage/squirrelly" >/dev/null; then installer_done=1; printf 'Already installed: %s\n' "$installer_target"; return fi [ "$update" -eq 1 ] || fail 'Existing skill preserved. To update with a backup, rerun with --update.' local backup_root="$(/usr/bin/dirname "$skills_dir")/bak" backup_slot='' /bin/mkdir -p "$backup_root" backup_slot=$(/usr/bin/mktemp -d "$backup_root/squirrelly-$client-$(/bin/date -u +%Y%m%dT%H%M%SZ).XXXXXX") /bin/rmdir "$backup_slot" /bin/mv "$installer_target" "$backup_slot"; installer_backup=$backup_slot fi [ ! -e "$installer_target" ] && [ ! -L "$installer_target" ] || fail 'Destination changed; installation stopped.' /bin/mv "$installer_stage/squirrelly" "$installer_target" installer_done=1 printf 'Installed: %s\n' "$installer_target" [ -z "$installer_backup" ] || printf 'Backup retained: %s\n' "$installer_backup" printf '%s\n' 'Reload your client or open a new session. MCP connection and App OAuth authorization are still required.' } main "$@"