#!/usr/bin/env bash # 10-packages — core CLI utilities, per distro. step_name="packages" step_desc="git byobu ufw mosh btop eza fzf ripgrep bat zoxide jq fd ..." step_run="run_packages" # Tools we want everywhere (names canonical, mapped per-distro). PKGS_COMMON=( git byobu ufw mosh btop fzf ripgrep fd bat dust procs jq yq tree ncdu duf tldr httpie rsync lazygit neovim curl unzip less starship eza zoxide openssh ) run_packages() { log "installing core packages ($DISTRO)" case "$DISTRO" in arch) _pkg_arch ;; debian) _pkg_debian ;; esac } # ── arch ───────────────────────────────────────────────────────────────────── # official-repo packages (verified in extra/core). Any name not in the official # repos would abort the whole `pacman --needed` batch, so keep this list clean # and route AUR-only tools through _install_aur_packages instead. ARCH_OFFICIAL=(git ufw mosh btop fzf ripgrep fd bat dust procs jq yq tree ncdu duf tldr httpie rsync lazygit neovim curl unzip less starship eza zoxide openssh) # AUR-only packages (not in official repos) — installed through yay after build. ARCH_AUR=(byobu) _pkg_arch() { sudo_ pacman -Sy --noconfirm --needed base-devel openssh sudo_ pacman -Sy --noconfirm --needed "${ARCH_OFFICIAL[@]}" _install_yay _install_aur_packages # fd/bat ship with their real names on arch — no shims needed } _install_yay() { if command -v yay >/dev/null 2>&1; then ok "yay already installed"; return; fi log "building yay from AUR (requires a non-root sudoer)" if (( EUID == 0 )) && [[ -z "${TARGET_USER:-${SUDO_USER:-}}" ]]; then err "running as root with no target/sudo user — yay cannot build. Run the create-user step first, or re-run bootstrap as a normal user with sudo." return 1 fi local tmp; tmp="$(mktemp -d)" # chown the build dir so the non-root build user can write into it if (( EUID == 0 )); then chown -R "${TARGET_USER:-${SUDO_USER}}" "$tmp" fi as_user git clone --depth 1 https://aur.archlinux.org/yay.git "$tmp/yay" ( cd "$tmp/yay" && as_user makepkg -si --noconfirm ) rm -rf "$tmp" command -v yay >/dev/null 2>&1 && ok "yay installed" || warn "yay install may need a manual pass" } _install_aur_packages() { [[ "${#ARCH_AUR[@]}" -eq 0 ]] && return if ! command -v yay >/dev/null 2>&1; then warn "yay not available — skipping AUR packages: ${ARCH_AUR[*]}" return fi log "installing AUR-only packages: ${ARCH_AUR[*]}" # yay -S as the non-root target user (makepkg refuses root; TARGET_USER via as_user). as_user yay -S --noconfirm --needed "${ARCH_AUR[@]}" \ || warn "some AUR packages failed: ${ARCH_AUR[*]} (install manually with: yay -S ${ARCH_AUR[*]})" } # ── debian/ubuntu ──────────────────────────────────────────────────────────── _pkg_debian() { sudo_ apt-get update -y sudo_ apt-get install -y ca-certificates curl gnupg lsb-release unzip less \ git byobu ufw mosh btop fzf ripgrep jq tree ncdu rsync neovim curl \ openssh-server sudo # rename packages: fd -> fdfind, bat -> batcat (fix via aliases in zsh) sudo_ apt-get install -y fdfind batcat 2>/dev/null || true # eza: the old apt.fury.io/gierens PPA is dead (401 Unauthorized). Install the # single binary from the upstream GitHub release tarball into /usr/local/bin # (system-wide — works for root and every sudo user; no HOME dependency). # Remove a stale eza.list left by the old (pre-fix) run that used the now-dead # apt.fury.io PPA — otherwise 'apt-get update' keeps hitting the 401 forever. sudo_ rm -f /etc/apt/sources.list.d/eza.list /etc/apt/keyrings/gierens.gpg if ! command -v eza >/dev/null 2>&1; then local eza_arch; case "$ARCH" in amd64) eza_arch=x86_64-unknown-linux-gnu ;; arm64) eza_arch=aarch64-unknown-linux-gnu ;; *) eza_arch="${ARCH}-unknown-linux-gnu" ;; esac local url="https://github.com/eza-community/eza/releases/latest/download/eza_${eza_arch}.tar.gz" local tmp; tmp="$(mktemp -d)" if wget -qO "$tmp/eza.tgz" "$url" && tar xzf "$tmp/eza.tgz" -C "$tmp"; then sudo_ install -m 0755 "$tmp/eza" /usr/local/bin/eza else warn "eza download failed ($url)" fi rm -rf "$tmp" fi # starship: official installer. NOTE — scrub $ARCH/$PLATFORM from the env first: # the installer reuses a pre-set ARCH (our lib/detect.sh exports ARCH=amd64) and # skips its own amd64->x86_64 mapping, yielding an unsupported 'amd64-unknown-linux- # musl' target ('builds for amd64-unknown-linux-musl are not yet available'). # Default bin dir is /usr/local/bin (system-wide). if ! command -v starship >/dev/null 2>&1; then curl -sS https://starship.rs/install.sh \ | env -u ARCH -u PLATFORM sh -s -- -y || warn "starship install failed" fi # zoxide: official installer. Install system-wide (/usr/local/bin) instead of the # default ~/.local/bin — when run as root the default lands in /root (wrong user # for the create-user path) and isn't on $PATH; /usr/local/bin is on PATH for all. if ! command -v zoxide >/dev/null 2>&1; then curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh \ | env -u ARCH sh -s -- --bin-dir /usr/local/bin --man-dir /usr/local/share/man \ || warn "zoxide install failed" fi # yq: direct binary from mikefarah/yq releases (stable URL) if ! command -v yq >/dev/null 2>&1; then local suffix="amd64"; [[ "$ARCH" == "arm64" ]] && suffix="arm64" sudo_ wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${suffix}" \ && sudo_ chmod +x /usr/local/bin/yq || warn "yq install failed" fi # tldr on debian: python3 package if present, else skip (npm-based out of scope here) if ! command -v tldr >/dev/null 2>&1; then sudo_ apt-get install -y tldr 2>/dev/null || warn "tldr not packaged on this release — skipped (install via npm later)" fi # rest via GitHub release tarballs where missing (best-effort, arch auto-detected) _gh_release_binary bootandy/dust dust _gh_release_binary muesli/duf duf _gh_release_binary dalance/procs procs _gh_release_binary jesseduffield/lazygit lazygit # fd/bat on debian come from apt as fdfind/batcat (shimmed below) _apt_aliases_fixup } # generic best-effort: fetch latest release archive matching host arch, # extract a single binary into /usr/local/bin. Warns and continues on failure. _gh_release_binary() { local repo="$1" binname="$2" command -v "$binname" >/dev/null 2>&1 && { ok "$binname already present"; return; } local api="https://api.github.com/repos/$repo/releases/latest" local archpat case "$ARCH" in amd64) archpat='(x86_64|amd64)' ;; arm64) archpat='(aarch64|arm64)' ;; *) archpat="$ARCH" ;; esac local url url_line url_line=$(curl -fsSL "$api" \ | grep -oE '"browser_download_url":\s*"[^"]+"' \ | sed -E 's/.*"([^"]+)"/\1/' \ | grep -Ei "\\.(tar\\.gz|zip)$" \ | grep -Ei "$archpat" \ | grep -Ei 'linux|musl|gnu|unknown' \ | grep -viE 'sig|sha|checksum|deb|rpm|apk|msi|darwin|windows' \ | head -1) url="$url_line" if [[ -z "$url" ]]; then warn "could not resolve $binname release asset for $ARCH — skipped"; return; fi local tmp; tmp="$(mktemp -d)" if ! curl -fsSL "$url" -o "$tmp/asset"; then warn "download $binname failed"; rm -rf "$tmp"; return; fi case "$url" in *.tar.gz|*.tgz) tar -xzf "$tmp/asset" -C "$tmp" ;; *.zip) unzip -oq "$tmp/asset" -d "$tmp" ;; esac local found found=$(find "$tmp" -type f -name "$binname" -perm -u+x | head -1) [[ -z "$found" ]] && found=$(find "$tmp" -type f -name "$binname" | head -1) if [[ -n "$found" ]]; then sudo_ install -m 0755 "$found" "/usr/local/bin/$binname" ok "$binname installed from GitHub release" else warn "$binname binary not found in archive — skipped" fi rm -rf "$tmp" } # fd/bat: apt ships fdfind/batcat on debian; create canonical symlinks in ~/.local/bin. _apt_aliases_fixup() { local bin="$HOME/.local/bin"; mkdir -p "$bin" if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then ln -sf "$(command -v fdfind)" "$bin/fd" fi if command -v batcat >/dev/null 2>&1 && ! command -v bat >/dev/null 2>&1; then ln -sf "$(command -v batcat)" "$bin/bat" fi }