From c10ec86a71420169282f9aa52bdb040ad12943d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20P=C4=99dzim=C4=85=C5=BC?= Date: Mon, 13 Apr 2026 18:06:24 +0200 Subject: [PATCH] chore: release v1.4.0 --- .github/workflows/install.yml | 46 ++++++++- .gitignore | 6 ++ CHANGELOG.md | 103 ++++++++++++++++++- CLAUDE.md | 33 +++++-- Dockerfile.nosudo | 82 +++++++++++++++ README.md | 8 +- VERSION | 2 +- test-local.sh => ci-local.sh | 181 ++++++++++++++++++++-------------- get.sh | 15 ++- git/.gitconfig | 30 +++++- install.sh | 12 ++- lib/utils.sh | 12 ++- modules/base.sh | 74 +++++++------- modules/neovim.sh | 6 +- modules/tmux.sh | 2 - modules/zsh.sh | 17 +++- scripts/install-cmake.sh | 6 +- scripts/install-git.sh | 6 +- test.sh | 52 ++++++---- tmux/.tmux.conf.local | 12 +-- update.sh | 108 +++++++++++--------- zsh/.zshrc | 11 +++ 22 files changed, 605 insertions(+), 219 deletions(-) create mode 100644 Dockerfile.nosudo rename test-local.sh => ci-local.sh (64%) diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index f962506..a52dc53 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -69,7 +69,7 @@ jobs: run: su -s /bin/bash -c 'HOME=/home/testuser; export HOME; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh ${{ matrix.profile }}' testuser install-nosudo: - name: "nosudo @ Ubuntu ${{ matrix.ubuntu }}" + name: "nosudo-${{ matrix.variant }} @ Ubuntu ${{ matrix.ubuntu }}" runs-on: ubuntu-latest timeout-minutes: 40 container: @@ -79,6 +79,9 @@ jobs: fail-fast: false matrix: ubuntu: ["20.04", "22.04", "24.04"] + variant: + - auto # user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false + - forced # user HAS passwordless sudo but NOSUDO=1 overrides it steps: - name: Bootstrap prerequisites (as root — mirrors a shared host) @@ -87,7 +90,14 @@ jobs: apt-get -yq update apt-get -yq install apt-utils git curl wget ca-certificates zsh tmux python3 - - name: Create non-root user WITHOUT sudo + - name: Install sudo and grant to user (nosudo-forced only) + if: matrix.variant == 'forced' + run: | + export DEBIAN_FRONTEND=noninteractive + apt-get -yq install --no-install-recommends sudo + echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + + - name: Create non-root user run: useradd -m -s /bin/bash user - name: Checkout dotfiles @@ -105,10 +115,17 @@ jobs: chown user:user /home/user/.curlrc chmod 600 /home/user/.curlrc - - name: Install — no-sudo path (minimal profile) + - name: Install (${{ matrix.variant }}) + env: + VARIANT: ${{ matrix.variant }} run: | + if [ "$VARIANT" = "forced" ]; then + install_cmd="NOSUDO=1 bash install.sh minimal" + else + install_cmd="bash install.sh minimal" + fi su -s /bin/bash -c \ - 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash install.sh minimal' \ + "HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \ user - name: Run test suite (nosudo profile) @@ -118,7 +135,26 @@ jobs: user - name: Idempotency — re-run install.sh + env: + VARIANT: ${{ matrix.variant }} + run: | + if [ "$VARIANT" = "forced" ]; then + install_cmd="NOSUDO=1 bash install.sh minimal" + else + install_cmd="bash install.sh minimal" + fi + su -s /bin/bash -c \ + "HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \ + user + + - name: Run update.sh run: | su -s /bin/bash -c \ - 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash install.sh minimal' \ + 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash update.sh' \ + user + + - name: Re-run test suite after update + run: | + su -s /bin/bash -c \ + 'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh nosudo' \ user diff --git a/.gitignore b/.gitignore index 4d2f7c1..8f59b55 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ parallel-*/ # Local test run artifacts .test-results/ + +# Neovim runtime artefacts (generated by lazy.nvim / neovim itself) +# nvim circular symlink: lazy.nvim adds the config dir to runtimepath via a self-referencing symlink +nvim/.config/nvim/nvim +# lazy-lock.json: plugin version lockfile — not tracked, always pull latest +nvim/.config/nvim/lazy-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index cee000d..0163f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.4.0] - 2026-04-13 + +### Added +- `Dockerfile.nosudo`: parameterized test image covering two no-sudo install + scenarios, selected via build args: + - `nosudo-auto` (`GRANT_SUDO=false`, `NOSUDO_INSTALL=""`): no `sudo` binary + present; `detect_sudo()` auto-detects `CAN_SUDO=false` — mirrors bare Ubuntu + containers and shared hosts without sudo + - `nosudo-forced` (`GRANT_SUDO=true`, `NOSUDO_INSTALL=1`): user has passwordless + `sudo` but `NOSUDO=1` overrides it — tests that the explicit env-var override + is respected even when sudo would otherwise work + Both variants install the `minimal` profile and land all binaries in `~/.local/bin`. + +### Changed +- `modules/tmux.sh`: removed `tmux-resurrect` and `tmux-continuum` from + `_TMUX_PLUGINS` — they were removed from `.tmux.conf.local` in v1.1.3 but the + installer was never updated; they were being cloned on every workstation install + without ever being sourced +- `test.sh`: comprehensive coverage improvements across all profiles: + - **Critical fix**: nosudo `sudo -v` check was `_fail` if sudo was present — + this always failed `nosudo-forced` (where user intentionally has sudo but + `NOSUDO=1` overrides it). Replaced with informational `_ok` for both variants; + the real invariant (binaries in `~/.local/bin`) is verified by `check_local_bin` + - `check_local_bin` made strict: no longer accepts system-path binaries via + `command -v` fallback — binary must be specifically at `~/.local/bin/` + so a `NOSUDO=1` regression (binary landing in `/usr/local/bin`) is caught + - Added `delta`, `jq`, `python3` to core tools section (installed by every + profile; were only partially or never tested) + - Added `eza` and `shellcheck` to the minimal/workstation section + - Added eza and delta functional smoke tests to the nosudo section + - Added tmux plugin dir checks (`tmux-fzf`, `tmux-cpu`) for workstation profile + - Fixed `_skip()`: used `$*` where `$1` was intended, doubling the label text + - Fixed fd smoke test label ("can find files") to actually search files instead of + running `--version` + - Removed unreachable `else _fail "zoxide not installed"` (zoxide is now in + core tools, which already catches a missing binary) + - Added `nosudo` to the profile list in the usage comment +- `ci-local.sh` (renamed from `test-local.sh`): expanded nosudo coverage from one + scenario to two variants (`forced` / `auto`); added `--profile nosudo-forced` and + `--profile nosudo-auto` CLI selectors (`--profile nosudo` still selects both); + `_run_step` gained an optional `-u USER` flag so nosudo containers run tests as + the owning `user` rather than root; `run_nosudo` now runs the full 5-step pipeline + (install → test → idempotency → update → re-test) matching `run_combination` for + regular profiles; total default combinations increased from 12 to 15 +- `.github/workflows/install.yml`: `install-nosudo` job expanded with + `variant: [auto, forced]` matrix axis (3 Ubuntu × 2 variants = 6 combinations, + up from 3); added `if: matrix.variant == 'forced'` step that installs `sudo` + and grants passwordless access only for the `forced` variant; added + `update.sh` step and re-test step so the no-sudo CI pipeline matches the + regular `install` job; total CI combinations increased from 12 to 15 + +### Fixed +- `get.sh` / `install.sh`: `NOSUDO=1 curl ... | bash` was silently ignored because + in a shell pipeline `VAR=val cmd1 | cmd2` the variable prefix is scoped only to + `cmd1` (curl), not `cmd2` (bash). Both scripts now accept `--nosudo` as a CLI flag + so the curl-pipe form works: `curl ... | bash -s -- --nosudo workstation`. + `NOSUDO=1 bash get.sh workstation` (local-file usage) is unchanged and still works. +- `README.md`: updated nosudo one-liner to use `--nosudo` flag instead of the broken + `NOSUDO=1 curl ...` form. +- `update.sh`: spurious `\"` in cheat URL pattern (`"linux-${cheat_arch}\""`) injected + a literal `"` into the grep pattern, making it impossible to match the `.gz` asset + URL. Cheat updates had been silently failing. Fixed to `"linux-${cheat_arch}.gz"`. +- `update.sh`: xcape update block used `local` and `trap ... RETURN` at script + top-level where `RETURN` traps never fire, leaking the mktemp directory on every + xcape rebuild. Extracted into `_do_update_xcape()` function so the trap fires + correctly on function return. +- `modules/base.sh`: no-sudo install paths for `delta`, `ripgrep`, `fd`, and `zoxide` + all constructed GitHub asset download URLs manually. CLAUDE.md forbids this because + asset names change between releases (delta 0.19.0 renamed its tarball). All four now + use `_gh_release_info` to look up the actual asset URL from the API, matching the + pattern already used by the sudo paths. +- `modules/zsh.sh` `_install_ohmyzsh`: `sh -c "$($installer)"` — if curl/wget failed, + `sh -c ""` returned 0 and `set -e` never triggered, silently reporting success while + oh-my-zsh was never installed. Now downloads the script into a variable with an + explicit failure guard before passing to `sh -c`. Installer failure also now warns + and returns instead of triggering `set -e`. +- `update.sh` oh-my-zsh update: `zsh ... || git pull` — if `git pull` (the fallback) + failed, `set -e` exited the entire update script, leaving all remaining tools not + updated with no warning message. Rewritten as `if/elif/else` that logs a warning + and continues. +- `lib/utils.sh` `_download_tar_bin`: no `trap RETURN` and no `|| return 1` on the + pipe, so a download failure triggered `set -e` exit inside the function — callers' + `|| log_warn` handlers were never reached. Added `trap RETURN` for cleanup and + `|| return 1` so failures propagate correctly to callers. +- `modules/neovim.sh` `install_neovim`, `update.sh` `_do_update_neovim`, + `update.sh` `_do_update_uv`, `update.sh` cheat update, + `modules/base.sh` `_install_eza` PPA: all had `curl | tar/gpg/gunzip` pipes with + no error handlers. A network failure would exit the whole script mid-run with no + user message. Each now logs a warning and returns/skips cleanly. +- `scripts/install-git.sh` / `scripts/install-cmake.sh`: `curl | tar` and `curl -o` + download failures were silently caught by `set -euo pipefail`, exiting the script + with no message. Both now use `|| die "…"` so network failures print an actionable + error before exiting. +- `modules/base.sh` `_install_eza` (no-sudo path) and `_install_jq`: used + `_gh_latest_tag_noapi` followed by manual URL construction — the same fragile + pattern that CLAUDE.md prohibits because asset names change between releases. + Both now use `_gh_release_info` to look up the verified asset URL in one API call. + ## [1.3.0] - 2026-04-11 ### Added @@ -232,7 +330,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `zsh` is not available; prints actionable reinstall hint - `test.sh`: `nosudo` profile — verifies all `~/.local/bin` binaries are present and functional; confirms `sudo` is absent in the test environment -- `test-local.sh`: `--profile nosudo` flag and `--skip-nosudo` flag; `run_nosudo` +- `ci-local.sh`: `--profile nosudo` flag and `--skip-nosudo` flag; `run_nosudo` runner builds via `Dockerfile.nosudo` and runs the nosudo test suite - `.github/workflows/install.yml`: `install-nosudo` job — matrix across Ubuntu 20.04 / 22.04 / 24.04 as a non-root user with no `sudo` @@ -332,7 +430,8 @@ Complete overhaul of the dotfiles infrastructure: modular profiles, Neovim, CI, ### Added - Initial dotfiles: Zsh (oh-my-zsh + fzf), Tmux, Vim, and monolithic `install.sh` -[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.3.0...HEAD +[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.4.0...HEAD +[1.4.0]: https://github.com/YASoftwareDev/dotfiles/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.5...v1.3.0 [1.2.5]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.4...v1.2.5 [1.2.4]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.3...v1.2.4 diff --git a/CLAUDE.md b/CLAUDE.md index 26188c2..8448a24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,10 @@ CI tests all profile × Ubuntu-version combinations on every push. install.sh entry point — profile selection, module orchestration update.sh managed tool updates with --check mode + PATH shadow check test.sh post-install validation suite (profile-aware) +ci-local.sh local Docker matrix runner — mirrors GitHub CI matrix get.sh curl-pipe bootstrap (clones repo → runs install.sh) +Dockerfile bakes docker profile into an image at build time +Dockerfile.nosudo parameterized no-sudo test image (two variants: forced/auto) lib/utils.sh shared logging, sudo detection, GitHub helpers, binary utils modules/ base.sh apt packages + per-tool installers (fzf, zoxide, delta, eza, …) @@ -316,10 +319,24 @@ bash test.sh [docker|minimal|workstation|nosudo] Exits 0 (all pass), 1 (any fail). Skips are not failures. Runs in CI after every install + again after update.sh. +Core checks (all profiles): zsh, tmux, git, python3, fzf, ripgrep, git-delta, +zoxide, jq, fd; fzf shell integration + functional filter; zsh syntax check; +oh-my-zsh + plugins + powerlevel10k dirs; tmux detached session start; git config +diff driver; zoxide init + add + query. + Profile-specific checks: -- `workstation` — nvim, delta, uv, cheat, config symlinks -- `minimal` — ranger, tig, parallel -- `nosudo` — all `~/.local/bin` binaries present + sudo NOT available +- `workstation` — nvim, uv, cheat; config symlinks (nvim, ripgrep, ranger); + tmux plugin dirs (tmux-fzf, tmux-cpu) +- `minimal` — ranger, tig, parallel, eza, shellcheck (all via apt) +- `nosudo` — strict `~/.local/bin` presence check for all 7 GitHub binaries + (rg, fd, jq, fzf, zoxide, delta, eza); sudo availability info (not a failure + condition — nosudo-forced has sudo available); functional smoke tests for each + +Two nosudo scenarios are validated by `Dockerfile.nosudo`: +- **nosudo-auto** — no `sudo` binary; `detect_sudo()` auto-detects `CAN_SUDO=false` + (`GRANT_SUDO=false NOSUDO_INSTALL=""` build args) +- **nosudo-forced** — user has passwordless `sudo` but `NOSUDO=1` overrides it + (`GRANT_SUDO=true NOSUDO_INSTALL=1` build args) --- @@ -330,10 +347,14 @@ Profile-specific checks: - testuser with passwordless sudo; curl auth via `~/.curlrc` - Flow: install → idempotency re-run → test → update → re-test -**Job `install-nosudo`** (3 combinations): -- Ubuntu 20.04 / 22.04 / 24.04 +**Job `install-nosudo`** (6 combinations — 3 Ubuntu × 2 variants): +- Ubuntu 20.04 / 22.04 / 24.04 × variant `auto` / `forced` - Root pre-installs: git, curl, wget, ca-certificates, zsh, tmux, python3 -- Regular user with no sudo; `NOSUDO=1` path exercised +- `auto`: no `sudo` binary installed; `detect_sudo()` auto-detects `CAN_SUDO=false` +- `forced`: passwordless `sudo` installed, but `NOSUDO=1` overrides it +- Flow: install → test → idempotency re-run → update → re-test + +**Total: 15 CI combinations** (9 regular + 6 nosudo) --- diff --git a/Dockerfile.nosudo b/Dockerfile.nosudo new file mode 100644 index 0000000..d81d4d0 --- /dev/null +++ b/Dockerfile.nosudo @@ -0,0 +1,82 @@ +# Dockerfile.nosudo +# +# Covers two user-local install scenarios (both land all binaries in ~/.local/bin): +# +# nosudo-auto — user has NO sudo; detect_sudo() auto-detects CAN_SUDO=false. +# Ubuntu base images ship without sudo, so no extra setup needed. +# Build args: GRANT_SUDO=false (default), NOSUDO_INSTALL="" (default) +# +# nosudo-forced — user HAS passwordless sudo but NOSUDO=1 overrides it. +# Tests that the explicit override is respected even when sudo works. +# Build args: GRANT_SUDO=true, NOSUDO_INSTALL=1 +# +# Used automatically by ci-local.sh (runs both variants) or directly: +# +# # nosudo-auto (default) +# docker build --build-arg UBUNTU=24.04 \ +# -t dotfiles-test:24.04-nosudo-auto -f Dockerfile.nosudo . +# +# # nosudo-forced +# docker build --build-arg UBUNTU=24.04 \ +# --build-arg GRANT_SUDO=true --build-arg NOSUDO_INSTALL=1 \ +# -t dotfiles-test:24.04-nosudo-forced -f Dockerfile.nosudo . +# +# Then run the test suite (ci-local.sh does this automatically): +# docker run --rm --user user -e TERM=xterm-256color \ +# -e POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true \ +# bash -c \ +# 'export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash test.sh nosudo' + +ARG UBUNTU=24.04 +FROM ubuntu:${UBUNTU} + +ENV DEBIAN_FRONTEND=noninteractive +ENV TERM=xterm-256color +ENV POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true + +# ARGs re-declared after FROM so they are available in RUN instructions. +# GRANT_SUDO=false : don't install sudo package (nosudo-auto) +# sudo binary absent → detect_sudo() → CAN_SUDO=false +# GRANT_SUDO=true : install sudo + add user to sudoers (nosudo-forced) +# sudo works, but NOSUDO=1 overrides below +ARG GRANT_SUDO=false + +# NOSUDO_INSTALL="" : no override — detect_sudo() decides (nosudo-auto) +# NOSUDO_INSTALL=1 : NOSUDO=1 passed to installer — forces user-local (nosudo-forced) +ARG NOSUDO_INSTALL= + +# ── Step 1-2: Base prerequisites (always installed) ─────────────────────────── +RUN apt-get -yq update && \ + apt-get -yq install --no-install-recommends \ + apt-utils git curl wget ca-certificates zsh tmux python3 && \ + rm -rf /var/lib/apt/lists/* + +# ── Step 2b: Optionally install sudo (nosudo-forced only) ───────────────────── +# nosudo-auto intentionally omits the sudo binary so detect_sudo() auto-detects. +# nosudo-forced needs a working sudo so the NOSUDO=1 override is meaningful. +RUN if [ "${GRANT_SUDO}" = "true" ]; then \ + apt-get -yq update && \ + apt-get -yq install --no-install-recommends sudo && \ + rm -rf /var/lib/apt/lists/*; \ + fi + +# ── Step 3: Create non-root user; conditionally grant passwordless sudo ─────── +RUN useradd -m -s /bin/bash user && \ + if [ "${GRANT_SUDO}" = "true" ]; then \ + echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers; \ + fi + +# ── Step 4: Copy dotfiles source owned by the non-root user ────────────────── +COPY --chown=user:user . /home/user/dotfiles + +# ── Step 5: Switch to non-privileged user ───────────────────────────────────── +USER user +WORKDIR /home/user + +# ── Step 6: Install ──────────────────────────────────────────────────────────── +# NOSUDO="${NOSUDO_INSTALL}" is "" for nosudo-auto (detect_sudo decides) +# and "1" for nosudo-forced (overrides working sudo). +# 'minimal' profile: zsh, tmux, git config + ~7 GitHub-tarball binaries. +RUN cd dotfiles && NOSUDO="${NOSUDO_INSTALL}" bash install.sh minimal + +CMD ["bash"] diff --git a/README.md b/README.md index 522b2b4..70e9049 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.s **nosudo** — any profile without apt/sudo; all tools land in `~/.local/bin` (~10–20 min) ```bash -NOSUDO=1 curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- workstation +curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- --nosudo workstation # or, after cloning: NOSUDO=1 ./install.sh workstation ``` @@ -237,8 +237,10 @@ cd ~/.dotfiles && ./update.sh ~/.dotfiles/ ├── install.sh # main entry point — profile wizard + runner ├── update.sh # updater for all managed tools and plugins -├── test.sh # integration tests -├── Dockerfile # for testing the docker profile +├── test.sh # integration tests (profile-aware) +├── ci-local.sh # local Docker matrix runner — mirrors GitHub CI +├── Dockerfile # for baking dotfiles into a Docker image (docker profile) +├── Dockerfile.nosudo # parameterized test image for no-sudo install scenarios │ ├── lib/ │ └── utils.sh # shared: logging, symlink, apt_install, GitHub helpers, checks diff --git a/VERSION b/VERSION index f0bb29e..88c5fb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 +1.4.0 diff --git a/test-local.sh b/ci-local.sh similarity index 64% rename from test-local.sh rename to ci-local.sh index 5a4ca2f..e2c599f 100755 --- a/test-local.sh +++ b/ci-local.sh @@ -2,12 +2,12 @@ # Local matrix test runner — mirrors the GitHub Actions CI matrix. # # Usage: -# bash test-local.sh # run all combinations -# bash test-local.sh --ubuntu 22.04 # single Ubuntu version -# bash test-local.sh --profile workstation # single profile -# bash test-local.sh --ubuntu 22.04 --profile docker # single cell -# bash test-local.sh --no-cache # rebuild images from scratch -# bash test-local.sh --help +# bash ci-local.sh # run all combinations +# bash ci-local.sh --ubuntu 22.04 # single Ubuntu version +# bash ci-local.sh --profile workstation # single profile +# bash ci-local.sh --ubuntu 22.04 --profile docker # single cell +# bash ci-local.sh --no-cache # rebuild images from scratch +# bash ci-local.sh --help # # Requirements: docker must be installed and running. # Log files: ./.test-results/-.log @@ -19,8 +19,11 @@ DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ── Defaults ────────────────────────────────────────────────────────────────── ALL_UBUNTU=(20.04 22.04 24.04) ALL_PROFILES=(docker minimal workstation) -# nosudo is a special profile: it uses Dockerfile.nosudo and a non-root user. +# nosudo variants use Dockerfile.nosudo with a non-root user. +# forced: user HAS sudo but NOSUDO=1 overrides it (tests the explicit override) +# auto: user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false ALL_NOSUDO_UBUNTU=(20.04 22.04 24.04) +ALL_NOSUDO_VARIANTS=(forced auto) FILTER_UBUNTU=() FILTER_PROFILES=() NO_CACHE=false @@ -38,7 +41,7 @@ NC='\033[0m' # ── Argument parsing ────────────────────────────────────────────────────────── usage() { cat <"$logfile" # truncate echo "" - echo -e "${BOLD}── Ubuntu ${ubuntu} / nosudo ──${NC}" + echo -e "${BOLD}── Ubuntu ${ubuntu} / nosudo-${variant} ──${NC}" # 1. Build (install.sh runs as non-root user during docker build) - _build_nosudo "$ubuntu" || return 1 + _build_nosudo "$ubuntu" "$variant" || return 1 - # 2. Test suite (run as the same non-root user) - echo -e " ${BLUE}→${NC} test.sh nosudo ..." - if docker run --rm \ - -e TERM=xterm-256color \ - -e POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true \ - --user user \ - "$tag" bash -c \ + # 2. Test suite + _run_step -u user "$tag" "test.sh nosudo" "$logfile" \ "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash test.sh nosudo" \ - >>"$logfile" 2>&1; then - echo -e " ${GREEN}✓${NC} test.sh nosudo" - else - echo -e " ${RED}✗${NC} test.sh nosudo FAILED (see $logfile)" - return 1 - fi + || return 1 - # 3. Idempotency — re-run install.sh as non-root user - echo -e " ${BLUE}→${NC} idempotency (re-run install.sh minimal) ..." - if docker run --rm \ - -e TERM=xterm-256color \ - --user user \ - "$tag" bash -c \ - "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash install.sh minimal" \ - >>"$logfile" 2>&1; then - echo -e " ${GREEN}✓${NC} idempotency" - else - echo -e " ${RED}✗${NC} idempotency FAILED (see $logfile)" - return 1 - fi + # 3. Idempotency — re-run install.sh (must be a no-op) + _run_step -u user "$tag" "idempotency (re-run install.sh minimal)" "$logfile" \ + "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && ${install_cmd}" \ + || return 1 + + # 4. update.sh + _run_step -u user "$tag" "update.sh" "$logfile" \ + "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash update.sh" \ + || return 1 + + # 5. Re-validate after update + _run_step -u user "$tag" "test.sh nosudo (after update)" "$logfile" \ + "export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && bash test.sh nosudo" \ + || return 1 return 0 } @@ -299,14 +324,22 @@ else NOSUDO_LIST=("${ALL_NOSUDO_UBUNTU[@]}") fi -nosudo_count=${#NOSUDO_LIST[@]} +# Resolve which nosudo variants to run +if [[ ${#FILTER_NOSUDO_VARIANTS[@]} -gt 0 ]]; then + NOSUDO_VARIANT_LIST=("${FILTER_NOSUDO_VARIANTS[@]}") +else + NOSUDO_VARIANT_LIST=("${ALL_NOSUDO_VARIANTS[@]}") +fi + +nosudo_count=$(( ${#NOSUDO_LIST[@]} * ${#NOSUDO_VARIANT_LIST[@]} )) regular_count=$(( ${#UBUNTU_LIST[@]} * ${#PROFILE_LIST[@]} )) -echo -e " Ubuntu versions : ${UBUNTU_LIST[*]}" -echo -e " Profiles : ${PROFILE_LIST[*]}" -echo -e " No-sudo Ubuntu : ${NOSUDO_LIST[*]:-none}" -echo -e " Combinations : $((regular_count + nosudo_count))" -echo -e " Logs : ${LOG_DIR}/" +echo -e " Ubuntu versions : ${UBUNTU_LIST[*]}" +echo -e " Profiles : ${PROFILE_LIST[*]:-none}" +echo -e " No-sudo Ubuntu : ${NOSUDO_LIST[*]:-none}" +echo -e " No-sudo variants : ${NOSUDO_VARIANT_LIST[*]:-none}" +echo -e " Combinations : $((regular_count + nosudo_count))" +echo -e " Logs : ${LOG_DIR}/" for ubuntu in "${UBUNTU_LIST[@]}"; do for profile in "${PROFILE_LIST[@]}"; do @@ -322,14 +355,16 @@ for ubuntu in "${UBUNTU_LIST[@]}"; do done for ubuntu in "${NOSUDO_LIST[@]}"; do - total=$((total + 1)) - if run_nosudo "$ubuntu"; then - passed=$((passed + 1)) - results+=("${GREEN}PASS${NC} Ubuntu ${ubuntu} / nosudo") - else - failed=$((failed + 1)) - results+=("${RED}FAIL${NC} Ubuntu ${ubuntu} / nosudo → ${LOG_DIR}/${ubuntu}-nosudo.log") - fi + for variant in "${NOSUDO_VARIANT_LIST[@]}"; do + total=$((total + 1)) + if run_nosudo "$ubuntu" "$variant"; then + passed=$((passed + 1)) + results+=("${GREEN}PASS${NC} Ubuntu ${ubuntu} / nosudo-${variant}") + else + failed=$((failed + 1)) + results+=("${RED}FAIL${NC} Ubuntu ${ubuntu} / nosudo-${variant} → ${LOG_DIR}/${ubuntu}-nosudo-${variant}.log") + fi + done done # ── Summary table ───────────────────────────────────────────────────────────── diff --git a/get.sh b/get.sh index 7e80004..e83d1ad 100755 --- a/get.sh +++ b/get.sh @@ -13,8 +13,8 @@ # curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- docker # # force user-local installs (no apt, ~/.local/bin only) — useful on shared machines: -# NOSUDO=1 bash get.sh workstation -# (NOSUDO=1 is forwarded automatically to install.sh via exec) +# curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- --nosudo workstation +# (or, if you have the file locally: NOSUDO=1 bash get.sh workstation) # # Or inspect first, then run (also gives you the interactive wizard): # curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh -o get.sh @@ -37,7 +37,16 @@ set -euo pipefail REPO="https://github.com/YASoftwareDev/dotfiles.git" DEST="${DOTFILES_DIR:-$HOME/.dotfiles}" -PROFILE="${1:-}" + +# Parse args: [--nosudo] [profile] +PROFILE="" +for _arg in "$@"; do + case "$_arg" in + --nosudo) export NOSUDO=1 ;; + *) PROFILE="$_arg" ;; + esac +done +unset _arg # ── Colour helpers ───────────────────────────────────────────────────────────── BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' diff --git a/git/.gitconfig b/git/.gitconfig index c28a296..6bf24a1 100644 --- a/git/.gitconfig +++ b/git/.gitconfig @@ -1,6 +1,7 @@ [core] attributesFile = ~/.gitattributes - pager = delta + pager = bat --paging=always + editor = nvim [diff "zip"] textconv = unzip -p @@ -35,9 +36,9 @@ textconv = hexdump -v -C [init] - defaultBranch = main + defaultBranch = main [color] - ui = true + ui = auto [interactive] diffFilter = delta --color-only @@ -51,6 +52,27 @@ [include] path = ~/.gitconfig.local +[user] + name = Wojciech Pędzimąż + email = wojciech.pedzimaz@techmo.pl +[pull] + rebase = true +[rebase] + autoStash = true +[fetch] + prune = true +[diff] + tool = vimdiff [credential "https://gitlab.gnome.org"] - helper = + helper = + helper = !/usr/bin/glab auth git-credential +[credential "https://github.com"] + helper = + helper = !/usr/bin/gh auth git-credential +[credential "https://gist.github.com"] + helper = + helper = !/usr/bin/gh auth git-credential +[url "https://gitlab.devtechmo.pl/"] + insteadOf = git@gitlab.devtechmo.pl: +[credential "https://gitlab.devtechmo.pl"] helper = !/usr/bin/glab auth git-credential diff --git a/install.sh b/install.sh index c585fe6..f4ba075 100755 --- a/install.sh +++ b/install.sh @@ -7,6 +7,7 @@ # ./install.sh workstation # everything (default for non-interactive runs) # ./install.sh docker # headless, CI-friendly, no shell change # NOSUDO=1 ./install.sh workstation # force user-local (~/.local/bin) installs, skip apt +# ./install.sh --nosudo workstation # same as above via flag (works in curl-pipe) # set -euo pipefail @@ -80,7 +81,16 @@ _link_git_config() { _banner # Determine profile: arg > wizard (if interactive) > default -PROFILE="${1:-}" +# Also accept --nosudo flag (needed when called from curl-pipe where env prefix +# would only apply to curl, not bash: curl ... | bash -s -- --nosudo workstation) +PROFILE="" +for _arg in "$@"; do + case "$_arg" in + --nosudo) export NOSUDO=1 ;; + *) PROFILE="$_arg" ;; + esac +done +unset _arg if [ -z "$PROFILE" ] && [ -t 0 ]; then PROFILE="$(_wizard)" elif [ -z "$PROFILE" ]; then diff --git a/lib/utils.sh b/lib/utils.sh index 9b3e4d9..bb09581 100755 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -236,19 +236,23 @@ _download_tar_bin() { mkdir -p "$(dirname "$dest")" local tmp tmp=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf '$tmp'" RETURN local -a tar_flags case "$url" in *.tar.xz|*.txz) tar_flags=(-xJ) ;; *) tar_flags=(-xz) ;; esac - if has curl; then curl -sfL "$url" | tar "${tar_flags[@]}" -C "$tmp" - else wget -qO- "$url" | tar "${tar_flags[@]}" -C "$tmp"; fi + # Use || return 1 so a pipe failure returns to the caller instead of + # triggering set -e and exiting the whole script — callers' || handlers + # would never execute if set -e exits inside this function. + if has curl; then curl -sfL "$url" | tar "${tar_flags[@]}" -C "$tmp" || return 1 + else wget -qO- "$url" | tar "${tar_flags[@]}" -C "$tmp" || return 1; fi local found found=$(find "$tmp" -name "$binname" -type f | head -1) - if [ -z "$found" ]; then rm -rf "$tmp"; return 1; fi + [ -z "$found" ] && return 1 mv "$found" "$dest" chmod +x "$dest" - rm -rf "$tmp" } # Return 0 (true) if version string $1 is strictly older than $2. diff --git a/modules/base.sh b/modules/base.sh index 7bf33f3..c47cd18 100755 --- a/modules/base.sh +++ b/modules/base.sh @@ -139,13 +139,10 @@ _install_zoxide() { ;; esac - # Resolve download URL via redirect (no GitHub API rate limits) - local url="" tag ver - tag=$(_gh_latest_tag_noapi "ajeetdsouza/zoxide") || tag="" - if [ -n "$tag" ]; then - ver="${tag#v}" - url="https://github.com/ajeetdsouza/zoxide/releases/download/${tag}/zoxide-${ver}-${zoxide_arch}.tar.gz" - fi + # Use _gh_release_info to look up the exact asset URL — avoids fragile manual + # construction that could break if zoxide renames assets between releases. + local tag="" url="" + read -r tag url < <(_gh_release_info "ajeetdsouza/zoxide" "${zoxide_arch}.tar.gz") || true if [ -z "$url" ]; then if $CAN_SUDO && [ -n "$apt_ver" ]; then @@ -226,14 +223,16 @@ _install_delta() { return ;; esac - local tag; tag=$(_gh_latest_tag_noapi "dandavison/delta") || tag="" + local tag="" url="" + # Use _gh_release_info to get the actual asset URL — never construct it + # manually; delta asset names have changed between releases. + read -r tag url < <(_gh_release_info "dandavison/delta" "${delta_arch}.tar.gz") || true local ver="${tag#v}" - if [ -z "$ver" ]; then - log_warn "git-delta: could not determine latest release — skipping" + if [ -z "$url" ]; then + log_warn "git-delta: could not find release URL — skipping" return fi - local url="https://github.com/dandavison/delta/releases/download/${ver}/delta-${ver}-${delta_arch}.tar.gz" - log_info "git-delta: installing $ver → ~/.local/bin/delta" + log_info "git-delta: installing ${ver:-unknown} → ~/.local/bin/delta" if _download_tar_bin "$url" "delta" ~/.local/bin/delta; then log_ok "git-delta installed → ~/.local/bin/delta ($(~/.local/bin/delta --version 2>/dev/null))" else @@ -263,10 +262,12 @@ _install_eza() { $SUDO mkdir -p /etc/apt/keyrings if has curl; then curl -fsSL https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ - | $SUDO gpg --dearmor -o /etc/apt/keyrings/gierens.gpg + | $SUDO gpg --dearmor -o /etc/apt/keyrings/gierens.gpg \ + || { log_warn "eza: failed to add PPA signing key — skipping"; return; } else wget -qO- https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ - | $SUDO gpg --dearmor -o /etc/apt/keyrings/gierens.gpg + | $SUDO gpg --dearmor -o /etc/apt/keyrings/gierens.gpg \ + || { log_warn "eza: failed to add PPA signing key — skipping"; return; } fi echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://deb.gierens.de stable main" \ | $SUDO tee /etc/apt/sources.list.d/gierens.list > /dev/null @@ -286,13 +287,15 @@ _install_eza() { return ;; esac - local tag; tag=$(_gh_latest_tag_noapi "eza-community/eza") || tag="" - if [ -z "$tag" ]; then - log_warn "eza: could not determine latest release — skipping" + local tag="" url="" + # Use _gh_release_info to get the verified asset URL — avoids fragile manual + # construction that could break if eza renames assets between releases. + read -r tag url < <(_gh_release_info "eza-community/eza" "eza_${eza_arch}.tar.gz") || true + if [ -z "$url" ]; then + log_warn "eza: could not find release URL — skipping" return fi - local url="https://github.com/eza-community/eza/releases/download/${tag}/eza_${eza_arch}.tar.gz" - log_info "eza: installing $tag → ~/.local/bin/eza" + log_info "eza: installing ${tag:-unknown} → ~/.local/bin/eza" if _download_tar_bin "$url" "eza" ~/.local/bin/eza; then log_ok "eza installed → ~/.local/bin/eza ($(~/.local/bin/eza --version 2>/dev/null | head -1))" else @@ -318,14 +321,13 @@ _install_ripgrep() { return ;; esac - local tag; tag=$(_gh_latest_tag_noapi "BurntSushi/ripgrep") || tag="" - if [ -z "$tag" ]; then - log_warn "ripgrep: could not determine latest release — skipping" + local tag="" url="" + read -r tag url < <(_gh_release_info "BurntSushi/ripgrep" "${rg_arch}.tar.gz") || true + if [ -z "$url" ]; then + log_warn "ripgrep: could not find release URL — skipping" return fi - local ver="${tag#v}" - local url="https://github.com/BurntSushi/ripgrep/releases/download/${tag}/ripgrep-${ver}-${rg_arch}.tar.gz" - log_info "ripgrep: installing $tag → ~/.local/bin/rg" + log_info "ripgrep: installing ${tag:-unknown} → ~/.local/bin/rg" if _download_tar_bin "$url" "rg" ~/.local/bin/rg; then log_ok "ripgrep installed → ~/.local/bin/rg ($(~/.local/bin/rg --version 2>/dev/null | head -1))" else @@ -350,13 +352,13 @@ _install_fd() { return ;; esac - local tag; tag=$(_gh_latest_tag_noapi "sharkdp/fd") || tag="" - if [ -z "$tag" ]; then - log_warn "fd: could not determine latest release — skipping" + local tag="" url="" + read -r tag url < <(_gh_release_info "sharkdp/fd" "${fd_arch}.tar.gz") || true + if [ -z "$url" ]; then + log_warn "fd: could not find release URL — skipping" return fi - local url="https://github.com/sharkdp/fd/releases/download/${tag}/fd-${tag}-${fd_arch}.tar.gz" - log_info "fd: installing $tag → ~/.local/bin/fd" + log_info "fd: installing ${tag:-unknown} → ~/.local/bin/fd" if _download_tar_bin "$url" "fd" ~/.local/bin/fd; then log_ok "fd installed → ~/.local/bin/fd ($(~/.local/bin/fd --version 2>/dev/null))" else @@ -381,13 +383,15 @@ _install_jq() { return ;; esac - local tag; tag=$(_gh_latest_tag_noapi "jqlang/jq") || tag="" - if [ -z "$tag" ]; then - log_warn "jq: could not determine latest release — skipping" + local tag="" url="" + # Use _gh_release_info to get the verified asset URL — avoids fragile manual + # construction that could break if jq renames assets between releases. + read -r tag url < <(_gh_release_info "jqlang/jq" "jq-linux-${jq_arch}") || true + if [ -z "$url" ]; then + log_warn "jq: could not find release URL — skipping" return fi - local url="https://github.com/jqlang/jq/releases/download/${tag}/jq-linux-${jq_arch}" - log_info "jq: installing $tag → ~/.local/bin/jq" + log_info "jq: installing ${tag:-unknown} → ~/.local/bin/jq" local ok=true if has curl; then curl -sfLo ~/.local/bin/jq "$url" || ok=false diff --git a/modules/neovim.sh b/modules/neovim.sh index 474652c..c486428 100755 --- a/modules/neovim.sh +++ b/modules/neovim.sh @@ -143,9 +143,11 @@ install_neovim() { trap "rm -rf '$tmp'" RETURN if has curl; then - curl -sfL "$url" | tar -xz -C "$tmp" + curl -sfL "$url" | tar -xz -C "$tmp" \ + || { log_warn "neovim: download/extraction failed — skipping"; return; } else - wget -qO- "$url" | tar -xz -C "$tmp" + wget -qO- "$url" | tar -xz -C "$tmp" \ + || { log_warn "neovim: download/extraction failed — skipping"; return; } fi # Tarball extracts to nvim-linux-x86_64/ or nvim-linux-arm64/ diff --git a/modules/tmux.sh b/modules/tmux.sh index 547844f..c1c2357 100755 --- a/modules/tmux.sh +++ b/modules/tmux.sh @@ -5,8 +5,6 @@ # Plugins sourced directly via run-shell in .tmux.conf.local # (gpakosz framework uses 'if ... source' syntax that TPM's auto-install can't parse) _TMUX_PLUGINS=( - "tmux-plugins/tmux-resurrect" - "tmux-plugins/tmux-continuum" "sainnhe/tmux-fzf" "tmux-plugins/tmux-cpu" ) diff --git a/modules/zsh.sh b/modules/zsh.sh index 366bd97..6f75ccc 100755 --- a/modules/zsh.sh +++ b/modules/zsh.sh @@ -35,14 +35,21 @@ _install_ohmyzsh() { return fi - local installer + local _omz_url="https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" + local _omz_script + log_info "oh-my-zsh: installing latest → ~/.oh-my-zsh" + # Download the installer into a variable first so that a network failure + # produces an explicit warning instead of silently running `sh -c ""` and + # reporting success. (sh -c "" exits 0, masking the download failure.) if has curl; then - installer="curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" + _omz_script=$(curl -fsSL "$_omz_url") \ + || { log_warn "oh-my-zsh: download failed — skipping"; return; } else - installer="wget -qO- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" + _omz_script=$(wget -qO- "$_omz_url") \ + || { log_warn "oh-my-zsh: download failed — skipping"; return; } fi - log_info "oh-my-zsh: installing latest → ~/.oh-my-zsh" - RUNZSH=no CHSH=no sh -c "$($installer)" 2>/dev/null + RUNZSH=no CHSH=no sh -c "$_omz_script" 2>/dev/null \ + || { log_warn "oh-my-zsh: installer failed — skipping"; return; } log_ok "oh-my-zsh installed" } diff --git a/scripts/install-cmake.sh b/scripts/install-cmake.sh index 179ee0d..31aae50 100755 --- a/scripts/install-cmake.sh +++ b/scripts/install-cmake.sh @@ -39,9 +39,11 @@ trap 'rm -f "$installer"' EXIT log_info "Downloading cmake ${CMAKE_VERSION} installer…" if has curl; then - curl -sfL "$installer_url" -o "$installer" + curl -sfL "$installer_url" -o "$installer" \ + || die "cmake: download failed (check network and version ${CMAKE_VERSION})" else - wget -qO "$installer" "$installer_url" + wget -qO "$installer" "$installer_url" \ + || die "cmake: download failed (check network and version ${CMAKE_VERSION})" fi chmod +x "$installer" diff --git a/scripts/install-git.sh b/scripts/install-git.sh index ad00df1..7186da4 100755 --- a/scripts/install-git.sh +++ b/scripts/install-git.sh @@ -43,9 +43,11 @@ trap 'rm -rf "$tmp"' EXIT url="https://www.kernel.org/pub/software/scm/git/git-${GIT_VERSION}.tar.gz" log_info "Downloading git ${GIT_VERSION}…" if has curl; then - curl -sfL "$url" | tar -xz -C "$tmp" + curl -sfL "$url" | tar -xz -C "$tmp" \ + || die "git: download/extraction failed (check network and version ${GIT_VERSION})" else - wget -qO- "$url" | tar -xz -C "$tmp" + wget -qO- "$url" | tar -xz -C "$tmp" \ + || die "git: download/extraction failed (check network and version ${GIT_VERSION})" fi src="${tmp}/git-${GIT_VERSION}" diff --git a/test.sh b/test.sh index 2ee72ef..34109ad 100644 --- a/test.sh +++ b/test.sh @@ -2,7 +2,7 @@ # Automated post-install test suite # # Usage: -# bash test.sh [PROFILE] # PROFILE: docker | minimal | workstation (default: docker) +# bash test.sh [PROFILE] # PROFILE: docker | minimal | workstation | nosudo (default: docker) # # Run inside a freshly built container: # docker run --rm dotfiles-test bash /root/dotfiles/test.sh @@ -23,7 +23,7 @@ SKIP=0 _ok() { echo -e " ${GREEN}✓${NC} $*"; PASS=$((PASS+1)); } _fail() { echo -e " ${RED}✗${NC} $*" >&2; FAIL=$((FAIL+1)); } -_skip() { echo -e " ${YELLOW}–${NC} $* (skip: $2)"; SKIP=$((SKIP+1)); } +_skip() { echo -e " ${YELLOW}–${NC} $1 (skip: $2)"; SKIP=$((SKIP+1)); } _hdr() { echo -e "\n${BOLD}── $* ──${NC}"; } # ── Helpers ─────────────────────────────────────────────────────────────────── @@ -109,9 +109,12 @@ _hdr "Core tools" check_cmd zsh check_cmd tmux check_cmd git +check_cmd python3 check_cmd fzf check_cmd rg "ripgrep" +check_cmd delta "git-delta" check_cmd zoxide +check_cmd jq # fd is installed as fdfind on Debian/Ubuntu; shim may live in ~/.local/bin if command -v fd &>/dev/null; then @@ -172,15 +175,11 @@ _hdr "git config" check_run "dotfiles git settings applied" \ bash -c 'git config --global diff.zip.textconv | grep -q unzip' -# ── 10. zoxide ──────────────────────────────────────────────────────────────── +# ── 10. zoxide functional ───────────────────────────────────────────────────── _hdr "zoxide" -if command -v zoxide &>/dev/null; then - check_run "zoxide init bash runs" bash -c 'eval "$(zoxide init bash)"' - check_run "zoxide add + query" \ - bash -c 'eval "$(zoxide init bash)"; zoxide add /tmp; zoxide query tmp | grep -q tmp' -else - _fail "zoxide not installed" -fi +check_run "zoxide init bash runs" bash -c 'eval "$(zoxide init bash)"' +check_run "zoxide add + query" \ + bash -c 'eval "$(zoxide init bash)"; zoxide add /tmp; zoxide query tmp | grep -q tmp' # ── 11. Profile-specific: minimal ───────────────────────────────────────────── if [ "$PROFILE" = "minimal" ] || [ "$PROFILE" = "workstation" ]; then @@ -188,13 +187,14 @@ if [ "$PROFILE" = "minimal" ] || [ "$PROFILE" = "workstation" ]; then check_cmd ranger check_cmd tig check_cmd parallel + check_cmd eza + check_cmd shellcheck fi # ── 12. Profile-specific: workstation ───────────────────────────────────────── if [ "$PROFILE" = "workstation" ]; then _hdr "Workstation tools" check_cmd nvim - check_cmd delta check_cmd uv check_cmd cheat @@ -204,6 +204,10 @@ if [ "$PROFILE" = "workstation" ]; then check_link ~/.config/ranger/rc.conf check_link ~/.config/ranger/rifle.conf check_link ~/.config/ranger/scope.sh + + _hdr "Workstation tmux plugins" + check_dir ~/.tmux/plugins/tmux-fzf "tmux-fzf" + check_dir ~/.tmux/plugins/tmux-cpu "tmux-cpu" fi # ── 13. Profile-specific: nosudo ────────────────────────────────────────────── @@ -217,11 +221,11 @@ if [ "$PROFILE" = "nosudo" ]; then local p="$HOME/.local/bin/$cmd" if [ -x "$p" ]; then _ok "$label → $p ($("$p" --version 2>&1 | head -1))" - elif command -v "$cmd" &>/dev/null; then - # Acceptable: binary on PATH even if not in ~/.local/bin - _ok "$label → $(command -v "$cmd") ($(${cmd} --version 2>&1 | head -1))" else - _fail "$label not found (expected in ~/.local/bin)" + # Strict: must be in ~/.local/bin — not just anywhere on PATH. + # For nosudo-forced this verifies NOSUDO=1 was respected (sudo was + # available but binaries still landed in ~/.local/bin, not /usr/local/bin). + _fail "$label not found in ~/.local/bin (got: $(command -v "$cmd" 2>/dev/null || echo 'missing'))" fi } @@ -233,11 +237,15 @@ if [ "$PROFILE" = "nosudo" ]; then check_local_bin delta "git-delta" check_local_bin eza "eza" - _hdr "No-sudo: sudo NOT available" + _hdr "No-sudo: sudo availability" + # nosudo-auto: sudo binary absent → detect_sudo() auto-detected CAN_SUDO=false + # nosudo-forced: sudo binary present but NOSUDO=1 overrides it → install still + # uses ~/.local/bin; sudo availability itself is not the invariant. + # The real invariant (NOSUDO respected) is already verified by check_local_bin above. if sudo -v 2>/dev/null; then - _fail "sudo is available — no-sudo test is invalid" + _ok "sudo available — NOSUDO=1 override mode (binaries forced to ~/.local/bin)" else - _ok "sudo not available (correct)" + _ok "sudo not available — auto-detect mode (CAN_SUDO=false)" fi _hdr "No-sudo: functional smoke tests" @@ -245,7 +253,7 @@ if [ "$PROFILE" = "nosudo" ]; then check_run "ripgrep can search" bash -c 'echo hello | rg hello' fi if command -v fd &>/dev/null; then - check_run "fd can find files" bash -c 'fd --version' + check_run "fd can search files" bash -c 'fd . /tmp --max-depth 1 | grep -q .' fi if command -v jq &>/dev/null; then check_run "jq can parse JSON" bash -c 'echo "{\"x\":1}" | jq .x | grep -q 1' @@ -257,6 +265,12 @@ if [ "$PROFILE" = "nosudo" ]; then if command -v zoxide &>/dev/null; then check_run "zoxide init" bash -c 'eval "$(zoxide init bash)"' fi + if command -v eza &>/dev/null; then + check_run "eza can list files" bash -c 'eza /tmp | grep -q .' + fi + if command -v delta &>/dev/null; then + check_run "delta --version runs" delta --version + fi fi # ── Summary ─────────────────────────────────────────────────────────────────── diff --git a/tmux/.tmux.conf.local b/tmux/.tmux.conf.local index 32168f9..32067cc 100644 --- a/tmux/.tmux.conf.local +++ b/tmux/.tmux.conf.local @@ -194,10 +194,10 @@ tmux_conf_theme_window_status_last_attr="none" #tmux_conf_theme_left_separator_sub="|" #tmux_conf_theme_right_separator_main="" #tmux_conf_theme_right_separator_sub="|" -tmux_conf_theme_left_separator_main='\uE0B0' # /!\ you don't need to install Powerline -tmux_conf_theme_left_separator_sub='\uE0B1' # you only need fonts patched with -tmux_conf_theme_right_separator_main='\uE0B2' # Powerline symbols or the standalone -tmux_conf_theme_right_separator_sub='\uE0B3' # PowerlineSymbols.otf font, see README.md +tmux_conf_theme_left_separator_main="" +tmux_conf_theme_left_separator_sub="|" +tmux_conf_theme_right_separator_main="" +tmux_conf_theme_right_separator_sub="|" # status left/right content: # - separate main sections with "|" @@ -226,8 +226,8 @@ tmux_conf_theme_right_separator_sub='\uE0B3' # PowerlineSymbols.otf font, see # - #{uptime_s} # - #{username} # - #{username_ssh} -tmux_conf_theme_status_left=" ❐ #S | ↑#{?uptime_y, #{uptime_y}y,}#{?uptime_d, #{uptime_d}d,}#{?uptime_h, #{uptime_h}h,}#{?uptime_m, #{uptime_m}m,} " -tmux_conf_theme_status_right=" #{prefix}#{pairing}#{synchronized}#{?battery_status,#{battery_status},}#{?battery_bar, #{battery_bar},}#{?battery_percentage, #{battery_percentage},} , %R , %d %b | #{loadavg} #(~/.tmux/plugins/tmux-cpu/scripts/cpu_percentage.sh) | #{username}#{root} | #{hostname} " +tmux_conf_theme_status_left=" #S " +tmux_conf_theme_status_right=" #{prefix}#{synchronized} %R | #{loadavg} | #{username}#{root}@#{hostname} " # status left style tmux_conf_theme_status_left_fg="$tmux_conf_theme_colour_6,$tmux_conf_theme_colour_7,$tmux_conf_theme_colour_8" diff --git a/update.sh b/update.sh index 7b267e9..6fa98fc 100755 --- a/update.sh +++ b/update.sh @@ -115,8 +115,10 @@ _do_update_uv() { local tmp; tmp=$(mktemp -d) # shellcheck disable=SC2064 trap "rm -rf '$tmp'" RETURN - if has curl; then curl -sfL "$url" | tar -xz -C "$tmp" - else wget -qO- "$url" | tar -xz -C "$tmp"; fi + if has curl; then curl -sfL "$url" | tar -xz -C "$tmp" \ + || { log_warn "uv: download/extraction failed — skipping"; return; } + else wget -qO- "$url" | tar -xz -C "$tmp" \ + || { log_warn "uv: download/extraction failed — skipping"; return; }; fi for bin in uv uvx; do local found; found=$(find "$tmp" -name "$bin" -type f | head -1) [ -n "$found" ] && mv "$found" ~/.local/bin/"$bin" && chmod +x ~/.local/bin/"$bin" @@ -167,8 +169,10 @@ _do_update_neovim() { local tmp; tmp=$(mktemp -d) # shellcheck disable=SC2064 trap "rm -rf '$tmp'" RETURN - if has curl; then curl -sfL "$nvim_url" | tar -xz -C "$tmp" - else wget -qO- "$nvim_url" | tar -xz -C "$tmp"; fi + if has curl; then curl -sfL "$nvim_url" | tar -xz -C "$tmp" \ + || { log_warn "neovim: download/extraction failed — skipping"; return; } + else wget -qO- "$nvim_url" | tar -xz -C "$tmp" \ + || { log_warn "neovim: download/extraction failed — skipping"; return; }; fi local extracted; extracted=$(find "$tmp" -maxdepth 1 -type d -name 'nvim-*' | head -1) if [ -z "$extracted" ]; then log_warn "neovim: unexpected archive layout — skipping"; return; fi local nvim_dest @@ -183,6 +187,42 @@ _do_update_neovim() { _verify_dest nvim "$nvim_dest" } +# xcape is source-built (no versioned releases) — tmpdir must live in a +# function so `trap ... RETURN` fires correctly on function exit. +_do_update_xcape() { + if has xcape; then + if $CHECK_ONLY; then + _check_git_updates "xcape" "" 2>/dev/null || true + log_info "xcape: installed at $(command -v xcape) — source-built from alols/xcape (no version tags)" + if $CAN_SUDO; then + log_info " → run './update.sh xcape' to rebuild from latest source" + else + log_warn " → sudo required to reinstall xcape" + fi + else + if ! $CAN_SUDO; then + log_warn "xcape: sudo required to install build deps and binary — skipping" + else + log_info "xcape: rebuilding from source (alols/xcape)" + apt_install libxtst-dev libx11-dev pkg-config make gcc + local _xc_tmp; _xc_tmp=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf '$_xc_tmp'" RETURN + if git clone --depth=1 https://github.com/alols/xcape.git "$_xc_tmp/xcape" 2>/dev/null \ + && make -C "$_xc_tmp/xcape" 2>/dev/null; then + $SUDO install -m 755 "$_xc_tmp/xcape/xcape" /usr/local/bin/xcape + log_ok "xcape rebuilt → /usr/local/bin/xcape" + else + log_warn "xcape: build failed — skipping" + fi + fi + fi + else + log_warn "xcape not installed — run install.sh workstation first" + log_warn " Requires: sudo apt-get install -y libxtst-dev libx11-dev pkg-config make gcc" + fi +} + # ── PATH shadow check ───────────────────────────────────────────────────────── # Detect binaries at higher-priority PATH locations that shadow dotfiles-managed # versions at /usr/local/bin. Runs always (read-only, no side effects). @@ -291,9 +331,13 @@ if _should_run omz; then if $CHECK_ONLY; then _check_git_updates "oh-my-zsh" ~/.oh-my-zsh else - zsh -c 'source ~/.oh-my-zsh/oh-my-zsh.sh; omz update --unattended' 2>/dev/null \ - || git -C ~/.oh-my-zsh pull --quiet - log_ok "oh-my-zsh updated" + if zsh -c 'source ~/.oh-my-zsh/oh-my-zsh.sh; omz update --unattended' 2>/dev/null; then + log_ok "oh-my-zsh updated" + elif git -C ~/.oh-my-zsh pull --quiet; then + log_ok "oh-my-zsh updated (via git pull)" + else + log_warn "oh-my-zsh: update failed (both omz and git pull) — skipping" + fi fi else log_warn "oh-my-zsh not installed — skipping" @@ -455,13 +499,19 @@ if _should_run cheat; then if [ -z "$cheat_arch" ]; then log_warn "cheat: unsupported arch $ARCH — skipping" else - url=$(_gh_latest_release "cheat/cheat" "linux-${cheat_arch}\"") || url="" + url=$(_gh_latest_release "cheat/cheat" "linux-${cheat_arch}.gz") || url="" if [ -n "$url" ]; then - if has curl; then curl -sfL "$url" | gunzip > ~/.local/bin/cheat - else wget -qO- "$url" | gunzip > ~/.local/bin/cheat; fi - chmod +x ~/.local/bin/cheat - log_ok "cheat updated" - _verify_dest cheat ~/.local/bin/cheat + _cheat_ok=true + if has curl; then curl -sfL "$url" | gunzip > ~/.local/bin/cheat || _cheat_ok=false + else wget -qO- "$url" | gunzip > ~/.local/bin/cheat || _cheat_ok=false; fi + if $_cheat_ok; then + chmod +x ~/.local/bin/cheat + log_ok "cheat updated" + _verify_dest cheat ~/.local/bin/cheat + else + log_warn "cheat: download/decompression failed — skipping" + rm -f ~/.local/bin/cheat + fi else log_warn "cheat: could not fetch release URL — skipping" fi @@ -477,37 +527,7 @@ fi # Deps: libxtst-dev libx11-dev pkg-config make gcc if _should_run xcape; then log_step "xcape" - if has xcape; then - if $CHECK_ONLY; then - _check_git_updates "xcape" "" 2>/dev/null || true - log_info "xcape: installed at $(command -v xcape) — source-built from alols/xcape (no version tags)" - if $CAN_SUDO; then - log_info " → run './update.sh xcape' to rebuild from latest source" - else - log_warn " → sudo required to reinstall xcape" - fi - else - if ! $CAN_SUDO; then - log_warn "xcape: sudo required to install build deps and binary — skipping" - else - log_info "xcape: rebuilding from source (alols/xcape)" - apt_install libxtst-dev libx11-dev pkg-config make gcc - local _xc_tmp; _xc_tmp=$(mktemp -d) - # shellcheck disable=SC2064 - trap "rm -rf '$_xc_tmp'" RETURN - if git clone --depth=1 https://github.com/alols/xcape.git "$_xc_tmp/xcape" 2>/dev/null \ - && make -C "$_xc_tmp/xcape" 2>/dev/null; then - $SUDO install -m 755 "$_xc_tmp/xcape/xcape" /usr/local/bin/xcape - log_ok "xcape rebuilt → /usr/local/bin/xcape" - else - log_warn "xcape: build failed — skipping" - fi - fi - fi - else - log_warn "xcape not installed — run install.sh workstation first" - log_warn " Requires: sudo apt-get install -y libxtst-dev libx11-dev pkg-config make gcc" - fi + _do_update_xcape fi # ── pre-commit common repo ───────────────────────────────────────────────────── diff --git a/zsh/.zshrc b/zsh/.zshrc index fe94560..afd6c7b 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -50,6 +50,7 @@ source "$ZSH/oh-my-zsh.sh" # ── Environment ─────────────────────────────────────────────────────────────── export LANG=en_US.UTF-8 +export LC_ALL=en_US.UTF-8 export EDITOR=nvim export VISUAL=nvim @@ -121,3 +122,13 @@ fgl() { # ── Local overrides (machine-specific, not tracked) ─────────────────────────── [[ -f ~/.zshrc.local ]] && source ~/.zshrc.local + +# bun completions +[ -s "/home/devuser/.bun/_bun" ] && source "/home/devuser/.bun/_bun" + +# bun +export BUN_INSTALL="$HOME/.bun" +export PATH="$BUN_INSTALL/bin:$PATH" + +# Generated for envman. Do not edit. +[ -s "$HOME/.config/envman/load.sh" ] && source "$HOME/.config/envman/load.sh"