From d4e3121d68a0440d1354d97f92337dee31a54a85 Mon Sep 17 00:00:00 2001 From: Nic O'Connor Date: Wed, 8 Jul 2026 09:36:52 -0400 Subject: [PATCH] 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 --- README.md | 17 +++++++++- bootstrap.sh | 12 +++++++ git/install.sh | 21 ++++++++++++ lib/os-detect.sh | 43 ++++++++++++++++++++++++ zsh/install.sh | 87 ++++++++++++++++++++++++++++++++++++++++++++++++ zsh/zshrc | 16 +++++++++ 6 files changed, 195 insertions(+), 1 deletion(-) create mode 100755 bootstrap.sh create mode 100755 git/install.sh create mode 100644 lib/os-detect.sh create mode 100755 zsh/install.sh create mode 100644 zsh/zshrc diff --git a/README.md b/README.md index 6202769..2f21cd1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ # dotfiles -Public collection of scripts and configs — incl. mcp-config/ (LiteLLM MCP gateway sync for laptops) \ No newline at end of file +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. \ No newline at end of file diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..5de7784 --- /dev/null +++ b/bootstrap.sh @@ -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" diff --git a/git/install.sh b/git/install.sh new file mode 100755 index 0000000..8e3047d --- /dev/null +++ b/git/install.sh @@ -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." diff --git a/lib/os-detect.sh b/lib/os-detect.sh new file mode 100644 index 0000000..3dbf03a --- /dev/null +++ b/lib/os-detect.sh @@ -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...]. + +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 +} diff --git a/zsh/install.sh b/zsh/install.sh new file mode 100755 index 0000000..404192f --- /dev/null +++ b/zsh/install.sh @@ -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." diff --git a/zsh/zshrc b/zsh/zshrc new file mode 100644 index 0000000..f74d8b6 --- /dev/null +++ b/zsh/zshrc @@ -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