1234 字
6 分钟
pnpm 环境下禁用 npm,用 npma 转发到真实 npm

背景#

日常开发里 npmpnpm 混用很容易出乱子:

  • 同一个全局包被两个包管理器各装一遍,命令 shim 互相覆盖、卸载时误删对方的入口;
  • 新装包时手一滑用了 npm,结果和 pnpm 管理的依赖布局不一致。

所以思路是:统一用 pnpm,把 npm 命令禁掉。但 pnpm 卸载/更新不了 npm 自己装的全局包——如果一刀切禁掉 npm,npm 原有的全局包就再也管不了了。

解法分两层:

  1. ps1 脚本(交互终端):npm 命令被拦截(提示用 pnpm),同时定义 npma 作为真实 npm 的转发出口,专门管理 npm 自己安装的全局包。
  2. AGENTS.md 说明(agent 环境):opencode 的 shell 用 -NoProfile 启动、不加载 profile,所以要在 AGENTS.md 里给 agent 写明同样的规则。

一、ps1 脚本部分#

把下面这段加到 PowerShell profile($PROFILE,即 Microsoft.PowerShell_profile.ps1):

Terminal window
# 统一 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' } 过滤,否则会死循环。

使用效果#

Terminal window
npm install -g foo
pnpm add -g foo
npma uninstall -g hexo

坑:命令名 ≠ 包名#

卸载时报”找不到这个包”,多半是把命令名当成了包名

命令名实际包名(npm ls -g 里显示的)
mimo@mimo-ai/cli
gemini@google/gemini-cli
hexohexo-cli
vercelvercel(还带个 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/
作者
Tokisaki Galaxy
发布于
2026-08-17
许可协议
CC BY