diff --git a/.gitignore b/.gitignore index ebf478a..639c980 100755 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ !bin/arty !bin/stacks !bin/README.md +!bin/setup !.vscode/ !.vscode/settings.json !.vscode/keybindings.json diff --git a/README.md b/README.md index e4e75a8..3337854 100755 --- a/README.md +++ b/README.md @@ -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` --- diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..28c2075 --- /dev/null +++ b/bin/setup @@ -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 "$@"