#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # dotfiles/setup.sh — provision a fresh box, then hand off to chezmoi # # Root work (apt, starship, chsh) happens here. Everything under $HOME is # owned by chezmoi. Run: sudo bash setup.sh # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' PASS=0; WARN=0; FAIL=0 log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_success() { echo -e "${GREEN}[ OK ]${NC} $*"; PASS=$((PASS+1)); } log_warning() { echo -e "${YELLOW}[WARN]${NC} $*"; WARN=$((WARN+1)); } log_error() { echo -e "${RED}[FAIL]${NC} $*"; FAIL=$((FAIL+1)); } log_step() { echo -e "\n${BLUE}▶${NC} ${1}"; } DOTFILES_REPO="${DOTFILES_REPO:-https://git.domverse-berlin.eu/domverse/dotfiles.git}" CARGO_TOOLS=(atuin eza macchina oxker) APT_PKGS=(fish bat ripgrep jq git curl nano build-essential pkg-config libssl-dev) echo -e "${BLUE}" cat <<'EOF' ╔══════════════════════════════════════════════════════════════╗ ║ Shell & dotfiles bootstrap ║ ║ fish · starship · atuin · eza · chezmoi · claude ║ ╚══════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" # ── Preflight ──────────────────────────────────────────────────────────────── log_step "Preflight" if [ "$(id -u)" -ne 0 ]; then log_error "Must run as root: sudo bash setup.sh" exit 1 fi TARGET_USER="${SUDO_USER:-}" if [ -z "$TARGET_USER" ] || [ "$TARGET_USER" = "root" ]; then log_error "Could not determine the non-root target user (SUDO_USER unset)." log_error "Run via 'sudo bash setup.sh' as your normal user, not as root directly." exit 1 fi TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6) log_success "Target user: $TARGET_USER (home: $TARGET_HOME)" # Run a command as the target user with a sane login-ish env. as_user() { sudo -u "$TARGET_USER" -H bash -lc "$*"; } # ── APT packages ───────────────────────────────────────────────────────────── log_step "APT packages" MISSING=() for p in "${APT_PKGS[@]}"; do dpkg -s "$p" >/dev/null 2>&1 || MISSING+=("$p") done if [ ${#MISSING[@]} -eq 0 ]; then log_success "All apt packages already present (${APT_PKGS[*]})" else log_info "Installing: ${MISSING[*]}" apt-get update -qq DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${MISSING[@]}" log_success "Installed ${#MISSING[@]} apt package(s)" fi # config.fish aliases `cat` to batcat; on Ubuntu the binary really is batcat. if command -v batcat >/dev/null 2>&1; then log_success "batcat present (config.fish aliases cat -> batcat)" else log_warning "batcat missing — the 'cat' alias in config.fish will break" fi # ── rustup + cargo tools ───────────────────────────────────────────────────── log_step "Rust toolchain (build dependency for ${CARGO_TOOLS[*]})" if as_user 'command -v cargo' >/dev/null 2>&1; then log_success "rustup/cargo already installed" else log_info "Installing rustup (non-interactive, default profile)..." as_user "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path" log_success "rustup installed" fi # `cargo install` is idempotent-ish: it rebuilds if the version differs. # --locked keeps builds reproducible. This is the slow part (~10-20 min cold). for tool in "${CARGO_TOOLS[@]}"; do if as_user "test -x \$HOME/.cargo/bin/$tool"; then log_success "$tool already built" else log_info "Building $tool from source (this takes a while)..." if as_user "\$HOME/.cargo/bin/cargo install --locked $tool"; then log_success "$tool built" else log_error "$tool failed to build" fi fi done # ── starship ───────────────────────────────────────────────────────────────── log_step "Starship prompt" if command -v starship >/dev/null 2>&1; then log_success "starship already installed ($(starship --version | head -1))" else log_info "Installing starship to /usr/local/bin..." curl -sS https://starship.rs/install.sh | sh -s -- --yes >/dev/null log_success "starship installed" fi # ── chezmoi + dotfiles ─────────────────────────────────────────────────────── log_step "chezmoi" if as_user 'command -v chezmoi' >/dev/null 2>&1; then log_success "chezmoi already installed" else log_info "Installing chezmoi to ~/.local/bin..." as_user "sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- -b \$HOME/.local/bin" log_success "chezmoi installed" fi CHEZMOI="\$HOME/.local/bin/chezmoi" # chezmoi does NOT preserve exact file modes — it tracks only the executable and # private attributes, then derives the mode from the umask. A umask of 002 (the # default here) would write world/group-writable 0664 dotfiles. Pin 022 so files # land as 0644, and let the private_ attribute handle the 0600 cases. log_step "chezmoi config" CHEZMOI_CFG="$TARGET_HOME/.config/chezmoi/chezmoi.toml" if [ -f "$CHEZMOI_CFG" ] && grep -q 'umask' "$CHEZMOI_CFG"; then log_success "chezmoi umask already configured" else as_user "mkdir -p \$HOME/.config/chezmoi" as_user "printf 'umask = 0o022\n' >> \$HOME/.config/chezmoi/chezmoi.toml" log_success "chezmoi umask pinned to 022" fi log_step "Applying dotfiles" if as_user "test -d \$HOME/.local/share/chezmoi/.git"; then log_info "chezmoi source already initialised — applying current state" as_user "$CHEZMOI apply" && log_success "dotfiles applied" || log_error "chezmoi apply failed" else log_info "Initialising chezmoi from $DOTFILES_REPO" if as_user "$CHEZMOI init --apply '$DOTFILES_REPO'"; then log_success "dotfiles initialised and applied" else log_error "chezmoi init failed — check repo access (private repo needs credentials)" fi fi # ── Default shell ──────────────────────────────────────────────────────────── log_step "Default shell" FISH_BIN=$(command -v fish || true) if [ -z "$FISH_BIN" ]; then log_error "fish not found — cannot set default shell" else grep -qxF "$FISH_BIN" /etc/shells || echo "$FISH_BIN" >> /etc/shells CURRENT_SHELL=$(getent passwd "$TARGET_USER" | cut -d: -f7) if [ "$CURRENT_SHELL" = "$FISH_BIN" ]; then log_success "fish is already the default shell for $TARGET_USER" else chsh -s "$FISH_BIN" "$TARGET_USER" log_success "default shell changed: $CURRENT_SHELL -> $FISH_BIN" fi fi # ── Summary ────────────────────────────────────────────────────────────────── echo echo "────────────────────────────────────────────────────────────────" echo -e " ${GREEN}pass: $PASS${NC} ${YELLOW}warn: $WARN${NC} ${RED}fail: $FAIL${NC}" echo "────────────────────────────────────────────────────────────────" echo log_info "Manual steps that cannot be automated:" echo " 1. atuin sync — key is NOT in this repo (tracked in secrets.yml):" echo " atuin login -u " echo " atuin sync" echo " Restore the key from secrets.yml into ~/.local/share/atuin/key" echo " BEFORE first sync, or history encrypted on other hosts won't decrypt." echo " 2. git identity is not set globally (only credential.helper=store):" echo " git config --global user.name ''" echo " git config --global user.email ''" echo " 3. Log out and back in for the fish shell change to take effect." echo [ "$FAIL" -gt 0 ] && exit 1 exit 0