Last active
November 20, 2025 14:43
-
-
Save shedali/38c78cbf3d42855a83a44f54bc1cc2ab to your computer and use it in GitHub Desktop.
bootstrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Run this on a new Mac: | |
| # curl -fsSL https://gist.githubusercontent.com/shedali/38c78cbf3d42855a83a44f54bc1cc2ab/raw/bootstrap.sh | bash | |
| set -e | |
| # Install Xcode Command Line Tools if not present | |
| if ! xcode-select -p &>/dev/null; then | |
| echo "Installing Xcode Command Line Tools..." | |
| xcode-select --install | |
| echo "Please complete the Xcode Command Line Tools installation in the dialog, then re-run this script." | |
| exit 0 | |
| fi | |
| if ! command -v nix &>/dev/null; then | |
| 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..." | |
| # Function to sync or clone a git repository | |
| 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 | |
| } | |
| # Use nix shell to temporarily get gh for authentication and repo management | |
| /nix/var/nix/profiles/default/bin/nix shell nixpkgs#gh nixpkgs#git --command bash -c ' | |
| # Only authenticate if not already logged in | |
| if ! gh auth status &>/dev/null; then | |
| gh auth login | |
| fi | |
| # Define the sync_or_clone_repo function in subshell | |
| 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_or_clone_repo shedali/home-manager ~/.config/home-manager | |
| sync_or_clone_repo shedali/nvim ~/.config/nvim | |
| ' | |
| cd ~/.config/home-manager && nix run home-manager/master -- switch -b backup | |
| # Clone additional personal repos after home-manager creates directory structure | |
| echo "Cloning additional personal repositories..." | |
| /nix/var/nix/profiles/default/bin/nix shell nixpkgs#gh nixpkgs#git --command bash -c ' | |
| # Define the sync_or_clone_repo function in subshell | |
| 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_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 | |
| ' | |
| # 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 if this is a work or personal setup using gum (now available after home-manager switch) | |
| echo "" | |
| echo "DEBUG: About to prompt for setup type..." | |
| # Try gum first, fall back to read if it fails | |
| # Temporarily disable 'set -e' to prevent exit on gum failure | |
| # Redirect both stdin and stderr to /dev/tty so gum UI is visible | |
| set +e | |
| setup_type=$(gum choose "Personal" "Work" </dev/tty 2>/dev/tty) | |
| gum_exit_code=$? | |
| set -e | |
| echo "DEBUG: gum choose returned exit code: $gum_exit_code, selected: '$setup_type'" | |
| if [ $gum_exit_code -eq 0 ] && [ -n "$setup_type" ]; then | |
| echo "DEBUG: Setup type selected via gum: '$setup_type'" | |
| else | |
| echo "DEBUG: gum choose failed, falling back to read prompt" | |
| echo "Select setup type:" | |
| echo " 1) Personal" | |
| echo " 2) Work" | |
| read -p "Enter choice (1 or 2): " choice </dev/tty | |
| case "$choice" in | |
| 1) setup_type="Personal" ;; | |
| 2) setup_type="Work" ;; | |
| *) | |
| echo "ERROR: Invalid choice" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "DEBUG: Setup type selected via read: '$setup_type'" | |
| fi | |
| if [ "$setup_type" = "Work" ]; then | |
| echo "DEBUG: Entering Work setup block..." | |
| echo "" | |
| echo "========================================" | |
| echo "Starting Chase work environment setup..." | |
| echo "========================================" | |
| echo "" | |
| # Apply work nix-darwin configuration | |
| echo "Step 1/4: Applying work nix-darwin configuration..." | |
| echo "This will install 1Password, Docker, Ghostty, Chrome, Slack, and VS Code" | |
| 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 | |
| echo "DEBUG: About to run nix-darwin command..." | |
| if ! sudo nix run nix-darwin -- switch --flake github:shedali/nix-darwin#work --refresh; then | |
| echo "DEBUG: nix-darwin command failed with exit code $?" | |
| echo "" | |
| echo "ERROR: Failed to apply nix-darwin work configuration" >&2 | |
| echo "You may need to run this manually later:" | |
| echo " sudo nix run nix-darwin -- switch --flake github:shedali/nix-darwin#work --refresh" | |
| exit 1 | |
| fi | |
| echo "✓ nix-darwin work configuration applied successfully" | |
| echo "" | |
| # Setup 1Password | |
| echo "Step 2/4: Setting up 1Password..." | |
| echo "1Password app should be installed via nix-darwin work 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 work configuration (github:shedali/nix-darwin#work)" | |
| 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 3/4: 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 "DEBUG: Password selected via gum: '$password_choice'" | |
| else | |
| echo "DEBUG: gum choose failed for password, falling back to read prompt" | |
| 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 | |
| echo "DEBUG: Password selected via read: '$password_choice'" | |
| 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 4/4: 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 "Applying personal 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 | |
| sudo nix run nix-darwin -- switch --flake github:shedali/nix-darwin#personal --refresh | |
| echo "Personal setup complete!" | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment