bin/setup: - Fix Step 1 of 6 → Step 1 of 7 - exec zsh -l </dev/tty to avoid inheriting stale curl pipe on stdin (without /dev/tty the curl|sh pipe bytes re-run the script as zsh) - Fall back to printing exec zsh hint when /dev/tty is unavailable - Check completion file existence instead of trusting arty exit code bin/arty: - Fix cmd_completion returning exit 1 for zsh: last statement was [[ "$shell" == "bash" ]] which is false, changed to if/fi Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
362 lines
14 KiB
Bash
Executable File
362 lines
14 KiB
Bash
Executable File
#!/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 </dev/tty
|
|
fi
|
|
echo -e "${YELLOW}[!]${NC} Start a new shell session: ${BOLD}exec zsh${NC}"
|
|
}
|
|
|
|
main "$@"
|