1234 字
6 分钟
pnpm 环境下禁用 npm,用 npma 转发到真实 npm
背景
日常开发里 npm 和 pnpm 混用很容易出乱子:
- 同一个全局包被两个包管理器各装一遍,命令 shim 互相覆盖、卸载时误删对方的入口;
- 新装包时手一滑用了
npm,结果和pnpm管理的依赖布局不一致。
所以思路是:统一用 pnpm,把 npm 命令禁掉。但 pnpm 卸载/更新不了 npm 自己装的全局包——如果一刀切禁掉 npm,npm 原有的全局包就再也管不了了。
解法分两层:
- ps1 脚本(交互终端):
npm命令被拦截(提示用 pnpm),同时定义npma作为真实 npm 的转发出口,专门管理 npm 自己安装的全局包。 - AGENTS.md 说明(agent 环境):opencode 的 shell 用
-NoProfile启动、不加载 profile,所以要在 AGENTS.md 里给 agent 写明同样的规则。
一、ps1 脚本部分
把下面这段加到 PowerShell profile($PROFILE,即 Microsoft.PowerShell_profile.ps1):
# 统一 UTF-8 编码,避免中文乱码(控制台代码页可能为 GBK 936)[Console]::OutputEncoding = [System.Text.Encoding]::UTF8[Console]::InputEncoding = [System.Text.Encoding]::UTF8$OutputEncoding = [System.Text.Encoding]::UTF8
# 禁用 npm,统一使用 pnpm(pnpm 不可用时回落到真实 npm,避免堵死包管理)function npm { if (Get-Command pnpm -ErrorAction SilentlyContinue) { Write-Error "npm 已被禁用,请使用 pnpm(管理 npm 自有全局包可用 npma 转发到真实 npm)。" throw "npm disabled: use pnpm instead" } $realNpm = @(Get-Command npm -All -ErrorAction SilentlyContinue | Where-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' } | Select-Object -First 1) if ($realNpm) { Write-Warning "未检测到 pnpm,调用真实 npm: $($realNpm.Source)" & $realNpm.Source @args return } Write-Error "npm 已被禁用,且未检测到 pnpm 与真实 npm" throw "npm disabled and no fallback npm found"}function npma { # npma = 真实 npm 转发:npm 名称已被上面的函数拦截, # 管理 npm 自己安装的全局包(npm ls -g 可见、pnpm 管不到)时用 npma。 $realNpm = @(Get-Command npm -All -ErrorAction SilentlyContinue | Where-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' } | Select-Object -First 1) if ($realNpm) { & $realNpm.Source @args return } Write-Error "npma: 未找到真实 npm" throw "npma: real npm not found"}逻辑说明
npm函数:只要检测到pnpm存在就直接报错拦截;万一机器上没有 pnpm,才回落到真实 npm(避免把包管理彻底堵死)。npma函数:npm这个名字被函数占用了,npma就是”真实 npm”的别名出口。它用Get-Command npm -All找到真正落在磁盘上的 npm(过滤掉 Function 和 Alias),再原样转发参数。- 关键细节:
Get-Command npm -All里会包含上面刚定义的npm函数,必须用Where-Object { $_.CommandType -ne 'Function' -and $_.CommandType -ne 'Alias' }过滤,否则会死循环。
使用效果
npm install -g foopnpm add -g foonpma uninstall -g hexo坑:命令名 ≠ 包名
卸载时报”找不到这个包”,多半是把命令名当成了包名:
| 命令名 | 实际包名(npm ls -g 里显示的) |
|---|---|
mimo | @mimo-ai/cli |
gemini | @google/gemini-cli |
hexo | hexo-cli |
vercel | vercel(还带个 vc 命令) |
判断包名以 npm ls -g / pnpm ls -g 列出来的为准,而不是 Get-Command 看到的命令名。
二、AGENTS.md 说明部分
opencode(或其他 agent)执行命令时用 pwsh -NoProfile -NonInteractive 启动,不会加载你的 PowerShell profile——所以上面的 npm 拦截函数和 npma 在 agent 的 shell 里都不存在:agent 里的 npm 就是真实 npm,且没有 npma。
因此需要在全局 AGENTS.md(~/.config/opencode/AGENTS.md)里给 agent 写同样的规则:
## 包管理工具约定
用户 PowerShell profile 已禁用 `npm` 命令(检测到 pnpm 时拦截),并定义 `npma` = 真实 npm 转发(用于管理 npm 自己安装的全局包)。**agent 的 shell 以 `-NoProfile` 启动,不加载该 profile**——所以 agent 环境里 `npm` 即真实 npm,且不存在 `npma`。据此约束:
- 包管理操作**默认用 `pnpm`**(安装 / 更新 / 卸载全局与项目依赖)。- 需要管理 **npm 自己安装的全局包**(`npm ls -g` 列出、pnpm 管不到的)时,直接用真实 npm:`npm uninstall/update/ls -g ...`(在用户交互终端里对应命令是 `npma`)。- **同一全局包只允许一个包管理器管理**:禁止用 pnpm 卸载/更新 npm 装的包,也禁止用 npm 卸载/更新 pnpm 装的包(同名包混管会互相覆盖/误删 shim)。- 全局目录已统一:npm prefix 与 pnpm global 均为 `Z:\Code\packages\npm`(pnpm store 在 `Z:\.pnpm-store\v11`,corepack 管 pnpm/yarn 版本)。为什么 agent 要单独一份规则
| 环境 | npm 是什么 | npma 是否存在 |
|---|---|---|
| 交互终端(加载 profile) | 被函数拦截,报错禁用 | 存在,转发到真实 npm |
agent shell(-NoProfile) | 真实 npm,无拦截 | 不存在 |
结论:agent 里跑 npm 就是在跑真实 npm——这正好是管理 npm 自有全局包时要的行为。所以 AGENTS.md 里约定”包管理默认用 pnpm,管理 npm 自有包时直接用 npm”即可,语义和交互终端的 npma 完全对应。
附:全局目录统一
这套方案还顺带把全局目录统一了,避免包散落在 C 盘和工具盘:
- npm prefix:
Z:\Code\packages\npm - pnpm global(
global-dir/global-bin-dir):同为Z:\Code\packages\npm - pnpm store:
Z:\.pnpm-store\v11 - pnpm / yarn 版本由 corepack 管理
pnpm 的全局配置写在它自己的 config.yaml(%LOCALAPPDATA%\pnpm\config\config.yaml),不写进 ~/.npmrc,避免 npm 报警告。
pnpm 环境下禁用 npm,用 npma 转发到真实 npm
https://tski.uk/blog/pnpm-disable-npm-npma/