mcpField
N
node-version-discipline

node-version-discipline

github.com/fudesign2008开发671 次安装1 天前更新
代码仓库

关于 node-version-discipline

供处理 Node 项目的编码 Agent 使用的技能,要求在安装、构建、检查或测试前查明并切换到项目声明的 Node 版本;未声明时则按流程询问。

主要功能

  1. 01按优先级检查版本文件和 CI 配置
  2. 02运行敏感命令前对齐 Node 版本
  3. 03切换后确认当前版本
  4. 04未声明版本时先询问
  5. 05经用户同意后写入版本声明

适用场景

  1. 01安装依赖前检查 Node 约束
  2. 02避免构建、检查或测试结果失真
  3. 03处理未声明 Node 版本的项目

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/fudesign2008/open-skills --skill node-version-discipline

安装后在 Claude Code 中输入/node-version-discipline确认已加载。

附带文件evals/evals.json

详细介绍

Node 版本规范

在 Node 项目中运行任何构建、lint、类型检查、测试或安装命令之前,请将 Node 版本与项目声明的版本对齐(按完整优先级链探查:.nvmrc → .node-version → .tool-versions → volta.node → engines.node → CI 配置)。否则,验证结果不可信。

1. 为什么必须对齐

主机的默认 Node 版本(例如 v22)经常与项目通过 .nvmrc 固定的版本(例如 v14)不一致。在默认版本下运行 tsc / eslint / npm run build / npm test 会导致:

风险影响
假通过较高版本的 Node 更宽松——tsc/eslint 通过,但在固定版本上实际运行时会崩溃
假失败较高版本会破坏旧版工具链(例如 Webpack 4 原生依赖),导致出现与代码无关的构建错误,误导排查
行为漂移较旧版本缺少某些 API(structuredClone、AbortController 等)——运行时行为会出现差异
依赖解析偏差node_modules 会为当前启用的 Node 版本安装原生模块;版本不匹配会安装错误的变体

核心教训:即使较高版本碰巧通过,且结果恰好一致,.nvmrc 固定的版本仍是事实依据。“在较高版本上通过了”绝不能成为跳过版本对齐的理由。

2. 判定规则

何时对齐

如果项目通过任何方式声明了 Node 版本(参见下方的优先级链),请在运行以下命令之前进行对齐:

  • tsc / npx tsc / npm run type-check
  • eslint / npm run lint
  • npm run build / webpack / 任何构建脚本
  • npm test / jest / 任何测试命令
  • npm install / npm ci(在错误版本下安装依赖会拉取错误的原生模块——必须先完成探查和版本对齐,才能安装)
  • 任何 npm run <script>(脚本内部可能会调用对版本敏感的工具)

如果 npm install / npm ci 输出 Exit handler never called!(或类似的安装损坏信息),即使进程退出代码为 0,也应将安装视为失败:停止使用该 node_modules,重新对齐 Node 版本,清理后重新安装,然后再继续。不要在可疑的依赖树上运行 tsc、构建或测试。

版本声明优先级链

.nvmrc 是 nvm 的约定,并非唯一约定。各 Node 版本管理器读取的文件各不相同,因此请按顺序检查整条链——第一个权威命中项优先:

#来源类型跨工具说明
1.nvmrc精确nvm 和 fnm 原生支持;asdf 需要 legacy_version_file;Volta 会忽略
2.node-version精确fnm / nodenv / nvs 原生支持;asdf 需要 legacy 标志;nvm 会忽略
3.tool-versions精确asdf 原生支持
4package.json → "volta".node精确Volta 原生支持——Volta 唯一会读取的来源
5package.json → engines.node范围仅当其非退化时适用(见下文);除非启用 engine-strict,否则 npm 会发出警告;选择范围内已安装的版本
6CI / Docker 中固定的 Node 版本精确推断值——在报告中标明来源。候选文件:.gitlab-ci.yml、.github/workflows/*、Jenkinsfile、常见的 Dockerfile / Dockerfile.* 名称。识别 image: node:<ver>、node-version:、NODE_VERSION、FROM node:<ver>(镜像 / 版本标记周围可有可选的 YAML 引号)

对于文件(第 1–3 行),请沿目录树向上查找;从项目根目录读取 package.json 和 CI / Docker 配置。第 1 行和第 2 行中这两个跨工具盲区并不明显——参见 §4。

退化的 engines.node: 将无法作为现代项目版本固定值的占位符 / 过时下限视为非权威值——至少包括 >=0.8.0、>0.8.0、>=0.10.0 及类似的过时下限。继续检查优先级链;询问用户时,不要将这些值与真正的 CI 版本固定值或 README 中的轶事并列为候选项。

适用于 Shell 的 CI / Docker 探查: 使用 [ -f path ](或 find / 可安全处理 nullglob 的循环)逐个检查候选路径。在 zsh nomatch 下,绝不要将必需的字面路径和可选 glob 放在同一个可展开的 argv 中(例如 ls .gitlab-ci.yml Dockerfile*——如果缺少 Dockerfile*,检查 .gitlab-ci.yml 之前就会中止)。优先使用固定的小型名称列表(Dockerfile、Dockerfile.web、……),并逐个路径检查;项目特定名称应单独使用 [ -f ] 检查,不要使用有风险的多重 glob ls。

没有版本声明时

如果检查链中没有找到任何版本声明,不要静默回退到默认 Node 版本,也不要擅自替用户选择版本——这正是此技能要避免的误通过/误失败陷阱,而猜测版本可能导致整个验证过程都在错误的 Node 版本上运行。将用户的回答保存为持久的项目资源,这样就不必重复这一轮确认:

  1. 停止并询问用户应使用哪个 Node 版本。提示用户:"此项目没有声明 Node 版本(未找到 .nvmrc / .node-version / .tool-versions / volta / engines.node / CI 版本锁定)。运行 [tsc / build / test / ...] 前,我应对齐到哪个 Node 版本?" 等待用户回答;不要猜测版本后继续。
  2. 使用只读的 detect_manager 代码片段(§2.2)检查工具链,以便推荐适合此项目/环境的版本声明文件。该代码片段只会输出信号,不会写入任何内容。
  3. 在持久化前征得用户同意。 提议写入推荐的版本声明文件,其中包含用户刚刚提供的确切版本(§2.1 用于选择文件类型;版本由用户指定,且必须采用完整的 X.Y.Z 格式,例如 22.5.0,不能只写主版本号)。只有用户同意后才进行持久化;如果用户拒绝,则跳过写入,本次会话仅进行版本对齐。无论用户是否同意写入,都要在运行实际命令前对齐到用户指定的版本(SOP §3)。
  4. 纯文件 I/O / git / grep 操作(不涉及 Node 运行时)无需进行版本对齐,也无需版本声明文件。

为什么要询问,而不是默认使用 Active LTS:项目“正确”的 Node 版本取决于 AI 并不了解的上下文,例如生产环境运行时、CI 矩阵、队友的环境和旧版兼容限制。错误猜测会悄无声息地导致所有下游结果失效。询问只需一次简单的往返确认;而误通过/误失败可能会让整个调查方向跑偏。

为什么要持久化答案(而不只是对齐一次):用户选择的版本是很有价值的工程信息。如果它只留在对话里,那么每个后续会话、CI 运行和协作者都会遇到同一个“没有版本声明”的障碍,还可能最终选择不同的版本。写入一个小型版本声明文件即可形成闭环——下次会话在首次检查时就会读取它,不再询问。

2.1 应写入哪种声明文件

根据 detect_manager(§2.2)报告的信号选择文件类型。优先使用第一个匹配的行;.nvmrc 是兜底默认选项,因为 nvm/fnm(以及启用了 legacy_version_file 的 asdf)都会读取它,因此能覆盖最广泛的工具。

信号(来自 §2.2)要写入的文件内容格式选择此文件的原因
package.json 已有 volta 块,或主机上已安装 voltapackage.json → volta.node 字段"volta": { "node": "22.5.0" }(合并到现有的 volta 对象中)Volta 只读取此来源;Volta 用户看不到 .nvmrc
主机上已安装 asdf,或项目已有供其他语言使用的 .tool-versions.tool-versions(追加一行)nodejs 22.5.0asdf 的原生来源;将所有语言的版本固定在一个文件中
已安装 fnm 或 nodenv(且不符合以上条件).node-version22.5.0fnm/nodenv 的原生来源;nvm 会忽略此文件,因此如果只有 nvm,就不要选择它
以上条件均不满足/不确定.nvmrc22.5.0覆盖范围最广:nvm + fnm 原生支持,asdf 通过 legacy_version_file 支持;安全的默认选项

版本格式 — 写入确切的补丁版本。 使用用户指定的完整 X.Y.Z 版本(如果用户说“使用已安装的版本”,则使用确切的已安装版本 — 通过 node -v 获取并去掉开头的 v)。不要只写主版本号(22),也不要写版本范围(>=20):声明文件的作用是固定一个可复现的版本,确保每个环境都一致。.nvmrc/.node-version 使用裸写的 22.5.0(不加 v 前缀,符合 nvm 约定);.tool-versions 使用 nodejs 22.5.0;engines.node/volta.node 使用带引号的字符串 "22.5.0"。(如果用户之后想为 engines.node 使用版本范围,那是需要另行明确决定的事项 — 此处的持久化步骤会固定确切版本。)

先持久化,再安装。 先写入声明文件,再处理安装。该文件表达了对 CI 和协作者的版本要求 — 不依赖本地机器是否已安装该版本。写入后,如果 ls ~/.nvm/versions/node/ 显示本地缺少该版本,则根据 SOP §3 步骤 2 提示运行 nvm install <version>(或对应版本管理器的命令),然后继续。这样一来,即使用户推迟安装,项目文件也已就位。

写入位置。 项目根目录(与 package.json 同级)。对于 .nvmrc/.node-version/.tool-versions,新建文件;对于 volta.node/engines.node,合并到现有的 package.json 中 — 绝不要覆盖文件,始终在原 JSON 中编辑。

2.2 detect_manager — 只读工具链探测

可直接粘贴运行,且只读。它会打印信号,以便 AI 根据 §2.1 选择正确的文件;不得写入任何文件。写入是单独的、需用户确认的步骤(§2 第 3 步)。

# Read-only: detect which Node version manager this project/env uses.
# Prints one signal per line. AI maps them to a declaration file via §2.1.
# Does NOT write anything — persistence is a separate, user-confirmed step.
[ -f package.json ] && node -e "require('./package.json').volta && console.log('volta-struct')" 2>/dev/null
command -v volta  >/dev/null && echo "volta-installed"
command -v asdf   >/dev/null && echo "asdf-installed"
[ -f .tool-versions ] && echo "tool-versions-exists"
command -v fnm    >/dev/null && echo "fnm-installed"
command -v nodenv >/dev/null && echo "nodenv-installed"
# No output above → default to .nvmrc (broadest cross-tool coverage).

此探测脚本检测的是版本管理器,而非版本。版本始终由用户指定(§2 第 1 步) — 绝不能使用主机默认版本。

3. 标准操作规程(SOP)

步骤 1 — 检测版本约束
# Prefer .nvmrc (walk up the tree)
cat .nvmrc 2>/dev/null
# Fallback to package.json engines
node -e "console.log(require('./package.json').engines?.node || 'no engines constraint')" 2>/dev/null

为便于理解,上面的代码片段展示了两种最常见的来源。完整的跨工具优先级链(.nvmrc → .node-version → .tool-versions → volta.node → engines.node → CI 配置)已实现为 §7 中可直接粘贴运行的脚本 — 实际操作时请使用该脚本。

步骤 2 — 确认目标版本已安装
ls ~/.nvm/versions/node/ | grep <target-version>

尚未安装 → 提示用户运行 nvm install <version>,然后停止。

步骤 3 — 在一条命令中切换并运行(推荐)
source ~/.nvm/nvm.sh && nvm use <target-version> >/dev/null 2>&1 && node -v && <actual-command>

Shell 状态不会持久保留。nvm use 只影响当前 Shell 进程。每次 Bash 工具调用都可能启动独立的 Shell,因此 nvm use 不会在不同调用之间生效。每条对版本敏感的命令都必须在同一次调用中执行 source ~/.nvm/nvm.sh && nvm use <version> && <command> — 绝不要拆成“先运行 nvm use,再运行命令”。

步骤 4 — 确认切换成功

切换后,务必先运行 node -v,再运行实际命令。nvm use 可能会静默失败(例如目标版本尚未安装);跳过确认,就可能误以为已切换,实际上并没有。

4. 常见错误模式(调用方必须拦截)

模式后果正确做法
直接运行 npx tsc --noEmit,忽略 .nvmrc结果不可信先检测 .nvmrc,再运行 nvm use,然后运行命令
分两步操作:先运行 nvm use 14,再在下一次调用中运行 tscShell 状态丢失,恢复为默认版本在单次调用中运行:source && nvm use && cmd
运行 nvm use 14 后不运行 node -v把静默失败误认为成功切换后务必运行 node -v
对未安装的版本运行 nvm use报错或静默失败先运行 ls ~/.nvm/versions/node/;如果缺少该版本,提示运行 nvm install
运行 nvm use node / nvm use default可能不会切换到 .nvmrc 指定的版本使用准确的版本,或运行 nvm use $(cat .nvmrc)
“更高版本通过” ≡ “指定版本通过”错误地判定通过指定版本才是唯一依据
项目有 .node-version,但你使用的是 nvmnvm 忽略 .node-version → 静默回退到默认版本探测完整链条(§2);如果只有 .node-version,运行 nvm use $(cat .node-version)
项目通过 Volta 的 package.json "volta" 指定版本,但你使用的是 nvm/fnmnvm/fnm 无法识别 Volta 指定的版本读取 package.json 的 "volta".node,并对该准确版本运行 nvm use
运行 ls .gitlab-ci.yml Dockerfile*(zsh 中没有 Dockerfile)Glob 匹配失败 → 整个命令无法运行 → 错误地判定“没有 CI”对每个路径使用 [ -f ] / §7 中的 CI 探测方式;切勿将字面路径与未匹配的 Glob 混用
将 engines: ">=0.8.0" 当作指定版本跳过真正的 CI 指定版本;给出荒谬的候选版本对宽泛的 engines 声明,继续检查链条(§2)
对齐版本前运行 npm install;退出码为 0 时忽略 Exit handler never called!node_modules / 锁文件损坏;后续检查结果不可信先对齐版本;将该消息视为安装失败(§2)

5. 验证报告披露

报告验证结果时,必须披露 Node 版本,而不只是通过或失败:

## Code quality
- Node version (.nvmrc v14.21.3): ✅ switched via nvm use
- tsc --noEmit: ✅ 0 errors
- eslint: ✅ no errors/warnings

如果项目未声明版本,而用户提供了版本,必须明确披露这一事实,以便审计来源。还要说明该声明是否已持久化(以便下次会话自动对齐),还是仅在本次应用:

- Node version (user-specified v20.11.0, no prior project declaration): ✅ aligned; persisted to .nvmrc (22.5.0) — future sessions will auto-align

如果用户拒绝持久化,必须明确说明,以便清楚显示这一缺口:

- Node version (user-specified v20.11.0, no project declaration): ✅ aligned for this run; user declined to persist — declaration still missing, recommend adding .nvmrc

如果命令运行时未切换版本,标记 ⚠️ 并重新运行。

6. 两种集成约定

此技能是被依赖方,提供两种引用方式。

6.1 硬依赖约定(核心技能)

适用于将版本对齐作为硬性前提、容错为零的场景(tsc / eslint / build):

# in the caller's frontmatter
dependencies:
  - node-version-discipline

调用方在启动时运行前置条件检查(与 solve-workflow 的模式相同):如果缺少此技能,立即中止并提示安装。不得静默降级——确保在任何环境中都能进行可信的验证。

6.2 软引用约定(扩展技能)

适用于将版本对齐视为最佳实践的场景(npm test / 浏览器调试 / 通用工程命令),在调用方正文的相关步骤中添加一行:

运行前,调用 node-version-discipline 技能,将 Node 对齐到 .nvmrc 指定的版本。

不声明 dependencies,也不进行前置条件检查——依赖 AI 在该步骤主动调用。

7. 自包含检测脚本(可直接粘贴)

# Probe the full version-declaration chain (cross-tool), including CI/Docker, then switch via nvm.
# First authoritative hit wins. Shell-safe: per-path [ -f ], no mixed literal+glob ls.
# See §2 for priority, degenerate engines, and CI rules.
probe_file() {  # $1 = filename; prints a version walking up the tree
  for d in . .. ../.. ../../..; do
    [ -f "$d/$1" ] && { cat "$d/$1" | grep -Eo 'v?[0-9]+(\.[0-9]+){0,2}' | head -1 | sed 's/^v//'; return; }
  done
}
extract_node_pin() {  # stdin → first node major/minor/patch from CI/Docker idioms
  # Strip YAML/Actions quotes so `node-version: '18'` / `image: "node:14"` still match.
  sed -E "s/[\"']//g" \
    | grep -Eo '(image:[[:space:]]*node:|FROM[[:space:]]+node:|node-version:[[:space:]]*|NODE_VERSION[=:[:space:]]+)v?[0-9]+(\.[0-9]+){0,2}' \
    | head -1 | grep -Eo 'v?[0-9]+(\.[0-9]+){0,2}' | head -1 | sed 's/^v//'
}
probe_ci() {
  # Fixed literals only — never `ls … Dockerfile*` under zsh nomatch.
  for f in .gitlab-ci.yml Jenkinsfile Dockerfile Dockerfile.web Dockerfile.node; do
    [ -f "$f" ] || continue
    ver="$(extract_node_pin < "$f")"
    [ -n "$ver" ] && { echo "$ver"; return; }
  done
  if [ -d .github/workflows ]; then
    # find avoids shell glob expansion failures
    while IFS= read -r f; do
      [ -f "$f" ] || continue
      ver="$(extract_node_pin < "$f")"
      [ -n "$ver" ] && { echo "$ver"; return; }
    done < <(find .github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null)
  fi
}
is_degenerate_engines() {
  case "$1" in
    '>=0.8.0'|'>0.8.0'|'>=0.10.0'|'>0.10.0'|'>=0.12.0') return 0 ;;
    *) return 1 ;;
  esac
}
NODE_VER="$(probe_file .nvmrc)"
[ -z "$NODE_VER" ] && NODE_VER="$(probe_file .node-version)"
[ -z "$NODE_VER" ] && NODE_VER="$(probe_file .tool-versions)"
if [ -z "$NODE_VER" ] && [ -f package.json ]; then
  NODE_VER="$(node -e "const p=require('./package.json'); console.log(p.volta?.node || '')" 2>/dev/null)"
fi
if [ -z "$NODE_VER" ] && [ -f package.json ]; then
  ENG="$(node -e "const p=require('./package.json'); console.log((p.engines&&p.engines.node)||'')" 2>/dev/null)"
  if [ -n "$ENG" ] && ! is_degenerate_engines "$ENG"; then
    NODE_VER="$(printf '%s' "$ENG" | grep -Eo '[0-9]+(\.[0-9]+){0,2}' | head -1)"
  fi
fi
[ -z "$NODE_VER" ] && NODE_VER="$(probe_ci)"
if [ -n "$NODE_VER" ]; then
  source ~/.nvm/nvm.sh 2>/dev/null && nvm use "$NODE_VER" >/dev/null 2>&1
  echo "✅ Node: $(node -v) (aligned to $NODE_VER)"
else
  echo "ℹ️ No declaration found — STOP and ask the user which Node version to use (do not guess). Current host default: $(node -v)"
  echo "   Then run detect_manager (§2.2) to pick a declaration file, confirm with the user, and persist it (§2.1) so future sessions auto-align."
fi

8. 对非 nvm 环境的回退方案

SOP 假定使用 nvm(Unix / macOS)。其他环境:

环境回退操作
Windows提示用户手动切换(nvm-windows / volta use);在报告中标记 ⚠️「未自动对齐,请手动确认版本」
fnm / volta / asdf将 nvm use 替换为相应命令(fnm use / volta pin / asdf local nodejs <version>)
容器 / CINode 版本由镜像固定;确认镜像版本与 .nvmrc 一致

切勿假装已完成对齐——未执行自动对齐时,务必明确说明。

9. 相关内容

  • 来源:一个真实案例修复:某移动项目将 .nvmrc 固定为 v14.21.3,而主机默认使用 v22,导致 tsc/eslint 在更高版本下误报通过。
  • CI 假阴性:某项目仅在 .gitlab-ci.yml 中通过 image: node:14 固定版本,却因 zsh 通配符展开中止而被误报为未声明——§2 的 shell 安全探测和 §7 的 probe_ci 可弥补这一遗漏。
  • 无声明路径:项目完全未声明 Node 版本时,§2 会将用户的回答写入持久化声明文件(§2.1 会根据 §2.2 的工具链探测选择类型),从而形成闭环,使下一次会话在首次探测时就能读取该文件,而不必再次询问。
  • 引用方:PDCA 主机和相关工具通过 frontmatter 中的 dependencies 引用(主机是 SoT;此技能不会维护集成方名称列表)。test-suite-ensure 可能会在测试前软引用对齐。

评论

支持 Markdown