feat: add bin/setup bootstrap script and update README installation docs
- bin/setup: curl-pipeable installer — re-execs with bash, detects OS, installs git (apt/xcode-select), clones dotfiles into ~, runs arty deps, installs arty zsh completion, prints next steps; fully idempotent - README.md: replace Quick Start with Installation one-liner, add step table, After Installation and Manual Setup sections; fix stale ~/scripts reference - .gitignore: whitelist bin/setup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
#!/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 (Debian/Ubuntu, macOS, other Linux)
|
||||
# 3. Installs git if missing — apt-get on Debian/Ubuntu, xcode-select on macOS
|
||||
# 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 to ~/.zsh/completions/_arty
|
||||
# 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 6 — 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 6 — 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: Clone or update dotfiles ─────────────────────────────────────────
|
||||
setup_dotfiles() {
|
||||
step_header "Step 3 of 6 — 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 4: Install dependencies via arty ────────────────────────────────────
|
||||
install_deps() {
|
||||
step_header "Step 4 of 6 — 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 5: Install arty zsh completion ──────────────────────────────────────
|
||||
install_completion() {
|
||||
step_header "Step 5 of 6 — Zsh completion"
|
||||
|
||||
local arty_bin="$HOME/bin/arty"
|
||||
local completion_content
|
||||
|
||||
if ! completion_content="$(cd "$HOME" && "$arty_bin" completion zsh 2>/dev/null)"; then
|
||||
log_warn "arty completion zsh failed — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sanity-check: a valid zsh completion file starts with #compdef.
|
||||
if ! echo "$completion_content" | head -1 | grep -q "#compdef"; then
|
||||
log_warn "arty completion output is not a valid zsh completion — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Target: ~/.zsh/completions/_arty
|
||||
# .zshenv sets ZSH_CUSTOM="$HOME/.zsh"; oh-my-zsh adds $ZSH_CUSTOM/completions
|
||||
# to fpath automatically — this is the correct path for this configuration.
|
||||
local comp_dir="$HOME/.zsh/completions"
|
||||
local comp_dest="$comp_dir/_arty"
|
||||
|
||||
mkdir -p "$comp_dir"
|
||||
printf '%s\n' "$completion_content" > "$comp_dest"
|
||||
log_success "Completion installed: $comp_dest"
|
||||
}
|
||||
|
||||
# ─── Step 6: Post-install summary ─────────────────────────────────────────────
|
||||
post_install() {
|
||||
step_header "Step 6 of 6 — Done"
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}${BOLD}Setup complete.${NC} Next steps:"
|
||||
echo
|
||||
echo -e " ${CYAN}1.${NC} Start a new shell session:"
|
||||
echo -e " ${BOLD}exec zsh${NC}"
|
||||
echo
|
||||
echo -e " ${CYAN}2.${NC} First time on this machine? Configure the Powerlevel10k prompt:"
|
||||
echo -e " ${BOLD}p10k configure${NC}"
|
||||
echo
|
||||
echo -e " ${CYAN}3.${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}4.${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
|
||||
setup_dotfiles # Step 3
|
||||
install_deps # Step 4
|
||||
install_completion # Step 5
|
||||
post_install # Step 6
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user