Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.1] - 2026-05-03

### Fixed
- `_install_fzf`: skipped symlink reconciliation when `~/.fzf` already
existed, so re-runs over partial state left `~/.local/bin/fzf` missing
and an older system fzf could win PATH lookup — breaking modern
`~/.fzf.zsh` integration with "unknown option: --zsh" on every shell
startup. Now always reconciles the symlink.
- `update.sh` fzf section: same self-heal — reconciles the symlink after
every update, fixing existing broken installs from older dotfiles
versions without requiring a full re-install.

### Added
- `test.sh`: regression guard verifying `~/.local/bin/fzf` symlink target —
fails CI if the symlink reconciliation is ever removed from the installer.

## [1.4.0] - 2026-04-13

### Added
Expand Down
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,23 @@ warns and bails rather than creating a link inside it.
## fzf integration

fzf is installed via **git clone** to `~/.fzf/` (not apt, not a binary release).
The installer generates `~/.fzf.zsh` which adds `~/.fzf/bin` to PATH and registers
`Ctrl+T`/`Ctrl+R`/`Alt+C` bindings.

`~/.fzf.zsh` is sourced **explicitly** in `.zshrc`:
The installer generates `~/.fzf.zsh` which contains `source <(fzf --zsh)` (modern
fzf integration, fzf ≥ 0.48) and is sourced **explicitly** in `.zshrc`:
```bash
[[ -f ~/.fzf.zsh ]] && source ~/.fzf.zsh
```
The oh-my-zsh `fzf` plugin was removed — this explicit source line is the **only**
thing that activates fzf shell integration. Do not remove it. Do not re-add the
oh-my-zsh `fzf` plugin without removing this line first.

**Load-bearing invariant**: `~/.local/bin/fzf` must symlink to `~/.fzf/bin/fzf`.
Because `~/.fzf.zsh` calls `fzf --zsh` at shell startup, an older system fzf at
`/usr/local/bin/fzf` or `/usr/bin/fzf` shadowing the managed install will print
`unknown option: --zsh` on every login. `_install_fzf` in `modules/base.sh` and
the fzf section of `update.sh` both **always reconcile this symlink** (idempotent
`ln -sf` after the existence check), even when the clone is already present —
so re-runs over partial state self-heal. `test.sh` carries a regression guard.

---

## Global flags
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.0
1.4.1
25 changes: 13 additions & 12 deletions modules/base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -408,20 +408,21 @@ _install_jq() {
}

# fzf: install via git clone so shell integration (~/.fzf.zsh) is generated
# automatically by the installer. This matches how update.sh manages fzf and
# what .zshrc expects (`source ~/.fzf.zsh`).
# A ~/.local/bin/fzf symlink is created so fzf is on PATH without sourcing
# ~/.fzf.zsh (important for install.sh and test.sh which don't source it).
# automatically by the installer. The ~/.local/bin/fzf symlink is what wins
# PATH lookup over any older system fzf at /usr/local/bin or /usr/bin — and
# ~/.fzf.zsh's `source <(fzf --zsh)` (modern fzf integration style) needs the
# new fzf to win, otherwise shell startup errors with "unknown option: --zsh".
# Always reconcile the symlink, even when the clone already exists, so re-runs
# over partial state self-heal.
_install_fzf() {
if [ -d ~/.fzf ]; then
log_ok "fzf already installed — skipping"
return
if [ ! -d ~/.fzf ]; then
log_step "fzf (git clone)"
log_info "fzf: installing latest → ~/.fzf/"
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf --quiet
~/.fzf/install --no-update-rc --key-bindings --completion
else
log_ok "fzf already installed — skipping clone"
fi
log_step "fzf (git clone)"
log_info "fzf: installing latest → ~/.fzf/ (shell integration via ~/.fzf.zsh, binary symlinked to ~/.local/bin/fzf)"
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf --quiet
~/.fzf/install --no-update-rc --key-bindings --completion
mkdir -p ~/.local/bin
ln -sf ~/.fzf/bin/fzf ~/.local/bin/fzf
log_ok "fzf installed → ~/.fzf (symlinked to ~/.local/bin/fzf)"
}
7 changes: 7 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ if [ -f ~/.fzf.zsh ]; then
check_run "~/.fzf.zsh sources without error" zsh -c "source ~/.fzf.zsh"
fi

# Regression guard: ~/.fzf cloned without the ~/.local/bin/fzf symlink lets an
# older system fzf win PATH; modern ~/.fzf.zsh then fails with "unknown option:
# --zsh" on every shell startup.
if [ -d ~/.fzf ] && [ "$(readlink ~/.local/bin/fzf 2>/dev/null)" != "$HOME/.fzf/bin/fzf" ]; then
_fail "~/.local/bin/fzf missing or wrong target (got: $(readlink ~/.local/bin/fzf 2>/dev/null || echo none))"
fi

# ── 5. fzf functional ─────────────────────────────────────────────────────────
_hdr "fzf functional"

Expand Down
4 changes: 4 additions & 0 deletions update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ if _should_run fzf; then
else
log_warn "fzf: git pull failed — skipping"
fi
# Self-heal the symlink in case an older dotfiles version (where
# _install_fzf returned early when ~/.fzf existed) left it missing.
mkdir -p ~/.local/bin
ln -sf ~/.fzf/bin/fzf ~/.local/bin/fzf
fi
elif has fzf; then
log_warn "fzf: not managed as a git clone at ~/.fzf — skipping (update manually)"
Expand Down
Loading