Skip to content

Shell

Unix shell configuration, SSH setup, and Git workflows for a productive development environment.

Overview

The Shell section is organized into three key areas:

  1. Basic - Shell profiles, environment variables, aliases, and history management
  2. SSH - SSH keys, keychain integration, connection multiplexing, and security best practices
  3. Git - Git configuration, aliases, workflows, and commit best practices

Quick Start

Run the comprehensive shell setup script to automatically configure everything:

1
mkdir -p ~/temp && curl -o ~/temp/shell-setup.sh "https://raw.githubusercontent.com/dirkpetersen/dok/main/scripts/shell-setup.sh?$(date +%s)" && bash ~/temp/shell-setup.sh

This script will: - Add ~/bin and ~/.local/bin to your PATH - Generate SSH keys with passphrase protection - Install and configure keychain - Set up SSH multiplexing and optional jump host configuration - Configure Git globally - Provide instructions for adding your SSH key to GitHub

View script contents
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
#!/bin/bash
# shell-setup.sh
# Comprehensive shell and SSH setup script for development environments
# Works on both Linux and macOS

# Check if running in bash, if not, automatically run with bash
if [ -z "$BASH_VERSION" ]; then
  if command -v bash >/dev/null 2>&1; then
    # When sourced in sh, $0 might be "-sh" or similar
    # We need to figure out the actual script path
    # This is tricky in POSIX sh, so we'll try a few approaches
    SCRIPT_PATH=""

    # Try to use the first argument if it looks like a path
    if [ -n "$1" ] && [ -f "$1" ]; then
      SCRIPT_PATH="$1"
    elif [ -f "$0" ]; then
      SCRIPT_PATH="$0"
    else
      # Last resort: search for the script in common locations
      for path in \
        "/home/dp/gh/docs/scripts/shell-setup.sh" \
        "$HOME/gh/docs/scripts/shell-setup.sh" \
        "$(pwd)/shell-setup.sh"
      do
        if [ -f "$path" ]; then
          SCRIPT_PATH="$path"
          break
        fi
      done
    fi

    if [ -n "$SCRIPT_PATH" ]; then
      echo "Switching to bash to run this script..."
      bash "$SCRIPT_PATH" "$@"
      return $? 2>/dev/null || exit $?
    else
      echo "Error: Cannot determine script path. Please run directly with bash:"
      echo "  bash /path/to/shell-setup.sh"
      return 1 2>/dev/null || exit 1
    fi
  else
    echo "Error: This script requires bash, but bash is not found in PATH"
    return 1 2>/dev/null || exit 1
  fi
fi

# Detect if script is being sourced or executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  SCRIPT_MODE="execute"
  set -e
else
  SCRIPT_MODE="source"
fi

# Function to exit/return appropriately
script_exit() {
  local code="${1:-0}"
  if [[ "$SCRIPT_MODE" == "source" ]]; then
    return "$code"
  else
    exit "$code"
  fi
}

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# Log file for tracking changes
LOG_FILE="$HOME/.local/state/shell-setup/shell-setup.log"
mkdir -p "$HOME/.local/state/shell-setup"

# Function to log changes
log_change() {
  local action="$1"
  local details="$2"
  local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
  echo "[$timestamp] $action: $details" >> "$LOG_FILE"
}

# Function to revert changes
revert_changes() {
  # Temporarily disable set -e for revert function
  # (arithmetic operations like ((skipped++)) can return 0 and exit with set -e)
  local old_opts=$-
  set +e

  if [[ ! -f "$LOG_FILE" ]]; then
    echo -e "${RED}No log file found at $LOG_FILE${NC}"
    echo "Nothing to revert."
    [[ $old_opts == *e* ]] && set -e
    script_exit 1
  fi

  echo -e "${YELLOW}=== Reverting shell-setup.sh changes ===${NC}\n"
  echo -e "${RED}WARNING: This will attempt to undo all changes made by shell-setup.sh${NC}"
  echo -e "${YELLOW}Log file: $LOG_FILE${NC}"

  # Show summary of what's in the log
  local total_entries=$(grep -c "^\[" "$LOG_FILE" 2>/dev/null || echo 0)
  local bashrc_entries=$(grep -c ".bashrc" "$LOG_FILE" 2>/dev/null || echo 0)
  local profile_entries=$(grep -c ".profile\|.zprofile\|.bash_profile" "$LOG_FILE" 2>/dev/null || echo 0)

  echo "  Total logged changes: $total_entries"
  echo "  .bashrc changed lines: $bashrc_entries"
  echo "  Profile changed lines: $profile_entries"
  echo ""

  read -p "Are you sure you want to revert? (yes/N): " confirm
  if [[ "$confirm" != "yes" ]]; then
    echo "Revert cancelled."
    script_exit 0
  fi

  local reverted=0
  local failed=0
  local skipped=0

  # Arrays to track what was done
  declare -a reverted_items
  declare -a failed_items
  declare -a skipped_items

  # Process log file in reverse order
  while IFS= read -r line; do
    if [[ "$line" =~ \[.*\][[:space:]](.*):[[:space:]](.*) ]]; then
      local action="${BASH_REMATCH[1]}"
      local details="${BASH_REMATCH[2]}"

      case "$action" in
        "ADDED_TO_FILE")
          local file=$(echo "$details" | cut -d'|' -f1)
          local content=$(echo "$details" | cut -d'|' -f2-)

          if [[ -f "$file" ]]; then
            # Check if the content exists in the file
            if grep -Fq "$content" "$file"; then
              # Remove the added line
              if grep -Fv "$content" "$file" > "$file.tmp" && mv "$file.tmp" "$file"; then
                echo -e "${GREEN}${NC} Removed line from $file"
                reverted_items+=("$file")
                ((reverted++))
              else
                echo -e "${RED}${NC} Failed to remove line from $file"
                failed_items+=("$file")
                ((failed++))
              fi
            else
              # Debug: show what we're looking for if it's a .bashrc or profile file
              if [[ "$file" == *".bashrc"* ]] || [[ "$file" == *".profile"* ]] || [[ "$file" == *".zprofile"* ]]; then
                echo -e "${YELLOW}${NC} Line not found in $file"
                echo -e "${YELLOW}   Looking for: ${content:0:70}${NC}"
                ((skipped++))
                skipped_items+=("$file (not found - check manually)")
              else
                echo -e "${YELLOW}${NC} Line already removed from $file"
                skipped_items+=("$file (already removed)")
                ((skipped++))
              fi
            fi
          else
            echo -e "${YELLOW}${NC} File $file not found"
            skipped_items+=("$file (not found)")
            ((skipped++))
          fi
          ;;
        "CREATED_FILE")
          local file="$details"
          if [[ -f "$file" ]]; then
            rm -f "$file"
            echo -e "${GREEN}${NC} Removed file $file"
            reverted_items+=("Removed file $file")
            ((reverted++))
          else
            echo -e "${YELLOW}${NC} File $file not found"
            skipped_items+=("File $file not found")
            ((skipped++))
          fi
          ;;
        "CREATED_DIR")
          # Skip directory removal - directories created should not be deleted
          # as they may contain user files or be in use
          local dir="$details"
          echo -e "${YELLOW}${NC} Skipped directory removal: $dir (may contain user files)"
          skipped_items+=("Skipped directory $dir")
          ((skipped++))
          ;;
        "GIT_CONFIG")
          # Check if Git is installed before attempting to revert Git config
          if ! command -v git &> /dev/null; then
            echo -e "${YELLOW}${NC} Git not installed, skipping Git config revert"
            skipped_items+=("Git config (Git not installed)")
            ((skipped++))
            continue
          fi

          local config=$(echo "$details" | cut -d'=' -f1)
          local old_value=$(echo "$details" | cut -d'=' -f2-)
          if [[ "$old_value" == "UNSET" ]]; then
            if git config --global --unset "$config" 2>/dev/null; then
              echo -e "${GREEN}${NC} Unset git config $config"
              reverted_items+=("Unset git config $config")
              ((reverted++))
            else
              echo -e "${YELLOW}${NC} Config $config already unset"
              skipped_items+=("Config $config already unset")
              ((skipped++))
            fi
          else
            if git config --global "$config" "$old_value"; then
              echo -e "${GREEN}${NC} Restored git config $config"
              reverted_items+=("Restored git config $config=$old_value")
              ((reverted++))
            else
              echo -e "${RED}${NC} Failed to restore git config $config"
              failed_items+=("Failed to restore git config $config")
              ((failed++))
            fi
          fi
          ;;
        "SSH_KEY_CREATED")
          local key_file="$details"
          if [[ -f "$key_file" ]]; then
            echo -e "${YELLOW}Found SSH key: $key_file${NC}"
            read -p "Delete this SSH key? (yes/N): " delete_key
            if [[ "$delete_key" == "yes" ]]; then
              rm -f "$key_file" "${key_file}.pub"
              echo -e "${GREEN}${NC} Removed SSH key"
              reverted_items+=("Removed SSH key $key_file")
              ((reverted++))
            else
              echo -e "${YELLOW}${NC} Skipped SSH key removal (user choice)"
              skipped_items+=("Skipped SSH key $key_file (user choice)")
              ((skipped++))
            fi
          else
            echo -e "${YELLOW}${NC} SSH key $key_file not found"
            skipped_items+=("SSH key $key_file not found")
            ((skipped++))
          fi
          ;;
        "GPG_KEY_CREATED")
          local key_id="$details"
          echo -e "${YELLOW}Found GPG key: $key_id${NC}"
          read -p "Delete this GPG key? (yes/N): " delete_gpg
          if [[ "$delete_gpg" == "yes" ]]; then
            if gpg --batch --yes --delete-secret-keys "$key_id" 2>/dev/null && \
               gpg --batch --yes --delete-keys "$key_id" 2>/dev/null; then
              echo -e "${GREEN}${NC} Removed GPG key"
              reverted_items+=("Removed GPG key $key_id")
              ((reverted++))
            else
              echo -e "${RED}${NC} Failed to remove GPG key"
              failed_items+=("Failed to remove GPG key $key_id")
              ((failed++))
            fi
          else
            echo -e "${YELLOW}${NC} Skipped GPG key removal (user choice)"
            skipped_items+=("Skipped GPG key $key_id (user choice)")
            ((skipped++))
          fi
          ;;
      esac
    fi
  done < <(tac "$LOG_FILE")

  echo ""
  echo -e "${GREEN}=== Revert Summary ===${NC}\n"

  # Show detailed summary
  echo -e "${GREEN}Successfully Reverted ($reverted):${NC}"
  if [[ ${#reverted_items[@]} -gt 0 ]]; then
    for item in "${reverted_items[@]}"; do
      echo -e "  ${GREEN}${NC} $item"
    done
  else
    echo "  (none)"
  fi
  echo ""

  if [[ $skipped -gt 0 ]]; then
    echo -e "${YELLOW}Skipped ($skipped):${NC}"
    for item in "${skipped_items[@]}"; do
      echo -e "  ${YELLOW}${NC} $item"
    done
    echo ""
  fi

  if [[ $failed -gt 0 ]]; then
    echo -e "${RED}Failed ($failed):${NC}"
    for item in "${failed_items[@]}"; do
      echo -e "  ${RED}${NC} $item"
    done
    echo ""
  fi

  # Archive the log file
  local archive_name="$LOG_FILE.$(date +%Y%m%d_%H%M%S).reverted"
  mv "$LOG_FILE" "$archive_name"
  echo -e "Log file archived to: ${YELLOW}$archive_name${NC}"

  # Restore set -e if it was enabled
  [[ $old_opts == *e* ]] && set -e

  script_exit 0
}

# Function to show help
show_help() {
  cat << 'EOF'
shell-setup.sh - Comprehensive shell and SSH setup script

USAGE:
  ./shell-setup.sh [OPTIONS]

OPTIONS:
  (none)      Interactive setup mode (default)
              - Prompts before overwriting existing configurations
              - Skips steps that are already configured
              - Safe for repeated runs (idempotent)

  --force     Force mode - overwrites all settings
              - Backs up existing SSH keys to ~/.ssh/backup-TIMESTAMP/
              - Backs up existing GPG keys to ~/.gnupg/backup-TIMESTAMP/
              - Removes and recreates all configurations
              - No prompts, fully automated
              - Use for: fresh setups, fixing corrupted configs

  --revert    Revert all changes made by this script
              - Removes lines added to .bashrc and .profile
              - Restores previous Git configurations
              - Optionally removes SSH/GPG keys (with confirmation)
              - Skips directory removal (may contain user files)
              - Archives log file after reverting

  --help      Show this help message

WHAT IT SETS UP:
  1. PATH directories (~/.local/bin and ~/bin)
  2. XDG_RUNTIME_DIR for container support (Linux only)
  3. Convenience settings (LS_COLORS cyan directories, history size)
  4. SSH key (ed25519 with mandatory passphrase)
  5. GPG key for Git commit signing (no passphrase)
  6. Keychain for SSH key management
  7. SSH config (optional, for jump hosts)
  8. Vim configuration (desert color scheme)
  9. Git global configuration
  10. Git commit signing with GPG

LOG FILE:
  All changes are logged to: ~/.local/state/shell-setup/shell-setup.log
  Use --revert to undo changes based on this log.

EXAMPLES:
  # First time setup (interactive)
  ./shell-setup.sh

  # Force reconfiguration with backups
  ./shell-setup.sh --force

  # Undo all changes
  ./shell-setup.sh --revert

MORE INFORMATION:
  Repository: https://github.com/dirkpetersen/dok
  Documentation: https://dirkpetersen.github.io/dok

EOF
  script_exit 0
}

# Check for flags
FORCE_MODE=false
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
  show_help
elif [[ "$1" == "--revert" ]]; then
  revert_changes
elif [[ "$1" == "--force" ]]; then
  FORCE_MODE=true
  echo -e "${YELLOW}=== Force Mode Enabled ===${NC}"
  echo -e "${YELLOW}This will overwrite existing configurations${NC}"
  echo -e "${YELLOW}Existing SSH/GPG keys will be backed up${NC}\n"
fi

echo -e "${GREEN}=== Development Environment Setup ===${NC}\n"
echo -e "${YELLOW}Changes will be logged to: $LOG_FILE${NC}\n"

# Function to detect user's login shell
get_login_shell_rc() {
  local login_shell="${SHELL##*/}"
  case "$login_shell" in
    zsh)
      echo "$HOME/.zshrc"
      ;;
    bash)
      echo "$HOME/.bashrc"
      ;;
    *)
      echo "$HOME/.bashrc"
      ;;
  esac
}

# Function to detect login shell profile for keychain
get_login_profile() {
  local login_shell="${SHELL##*/}"
  case "$login_shell" in
    zsh)
      echo "$HOME/.zprofile"
      ;;
    bash)
      if [[ -f "$HOME/.bash_profile" ]]; then
        echo "$HOME/.bash_profile"
      else
        echo "$HOME/.profile"
      fi
      ;;
    *)
      echo "$HOME/.profile"
      ;;
  esac
}

# Function to add directories to beginning of PATH
add_to_begin_of_path() {
  local shell_rc=$(get_login_shell_rc)
  local bin_home="$HOME/bin"
  local bin_local="$HOME/.local/bin"
  local created_any=false

  # Create directories if they don't exist
  if [[ ! -d "$bin_home" ]]; then
    mkdir -p "$bin_home"
    log_change "CREATED_DIR" "$bin_home"
    created_any=true
  fi

  if [[ ! -d "$bin_local" ]]; then
    mkdir -p "$bin_local"
    log_change "CREATED_DIR" "$bin_local"
    created_any=true
  fi

  if [[ "$created_any" == true ]]; then
    echo -e "${GREEN}${NC} Created directories: $bin_home and $bin_local"
  else
    echo -e "${GREEN}${NC} Directories already exist: $bin_home and $bin_local"
  fi

  # Check if PATH entries already exist in RC file
  # Look for the exact PATH export line we add
  if grep -Fq "export PATH=\$HOME/bin:\$HOME/.local/bin:\$PATH" "$shell_rc" 2>/dev/null; then
    if [[ "$FORCE_MODE" != true ]]; then
      echo -e "${GREEN}${NC} PATH directories already configured in $shell_rc"
      return 0
    else
      echo -e "${YELLOW}Force mode: Removing existing PATH configuration${NC}"
      grep -Fv "export PATH=\$HOME/bin:\$HOME/.local/bin:\$PATH" "$shell_rc" > "$shell_rc.tmp" && mv "$shell_rc.tmp" "$shell_rc"
      grep -Fv "# Add local bin directories to PATH (front)" "$shell_rc" > "$shell_rc.tmp" && mv "$shell_rc.tmp" "$shell_rc"
    fi
  fi

  # Add to beginning of PATH (in correct order)
  echo "" >> "$shell_rc"
  echo "# Add local bin directories to PATH (front)" >> "$shell_rc"
  echo "export PATH=\$HOME/bin:\$HOME/.local/bin:\$PATH" >> "$shell_rc"
  log_change "ADDED_TO_FILE" "$shell_rc|# Add local bin directories to PATH (front)"
  log_change "ADDED_TO_FILE" "$shell_rc"'|export PATH=$HOME/bin:$HOME/.local/bin:$PATH'
  echo -e "${GREEN}${NC} Added PATH configuration to $shell_rc"
}

# Function to setup XDG_RUNTIME_DIR for container support (Linux only)
setup_xdg_runtime_dir() {
  # Only run on Linux, skip on macOS
  if [[ "$(uname -s)" != "Linux" ]]; then
    echo -e "${GREEN}${NC} Skipping XDG_RUNTIME_DIR (not Linux)"
    return 0
  fi

  local shell_rc=$(get_login_shell_rc)
  local xdg_line='export XDG_RUNTIME_DIR="/run/user/$(id -u)"'

  # Check if XDG_RUNTIME_DIR is already set in shell rc
  if grep -q "export XDG_RUNTIME_DIR=" "$shell_rc" 2>/dev/null; then
    if [[ "$FORCE_MODE" != true ]]; then
      echo -e "${GREEN}${NC} XDG_RUNTIME_DIR already configured in $shell_rc"
      return 0
    else
      echo -e "${YELLOW}Force mode: Removing existing XDG_RUNTIME_DIR configuration${NC}"
      grep -Fv 'export XDG_RUNTIME_DIR=' "$shell_rc" > "$shell_rc.tmp" && mv "$shell_rc.tmp" "$shell_rc"
      grep -Fv "# Container support" "$shell_rc" > "$shell_rc.tmp" && mv "$shell_rc.tmp" "$shell_rc"
    fi
  fi

  # Add XDG_RUNTIME_DIR configuration
  echo "" >> "$shell_rc"
  echo "# Container support" >> "$shell_rc"
  echo "$xdg_line" >> "$shell_rc"
  log_change "ADDED_TO_FILE" "$shell_rc|# Container support"
  log_change "ADDED_TO_FILE" "$shell_rc"'|export XDG_RUNTIME_DIR="/run/user/$(id -u)"'
  echo -e "${GREEN}${NC} Added XDG_RUNTIME_DIR configuration to $shell_rc"
}

# Function to setup convenience environment settings (LS_COLORS and history)
setup_convenience_settings() {
  local shell_rc=$(get_login_shell_rc)
  local needs_update=false

  # Check if our convenience settings marker exists
  if grep -q "# Convenience environment settings" "$shell_rc" 2>/dev/null; then
    if [[ "$FORCE_MODE" != true ]]; then
      echo -e "${GREEN}${NC} Convenience settings already configured in $shell_rc"
      return 0
    else
      echo -e "${YELLOW}Force mode: Removing existing convenience settings${NC}"
      # Remove old convenience settings block
      sed -i.bak '/# Convenience environment settings/,/^$/d' "$shell_rc"
      needs_update=true
    fi
  else
    needs_update=true
  fi

  if [[ "$needs_update" != true ]]; then
    return 0
  fi

  # Get current LS_COLORS if it exists, otherwise use default
  local current_ls_colors=""
  if grep -q "^export LS_COLORS=" "$shell_rc" 2>/dev/null; then
    current_ls_colors=$(grep "^export LS_COLORS=" "$shell_rc" | head -1 | sed 's/^export LS_COLORS="//' | sed 's/"$//')
  else
    # Use system default or a basic one
    current_ls_colors="rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32"
  fi

  # Replace di=01;34 with di=01;36 (change directory color from blue to cyan)
  local new_ls_colors="${current_ls_colors//di=01;34/di=01;36}"
  # Also handle di=34 without the bold
  new_ls_colors="${new_ls_colors//di=34/di=01;36}"

  # Check if HISTCONTROL is already set in the file
  local histcontrol_line=""
  if ! grep -q "^export HISTCONTROL=" "$shell_rc" 2>/dev/null && \
     ! grep -q "^HISTCONTROL=" "$shell_rc" 2>/dev/null; then
    # HISTCONTROL not set, add it with ignoreboth
    histcontrol_line="export HISTCONTROL=ignoreboth"
  fi

  # Add convenience settings block
  echo "" >> "$shell_rc"
  echo "# Convenience environment settings" >> "$shell_rc"
  echo "" >> "$shell_rc"
  echo "# Change directory color from dark blue to cyan for better visibility" >> "$shell_rc"
  echo "export LS_COLORS=\"${new_ls_colors}\"" >> "$shell_rc"
  echo "" >> "$shell_rc"
  echo "# Increase history size" >> "$shell_rc"
  echo "export HISTSIZE=10000" >> "$shell_rc"
  echo "export HISTFILESIZE=20000" >> "$shell_rc"

  # Only add HISTCONTROL if it wasn't already set
  if [[ -n "$histcontrol_line" ]]; then
    echo "export HISTCONTROL=ignoreboth" >> "$shell_rc"
  fi

  # Log changes
  log_change "ADDED_TO_FILE" "$shell_rc|# Convenience environment settings"
  log_change "ADDED_TO_FILE" "$shell_rc|# Change directory color from dark blue to cyan for better visibility"
  log_change "ADDED_TO_FILE" "$shell_rc|export LS_COLORS=\"${new_ls_colors}\""
  log_change "ADDED_TO_FILE" "$shell_rc|# Increase history size"
  log_change "ADDED_TO_FILE" "$shell_rc|export HISTSIZE=10000"
  log_change "ADDED_TO_FILE" "$shell_rc|export HISTFILESIZE=20000"

  # Only log HISTCONTROL if we added it
  if [[ -n "$histcontrol_line" ]]; then
    log_change "ADDED_TO_FILE" "$shell_rc|export HISTCONTROL=ignoreboth"
  fi

  echo -e "${GREEN}${NC} Added convenience settings to $shell_rc"
}

# Function to check if SSH key has a password
ssh_key_has_password() {
  local key_file="$1"
  # Check for encryption markers in OpenSSH format keys
  # Encrypted keys contain "aes" or "ENCRYPTED" markers
  if [[ ! -f "$key_file" ]]; then
    return 1  # File doesn't exist
  fi

  # Check in the base64-decoded content (OpenSSH format stores cipher info base64-encoded)
  # Extract the second line (first base64 line) and decode it
  local key_header=$(head -2 "$key_file" | tail -1)
  if echo "$key_header" | base64 -d 2>/dev/null | grep -q "aes"; then
    return 0  # Has password (encrypted)
  fi

  # Also check for legacy PEM format with ENCRYPTED marker
  if grep -q "ENCRYPTED" "$key_file" 2>/dev/null; then
    return 0  # Has password (encrypted)
  fi

  return 1  # No password
}

# Function to setup SSH key
setup_ssh_key() {
  local email="$1"
  local ssh_dir="$HOME/.ssh"
  local key_file="$ssh_dir/id_ed25519"

  mkdir -p "$ssh_dir"
  chmod 700 "$ssh_dir"

  if [[ -f "$key_file" ]]; then
    echo -e "${YELLOW}SSH key already exists at $key_file${NC}"

    if [[ "$FORCE_MODE" == true ]]; then
      # Backup existing key
      local backup_dir="$ssh_dir/backup-$(date +%Y%m%d_%H%M%S)"
      mkdir -p "$backup_dir"
      cp "$key_file" "$backup_dir/"
      [[ -f "${key_file}.pub" ]] && cp "${key_file}.pub" "$backup_dir/"
      echo -e "${GREEN}${NC} Backed up existing SSH key to $backup_dir"
      # Remove existing keys so ssh-keygen doesn't prompt
      rm -f "$key_file" "${key_file}.pub"
      # Continue to generate new key
    elif ssh_key_has_password "$key_file"; then
      echo -e "${GREEN}${NC} SSH key has a password (encrypted)"
      return 0
    else
      echo -e "${RED}✗ SSH key exists but has NO password!${NC}"
      echo -e "${YELLOW}Please:${NC}"
      echo "  1. Backup existing key: mv $key_file ${key_file}.bak"
      echo "  2. Add a password to your existing key: ssh-keygen -p -f $key_file"
      echo "  3. Run this script again, or use --force to backup and replace"
      return 1
    fi
  fi

  if [[ ! -f "$key_file" ]] || [[ "$FORCE_MODE" == true ]]; then
    echo -e "${YELLOW}Generating new SSH key with passphrase...${NC}"
    echo -e "${YELLOW}You will be prompted to enter a passphrase (REQUIRED for security):${NC}"

    # Use read to get passphrase from user
    local passphrase=""
    local passphrase_confirm=""

    while true; do
      read -sp "Enter passphrase: " passphrase
      echo

      # Check for empty passphrase
      if [[ -z "$passphrase" ]]; then
        echo -e "${RED}✗ Passphrase cannot be empty for security reasons. Please try again.${NC}"
        echo
        continue
      fi

      echo
      read -sp "Enter passphrase again: " passphrase_confirm
      echo

      if [[ "$passphrase" == "$passphrase_confirm" ]]; then
        break
      else
        echo -e "${RED}✗ Passphrases do not match. Try again.${NC}"
        echo
      fi
    done

    # Generate key with the passphrase using -N flag
    ssh-keygen -t ed25519 -C "$email" -f "$key_file" -N "$passphrase" -q
    log_change "SSH_KEY_CREATED" "$key_file"

    # Check if key file was actually created
    if [[ ! -f "$key_file" ]]; then
      echo -e "${RED}✗ Failed to create SSH key file!${NC}"
      return 1
    fi

    # Verify key was created with password
    if ssh_key_has_password "$key_file"; then
      echo -e "${GREEN}${NC} SSH key generated successfully with passphrase"
      return 0
    else
      echo -e "${RED}✗ SSH key was generated without a passphrase!${NC}"
      echo -e "${YELLOW}Removing insecure key. Please run this script again and set a passphrase.${NC}"
      rm -f "$key_file" "${key_file}.pub"
      return 1
    fi
  fi
}

# Function to install keychain
install_keychain() {
  if command -v keychain &> /dev/null; then
    echo -e "${GREEN}${NC} Keychain is already installed"
    return 0
  fi

  echo -e "${YELLOW}Installing keychain...${NC}"
  mkdir -p "$HOME/bin"
  curl -s https://raw.githubusercontent.com/danielrobbins/keychain/refs/heads/master/keychain.sh -o "$HOME/bin/keychain"
  chmod +x "$HOME/bin/keychain"
  echo -e "${GREEN}${NC} Keychain installed to $HOME/bin/keychain"
}

# Function to setup GPG key
setup_gpg_key() {
  local name="$1"
  local email="$2"

  # Check if GPG is installed
  if ! command -v gpg &> /dev/null; then
    echo -e "${YELLOW}GPG not installed. Install it with: sudo apt install gnupg (or brew install gnupg on macOS)${NC}"
    return 1
  fi

  # Check if user already has a GPG key
  if gpg --list-secret-keys --keyid-format=long "$email" &>/dev/null; then
    local key_id=$(gpg --list-secret-keys --keyid-format=long "$email" | grep sec | awk '{print $2}' | cut -d'/' -f2 | head -1)

    if [[ "$FORCE_MODE" == true ]]; then
      # Export existing key as backup
      local backup_dir="$HOME/.gnupg/backup-$(date +%Y%m%d_%H%M%S)"
      mkdir -p "$backup_dir"
      gpg --export-secret-keys --armor "$email" > "$backup_dir/private-key-$key_id.asc" 2>/dev/null
      gpg --export --armor "$email" > "$backup_dir/public-key-$key_id.asc" 2>/dev/null
      echo -e "${GREEN}${NC} Backed up existing GPG key to $backup_dir" >&2
      # Continue to generate new key (old key will remain in keyring)
    else
      echo -e "${GREEN}${NC} GPG key already exists for $email" >&2
      echo "$key_id"
      return 0
    fi
  fi

  echo -e "${YELLOW}Generating GPG key for Git commit signing (no passphrase required)...${NC}" >&2

  # Configure GPG directory
  mkdir -p "$HOME/.gnupg"
  chmod 700 "$HOME/.gnupg"

  # Create GPG key generation configuration without passphrase
  # No passphrase is fine for commit signing keys since they only sign, not encrypt
  cat > /tmp/gpg-gen-key.conf << EOF
%echo Generating GPG key
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: $name
Name-Email: $email
Expire-Date: 0
%no-protection
%commit
%echo Done
EOF

  # Generate the key
  gpg --batch --generate-key /tmp/gpg-gen-key.conf 2>/dev/null
  rm -f /tmp/gpg-gen-key.conf

  # Get the key ID
  local key_id=$(gpg --list-secret-keys --keyid-format=long "$email" | grep sec | awk '{print $2}' | cut -d'/' -f2 | head -1)

  if [[ -n "$key_id" ]]; then
    log_change "GPG_KEY_CREATED" "$key_id"
    echo -e "${GREEN}${NC} GPG key generated successfully" >&2
    echo -e "${GREEN}   Key ID: $key_id${NC}" >&2
    echo "$key_id"
    return 0
  else
    echo -e "${RED}✗ Failed to generate GPG key${NC}" >&2
    return 1
  fi
}

# Function to setup keychain in login profile
setup_keychain_profile() {
  local profile=$(get_login_profile)

  # Build keychain command for SSH key management
  # Use --nogui to avoid pinentry requirement (uses console prompts instead)
  local keychain_line='[ -z $SLURM_PTY_PORT ] && eval $(keychain --nogui --quiet --eval ~/.ssh/id_ed25519)'

  if grep -q "keychain" "$profile" 2>/dev/null; then
    # Check if the desired SSH configuration already exists
    if grep -q "keychain.*--nogui.*--quiet.*--eval.*id_ed25519" "$profile" 2>/dev/null; then
      echo -e "${GREEN}${NC} Keychain already configured in $profile"
      return 0
    fi

    # Configuration exists but doesn't match what we want
    if [[ "$FORCE_MODE" == true ]]; then
      echo -e "${YELLOW}Force mode: Removing existing keychain configuration${NC}"
      sed -i.bak '/keychain/d' "$profile"
      sed -i.bak '/SSH Keychain/d' "$profile"
      sed -i.bak '/Only run on interactive/d' "$profile"
    else
      echo -e "${YELLOW}Keychain already configured in $profile${NC}"
      read -p "Update keychain configuration? (y/N): " update_keychain
      if [[ "$update_keychain" == "y" || "$update_keychain" == "Y" ]]; then
        # Remove old keychain lines
        sed -i.bak '/keychain/d' "$profile"
        sed -i.bak '/SSH Keychain/d' "$profile"
        sed -i.bak '/Only run on interactive/d' "$profile"
      else
        echo -e "${GREEN}${NC} Keeping existing keychain configuration"
        return 0
      fi
    fi
  fi

  echo "" >> "$profile"
  echo "# SSH Keychain - loads SSH key passphrase into memory" >> "$profile"
  echo "# Only run on interactive login shells (not in Slurm jobs)" >> "$profile"
  echo "$keychain_line" >> "$profile"
  log_change "ADDED_TO_FILE" "$profile|# SSH Keychain - loads SSH key passphrase into memory"
  log_change "ADDED_TO_FILE" "$profile|# Only run on interactive login shells (not in Slurm jobs)"
  log_change "ADDED_TO_FILE" "$profile|$keychain_line"
  echo -e "${GREEN}${NC} Added keychain configuration to $profile"
}

# Function to setup SSH config
setup_ssh_config() {
  local ssh_config="$HOME/.ssh/config"

  if [[ -f "$ssh_config" ]]; then
    echo -e "${GREEN}${NC} SSH config already exists at $ssh_config"
    return 0
  fi

  echo -e "${YELLOW}Setting up SSH configuration...${NC}"
  read -p "Do you connect through a ssh jump/bastion host (for HPC/AI)? [y/N]: " has_remote

  # If user doesn't have remote hosts to configure, skip SSH config creation
  if [[ "$has_remote" != "y" && "$has_remote" != "Y" ]]; then
    echo -e "${GREEN}${NC} Skipping SSH config setup (no remote hosts to configure)"
    return 0
  fi

  local username=""
  local jumphost=""
  local jumphost_user=""
  local hpc_host=""
  local hpc_username=""

  # Ask for username once (used for both jump host and HPC/AI login node)
  read -p "Your username: " username

  # User said yes to jump/bastion host, so ask for details
  read -p "Jump/bastion host hostname (e.g., jump.example.edu): " jumphost
  jumphost_user="$username"

  # Now ask for HPC/AI login node details
  read -p "HPC/AI login node hostname (e.g., login.hpc.university.edu): " hpc_host
  hpc_username="$username"

  # Create SSH config
  mkdir -p "$HOME/.ssh/controlmasters"
  chmod 700 "$HOME/.ssh"

  cat > "$ssh_config" << 'EOF'
# SSH Connection Multiplexing and Global Defaults
Host *
    ControlPath ~/.ssh/controlmasters/%r@%h:%p
    ControlMaster auto
    ControlPersist 10m
    ServerAliveInterval 10
    ServerAliveCountMax 3

# GitHub
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519
    AddKeysToAgent yes
EOF

  # Add jump host if provided
  if [[ -n "$jumphost" && -n "$jumphost_user" ]]; then
    cat >> "$ssh_config" << EOF

# Jump Host
Host jumphost
    HostName $jumphost
    User $jumphost_user
    ControlMaster auto
    DynamicForward 1080
EOF
  fi

  # Add HPC config if provided
  if [[ -n "$hpc_host" && -n "$hpc_username" ]]; then
    cat >> "$ssh_config" << EOF

# HPC Login Node
Host hpc
    HostName $hpc_host
    User $hpc_username
EOF

    # Add ProxyJump if jumphost exists
    if [[ -n "$jumphost" ]]; then
      echo "    ProxyJump jumphost" >> "$ssh_config"
    fi
  fi

  chmod 600 "$ssh_config"
  chmod 700 "$HOME/.ssh/controlmasters"
  echo -e "${GREEN}${NC} SSH config created at $ssh_config"
}

# Function to setup Vim configuration
setup_vim_config() {
  local vimrc="$HOME/.vimrc"
  local vim_wrapper="$HOME/bin/vim-wrapper"
  local edr_symlink="$HOME/bin/edr"

  # Check if vim is installed
  if ! command -v vim &> /dev/null; then
    echo -e "${YELLOW}Vim not installed. Skipping Vim configuration.${NC}"
    return 0
  fi

  # Check if .vimrc already has our configuration
  local has_syntax=false
  local has_colorscheme=false

  if [[ -f "$vimrc" ]]; then
    grep -q "^syntax on" "$vimrc" 2>/dev/null && has_syntax=true
    grep -q "^colorscheme desert" "$vimrc" 2>/dev/null && has_colorscheme=true

    if [[ "$has_syntax" == true && "$has_colorscheme" == true ]]; then
      if [[ "$FORCE_MODE" != true ]]; then
        echo -e "${GREEN}${NC} Vim already configured with desert color scheme"
      else
        echo -e "${YELLOW}Force mode: Reconfiguring Vim${NC}"
        # Backup existing .vimrc
        local backup_file="$vimrc.backup-$(date +%Y%m%d_%H%M%S)"
        cp "$vimrc" "$backup_file"
        echo -e "${GREEN}${NC} Backed up existing .vimrc to $backup_file"
      fi
    fi
  fi

  # Create or append to .vimrc if needed
  if [[ ! -f "$vimrc" ]]; then
    # Create new .vimrc
    cat > "$vimrc" << 'EOF'
" Enable syntax highlighting
syntax on

" Use desert color scheme
colorscheme desert
EOF
    log_change "CREATED_FILE" "$vimrc"
    echo -e "${GREEN}${NC} Created ~/.vimrc with desert color scheme"
  elif [[ "$has_syntax" != true ]] || [[ "$has_colorscheme" != true ]]; then
    # Append to existing .vimrc
    echo "" >> "$vimrc"
    echo "\" Enable syntax highlighting" >> "$vimrc"
    echo "syntax on" >> "$vimrc"
    echo "" >> "$vimrc"
    echo "\" Use desert color scheme" >> "$vimrc"
    echo "colorscheme desert" >> "$vimrc"
    log_change "ADDED_TO_FILE" "$vimrc|\" Enable syntax highlighting"
    log_change "ADDED_TO_FILE" "$vimrc|syntax on"
    log_change "ADDED_TO_FILE" "$vimrc|\" Use desert color scheme"
    log_change "ADDED_TO_FILE" "$vimrc|colorscheme desert"
    echo -e "${GREEN}${NC} Added desert color scheme to ~/.vimrc"
  fi

  # Install vim wrapper script
  if [[ ! -f "$vim_wrapper" ]] || [[ "$FORCE_MODE" == true ]]; then
    mkdir -p "$HOME/bin"

    # Download or copy vim-wrapper.sh
    if curl -fsSL "https://raw.githubusercontent.com/dirkpetersen/dok/main/scripts/vim-wrapper.sh" -o "$vim_wrapper" 2>/dev/null; then
      chmod +x "$vim_wrapper"
      log_change "CREATED_FILE" "$vim_wrapper"
      echo -e "${GREEN}${NC} Installed vim wrapper to ~/bin/vim-wrapper"
    else
      echo -e "${YELLOW}Could not download vim-wrapper.sh from GitHub${NC}"
      echo -e "${YELLOW}Creating vim wrapper locally...${NC}"
      cat > "$vim_wrapper" << 'EOF'
#!/bin/bash
# vim-wrapper.sh - Simple vim wrapper for easy editing
# Starts in insert mode and uses double-escape to save/quit

vim -c "startinsert" \
    -c "let g:esc_pressed = 0" \
    -c "function! SaveAndQuit()
        if &modified
            let choice = confirm('Save changes?', \"&Yes\n&No\n&Cancel\", 1)
            if choice == 1
                wq
            elseif choice == 2
                q!
            endif
        else
            q
        endif
    endfunction" \
    -c "function! HandleEscape()
        if g:esc_pressed
            let g:esc_pressed = 0
            call SaveAndQuit()
        else
            let g:esc_pressed = 1
            call timer_start(500, {-> execute('let g:esc_pressed = 0')})
            return \"\\<Esc>\"
        endif
        return ''
    endfunction" \
    -c "inoremap <expr> <Esc> HandleEscape()" \
    -c "nnoremap <Esc><Esc> :call SaveAndQuit()<CR>" \
    "$@"
EOF
      chmod +x "$vim_wrapper"
      log_change "CREATED_FILE" "$vim_wrapper"
      echo -e "${GREEN}${NC} Created vim wrapper at ~/bin/vim-wrapper"
    fi
  else
    echo -e "${GREEN}${NC} Vim wrapper already installed"
  fi

  # Create edr symlink
  if [[ ! -L "$edr_symlink" ]] || [[ "$FORCE_MODE" == true ]]; then
    ln -sf "vim-wrapper" "$edr_symlink"
    log_change "CREATED_FILE" "$edr_symlink"
    echo -e "${GREEN}${NC} Created symlink ~/bin/edr -> vim-wrapper"
  else
    echo -e "${GREEN}${NC} Symlink ~/bin/edr already exists"
  fi
}

# Function to setup git configuration
setup_git_config() {
  local git_name="$1"
  local git_email="$2"

  # Check if Git is installed
  if ! command -v git &> /dev/null; then
    echo -e "${YELLOW}Git not installed. Skipping Git configuration.${NC}"
    echo -e "${YELLOW}To install: sudo apt install git (or brew install git on macOS)${NC}"
    return 0
  fi

  # Check if git is already configured
  local current_name=$(git config --global user.name 2>/dev/null)
  local current_email=$(git config --global user.email 2>/dev/null)
  local current_branch=$(git config --global init.defaultBranch 2>/dev/null)

  if [[ -n "$current_name" && -n "$current_email" ]]; then
    echo -e "${GREEN}${NC} Git already configured:"
    echo "   Name: $current_name"
    echo "   Email: $current_email"

    if [[ "$current_name" == "$git_name" && "$current_email" == "$git_email" ]]; then
      if [[ "$FORCE_MODE" != true ]]; then
        echo -e "${GREEN}${NC} Configuration matches input (no changes needed)"

        # Still set default branch if not set
        if [[ "$current_branch" != "main" ]]; then
          git config --global init.defaultBranch main
          echo -e "${GREEN}${NC} Set default branch to 'main'"
        fi
        return 0
      else
        echo -e "${YELLOW}Force mode: Reconfiguring Git${NC}"
      fi
    else
      if [[ "$FORCE_MODE" == true ]]; then
        echo -e "${YELLOW}Force mode: Updating Git configuration${NC}"
      else
        read -p "Update git config with new values? (y/N): " update_git
        if [[ "$update_git" != "y" && "$update_git" != "Y" ]]; then
          echo -e "${YELLOW}Keeping existing git configuration${NC}"
          return 0
        fi
      fi
    fi
  fi

  # Apply configuration
  git config --global user.name "$git_name"
  git config --global user.email "$git_email"
  git config --global init.defaultBranch main
  echo -e "${GREEN}${NC} Git configuration complete"
}

# ============================================================================
# MAIN SCRIPT
# ============================================================================

# Step 1: Get user information
echo -e "${YELLOW}Step 1: User Information${NC}"

# Try to get existing git config (if git is installed)
existing_name=""
existing_email=""
if command -v git &> /dev/null; then
  existing_name=$(git config --global user.name 2>/dev/null || true)
  existing_email=$(git config --global user.email 2>/dev/null || true)
fi

if [[ -n "$existing_name" && -n "$existing_email" ]]; then
  echo -e "${GREEN}Found existing git configuration:${NC}"
  echo "   Name: $existing_name"
  echo "   Email: $existing_email"
  read -p "Use this configuration? (Y/n): " use_existing

  if [[ "$use_existing" == "y" || "$use_existing" == "Y" || -z "$use_existing" ]]; then
    user_name="$existing_name"
    user_email="$existing_email"
    echo -e "${GREEN}${NC} Using existing configuration"
  else
    read -p "Your name (for Git/SSH): " user_name
    read -p "Your email (for Git/SSH): " user_email
  fi
else
  read -p "Your name (for Git/SSH): " user_name
  read -p "Your email (for Git/SSH): " user_email
fi

if [[ -z "$user_name" || -z "$user_email" ]]; then
  echo -e "${RED}✗ Name and email are required${NC}"
  script_exit 1
fi

# Step 2: Setup PATH directories
echo -e "\n${YELLOW}Step 2: Setting up PATH directories${NC}"
add_to_begin_of_path

# Step 2b: Setup XDG_RUNTIME_DIR for container support (Linux only)
echo -e "\n${YELLOW}Step 2b: Setting up container support${NC}"
setup_xdg_runtime_dir

# Step 2c: Setup convenience environment settings
echo -e "\n${YELLOW}Step 2c: Setting up convenience environment settings${NC}"
setup_convenience_settings

# Step 3: Setup SSH key
echo -e "\n${YELLOW}Step 3: Setting up SSH key${NC}"
if ! setup_ssh_key "$user_email"; then
  echo -e "${RED}✗ SSH key setup failed. Please resolve the issue and run this script again.${NC}"
  script_exit 1
fi

# Step 4: Setup GPG key for Git commit signing
echo -e "\n${YELLOW}Step 4: Setting up GPG key for Git commit signing${NC}"
gpg_key_id=""

# Check if GPG is installed
if ! command -v gpg &> /dev/null; then
  echo -e "${YELLOW}GPG not installed. Skipping GPG key setup.${NC}"
  echo -e "${YELLOW}To install: sudo apt install gnupg (or brew install gnupg on macOS)${NC}"
else
  # Check if user already has a GPG key for this email
  if gpg --list-secret-keys --keyid-format=long "$user_email" &>/dev/null; then
    echo -e "${GREEN}${NC} GPG key already exists for $user_email"
    gpg_key_id=$(gpg --list-secret-keys --keyid-format=long "$user_email" | grep sec | awk '{print $2}' | cut -d'/' -f2 | head -1)
    echo -e "${GREEN}   Key ID: $gpg_key_id${NC}"
  else
    # Generate GPG key without passphrase (only used for commit signing)
    gpg_key_id=$(setup_gpg_key "$user_name" "$user_email")
    if [[ $? -eq 0 && -n "$gpg_key_id" ]]; then
      echo -e "${GREEN}${NC} GPG key setup complete"
    else
      echo -e "${YELLOW}Skipping GPG key setup${NC}"
      gpg_key_id=""
    fi
  fi
fi

# Step 5: Install keychain
echo -e "\n${YELLOW}Step 5: Installing keychain${NC}"
install_keychain

# Step 6: Setup keychain in login profile
echo -e "\n${YELLOW}Step 6: Configuring keychain in login profile${NC}"
setup_keychain_profile

# Step 7: Setup SSH config
echo -e "\n${YELLOW}Step 7: Setting up SSH configuration${NC}"
setup_ssh_config

# Step 8: Setup Vim
echo -e "\n${YELLOW}Step 8: Configuring Vim${NC}"
setup_vim_config

# Step 9: Setup Git
echo -e "\n${YELLOW}Step 9: Configuring Git${NC}"
setup_git_config "$user_name" "$user_email"

# Configure GPG signing if we have a GPG key
previous_signing_key=""
if [[ -n "$gpg_key_id" ]] && command -v git &> /dev/null; then
  # Check if Git signing is already configured with a different key
  current_signing_key=$(git config --global user.signingkey 2>/dev/null || echo "")
  current_gpgsign=$(git config --global commit.gpgsign 2>/dev/null || echo "false")

  if [[ "$current_signing_key" != "$gpg_key_id" ]] || [[ "$current_gpgsign" != "true" ]]; then
    # Save the previous key if it was different
    if [[ -n "$current_signing_key" ]] && [[ "$current_signing_key" != "$gpg_key_id" ]]; then
      previous_signing_key="$current_signing_key"
    fi

    log_change "GIT_CONFIG" "user.signingkey=${current_signing_key:-UNSET}"
    log_change "GIT_CONFIG" "commit.gpgsign=${current_gpgsign}"
    git config --global user.signingkey "$gpg_key_id"
    git config --global commit.gpgsign true
    echo -e "${GREEN}${NC} Configured Git to sign commits with GPG key $gpg_key_id"
  else
    echo -e "${GREEN}${NC} Git commit signing already configured with key $gpg_key_id"
  fi
fi

# Inform user about revert option
echo ""
echo -e "${YELLOW}Note:${NC} All changes have been logged to: ${YELLOW}$LOG_FILE${NC}"
echo -e "To revert these changes later, run: ${YELLOW}$0 --revert${NC}"
echo ""

# Step 10: Display completion summary
echo -e "${GREEN}=== Setup Complete! ===${NC}\n"

echo -e "${YELLOW}Next steps:${NC}\n"

echo "1. Reload your shell configuration:"
CURRENT_SHELL="${SHELL##*/}"
if [[ "$CURRENT_SHELL" == "zsh" ]]; then
  echo "   . ~/.zprofile"
else
  if [[ -f "$HOME/.bash_profile" ]]; then
    echo "   . ~/.bash_profile"
  else
    echo "   . ~/.profile"
  fi
fi
echo ""

echo "2. Add your SSH public key to GitHub:"
echo "   cat ~/.ssh/id_ed25519.pub"
echo "   Visit: https://github.com/settings/ssh/new"
echo ""

echo "3. Test your SSH connection:"
echo "   ssh -T git@github.com"
echo ""

if [[ -n "$gpg_key_id" ]]; then
  step=4

  # Only show note if there was a different signing key
  if [[ -n "$previous_signing_key" ]]; then
    echo "4. Note: You had a different GPG signing key ($previous_signing_key)"
    echo "   Git is now configured to use: $gpg_key_id"
    echo "   To revert to the old key, run:"
    echo "   git config --global user.signingkey $previous_signing_key"
    echo ""
    step=5
  fi

  echo "$step. Export your GPG public key to add to GitHub:"
  echo "   gpg --armor --export $gpg_key_id"
  echo "   Visit: https://github.com/settings/gpg/new"
  echo ""

  step=$((step + 1))
  echo "$step. Your SSH key passphrase will be loaded on next login via keychain"
  echo "   (GPG key has no passphrase - only used for commit signing)"
else
  echo "4. Your SSH key passphrase will be loaded on next login via keychain"
  echo "   (no need to run keychain manually)"
fi
echo ""

echo -e "${GREEN}Happy coding!${NC}"

Manual Setup

If you prefer to set up components manually:

  1. Set Up Your Environment - Start with Basic to configure your shell profile with essential environment variables and aliases.

  2. Configure SSH - Follow SSH to generate SSH keys with passphrases and set up keychain for secure, convenient authentication.

  3. Configure Git - Finally, set up Git with global configuration, aliases, and workflows for efficient version control.

Key Components

Basic Shell

  • Shell profile configuration (Bash/Zsh)
  • Environment variables
  • Useful aliases
  • Command history management

SSH Configuration

  • Ed25519 key generation with passphrases
  • SSH key permissions and security
  • SSH multiplexing for faster connections
  • SSH keychain for passphrase caching
  • Jump host proxy configuration
  • Connection optimization

Git Workflows

  • Git configuration and aliases
  • Commit message best practices
  • Branch naming conventions
  • Common Git workflows
  • Helpful Git commands
  • SSH integration with Git

Security-First Approach

This setup emphasizes:

  • 🔐 SSH keys with passphrases - Protecting your keys even if compromised
  • 🔄 Keychain caching - Convenience without sacrificing security
  • 🚀 Connection multiplexing - Faster, more reliable remote access
  • 📝 Clear Git workflows - Organized, traceable development history
  • 🛡️ Best practices - Industry standards for secure shell usage

Topics by Use Case

Remote Server Access

  1. Generate SSH keys (SSH)
  2. Configure SSH multiplexing (SSH)
  3. Set up jump hosts for secure access (SSH)

Git Development

  1. Configure Git globally (Git)
  2. Add Git aliases for productivity (Git)
  3. Follow branch naming conventions (Git)

Shell Productivity

  1. Set up shell profile (Basic)
  2. Add useful aliases (Basic)
  3. Configure environment variables (Basic)