Skip to content

Instantly share code, notes, and snippets.

@mturilin
Last active April 11, 2026 03:50
Show Gist options
  • Select an option

  • Save mturilin/7f87b1f3b1c26115ac6c82b94f9e0cc4 to your computer and use it in GitHub Desktop.

Select an option

Save mturilin/7f87b1f3b1c26115ac6c82b94f9e0cc4 to your computer and use it in GitHub Desktop.
Headless Arch Linux dev environment bootstrap
#!/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