Skip to content

Instantly share code, notes, and snippets.

@JesperDramsch
Created July 2, 2026 10:11
Show Gist options
  • Select an option

  • Save JesperDramsch/c82d8be11e665e0f514d2d5a79b1b6d0 to your computer and use it in GitHub Desktop.

Select an option

Save JesperDramsch/c82d8be11e665e0f514d2d5a79b1b6d0 to your computer and use it in GitHub Desktop.
Setup scripts for chezmoi
#Requires -Version 5.1
<#
.SYNOPSIS
Bootstrap shim — the only public entry point.
.DESCRIPTION
The dotfiles repo is private, so the real bootstrap script can't be
served from raw.githubusercontent.com.
What it does:
1. Sets ExecutionPolicy for CurrentUser
2. Installs scoop, then gh via scoop (gh is scoop-managed in this repo)
3. Signs in to GitHub via the browser device flow (no PAT to type)
4. Fetches bootstrap/bootstrap.ps1 from the private repo via the
GitHub API and runs it
The real bootstrap never leaves the private repo.
#>
$ErrorActionPreference = 'Stop'
$owner = 'jesperdramsch'
Write-Host "`n=== dotfiles stage-0 bootstrap ===" -ForegroundColor Cyan
# The repo name stays out of the published shim — ask for it instead.
# A bare name is prefixed with the owner; a full owner/name slug passes through.
$repoInput = (Read-Host "Dotfiles repo (name under $owner/, or full owner/name slug)").Trim()
if (-not $repoInput) {
Write-Error "No repo given"
exit 1
}
$repo = if ($repoInput -match '/') { $repoInput } else { "$owner/$repoInput" }
# --- 1. ExecutionPolicy (scoop's installer needs RemoteSigned) ---
$policy = Get-ExecutionPolicy -Scope CurrentUser
if ($policy -eq 'Undefined' -or $policy -eq 'Restricted') {
Write-Host "[1/4] Setting ExecutionPolicy to RemoteSigned..."
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force
} else {
Write-Host "[1/4] ExecutionPolicy already set: $policy"
}
# --- 2. Scoop + gh ---
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
Write-Host "[2/4] Installing scoop..."
Invoke-RestMethod get.scoop.sh | Invoke-Expression
} else {
Write-Host "[2/4] Scoop already installed"
}
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
Write-Host " Installing gh via scoop..."
scoop install gh
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
Write-Error "gh not found on PATH after scoop install"
exit 1
}
}
# --- 3. GitHub auth (browser device flow) ---
gh auth status --hostname github.com 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host "[3/4] Signing in to GitHub (browser will open, enter the one-time code)..." -ForegroundColor Yellow
# https so the token also covers the private-repo clone during bootstrap;
# post-bootstrap docs cover switching to the SSH alias.
gh auth login --hostname github.com --git-protocol https --web
gh auth status --hostname github.com 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "GitHub authentication failed or was cancelled"
exit 1
}
} else {
Write-Host "[3/4] Already signed in to GitHub"
}
# --- 4. Fetch and run the real bootstrap from the private repo ---
Write-Host "[4/4] Fetching bootstrap.ps1 from $repo..."
$script = gh api -H 'Accept: application/vnd.github.raw' "repos/$repo/contents/bootstrap/bootstrap.ps1"
if ($LASTEXITCODE -ne 0 -or -not $script) {
Write-Error "Failed to fetch bootstrap.ps1 from $repo (check repo access for the signed-in account)"
exit 1
}
Invoke-Expression ($script -join "`n")
#!/bin/bash
# Bootstrap shim (Linux / WSL2 / macOS / HPC) — the only public entry point.
#
# The dotfiles repo is private, so the real bootstrap script can't be served
# from raw.githubusercontent.com.
#
# Run with (process substitution keeps stdin on the terminal, which the
# interactive gh/bw prompts need — do NOT use `curl | bash`):
#
# bash <(curl -fsLS https://gist.githubusercontent.com/JesperDramsch/GIST_ID/raw/bootstrap-stage0.sh)
#
# What it does:
# 1. Ensures gh exists (official release binary into ~/.local/bin — rootless,
# no sudo/apt, HPC-safe; skipped where gh is already present)
# 2. Signs in to GitHub via the browser device flow (no PAT to type;
# on headless hosts gh prints the URL + one-time code instead)
# 3. Fetches bootstrap/bootstrap.sh from the private repo via the GitHub
# API and runs it
set -euo pipefail
OWNER="jesperdramsch"
echo ""
echo "=== dotfiles stage-0 bootstrap ==="
echo ""
# gh auth login and the downstream bw prompts need a real terminal.
if [ ! -t 0 ]; then
echo "ERROR: stdin is not a terminal (did you use 'curl | bash'?)." >&2
echo "Run via process substitution instead:" >&2
echo " bash <(curl -fsLS <shim-url>)" >&2
exit 1
fi
# The repo name stays out of the published shim — ask for it instead.
# A bare name is prefixed with the owner; a full owner/name slug passes through.
read -r -p "Dotfiles repo (name under $OWNER/, or full owner/name slug): " repo_input
repo_input="$(echo "$repo_input" | tr -d '[:space:]')"
if [ -z "$repo_input" ]; then
echo "ERROR: no repo given" >&2
exit 1
fi
case "$repo_input" in
*/*) REPO="$repo_input" ;;
*) REPO="$OWNER/$repo_input" ;;
esac
# git is needed by gh auth setup-git and the chezmoi clone later.
if ! command -v git >/dev/null 2>&1; then
if [ "$(uname -s)" = "Darwin" ]; then
echo "git not found — triggering the Xcode Command Line Tools install."
echo "Re-run this shim once it finishes."
xcode-select --install
else
echo "ERROR: git not found. Install it first (e.g. 'sudo apt install git' on WSL2)." >&2
fi
exit 1
fi
export PATH="$HOME/.local/bin:$PATH"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
# --- 1. gh (user-local, rootless — no sudo, no /usr/local) ---
if command -v gh >/dev/null 2>&1; then
echo "[1/3] gh already installed: $(gh --version | head -n1)"
else
echo "[1/3] Installing gh to ~/.local/bin..."
case "$(uname -m)" in
x86_64) arch="amd64" ;;
aarch64 | arm64) arch="arm64" ;;
*) echo "ERROR: unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
# Resolve the latest release tag from the redirect — no JSON parsing, no auth.
tag="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/cli/cli/releases/latest)"
tag="${tag##*/}"
ver="${tag#v}"
mkdir -p "$HOME/.local/bin"
if [ "$(uname -s)" = "Darwin" ]; then
curl -fsSL -o "$tmpdir/gh.zip" \
"https://github.com/cli/cli/releases/download/${tag}/gh_${ver}_macOS_${arch}.zip"
unzip -q "$tmpdir/gh.zip" -d "$tmpdir"
cp "$tmpdir/gh_${ver}_macOS_${arch}/bin/gh" "$HOME/.local/bin/gh"
else
curl -fsSL "https://github.com/cli/cli/releases/download/${tag}/gh_${ver}_linux_${arch}.tar.gz" |
tar -xz -C "$tmpdir"
cp "$tmpdir/gh_${ver}_linux_${arch}/bin/gh" "$HOME/.local/bin/gh"
fi
chmod +x "$HOME/.local/bin/gh"
echo " Installed: $(gh --version | head -n1)"
fi
# --- 2. GitHub auth (browser device flow) ---
if gh auth status --hostname github.com >/dev/null 2>&1; then
echo "[2/3] Already signed in to GitHub"
else
echo "[2/3] Signing in to GitHub (follow the one-time-code prompt)..."
# https so the token also covers the private-repo clone during bootstrap;
# post-bootstrap docs cover switching to the SSH alias.
gh auth login --hostname github.com --git-protocol https --web
if ! gh auth status --hostname github.com >/dev/null 2>&1; then
echo "ERROR: GitHub authentication failed or was cancelled" >&2
exit 1
fi
fi
# --- 3. Fetch and run the real bootstrap from the private repo ---
echo "[3/3] Fetching bootstrap.sh from $REPO..."
script="$(gh api -H 'Accept: application/vnd.github.raw' "repos/$REPO/contents/bootstrap/bootstrap.sh")" || {
echo "ERROR: failed to fetch bootstrap.sh from $REPO (check repo access for the signed-in account)" >&2
exit 1
}
# Run from a file (not a pipe) so the real bootstrap's prompts keep the terminal.
printf '%s\n' "$script" > "$tmpdir/bootstrap.sh"
bash "$tmpdir/bootstrap.sh"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment