文章

一次 npm 全局重装踩坑:ENOTEMPTY 与那句阴魂不散的 no-proxy 警告

卸载再全局重装一个 npm 包,先撞上 ENOTEMPTY: directory not empty,又顺手扒了扒那句 Unknown env config "no-proxy" 到底从哪冒出来的。两个小问题,一起记一下。

目录
阅读字号18

事情很简单:想更新一下全局装的 @openai/codex,于是先卸载再装。

npm uninstall @openai/codex
npm install -g @openai/codex

结果第二条命令直接翻车:

npm error code ENOTEMPTY
npm error syscall rename
npm error path /opt/homebrew/lib/node_modules/@openai/codex
npm error dest /opt/homebrew/lib/node_modules/@openai/.codex-QDftHxCm
npm error errno -66
npm error ENOTEMPTY: directory not empty, rename
  '/opt/homebrew/lib/node_modules/@openai/codex' ->
  '/opt/homebrew/lib/node_modules/@openai/.codex-QDftHxCm'

一个 ENOTEMPTY,一句 Unknown env config "no-proxy" 的警告。两个问题正好凑一篇。

一、ENOTEMPTY:npm 的「先改名再删除」被残留卡住了

先看现场:

$ ls -la /opt/homebrew/lib/node_modules/@openai/
drwxr-xr-x  .codex-QDftHxCm   # 6 月 30 日的
drwxr-xr-x  codex             # 当前这次

关键就在那个隐藏的 .codex-QDftHxCm。

npm 在覆盖 / 清理一个已存在的包时,用的不是「直接删掉再写」,而是一套更保守的动作:先把旧目录 rename 成一个隐藏的临时名(.codex-xxxxxxxx),确认新内容就位后,再回头删掉这个临时目录。 这样即使中途失败,也不至于把你原来的包删得只剩一半。

问题是,上一次操作里那步「回头删掉临时目录」没走完,.codex-QDftHxCm 就这么留在原地了。等到这次重装,npm 又想把当前的 codex 目录 rename 成一个新的 .codex-* 临时名 —— 底层 rename(2) 在目标已存在且非空时会返回 ENOTEMPTY(errno 66),于是就报了出来。

说白了:不是这次装坏了,是上次没删干净的残留挡了路。

解决

目录属主就是当前用户(/opt/homebrew 下,无需 sudo),手动清掉残留和当前目录,再重装即可:

rm -rf /opt/homebrew/lib/node_modules/@openai/.codex-QDftHxCm \
       /opt/homebrew/lib/node_modules/@openai/codex
npm install -g @openai/codex
added 7 packages in 12s
$ codex --version
codex-cli 0.143.0

搞定。

小结:以后再遇到 npm error ENOTEMPTY ... rename ... .<pkg>-xxxxxxxx,基本都是同一个套路 —— 去对应的 node_modules 目录里找那个隐藏的 .<包名>-随机串,删掉它再装就行。

二、那句 Unknown env config "no-proxy" 到底从哪来

重装成功的输出里,还夹着一句警告:

npm warn Unknown env config "no-proxy". This will stop working
in the next major version of npm.

它不影响安装,但既然看见了,就顺手查清楚。

先在配置文件里找 —— 结果一无所获

直觉是某个 .npmrc 或 shell 配置写了它。于是把能想到的地方都翻了一遍:

# shell 配置:.zshrc / .zshenv / .zprofile / .bash_profile / .bashrc / .profile
# npm 配置:~/.npmrc / /opt/homebrew/etc/npmrc
# 系统级:launchctl getenv、/etc/profile.d ...

一条都没有。 静态文件里根本搜不到 no-proxy。

但当前 shell 的环境变量里,它明明白白躺着:

$ env | grep -i npm_config
npm_config_noproxy=localhost,127.0.0.1,::1     # 正确的那个
npm_config_no_proxy=localhost,127.0.0.1,::1    # 报警的那个

两个几乎同名、值还一样的变量同时存在。旁边还有整整一套 HTTP_PROXY / HTTPS_PROXY / NO_PROXY,全指向 127.0.0.1 —— 典型的本机代理环境。

症结:一个连字符

npm 会把任何 npm_config_<xxx> 形式的环境变量,自动当成配置项 <xxx> 来读。映射时它把变量名里的下划线转成连字符:

  • npm_config_noproxy → 配置项 noproxy ✅ npm 认识
  • npm_config_no_proxy → 配置项 no-proxy ❌ npm 不认识

npm 只有 noproxy(无连字符)这个配置项,压根没有 no-proxy。所以 npm_config_no_proxy 一进来就被判为「未知配置」,触发那句警告 —— 而且它不产生任何实际效果,真正让代理绕过 localhost 生效的,是旁边那个 noproxy。

那它是谁设的

既然任何磁盘配置文件里都搜不到,结论只能是:它是运行时由父进程注入到会话里的,和那一整套 *_PROXY 变量同一批出现。最常见的来源就是本机代理软件的「终端代理」一键脚本,或某次手动 export 后留在当前会话里的(重开终端往往就没了)。

所以这句警告的定性很清楚:

变量 npm 是否识别 作用
npm_config_noproxy ✅ 真正生效
npm_config_no_proxy ❌ 只触发警告,无任何实际作用

纯冗余 + 纯噪音,无害。 想让它闭嘴,两个方向:

  1. 从源头改:如果是代理软件的脚本注入的,让它只导出 NO_PROXY,别顺手导出 npm_config_no_proxy;
  2. 会话级压掉:在 ~/.zshrc 顶部加一行 unset npm_config_no_proxy,对新开的终端生效,noproxy 保留,代理绕过不受影响。

收尾

两个问题都不大,但都挺典型:

  • ENOTEMPTY 提醒你,npm 的很多「装不上」其实是上一次操作留下的残留,而不是这一次的问题;
  • no-proxy 警告提醒你,环境变量到配置项之间有一层命名映射,一个下划线和连字符的差别,就能凭空造出一句「未知配置」。

排查工具其实就那么几样:ls 看现场,env | grep 看变量,grep -r 扫配置文件。够用了。