DeepSeek Harness rc.2:DSH plugin pnpm run clean 报 outDir 错
在 dsh-v0.1.7-rc.2(477b4f4205)的全新检出上跑 pnpm run clean,会立刻以退出码 1 失败——错误是 clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types。 原因是 rc.2 新增的 tsconfig.desktop-keyboard-tests.json(提交 6a82709a2b)声明了一个不以 /types 结尾的 outDir,并且能经由 tsconfig.client.json 进入根项目的引用图;而 scripts/clean.ts 的 buildOutputDirectories() 只接受两种形态的 outDir,其余一律抛错(#8127)。它最讽刺的地方在于:clean 脚本存在的意义正是「清掉上一版本留下的陈旧 lib/」,而它在最需要这个能力的场景里恰恰坏了。 该问题已在 master 上由 a08472e 修掉并随 dsh-v0.2.0-rc.1 发布;本文按「先分诊 → 机制 → 三种修法 → 修好后的一个残留」展开,并给出一份可以照抄的手工清理清单。
先分诊:这是「计划期」的失败,不是「执行期」的失败
一句话:这条错误是在读 tsconfig、计算「该删什么」的阶段抛出来的,跟磁盘上有没有那些目录、有没有权限都无关——所以它不会因为你先手工删掉某个子目录而消失。
先把两种失败模式分开,能省掉一轮无效尝试:
| 判据 | 计划期失败(本例) | 执行期失败 |
|---|---|---|
| 报错时机 | 立刻,还没开始删任何东西 | 删到一半才报 |
| 错误文本 | 关于 tsconfig / outDir 的语义校验 | 关于文件占用、权限、路径 |
| 先手工删目录能否绕过 | 不能 | 常常能 |
| 修法 | 改配置或改脚本 | 关进程 / 提权 / 清占用 |
本例的证据很直接:错误文本是 expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types,这是对配置值的断言,不是对文件系统的断言。所以一切「先删那个目录再跑」的尝试都是方向错误的。
机制:clean 的计划器只认两种 outDir 形态
一句话:buildOutputDirectories() 的合法输入只有「basename 为 types」和「native 入口的输出目录」,其它名字直接抛错;而 rc.2 新增的键盘测试 tsconfig 正好是第三种名字,还挂在引用图上。
三个条件同时成立,故障才会发生:
-
有一个非
/types结尾的 outDir。tsconfig.desktop-keyboard-tests.json声明:jsonc{ // emitDeclarationOnly 的键盘测试 fixture "outDir": "lib/desktop-keyboard-test-types" } -
它能被根项目的引用图看到。 该 tsconfig 经由
tsconfig.client.json被根项目引用——只要在图上,就会被 clean 检查,无论它服务的是产品代码还是测试夹具。 -
脚本对未知形态零容忍。
scripts/clean.ts的buildOutputDirectories()接受且仅接受两种形态:text① basename 为 types (常规类型产物目录) ② native 入口的输出目录 (已被特判) 其它任何名字 → 抛错,而不是跳过或警告
于是「新增一个测试用 tsconfig」这个看似无害的构建改动,直接把 clean 变成了全局失败。
为什么不是「警告后跳过」就好
因为对 clean 来说,「不认识的 outDir」是一个意义不明的状态。 跳过它可能漏清理(留下陈旧产物),照删又可能删掉不该删的东西。抛错是保守选择——问题是它把「一个测试 fixture 的命名」升级成了「整条 clean 不可用」。上游的修法因此不是放宽校验,而是把第三种形态从根上消掉(见修法一)。
为什么这个 bug 值得单独立文
它符合三个「构建脚本站得住脚、用起来要命」的特征:
- 只在全新检出上炸。 全新 checkout +
pnpm install之后是 clean 的正题场景——你要清掉上一版本留下的lib/。平时本地反复构建、目录早被清过的环境反而可能没撞上。 - 症状与意图相反。 clean 的存在意义是「清掉陈旧产物」,而它恰恰在「有陈旧产物要清」时失败。
- 错误信息指向配置,不指向行为。 看到
outDir的人第一反应是改 outDir,而正确方向是删掉那个 tsconfig(见 FAQ)。
修法与残留:升级 / cherry-pick / 手工清理,以及修复后活下来的目录
修法一(推荐):升级到 dsh-v0.2.0-rc.1
一句话:该问题已在 master 上由 a08472e(fix(build): fold desktop keyboard tests into client typecheck)修复,并随 dsh-v0.2.0-rc.1 发布。
修复做的事:
- 删除
tsconfig.desktop-keyboard-tests.json; - 把这些文件并入
tsconfig.client.json的类型检查; - 于是项目图里唯一不以
/types结尾的 outDir 只剩 native 入口的lib,而scripts/clean.ts本来就特判了它。
在全新克隆上对照验证:
git checkout dsh-v0.1.7-rc.2 && pnpm run clean
# clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types # exit 1
git checkout dsh-v0.2.0-rc.1 && pnpm run clean
# clean: already clean # exit 0
这是最省事的路线,也是生产团队倾向的选择:不在发布波浪中途改动构建脚本,等下一次固定的版本升级时自然带上。
修法二(留在 rc.2):cherry-pick 修复提交
一句话:如果你必须钉在 rc.2,这个修复能干净地摘过来,摘完 clean 立刻退出 0。
git cherry-pick a08472e982
pnpm run clean
适用与不适用:
- 适用:你已经钉在 rc.2、且需要一个能用的 clean;
- 不适用:你正处在发布窗口内、不希望引入构建脚本改动——那就用修法三。
修法三(不改版本):按清单手工清理
一句话:这是上游与生产团队都文档化过的临时办法——手工删掉 gitignore 的 lib/ 目录与根级 *.tsbuildinfo,等价于把 clean 的计划手动执行一遍。
报告人验证过的顺序与结果:
- 手工删除各个 gitignored 的
lib/目录; - 删除根级的
*.tsbuildinfo(这一步别漏:TS 增量构建的缓存文件没清掉,下一次 build 可能仍按旧图的判定走); - 之后
pnpm run build退出 0,产出 343 个 client artifact。
这份清单之所以靠谱,是因为它复刻的就是 clean 原本要做的事,只是把「计算」换成了「人工枚举」。它的代价是需要随版本更新人工维护——所以生产团队的态度是「先用着,等下一次版本升级换掉它」。
修好之后的一个残留:那个目录会活下来
一句话:修法是删掉 tsconfig,于是 clean 从此不再知道 lib/desktop-keyboard-test-types 的存在;如果 rc.2 已经把它生成出来,它会在 pnpm run clean 之后依然存活。
这是修法一唯一的副作用,值得单独提醒(已在讨论中被实测确认:跑出 already clean 之后,那个文件夹仍然在):
# 一次性手工删除
rm -rf lib/desktop-keyboard-test-types
# PowerShell
Remove-Item -Recurse -Force lib\desktop-keyboard-test-types
删一次即可——配置没了,之后不会再生成它。但如果你在升级前就用 rc.2 构建过,务必记得删这一步,否则一个陈旧的类型目录会一直留在工作区里,而 clean 再也不会替你处理它。
排查注意事项
- 先看错误是「配置语义」还是「文件系统」。 前者改配置,后者清占用;本例属于前者,先删目录没用。
- 别去改那个 outDir 的名字。 它服务于键盘测试 fixture,改成
lib/types会与真正的类型目录互相覆盖——上游的方向是删掉这个 tsconfig。 - 检查新增的 tsconfig 是否挂在项目引用图上。 只要能从根项目引用到,就会被所有基于引用图运行的脚本(clean、build、增量检查)看到。
- 构建脚本对未知输入零容忍时要当心。 抛错比跳过保守,但会把「一个 fixture 的命名」升级成「整条链路不可用」——新增配置时值得先跑一遍 clean。
- 全新检出才是这类问题的正题场景。 本地反复构建过的环境可能掩盖它;验证修复请在 fresh clone 上做。
- 手工清理别漏
*.tsbuildinfo。 只删lib/而留下增量缓存,下一次构建可能仍按旧判定走。 - 升级时记得手工删一次残留目录。
lib/desktop-keyboard-test-types不会因修复被自动清掉。 - 发布窗口内优先「等等再升」。 生产团队的取舍是有道理的:钉在 rc.2 + 手工清理,比波浪中途 cherry-pick 一个构建脚本改动更稳。
- cherry-pick 前确认干净度。 该提交能干净摘取(讨论中已验证),但摘取后仍要跑一次
pnpm run clean确认退出 0。 - 把「clean 能否通过在全新检出上」纳入升级检查表。 这类问题 CI 未必覆盖,但一次 fresh clone 就能发现。
来源
- #8127 — pnpm run clean fails on v0.1.7-rc.2: "expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types"(问题报告、
a08472e修复说明与已采纳答案、cherry-pick 验证、残留目录确认、生产团队的取舍,均出自该讨论) - deepseek-ai/deepseek-harness(
scripts/clean.ts、tsconfig.desktop-keyboard-tests.json、tsconfig.client.json;锚点dsh-v0.1.7-rc.2=477b4f4205,修复提交a08472e982)
这条例子的价值不在修法有多难,而在于它暴露了一条容易被忽略的耦合:项目引用图是「所有基于 TS 的构建脚本」的共同输入,往图上加一个 tsconfig 的副作用,可能落在与它毫不相干的脚本上。 如果你也在维护带项目引用的 monorepo,建议在新增任何 tsconfig 后跑一遍 clean 与 build,并把「fresh clone 上 clean 是否退出 0」写进升级检查表——这类只在新检出上复现的失败,靠日常开发是很难撞见的。把插件的安装、升级与系统日志集中到 DSH Plugin Hub 里对照,升级类问题会更容易定位。

常见问题
因为触发它的是**项目引用图里那个新加入的 tsconfig**,而不是你本地有没有构建产物。tsconfig.desktop-keyboard-tests.json 在 rc.2 里通过 tsconfig.client.json 进入根项目的引用图,scripts/clean.ts 的计划器一看到它的 outDir 不以 /types 结尾就抛错。如果你本地那份 lib/ 早就被旧版本或其它方式清过,可能一时没撞上;但在**全新检出、且上一版本留下 lib/** 这个 clean 存在的正题场景里,它必然失败。
不建议。lib/desktop-keyboard-test-types 是那个 **emitDeclarationOnly 的键盘测试 fixture** 的输出目录,改成 lib/types 会与真正的类型产物目录撞名、互相覆盖。上游选择的修法方向恰好相反:**删掉这个独立 tsconfig**,把这些文件并入 tsconfig.client.json 的类型检查,让引用图里只剩「以 /types 结尾」和「native 入口的 lib」两种形态——后者 scripts/clean.ts 本来就特判了。
没用,错误跟那个目录是否存在无关——它是在**读 tsconfig 的计划阶段**抛的,不是遍历文件时抛的。要留在 rc.2,正确做法是 cherry-pick a08472e982,或者按手工清单清理。
这是修复留下的一个残留:修法是**删掉那个 tsconfig**,于是 clean 从此不再知道这个目录的存在。如果某个 rc.2 构建已经把它生成出来,它会**在 pnpm run clean 之后依然存活**(已被实测确认:「already clean」跑完目录仍在)。手工删一次即可,之后不会再生成。
按讨论里生产团队的取舍来:**保留已文档化的手工清理流程**(清 gitignore 的 lib/ 目录加根级 *.tsbuildinfo),不做波浪中途的 cherry-pick,等下一次固定的版本升级时随 a08472e 一起带上。这样避免在发布窗口里引入构建脚本改动。
相关术语
- scripts/clean.ts / buildOutputDirectories()
- DSH 的 clean 脚本用它计算「该删哪些构建输出目录」。它只接受两种形态的 `outDir`:①basename 为 `types`;②native 入口的输出目录。**任何其它名字都会直接抛错**,而不是跳过或警告——这就是一条新增的测试 tsconfig 能整体打断 clean 的原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- 项目引用(project references)图
- TypeScript 的 `references` 机制把多个 tsconfig 组织成有向图,根项目通过它聚合各子项目。`tsconfig.desktop-keyboard-tests.json` 正是因为能经由 `tsconfig.client.json` 被根项目引用到,才进入了 clean 的扫描范围——**只要在图上,就会被检查,无论它服务于产品代码还是测试 fixture**。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- emitDeclarationOnly fixture
- `tsconfig.desktop-keyboard-tests.json` 是一个只产出 `.d.ts`(不产出 JS)的配置,用于键盘测试相关的类型夹具,其 `outDir` 被命名为 `lib/desktop-keyboard-test-types`。它是本次故障的触发物:既不在 clean 的两种合法形态里,又确实挂在项目引用图上。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- a08472e(修复提交)
- `fix(build): fold desktop keyboard tests into client typecheck`。它删除 `tsconfig.desktop-keyboard-tests.json`,把这些文件并入 `tsconfig.client.json` 的类型检查,于是项目图里唯一不以 `/types` 结尾的 outDir 只剩 native 入口的 `lib`(clean 已特判)。该提交随 **dsh-v0.2.0-rc.1** 发布,也能干净地 cherry-pick 回 rc.2。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
来源
- #8127 — pnpm run clean fails on v0.1.7-rc.2: "expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types"· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub