#!/bin/sh # setup — Dotfiles bootstrap for https://dev.pivoine.art/valknar/home # # Quick install: # curl https://dev.pivoine.art/valknar/home/raw/branch/main/bin/setup -sSf | sh # # Safe to re-run — all steps are idempotent. # # What this script does: # 1. Re-execs itself with bash (curl|sh consumes stdin, so bash -c re-fetch is required) # 2. Detects OS, installs git if missing — apt-get on Debian/Ubuntu, xcode-select on macOS # 3. Installs zsh if missing, sets it as the default shell # 4. Clones this dotfiles repo into ~ via HTTPS (or pulls if already present) # 5. Runs arty deps — oh-my-zsh, p10k, nvm, rbenv, pyenv, zsh plugins # 6. Installs arty zsh completion via: arty completion zsh --install # 7. Prints next steps # ─── Step 0: Re-exec with bash ──────────────────────────────────────────────── # curl URL | sh feeds the script on stdin. Once read, stdin is exhausted — # `exec bash -s` would give bash nothing to interpret. The only reliable # solution (used by Homebrew and rustup) is to re-download the script and # hand it to `bash -c`. This costs one extra HTTP request but is correct. if [ -z "${BASH_VERSION:-}" ]; then _SETUP_URL="https://dev.pivoine.art/valknar/home/raw/branch/main/bin/setup" if command -v curl >/dev/null 2>&1; then exec bash -c "$(curl -fsSL "$_SETUP_URL")" -- "$@" elif command -v wget >/dev/null 2>&1; then exec bash -c "$(wget -qO- "$_SETUP_URL")" -- "$@" else echo "[x] curl or wget is required. Please install one and retry." >&2 exit 1 fi fi # ─── Bash from here ─────────────────────────────────────────────────────────── set -euo pipefail # ─── Constants ──────────────────────────────────────────────────────────────── readonly REPO_HTTPS="https://dev.pivoine.art/valknar/home.git" readonly SETUP_URL="https://dev.pivoine.art/valknar/home/raw/branch/main/bin/setup" # ─── Colors ─────────────────────────────────────────────────────────────────── # Matches arty's FORCE_COLOR convention: colors only when stdout is a terminal. if [[ -t 1 && "${TERM:-}" != "dumb" ]]; then export FORCE_COLOR="${FORCE_COLOR:-1}" fi if [[ "${FORCE_COLOR:-0}" == "0" ]]; then RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC='' else RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' fi # ─── Logging ────────────────────────────────────────────────────────────────── log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[ok]${NC} $1"; } log_warn() { echo -e "${YELLOW}[!]${NC} $1"; } log_error() { echo -e "${RED}[x]${NC} $1" >&2; } step_header() { echo echo -e "${CYAN}${BOLD}══════════════════════════════════════════${NC}" echo -e "${CYAN}${BOLD} $1${NC}" echo -e "${CYAN}${BOLD}══════════════════════════════════════════${NC}" } # ─── OS Detection ───────────────────────────────────────────────────────────── OS="unknown" detect_os() { local kernel kernel="$(uname -s | tr '[:upper:]' '[:lower:]')" case "$kernel" in darwin) OS="macos" ;; linux) if [[ -f /etc/os-release ]]; then local os_id os_id="$(. /etc/os-release && echo "${ID:-}")" case "$os_id" in debian|ubuntu|linuxmint|pop) OS="debian" ;; *) OS="linux" ;; esac else OS="linux" fi ;; *) OS="unknown" ;; esac } # ─── Step 1: Prerequisites ──────────────────────────────────────────────────── check_prerequisites() { step_header "Step 1 of 7 — Prerequisites" detect_os log_info "OS detected: ${BOLD}$OS${NC}" if command -v curl >/dev/null 2>&1; then log_success "curl $(curl --version | head -1 | awk '{print $2}')" elif command -v wget >/dev/null 2>&1; then log_success "wget $(wget --version 2>&1 | head -1 | awk '{print $3}')" else log_error "curl or wget is required." exit 1 fi } # ─── Step 2: Ensure git ─────────────────────────────────────────────────────── ensure_git() { step_header "Step 2 of 7 — Git" if command -v git >/dev/null 2>&1; then log_success "git $(git --version | awk '{print $3}')" return 0 fi log_warn "git not found — installing for OS: $OS" case "$OS" in debian) if ! command -v sudo >/dev/null 2>&1; then log_error "sudo is required to install git on Debian/Ubuntu." log_error "Run as root: apt-get install -y git — then re-run this script." exit 1 fi sudo apt-get update -qq sudo apt-get install -y git log_success "git installed: $(git --version | awk '{print $3}')" ;; macos) # xcode-select --install shows a GUI dialog — it cannot run non-interactively. # We trigger it and exit, asking the user to re-run after the tools install. log_warn "git on macOS is provided by Xcode Command Line Tools." log_warn "A system dialog will appear — click 'Install' to proceed." xcode-select --install 2>/dev/null || true echo log_warn "After the tools finish installing, re-run:" log_warn " curl $SETUP_URL -sSf | sh" exit 0 ;; *) log_error "Cannot install git automatically on OS: $OS" log_error "Please install git manually, then re-run this script." exit 1 ;; esac } # ─── Step 3: Ensure zsh ─────────────────────────────────────────────────────── ensure_zsh() { step_header "Step 3 of 7 — Zsh" if command -v zsh >/dev/null 2>&1; then log_success "zsh $(zsh --version | awk '{print $2}')" else log_warn "zsh not found — installing for OS: $OS" case "$OS" in debian) if ! command -v sudo >/dev/null 2>&1; then log_error "sudo is required to install zsh on Debian/Ubuntu." log_error "Run as root: apt-get install -y zsh — then re-run this script." exit 1 fi sudo apt-get update -qq sudo apt-get install -y zsh log_success "zsh installed: $(zsh --version | awk '{print $2}')" ;; macos) if command -v brew >/dev/null 2>&1; then brew install zsh log_success "zsh installed: $(zsh --version | awk '{print $2}')" else log_warn "zsh not found and Homebrew is not available." log_warn "Install zsh manually: https://www.zsh.org" fi ;; *) log_warn "Cannot install zsh automatically on OS: $OS" log_warn "Please install zsh manually, then re-run this script." ;; esac fi # Set zsh as the default shell if it isn't already. local zsh_path zsh_path="$(command -v zsh 2>/dev/null || true)" if [[ -n "$zsh_path" && "$SHELL" != "$zsh_path" ]]; then log_info "Setting zsh as default shell..." if command -v chsh >/dev/null 2>&1; then # Add zsh to /etc/shells if missing (required by chsh). if ! grep -qx "$zsh_path" /etc/shells 2>/dev/null; then echo "$zsh_path" | sudo tee -a /etc/shells >/dev/null fi if sudo chsh -s "$zsh_path" "$USER" 2>/dev/null || chsh -s "$zsh_path" 2>/dev/null; then log_success "Default shell set to zsh (takes effect on next login)" else log_warn "Could not set default shell automatically." log_warn "Run manually: chsh -s $zsh_path" fi else log_warn "chsh not available — set your default shell manually: chsh -s $zsh_path" fi fi } # ─── Step 4: Clone or update dotfiles ───────────────────────────────────────── setup_dotfiles() { step_header "Step 4 of 7 — Dotfiles" local is_git_repo=0 local is_this_repo=0 if git -C "$HOME" rev-parse --git-dir >/dev/null 2>&1; then is_git_repo=1 local remote_url remote_url="$(git -C "$HOME" remote get-url origin 2>/dev/null || echo '')" if echo "$remote_url" | grep -q "dev.pivoine.art/valknar/home"; then is_this_repo=1 fi fi # Idempotent update: repo already present and pointing to this remote → pull. if [[ -f "$HOME/arty.yml" && "$is_this_repo" == 1 ]]; then log_info "Dotfiles already present at $HOME" log_info "Pulling latest changes..." if ! git -C "$HOME" pull --ff-only 2>&1; then log_warn "Fast-forward pull failed — repo may have local divergence." log_warn "Inspect with: git -C ~ status" log_warn "Continuing with current state." else log_success "Dotfiles up to date" fi return 0 fi # Fresh install: initialise ~ as a git repo. log_info "Initialising dotfiles repository in $HOME" log_info "Remote: $REPO_HTTPS" ( cd "$HOME" git init if git remote get-url origin >/dev/null 2>&1; then log_warn "Replacing existing 'origin' remote (was: $(git remote get-url origin))" git remote remove origin fi git remote add origin "$REPO_HTTPS" log_info "Fetching repository..." git fetch --quiet # Force-checkout is intentional: this dotfiles repo uses an inverted # .gitignore (ignore everything, whitelist specific files), so only the # tracked dotfiles are overwritten — no other home directory files are touched. git checkout -f -t origin/main # Silence `git status` noise — the home directory has thousands of # untracked files that we deliberately do not track. git config status.showUntrackedFiles no ) log_success "Dotfiles cloned into $HOME" } # ─── Step 5: Install dependencies via arty ──────────────────────────────────── install_deps() { step_header "Step 5 of 7 — Dependencies (arty deps)" local arty_bin="$HOME/bin/arty" if [[ ! -f "$arty_bin" ]]; then log_error "arty not found at $arty_bin" log_error "Step 3 (dotfiles clone) may have failed." exit 1 fi if [[ ! -x "$arty_bin" ]]; then chmod +x "$arty_bin" fi log_info "Installing: oh-my-zsh, powerlevel10k, nvm, rbenv, pyenv, zsh plugins..." ( cd "$HOME" "$arty_bin" deps ) log_success "All dependencies installed" } # ─── Step 6: Install arty zsh completion ────────────────────────────────────── install_completion() { step_header "Step 6 of 7 — Zsh completion" local arty_bin="$HOME/bin/arty" # Ensure tools that arty deps may have installed (yq, etc.) are on PATH. # arty deps runs in a subshell — its PATH exports don't survive back here. export PATH="$HOME/.local/bin:$HOME/go/bin:$PATH" # arty completion zsh --install writes to the first writable fpath dir, # defaulting to ~/.zsh/completions/_arty (which .zshenv/oh-my-zsh picks up). (cd "$HOME" && "$arty_bin" completion zsh --install) || true if [[ ! -f "$HOME/.zsh/completions/_arty" ]]; then log_warn "arty completion zsh --install failed — skipping." fi } # ─── Step 7: Post-install summary ───────────────────────────────────────────── post_install() { step_header "Step 7 of 7 — Done" echo echo -e "${GREEN}${BOLD}Setup complete.${NC} Next steps:" echo echo -e " ${CYAN}1.${NC} First time on this machine? Configure the Powerlevel10k prompt:" echo -e " ${BOLD}p10k configure${NC}" echo echo -e " ${CYAN}2.${NC} Generate an SSH key for git push access to dev.pivoine.art:" echo -e " ${BOLD}arty ssh:keygen${NC}" echo -e " Then add the public key at ${BLUE}https://dev.pivoine.art${NC}" echo echo -e " ${CYAN}3.${NC} Switch the dotfiles remote from HTTPS to SSH (after key is set up):" echo -e " ${BOLD}git -C ~ remote set-url origin ssh://git@dev.pivoine.art/valknar/home.git${NC}" echo } # ─── Main ───────────────────────────────────────────────────────────────────── main() { echo echo -e "${BOLD}${CYAN}Valknar's dotfiles${NC} — ${BLUE}https://dev.pivoine.art/valknar/home${NC}" echo check_prerequisites # Step 1 ensure_git # Step 2 ensure_zsh # Step 3 setup_dotfiles # Step 4 install_deps # Step 5 install_completion # Step 6 post_install # Step 7 # Hand off to a zsh login shell immediately. # Read from /dev/tty so zsh gets the terminal, not the leftover pipe bytes # from the curl|sh pattern (which still has the tail of this script buffered). if command -v zsh >/dev/null 2>&1 && [[ -e /dev/tty ]]; then exec zsh -l