Add bootstrap.sh with zsh and git modules

Cross-distro (RHEL/Debian) VM setup: zsh + oh-my-zsh with a self-healing
bash fallback for environments that reset the root filesystem or hardcode
bash for new terminals (e.g. Kasm Workspaces), plus git identity and
HTTPS credential-store configuration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 09:36:52 -04:00
co-authored by Claude Sonnet 5
parent 7943600a33
commit d4e3121d68
6 changed files with 195 additions and 1 deletions
+16 -1
View File
@@ -1,3 +1,18 @@
# dotfiles
Public collection of scripts and configs — incl. mcp-config/ (LiteLLM MCP gateway sync for laptops)
Configures fresh VMs (RHEL-family or Debian-family) with a consistent
environment. Run `./bootstrap.sh` (or curl-pipe it) to apply everything;
each module also runs standalone and is safe to re-run.
## Modules
- `zsh/` — installs zsh + oh-my-zsh, drops a managed `~/.zshrc`
(robbyrussell theme, git plugin), and sets zsh as the login shell.
- `git/` — sets git `user.name`/`user.email` and configures
`credential.helper store` (plaintext `~/.git-credentials`, 0600) for
HTTPS auth.
- `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM /
opencode). Independent of `bootstrap.sh`; see its own README.
`lib/os-detect.sh` provides shared package-manager detection (`dnf`/`yum`
vs `apt`) used by module install scripts.
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env sh
# bootstrap.sh — entrypoint for configuring a fresh VM (RHEL-family or
# Debian-family) with this repo's dotfiles. Safe to re-run.
#
# curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/bootstrap.sh | sh
# (or, from a checked-out clone: ./bootstrap.sh)
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
"$SCRIPT_DIR/zsh/install.sh"
"$SCRIPT_DIR/git/install.sh"
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env sh
# git/install.sh — configure git identity + HTTPS credential storage.
# Idempotent: safe to re-run.
set -eu
say() { printf '%s\n' "$*" >&2; }
GIT_NAME="Nic O'Connor"
GIT_EMAIL="nic.oconnor@gmail.com"
git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"
say "git identity: $GIT_NAME <$GIT_EMAIL>"
# Plaintext credential cache in $HOME (survives container/VM rebuilds that
# wipe everything outside $HOME). Git creates ~/.git-credentials with 0600
# the first time a credential is saved.
git config --global credential.helper store
say "credential.helper = store (~/.git-credentials, created on first auth)"
say "Done. On the next HTTPS push/pull to a new host, git prompts once and caches the credential in ~/.git-credentials."
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env sh
# lib/os-detect.sh — shared OS/package-manager detection for module installers.
# Source this, then call pkg_install <pkg> [pkg...].
detect_pkg_family() {
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
id_line="$(. /etc/os-release && printf '%s %s' "${ID:-}" "${ID_LIKE:-}")"
case "$id_line" in
*debian*|*ubuntu*) printf 'debian\n'; return 0 ;;
*rhel*|*fedora*|*centos*) printf 'rhel\n'; return 0 ;;
esac
fi
if command -v apt-get >/dev/null 2>&1; then
printf 'debian\n'; return 0
fi
if command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then
printf 'rhel\n'; return 0
fi
printf 'unknown\n'
return 1
}
pkg_install() {
family="$(detect_pkg_family)"
case "$family" in
debian)
sudo apt-get update -qq
sudo apt-get install -y "$@"
;;
rhel)
if command -v dnf >/dev/null 2>&1; then
sudo dnf install -y "$@"
else
sudo yum install -y "$@"
fi
;;
*)
printf 'error: unsupported/undetected OS family; install manually: %s\n' "$*" >&2
return 1
;;
esac
}
Executable
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env sh
# zsh/install.sh — install zsh + oh-my-zsh and make zsh the login shell.
# Works on RHEL-family (dnf/yum) and Debian-family (apt) hosts. Idempotent:
# safe to re-run on a box that already has some/all of this in place.
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
# shellcheck source=../lib/os-detect.sh
. "$REPO_ROOT/lib/os-detect.sh"
say() { printf '%s\n' "$*" >&2; }
# --- 1. install zsh ---------------------------------------------------------
if command -v zsh >/dev/null 2>&1; then
say "zsh already installed ($(command -v zsh))."
else
say "Installing zsh..."
pkg_install zsh
fi
ZSH_BIN="$(command -v zsh)"
# --- 2. install oh-my-zsh (unattended) --------------------------------------
if [ -d "$HOME/.oh-my-zsh" ]; then
say "oh-my-zsh already installed."
else
say "Installing oh-my-zsh..."
command -v curl >/dev/null 2>&1 || pkg_install curl
RUNZSH=no CHSH=no KEEP_ZSHRC=yes \
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
fi
# --- 3. install our .zshrc ---------------------------------------------------
DEST="$HOME/.zshrc"
SRC="$SCRIPT_DIR/zshrc"
if [ -f "$DEST" ] && ! cmp -s "$SRC" "$DEST"; then
BACKUP="$DEST.bak.$(date +%Y%m%d%H%M%S 2>/dev/null || printf 'pre-dotfiles')"
say "Backing up existing $DEST -> $BACKUP"
cp "$DEST" "$BACKUP"
fi
cp "$SRC" "$DEST"
say "Installed $DEST"
# --- 4. make zsh the login shell --------------------------------------------
if ! grep -qxF "$ZSH_BIN" /etc/shells 2>/dev/null; then
say "Adding $ZSH_BIN to /etc/shells"
echo "$ZSH_BIN" | sudo tee -a /etc/shells >/dev/null
fi
CURRENT_SHELL="$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)"
if [ "$CURRENT_SHELL" = "$ZSH_BIN" ]; then
say "zsh is already the login shell."
else
say "Changing login shell to $ZSH_BIN"
# Use usermod (root, via sudo) instead of chsh: chsh runs its own PAM
# authentication for the invoking user's password, which fails with a PAM
# error in minimal containers (e.g. Kasm images) even when sudo itself is
# passwordless. usermod edits /etc/passwd directly as root, no PAM prompt.
sudo usermod -s "$ZSH_BIN" "$(id -un)"
fi
# --- 5. bash fallback: auto-exec into zsh -----------------------------------
# Some environments (e.g. Kasm Workspaces) hardcode/inherit bash for new
# terminals regardless of the /etc/passwd shell, and reset the root
# filesystem (including installed packages and /etc/passwd) on rebuild while
# keeping $HOME. Sourcing this from ~/.bashrc — which lives in $HOME — makes
# every fresh bash self-heal into zsh, reinstalling it first if needed.
BASHRC="$HOME/.bashrc"
HOOK_MARKER="# >>> dotfiles zsh auto-exec >>>"
if [ -f "$BASHRC" ] && grep -qF "$HOOK_MARKER" "$BASHRC" 2>/dev/null; then
say "bash auto-exec hook already present in $BASHRC."
else
say "Adding zsh auto-exec hook to $BASHRC (covers terminals that always spawn bash)."
{
printf '\n%s\n' "$HOOK_MARKER"
printf '%s\n' "# Re-exec into zsh for interactive shells, self-healing (reinstalling"
printf '%s\n' "# zsh) if this container's root filesystem was reset since last run."
printf '%s\n' 'if [ -n "$PS1" ] && [ -z "${ZSH_VERSION:-}" ]; then'
printf ' %s\n' "command -v zsh >/dev/null 2>&1 || \"$SCRIPT_DIR/install.sh\" >/dev/null 2>&1 || true"
printf ' %s\n' 'command -v zsh >/dev/null 2>&1 && exec zsh -l'
printf '%s\n' 'fi'
printf '%s\n' "# <<< dotfiles zsh auto-exec <<<"
} >> "$BASHRC"
fi
say "Done. Open a new terminal to use zsh."
+16
View File
@@ -0,0 +1,16 @@
# Managed by dotfiles/zsh/install.sh — edits made directly to ~/.zshrc will be
# overwritten on the next bootstrap run. Edit zsh/zshrc in the dotfiles repo
# instead.
export ZSH="$HOME/.oh-my-zsh"
ZSH_THEME="robbyrussell"
plugins=(git)
source "$ZSH/oh-my-zsh.sh"
HISTSIZE=10000
SAVEHIST=10000
setopt SHARE_HISTORY
setopt HIST_IGNORE_DUPS