dotfiles

My dotfiles and configs
git clone https://git.dasho.dev/dotfiles.git
Log | Files | Refs | README

wizard (25446B)


      1 #!/usr/bin/env zsh
      2 # Dasho's Dotfiles Wizard 🧙
      3 # A magical, self-contained interactive wizard for managing dotfiles
      4 # Works on fresh Linux/macOS systems with just internet access
      5 
      6 set -eo pipefail
      7 
      8 # Color Palette (Soft Gradients & Pastels)
      9 # ----------------------------------------
     10 # 183 - Soft Lavender (headers, prompts)
     11 # 219 - Light Pink (selected items, cursor)
     12 # 147 - Muted Purple (list items)
     13 # 117 - Pale Cyan (info messages)
     14 # 114 - Soft Green (success messages)
     15 # 210 - Coral (error messages)
     16 # 222 - Peach (warnings)
     17 # 242 - Warm Gray (subtle text)
     18 
     19 # Colors for fallback (before gum is available)
     20 RED='\033[0;31m'
     21 GREEN='\033[0;32m'
     22 YELLOW='\033[1;33m'
     23 BLUE='\033[0;34m'
     24 MAGENTA='\033[0;35m'
     25 CYAN='\033[0;36m'
     26 BOLD='\033[1m'
     27 RESET='\033[0m'
     28 
     29 # Paths
     30 SCRIPT_DIR="$(cd "$(dirname "${(%):-%x}")" && pwd)"
     31 ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
     32 PRIVATE_DIR="$ROOT/private"
     33 
     34 # ============================================================================
     35 # SELF-BOOTSTRAP: Install wizard dependencies
     36 # ============================================================================
     37 
     38 has_command() {
     39   command -v "$1" >/dev/null 2>&1
     40 }
     41 
     42 install_homebrew() {
     43   echo "${BLUE}📦 Installing Homebrew...${RESET}"
     44   if [[ "$OSTYPE" == "darwin"* ]]; then
     45     /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
     46     if [[ -x /opt/homebrew/bin/brew ]]; then
     47       eval "$(/opt/homebrew/bin/brew shellenv)"
     48     elif [[ -x /usr/local/bin/brew ]]; then
     49       eval "$(/usr/local/bin/brew shellenv)"
     50     fi
     51   else
     52     # Linux
     53     /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
     54     if [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
     55       eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
     56     fi
     57   fi
     58 }
     59 
     60 wizard_bootstrap() {
     61   echo "${MAGENTA}${BOLD}"
     62   echo "╔════════════════════════════════════════════════════════════╗"
     63   echo "║                                                            ║"
     64   echo "║           🧙  Dasho's Dotfiles Wizard  🧙                  ║"
     65   echo "║                                                            ║"
     66   echo "║         Making magic happen on your machine...             ║"
     67   echo "║                                                            ║"
     68   echo "╚════════════════════════════════════════════════════════════╝"
     69   echo "${RESET}"
     70   echo ""
     71   echo "${CYAN}Checking for wizard dependencies...${RESET}"
     72   echo ""
     73 
     74   # Check/install Homebrew first
     75   if ! has_command brew; then
     76     echo "${YELLOW}⚠️  Homebrew not found. Installing...${RESET}"
     77     install_homebrew
     78   else
     79     echo "${GREEN}✓${RESET} Homebrew"
     80   fi
     81 
     82   # Install essential tools
     83   local tools=("gum" "glow" "age" "jq")
     84   local to_install=()
     85 
     86   for tool in "${tools[@]}"; do
     87     if ! has_command "$tool"; then
     88       echo "${YELLOW}⚠️  $tool not found${RESET}"
     89       to_install+=("$tool")
     90     else
     91       echo "${GREEN}✓${RESET} $tool"
     92     fi
     93   done
     94 
     95   if [[ ${#to_install[@]} -gt 0 ]]; then
     96     echo ""
     97     echo "${BLUE}📦 Installing missing tools: ${to_install[*]}${RESET}"
     98     brew install "${to_install[@]}"
     99     echo ""
    100     echo "${GREEN}✓ All wizard dependencies installed!${RESET}"
    101   else
    102     echo ""
    103     echo "${GREEN}✓ All wizard dependencies present!${RESET}"
    104   fi
    105 
    106   echo ""
    107   sleep 1
    108 }
    109 
    110 # ============================================================================
    111 # GUM-POWERED UI HELPERS
    112 # ============================================================================
    113 
    114 show_header() {
    115   clear
    116   gum style \
    117     --border double \
    118     --border-foreground 183 \
    119     --padding "1 2" \
    120     --margin "1 0" \
    121     --align center \
    122     --foreground 219 \
    123     "🧙  Dasho's Dotfiles Wizard  🧙" \
    124     "" \
    125     "Your magical companion for dotfiles mastery"
    126 }
    127 
    128 show_success() {
    129   gum style \
    130     --foreground 114 \
    131     "✓ $1"
    132 }
    133 
    134 show_error() {
    135   gum style \
    136     --foreground 210 \
    137     "✗ $1"
    138 }
    139 
    140 show_info() {
    141   gum style \
    142     --foreground 117 \
    143     "ℹ $1"
    144 }
    145 
    146 spinner_run() {
    147   local title="$1"
    148   shift
    149   gum spin --spinner dot --title "$title" -- "$@"
    150 }
    151 
    152 wait_for_key() {
    153   gum style --foreground 242 --italic "Press any key to continue..."
    154   read -k1 -s
    155   return 0
    156 }
    157 
    158 confirm() {
    159   command gum confirm \
    160     --prompt.foreground 183 \
    161     --selected.foreground 219 \
    162     "$@"
    163 }
    164 
    165 # ============================================================================
    166 # ERROR HANDLING WITH RETRY/SKIP OPTIONS
    167 # ============================================================================
    168 
    169 handle_error() {
    170   local error_msg="$1"
    171   local context="$2"
    172   
    173   show_error "$error_msg"
    174   echo ""
    175   
    176   gum style --foreground 183 --italic "What would you like to do?"
    177   
    178   local choice=$(gum choose \
    179     --cursor.foreground 219 \
    180     --selected.foreground 183 \
    181     --cursor "→ " \
    182     "🔄 Retry" \
    183     "⏭️  Skip and continue" \
    184     "🛑 Exit wizard" \
    185     "📋 Show error details")
    186   
    187   case "$choice" in
    188     *Retry*)
    189       return 0
    190       ;;
    191     *Skip*)
    192       show_info "Skipping: $context"
    193       sleep 1
    194       return 1
    195       ;;
    196     *Exit*)
    197       echo ""
    198       gum style --foreground 9 "Exiting wizard. See you next time! 👋"
    199       exit 1
    200       ;;
    201     *details*)
    202       gum style --foreground 8 "$context"
    203       echo ""
    204       wait_for_key
    205       handle_error "$error_msg" "$context"
    206       return $?
    207       ;;
    208   esac
    209 }
    210 
    211 safe_run() {
    212   local description="$1"
    213   shift
    214   
    215   local output
    216   local exit_code
    217   
    218   while true; do
    219     output=$("$@" 2>&1) && exit_code=0 || exit_code=$?
    220     
    221     if [[ $exit_code -eq 0 ]]; then
    222       return 0
    223     else
    224       if handle_error "$description failed" "$output"; then
    225         continue  # Retry
    226       else
    227         return 1  # Skip
    228       fi
    229     fi
    230   done
    231 }
    232 
    233 # ============================================================================
    234 # CORE OPERATIONS
    235 # ============================================================================
    236 
    237 run_bootstrap() {
    238   show_header
    239   gum style --foreground 183 --bold "🚀 Bootstrap: Fresh Machine Setup"
    240   echo ""
    241   
    242   gum style --foreground 183 "This will install Homebrew (if needed), install packages from Brewfile,"
    243   gum style --foreground 183 "and link all your dotfiles to the right places."
    244   echo ""
    245   
    246   if ! confirm "Ready to bootstrap this machine?"; then
    247     return
    248   fi
    249   
    250   echo ""
    251   spinner_run "Installing Homebrew if needed..." bash "$ROOT/bin/bootstrap"
    252   
    253   if [[ $? -eq 0 ]]; then
    254     echo ""
    255     show_success "Bootstrap completed successfully!"
    256     echo ""
    257     gum style --foreground 222 "⚠️  Remember to restart your shell or run: source ~/.zshrc"
    258   else
    259     safe_run "Bootstrap" bash "$ROOT/bin/bootstrap"
    260   fi
    261   
    262   echo ""
    263   wait_for_key
    264 }
    265 
    266 run_backup() {
    267   show_header
    268   gum style --foreground 183 --bold "💾 Backup: Save Brewfile & Package List"
    269   echo ""
    270   
    271   gum style --foreground 183 "Updates Brewfile and brew/leaves.txt with currently installed packages."
    272   echo ""
    273   
    274   if ! confirm "Run backup now?"; then
    275     return
    276   fi
    277   
    278   echo ""
    279   spinner_run "Backing up Brewfile and brew leaves..." bash "$ROOT/bin/backup"
    280   
    281   echo ""
    282   show_success "Backup completed!"
    283   
    284   if has_command glow && [[ -f "$ROOT/Brewfile" ]]; then
    285     echo ""
    286     if confirm "View the updated Brewfile?"; then
    287       glow "$ROOT/Brewfile" -p
    288     fi
    289   fi
    290   
    291   echo ""
    292   wait_for_key
    293 }
    294 
    295 run_backup_secrets() {
    296   show_header
    297   gum style --foreground 183 --bold "🔐 Backup Secrets: Encrypt GPG/SSH/Age/Skate/Oh-My-Zsh"
    298   echo ""
    299   
    300   gum style --foreground 183 "Creates an encrypted archive with:"
    301   gum style --foreground 147 "  • GPG keys"
    302   gum style --foreground 147 "  • SSH keys and config"
    303   gum style --foreground 147 "  • Age identities"
    304   gum style --foreground 147 "  • Skate database"
    305   gum style --foreground 147 "  • Oh-My-Zsh custom configs"
    306   echo ""
    307   
    308   local default_name="keys-$(date +%Y%m%d).tar.gz.age"
    309   local archive_name=$(gum input \
    310     --placeholder "$default_name" \
    311     --prompt "Archive name: " \
    312     --value "$default_name" \
    313     --prompt.foreground 183 \
    314     --cursor.foreground 219)
    315   
    316   [[ -z "$archive_name" ]] && archive_name="$default_name"
    317   
    318   local archive_path="$PRIVATE_DIR/$archive_name"
    319   
    320   echo ""
    321   spinner_run "Creating encrypted backup..." bash "$ROOT/bin/backup-secrets" "$archive_path"
    322   
    323   echo ""
    324   show_success "Secrets backed up to: $archive_name"
    325   
    326   echo ""
    327   wait_for_key
    328 }
    329 
    330 run_restore_secrets() {
    331   show_header
    332   gum style --foreground 183 --bold "🔓 Restore Secrets: Decrypt & Restore"
    333   echo ""
    334   
    335   if [[ ! -d "$PRIVATE_DIR" ]] || [[ -z "$(ls -A "$PRIVATE_DIR"/*.age 2>/dev/null)" ]]; then
    336     show_error "No encrypted archives found in $PRIVATE_DIR"
    337     echo ""
    338     wait_for_key
    339     return
    340   fi
    341   
    342   local archives=($(ls "$PRIVATE_DIR"/*.age 2>/dev/null))
    343   
    344   if [[ ${#archives[@]} -eq 0 ]]; then
    345     show_error "No .age archives found"
    346     echo ""
    347     wait_for_key
    348     return
    349   fi
    350   
    351   gum style --foreground 183 "Select an archive to restore:"
    352   echo ""
    353   
    354   local selected=$(gum choose \
    355     --cursor.foreground 219 \
    356     --selected.foreground 183 \
    357     --cursor "→ " \
    358     "${archives[@]##*/}")
    359   local archive_path="$PRIVATE_DIR/$selected"
    360   
    361   echo ""
    362   gum style --foreground 222 "⚠️  This will restore GPG, SSH, Age, Skate, and Oh-My-Zsh configs"
    363   
    364   if ! confirm "Restore from $selected?"; then
    365     return
    366   fi
    367   
    368   echo ""
    369   spinner_run "Restoring secrets..." bash "$ROOT/bin/restore-secrets" "$archive_path"
    370   
    371   echo ""
    372   show_success "Secrets restored successfully!"
    373   
    374   echo ""
    375   wait_for_key
    376 }
    377 
    378 run_link() {
    379   show_header
    380   gum style --foreground 183 --bold "🔗 Link: Symlink Dotfiles"
    381   echo ""
    382   
    383   gum style --foreground 183 "Creates symlinks for:"
    384   gum style --foreground 147 "  • ~/.zshrc"
    385   gum style --foreground 147 "  • ~/.config/direnv/direnvrc"
    386   gum style --foreground 147 "  • ~/.gitconfig"
    387   gum style --foreground 147 "  • ~/.config/nvim/init.lua"
    388   gum style --foreground 147 "  • ~/.config/redbrick/*"
    389   gum style --foreground 147 "  • ~/,config/yazi/*"
    390   echo ""
    391   
    392   if ! confirm "Create/update symlinks?"; then
    393     return
    394   fi
    395   
    396   echo ""
    397   
    398   mkdir -p "$HOME/.config/direnv"
    399   mkdir -p "$HOME/.config/git"
    400   mkdir -p "$HOME/.config/nvim"
    401   mkdir -p "$HOME/.config/redbrick"
    402   mkdir -p "$HOME/.config/yazi"
    403   
    404   ln -snf "$ROOT/zsh/zshrc" "$HOME/.zshrc" && show_success "Linked ~/.zshrc"
    405   ln -snf "$ROOT/config/direnv/direnvrc" "$HOME/.config/direnv/direnvrc" && show_success "Linked direnvrc"
    406   ln -snf "$ROOT/config/git/config" "$HOME/.gitconfig" && show_success "Linked gitconfig"
    407   ln -snf "$ROOT/config/nvim/init.lua" "$HOME/.config/nvim/init.lua" && show_success "Linked nvim config"
    408   ln -snf "$ROOT/config/redbrick" "$HOME/.config/redbrick" && show_success "Linked redbrick config"
    409   ln -snf "$ROOT/config/yazi/yazi.toml" "$HOME/.config/yazi/yazi.toml" && show_success "Linked yazi config"
    410   
    411   echo ""
    412   show_success "All dotfiles linked!"
    413   
    414   echo ""
    415   wait_for_key
    416 }
    417 
    418 run_check() {
    419   show_header
    420   gum style --foreground 183 --bold "🔍 Check: Verify Dependencies"
    421   echo ""
    422   
    423   local tools=("direnv" "skate" "age" "just" "brew" "gum" "glow" "git" "zsh")
    424   local missing=()
    425   
    426   for tool in "${tools[@]}"; do
    427     if has_command "$tool"; then
    428       show_success "$tool"
    429     else
    430       show_error "$tool (missing)"
    431       missing+=("$tool")
    432     fi
    433   done
    434   
    435   echo ""
    436   
    437   if [[ ${#missing[@]} -eq 0 ]]; then
    438     gum style --foreground 10 --bold "✨ All dependencies present! You're all set!"
    439   else
    440     gum style --foreground 222 "Missing tools: ${missing[*]}"
    441     echo ""
    442     if confirm "Install missing tools via Homebrew?"; then
    443       echo ""
    444       spinner_run "Installing missing tools..." brew install "${missing[@]}"
    445       echo ""
    446       show_success "Tools installed!"
    447     fi
    448   fi
    449   
    450   echo ""
    451   wait_for_key
    452 }
    453 
    454 # ============================================================================
    455 # GIT OPERATIONS
    456 # ============================================================================
    457 
    458 check_git_repo() {
    459   if ! git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    460     show_header
    461     show_error "Not a git repository"
    462     gum style --foreground 147 "The directory $ROOT is not a git repository."
    463     gum style --foreground 147 "Please initialize a repository first."
    464     echo ""
    465     if confirm "Initialize a new git repository here?"; then
    466       git -C "$ROOT" init
    467       show_success "Git repository initialized at $ROOT"
    468     fi
    469     echo ""
    470     wait_for_key
    471     return 1
    472   fi
    473   return 0
    474 }
    475 
    476 git_menu() {
    477   if ! check_git_repo; then
    478     return
    479   fi
    480 
    481   while true; do
    482     show_header
    483     gum style --foreground 183 --bold "🌳 Git Operations"
    484     echo ""
    485     
    486     local choice=$(gum choose \
    487       --cursor.foreground 219 \
    488       --selected.foreground 183 \
    489       --cursor "→ " \
    490       "📊 Status" \
    491       "💾 Commit changes" \
    492       "🔄 Sync (pull + push)" \
    493       "⬆️  Push" \
    494       "⬇️  Pull" \
    495       "🔧 Configure remote" \
    496       "🔑 GitHub authentication" \
    497       "← Back to main menu")
    498     
    499     case "$choice" in
    500       *Status*)
    501         git_status
    502         ;;
    503       *Commit*)
    504         git_commit
    505         ;;
    506       *Sync*)
    507         git_sync
    508         ;;
    509       *Push*)
    510         git_push
    511         ;;
    512       *Pull*)
    513         git_pull
    514         ;;
    515       *remote*)
    516         git_configure_remote
    517         ;;
    518       *authentication*)
    519         github_auth
    520         ;;
    521       *Back*)
    522         break
    523         ;;
    524     esac
    525   done
    526 }
    527 
    528 git_status() {
    529   show_header
    530   gum style --foreground 183 --bold "📊 Git Status"
    531   echo ""
    532   
    533   cd "$ROOT"
    534   git status
    535   
    536   echo ""
    537   wait_for_key
    538 }
    539 
    540 git_commit() {
    541   show_header
    542   gum style --foreground 183 --bold "💾 Commit Changes"
    543   echo ""
    544   
    545   cd "$ROOT"
    546   
    547   local status_output=$(git status --porcelain)
    548   
    549   if [[ -z "$status_output" ]]; then
    550     show_info "No changes to commit"
    551     echo ""
    552     wait_for_key
    553     return
    554   fi
    555   
    556   echo "Changed files:"
    557   git status --short
    558   echo ""
    559   
    560   if ! confirm "Stage all changes?"; then
    561     show_info "Commit cancelled"
    562     echo ""
    563     wait_for_key
    564     return
    565   fi
    566   
    567   git add -A
    568   
    569   echo ""
    570   local commit_msg=$(gum input \
    571     --placeholder "Enter commit message" \
    572     --prompt "Message: " \
    573     --prompt.foreground 183 \
    574     --cursor.foreground 219)
    575   
    576   if [[ -z "$commit_msg" ]]; then
    577     show_error "Commit message required"
    578     echo ""
    579     wait_for_key
    580     return
    581   fi
    582   
    583   echo ""
    584   spinner_run "Committing changes..." git commit -m "$commit_msg"
    585   
    586   echo ""
    587   show_success "Changes committed!"
    588   
    589   echo ""
    590   if confirm "Push to remote?"; then
    591     git_push
    592   else
    593     wait_for_key
    594   fi
    595 }
    596 
    597 git_sync() {
    598   show_header
    599   gum style --foreground 183 --bold "🔄 Sync: Pull + Push"
    600   echo ""
    601   
    602   cd "$ROOT"
    603   
    604   # Check if remote is configured
    605   if ! git remote get-url origin >/dev/null 2>&1; then
    606     show_error "No remote repository configured"
    607     gum style --foreground 147 "Configure a remote first in the Git menu."
    608     echo ""
    609     wait_for_key
    610     return
    611   fi
    612   
    613   if spinner_run "Pulling from remote..." git pull 2>&1; then
    614     show_success "Pulled successfully"
    615   else
    616     echo ""
    617     show_error "Git pull failed"
    618     gum style --foreground 147 "Check your internet connection and authentication."
    619     echo ""
    620     wait_for_key
    621     return
    622   fi
    623   
    624   echo ""
    625   
    626   local status_output=$(git status --porcelain)
    627   
    628   if [[ -n "$status_output" ]]; then
    629     gum style --foreground 222 "You have uncommitted changes."
    630     if confirm "Commit and push?"; then
    631       git_commit
    632       return
    633     fi
    634   fi
    635   
    636   if spinner_run "Pushing to remote..." git push 2>&1; then
    637     echo ""
    638     show_success "Synced successfully!"
    639   else
    640     echo ""
    641     show_error "Git push failed"
    642     gum style --foreground 147 "Check your authentication and permissions."
    643     echo ""
    644     wait_for_key
    645     return
    646   fi
    647   
    648   echo ""
    649   wait_for_key
    650 }
    651 
    652 git_push() {
    653   show_header
    654   gum style --foreground 183 --bold "⬆️  Push to Remote"
    655   echo ""
    656   
    657   cd "$ROOT"
    658   
    659   # Check if remote is configured
    660   if ! git remote get-url origin >/dev/null 2>&1; then
    661     show_error "No remote repository configured"
    662     gum style --foreground 147 "Configure a remote first in the Git menu."
    663     echo ""
    664     wait_for_key
    665     return
    666   fi
    667   
    668   if spinner_run "Pushing to remote..." git push 2>&1; then
    669     echo ""
    670     show_success "Pushed successfully!"
    671   else
    672     echo ""
    673     show_error "Git push failed"
    674     gum style --foreground 147 "Check authentication and permissions."
    675     echo ""
    676     wait_for_key
    677     return
    678   fi
    679   
    680   echo ""
    681   wait_for_key
    682 }
    683 
    684 git_pull() {
    685   show_header
    686   gum style --foreground 183 --bold "⬇️  Pull from Remote"
    687   echo ""
    688   
    689   cd "$ROOT"
    690   
    691   # Check if remote is configured
    692   if ! git remote get-url origin >/dev/null 2>&1; then
    693     show_error "No remote repository configured"
    694     gum style --foreground 147 "Configure a remote first in the Git menu."
    695     echo ""
    696     wait_for_key
    697     return
    698   fi
    699   
    700   if spinner_run "Pulling from remote..." git pull 2>&1; then
    701     echo ""
    702     show_success "Pulled successfully!"
    703   else
    704     echo ""
    705     show_error "Git pull failed"
    706     gum style --foreground 147 "Check your internet connection and authentication."
    707     echo ""
    708     wait_for_key
    709     return
    710   fi
    711   
    712   echo ""
    713   wait_for_key
    714 }
    715 
    716 git_configure_remote() {
    717   show_header
    718   gum style --foreground 183 --bold "🔧 Configure Git Remote"
    719   echo ""
    720   
    721   cd "$ROOT"
    722   
    723   local current_remote=$(git remote get-url origin 2>/dev/null || echo "")
    724   
    725   if [[ -n "$current_remote" ]]; then
    726     gum style "Current remote: $current_remote"
    727     echo ""
    728   else
    729     show_info "No remote configured"
    730     echo ""
    731   fi
    732   
    733   local action=$(gum choose \
    734     --cursor.foreground 219 \
    735     --selected.foreground 183 \
    736     --cursor "→ " \
    737     "➕ Add/Update remote" \
    738     "🗑️  Remove remote" \
    739     "← Back")
    740   
    741   case "$action" in
    742     *Add*)
    743       echo ""
    744       local remote_url=$(gum input \
    745         --placeholder "https://github.com/username/repo.git" \
    746         --prompt "Remote URL: " \
    747         --prompt.foreground 183 \
    748         --cursor.foreground 219)
    749       
    750       if [[ -z "$remote_url" ]]; then
    751         show_error "Remote URL required"
    752       else
    753         if [[ -n "$current_remote" ]]; then
    754           git remote set-url origin "$remote_url"
    755         else
    756           git remote add origin "$remote_url"
    757         fi
    758         show_success "Remote configured: $remote_url"
    759       fi
    760       ;;
    761     *Remove*)
    762       git remote remove origin 2>/dev/null
    763       show_success "Remote removed"
    764       ;;
    765   esac
    766   
    767   echo ""
    768   wait_for_key
    769 }
    770 
    771 github_auth() {
    772   show_header
    773   gum style --foreground 183 --bold "🔑 GitHub Authentication"
    774   echo ""
    775   
    776   gum style --foreground 183 "Choose authentication method:"
    777   echo ""
    778   
    779   local method=$(gum choose \
    780     --cursor.foreground 219 \
    781     --selected.foreground 183 \
    782     --cursor "→ " \
    783     "🌐 Device flow (browser)" \
    784     "🔑 Personal Access Token" \
    785     "📋 Check current auth" \
    786     "← Back")
    787   
    788   case "$method" in
    789     *Device*)
    790       github_device_flow
    791       ;;
    792     *Token*)
    793       github_token_setup
    794       ;;
    795     *Check*)
    796       github_check_auth
    797       ;;
    798   esac
    799 }
    800 
    801 github_device_flow() {
    802   show_header
    803   gum style --foreground 183 --bold "🌐 GitHub Device Flow Authentication"
    804   echo ""
    805   
    806   if ! has_command gh; then
    807     gum style --foreground 222 "GitHub CLI (gh) not found"
    808     echo ""
    809     if confirm "Install GitHub CLI?"; then
    810       spinner_run "Installing gh..." brew install gh
    811       echo ""
    812     else
    813       return
    814     fi
    815   fi
    816   
    817   # Check if GITHUB_TOKEN is set
    818   if [[ -n "${GITHUB_TOKEN:-}" ]]; then
    819     show_info "GITHUB_TOKEN environment variable is set"
    820     echo ""
    821     gum style --foreground 147 "GitHub CLI will use this token automatically."
    822     gum style --foreground 147 "To use device flow instead, unset GITHUB_TOKEN first:"
    823     gum style --foreground 242 "  unset GITHUB_TOKEN"
    824     echo ""
    825     wait_for_key
    826     return
    827   fi
    828   
    829   gum style --foreground 183 "This will open GitHub in your browser for authentication."
    830   echo ""
    831   
    832   if ! confirm "Continue?"; then
    833     return
    834   fi
    835   
    836   echo ""
    837   
    838   if gh auth login 2>&1; then
    839     echo ""
    840     show_success "Authentication complete!"
    841     
    842     echo ""
    843     if confirm "Configure git to use gh credentials?"; then
    844       gh auth setup-git 2>/dev/null && show_success "Git configured to use gh credentials" || show_info "Git already configured"
    845     fi
    846   else
    847     local gh_exit=$?
    848     echo ""
    849     show_error "GitHub authentication failed"
    850     gum style --foreground 147 "You may already be authenticated, or there was a network issue."
    851     echo ""
    852   fi
    853   
    854   echo ""
    855   wait_for_key
    856 }
    857 
    858 github_token_setup() {
    859   show_header
    860   gum style --foreground 183 --bold "🔑 GitHub Personal Access Token Setup"
    861   echo ""
    862   
    863   gum style --foreground 183 "To create a token:"
    864   gum style --foreground 147 "  1. Go to: https://github.com/settings/tokens"
    865   gum style --foreground 147 "  2. Click 'Generate new token (classic)'"
    866   gum style --foreground 147 "  3. Select scopes: repo, workflow, write:packages"
    867   gum style --foreground 147 "  4. Generate and copy the token"
    868   echo ""
    869   
    870   if ! confirm "Have you created a token?"; then
    871     return
    872   fi
    873   
    874   echo ""
    875   local token=$(gum input \
    876     --password \
    877     --placeholder "ghp_..." \
    878     --prompt "Token: " \
    879     --prompt.foreground 183 \
    880     --cursor.foreground 219)
    881   
    882   if [[ -z "$token" ]]; then
    883     show_error "Token required"
    884     echo ""
    885     wait_for_key
    886     return
    887   fi
    888   
    889   # Store in git config
    890   cd "$ROOT"
    891   local remote_url=$(git remote get-url origin 2>/dev/null || echo "")
    892   
    893   if [[ -z "$remote_url" ]]; then
    894     show_error "No git remote configured. Please configure remote first."
    895   else
    896     # Update URL to use token
    897     if [[ "$remote_url" =~ ^https://github.com/ ]]; then
    898       local new_url=$(echo "$remote_url" | sed "s|https://github.com/|https://${token}@github.com/|")
    899       git remote set-url origin "$new_url"
    900       show_success "Token configured for this repository"
    901     else
    902       show_error "Remote is not an HTTPS GitHub URL"
    903     fi
    904   fi
    905   
    906   echo ""
    907   wait_for_key
    908 }
    909 
    910 github_check_auth() {
    911   show_header
    912   gum style --foreground 183 --bold "📋 GitHub Authentication Status"
    913   echo ""
    914   
    915   if has_command gh; then
    916     gh auth status
    917   else
    918     show_info "GitHub CLI not installed"
    919   fi
    920   
    921   echo ""
    922   echo "Git config:"
    923   git config --get user.name && show_success "Name: $(git config --get user.name)" || show_error "No user.name set"
    924   git config --get user.email && show_success "Email: $(git config --get user.email)" || show_error "No user.email set"
    925   
    926   echo ""
    927   wait_for_key
    928 }
    929 
    930 # ============================================================================
    931 # INSTALL OH-MY-ZSH
    932 # ============================================================================
    933 
    934 install_omz() {
    935   show_header
    936   gum style --foreground 183 --bold "🎨 Install Oh-My-Zsh"
    937   echo ""
    938   
    939   if [[ -d "$HOME/.oh-my-zsh" ]]; then
    940     show_info "Oh-My-Zsh already installed"
    941     echo ""
    942     wait_for_key
    943     return
    944   fi
    945   
    946   gum style --foreground 183 "This will install Oh-My-Zsh with recommended settings."
    947   echo ""
    948   
    949   if ! confirm "Install Oh-My-Zsh?"; then
    950     return
    951   fi
    952   
    953   echo ""
    954   sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
    955   
    956   echo ""
    957   show_success "Oh-My-Zsh installed!"
    958   
    959   echo ""
    960   wait_for_key
    961 }
    962 
    963 # ============================================================================
    964 # MAIN MENU
    965 # ============================================================================
    966 
    967 main_menu() {
    968   while true; do
    969     show_header
    970     
    971     echo ""
    972     gum style --foreground 183 --italic "What magical task shall we perform today?"
    973     echo ""
    974     
    975     local choice=$(gum choose \
    976       --cursor.foreground 219 \
    977       --selected.foreground 183 \
    978       --cursor "✨ " \
    979       --height 12 \
    980       "🚀 Bootstrap (fresh machine setup)" \
    981       "💾 Backup (Brewfile & packages)" \
    982       "🔐 Backup secrets (GPG/SSH/Age/Skate/OMZ)" \
    983       "🔓 Restore secrets" \
    984       "🔗 Link dotfiles" \
    985       "🔍 Check dependencies" \
    986       "🌳 Git operations" \
    987       "🎨 Install Oh-My-Zsh" \
    988       "🚪 Exit")
    989     
    990     case "$choice" in
    991       *Bootstrap*)
    992         run_bootstrap
    993         ;;
    994       *"Backup ("*)
    995         run_backup
    996         ;;
    997       *"Backup secrets"*)
    998         run_backup_secrets
    999         ;;
   1000       *Restore*)
   1001         run_restore_secrets
   1002         ;;
   1003       *Link*)
   1004         run_link
   1005         ;;
   1006       *Check*)
   1007         run_check
   1008         ;;
   1009       *Git*)
   1010         git_menu
   1011         ;;
   1012       *Oh-My-Zsh*)
   1013         install_omz
   1014         ;;
   1015       *Exit*)
   1016         clear
   1017         gum style \
   1018           --foreground 219 \
   1019           --align center \
   1020           --padding "1 2" \
   1021           --border rounded \
   1022           --border-foreground 183 \
   1023           "✨ Thank you for using Dasho's Dotfiles Wizard! ✨" \
   1024           "" \
   1025           "May your configs be ever in your favor! 🧙"
   1026         echo ""
   1027         exit 0
   1028         ;;
   1029     esac
   1030   done
   1031 }
   1032 
   1033 # ============================================================================
   1034 # ENTRY POINT
   1035 # ============================================================================
   1036 
   1037 # First-time bootstrap of wizard itself
   1038 wizard_bootstrap
   1039 
   1040 # Launch main menu
   1041 main_menu