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:
2026-06-22 15:57:45 +02:00
co-authored by Claude Sonnet 4.6
parent cfee542f35
commit 7e16a6475b
3 changed files with 356 additions and 10 deletions
+1
View File
@@ -20,6 +20,7 @@
!bin/arty
!bin/stacks
!bin/README.md
!bin/setup
!.vscode/
!.vscode/settings.json
!.vscode/keybindings.json
+46 -10
View File
@@ -46,18 +46,53 @@
---
## QUICK START RITUAL
## INSTALLATION
### Summoning the Environment
### One-liner bootstrap
```bash
# Clone this unholy repository
git init && git remote add origin ssh://git@dev.pivoine.art/valknar/home.git
git fetch && git reset --hard origin/main
git branch --set-upstream-to=origin/main main
curl https://dev.pivoine.art/valknar/home/raw/branch/main/bin/setup -sSf | sh
```
# Clone scripts repository
git clone ssh://git@dev.pivoine.art/valknar/scripts.git ~/scripts
**Prerequisites:** `curl` or `wget` · `sh`/`bash` · `sudo` *(only needed on Debian/Ubuntu if git is missing)*
### What the setup script does
| Step | Action |
|------|--------|
| 1 | Detects OS (Debian/Ubuntu, macOS, other Linux) |
| 2 | Installs `git` if missing — `apt-get` on Debian/Ubuntu, `xcode-select` on macOS |
| 3 | Clones this repo into `~` via HTTPS — or `git pull` if already present *(idempotent)* |
| 4 | Runs `arty deps` — clones nvm, rbenv, pyenv, oh-my-zsh, powerlevel10k, zsh plugins |
| 5 | Installs `arty` zsh completion to `~/.zsh/completions/_arty` |
| 6 | Prints next steps |
### After installation
```bash
exec zsh # start a new shell session
p10k configure # first time: configure the Powerlevel10k prompt
arty ssh:keygen # generate an SSH key, then add it at https://dev.pivoine.art
# switch the dotfiles remote from HTTPS to SSH (after key is set up)
git -C ~ remote set-url origin ssh://git@dev.pivoine.art/valknar/home.git
```
---
## MANUAL SETUP
> The one-liner above handles all of this automatically.
```bash
cd ~
git init
git remote add origin https://dev.pivoine.art/valknar/home.git
git fetch && git checkout -f -t origin/main
git config status.showUntrackedFiles no
arty deps
```
---
@@ -98,9 +133,10 @@ arty docker:install # Docker Engine with user group
### Custom Scripts
Scripts in `~/scripts/` are loaded via PATH. They live in their own repo: `git@dev.pivoine.art:valknar/scripts.git`.
Scripts live in `~/bin/` and are tracked in this repo.
Available scripts: `stacks.sh` (Docker Compose stack manager), `arty.sh` (repository orchestration).
- **`arty`** — repository orchestration, reads `arty.yml`
- **`stacks`** — Docker Compose stack manager for `~/stacks`
---
Executable
+309
View File
@@ -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 "$@"