#!/bin/sh # Bootstrap script for dotfiles. # Works standalone or piped: curl -fsSL https://dotfiles.greiber.dev | sh -s -- --git # POSIX sh — must run before bash is installed. set -eu # ── Constants ──────────────────────────────────────────────────────────────── # Overridable for testing against `wrangler dev` or a local repository. API_BASE="${DOTFILES_API:-https://dotfiles.greiber.dev}" REPO_URL="${DOTFILES_REPO:-git@github.com:ju57u5/dotfiles.git}" DEFAULT_TARGET="$HOME/.envcfg" DEPLOY_KEY="$HOME/.ssh/dotfiles_deploy" # Published at https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints GITHUB_HOST_KEY="github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" # ── Helpers ────────────────────────────────────────────────────────────────── log() { printf '[bootstrap] %s\n' "$*"; } err() { printf '[bootstrap] ERROR: %s\n' "$*" >&2; } die() { err "$@"; exit 1; } has() { command -v "$1" >/dev/null 2>&1; } usage() { cat < TOTP code for API authentication (prompted if omitted) --target Target directory (default: ~/.envcfg) -h, --help Show this help message Everything after -- is passed through to run.sh. Running --git on a directory created by --curl turns it into a git checkout. Examples: curl -fsSL ${API_BASE} | sh -s -- --git curl -fsSL ${API_BASE} | sh -s -- --curl --totp 123456 -- --work ./bootstrap.sh --git -- --dry-run EOF } # ── Argument parsing ───────────────────────────────────────────────────────── parse_args() { MODE="" TOTP="" TARGET_DIR="$DEFAULT_TARGET" PASSTHROUGH_ARGS="" while [ $# -gt 0 ]; do case "$1" in --git) MODE="git" shift ;; --curl) MODE="curl" shift ;; --totp) [ $# -ge 2 ] || die "--totp requires a value" TOTP="$2" shift 2 ;; --target) [ $# -ge 2 ] || die "--target requires a value" TARGET_DIR="${2%/}" shift 2 ;; -h|--help) usage exit 0 ;; --) shift # Collect remaining args for passthrough to run.sh for arg in "$@"; do if [ -z "$PASSTHROUGH_ARGS" ]; then PASSTHROUGH_ARGS="$arg" else PASSTHROUGH_ARGS="$PASSTHROUGH_ARGS $arg" fi done break ;; *) die "Unknown option: $1 (use -- to separate run.sh arguments)" ;; esac done } # stdin is the script itself when piped from curl, so read from the terminal. prompt_mode() { if [ -z "$MODE" ]; then if ! { : /dev/null; then die "Must specify either --git or --curl" fi printf '[bootstrap] Select mode — (g)it or (c)url: ' >/dev/tty read -r mode_choice /dev/null; then die "No --totp provided and no TTY available for interactive prompt" fi printf '[bootstrap] Enter TOTP code: ' >/dev/tty read -r TOTP /dev/null 2>&1 || getopt_rc=$? [ "$getopt_rc" -eq 4 ] || missing="$missing $(pkg_name getopt)" # run.sh uses GNU coreutils options (busybox lacks realpath --relative-to) realpath --relative-to=/ / >/dev/null 2>&1 || missing="$missing coreutils" if [ "$MODE" = "git" ]; then has git || missing="$missing git" has ssh-keygen || missing="$missing $(pkg_name openssh)" else has tar || missing="$missing tar" has gzip || missing="$missing gzip" fi if [ -n "$missing" ]; then # shellcheck disable=SC2086 pkg_install $missing fi } # ── API ────────────────────────────────────────────────────────────────────── # api_request — prints the HTTP status code api_request() { output="$1" shift curl -sS -o "$output" -w '%{http_code}' "$@" || echo 000 } # ── Git mode ───────────────────────────────────────────────────────────────── setup_git() { # A key used only for this repository, independent of the user's own keys. if [ ! -f "$DEPLOY_KEY" ]; then log "Generating deploy key $DEPLOY_KEY..." mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" ssh-keygen -q -t ed25519 -f "$DEPLOY_KEY" -N "" -C "dotfiles-deploy@$(uname -n)" fi if ! grep -qs "^$GITHUB_HOST_KEY" "$HOME/.ssh/known_hosts"; then echo "$GITHUB_HOST_KEY" >>"$HOME/.ssh/known_hosts" fi host=$(uname -n | tr -c 'A-Za-z0-9._\n-' '-' | cut -c1-64) pub_key=$(cat "$DEPLOY_KEY.pub") prompt_totp log "Registering deploy key via API..." response=$(mktemp) http_code=$(api_request "$response" \ -X POST \ -H "Content-Type: application/json" \ -d "{\"public_key\": \"$pub_key\", \"totp\": \"$TOTP\", \"host\": \"$host\"}" \ "${API_BASE}/deploy-key") message=$(cat "$response") rm -f "$response" case "$http_code" in 2??) log "$message" ;; *) die "Failed to register deploy key (HTTP $http_code): $message" ;; esac ssh_command="ssh -i $DEPLOY_KEY -o IdentitiesOnly=yes" if [ -d "$TARGET_DIR/.git" ]; then log "Repository already exists at $TARGET_DIR — pulling latest..." git -C "$TARGET_DIR" pull --ff-only elif [ -d "$TARGET_DIR" ]; then # e.g. created by --curl: adopt the files instead of re-downloading. log "Converting existing $TARGET_DIR into a git checkout..." # Clone aside and only move .git in once that worked, so a failure # doesn't leave a half-initialized repository behind. staging=$(mktemp -d "$TARGET_DIR.clone.XXXXXX") git -c core.sshCommand="$ssh_command" clone -q --no-checkout "$REPO_URL" "$staging" mv "$staging/.git" "$TARGET_DIR/.git" rmdir "$staging" git -C "$TARGET_DIR" config core.sshCommand "$ssh_command" # Index from HEAD, working tree untouched. git -C "$TARGET_DIR" reset -q log "Local differences to upstream (if any) show up in git status" else log "Cloning repository to $TARGET_DIR..." git -c core.sshCommand="$ssh_command" clone -q "$REPO_URL" "$TARGET_DIR" git -C "$TARGET_DIR" config core.sshCommand "$ssh_command" fi } # ── Curl mode ──────────────────────────────────────────────────────────────── setup_curl() { if [ -d "$TARGET_DIR/.git" ]; then die "$TARGET_DIR is a git checkout, update it with git pull instead" fi prompt_totp log "Downloading tarball..." tarball=$(mktemp) http_code=$(api_request "$tarball" -L -H "Authorization: Bearer $TOTP" "${API_BASE}/archive") case "$http_code" in 2??) ;; *) message=$(cat "$tarball"); rm -f "$tarball"; die "Failed to download tarball (HTTP $http_code): $message" ;; esac # Extract next to the target and swap, so files deleted upstream disappear. mkdir -p "$(dirname "$TARGET_DIR")" staging=$(mktemp -d "$TARGET_DIR.new.XXXXXX") tar -xzf "$tarball" -C "$staging" --strip-components=1 rm -f "$tarball" rm -rf "$TARGET_DIR" mv "$staging" "$TARGET_DIR" log "Extracted to $TARGET_DIR" } # ── Main ───────────────────────────────────────────────────────────────────── main() { parse_args "$@" prompt_mode PKG_MANAGER=$(detect_pkg_manager) log "Detected package manager: $PKG_MANAGER" ensure_deps if [ "$MODE" = "git" ]; then setup_git else setup_curl fi log "Running dotfiles setup..." cd "$TARGET_DIR" # shellcheck disable=SC2086 exec bash run.sh $PASSTHROUGH_ARGS } main "$@"