Files
home/bin/setup
T
valknarandClaude Sonnet 4.6 7f5991c1bd fix: setup and arty completion bugs
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>
2026-06-22 16:26:13 +02:00

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 "$@"