Last active
January 13, 2026 08:04
-
-
Save anschmieg/59f8bca8527c7745aa3781f49c8bb313 to your computer and use it in GitHub Desktop.
π One-Command Dotfiles Deployment Script
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 | |
| # π One-Command Dotfiles Deployment Script | |
| # | |
| # Quick deployment: bash <(curl -sfL https://nothing.pink/zsh) | |
| # | |
| # (Note: 'curl | bash' works for automated/apply-only runs, but breaks interactive prompts) | |
| # bash deploy.sh --apply-only | |
| # Or set the environment: APPLY_ONLY=1 bash deploy.sh | |
| # | |
| # What this script does: | |
| # 1. Installs chezmoi if needed | |
| # 2. Helps you set up your age encryption key | |
| # 3. Deploys your complete dotfiles setup | |
| # 4. Runs all automated configuration scripts | |
| # 5. Validates the installation (37 checks) | |
| # | |
| # Prerequisites: Your age encryption key from ~/.config/age/chezmoi.txt | |
| set -euo pipefail | |
| # --- Safety Check: Detect Pipe usage vs Inteactivity --- | |
| if [[ ! -t 0 ]] && [[ -z "${APPLY_ONLY:-}" ]] && [[ -z "${CHEZMOI_AGE_KEY:-}" ]]; then | |
| # If stdin is not a TTY (likely curl | bash), not in apply-only mode, and no key provided: | |
| # We might need interactivity, which fails in pipe mode (borked input). | |
| # Check if /dev/tty is available to warn the user. | |
| if [[ -e /dev/tty ]]; then | |
| echo "β οΈ Launched via pipe (curl | bash). Interactive prompts WILL FAIL." | |
| echo "π‘ Please use this command instead:" | |
| echo " bash <(curl -sfL https://gist.github.com/anschmieg/59f8bca8527c7745aa3781f49c8bb313/raw/deploy.sh)" | |
| echo "" | |
| echo " (Or provide inputs via environment variables to run non-interactively)" | |
| echo " Sleeping 5s so you can abort..." | |
| sleep 5 | |
| fi | |
| fi | |
| # --- Section status tracking --- | |
| declare -A SECTION_STATUS | |
| declare -A SECTION_MSGS | |
| SECTION_ORDER=( | |
| "dependencies" | |
| "pkgx" | |
| "github" | |
| "age" | |
| "pyenv" | |
| "chezmoi" | |
| "antidote" | |
| "validation" | |
| ) | |
| # --- Styled summary function --- | |
| print_summary() { | |
| local color_reset="\033[0m" | |
| local color_green="\033[1;32m" | |
| local color_red="\033[1;31m" | |
| local color_yellow="\033[1;33m" | |
| local color_blue="\033[1;34m" | |
| local sep="βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" | |
| local overall_status="success" | |
| local summary_msgs="" | |
| local fail_msgs="" | |
| local warn_msgs="" | |
| echo -e "\n$sep" | |
| echo -e "π ${color_blue}DEPLOYMENT SUMMARY${color_reset}" | |
| echo -e "$sep" | |
| for section in "${SECTION_ORDER[@]}"; do | |
| local status="${SECTION_STATUS[$section]:-skipped}" | |
| local msg="${SECTION_MSGS[$section]:-}" | |
| local icon="" | |
| local color="$color_reset" | |
| if [[ "$status" == "success" ]]; then | |
| icon="β " | |
| color="$color_green" | |
| elif [[ "$status" == "fail" ]]; then | |
| icon="β" | |
| color="$color_red" | |
| overall_status="fail" | |
| fail_msgs+=" $icon ${section^}: $msg\n" | |
| elif [[ "$status" == "warning" ]]; then | |
| icon="β οΈ" | |
| color="$color_yellow" | |
| [[ "$overall_status" != "fail" ]] && overall_status="warning" | |
| warn_msgs+=" $icon ${section^}: $msg\n" | |
| else | |
| icon="β" | |
| color="$color_blue" | |
| fi | |
| echo -e "$icon ${color}${section^}${color_reset}: $msg" | |
| done | |
| echo -e "$sep" | |
| if [[ "$overall_status" == "success" ]]; then | |
| echo -e "π ${color_green}DEPLOYMENT SUCCESSFUL!${color_reset} π" | |
| elif [[ "$overall_status" == "warning" ]]; then | |
| echo -e "β οΈ ${color_yellow}DEPLOYMENT COMPLETED WITH WARNINGS${color_reset} β οΈ" | |
| else | |
| echo -e "β ${color_red}DEPLOYMENT FAILED!${color_reset} β" | |
| fi | |
| if [[ -n "$fail_msgs" ]]; then | |
| echo -e "\n${color_red}Errors:${color_reset}\n$fail_msgs" | |
| fi | |
| if [[ -n "$warn_msgs" ]]; then | |
| echo -e "\n${color_yellow}Warnings:${color_reset}\n$warn_msgs" | |
| fi | |
| echo -e "$sep\n" | |
| [[ "$overall_status" == "success" ]] && exit 0 || exit 1 | |
| } | |
| # --- Section status tracking --- | |
| declare -A SECTION_STATUS | |
| declare -A SECTION_MSGS | |
| SECTION_ORDER=( | |
| "dependencies" | |
| "pkgx" | |
| "github" | |
| "age" | |
| "pyenv" | |
| "chezmoi" | |
| "antidote" | |
| "validation" | |
| ) | |
| # Ensure ~/.local/bin is in PATH for this script and all child processes | |
| export PATH="$HOME/.pkgx:$HOME/.local/bin:$PATH" | |
| # Error handling function | |
| handle_error() { | |
| local exit_code=$? | |
| echo "β Error occurred at line ${1:-"unknown"} with exit code $exit_code" | |
| # Check if script is being sourced | |
| if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then | |
| return "$exit_code" | |
| else | |
| exit "$exit_code" | |
| fi | |
| } | |
| # --- Legacy Protection Cleanup --- | |
| # If upgrading from a version with immutable file protection, we need to clear flags once. | |
| clear_legacy_protection() { | |
| local files=(~/.gitconfig ~/.zshrc ~/.bashrc ~/.profile ~/.shellrc ~/.config/fish/config.fish) | |
| local found_immutable=0 | |
| for f in "${files[@]}"; do | |
| [ -f "$f" ] || continue | |
| if command -v lsattr >/dev/null 2>&1; then | |
| if lsattr "$f" 2>/dev/null | grep -q 'i'; then | |
| found_immutable=1 | |
| break | |
| fi | |
| elif command -v ls >/dev/null 2>&1 && [[ "$OSTYPE" == "darwin"* ]]; then | |
| if ls -lO "$f" 2>/dev/null | grep -q 'uchg'; then | |
| found_immutable=1 | |
| break | |
| fi | |
| fi | |
| done | |
| if [ "$found_immutable" -eq 1 ]; then | |
| echo "π Detecting legacy file protection. Clearing immutable flags..." | |
| for f in "${files[@]}"; do | |
| [ -f "$f" ] || continue | |
| # Use sudo for attributes if possible, but don't fail if sudo isn't available | |
| if command -v chattr >/dev/null 2>&1; then | |
| sudo chattr -i "$f" 2>/dev/null || chattr -i "$f" 2>/dev/null || true | |
| elif command -v chflags >/dev/null 2>&1; then | |
| sudo chflags nouchg "$f" 2>/dev/null || chflags nouchg "$f" 2>/dev/null || true | |
| fi | |
| sudo chmod +w "$f" 2>/dev/null || chmod +w "$f" 2>/dev/null || true | |
| done | |
| echo "β Legacy protection cleared." | |
| fi | |
| } | |
| # Set up error trap with line number tracking | |
| trap 'handle_error $LINENO' ERR | |
| # Ensure script doesn't continue on errors in pipes | |
| set -o pipefail | |
| # --- Dependency Bootstrapping --- | |
| REQUIRED_CMDS=(curl git zsh fish jq age python3) | |
| MISSING_CMDS=() | |
| # Detect OS and package manager | |
| if command -v apt &>/dev/null; then | |
| PKG_INSTALL="apt-get install -y" | |
| APT_UPDATE_NEEDED=1 | |
| elif command -v dnf &>/dev/null; then | |
| PKG_INSTALL="dnf install -y" | |
| APT_UPDATE_NEEDED=0 | |
| elif command -v yum &>/dev/null; then | |
| PKG_INSTALL="yum install -y" | |
| APT_UPDATE_NEEDED=0 | |
| elif command -v apk &>/dev/null; then | |
| PKG_INSTALL="apk add --no-cache" | |
| APT_UPDATE_NEEDED=0 | |
| elif command -v brew &>/dev/null; then | |
| PKG_INSTALL="brew install" | |
| APT_UPDATE_NEEDED=0 | |
| else | |
| PKG_INSTALL="" | |
| APT_UPDATE_NEEDED=0 | |
| fi | |
| # Function to install missing dependencies | |
| install_missing_dependencies() { | |
| local missing=("$@") | |
| echo "π Missing dependencies: ${missing[*]}" | |
| if [ -n "$PKG_INSTALL" ]; then | |
| # Detect if we need sudo (not running as root and sudo is available) | |
| USE_SUDO="" | |
| if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then | |
| USE_SUDO="sudo" | |
| fi | |
| if [ "${APT_UPDATE_NEEDED:-0}" -eq 1 ]; then | |
| echo "π Running apt-get update..." | |
| $USE_SUDO apt-get update | |
| APT_UPDATE_NEEDED=0 | |
| fi | |
| for pkg in "${missing[@]}"; do | |
| echo "π₯ Installing $pkg..." | |
| if ! $USE_SUDO $PKG_INSTALL $pkg; then | |
| echo "β Failed to install $pkg. Please install it manually." | |
| exit 1 | |
| fi | |
| done | |
| else | |
| echo "β No supported package manager found. Please install: ${missing[*]} manually." | |
| exit 1 | |
| fi | |
| } | |
| # Check for missing commands (except pkgx) | |
| for cmd in "${REQUIRED_CMDS[@]}"; do | |
| if ! command -v "$cmd" &>/dev/null; then | |
| MISSING_CMDS+=("$cmd") | |
| fi | |
| done | |
| # Check and install missing dependencies | |
| if [ "${#MISSING_CMDS[@]}" -gt 0 ]; then | |
| if install_missing_dependencies "${MISSING_CMDS[@]}"; then | |
| SECTION_STATUS[dependencies]="success" | |
| SECTION_MSGS[dependencies]="All dependencies installed." | |
| else | |
| SECTION_STATUS[dependencies]="fail" | |
| SECTION_MSGS[dependencies]="Failed to install dependencies." | |
| fi | |
| else | |
| echo "β All required dependencies are installed." | |
| SECTION_STATUS[dependencies]="success" | |
| SECTION_MSGS[dependencies]="All dependencies already present." | |
| fi | |
| # Ensure locales are generated on Linux to fix SSH warnings | |
| if [ -f /etc/debian_version ] && command -v locale-gen >/dev/null; then | |
| # Detect sudo | |
| _SUDO="" | |
| if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then | |
| _SUDO="sudo" | |
| fi | |
| if ! locale -a 2>/dev/null | grep -qi "en_US.utf8"; then | |
| echo "π Generating en_US.UTF-8 locale..." | |
| $_SUDO locale-gen en_US.UTF-8 || true | |
| $_SUDO update-locale LANG=en_US.UTF-8 || true | |
| fi | |
| fi | |
| # Always install pkgx using the official installer if missing | |
| if ! command -v pkgx &>/dev/null; then | |
| echo "π₯ Installing pkgx using the official installer..." | |
| if curl -fsSL https://pkgx.sh | bash; then | |
| echo "β pkgx installed successfully" | |
| SECTION_STATUS[pkgx]="success" | |
| SECTION_MSGS[pkgx]="pkgx installed successfully." | |
| elif [ ! -x "$(command -v pkgx 2>/dev/null)" ]; then | |
| # Fallback: find first writable dir in PATH | |
| for dir in $(echo "$PATH" | tr ':' ' '); do | |
| if [ -w "$dir" ]; then | |
| echo "π₯ Retrying pkgx install to $dir ..." | |
| mkdir -p "$dir" | |
| if curl -fsSL https://pkgx.sh | bash -s -- -b "$dir"; then | |
| export PATH="$dir:$PATH" | |
| echo "β pkgx installed successfully in $dir" | |
| SECTION_STATUS[pkgx]="success" | |
| SECTION_MSGS[pkgx]="pkgx installed successfully in $dir." | |
| break | |
| fi | |
| fi | |
| done | |
| # If still not found | |
| if ! command -v pkgx >/dev/null 2>&1; then | |
| echo "β Failed to install pkgx! Please install it manually: curl -fsSL https://pkgx.sh | bash" | |
| SECTION_STATUS[pkgx]="fail" | |
| SECTION_MSGS[pkgx]="Failed to install pkgx." | |
| print_summary | |
| fi | |
| fi | |
| else | |
| echo "β pkgx is already installed." | |
| SECTION_STATUS[pkgx]="success" | |
| SECTION_MSGS[pkgx]="pkgx already installed." | |
| fi | |
| # --- Install essential tools via pkgx if missing --- | |
| # --- Install essential tools via pkgx if missing --- | |
| for tool in git zsh jq age python3; do | |
| if ! command -v "$tool" >/dev/null 2>&1; then | |
| echo "π¦ Installing $tool with pkgx..." | |
| pkgx "$tool" --version >/dev/null 2>&1 || true | |
| fi | |
| done | |
| # --- GitHub CLI Authentication Setup (before any git operations) --- | |
| # Cleanup broken symlinks for critical files (e.g. .gitconfig) to prevent git config failures | |
| if [ -L "$HOME/.gitconfig" ] && [ ! -e "$HOME/.gitconfig" ]; then | |
| echo "π§Ή Removing broken .gitconfig symlink..." | |
| rm "$HOME/.gitconfig" | |
| fi | |
| # Ensure gh CLI is available everywhere (shim, not just alias) | |
| if ! command -v gh >/dev/null 2>&1; then | |
| if command -v pkgm >/dev/null 2>&1; then | |
| pkgm shim gh || echo "β οΈ Failed to shim gh (pkgm)" | |
| echo "βΉοΈ Created persistent gh shim with pkgm." | |
| elif command -v pkgx >/dev/null 2>&1; then | |
| pkgx pkgm shim gh || echo "β οΈ Failed to shim gh (pkgx)" | |
| echo "βΉοΈ Created persistent gh shim with pkgx pkgm." | |
| fi | |
| fi | |
| # Authenticate with GitHub CLI and set up git credential helper | |
| if command -v gh >/dev/null 2>&1; then | |
| # Clear legacy protection if we are about to touch git config | |
| clear_legacy_protection | |
| if [ -z "${APPLY_ONLY:-}" ] || [ "${APPLY_ONLY}" = "0" ]; then | |
| if ! gh auth status --hostname github.com 2>&1 | grep -q 'Logged in to github.com'; then | |
| echo "π Launching GitHub CLI authentication flow..." | |
| if ! gh auth login; then | |
| echo "β GitHub authentication failed. You will be prompted for username/password on git operations." | |
| echo "For best results, authenticate with GitHub CLI before running this script." | |
| SECTION_STATUS[github]="warning" | |
| SECTION_MSGS[github]="GitHub CLI authentication failed. You may be prompted for credentials." | |
| fi | |
| fi | |
| fi | |
| # Always try to setup git if gh is available, but skip error if already configured | |
| if ! git config --global credential.helper | grep -q 'gh'; then | |
| echo "π§ Configuring git to use gh as credential helper..." | |
| if ! gh auth setup-git; then | |
| echo "β οΈ Failed to set up git credential helper with gh. You may be prompted for credentials." | |
| SECTION_STATUS[github]="warning" | |
| SECTION_MSGS[github]="Failed to set up git credential helper with gh." | |
| fi | |
| fi | |
| if [[ -z "${SECTION_STATUS[github]:-}" ]]; then | |
| SECTION_STATUS[github]="success" | |
| SECTION_MSGS[github]="GitHub CLI authenticated and git credential helper configured." | |
| fi | |
| else | |
| echo "β GitHub CLI (gh) is still not available after shimming. Please ensure ~/.local/bin is in your PATH and try again." | |
| SECTION_STATUS[github]="fail" | |
| SECTION_MSGS[github]="GitHub CLI not available after shimming." | |
| fi | |
| # --- Deployment mode selection --- | |
| DEPLOY_MODE="" | |
| CUSTOM_DATA_ARGS=() | |
| EXCLUDE_ARGS=() | |
| APPLY_ONLY="" | |
| # Parse --mode argument or prompt interactively | |
| for arg in "$@"; do | |
| case $arg in | |
| --mode=*) DEPLOY_MODE="${arg#*=}" ;; | |
| --mode) shift; DEPLOY_MODE="$1" ;; | |
| --apply-only) APPLY_ONLY="1" ;; | |
| --custom-*) # For custom mode, collect feature flags as --custom-foo=bar | |
| CUSTOM_DATA_ARGS+=("${arg#--}") | |
| ;; | |
| esac | |
| done | |
| if [ -z "$DEPLOY_MODE" ]; then | |
| printf "\nChoose deployment mode:\n" | |
| echo " 1) Full - Everything (personal + work, all plugins, all features)" | |
| echo " 2) Server - No SSH keys, no heavy dependencies, but most config" | |
| echo " 3) Custom - Choose each feature interactively" | |
| if { [ -n "${APPLY_ONLY:-}" ] && [ "${APPLY_ONLY}" != "0" ]; } || { [ -n "${NONINTERACTIVE:-}" ] && [ "${NONINTERACTIVE}" != "0" ]; }; then | |
| mode_choice=1 | |
| else | |
| read -p "Enter choice [1-3, default 1]: " mode_choice | |
| fi | |
| case "$mode_choice" in | |
| 2) DEPLOY_MODE="server" ;; | |
| 3) DEPLOY_MODE="custom" ;; | |
| *) DEPLOY_MODE="full" ;; | |
| esac | |
| fi | |
| # Exclusions for modes are now handled via .chezmoiexclude.tmpl | |
| if [ "$DEPLOY_MODE" = "server" ]; then | |
| echo "π Server mode: SSH keys will be excluded via .chezmoiexclude.tmpl" | |
| fi | |
| printf "\nπ οΈ Deployment mode: %s\n" "$DEPLOY_MODE" | |
| if [ "$DEPLOY_MODE" = "custom" ]; then | |
| # For each major feature, prompt user and build --data args for chezmoi | |
| printf "\nCustom mode: Select which features to enable. (y/n, default y)\n" | |
| read -p "Include personal SSH keys? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_personal_ssh=false") || CUSTOM_DATA_ARGS+=("custom_include_personal_ssh=true") | |
| read -p "Include work SSH keys? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_work_ssh=false") || CUSTOM_DATA_ARGS+=("custom_include_work_ssh=true") | |
| read -p "Include custom prompt (Powerlevel10k)? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_prompt=false") || CUSTOM_DATA_ARGS+=("custom_include_prompt=true") | |
| read -p "Include all plugins? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_all_plugins=false") || CUSTOM_DATA_ARGS+=("custom_include_all_plugins=true") | |
| read -p "Include basic plugins? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_basic_plugins=false") || CUSTOM_DATA_ARGS+=("custom_include_basic_plugins=true") | |
| read -p "Include all languages/managers? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_all_languages=false") || CUSTOM_DATA_ARGS+=("custom_include_all_languages=true") | |
| read -p "Include LLM/GPT wrapper? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_llm_wrapper=false") || CUSTOM_DATA_ARGS+=("custom_include_llm_wrapper=true") | |
| read -p "Include all aliases? [Y/n]: " ans; [[ "$ans" =~ ^[Nn]$ ]] && CUSTOM_DATA_ARGS+=("custom_include_all_aliases=false") || CUSTOM_DATA_ARGS+=("custom_include_all_aliases=true") | |
| fi | |
| echo "οΏ½π Starting automated dotfiles deployment..." | |
| echo "π Repository: https://github.com/anschmieg/dotfiles.git" | |
| echo "" | |
| # --- Ensure pkgx/pkgm has at least one package installed for validation --- | |
| if command -v pkgx >/dev/null 2>&1; then | |
| if ! pkgx pkgm list | grep -q fastfetch; then | |
| echo "π¦ Installing fastfetch for pkgx/pkgm validation..." | |
| pkgx pkgm install fastfetch || true | |
| fi | |
| fi | |
| # Ensure ~/.local/bin is in PATH first | |
| export PATH="$HOME/.local/bin:$PATH" | |
| # --- Auto-configure git user from GitHub CLI if possible --- | |
| if command -v gh &>/dev/null; then | |
| GIT_EMAIL=$(gh api user/emails --jq 'map(select(.primary == true and .verified == true)) | .[0].email' 2>/dev/null || true) | |
| GIT_NAME=$(gh api user --jq '.name' 2>/dev/null || true) | |
| GIT_LOGIN=$(gh api user --jq '.login' 2>/dev/null || true) | |
| GIT_ID=$(gh api user --jq '.id' 2>/dev/null || true) | |
| # Always use GitHub username and noreply email format | |
| if [ -n "$GIT_LOGIN" ] && [ -n "$GIT_ID" ]; then | |
| GIT_NAME="$GIT_LOGIN" | |
| GIT_EMAIL="${GIT_ID}+${GIT_LOGIN}@users.noreply.github.com" | |
| echo "βΉοΈ Using GitHub noreply email: $GIT_EMAIL" | |
| fi | |
| if ! git config --global user.email &>/dev/null && [ -n "$GIT_EMAIL" ]; then | |
| git config --global user.email "$GIT_EMAIL" | |
| echo "β Set git user.email: $GIT_EMAIL" | |
| fi | |
| if ! git config --global user.name &>/dev/null && [ -n "$GIT_NAME" ]; then | |
| git config --global user.name "$GIT_NAME" | |
| echo "β Set git user.name: $GIT_NAME" | |
| fi | |
| fi | |
| # Install chezmoi if not present | |
| if ! command -v chezmoi &>/dev/null; then | |
| echo "π₯ Installing chezmoi..." | |
| # Force installation to ~/.local/bin directory | |
| mkdir -p ~/.local/bin | |
| if curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin; then | |
| echo "β chezmoi installation completed" | |
| SECTION_STATUS[chezmoi]="success" | |
| SECTION_MSGS[chezmoi]="chezmoi installed successfully." | |
| else | |
| echo "β Failed to install chezmoi!" | |
| echo "π‘ Please install chezmoi manually:" | |
| echo " curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin" | |
| echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| SECTION_STATUS[chezmoi]="fail" | |
| SECTION_MSGS[chezmoi]="Failed to install chezmoi." | |
| print_summary | |
| fi | |
| else | |
| SECTION_STATUS[chezmoi]="success" | |
| SECTION_MSGS[chezmoi]="chezmoi already installed." | |
| fi | |
| # Verify chezmoi is available and set the command | |
| CHEZMOI_CMD="" | |
| if command -v chezmoi &>/dev/null; then | |
| CHEZMOI_CMD="chezmoi" | |
| echo "β chezmoi found in PATH: $(command -v chezmoi)" | |
| elif [[ -x "$HOME/.local/bin/chezmoi" ]]; then | |
| CHEZMOI_CMD="$HOME/.local/bin/chezmoi" | |
| echo "β chezmoi found at ~/.local/bin/chezmoi" | |
| elif [[ -x "./bin/chezmoi" ]]; then | |
| CHEZMOI_CMD="./bin/chezmoi" | |
| echo "β chezmoi found at ./bin/chezmoi" | |
| else | |
| echo "β chezmoi not found! Checking installation directory..." | |
| echo "π Current directory: $(pwd)" | |
| echo "π Contents of ~/.local/bin/:" | |
| ls -la "$HOME/.local/bin/" 2>/dev/null || echo " Directory ~/.local/bin/ does not exist" | |
| echo "π Contents of ./bin/:" | |
| ls -la "./bin/" 2>/dev/null || echo " Directory ./bin/ does not exist" | |
| echo "" | |
| echo "π‘ Manual installation required:" | |
| echo " curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin" | |
| echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| exit 1 | |
| fi | |
| # Parse --verbose or -v flag | |
| VERBOSE=0 | |
| for arg in "$@"; do | |
| if [ "$arg" = "--verbose" ] || [ "$arg" = "-v" ]; then | |
| VERBOSE=1 | |
| break | |
| fi | |
| # Remove the flag from positional args | |
| shift | |
| done | |
| # Helper to run commands quietly unless VERBOSE is set | |
| run_quiet() { | |
| if [ "$VERBOSE" -eq 1 ]; then | |
| "$@" | |
| else | |
| "$@" >/dev/null 2>&1 | |
| fi | |
| } | |
| # Check for age key | |
| if [[ ! -f ~/.config/age/chezmoi.txt ]]; then | |
| echo "π Age encryption key not found at ~/.config/age/chezmoi.txt" | |
| echo "" | |
| # Check for environment variable first | |
| if [[ -n "${CHEZMOI_AGE_KEY:-}" ]]; then | |
| echo "π Found age key in CHEZMOI_AGE_KEY environment variable" | |
| mkdir -p ~/.config/age | |
| echo "$CHEZMOI_AGE_KEY" >~/.config/age/chezmoi.txt | |
| chmod 600 ~/.config/age/chezmoi.txt | |
| echo "β Age key saved from environment variable!" | |
| SECTION_STATUS[age]="success" | |
| SECTION_MSGS[age]="Age key loaded from environment variable." | |
| choice="none" | |
| # Check if we're running interactively | |
| elif [[ -t 0 ]] && { [ -z "${APPLY_ONLY:-}" ] || [ "${APPLY_ONLY}" = "0" ]; }; then | |
| # Interactive mode - show menu | |
| echo "Choose an option to provide your age key:" | |
| echo " 1) Paste the key content (I'll create the file)" | |
| echo " 2) I'll copy the file manually" | |
| echo "" | |
| read -p "Enter choice (1 or 2): " choice | |
| else | |
| # Non-interactive mode (piped script) - default to paste option | |
| echo "π€ Running in non-interactive mode. Starting age key paste mode..." | |
| echo "" | |
| echo "π Please paste your age key content now and press Ctrl+D when finished:" | |
| echo " (Age key format: AGE-SECRET-KEY-1...)" | |
| echo "" | |
| if { [ -n "${APPLY_ONLY:-}" ] && [ "${APPLY_ONLY}" != "0" ]; } || { [ -n "${NONINTERACTIVE:-}" ] && [ "${NONINTERACTIVE}" != "0" ]; }; then | |
| # In non-interactive or apply-only mode we prefer not to wait for interactive input; if an age key isn't provided, skip encrypted files with a warning | |
| if [[ -n "${CHEZMOI_AGE_KEY:-}" ]]; then | |
| choice="none" | |
| else | |
| echo "β οΈ apply-only mode: age key not provided β encrypted files will be skipped with a warning." | |
| SECTION_STATUS[age]="warning" | |
| SECTION_MSGS[age]="Age key missing in apply-only mode; encrypted files will be skipped." | |
| choice="none" | |
| fi | |
| else | |
| choice="1" | |
| fi | |
| fi | |
| case $choice in | |
| none) | |
| # Already handled by environment variable or skip | |
| : | |
| ;; | |
| 1) | |
| echo "" | |
| echo "π Please paste your age key content (from ~/.config/age/chezmoi.txt):" | |
| echo " (Press Ctrl+D when finished)" | |
| echo "" | |
| # Create the directory | |
| mkdir -p ~/.config/age | |
| # Read the key content with timeout in non-interactive mode | |
| if [[ ! -t 0 ]]; then | |
| echo "β³ Waiting for age key input... (timeout in 30 seconds)" | |
| if ! key_content=$(timeout 30s cat); then | |
| echo "" | |
| echo "β No key input received or timeout reached." | |
| SECTION_STATUS[age]="fail" | |
| SECTION_MSGS[age]="No age key input received or timeout reached." | |
| print_summary | |
| fi | |
| else | |
| # Interactive mode | |
| if ! key_content=$(cat); then | |
| echo "β Failed to read key content" | |
| SECTION_STATUS[age]="fail" | |
| SECTION_MSGS[age]="Failed to read key content." | |
| print_summary | |
| fi | |
| fi | |
| if [[ -n "$key_content" ]]; then | |
| echo "$key_content" >~/.config/age/chezmoi.txt | |
| chmod 600 ~/.config/age/chezmoi.txt | |
| echo "β Age key saved successfully!" | |
| SECTION_STATUS[age]="success" | |
| SECTION_MSGS[age]="Age key pasted and saved." | |
| else | |
| echo "β No key content provided" | |
| SECTION_STATUS[age]="fail" | |
| SECTION_MSGS[age]="No key content provided." | |
| print_summary | |
| fi | |
| ;; | |
| 2) | |
| echo "" | |
| echo "π Please manually copy your age key file:" | |
| echo " Source: ~/.config/age/chezmoi.txt (from your existing machine)" | |
| echo " Target: ~/.config/age/chezmoi.txt (on this machine)" | |
| echo "" | |
| echo "π‘ Run these commands:" | |
| echo " mkdir -p ~/.config/age" | |
| echo " # Copy chezmoi.txt from your existing machine to ~/.config/age/" | |
| echo "" | |
| read -p "Press Enter when you've copied the file..." | |
| # Verify the file exists | |
| if [[ ! -f ~/.config/age/chezmoi.txt ]]; then | |
| echo "β Age key file still not found. Please copy it and run this script again." | |
| SECTION_STATUS[age]="fail" | |
| SECTION_MSGS[age]="Age key file not found after manual copy." | |
| print_summary | |
| fi | |
| echo "β Age key file found!" | |
| SECTION_STATUS[age]="success" | |
| SECTION_MSGS[age]="Age key file found after manual copy." | |
| ;; | |
| *) | |
| echo "β Invalid choice. Please run the script again and choose 1 or 2." | |
| SECTION_STATUS[age]="fail" | |
| SECTION_MSGS[age]="Invalid choice for age key input." | |
| print_summary | |
| ;; | |
| esac | |
| else | |
| SECTION_STATUS[age]="success" | |
| SECTION_MSGS[age]="Age key already present." | |
| fi | |
| ############################################################### | |
| # Remove Homebrew pyenv/pyenv-virtualenv if present (avoid conflicts) | |
| if command -v brew >/dev/null 2>&1; then | |
| if brew list pyenv >/dev/null 2>&1; then | |
| echo "π§Ή Uninstalling Homebrew pyenv to avoid conflicts..." | |
| brew uninstall --ignore-dependencies pyenv || true | |
| fi | |
| if brew list pyenv-virtualenv >/dev/null 2>&1; then | |
| echo "π§Ή Uninstalling Homebrew pyenv-virtualenv to avoid conflicts..." | |
| brew uninstall --ignore-dependencies pyenv-virtualenv || true | |
| fi | |
| fi | |
| # Parse custom flags into shell variables for use in logic | |
| custom_include_all_languages="${custom_include_all_languages:-}" | |
| if [ -z "$custom_include_all_languages" ]; then | |
| # Try to get from environment if not set (for custom mode) | |
| custom_include_all_languages="${CUSTOM_INCLUDE_ALL_LANGUAGES:-}" | |
| fi | |
| # Install and initialize pyenv only if in full mode, or in custom mode with all languages/managers enabled | |
| if [ "$DEPLOY_MODE" = "full" ] || { [ "$DEPLOY_MODE" = "custom" ] && [ "${custom_include_all_languages}" = "true" ]; }; then | |
| if [ -n "${APPLY_ONLY:-}" ] && [ "${APPLY_ONLY}" != "0" ]; then | |
| echo "βΉοΈ Skipping pyenv installation in apply-only mode" | |
| SECTION_STATUS[pyenv]="skipped" | |
| SECTION_MSGS[pyenv]="Skipped pyenv installation in apply-only mode." | |
| else | |
| # Install official pyenv (and pyenv-virtualenv) if not present | |
| if [ ! -d "$HOME/.pyenv" ]; then | |
| echo "π₯ Installing official pyenv and plugins..." | |
| if curl https://pyenv.run | bash; then | |
| SECTION_STATUS[pyenv]="success" | |
| SECTION_MSGS[pyenv]="pyenv installed successfully." | |
| else | |
| SECTION_STATUS[pyenv]="fail" | |
| SECTION_MSGS[pyenv]="Failed to install pyenv." | |
| print_summary | |
| fi | |
| else | |
| SECTION_STATUS[pyenv]="success" | |
| SECTION_MSGS[pyenv]="pyenv already installed." | |
| fi | |
| # Official pyenv initialization (for this session) | |
| export PYENV_ROOT="$HOME/.pyenv" | |
| export PATH="$PYENV_ROOT/bin:$PATH" | |
| if command -v pyenv >/dev/null 2>&1; then | |
| eval "$(pyenv init --path)" | |
| eval "$(pyenv virtualenv-init -)" | |
| SECTION_STATUS[pyenv]="success" | |
| SECTION_MSGS[pyenv]="pyenv initialized for this session." | |
| fi | |
| fi | |
| else | |
| SECTION_STATUS[pyenv]="skipped" | |
| SECTION_MSGS[pyenv]="Skipped pyenv install and initialization (not enabled in this mode)." | |
| echo "βΉοΈ Skipping pyenv installation and initialization (not enabled in this mode)." | |
| fi | |
| echo "π Deploying dotfiles..." | |
| echo " This will run all setup scripts automatically:" | |
| echo " β’ Install dependencies and tools" | |
| echo " β’ Configure package managers" | |
| echo " β’ Set up Zsh with plugins" | |
| echo " β’ Run validation checks" | |
| echo "" | |
| # Export deployment mode and custom flags as environment variables for chezmoi templating | |
| export DEPLOY_MODE="$DEPLOY_MODE" | |
| for arg in "${CUSTOM_DATA_ARGS[@]}"; do | |
| key="$(echo "$arg" | cut -d= -f1)" | |
| val="$(echo "$arg" | cut -d= -f2-)" | |
| export "${key^^}"="$val" | |
| done | |
| # Robust chezmoi init/apply sequence | |
| if $CHEZMOI_CMD init https://github.com/anschmieg/dotfiles.git; then | |
| SECTION_STATUS[chezmoi]="success" | |
| SECTION_MSGS[chezmoi]="chezmoi initialized with your dotfiles repo." | |
| # Build an exclude list if we need to skip encrypted files when no key is present | |
| if [[ -z "${CHEZMOI_AGE_KEY:-}" && ! -f ~/.config/age/chezmoi.txt ]]; then | |
| echo "β οΈ No CHEZMOI_AGE_KEY provided; skipping encrypted files via --exclude encrypted" | |
| EXCLUDE_ARGS+=(--exclude encrypted) | |
| SECTION_STATUS[age]="warning" | |
| SECTION_MSGS[age]="Skipped encrypted files; provide CHEZMOI_AGE_KEY to apply them." | |
| fi | |
| if $CHEZMOI_CMD apply --force "${EXCLUDE_ARGS[@]}"; then | |
| SECTION_STATUS[validation]="success" | |
| SECTION_MSGS[validation]="chezmoi apply and validation completed successfully." | |
| else | |
| SECTION_STATUS[validation]="fail" | |
| SECTION_MSGS[validation]="Deployment failed during chezmoi apply! Check the output above for errors." | |
| print_summary | |
| fi | |
| else | |
| SECTION_STATUS[chezmoi]="fail" | |
| SECTION_MSGS[chezmoi]="Deployment failed during chezmoi init! Check the output above for errors." | |
| print_summary | |
| fi | |
| print_summary | |
| # Attempt to change default shell to zsh if it isn't already | |
| if command -v zsh >/dev/null; then | |
| ZSH_PATH=$(command -v zsh) | |
| # Simple check against $SHELL | |
| if [ "$SHELL" != "$ZSH_PATH" ]; then | |
| echo "π Changing default shell to $ZSH_PATH..." | |
| if chsh -s "$ZSH_PATH"; then | |
| echo "β Default shell changed to zsh." | |
| else | |
| echo "β οΈ Failed to change default shell. You may need to run: chsh -s $ZSH_PATH" | |
| fi | |
| fi | |
| fi | |
| echo "" | |
| echo "π Starting new shell session with your configuration..." | |
| if [[ -n "${APPLY_ONLY:-}" ]] && [[ "${APPLY_ONLY}" = "1" ]]; then | |
| echo "βΉοΈ Apply-only mode: not launching an interactive shell" | |
| else | |
| exec zsh | |
| fi |
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 | |
| # π One-Command Dotfiles Deployment Script | |
| # | |
| # Quick deployment: curl -sfL https://nothing.pink/zsh | bash | |
| # Or for interactivity: bash <(curl -sfL https://nothing.pink/zsh) < /dev/tty | |
| # | |
| # What this script does: | |
| # 1. Installs chezmoi if needed | |
| # 2. Helps you set up your age encryption key | |
| # 3. Deploys your complete dotfiles setup | |
| # 4. Runs all automated configuration scripts | |
| # 5. Validates the installation (37 checks) | |
| # | |
| # Prerequisites: Your age encryption key from ~/.config/age/chezmoi.txt | |
| set -euo pipefail | |
| echo "π Starting automated dotfiles deployment..." | |
| echo "π Repository: https://github.com/anschmieg/dotfiles.git" | |
| echo "" | |
| # Ensure ~/.local/bin is in PATH first | |
| export PATH="$HOME/.local/bin:$PATH" | |
| # --- Auto-configure git user from GitHub CLI if possible --- | |
| if command -v gh &>/dev/null; then | |
| GIT_EMAIL=$(gh api user/emails --jq 'map(select(.primary == true and .verified == true)) | .[0].email' 2>/dev/null || true) | |
| GIT_NAME=$(gh api user --jq '.name' 2>/dev/null || true) | |
| GIT_LOGIN=$(gh api user --jq '.login' 2>/dev/null || true) | |
| GIT_ID=$(gh api user --jq '.id' 2>/dev/null || true) | |
| # Fallback: use login if name is empty or null | |
| if [ -z "$GIT_NAME" ] || [ "$GIT_NAME" = "null" ]; then | |
| if [ -n "$GIT_LOGIN" ]; then | |
| GIT_NAME="$GIT_LOGIN" | |
| fi | |
| fi | |
| # Fallback: use noreply email if email is empty | |
| if [ -z "$GIT_EMAIL" ] && [ -n "$GIT_ID" ] && [ -n "$GIT_LOGIN" ]; then | |
| GIT_EMAIL="${GIT_ID}+${GIT_LOGIN}@users.noreply.github.com" | |
| echo "βΉοΈ Using inferred GitHub noreply email: $GIT_EMAIL" | |
| fi | |
| if ! git config --global user.email &>/dev/null && [ -n "$GIT_EMAIL" ]; then | |
| git config --global user.email "$GIT_EMAIL" | |
| echo "β Set git user.email from GitHub CLI: $GIT_EMAIL" | |
| fi | |
| if ! git config --global user.name &>/dev/null && [ -n "$GIT_NAME" ]; then | |
| git config --global user.name "$GIT_NAME" | |
| echo "β Set git user.name from GitHub CLI: $GIT_NAME" | |
| fi | |
| fi | |
| # Install chezmoi if not present | |
| if ! command -v chezmoi &>/dev/null; then | |
| echo "π₯ Installing chezmoi..." | |
| # Force installation to ~/.local/bin directory | |
| mkdir -p ~/.local/bin | |
| if curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin; then | |
| echo "β chezmoi installation completed" | |
| else | |
| echo "β Failed to install chezmoi!" | |
| echo "π‘ Please install chezmoi manually:" | |
| echo " curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin" | |
| echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| exit 1 | |
| fi | |
| fi | |
| # Verify chezmoi is available and set the command | |
| CHEZMOI_CMD="" | |
| if command -v chezmoi &>/dev/null; then | |
| CHEZMOI_CMD="chezmoi" | |
| echo "β chezmoi found in PATH: $(command -v chezmoi)" | |
| elif [[ -x "$HOME/.local/bin/chezmoi" ]]; then | |
| CHEZMOI_CMD="$HOME/.local/bin/chezmoi" | |
| echo "β chezmoi found at ~/.local/bin/chezmoi" | |
| elif [[ -x "./bin/chezmoi" ]]; then | |
| CHEZMOI_CMD="./bin/chezmoi" | |
| echo "β chezmoi found at ./bin/chezmoi" | |
| else | |
| echo "β chezmoi not found! Checking installation directory..." | |
| echo "π Current directory: $(pwd)" | |
| echo "π Contents of ~/.local/bin/:" | |
| ls -la "$HOME/.local/bin/" 2>/dev/null || echo " Directory ~/.local/bin/ does not exist" | |
| echo "π Contents of ./bin/:" | |
| ls -la "./bin/" 2>/dev/null || echo " Directory ./bin/ does not exist" | |
| echo "" | |
| echo "π‘ Manual installation required:" | |
| echo " curl -sfL https://get.chezmoi.io | sh -s -- -b ~/.local/bin" | |
| echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" | |
| exit 1 | |
| fi | |
| # Parse --verbose or -v flag | |
| VERBOSE=0 | |
| for arg in "$@"; do | |
| if [ "$arg" = "--verbose" ] || [ "$arg" = "-v" ]; then | |
| VERBOSE=1 | |
| break | |
| fi | |
| # Remove the flag from positional args | |
| shift | |
| done | |
| # Helper to run commands quietly unless VERBOSE is set | |
| run_quiet() { | |
| if [ "$VERBOSE" -eq 1 ]; then | |
| "$@" | |
| else | |
| "$@" >/dev/null 2>&1 | |
| fi | |
| } | |
| # Check for age key | |
| if [[ ! -f ~/.config/age/chezmoi.txt ]]; then | |
| echo "π Age encryption key not found at ~/.config/age/chezmoi.txt" | |
| echo "" | |
| # Check for environment variable first | |
| if [[ -n "${CHEZMOI_AGE_KEY:-}" ]]; then | |
| echo "π Found age key in CHEZMOI_AGE_KEY environment variable" | |
| mkdir -p ~/.config/age | |
| echo "$CHEZMOI_AGE_KEY" >~/.config/age/chezmoi.txt | |
| chmod 600 ~/.config/age/chezmoi.txt | |
| echo "β Age key saved from environment variable!" | |
| # Check if we're running interactively | |
| elif [[ -t 0 ]]; then | |
| # Interactive mode - show menu | |
| echo "Choose an option to provide your age key:" | |
| echo " 1) Paste the key content (I'll create the file)" | |
| echo " 2) I'll copy the file manually" | |
| echo "" | |
| read -p "Enter choice (1 or 2): " choice | |
| else | |
| # Non-interactive mode (piped script) - default to paste option | |
| echo "π€ Running in non-interactive mode. Starting age key paste mode..." | |
| echo "" | |
| echo "π Please paste your age key content now and press Ctrl+D when finished:" | |
| echo " (Age key format: AGE-SECRET-KEY-1...)" | |
| echo "" | |
| choice="1" | |
| fi | |
| case $choice in | |
| 1) | |
| echo "" | |
| echo "π Please paste your age key content (from ~/.config/age/chezmoi.txt):" | |
| echo " (Press Ctrl+D when finished)" | |
| echo "" | |
| # Create the directory | |
| mkdir -p ~/.config/age | |
| # Read the key content with timeout in non-interactive mode | |
| if [[ ! -t 0 ]]; then | |
| echo "β³ Waiting for age key input... (timeout in 30 seconds)" | |
| if ! key_content=$(timeout 30s cat); then | |
| echo "" | |
| echo "β No key input received or timeout reached." | |
| echo "π‘ Alternative: Run the script interactively instead:" | |
| echo " 1. Download: curl -sL https://nothing.pink/zsh > deploy.sh" | |
| echo " 2. Run: bash deploy.sh" | |
| echo " 3. Or manually setup:" | |
| echo " mkdir -p ~/.config/age" | |
| echo " # Copy your age key to ~/.config/age/chezmoi.txt" | |
| echo " chezmoi init --apply https://github.com/anschmieg/dotfiles.git" | |
| exit 1 | |
| fi | |
| else | |
| # Interactive mode | |
| if ! key_content=$(cat); then | |
| echo "β Failed to read key content" | |
| exit 1 | |
| fi | |
| fi | |
| if [[ -n "$key_content" ]]; then | |
| echo "$key_content" >~/.config/age/chezmoi.txt | |
| chmod 600 ~/.config/age/chezmoi.txt | |
| echo "β Age key saved successfully!" | |
| else | |
| echo "β No key content provided" | |
| exit 1 | |
| fi | |
| ;; | |
| 2) | |
| echo "" | |
| echo "π Please manually copy your age key file:" | |
| echo " Source: ~/.config/age/chezmoi.txt (from your existing machine)" | |
| echo " Target: ~/.config/age/chezmoi.txt (on this machine)" | |
| echo "" | |
| echo "π‘ Run these commands:" | |
| echo " mkdir -p ~/.config/age" | |
| echo " # Copy chezmoi.txt from your existing machine to ~/.config/age/" | |
| echo "" | |
| read -p "Press Enter when you've copied the file..." | |
| # Verify the file exists | |
| if [[ ! -f ~/.config/age/chezmoi.txt ]]; then | |
| echo "β Age key file still not found. Please copy it and run this script again." | |
| exit 1 | |
| fi | |
| echo "β Age key file found!" | |
| ;; | |
| *) | |
| echo "β Invalid choice. Please run the script again and choose 1 or 2." | |
| exit 1 | |
| ;; | |
| esac | |
| fi | |
| # Deploy dotfiles | |
| echo "π Deploying dotfiles..." | |
| echo " This will run all setup scripts automatically:" | |
| echo " β’ Install dependencies and tools" | |
| echo " β’ Configure package managers" | |
| echo " β’ Set up Zsh with plugins" | |
| echo " β’ Run validation checks" | |
| echo "" | |
| if $CHEZMOI_CMD init --apply https://github.com/anschmieg/dotfiles.git; then | |
| echo "" | |
| echo "β Deployment completed successfully!" | |
| echo "" | |
| echo "π What was set up:" | |
| echo " β’ App package manager with auto-detection" | |
| echo " β’ Zsh with Antidote plugin manager" | |
| echo " β’ Git configuration" | |
| echo " β’ SSH configuration" | |
| echo " β’ Development tools and utilities" | |
| echo "" | |
| echo "π Validation results should appear above βοΈ" | |
| echo "π‘ If any validations failed, run: chezmoi cd && ./scripts/troubleshoot.sh" | |
| else | |
| echo "β Deployment failed! Check the output above for errors." | |
| echo "π For troubleshooting, visit: https://github.com/anschmieg/dotfiles" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "π Starting new shell session with your configuration..." | |
| exec zsh |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment