Last active
April 11, 2026 03:50
-
-
Save mturilin/7f87b1f3b1c26115ac6c82b94f9e0cc4 to your computer and use it in GitHub Desktop.
Headless Arch Linux dev environment bootstrap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ─── Headless Bootstrap ────────────────────────────────────────────────────── | |
| # Bootstraps a bare Arch Linux headless environment (e.g. LXC) from nothing | |
| # to a working chezmoi-managed dev setup. | |
| # | |
| # Assumptions: | |
| # - Non-root user with SSH access | |
| # - pacman works | |
| # - Nothing else is installed (no git, no chezmoi, no fish) | |
| # | |
| # This script is self-contained — copy-paste or curl it onto the machine. | |
| # It prints a full step list up front so you can pick up manually if needed. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| bold="\033[1m" | |
| dim="\033[2m" | |
| reset="\033[0m" | |
| green="\033[32m" | |
| step_num=0 | |
| step() { | |
| step_num=$((step_num + 1)) | |
| printf "\n${bold}${green}[Step %d]${reset} ${bold}%s${reset}\n" "$step_num" "$1" | |
| } | |
| info() { | |
| printf "${dim} → %s${reset}\n" "$1" | |
| } | |
| ask_continue() { | |
| printf "\n Press Enter to continue (Ctrl-C to abort)... " | |
| read -r | |
| } | |
| confirm() { | |
| printf "\n %s [Y/n] " "$1" | |
| read -r answer | |
| case "${answer:-y}" in | |
| [Yy]*) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| # ─── Print the full plan ───────────────────────────────────────────────────── | |
| cat <<'PLAN' | |
| ╔══════════════════════════════════════════════════════════════════╗ | |
| ║ Headless Dev Environment Bootstrap ║ | |
| ╠══════════════════════════════════════════════════════════════════╣ | |
| ║ ║ | |
| ║ This script will: ║ | |
| ║ ║ | |
| ║ 1. Verify sudo access ║ | |
| ║ 2. Install base packages (git, gh, chezmoi, jq, base-devel) ║ | |
| ║ 3. Install paru (AUR helper) ║ | |
| ║ 4. Authenticate GitHub CLI (gh auth login) ║ | |
| ║ 5. Set up git credential helper (gh auth setup-git) ║ | |
| ║ 6. Run chezmoi init --apply (clones dotfiles, prompts for ║ | |
| ║ headless profile) ║ | |
| ║ 7. Run chezmoi-managed install scripts: ║ | |
| ║ - 10-install-arch-packages.sh (system packages) ║ | |
| ║ - 20-install-mise.fish (language toolchains) ║ | |
| ║ - 30-install-nix.sh (Nix package manager) ║ | |
| ║ - 40-configure-fish.sh (fish as login shell) ║ | |
| ║ - 50-install-npm-tools.sh (claude-code, etc.) ║ | |
| ║ - 80-setup-tailscale.sh (Tailscale VPN) ║ | |
| ║ - 90-setup-tmux.fish (TPM for tmux) ║ | |
| ║ 8. Sign in to 1Password CLI ║ | |
| ║ 9. Restore SSH keys from 1Password ║ | |
| ║ 10. Restore secrets from 1Password ║ | |
| ║ ║ | |
| ║ If any step fails, you can re-run this script — each step is ║ | |
| ║ idempotent. Or pick up manually using the commands shown. ║ | |
| ║ ║ | |
| ╚══════════════════════════════════════════════════════════════════╝ | |
| PLAN | |
| ask_continue | |
| # ─── Step 1: Verify sudo ───────────────────────────────────────────────────── | |
| step "Verify sudo access" | |
| info "Running: sudo -v" | |
| if ! sudo -v; then | |
| echo "ERROR: You need sudo access. Ask the host admin to add you to sudoers." >&2 | |
| echo " Manual fix: su -c 'usermod -aG wheel $USER'" >&2 | |
| exit 1 | |
| fi | |
| echo " ✓ sudo works" | |
| # ─── Step 2: Base packages ─────────────────────────────────────────────────── | |
| step "Install base packages" | |
| info "Running: sudo pacman -Syu --needed git github-cli chezmoi jq base-devel" | |
| sudo pacman -Syu --needed git github-cli chezmoi jq base-devel | |
| # ─── Step 3: Install paru ──────────────────────────────────────────────────── | |
| step "Install paru (AUR helper)" | |
| if command -v paru >/dev/null 2>&1; then | |
| echo " ✓ paru already installed" | |
| else | |
| info "Building paru from AUR..." | |
| info "Running: git clone https://aur.archlinux.org/paru-bin.git /tmp/paru-bin && cd /tmp/paru-bin && makepkg -si --noconfirm" | |
| rm -rf /tmp/paru-bin | |
| git clone https://aur.archlinux.org/paru-bin.git /tmp/paru-bin | |
| (cd /tmp/paru-bin && makepkg -si --noconfirm) | |
| rm -rf /tmp/paru-bin | |
| echo " ✓ paru installed" | |
| fi | |
| # ─── Step 4: GitHub auth ───────────────────────────────────────────────────── | |
| step "Authenticate with GitHub" | |
| if gh auth status >/dev/null 2>&1; then | |
| echo " ✓ Already authenticated" | |
| else | |
| info "This will open an interactive login. On headless, choose 'Paste an authentication token'." | |
| info "Create a token at: https://github.com/settings/tokens" | |
| info "Required scopes: repo, read:org" | |
| echo "" | |
| gh auth login | |
| fi | |
| # ─── Step 5: Git credential helper ─────────────────────────────────────────── | |
| step "Configure git credential helper" | |
| info "Running: gh auth setup-git" | |
| gh auth setup-git | |
| echo " ✓ git now uses gh for authentication" | |
| # ─── Step 6: chezmoi init ──────────────────────────────────────────────────── | |
| step "Initialize chezmoi (clone dotfiles + apply)" | |
| info "Running: chezmoi init --apply mturilin/dotfiles" | |
| info "When prompted 'Is this a headless (no desktop) machine?' → answer true" | |
| echo "" | |
| chezmoi init --apply mturilin/dotfiles | |
| echo " ✓ dotfiles applied" | |
| # ─── Step 7: Install system packages ───────────────────────────────────────── | |
| step "Install system packages (fish, op, and everything else)" | |
| scripts_dir="$HOME/.local/share/chezmoi/scripts" | |
| run_script() { | |
| local script="$1" | |
| local desc="$2" | |
| local path="$scripts_dir/$script" | |
| if [ ! -f "$path" ]; then | |
| echo " ⚠ $script not found, skipping" | |
| return 0 | |
| fi | |
| if confirm "Run $script ($desc)?"; then | |
| info "Running: $path" | |
| bash "$path" || fish "$path" || { | |
| echo " ⚠ $script failed — you can re-run it manually later:" | |
| echo " $path" | |
| } | |
| else | |
| echo " Skipped. Run manually later:" | |
| echo " $path" | |
| fi | |
| } | |
| run_script "10-install-arch-packages.sh" "system packages via pacman + paru (includes fish, op, etc.)" | |
| run_script "20-install-mise.fish" "global mise toolchains (node, python, go, bb)" | |
| run_script "30-install-nix.sh" "multi-user Nix" | |
| run_script "40-configure-fish.sh" "set fish as login shell" | |
| run_script "50-install-npm-tools.sh" "claude-code, aicommit2, gemini-cli, etc." | |
| run_script "80-setup-tailscale.sh" "enable and start Tailscale" | |
| run_script "90-setup-tmux.fish" "install TPM for tmux" | |
| # ─── Step 8: 1Password CLI sign-in ─────────────────────────────────────────── | |
| step "Sign in to 1Password CLI" | |
| if ! command -v op >/dev/null 2>&1; then | |
| echo " ⚠ op not found — install 1password-cli first (step 7), then re-run" | |
| echo " Manual: paru -S 1password-cli" | |
| elif op account list 2>/dev/null | grep -q "my.1password.com"; then | |
| echo " ✓ Already signed in" | |
| else | |
| info "Running: op account add" | |
| info "You'll need your 1Password email, secret key, and master password." | |
| echo "" | |
| op account add --address my.1password.com | |
| eval "$(op signin)" | |
| fi | |
| # ─── Step 9: SSH keys ──────────────────────────────────────────────────────── | |
| step "Restore SSH keys from 1Password" | |
| info "Running: ~/.local/bin/ssh-key-from-1password.sh" | |
| info "Source: op://Private/SSH Keys/notesPlain" | |
| if ! command -v fish >/dev/null 2>&1; then | |
| echo " ⚠ fish not installed — install packages first (step 7), then re-run" | |
| elif ! command -v op >/dev/null 2>&1; then | |
| echo " ⚠ op not found — sign in to 1Password first (step 8)" | |
| elif [ -f "$HOME/.ssh/id_rsa" ]; then | |
| echo " ✓ SSH key already exists at ~/.ssh/id_rsa" | |
| if confirm "Overwrite with key from 1Password?"; then | |
| fish -c "~/.local/bin/ssh-key-from-1password.sh" | |
| fi | |
| else | |
| fish -c "~/.local/bin/ssh-key-from-1password.sh" | |
| fi | |
| # ─── Step 10: Restore secrets ──────────────────────────────────────────────── | |
| step "Restore secrets from 1Password" | |
| if ! command -v fish >/dev/null 2>&1 || ! command -v op >/dev/null 2>&1; then | |
| echo " ⚠ Requires fish and op — complete earlier steps first" | |
| else | |
| info "Running: ~/.local/bin/secrets-refresh-1password.sh (env vars)" | |
| fish -c "~/.local/bin/secrets-refresh-1password.sh" | |
| info "Running: ~/.local/bin/secrets-refresh-1password-files.sh (secret files)" | |
| fish -c "~/.local/bin/secrets-refresh-1password-files.sh" | |
| echo " ✓ Secrets restored" | |
| fi | |
| # ─── Done ───────────────────────────────────────────────────────────────────── | |
| cat <<'DONE' | |
| ╔══════════════════════════════════════════════════════════════════╗ | |
| ║ Bootstrap complete! ║ | |
| ╠══════════════════════════════════════════════════════════════════╣ | |
| ║ ║ | |
| ║ Log out and back in to pick up: ║ | |
| ║ - fish as your login shell ║ | |
| ║ - Nix paths ║ | |
| ║ - Environment secrets ║ | |
| ║ ║ | |
| ║ Optional next steps: ║ | |
| ║ - chezmoi update (pull latest dotfiles) ║ | |
| ║ - tmux, then prefix + I (install tmux plugins) ║ | |
| ║ - tailscale up (if not done by setup script) ║ | |
| ║ ║ | |
| ╚══════════════════════════════════════════════════════════════════╝ | |
| DONE |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment