#!/usr/bin/env bash set -euo pipefail log() { printf '[setup] %s\n' "$@" } die() { printf 'Error: %s\n' "$@" >&2 exit 1 } pause() { printf '%s\n' "$@" read -r _ /dev/null 2>&1; then return 0 fi log "Installing Xcode Command Line Tools" if ! xcode-select --install 2>/dev/null; then log "Xcode tools installation already in progress or complete" fi log "Waiting for Xcode Command Line Tools to be available..." local i for i in {1..60}; do xcode-select -p >/dev/null 2>&1 && return 0 sleep 10 done die "Xcode Command Line Tools still not available after 10 minutes." } ensure_homebrew() { if command -v brew >/dev/null 2>&1; then return 0 fi log "Installing Homebrew" /bin/bash -c "$(curl --proto '=https' --tlsv1.2 -fsSL \ https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" export PATH="/opt/homebrew/bin:$PATH" if ! command -v brew >/dev/null 2>&1; then die "Homebrew installed but brew is not available in PATH." fi } brew_install_packages() { log "Installing packages with Homebrew Bundle" brew bundle --no-lock --file=/dev/stdin <<-'EOF' || true brew "curl" brew "ffmpeg" brew "git" brew "gnupg" brew "lua" brew "mise" brew "pinentry-mac" brew "stow" brew "tig" brew "zsh" cask "1password@7" cask "bartender" cask "firefox@developer-edition" cask "flux-app" cask "font-maple-mono-nf" cask "font-sf-pro" cask "ghostty" cask "hammerspoon" cask "hazeover" cask "libreoffice" cask "macdown" cask "qlcolorcode" cask "qlmarkdown" cask "qlstephen" cask "raycast" cask "the-unarchiver" cask "todoist-app" EOF } ensure_github_ssh_key() { local key_path="$HOME/.ssh/github" local key_comment key_comment=$(git config --global user.email 2>/dev/null || true) key_comment=${key_comment:-"github-key"} if [[ -f "$key_path" ]]; then log "GitHub SSH key already exists at $key_path" return 0 fi log "First, log into GitHub using a passkey." log "Visit: https://github.com/login" log "Use your iPhone or another device with a passkey to authenticate." pause "After logging in, press enter to continue." mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" log "You will be prompted for a passphrase — using one is strongly recommended." log "Generating GitHub SSH key at $key_path" ssh-keygen -t ed25519 -C "$key_comment" -f "$key_path" ssh-add --apple-use-keychain "$key_path" log "Copying public key to clipboard" pbcopy <"$key_path.pub" pause "Add the key at https://github.com/settings/ssh/new then press enter." log "Testing GitHub SSH connection" local ssh_output ssh_output="$(ssh -T -i "$key_path" -o IdentitiesOnly=yes \ -o BatchMode=yes git@github.com 2>&1 || true)" if ! grep -q "successfully authenticated" <<<"$ssh_output"; then die "GitHub SSH connection failed. Verify the key was added." fi log "GitHub SSH connection successful" } ensure_gpg_keys() { if gpg --batch --list-secret-keys --with-colons "$EXPECTED_GPG_FPR" \ >/dev/null 2>&1; then log "GPG key already in keyring." return 0 fi local encoded sec_err sec_err="$(mktemp)" if ! encoded="$(security find-generic-password -a "$USER" \ -s "$GPG_KEYCHAIN_ACCOUNT" -w 2>"$sec_err")"; then local err_msg err_msg="$(<"$sec_err")" rm -f "$sec_err" if [[ "$err_msg" == *"could not be found"* ]]; then log "GPG secret key not found in Keychain Access ($GPG_KEYCHAIN_ACCOUNT)." log "Store it on a trusted machine with:" log " security add-generic-password -a \"\$USER\" -s \"$GPG_KEYCHAIN_ACCOUNT\" \\" log " -w \"\$(gpg --export-secret-keys $EXPECTED_GPG_FPR | base64)\" -U" return 1 fi die "Keychain Access read failed: $err_msg" fi rm -f "$sec_err" printf '%s' "$encoded" | base64 -D | gpg --batch --yes --import \ || die "Failed to import GPG secret key." if ! gpg --batch --list-secret-keys --with-colons "$EXPECTED_GPG_FPR" \ >/dev/null 2>&1; then gpg --batch --yes --delete-secret-and-public-key "$EXPECTED_GPG_FPR" \ 2>/dev/null || true die "Imported key does not match expected fingerprint." fi log "GPG key imported from Keychain Access and verified." } readonly DOTFILES_DIR="$HOME/.dotfiles" readonly REPO_URL="https://github.com/majjoha/dotfiles.git" ensure_xcode_cli if [[ ! -d "$DOTFILES_DIR/.git" ]]; then if [[ -d "$DOTFILES_DIR" ]] \ && find "$DOTFILES_DIR" -mindepth 1 -maxdepth 1 -print -quit \ | grep -q .; then die "DOTFILES_DIR ($DOTFILES_DIR) exists and is not empty." fi log "Cloning dotfiles into $DOTFILES_DIR" git clone "$REPO_URL" "$DOTFILES_DIR" fi cd "$DOTFILES_DIR" || die "Failed to enter $DOTFILES_DIR" ensure_homebrew brew_install_packages if command -v mise >/dev/null 2>&1; then log "Trusting dotfiles directory with mise" if ! mise trust "$DOTFILES_DIR"; then die "mise trust failed. Check permissions and try again." fi fi if [[ -f .gitmodules ]]; then log "Updating git submodules" git submodule update --init --recursive fi xdg_state=${XDG_STATE_HOME:-"$HOME/.local/state"} xdg_cache=${XDG_CACHE_HOME:-"$HOME/.cache"} xdg_data=${XDG_DATA_HOME:-"$HOME/.local/share"} log "Ensuring XDG directories" mkdir -p -- \ "$xdg_state/zsh" \ "$xdg_cache/zsh" \ "$xdg_data/zlua" log "Stowing dotfiles" stow --restow . if confirm "Setup GitHub SSH key?"; then ensure_github_ssh_key fi if confirm "Import GPG secret key from Keychain Access?"; then ensure_gpg_keys || true fi if command -v mise >/dev/null 2>&1; then local_github_token="" if [[ -z "${MISE_GITHUB_TOKEN:-}" ]] && \ [[ -z "${GITHUB_TOKEN:-}" ]] && \ [[ -z "${GITHUB_API_TOKEN:-}" ]]; then log "GitHub token not found. Setting a token avoids rate limiting." log "Create a token at: https://github.com/settings/tokens/new" log "No scopes are required." pause "After creating the token, press enter to continue." printf 'Paste your GitHub token (input will be hidden): ' read -rs local_github_token