事情很简单:想更新一下全局装的 @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 |
❌ | 只触发警告,无任何实际作用 |
纯冗余 + 纯噪音,无害。 想让它闭嘴,两个方向:
-
从源头改:如果是代理软件的脚本注入的,让它只导出
NO_PROXY,别顺手导出npm_config_no_proxy; -
会话级压掉:在
~/.zshrc顶部加一行unset npm_config_no_proxy,对新开的终端生效,noproxy保留,代理绕过不受影响。
收尾
两个问题都不大,但都挺典型:
-
ENOTEMPTY提醒你,npm 的很多「装不上」其实是上一次操作留下的残留,而不是这一次的问题; -
no-proxy警告提醒你,环境变量到配置项之间有一层命名映射,一个下划线和连字符的差别,就能凭空造出一句「未知配置」。
排查工具其实就那么几样:ls 看现场,env | grep 看变量,grep -r 扫配置文件。够用了。