fix(setup): mirror nvm init into .zshrc so zsh sees node + pnpm (#255)
CI / scan (push) Successful in 3m11s
CI / commits (push) Has been skipped
CI / check (push) Successful in 3m47s
CI / a11y (push) Successful in 4m31s
CI / perf (push) Successful in 7m5s
Docs site / build (push) Successful in 6m47s

## Summary

Fix the dev-VM bootstrap so `node` and `pnpm` are on `PATH` in zsh sessions. On a fresh VM provisioned with `docs/setup/scripts/`, a freshly-cloned checkout fails to run anything (`command not found: pnpm`, and `node` too) because the nvm init never reaches the interactive shell.

## Root cause

Two setup scripts interact badly:

- `20-zsh.sh` patches `~/.bashrc` with an `exec zsh -l` hand-off on interactive shells (chsh is blocked on the corp VM, so this is how zsh becomes the effective shell), and patches `~/.zshrc` with theme/plugins/fzf only.
- `40-node.sh` runs the upstream nvm installer, which appends its init block to **`~/.bashrc`** (its default target).

Because `20-zsh.sh` runs first, the `exec zsh -l` guard sits **above** the nvm block in `~/.bashrc`. On every interactive shell, bash execs into zsh before reaching the nvm block — so it never runs — and `~/.zshrc` has no nvm init at all. Net result: zsh sessions have neither `node` nor `pnpm` on `PATH`, even though nvm + Node + corepack installed correctly.

This is a procedure bug, not a one-off: every VM built from these scripts hits it.

## Fix

`40-node.sh` now mirrors the nvm init block into `~/.zshrc` after enabling corepack — idempotent via a managed marker, same pattern `20-zsh.sh` already uses for its `~/.bashrc` patch:

```sh
# Managed by docs/setup/scripts/40-node.sh — nvm init for zsh.
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
```

`docs/setup/01-dev-debian-vm-setup.md` — the `40-node.sh` row in the bootstrap table now notes the `~/.zshrc` patch and why it is needed.

## What lands

| File | Change |
| --- | --- |
| `docs/setup/scripts/40-node.sh` | Append an idempotent nvm-init block to `~/.zshrc` after corepack enable. |
| `docs/setup/01-dev-debian-vm-setup.md` | Document the `~/.zshrc` nvm patch in the `40-node.sh` table row. |

## Manual remediation for already-provisioned VMs

VMs built before this fix won't be retroactively patched (the scripts are idempotent and skip already-installed nvm). On those, run once:

```sh
cat >> ~/.zshrc <<'EOF'

# nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
exec zsh -l
```

(Or re-run `40-node.sh` once this lands — the marker check makes it safe; it will add the block and skip the rest.)

## Test plan

- [x] `bash -n docs/setup/scripts/40-node.sh` — syntax valid.
- [x] Manual remediation block verified on the affected VM: after appending + `exec zsh -l`, `node --version` (v24) and `pnpm --version` (10.34.1) both resolve.
- [ ] Next fresh VM bootstrap: `node` + `pnpm` resolve in the first zsh session with no manual step.
- [ ] Re-running `40-node.sh` on an already-set-up VM is a no-op on the nvm install and adds the `~/.zshrc` block exactly once (marker idempotency).

## Related

- `docs/setup/01-dev-debian-vm-setup.md` — dev-VM bootstrap procedure.
- `20-zsh.sh` — owns the `.bashrc` → zsh hand-off this fix complements.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #255
This commit was merged in pull request #255.
This commit is contained in:
2026-05-29 18:55:30 +02:00
parent 741fc07d40
commit 645f81a443
2 changed files with 33 additions and 10 deletions
+10 -10
View File
@@ -160,16 +160,16 @@ The `bootstrap.sh` script runs every numbered script (`10-…` → `80-…`) in
What each script does:
| Script | Effect | Idempotent? |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [`10-base-packages.sh`](scripts/10-base-packages.sh) | `apt update` + install the minimal set every other script depends on: `curl wget git ca-certificates gnupg build-essential pkg-config`. | ✅ |
| [`20-zsh.sh`](scripts/20-zsh.sh) | Install zsh + Oh My Zsh + Powerlevel10k + plugins (autosuggestions, syntax-highlighting). Patch `~/.zshrc` (theme + plugins + fzf hook) and `~/.bashrc` (exec zsh on interactive shells — `chsh` is blocked on the corp VM). | ✅ |
| [`30-cli-tools.sh`](scripts/30-cli-tools.sh) | Install `bat eza fd-find ripgrep fzf zoxide ncdu keychain` + `jq yq httpie make tree htop tmux direnv dnsutils unzip`. Aliases for the Debian-renamed binaries (`batcat``bat`, `fdfind``fd`). | ✅ |
| [`40-node.sh`](scripts/40-node.sh) | Install nvm. Install Node from the repo's `.nvmrc` (currently `24`). Enable corepack, pin pnpm to `package.json`'s `packageManager`. | ✅ |
| [`50-docker.sh`](scripts/50-docker.sh) | Install Docker CE + compose plugin from the Docker apt repo. Add the current user to the `docker` group. Enable `docker.service`. | ✅ |
| [`60-tuning.sh`](scripts/60-tuning.sh) | `fs.inotify.max_user_watches=524288` (Vite / Nx watch). Prompt for hostname (only if generic). Add a 4 GB swapfile if none. | ✅ |
| [`70-hardening.sh`](scripts/70-hardening.sh) | Best-effort UFW (allow SSH only, prompts before enabling), `unattended-upgrades` on security channel, `fail2ban` (opt-in — prompts before installing), sshd hardening (no root, no password — prompts before applying). **Probes before applying** — skips what's already done. | ✅ |
| [`80-dotfiles.sh`](scripts/80-dotfiles.sh) | Symlink `docs/setup/dotfiles/aliases.zsh``~/.oh-my-zsh/custom/aliases.zsh`. Generate `~/.gitconfig` from `docs/setup/dotfiles/gitconfig.txt`, prompt for identity (name + email). | ✅ |
| Script | Effect | Idempotent? |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [`10-base-packages.sh`](scripts/10-base-packages.sh) | `apt update` + install the minimal set every other script depends on: `curl wget git ca-certificates gnupg build-essential pkg-config`. | ✅ |
| [`20-zsh.sh`](scripts/20-zsh.sh) | Install zsh + Oh My Zsh + Powerlevel10k + plugins (autosuggestions, syntax-highlighting). Patch `~/.zshrc` (theme + plugins + fzf hook) and `~/.bashrc` (exec zsh on interactive shells — `chsh` is blocked on the corp VM). | ✅ |
| [`30-cli-tools.sh`](scripts/30-cli-tools.sh) | Install `bat eza fd-find ripgrep fzf zoxide ncdu keychain` + `jq yq httpie make tree htop tmux direnv dnsutils unzip`. Aliases for the Debian-renamed binaries (`batcat``bat`, `fdfind``fd`). | ✅ |
| [`40-node.sh`](scripts/40-node.sh) | Install nvm. Install Node from the repo's `.nvmrc` (currently `24`). Enable corepack, pin pnpm to `package.json`'s `packageManager`. Mirror the nvm init into `~/.zshrc` (the installer only patches `~/.bashrc`, which the zsh hand-off execs past — without this, zsh has no node/pnpm on PATH). | ✅ |
| [`50-docker.sh`](scripts/50-docker.sh) | Install Docker CE + compose plugin from the Docker apt repo. Add the current user to the `docker` group. Enable `docker.service`. | ✅ |
| [`60-tuning.sh`](scripts/60-tuning.sh) | `fs.inotify.max_user_watches=524288` (Vite / Nx watch). Prompt for hostname (only if generic). Add a 4 GB swapfile if none. | ✅ |
| [`70-hardening.sh`](scripts/70-hardening.sh) | Best-effort UFW (allow SSH only, prompts before enabling), `unattended-upgrades` on security channel, `fail2ban` (opt-in — prompts before installing), sshd hardening (no root, no password — prompts before applying). **Probes before applying** — skips what's already done. | ✅ |
| [`80-dotfiles.sh`](scripts/80-dotfiles.sh) | Symlink `docs/setup/dotfiles/aliases.zsh``~/.oh-my-zsh/custom/aliases.zsh`. Generate `~/.gitconfig` from `docs/setup/dotfiles/gitconfig.txt`, prompt for identity (name + email). | ✅ |
After bootstrap, reload your shell to pick up the new PATH + zsh:
+23
View File
@@ -53,4 +53,27 @@ if [[ -f "$REPO_ROOT/package.json" ]]; then
popd >/dev/null
fi
# The nvm installer appends its init to ~/.bashrc only. But the
# interactive shell on this VM is zsh: 20-zsh.sh hands off via
# `exec zsh -l` (chsh is blocked on the corp VM). That hand-off execs
# zsh *before* the nvm block in ~/.bashrc runs, and the nvm installer
# leaves ~/.zshrc untouched — so without this, zsh sessions have
# neither node nor pnpm on PATH. Mirror the nvm init into ~/.zshrc
# (idempotent via marker, same pattern as 20-zsh.sh's .bashrc patch).
ZSHRC="$HOME/.zshrc"
[[ -f "$ZSHRC" ]] || touch "$ZSHRC"
NVM_MARKER='# Managed by docs/setup/scripts/40-node.sh — nvm init for zsh.'
if grep -qF "$NVM_MARKER" "$ZSHRC"; then
skip "nvm init already in $ZSHRC."
else
cat >> "$ZSHRC" <<'EOF'
# Managed by docs/setup/scripts/40-node.sh — nvm init for zsh.
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
ok "Patched $ZSHRC with nvm init."
fi
log "Reload your shell (exec zsh -l) so nvm + node + pnpm are on PATH."