# pnpm 和 Windows 讓這個部落格的建置壞了兩次 — 先是 Chocolatey 安裝被蓋掉，後是符號連結被擋下

> pnpm 的版本切換器，在一個被蓋掉的 Chocolatey 安裝背後弄壞了快取；Windows 另外也擋下了建置時對依賴項目建立符號連結。

- Source: https://oharu121.com/zh-tw/blog/pnpm-version-switcher-enoent-chocolatey-shadowed-path/
- Published: 2026-08-22T12:05:09+09:00
- Updated: 2026-08-23T04:54:40+09:00
- Tags: pnpm, 開發工具

---
**重點摘要**

- pnpm 自己那套自我管理的版本切換器，由 `package.json` 裡的 `packageManager` 欄位觸發，可能在下載釘選版本的途中失敗，讓自己對任何指令都完全無法執行。
- 這不是偶發事件，也不是安裝過期的問題。多個公開的 pnpm issue，從彼此毫無關聯的環境裡回報了完全相同的 ENOENT 失敗：Docker 映像檔、CI 執行器、別人的機器。
- 重新安裝 pnpm 能修好損壞的快取，但如果同一個工具還有第二個被遺忘、排在某個 shell 的 PATH 更前面的安裝，這次修正看起來就會不完整。
- `which pnpm` 其實在最初的診斷階段就已經揭露了那第二個安裝。只是它不是看起來壞掉的那一行，所以被讀過去了。
- 把能正常運作的安裝在 `PATH` 裡排到前面，修好的只有執行那個指令的 shell，再往後就沒有作用了。隔天在一個新的 shell 裡，同樣的錯誤又出現了一次，原因是 Chocolatey 的安裝其實還在，只是被排到後面，並沒有被移除。
- 後來另一次建置因為 `EPERM` 錯誤在建立符號連結時失敗。原因是 Windows 對標準帳戶預設會擋下建立符號連結，開啟開發人員模式即可修正，不需要提升權限的 shell。

## 引言

在為這個儲存庫裡另一篇文章正規化一張新的縮圖時，`pnpm thumbnails:fix` 徹底壞掉了：不是一般的指令失敗，而是 pnpm 在任何 shell 裡、對任何指令都完全拒絕執行。這連帶擋住了後續的一切，包括這個部落格自己的流程在把草稿視為已檢查之前要求的驗證關卡。第一個直覺的猜測是安裝過期或損壞，而這個猜測錯得很有啟發性：**真正的原因是 pnpm 自己那套自我管理的版本切換機制，沒能完成自身的安裝**，再加上某個 shell 的 PATH 裡，還有一個被遺忘、排在剛修好的那份之前的第二個 pnpm 安裝。本文說明那次自我切換失敗長什麼樣子、它是一個已知上游臭蟲而非這台機器特有問題的證據、被蓋掉的安裝如何把原本一行就能解決的修正變成一場多步驟的診斷、那次修正為什麼沒能撐過下一個工作階段，以及後來在同一台機器上、公開另一篇文章時遇到的第二個、與此無關的 Windows 失敗：一次建置被擋下、無法為自己的依賴項目建立符號連結。

## 切換器自己壞掉了

`pnpm thumbnails:fix` 在背後的指令碼還沒執行到任何一行之前就失敗了：

```
[WARN] Unsupported engine: wanted: {"node":">=24"} (current: {"node":"v22.12.0","pnpm":"11.3.0"})
 ERROR  Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrect
spawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENT
```

這個儲存庫的 `package.json` 釘住了 `"packageManager": "pnpm@11.3.0"`。近代版本的 pnpm 會自己讀取這個欄位，如果指定的版本還沒放在使用者自己的工具快取裡，就會就地下載並安裝，再重新執行進那個版本。**這個快取放在 `PNPM_HOME/.tools/@pnpm+win-x64/<version>/`，而且這是 pnpm 在管理自己的版本，不是像 Corepack 那樣由另一個工具在做這件事。**

`11.3.0` 的快取資料夾本身確實存在，只是沒有包含 `bin` 子資料夾：

```
total 15
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 .
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 ..
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 node_modules
-rw-r--r-- 1 <user> 197609  43 Aug 22 10:11 package.json
-rw-r--r-- 1 <user> 197609 438 Aug 22 10:11 pnpm-lock.yaml
-rw-r--r-- 1 <user> 197609  39 Aug 22 10:11 pnpm-workspace.yaml
```

**自我安裝在放上執行檔之前會寫入的每一樣東西都在，唯獨讓這次安裝真正可用的那一個檔案不在。** 重新執行 `pnpm --version` 重現了完全相同的錯誤，這排除了一次性網路問題的可能：這個自我管理的切換器，把自己的快取留在一個它自己也無法恢復的狀態，而之後不管是哪一個實體 pnpm 執行檔觸發呼叫，每一次都撞上同一個缺少的路徑。

## 既不是巧合，也不是這台機器的問題

當下的直覺是懷疑安裝本身出了問題。但在照著這個直覺採取行動之前先簡單搜尋了一下，診斷就改變了：**這個一字不差的錯誤，出現在彼此毫無共通之處的各種環境裡。**Renovate 的 Docker 映像檔、Render.com 的部署，以及幾個各自獨立的專案，全都回報了完全相同的 `Failed to switch pnpm to v<X>. Looks like pnpm CLI is missing... ENOENT` 訊息，橫跨好幾個小版本、被記錄在 pnpm 公開的 issue 裡。

**脆弱的是 pnpm 自己那套自我管理的版本切換器，而不是任何特定的 pnpm 安裝方式。**一個 Docker 映像檔、一個 CI 執行器，和一台個人的 Windows 機器，安裝方式、作業系統、歷史背景沒有一項相同，卻撞上了同樣措辭的同樣失敗，原因是它們唯一共有、也是真正重要的那一件事：切換器自己下載並解壓縮一個釘選版本的那套邏輯。

## 修正，以及沒清掉的殘影

當代理程式開始嘗試用環境變數的權宜做法，在沒有先說明的情況下試圖繞過這個壞掉的狀態時，我讓它停下來，問了為什麼。這個提問改變了修正的方向：與其繞過壞掉的 pnpm，我要求代理程式好好重新安裝 pnpm，並核准直接執行官方安裝程式：

```powershell
iwr https://get.pnpm.io/install.ps1 -useb | iex
```

這產生了一個全新的 shim 和乾淨的 `PNPM_HOME`。在一個已經載入更新後 `PATH` 的 PowerShell 工作階段裡，`pnpm --version` 乾淨地印出了 `11.3.0`。在那個 shell 裡，修正是有效的。

接著在另一個 Bash 工作階段執行的 `pnpm install`，卻出現**一模一樣**的 ENOENT 錯誤，參照的是完全相同的損壞快取路徑，彷彿什麼都沒重新安裝過一樣。這時候的反射動作，是懷疑重新安裝沒有生效，或是快取第二次自己壞掉了。兩者都不是：

```bash
which pnpm
# /c/ProgramData/chocolatey/bin/pnpm
```

Bash 執行的並不是剛剛修好的那個 pnpm。它執行的是**一個完全不同、透過 Chocolatey 安裝的舊版本**，在 Bash 的 `PATH` 上排在新版本之前，而且舊到仍然會被同樣壞掉的切換器行為影響。**這個證據其實早就出現過一次，就在最初的診斷階段**，出現在和原始錯誤一起執行的 `which pnpm` 檢查裡。當時它被讀過去卻沒被注意到，因為又大聲又醒目的 ENOENT 區塊是理所當然該回應的對象，旁邊那條不起眼的檔案路徑則不是。

*Figure — PathResolution: 兩個 shell 搜尋的都是 PATH 上同樣的兩個資料夾。差別只在順序，而這個順序決定了每一個 shell 實際執行的是哪一個 pnpm 執行檔。*

**一旦看清楚原因，修正就只有一行**：把正確安裝的 `bin` 資料夾，在那個 shell 的 `PATH` 裡排到 Chocolatey 的前面。

```bash
export PATH="/c/Users/<user>/AppData/Local/pnpm/bin:$PATH"
which pnpm
# /c/Users/<user>/AppData/Local/pnpm/bin/pnpm
```

## 驗證

解析到正確的執行檔之後，`pnpm install` 順利完成，在這台機器上第一次填滿了 `node_modules`，`pnpm thumbnails:fix` 也真正執行了正規化指令碼，而不是在開始前就失敗。**兩個 shell 裡的 `pnpm --version` 現在一致回報 `11.3.0`，而且每次都是來自同一個安裝。**

## 另一個無關的 Windows 失敗：建置被擋下、無法為自身依賴項目建立符號連結

後來在同一台機器上，發布另一篇無關的文章時，浮現了一個獨立的失敗：`pnpm build` 的 Vercel 轉接器在伺服器套件本身已經成功建置完之後，中途當機了：

```
EPERM: operation not permitted, symlink '.pnpm\sharp@0.35.3_@types+node@24.13.3\node_modules\sharp' -> 'C:\repositories\personal\oharu-tech-blog\.vercel\output\functions\_render.func\node_modules\sharp'
```

這個轉接器是透過把依賴項目從 pnpm 的內容定址儲存區建立符號連結出來，藉此打包一個無伺服器函式的依賴項目，而建立符號連結正是 Windows 對標準帳戶預設會擋下的那個操作。**修正的方法是開發人員模式，不是提升權限的 shell**：設定 → 隱私權與安全性 → 開發人員專用 → 開發人員模式，會授予一個非管理員工作階段原本沒有的 `SeCreateSymbolicLinkPrivilege`，而在那裡把它打開（完全不必以系統管理員身分執行任何東西）就足夠了。它所設定的登錄檔機碼 `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\AllowDevelopmentWithoutDevLicense`，值得在認定符號連結失敗需要完整提升權限的工作階段之前先確認一下：不存在或為 `0`，就代表開發人員模式是關閉的，光是這一點就重現了這次建置失敗。

同一次建置在當機前，還印出了第二個容易被混為一談的警告：

```
The local Node.js version (26) is not supported by Vercel Serverless Functions.
Your project will use Node.js 24 as the runtime instead.
Consider switching your local version to 24.
```

Node 26 是這台機器上的 *Current* 發行線，不是 Vercel 的函式實際執行所在的 LTS 發行線。**這個警告並沒有讓建置停下來；緊接在後的 EPERM 錯誤才讓它停下來**，這讓人很容易把兩者讀成同一個失敗，但其實它們毫不相干。Node 版本不符是一個 Astro 的 Vercel 轉接器會自動降級繞過的執行環境支援警告；符號連結錯誤則是一個 Windows 權限上的落差，跟目前執行的是哪一個 Node 版本完全無關。

## 那次修正沒能撐住

**先前修好被蓋掉安裝的那個 `export`，修好的只是執行它的那個 shell，再往後就沒有作用。** shell 範圍內的環境變數不會延續到新的終端機，所以隔天，一個全新的工作階段又撞上了完全相同的錯誤：

```
ERROR  Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrect
spawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENT
```

代理程式把先前的診斷從頭做了一遍：刪掉損壞的 `.tools\@pnpm+win-x64\11.3.0` 資料夾再重試，藉此確認這究竟是一次新的下載中斷，還是同一個決定論式的失敗。結果是同一個失敗。在一個沒有覆寫設定的 shell 裡，`where pnpm` 再次把 Chocolatey 的執行檔列在最前面，而直接用完整路徑執行 AppData 底下的安裝，則乾淨地切換成了 `11.3.0`。**能正常運作的那份安裝從來沒有壞過。它只是一直被排到後面，而先前的修正也只改了其中一個 shell 的排序。**

這一次的修正把目標對準了那個互相競爭的安裝本身：`choco uninstall pnpm -y`。第一次嘗試同樣失敗，出現了 `UnauthorizedAccessException: Access to the path 'C:\ProgramData\chocolatey\bin\pnpm.exe' is denied`。Chocolatey 的檔案放在 `C:\ProgramData` 底下，要移除它們需要一個提升權限的 shell，而這是代理程式自己開不了的。我自己從以系統管理員身分開啟的 PowerShell 執行了這次解除安裝。Chocolatey 的套件移除之後，`where pnpm` 現在在每一個 shell 裡都只會回傳一條路徑，不再需要記得帶著 `export` 走。

## 總結

**「是一個過期的 Chocolatey 安裝搞的鬼」這個說法是錯的，而且值得把這個錯誤大聲說出來。** 真正壞掉的，是 pnpm 自己那套自我管理的版本切換器，沒能完成一個釘選版本的安裝，這是一個橫跨互不相干的各種環境、以完全相同錯誤文字被記錄下來的臭蟲。讓這次修正花了兩輪而不是一輪的原因，是某個 shell 的 `PATH` 上排得更前面的另一個舊版 pnpm，而這個證據其實早在第一個診斷指令就已經浮現，只是沒被讀到。重新安裝 pnpm 修好了切換器。如果在相信 shell 回報的版本號之前先確認一下 `which pnpm`，本來在第一輪就能抓到那個被蓋掉的安裝，而不必等到第二輪。**`PATH` 的重新排序只作用在執行它的那個 shell，所以它從一開始就不可能是持久的修正。**真正持久的修正，是在一個提升權限的 shell 裡，把蓋掉它的那個安裝整個移除。

## 參考連結

- [pnpm#9183: Failed to switch pnpm to vXX, ENOENT after a partial self-install](https://github.com/pnpm/pnpm/issues/9183)
- [pnpm#9046: Failed to switch pnpm to vv10.2.0](https://github.com/pnpm/pnpm/issues/9046)
- [pnpm#9192: Looks like pnpm CLI is missing at \\pnpm\\.tools\\](https://github.com/pnpm/pnpm/issues/9192)
- [pnpm#9715: Automatic download of pnpm version breaks when a home directory is a symlink](https://github.com/pnpm/pnpm/issues/9715)
