Skip to content

Instantly share code, notes, and snippets.

@shedali
Last active July 1, 2026 15:12
Show Gist options
  • Select an option

  • Save shedali/38c78cbf3d42855a83a44f54bc1cc2ab to your computer and use it in GitHub Desktop.

Select an option

Save shedali/38c78cbf3d42855a83a44f54bc1cc2ab to your computer and use it in GitHub Desktop.
bootstrap
#!/bin/bash
# Run this on a new Mac:
# curl -fsSL https://gist.githubusercontent.com/shedali/38c78cbf3d42855a83a44f54bc1cc2ab/raw/bootstrap.sh | bash
set -e
# Ensure Xcode Command Line Tools are installed AND usable before anything that
# needs a compiler/git (Homebrew, nix-darwin casks). A bare `xcode-select -p` can
# succeed even when the tools are missing/broken, so also verify `clang` resolves.
if ! xcode-select -p &>/dev/null || ! /usr/bin/xcrun --find clang &>/dev/null; then
echo "Installing Xcode Command Line Tools (required before Homebrew)..."
# Prefer a headless install via softwareupdate (no GUI dialog to babysit).
# The placeholder file makes the CLT package show up in `softwareupdate -l`.
CLT_PLACEHOLDER="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
touch "$CLT_PLACEHOLDER"
CLT_LABEL=$(softwareupdate -l 2>/dev/null \
| grep -E 'Command Line Tools' \
| awk -F'Label: ' '/Label:/ {print $2}' \
| sort -V | tail -n1)
if [ -n "$CLT_LABEL" ]; then
echo "Installing '$CLT_LABEL' via softwareupdate..."
softwareupdate -i "$CLT_LABEL" --verbose || true
else
# Fall back to the GUI installer if the package label can't be found.
xcode-select --install &>/dev/null || true
echo "Complete the Xcode Command Line Tools dialog if it appears; waiting for it to finish..."
fi
rm -f "$CLT_PLACEHOLDER"
# Block until the tools are actually usable (survives GUI-driven installs too).
until xcode-select -p &>/dev/null && /usr/bin/xcrun --find clang &>/dev/null; do
echo "Waiting for Command Line Tools to finish installing..."
sleep 15
done
echo "✓ Xcode Command Line Tools installed"
fi
NIX_BIN="/nix/var/nix/profiles/default/bin/nix"
if ! "$NIX_BIN" --version &>/dev/null 2>&1; then
# Before installing, check if an APFS Nix Store volume already exists.
# If it does, the volume is likely locked/unmounted (daemon not running) rather
# than truly absent. Attempting to reinstall in this state triggers a broken
# uninstall flow. Abort and let the user fix the daemon instead.
if /usr/sbin/diskutil list 2>/dev/null | grep -q "Nix Store"; then
echo ""
echo "ERROR: A Nix Store APFS volume already exists but Nix is not accessible." >&2
echo "The volume is likely encrypted and not mounted (daemon not running)." >&2
echo "" >&2
echo "To fix, try one of:" >&2
echo " 1. Load the Nix daemon LaunchDaemon:" >&2
echo " sudo launchctl load /Library/LaunchDaemons/systems.determinate-nix-installer.nix-hook.plist" >&2
echo " 2. If the daemon plist is missing, delete the volume and reinstall:" >&2
echo " VOLUME=\$(/usr/sbin/diskutil list | awk '/Nix Store/{print \$NF}')" >&2
echo " sudo /usr/sbin/diskutil apfs deleteVolume \"\$VOLUME\"" >&2
echo " curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate" >&2
echo "" >&2
exit 1
fi
echo "Nix not found, installing..."
curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate
echo "Nix installed, sourcing environment..."
# Source Nix to make it available in current shell
if [ -e "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]; then
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
fi
fi
echo "Nix is available, continuing setup..."
# Ask if user wants full setup or minimal (nix-darwin only)
echo ""
echo "Setup options:"
echo " Full: Clone repos (home-manager, nvim, etc.) + nix-darwin (requires GitHub auth)"
echo " Minimal: Just apply nix-darwin configuration (no GitHub auth needed)"
echo ""
set +e
setup_mode=$("$NIX_BIN" shell nixpkgs#gum --command gum choose "Full setup" "Minimal (nix-darwin only)" </dev/tty 2>/dev/tty)
gum_mode_exit=$?
set -e
if [ $gum_mode_exit -ne 0 ] || [ -z "$setup_mode" ]; then
echo " 1) Full setup"
echo " 2) Minimal (nix-darwin only)"
read -p "Enter choice (1 or 2): " mode_choice </dev/tty
case "$mode_choice" in
1) setup_mode="Full setup" ;;
2) setup_mode="Minimal (nix-darwin only)" ;;
*)
echo "ERROR: Invalid choice" >&2
exit 1
;;
esac
fi
# If minimal, skip to nix-darwin setup
if [ "$setup_mode" = "Minimal (nix-darwin only)" ]; then
echo ""
echo "Skipping GitHub auth and repo cloning..."
echo ""
# Jump to profile selection
set +e
setup_type=$("$NIX_BIN" shell nixpkgs#gum --command gum choose "Personal" "Work" "Air" "Mini" </dev/tty 2>/dev/tty)
gum_exit_code=$?
set -e
if [ $gum_exit_code -ne 0 ] || [ -z "$setup_type" ]; then
echo "Select setup type:"
echo " 1) Personal"
echo " 2) Work"
echo " 3) Air"
echo " 4) Mini"
read -p "Enter choice (1-4): " choice </dev/tty
case "$choice" in
1) setup_type="Personal" ;;
2) setup_type="Work" ;;
3) setup_type="Air" ;;
4) setup_type="Mini" ;;
*)
echo "ERROR: Invalid choice" >&2
exit 1
;;
esac
fi
profile_name=$(echo "$setup_type" | tr '[:upper:]' '[:lower:]')
# Map friendly names to actual flake attribute names
case "$profile_name" in
work) profile_name="chasevm" ;;
esac
echo "Applying $setup_type nix-darwin configuration..."
echo "You will be prompted for your sudo password..."
# Back up existing /etc files that nix-darwin needs to manage
if [ -f /etc/zshrc ] && [ ! -f /etc/zshrc.before-nix-darwin ]; then
echo "Backing up /etc/zshrc to /etc/zshrc.before-nix-darwin..."
sudo mv /etc/zshrc /etc/zshrc.before-nix-darwin
fi
if [ -f /etc/zprofile ] && [ ! -f /etc/zprofile.before-nix-darwin ]; then
echo "Backing up /etc/zprofile to /etc/zprofile.before-nix-darwin..."
sudo mv /etc/zprofile /etc/zprofile.before-nix-darwin
fi
if [ -f /etc/zshenv ] && [ ! -f /etc/zshenv.before-nix-darwin ]; then
echo "Backing up /etc/zshenv to /etc/zshenv.before-nix-darwin..."
sudo mv /etc/zshenv /etc/zshenv.before-nix-darwin
fi
sudo "$NIX_BIN" run nix-darwin -- switch --flake "github:shedali/nix-darwin#${profile_name}" --refresh
echo "$setup_type setup complete!"
# Chase profiles need the imperative Chase/fincloud setup (GitHub Enterprise
# login, MDM, fincloud-config sync) which isn't part of nix-darwin. Offer it in
# Minimal mode too so a Work/chasevm machine isn't left half-configured.
if [[ "$profile_name" == "chasevm" || "$profile_name" == "chasehost" ]]; then
echo ""
if "$NIX_BIN" shell nixpkgs#gum --command gum confirm "Chase profile selected — run Chase-specific setup (chase-setup.sh) now?" </dev/tty; then
if [ -f ~/.config/home-manager/chase-setup.sh ]; then
cs=~/.config/home-manager/chase-setup.sh
else
curl -fsSL https://gist.githubusercontent.com/shedali/7f96ef92ead665e7cfc2f7652cb0b179/raw/chase-setup.sh -o /tmp/chase-setup.sh && chmod +x /tmp/chase-setup.sh && cs=/tmp/chase-setup.sh
fi
[ -n "${cs:-}" ] && bash "$cs" </dev/tty
else
echo "Skipping Chase setup. Run it later: bash ~/.config/home-manager/chase-setup.sh"
fi
fi
exit 0
fi
echo "Proceeding with full setup..."
# Check if already authenticated with GitHub before entering nix shell
export GH_AUTHENTICATED=false
if [ -f ~/.config/gh/hosts.yml ] && grep -q "github.com" ~/.config/gh/hosts.yml 2>/dev/null; then
echo "✓ Found existing GitHub authentication"
export GH_AUTHENTICATED=true
fi
# Write sync_or_clone_repo to temp file for reuse across nix shell subshells
_SYNC_FUNC=/tmp/bootstrap-sync-func.sh
cat > "$_SYNC_FUNC" << 'SYNC_FUNC_EOF'
sync_or_clone_repo() {
local repo=$1
local target_dir=$2
if [ -d "$target_dir/.git" ]; then
echo "Repository exists at $target_dir, syncing..."
cd "$target_dir"
# Check for unmerged paths (merge conflicts)
if ! git diff --check &>/dev/null && git ls-files -u | grep -q .; then
echo "ERROR: Repository has unmerged files (merge conflicts)"
echo "Please resolve conflicts manually in $target_dir"
cd - >/dev/null
return 1
fi
# Check for any changes (tracked, untracked, or modified)
local has_changes=false
if ! git diff-index --quiet HEAD -- 2>/dev/null || [ -n "$(git ls-files --others --exclude-standard)" ]; then
echo "Stashing local changes (including untracked files)..."
git stash push -u -m "bootstrap.sh auto-stash $(date +%Y-%m-%d_%H-%M-%S)"
has_changes=true
fi
# Sync with remote
echo "Pulling latest changes..."
git pull --rebase || {
echo "WARNING: Failed to pull changes, repository may have conflicts"
if [ "$has_changes" = true ]; then
echo "Attempting to pop stash..."
git stash pop || echo "WARNING: Could not auto-pop stash, run 'git stash pop' manually later"
fi
cd - >/dev/null
return 1
}
# Pop stash if we created one
if [ "$has_changes" = true ]; then
echo "Restoring stashed changes..."
git stash pop || echo "WARNING: Could not auto-pop stash, changes are still in stash. Run 'git stash list' to see them."
fi
cd - >/dev/null
else
echo "Cloning $repo to $target_dir..."
if [ -d "$target_dir" ]; then
echo "WARNING: Directory exists but is not a git repo, removing..."
/bin/rm -rf "$target_dir"
fi
gh repo clone "$repo" "$target_dir"
fi
}
SYNC_FUNC_EOF
# Use nix shell to temporarily get gh for authentication and repo management
"$NIX_BIN" shell nixpkgs#gh nixpkgs#git --command bash -c '
source /tmp/bootstrap-sync-func.sh
# Only authenticate if not already logged in
mkdir -p ~/.config/gh
if [ -f ~/.config/gh/hosts.yml ] && grep -q "github.com" ~/.config/gh/hosts.yml 2>/dev/null; then
echo "Using existing GitHub authentication..."
elif ! gh auth status &>/dev/null; then
gh auth login
fi
# Inject token directly into git URL config so git never prompts for credentials
GH_TOKEN=$(gh auth token 2>/dev/null)
if [ -n "$GH_TOKEN" ]; then
git config --global url."https://oauth2:${GH_TOKEN}@github.com/".insteadOf "https://github.com/"
echo "✓ GitHub token configured for git"
else
echo "WARNING: Could not get GitHub token, git operations may prompt for credentials"
fi
sync_or_clone_repo shedali/home-manager ~/.config/home-manager
sync_or_clone_repo shedali/nvim ~/.config/nvim
'
# Use a unique backup extension per run so a retried bootstrap never fails on a
# pre-existing *.backup (home-manager refuses "would be clobbered by backup").
# gh (and HM's own gh activation) rewrites ~/.config/gh/config.yml as a real file,
# so this backup is expected on every run.
cd ~/.config/home-manager && nix run home-manager/master -- switch -b "backup-$(date +%Y%m%d%H%M%S)"
# Clone additional personal repos after home-manager creates directory structure
echo "Cloning additional personal repositories..."
"$NIX_BIN" shell nixpkgs#gh nixpkgs#git --command bash -c '
source /tmp/bootstrap-sync-func.sh
sync_or_clone_repo shedali/blog ~/dev/shedali/writing/blog
sync_or_clone_repo shedali/text-blog ~/dev/shedali/writing/text-blog
sync_or_clone_repo shedali/citations ~/dev/shedali/writing/citations
sync_or_clone_repo shedali/cv ~/dev/shedali/cv
'
rm -f /tmp/bootstrap-sync-func.sh
# Source the new profile to make newly installed packages available
if [ -f ~/.nix-profile/etc/profile.d/hm-session-vars.sh ]; then
. ~/.nix-profile/etc/profile.d/hm-session-vars.sh
fi
echo "Installing neovim plugins..."
nvim --headless +'lua require("lazy").sync({wait=true})' +qall
echo "Setup complete! Opening new terminal for verification..."
osascript -e 'tell application "Terminal" to do script ""'
# Ask which profile to use (now available after home-manager switch)
echo ""
echo "Select nix-darwin profile to apply..."
# Available profiles
PROFILES="personal
mini
chasehost
air
chasevm"
# Try gum first, fall back to read if it fails
set +e
selected_profile=$(echo "$PROFILES" | gum choose --header "Select nix-darwin profile:" </dev/tty 2>/dev/tty)
gum_exit_code=$?
set -e
if [ $gum_exit_code -eq 0 ] && [ -n "$selected_profile" ]; then
echo "Profile selected: $selected_profile"
else
echo "Select profile:"
echo " 1) personal - Full personal setup"
echo " 2) mini - Mac mini server"
echo " 3) chasehost - Chase work host machine"
echo " 4) air - MacBook Air portable"
echo " 5) chasevm - Chase virtual machines"
read -p "Enter choice (1-5): " choice </dev/tty
case "$choice" in
1) selected_profile="personal" ;;
2) selected_profile="mini" ;;
3) selected_profile="chasehost" ;;
4) selected_profile="air" ;;
5) selected_profile="chasevm" ;;
*)
echo "ERROR: Invalid choice" >&2
exit 1
;;
esac
echo "Profile selected: $selected_profile"
fi
# Back up existing /etc files that nix-darwin needs to manage
if [ -f /etc/zshrc ] && [ ! -f /etc/zshrc.before-nix-darwin ]; then
echo "Backing up /etc/zshrc to /etc/zshrc.before-nix-darwin..."
sudo mv /etc/zshrc /etc/zshrc.before-nix-darwin
fi
if [ -f /etc/zprofile ] && [ ! -f /etc/zprofile.before-nix-darwin ]; then
echo "Backing up /etc/zprofile to /etc/zprofile.before-nix-darwin..."
sudo mv /etc/zprofile /etc/zprofile.before-nix-darwin
fi
if [ -f /etc/zshenv ] && [ ! -f /etc/zshenv.before-nix-darwin ]; then
echo "Backing up /etc/zshenv to /etc/zshenv.before-nix-darwin..."
sudo mv /etc/zshenv /etc/zshenv.before-nix-darwin
fi
# Apply the selected nix-darwin configuration
echo ""
echo "Applying nix-darwin configuration: $selected_profile"
echo "You will be prompted for your sudo password..."
if ! sudo "$NIX_BIN" run nix-darwin -- switch --flake "github:shedali/nix-darwin#${selected_profile}" --refresh; then
echo ""
echo "ERROR: Failed to apply nix-darwin configuration" >&2
echo "You may need to run this manually later:"
echo " sudo $NIX_BIN run nix-darwin -- switch --flake github:shedali/nix-darwin#${selected_profile} --refresh"
exit 1
fi
echo "✓ nix-darwin $selected_profile configuration applied successfully"
# Run Chase-specific setup for chase profiles
if [[ "$selected_profile" == "chasevm" || "$selected_profile" == "chasehost" ]]; then
echo ""
echo "========================================"
echo "Starting Chase work environment setup..."
echo "========================================"
echo ""
# Setup 1Password
echo "Step 1/3: Setting up 1Password..."
echo "1Password app should be installed via nix-darwin configuration"
echo "Now you need to sign in to 1Password and authenticate the CLI"
# Check if user wants to setup 1Password
setup_1password=false
set +e
if gum confirm "Open 1Password app to sign in?" </dev/tty 2>/dev/tty; then
setup_1password=true
else
gum_exit=$?
if [ $gum_exit -ne 0 ]; then
# gum failed, fall back to read
read -p "Open 1Password app to sign in? (y/n): " answer </dev/tty
if [[ "$answer" =~ ^[Yy] ]]; then
setup_1password=true
fi
fi
fi
set -e
if [ "$setup_1password" = "true" ]; then
# Check if 1Password app exists
if [ ! -e "/Applications/1Password.app" ]; then
echo "ERROR: 1Password app not found!"
echo "Ensure 1Password is included in your nix-darwin configuration"
if ! { gum confirm "Continue without 1Password setup?" </dev/tty 2>/dev/tty || { read -p "Continue without 1Password setup? (y/n): " answer </dev/tty && [[ "$answer" =~ ^[Yy] ]]; }; }; then
exit 1
fi
fi
if [ -e "/Applications/1Password.app" ]; then
open -a "1Password"
echo "Please sign in to 1Password with your Chase account"
if gum confirm "1Password sign-in complete?" </dev/tty 2>/dev/tty || { read -p "1Password sign-in complete? (y/n): " answer </dev/tty && [[ "$answer" =~ ^[Yy] ]]; }; then
echo "1Password app setup confirmed"
# Authenticate op CLI with the 1Password app
echo "Now authenticating 1Password CLI (op)..."
echo "The op CLI will connect to the 1Password app you just signed into"
if op account list &>/dev/null; then
echo "op CLI already authenticated"
else
# The op CLI should automatically connect to the 1Password app
if op account get &>/dev/null 2>&1; then
echo "op CLI connected to 1Password app successfully"
else
echo "op CLI authentication may need manual setup"
echo "If prompted, follow the op CLI authentication flow"
eval $(op signin) || echo "op signin had issues, but continuing..."
fi
fi
fi
fi
fi
# Setup sudo password caching
echo ""
echo "Step 2/3: Setting up sudo password caching with 1Password..."
echo ""
# Select which password to use
echo "Which password should be used for sudo commands throughout this setup?"
set +e
password_choice=$(gum choose \
"Default Password (before MDM/Jamf Connect setup)" \
"System Password (after MDM/Jamf Connect setup)" </dev/tty 2>/dev/tty)
gum_pwd_exit=$?
set -e
if [ $gum_pwd_exit -eq 0 ] && [ -n "$password_choice" ]; then
echo "Password selected: $password_choice"
else
echo "Select password:"
echo " 1) Default Password (before MDM/Jamf Connect setup)"
echo " 2) System Password (after MDM/Jamf Connect setup)"
read -p "Enter choice (1 or 2): " choice </dev/tty
case "$choice" in
1) password_choice="Default Password (before MDM/Jamf Connect setup)" ;;
2) password_choice="System Password (after MDM/Jamf Connect setup)" ;;
*)
echo "WARNING: Invalid choice, skipping sudo caching"
password_choice=""
;;
esac
fi
if [ -n "$password_choice" ]; then
case "$password_choice" in
"Default Password (before MDM/Jamf Connect setup)")
SUDO_PASSWORD_REF="op://Chase/JPMorgan Chase/Bootstrapping/default password"
;;
"System Password (after MDM/Jamf Connect setup)")
SUDO_PASSWORD_REF="op://Chase/JPMorgan Chase/Bootstrapping/system password"
;;
esac
fi
# Cache the sudo password
if [ -n "$SUDO_PASSWORD_REF" ] && command -v op &>/dev/null && op account get &>/dev/null; then
echo "Retrieving sudo password from 1Password..."
echo "Using: ${SUDO_PASSWORD_REF}"
if op read "$SUDO_PASSWORD_REF" | sudo -S -v 2>/dev/null; then
echo "✓ Sudo credentials cached successfully"
echo "All subsequent sudo commands will use this cached password"
# Keep sudo session alive in background
(
while true; do
sleep 50
sudo -n true 2>/dev/null || break
done
) &
SUDO_REFRESH_PID=$!
# Setup trap to kill background process on exit
trap "kill $SUDO_REFRESH_PID 2>/dev/null || true" EXIT
else
echo "WARNING: Failed to authenticate with 1Password password"
echo "You may need to enter your password manually for sudo commands"
fi
else
if [ -z "$SUDO_PASSWORD_REF" ]; then
echo "WARNING: No password selected, sudo will prompt for password normally"
else
echo "WARNING: 1Password CLI not available, sudo will prompt for password normally"
fi
fi
# Now run chase-setup.sh for Chase-specific configuration
echo ""
echo "Step 3/3: Running Chase-specific setup (chase-setup.sh)..."
# Check if local copy exists (for development), otherwise download from gist
if [ -f ~/.config/home-manager/chase-setup.sh ]; then
echo "Using local chase-setup.sh from ~/.config/home-manager/"
chase_setup_script=~/.config/home-manager/chase-setup.sh
else
echo "Downloading chase-setup.sh from gist..."
if curl -fsSL https://gist.githubusercontent.com/shedali/7f96ef92ead665e7cfc2f7652cb0b179/raw/chase-setup.sh -o /tmp/chase-setup.sh; then
echo "chase-setup.sh downloaded successfully"
chmod +x /tmp/chase-setup.sh
chase_setup_script=/tmp/chase-setup.sh
else
echo "ERROR: Failed to download chase-setup.sh" >&2
exit 1
fi
fi
echo "Running chase-setup.sh..."
bash "$chase_setup_script"
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Chase setup completed successfully!"
elif [ $exit_code -eq 130 ]; then
echo "Chase setup was cancelled by user"
exit 0
else
echo "ERROR: chase-setup.sh failed with exit code $exit_code" >&2
echo "You can try running it manually: bash $chase_setup_script"
exit $exit_code
fi
else
echo ""
echo "$selected_profile setup complete!"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment