@deepwhale/coding-agent 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/dist/agent/agent-compaction.d.ts +0 -74
- package/dist/agent/agent-compaction.d.ts.map +0 -1
- package/dist/agent/agent-compaction.js +0 -145
- package/dist/agent/agent-compaction.js.map +0 -1
- package/dist/agent/index.d.ts +0 -16
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -17
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/session-adapter.d.ts +0 -177
- package/dist/agent/session-adapter.d.ts.map +0 -1
- package/dist/agent/session-adapter.js +0 -365
- package/dist/agent/session-adapter.js.map +0 -1
- package/dist/agent/tool-loop.d.ts +0 -123
- package/dist/agent/tool-loop.d.ts.map +0 -1
- package/dist/agent/tool-loop.js +0 -436
- package/dist/agent/tool-loop.js.map +0 -1
- package/dist/env/load-project-env.d.ts +0 -40
- package/dist/env/load-project-env.d.ts.map +0 -1
- package/dist/env/load-project-env.js +0 -80
- package/dist/env/load-project-env.js.map +0 -1
- package/dist/index.d.ts +0 -32
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -33
- package/dist/index.js.map +0 -1
- package/dist/llm-factory.d.ts +0 -50
- package/dist/llm-factory.d.ts.map +0 -1
- package/dist/llm-factory.js +0 -110
- package/dist/llm-factory.js.map +0 -1
- package/dist/modes/index.d.ts +0 -14
- package/dist/modes/index.d.ts.map +0 -1
- package/dist/modes/index.js +0 -14
- package/dist/modes/index.js.map +0 -1
- package/dist/modes/print.d.ts +0 -50
- package/dist/modes/print.d.ts.map +0 -1
- package/dist/modes/print.js +0 -236
- package/dist/modes/print.js.map +0 -1
- package/dist/modes/rpc.d.ts +0 -52
- package/dist/modes/rpc.d.ts.map +0 -1
- package/dist/modes/rpc.js +0 -316
- package/dist/modes/rpc.js.map +0 -1
- package/dist/modes/tui.d.ts +0 -112
- package/dist/modes/tui.d.ts.map +0 -1
- package/dist/modes/tui.js +0 -733
- package/dist/modes/tui.js.map +0 -1
- package/dist/policy/args-digest.d.ts +0 -13
- package/dist/policy/args-digest.d.ts.map +0 -1
- package/dist/policy/args-digest.js +0 -29
- package/dist/policy/args-digest.js.map +0 -1
- package/dist/policy/chain.d.ts +0 -19
- package/dist/policy/chain.d.ts.map +0 -1
- package/dist/policy/chain.js +0 -24
- package/dist/policy/chain.js.map +0 -1
- package/dist/policy/index.d.ts +0 -17
- package/dist/policy/index.d.ts.map +0 -1
- package/dist/policy/index.js +0 -16
- package/dist/policy/index.js.map +0 -1
- package/dist/policy/sanitize-reason.d.ts +0 -11
- package/dist/policy/sanitize-reason.d.ts.map +0 -1
- package/dist/policy/sanitize-reason.js +0 -24
- package/dist/policy/sanitize-reason.js.map +0 -1
- package/dist/policy/static-rules.d.ts +0 -32
- package/dist/policy/static-rules.d.ts.map +0 -1
- package/dist/policy/static-rules.js +0 -106
- package/dist/policy/static-rules.js.map +0 -1
- package/dist/policy/types.d.ts +0 -56
- package/dist/policy/types.d.ts.map +0 -1
- package/dist/policy/types.js +0 -13
- package/dist/policy/types.js.map +0 -1
- package/dist/repl/repl-confirm.d.ts +0 -49
- package/dist/repl/repl-confirm.d.ts.map +0 -1
- package/dist/repl/repl-confirm.js +0 -88
- package/dist/repl/repl-confirm.js.map +0 -1
- package/dist/repl.d.ts +0 -154
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js +0 -780
- package/dist/repl.js.map +0 -1
- package/dist/sandbox/docker-runner.d.ts +0 -147
- package/dist/sandbox/docker-runner.d.ts.map +0 -1
- package/dist/sandbox/docker-runner.js +0 -426
- package/dist/sandbox/docker-runner.js.map +0 -1
- package/dist/sandbox/env-gate.d.ts +0 -28
- package/dist/sandbox/env-gate.d.ts.map +0 -1
- package/dist/sandbox/env-gate.js +0 -65
- package/dist/sandbox/env-gate.js.map +0 -1
- package/dist/sandbox/local-runner.d.ts +0 -29
- package/dist/sandbox/local-runner.d.ts.map +0 -1
- package/dist/sandbox/local-runner.js +0 -79
- package/dist/sandbox/local-runner.js.map +0 -1
- package/dist/sandbox/types.d.ts +0 -80
- package/dist/sandbox/types.d.ts.map +0 -1
- package/dist/sandbox/types.js +0 -25
- package/dist/sandbox/types.js.map +0 -1
- package/dist/tools/bash.d.ts +0 -35
- package/dist/tools/bash.d.ts.map +0 -1
- package/dist/tools/bash.js +0 -233
- package/dist/tools/bash.js.map +0 -1
- package/dist/tools/edit-file.d.ts +0 -22
- package/dist/tools/edit-file.d.ts.map +0 -1
- package/dist/tools/edit-file.js +0 -79
- package/dist/tools/edit-file.js.map +0 -1
- package/dist/tools/find.d.ts +0 -21
- package/dist/tools/find.d.ts.map +0 -1
- package/dist/tools/find.js +0 -168
- package/dist/tools/find.js.map +0 -1
- package/dist/tools/grep.d.ts +0 -19
- package/dist/tools/grep.d.ts.map +0 -1
- package/dist/tools/grep.js +0 -170
- package/dist/tools/grep.js.map +0 -1
- package/dist/tools/index.d.ts +0 -10
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -10
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/read-file.d.ts +0 -18
- package/dist/tools/read-file.d.ts.map +0 -1
- package/dist/tools/read-file.js +0 -52
- package/dist/tools/read-file.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -39
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -67
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/write-file.d.ts +0 -18
- package/dist/tools/write-file.d.ts.map +0 -1
- package/dist/tools/write-file.js +0 -47
- package/dist/tools/write-file.js.map +0 -1
- package/dist/tui-ink-bundle.js +0 -39104
- package/dist/types.d.ts +0 -89
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/util/index.d.ts +0 -16
- package/dist/util/index.d.ts.map +0 -1
- package/dist/util/index.js +0 -16
- package/dist/util/index.js.map +0 -1
- package/dist/util/tui-history.d.ts +0 -37
- package/dist/util/tui-history.d.ts.map +0 -1
- package/dist/util/tui-history.js +0 -93
- package/dist/util/tui-history.js.map +0 -1
- package/dist/verify/format-report.d.ts +0 -57
- package/dist/verify/format-report.d.ts.map +0 -1
- package/dist/verify/format-report.js +0 -128
- package/dist/verify/format-report.js.map +0 -1
- package/dist/verify/index.d.ts +0 -8
- package/dist/verify/index.d.ts.map +0 -1
- package/dist/verify/index.js +0 -8
- package/dist/verify/index.js.map +0 -1
- package/dist/verify/verify-runner.d.ts +0 -186
- package/dist/verify/verify-runner.d.ts.map +0 -1
- package/dist/verify/verify-runner.js +0 -707
- package/dist/verify/verify-runner.js.map +0 -1
|
@@ -1,707 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @deepwhale/coding-agent — Verify runner
|
|
3
|
-
*
|
|
4
|
-
* Sprint 1c-revive-2-D-11 (2026-06-04): `deepwhale --verify` 跟 REPL `/verify`
|
|
5
|
-
* 内部统一走 `runVerify()` — 跑 4 步真验证 (build / lint / typecheck / test),
|
|
6
|
-
* 不走 LLM, 不走 tool loop, 生成 `VerificationReport`.
|
|
7
|
-
*
|
|
8
|
-
* 设计拍板:
|
|
9
|
-
* - 4 步默认: corepack pnpm build / lint / typecheck / test
|
|
10
|
-
* - 用户可覆盖 checks (单测用) / cwd (单测 tmpdir) / signal (取消)
|
|
11
|
-
* - 每步 spawn child_process 同步等 exit, **不**用 stream (避免长 stdout 卡 session)
|
|
12
|
-
* - stdout/stderr 截断: 默认 4 KB 尾, 防止 session JSONL 撑爆
|
|
13
|
-
* - timeout 默认 5 min/步, 总 timeout 默认 30 min (build+lint+typecheck+test 全跑)
|
|
14
|
-
* - 任何一步 fail → 整体 fail, **不**继续后续步 (用户拍的 D-11 review: fail-fast
|
|
15
|
-
* 比继续跑节省时间, 而且后续步基于 build 产物, fail-fast 防止假绿)
|
|
16
|
-
* - 不读 .env, 不动 LLM, 不调 tool loop — 真·本地 CLI
|
|
17
|
-
*
|
|
18
|
-
* 不变量 (跟 D-10c 集成测语义 hotfix 一脉相承):
|
|
19
|
-
* - 不打印 / 不写 key 值
|
|
20
|
-
* - 不做 ASCII-only sanitize
|
|
21
|
-
* - 不读 .env 文件 (D-7 loadProjectEnv 是 caller 职责)
|
|
22
|
-
* - 不依赖 LLM client (跑 verify 不需要 key)
|
|
23
|
-
*
|
|
24
|
-
* @module @deepwhale/coding-agent/verify-runner
|
|
25
|
-
*/
|
|
26
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
27
|
-
import { createRequire } from 'node:module';
|
|
28
|
-
import { fileURLToPath } from 'node:url';
|
|
29
|
-
import { dirname, join } from 'node:path';
|
|
30
|
-
import { spawn } from 'node:child_process';
|
|
31
|
-
import process from 'node:process';
|
|
32
|
-
export function detectContext(cwd = process.cwd()) {
|
|
33
|
-
const hasWorkspaceYaml = existsSync(join(cwd, 'pnpm-workspace.yaml'));
|
|
34
|
-
let hasWorkspacesField = false;
|
|
35
|
-
try {
|
|
36
|
-
const rootPkgPath = join(cwd, 'package.json');
|
|
37
|
-
if (existsSync(rootPkgPath)) {
|
|
38
|
-
const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf8'));
|
|
39
|
-
hasWorkspacesField =
|
|
40
|
-
rootPkg.workspaces !== undefined &&
|
|
41
|
-
rootPkg.workspaces !== null &&
|
|
42
|
-
(Array.isArray(rootPkg.workspaces) || typeof rootPkg.workspaces === 'object');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// package.json 读不到 / JSON 坏 → 视作 installed (保守)
|
|
47
|
-
hasWorkspacesField = false;
|
|
48
|
-
}
|
|
49
|
-
return hasWorkspaceYaml || hasWorkspacesField ? 'monorepo' : 'installed';
|
|
50
|
-
}
|
|
51
|
-
const MONOREPO_CHECKS = [
|
|
52
|
-
// 拍板: args[0] 是实际 spawn 的可执行 (e.g. 'corepack'), 后面是它的参数.
|
|
53
|
-
// 4 步 default 用 corepack 跑 pnpm, 跟用户日常工作流一致
|
|
54
|
-
// (corepack 自身 < 5MB, 启动 < 200ms, 不显著影响 verify 总耗时).
|
|
55
|
-
//
|
|
56
|
-
// Sprint 1c-revive-2-D-11-4 review P1 修复 (2026-06-04): args[0] 在 Windows 上
|
|
57
|
-
// 由 resolveRunner() 转成 'corepack.cmd'. 默认仍写 'corepack' (Linux/macOS 不变,
|
|
58
|
-
// 跟 1c 时代兼容性), 真 spawn 时再换. 单测可注入 platform = 'win32' 验证.
|
|
59
|
-
{
|
|
60
|
-
name: 'build',
|
|
61
|
-
command: 'corepack pnpm build',
|
|
62
|
-
args: ['corepack', 'pnpm', 'build'],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'lint',
|
|
66
|
-
command: 'corepack pnpm lint',
|
|
67
|
-
args: ['corepack', 'pnpm', 'lint'],
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: 'typecheck',
|
|
71
|
-
command: 'corepack pnpm typecheck',
|
|
72
|
-
args: ['corepack', 'pnpm', 'typecheck'],
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'test',
|
|
76
|
-
command: 'corepack pnpm vitest run --exclude "packages/*/test/integration/**"',
|
|
77
|
-
args: ['corepack', 'pnpm', 'vitest', 'run', '--exclude', 'packages/*/test/integration/**'],
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
/**
|
|
81
|
-
* Installed-context checks (D-21.0, 2026-06-06).
|
|
82
|
-
*
|
|
83
|
-
* 拍板: 当 `npm install -g @deepwhale/coding-agent` 之后, 用户的 cwd 一般是
|
|
84
|
-
* "我的项目" 而非 "deepwhale 源码仓库". 此时 verify 不能再跑 pnpm build/lint/
|
|
85
|
-
* typecheck/vitest — 这些 devDep 没 ship. 改成 4 步 "装出来能跑" 的 sanity:
|
|
86
|
-
*
|
|
87
|
-
* 1. syntax-check : `node --check <main>` — dist JS 能 parse
|
|
88
|
-
* 2. import-check : `node -e "import('${pkg}')"` — 包名 (走 exports 重定向)
|
|
89
|
-
* 3. bin-check : `ls -la <bin-path>` — bin 文件存在
|
|
90
|
-
* 4. exports-check : `node -e "import('${pkg}').then(m => Object.keys(m).length >= 1)"`
|
|
91
|
-
* — 至少 export 1 个 symbol
|
|
92
|
-
*
|
|
93
|
-
* 关键设计: 不调 pnpm / vitest / eslint / tsc. 只用 node 内置 + 文件系统, 单包
|
|
94
|
-
* 装出来后能跑. 拍板 (D-21.0, 2026-06-06, bugfix 2): import-check / exports-check
|
|
95
|
-
* 用**包名** (e.g. '@deepwhale/coding-agent') 不是**绝对路径**. Node 22 ESM
|
|
96
|
-
* 不支持 `import('/abs/path/to/dir')` (Directory import), 必须用包名让 Node 走
|
|
97
|
-
* node_modules 解析 + package.json#exports 重定向. 绝对路径只用于 syntax-check /
|
|
98
|
-
* bin-check (--check + ls 不解 import).
|
|
99
|
-
*
|
|
100
|
-
* 拍板 (D-21.0, 2026-06-06, bugfix 1): import-check / exports-check 的 args[2]
|
|
101
|
-
* **必须**是完整 JS 字符串, 内含 `import('${pkg}')` 调用, 渲染时只替换包路径.
|
|
102
|
-
* 不能让 `args[2]` 等于包路径 (那是 shell exec 的 bug, 不是 JS 源码).
|
|
103
|
-
*
|
|
104
|
-
* 已知局限: installed check 不验证"工具调用"/"session JSONL"等运行时行为 — 这
|
|
105
|
-
* 些靠 v1.0.1 真实跑 print / REPL 模式覆盖. verify 在 installed 模式下定位是
|
|
106
|
-
* "装出来能装能 import", 不是 "功能 100% 端到端".
|
|
107
|
-
*/
|
|
108
|
-
const INSTALLED_CHECKS_TEMPLATE = [
|
|
109
|
-
{
|
|
110
|
-
name: 'syntax-check',
|
|
111
|
-
commandTemplate: 'node --check ${CWD}/dist/index.js',
|
|
112
|
-
args: ['node', '--check', '__CWD__/dist/index.js'],
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: 'import-check',
|
|
116
|
-
commandTemplate: 'node -e "import(\'${PKG}\').then(m => process.exit(0)).catch(e => { process.stderr.write(e.message); process.exit(1) })"',
|
|
117
|
-
args: [
|
|
118
|
-
'node',
|
|
119
|
-
'-e',
|
|
120
|
-
"import('__PKG__').then(m => process.exit(0)).catch(e => { process.stderr.write(e.message); process.exit(1); })",
|
|
121
|
-
],
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: 'bin-check',
|
|
125
|
-
// D-25 A3 (F3, 2026-06-06): POSIX `test -f` 改 `node -e` inline JS, 跨平台 0 差异.
|
|
126
|
-
// 修前: args: ['test', '-f', '__CWD__/bin/deepwhale.js'] (Win32: 'test' is not recognized, fail)
|
|
127
|
-
// 修后: args: ['node', '-e', 'require("fs").existsSync(...) ? 0 : 1'] (跨平台一致)
|
|
128
|
-
commandTemplate: "node -e \"require('fs').existsSync('${CWD}/bin/deepwhale.js') ? process.exit(0) : (process.stderr.write('missing: ' + '${CWD}/bin/deepwhale.js'), process.exit(1))\"",
|
|
129
|
-
args: [
|
|
130
|
-
'node',
|
|
131
|
-
'-e',
|
|
132
|
-
"require('fs').existsSync('__CWD__/bin/deepwhale.js') ? process.exit(0) : (process.stderr.write('missing: __CWD__/bin/deepwhale.js'), process.exit(1))",
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: 'exports-check',
|
|
137
|
-
commandTemplate: 'node -e "import(\'${PKG}\').then(m => process.exit(Object.keys(m).length >= 1 ? 0 : 1)).catch(e => { process.stderr.write(e.message); process.exit(1) })"',
|
|
138
|
-
args: [
|
|
139
|
-
'node',
|
|
140
|
-
'-e',
|
|
141
|
-
"import('__PKG__').then(m => process.exit(Object.keys(m).length >= 1 ? 0 : 1)).catch(e => { process.stderr.write(e.message); process.exit(1); })",
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
];
|
|
145
|
-
/**
|
|
146
|
-
* 给定"装出来的 coding-agent 包的根路径" 渲染 INSTALLED_CHECKS_TEMPLATE: 替换
|
|
147
|
-
* args 里的占位符, 同时生成可读的 command 字符串. 模板 → 实例 是纯映射.
|
|
148
|
-
*
|
|
149
|
-
* 拍板: 包根 = "包含 dist/ + bin/ + package.json 的目录". 1.0.1 实测路径:
|
|
150
|
-
* global install: `<prefix>/lib/node_modules/@deepwhale/coding-agent`
|
|
151
|
-
* local install: `<proj>/node_modules/@deepwhale/coding-agent`
|
|
152
|
-
*
|
|
153
|
-
* 拍板 (D-21.0, 2026-06-06, bugfix 3): import-check / exports-check 必须把
|
|
154
|
-
* cwd 设到 `dirname(packageRoot)` (即 node_modules 的**父目录**), 不然 Node
|
|
155
|
-
* 在用户 cwd (/tmp 或别的项目) 跑 `import('@deepwhale/coding-agent')` 时报
|
|
156
|
-
* "Cannot find package" — Node 解析包名从 cwd 向上找 node_modules, cwd 错
|
|
157
|
-
* 就找不到. 把 cwd 设到包根**之上**, Node 解析时能找到 node_modules/@deepwhale/...
|
|
158
|
-
*
|
|
159
|
-
* 调用方 (pickChecksForContext) 用 resolveInstalledPackageRoot() 拿这个路径.
|
|
160
|
-
* 包名 (PKG) 硬编码 '@deepwhale/coding-agent' — 这是 verify 验证的唯一目标包.
|
|
161
|
-
*/
|
|
162
|
-
/**
|
|
163
|
-
* D-25 A3 (F3, 2026-06-06): 内部 helper export 出, 仅供测试断言 args 形态.
|
|
164
|
-
* 业务 0 改, 仅 +1 个 export, 0 影响现有调用方 (pickChecksForContext 内部仍调).
|
|
165
|
-
* 测试用 `renderInstalledChecks('/fake/pkg/root')` 拿 4 check 验 args[0]==='node' / args[1] 不是 POSIX 标志.
|
|
166
|
-
*/
|
|
167
|
-
export function renderInstalledChecks(packageRoot) {
|
|
168
|
-
const pkgName = '@deepwhale/coding-agent';
|
|
169
|
-
// 包根的父目录 = node_modules 所在目录. global: `<prefix>/lib/node_modules`;
|
|
170
|
-
// local: `<proj>/node_modules`. Node 从这个目录起能找到包.
|
|
171
|
-
const nodeModulesDir = dirname(packageRoot);
|
|
172
|
-
return INSTALLED_CHECKS_TEMPLATE.map((t) => {
|
|
173
|
-
const renderedCommand = t.commandTemplate
|
|
174
|
-
.replace(/\$\{CWD\}/g, packageRoot)
|
|
175
|
-
.replace(/\$\{PKG\}/g, pkgName);
|
|
176
|
-
const renderedArgs = t.args.map((a) => a
|
|
177
|
-
.replace(/__CWD__/g, packageRoot)
|
|
178
|
-
.replace(/__CWD_DIST__/g, `${packageRoot}/dist/index.js`)
|
|
179
|
-
.replace(/__PKG__/g, pkgName));
|
|
180
|
-
// import-check / exports-check 需要 cwd = node_modules 所在目录才能解析包名.
|
|
181
|
-
// syntax-check / bin-check 用绝对路径, 不需要特殊 cwd.
|
|
182
|
-
const needsNodeModulesCwd = t.name === 'import-check' || t.name === 'exports-check';
|
|
183
|
-
return {
|
|
184
|
-
name: t.name,
|
|
185
|
-
command: renderedCommand,
|
|
186
|
-
args: renderedArgs,
|
|
187
|
-
...(needsNodeModulesCwd ? { cwd: nodeModulesDir } : {}),
|
|
188
|
-
};
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* 解析"装出来的 @deepwhale/coding-agent 包的根目录" (绝对路径).
|
|
193
|
-
*
|
|
194
|
-
* 拍板 (D-21.0, 2026-06-06): verify 验证的目标是这个**包本身**, 不是用户
|
|
195
|
-
* cwd. 跟用户跑 `deepwhale --verify` 在哪个目录无关. resolveInstalledPackageRoot
|
|
196
|
-
* 必须**独立于 cwd** — 否则用户在 /tmp 跑, verify 就跑去 /tmp 检查, 完全错.
|
|
197
|
-
*
|
|
198
|
-
* 实现策略: ESM 里用 `import.meta.url` 不行 (verify-runner 不是 entry). 用
|
|
199
|
-
* `require.resolve('@deepwhale/coding-agent/package.json')` 找自身包的 package.json,
|
|
200
|
-
* 再 dirname 拿根. CommonJS require 在 ESM module 里走 createRequire. 兼容 tsc
|
|
201
|
-
* 输出 CommonJS 跟 ESM 两种 module 格式.
|
|
202
|
-
*/
|
|
203
|
-
export function resolveInstalledPackageRoot() {
|
|
204
|
-
// createRequire 在 ESM 文件里是合法用法 (Node 12+). 注意: 装出来的 dist/verify/
|
|
205
|
-
// verify-runner.js 跟 @deepwhale/coding-agent/package.json 在同一 node_modules
|
|
206
|
-
// 树下, require.resolve 一定能找到 (前提是这个包真被 require).
|
|
207
|
-
//
|
|
208
|
-
// 拍板 (D-21.0, 2026-06-06): 用 fileURLToPath(import.meta.url) 拿当前 ESM
|
|
209
|
-
// module 的 file:// URL, 换成本地 path, 再 createRequire 拿 req. 这是 ESM
|
|
210
|
-
// 文件里 "走 require" 的官方推荐姿势 — 直接写 `require(...)` 在 ESM 顶层
|
|
211
|
-
// ReferenceError (strict mode 报 require is not defined).
|
|
212
|
-
const req = createRequire(fileURLToPath(import.meta.url));
|
|
213
|
-
const pkgJsonPath = req.resolve('@deepwhale/coding-agent/package.json');
|
|
214
|
-
return dirname(pkgJsonPath);
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* 给定 cwd 选对应 context 的 check 集. 拍板 (D-21.0): 单点决策, 后续要加
|
|
218
|
-
* 'ci' (GitHub Actions) 或 'docker' (容器内) 上下文, 改这里.
|
|
219
|
-
*
|
|
220
|
-
* 重要: cwd 参数**只用于 detectContext** (判 monorepo). installed 模式的
|
|
221
|
-
* check 始终以 coding-agent 包自身根为基准, 不受 cwd 影响.
|
|
222
|
-
*/
|
|
223
|
-
export function pickChecksForContext(cwd = process.cwd()) {
|
|
224
|
-
const ctx = detectContext(cwd);
|
|
225
|
-
if (ctx === 'monorepo')
|
|
226
|
-
return MONOREPO_CHECKS;
|
|
227
|
-
return renderInstalledChecks(resolveInstalledPackageRoot());
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Sprint 1c-revive-2-D-11-4 review P1 修复: Windows 上 `corepack` 在 PATH 解析不到
|
|
231
|
-
* (Node `spawn('corepack', ...)` 在 Win32 默认走 CreateProcessW, 不接 .cmd shim).
|
|
232
|
-
* 实际 `where corepack` 只返 `corepack.cmd`. 修复: Win32 上把 'corepack' → 'corepack.cmd'.
|
|
233
|
-
*
|
|
234
|
-
* 设计: 接受 platform 参数, 默认用 process.platform, 单测可注入 'win32' 验证.
|
|
235
|
-
* 仅在 args0 严格 === 'corepack' 时转换; 其它可执行 (node / bash / .exe) 透传,
|
|
236
|
-
* 跟单测里 mock 各种 runner 的行为兼容.
|
|
237
|
-
*/
|
|
238
|
-
export function resolveRunner(args0, platform = process.platform) {
|
|
239
|
-
if (args0 === 'corepack' && platform === 'win32') {
|
|
240
|
-
return 'corepack.cmd';
|
|
241
|
-
}
|
|
242
|
-
return args0;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* 判定 child close 时的 stderr / stdout 是不是 "启错" 文本 (Win32 shell:true
|
|
246
|
-
* 路径下, "命令不存在" / "No such file" 走 cmd.exe exit 1, 不会 sync spawn
|
|
247
|
-
* 抛). 跨 Win32 shell 启错模式: cmd.exe "X is not recognized" + PowerShell
|
|
248
|
-
* "term not recognized" + POSIX shell 偶发 (e.g. via /bin/sh -c) "No such
|
|
249
|
-
* file". 命中返 true, caller 报 'spawn-error' 跟 POSIX 语义对齐.
|
|
250
|
-
*
|
|
251
|
-
* 设计: 故意**不**用 ENOENT (Node child 不暴露), 也不查 PATH. 简单字符串匹配
|
|
252
|
-
* 几个高置信度关键词, 误伤率低. 留 D-20.8 用 stderr 双检 + Node fs.access
|
|
253
|
-
* 再强化.
|
|
254
|
-
*
|
|
255
|
-
* 拍板 (D-20.7.7, 2026-06-06): 保守匹配, 不命中普通命令 exit 1 (e.g. 编译失败,
|
|
256
|
-
* 测试 fail) 误报.
|
|
257
|
-
*/
|
|
258
|
-
export function looksLikeSpawnError(stdout, stderr, exitCode) {
|
|
259
|
-
if (exitCode === 0)
|
|
260
|
-
return false;
|
|
261
|
-
const text = `${stdout}\n${stderr}`;
|
|
262
|
-
// 关键词按 Win32 cmd / PowerShell / POSIX /bin/sh 优先级排
|
|
263
|
-
// D-25 A2 (F5, 2026-06-06) **更激进删** (用户 22:50 拍板):
|
|
264
|
-
// - 修前: 6 patterns 含 `/No such file/i` 短匹配, 误伤 Vitest ENOENT 业务错误
|
|
265
|
-
// - 用户 plan 原拍"删短匹配保留完整短语", 实测 plan §1.2 自相矛盾:
|
|
266
|
-
// 完整短语 `/No such file or directory/i` 仍命中 Node ENOENT 业务文本
|
|
267
|
-
// (`Error: ENOENT: no such file or directory, open '/x'`)
|
|
268
|
-
// - 用户 22:50 拍板"更激进删", 同时删短匹配 + 完整短语, 只留
|
|
269
|
-
// cmd.exe / bash / PowerShell 5 个非 POSIX 关键词
|
|
270
|
-
// - 跟 memory §10c 7 关键词 shape 不变量兼容 (实测 7 关键词里 POSIX
|
|
271
|
-
// 文本只在 spawn-error 真实场景出现, 业务 ENOENT 走 status='failed' 真实路径)
|
|
272
|
-
// - 同步: 删 plan 写的"保留完整短语"那段, 拍板更新到 .hermes/plans/d19/
|
|
273
|
-
const patterns = [
|
|
274
|
-
/is not recognized/i, // cmd.exe: 'X' is not recognized as an internal or external command
|
|
275
|
-
/not recognized as/i, // cmd.exe 长串前段
|
|
276
|
-
/command not found/i, // POSIX bash 完整短语
|
|
277
|
-
/cannot find the (path|file)/i, // cmd.exe 'The system cannot find the path specified'
|
|
278
|
-
/is not a (recognized|valid) command/i, // PowerShell
|
|
279
|
-
];
|
|
280
|
-
return patterns.some((re) => re.test(text));
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* 跑 4 步验证 (默认), 返回 `VerificationReport`.
|
|
284
|
-
*
|
|
285
|
-
* 拍板 (D-11, 2026-06-04):
|
|
286
|
-
* - 任一 step 失败 → 整体 fail, **不**继续后续 step (fail-fast).
|
|
287
|
-
* 后续 step 基于 build 产物, 跑也是浪费; 而且 build fail 时 typecheck/test 必挂,
|
|
288
|
-
* 显式 fail-fast 比假绿更诚实.
|
|
289
|
-
* - 步骤间**不**共享 stdout/stderr buffer: 每步清零 Buffer 防止污染.
|
|
290
|
-
* - 返回的报告 `overallStatus` = 'failed' iff 任一 step status ∈ {failed, timed-out, spawn-error}.
|
|
291
|
-
* - summary / nextSuggestedAction 由调用方 (REPL/CLI/format-report) 写.
|
|
292
|
-
* runner 不写自然语言, 保持纯函数语义 (便于单测 / roundtrip).
|
|
293
|
-
*/
|
|
294
|
-
export async function runVerify(options = {}) {
|
|
295
|
-
// Sprint D-21.0 (2026-06-06): 默认走 pickChecksForContext(cwd) — 根据 cwd 是
|
|
296
|
-
// monorepo 还是 installed, 选对应 check 集. 调用方仍可传 options.checks 显式覆盖
|
|
297
|
-
// (单测, REPL 强制 4 步等).
|
|
298
|
-
const checks = options.checks ?? pickChecksForContext(options.cwd ?? process.cwd());
|
|
299
|
-
const cwd = options.cwd ?? process.cwd();
|
|
300
|
-
const defaultTimeoutMs = options.defaultTimeoutMs ?? 300_000;
|
|
301
|
-
const stdoutCapBytes = options.stdoutCapBytes ?? 4096;
|
|
302
|
-
const continueOnError = options.continueOnError ?? false;
|
|
303
|
-
const overallStart = Date.now();
|
|
304
|
-
const results = [];
|
|
305
|
-
let aborted = false;
|
|
306
|
-
// 监听外部 signal: 任何 step 跑时如果外部 abort, 立即停止后续 step
|
|
307
|
-
const onAbort = () => {
|
|
308
|
-
aborted = true;
|
|
309
|
-
};
|
|
310
|
-
if (options.signal) {
|
|
311
|
-
if (options.signal.aborted) {
|
|
312
|
-
aborted = true;
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
for (const check of checks) {
|
|
320
|
-
if (aborted) {
|
|
321
|
-
// 外部 signal 触发: 后续 step 全 mark 跳过, 不跑
|
|
322
|
-
const now = Date.now();
|
|
323
|
-
results.push({
|
|
324
|
-
name: check.name,
|
|
325
|
-
command: check.command,
|
|
326
|
-
status: 'spawn-error',
|
|
327
|
-
exitCode: null,
|
|
328
|
-
startedAt: now,
|
|
329
|
-
endedAt: now,
|
|
330
|
-
durationMs: 0,
|
|
331
|
-
stdoutTail: '',
|
|
332
|
-
stderrTail: '',
|
|
333
|
-
errorMessage: 'aborted by external signal',
|
|
334
|
-
});
|
|
335
|
-
if (!continueOnError)
|
|
336
|
-
break;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: 传 signal 进 runOneCheck 让
|
|
340
|
-
// 当前 child 在外部 abort 触发时被 kill, 跟"取消不能卡住"目标一致.
|
|
341
|
-
// 之前 signal 只在 runVerify 主循环设 aborted, **不**影响当前 child,
|
|
342
|
-
// 只能"等下 step 跳过" — race 时 child 跑完才看到 abort.
|
|
343
|
-
const result = await runOneCheck(check, {
|
|
344
|
-
cwd,
|
|
345
|
-
defaultTimeoutMs,
|
|
346
|
-
stdoutCapBytes,
|
|
347
|
-
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
348
|
-
});
|
|
349
|
-
results.push(result);
|
|
350
|
-
if (result.status !== 'passed' && !continueOnError) {
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
finally {
|
|
356
|
-
if (options.signal) {
|
|
357
|
-
options.signal.removeEventListener('abort', onAbort);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
const overallEnd = Date.now();
|
|
361
|
-
const overallFailed = results.some((r) => r.status !== 'passed');
|
|
362
|
-
const overallStatus = overallFailed ? 'failed' : 'passed';
|
|
363
|
-
return {
|
|
364
|
-
startedAt: overallStart,
|
|
365
|
-
endedAt: overallEnd,
|
|
366
|
-
durationMs: overallEnd - overallStart,
|
|
367
|
-
overallStatus,
|
|
368
|
-
checks: results,
|
|
369
|
-
// summary / nextSuggestedAction 由 caller 写 (保持 runner 纯函数)
|
|
370
|
-
summary: '',
|
|
371
|
-
nextSuggestedAction: '',
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
async function runOneCheck(check, opts) {
|
|
375
|
-
const start = Date.now();
|
|
376
|
-
const timeoutMs = check.timeoutMs ?? opts.defaultTimeoutMs;
|
|
377
|
-
const cwd = check.cwd ?? opts.cwd;
|
|
378
|
-
const cap = opts.stdoutCapBytes;
|
|
379
|
-
return new Promise((resolve) => {
|
|
380
|
-
let stdoutBuf = Buffer.alloc(0);
|
|
381
|
-
let stderrBuf = Buffer.alloc(0);
|
|
382
|
-
let resolved = false;
|
|
383
|
-
let child = null;
|
|
384
|
-
let timer = null;
|
|
385
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: 独立于 runVerify 主循环的 aborted,
|
|
386
|
-
// 标 "当前 child 因外部 signal 被 kill". 避免跟 runVerify 的 "跳过下一 step" 语义混淆.
|
|
387
|
-
let childAborted = false;
|
|
388
|
-
// Sprint D-20.7.2 (2026-06-06): 标 timer-fired 状态. 不再在 timer fired 时
|
|
389
|
-
// 立刻 finalize, 改在 child.on('close') 判定 → 避免 Windows 上 child cwd
|
|
390
|
-
// 句柄没释放时 caller rmSync(workDir) EPERM.
|
|
391
|
-
let timedOut = false;
|
|
392
|
-
// Sprint D-20.7.7.1 (2026-06-06): hoist useShell 到外层, 让 try 块内 (line 363)
|
|
393
|
-
// 和 try 块**外**的 child.on('close') handler (line 524+) 都能闭包访问.
|
|
394
|
-
// 之前 try 块内 const, close handler 在 try 块外, ReferenceError. 拍板: useShell
|
|
395
|
-
// 是 "platform → boolean" 的纯计算, 无副作用, hoist 安全.
|
|
396
|
-
const useShell = process.platform === 'win32';
|
|
397
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: signal abort 触发的 1s grace timer,
|
|
398
|
-
// 提前到 finalize 前声明 (闭包共享). child 不响应 SIGTERM 时用它兜底 SIGKILL.
|
|
399
|
-
let sigkillTimer = null;
|
|
400
|
-
// Sprint 1c-revive-2-D-11+4 review P2 修复 (2026-06-05): 闭包持有的 "child 已
|
|
401
|
-
// 退出" 标记. child.on('close', ...) 里设 true. 用这个代替 Node 内置 child.killed,
|
|
402
|
-
// 因为 child.killed 表示"信号已发送" 不是"进程已退出" — 后者才是 grace timer
|
|
403
|
-
// 判断要不要发 SIGKILL 兜底的依据.
|
|
404
|
-
let childClosed = false;
|
|
405
|
-
// Sprint 1c-revive-3-D-12 review P3 修复 (2026-06-05, 基于 fea52d1 review):
|
|
406
|
-
// 提到外层, 让 finalize() 能 removeEventListener 兜底 listener leak.
|
|
407
|
-
// 之前 onAbort 是 if 块内的 const, finalize 拿不到引用; listener 在
|
|
408
|
-
// addEventListener { once: true } 时 fire 后自动移除, 但**未 fire** 走
|
|
409
|
-
// (正常 step 跑完 + finalize) 时 listener 永远挂 signal 上. 多 checks /
|
|
410
|
-
// 自定义 checks 链路下 listener 累积, 主流程影响小, 但顺手收干净.
|
|
411
|
-
// 声明 () => void 而非 Event listener 签名, 避免 onAbort() 调用 TS2554.
|
|
412
|
-
let onAbort = null;
|
|
413
|
-
const finalize = (result) => {
|
|
414
|
-
if (resolved)
|
|
415
|
-
return;
|
|
416
|
-
resolved = true;
|
|
417
|
-
if (timer)
|
|
418
|
-
clearTimeout(timer);
|
|
419
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: 清 sigkill grace timer, 避免
|
|
420
|
-
// child 已 close 后被 SIGKILL 错杀.
|
|
421
|
-
if (sigkillTimer) {
|
|
422
|
-
clearTimeout(sigkillTimer);
|
|
423
|
-
sigkillTimer = null;
|
|
424
|
-
}
|
|
425
|
-
if (opts.signal && onAbort) {
|
|
426
|
-
// Sprint 1c-revive-3-D-12 review P3 修复 (2026-06-05, 基于 fea52d1 review):
|
|
427
|
-
// 显式 removeEventListener 兜底. onAbort 在 spawn 成功后 addEventListener
|
|
428
|
-
// { once: true }, fire 后 Node 自动移除; 但**未 fire** 走 (正常 step
|
|
429
|
-
// 跑完 + finalize, 没触发外部 abort) 时 listener 永远挂 signal 上.
|
|
430
|
-
// 主流程影响小 (resolved guard 兜住重复 fire), 但多 checks 链路下
|
|
431
|
-
// listener 累积, 顺手收干净.
|
|
432
|
-
opts.signal.removeEventListener('abort', onAbort);
|
|
433
|
-
onAbort = null;
|
|
434
|
-
}
|
|
435
|
-
if (child && !childClosed) {
|
|
436
|
-
try {
|
|
437
|
-
child.kill('SIGKILL');
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
/* best-effort */
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
resolve(result);
|
|
444
|
-
};
|
|
445
|
-
try {
|
|
446
|
-
// 拍板 (D-11, 2026-06-04): 透传 args[0] 给 spawn, 不写死 corepack.
|
|
447
|
-
// 调用方可以传 'node' / 'bash' / 'corepack' / 任何 spawn 兼容的可执行.
|
|
448
|
-
// 4 步 default 用 corepack, 跟用户日常工作流一致.
|
|
449
|
-
// Sprint 1c-revive-2-D-11-4 review P1 修复: spawn 前用 resolveRunner 转换
|
|
450
|
-
// runner 字符串, Windows 上 'corepack' → 'corepack.cmd'.
|
|
451
|
-
const rawRunner = check.args[0];
|
|
452
|
-
if (typeof rawRunner !== 'string' || rawRunner.length === 0) {
|
|
453
|
-
throw new Error(`VerifyCheck '${check.name}' args[0] must be a non-empty string (the runner binary)`);
|
|
454
|
-
}
|
|
455
|
-
const runner = resolveRunner(rawRunner);
|
|
456
|
-
const subArgs = check.args.slice(1);
|
|
457
|
-
// useShell 必须在 try 块内声明, 但 close handler 在 try 块**外** (line 524+)
|
|
458
|
-
// 闭包共享. hoist 到 try 块前的外层 (line 335) 让两边都能访问. Sprint
|
|
459
|
-
// D-20.7.7.1 (2026-06-06): 修 useShell ReferenceError, 原代码 try 块内 const
|
|
460
|
-
// 闭包不外传, child.on('close') 报 'useShell is not defined' → 测全 fail.
|
|
461
|
-
// 不再重复声明.
|
|
462
|
-
child = spawn(runner, subArgs, {
|
|
463
|
-
cwd,
|
|
464
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
465
|
-
env: process.env, // 透传 env, 不注入任何东西 (loadProjectEnv 是 caller 职责)
|
|
466
|
-
shell: useShell,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
catch (e) {
|
|
470
|
-
// spawn 同步失败 (e.g. corepack 不在 PATH)
|
|
471
|
-
const end = Date.now();
|
|
472
|
-
finalize({
|
|
473
|
-
name: check.name,
|
|
474
|
-
command: check.command,
|
|
475
|
-
status: 'spawn-error',
|
|
476
|
-
exitCode: null,
|
|
477
|
-
startedAt: start,
|
|
478
|
-
endedAt: end,
|
|
479
|
-
durationMs: end - start,
|
|
480
|
-
stdoutTail: '',
|
|
481
|
-
stderrTail: '',
|
|
482
|
-
errorMessage: e instanceof Error ? e.message : String(e),
|
|
483
|
-
});
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: spawn 成功后立刻注册 signal 监听,
|
|
487
|
-
// 外部 abort 触发时:
|
|
488
|
-
// 1. 设 childAborted = true (close handler 用它返回 status='aborted')
|
|
489
|
-
// 2. kill 当前 child (SIGTERM 优先, 1s grace 后 SIGKILL 兜底, sigkillTimer 见 Promise 开头声明)
|
|
490
|
-
// 之前不传 signal 进 runOneCheck, 当前 child 不被 kill, 只能"等下 step 跳过"
|
|
491
|
-
// — 跟"取消不能卡住"目标不一致, 也会造成资源泄漏.
|
|
492
|
-
//
|
|
493
|
-
// Sprint 1c-revive-2-D-11+4 review P2 修复 (2026-06-05): 改用 childClosed 闭包
|
|
494
|
-
// 变量判断"child 真的退出了", 不再用 child.killed (Node 标记"信号已发", 跟
|
|
495
|
-
// "进程已退出" 语义不同). 否则 child 忽略 SIGTERM (e.g. 阻塞 IO / 自定义
|
|
496
|
-
// handler trap) 时, 1s 后 grace timer 判 !child.killed = false 跳过 SIGKILL,
|
|
497
|
-
// 子进程仍卡到 timeout 之前的所有步骤.
|
|
498
|
-
if (opts.signal) {
|
|
499
|
-
onAbort = () => {
|
|
500
|
-
if (resolved)
|
|
501
|
-
return;
|
|
502
|
-
childAborted = true;
|
|
503
|
-
if (child && !childClosed) {
|
|
504
|
-
try {
|
|
505
|
-
child.kill('SIGTERM');
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
508
|
-
/* best-effort */
|
|
509
|
-
}
|
|
510
|
-
// 1s grace: 给 child 清理时间. 真不响应 (e.g. blocking IO) → SIGKILL.
|
|
511
|
-
// close 完成后 finalize 会清掉这个 timer (见下), 避免错杀.
|
|
512
|
-
sigkillTimer = setTimeout(() => {
|
|
513
|
-
if (child && !childClosed) {
|
|
514
|
-
try {
|
|
515
|
-
child.kill('SIGKILL');
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
/* best-effort */
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}, 1000);
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
if (opts.signal.aborted) {
|
|
525
|
-
onAbort();
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
// 装 stdout/stderr 流, 保留 cap bytes 尾.
|
|
532
|
-
// 用 Buffer.concat 重建 (subarray 共享内存但类型 Buffer<ArrayBufferLike> 不赋给
|
|
533
|
-
// Buffer<ArrayBuffer>, ts 5.x 严格模式抓得到, 改 Buffer.from(merged) 重新建独立 Buffer)
|
|
534
|
-
const capAppend = (buf, chunk) => {
|
|
535
|
-
const merged = Buffer.concat([buf, chunk]);
|
|
536
|
-
if (merged.length <= cap)
|
|
537
|
-
return merged;
|
|
538
|
-
return Buffer.from(merged.subarray(merged.length - cap));
|
|
539
|
-
};
|
|
540
|
-
child.stdout?.on('data', (chunk) => {
|
|
541
|
-
stdoutBuf = capAppend(stdoutBuf, chunk);
|
|
542
|
-
});
|
|
543
|
-
child.stderr?.on('data', (chunk) => {
|
|
544
|
-
stderrBuf = capAppend(stderrBuf, chunk);
|
|
545
|
-
});
|
|
546
|
-
// Sprint D-20.7.2 (2026-06-06): timer fired 不再立刻 finalize.
|
|
547
|
-
// 之前直接 resolve → Windows 上 child cwd 句柄还占着, 测里 rmSync(workDir)
|
|
548
|
-
// EPERM. 修法: 标 timedOut=true, 调 child.kill, 等 'close' 才 finalize.
|
|
549
|
-
// 双重保险: 5s 后 child 还没 close, 走 grace kill + 再 finalize (避免永远 hang).
|
|
550
|
-
timer = setTimeout(() => {
|
|
551
|
-
timedOut = true;
|
|
552
|
-
try {
|
|
553
|
-
if (child && !child.killed) {
|
|
554
|
-
// Windows 上 SIGTERM 等价 kill 进程, Node 内部走 TerminateProcess.
|
|
555
|
-
child.kill('SIGTERM');
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
catch {
|
|
559
|
-
// best-effort; 如果 kill 失败, 走 grace timer SIGKILL 兜底
|
|
560
|
-
}
|
|
561
|
-
// grace timer: 5s 内 child 没 close → SIGKILL
|
|
562
|
-
sigkillTimer = setTimeout(() => {
|
|
563
|
-
try {
|
|
564
|
-
if (child && !childClosed) {
|
|
565
|
-
child.kill('SIGKILL');
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
// best-effort
|
|
570
|
-
}
|
|
571
|
-
// grace 仍 timeout 时强制 finalize, 避免 caller 永远 hang
|
|
572
|
-
const end = Date.now();
|
|
573
|
-
finalize({
|
|
574
|
-
name: check.name,
|
|
575
|
-
command: check.command,
|
|
576
|
-
status: 'timed-out',
|
|
577
|
-
exitCode: null,
|
|
578
|
-
startedAt: start,
|
|
579
|
-
endedAt: end,
|
|
580
|
-
durationMs: end - start,
|
|
581
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
582
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
583
|
-
errorMessage: `timeout after ${timeoutMs}ms (grace SIGKILL fired, child still not closed)`,
|
|
584
|
-
});
|
|
585
|
-
}, 5_000);
|
|
586
|
-
}, timeoutMs);
|
|
587
|
-
child.on('close', (code, signal) => {
|
|
588
|
-
// Sprint 1c-revive-2-D-11+4 review P2 修复 (2026-06-05): close 触发 = child 真退出.
|
|
589
|
-
// sigkillTimer 内部据此判断, 不再依赖 child.killed (Node 标记语义不对).
|
|
590
|
-
childClosed = true;
|
|
591
|
-
const end = Date.now();
|
|
592
|
-
// Sprint D-20.7.2 (2026-06-06): timeout 路径优先于 signal 判定. 如果 timer
|
|
593
|
-
// 先 fired, 我们 kill 了 child, 那 close 触发时 timedOut=true 应当报 'timed-out',
|
|
594
|
-
// 不报 'spawn-error' (SIGTERM 误报) 或 'aborted' (跟外部 signal 混淆).
|
|
595
|
-
// 同时清 sigkillTimer, 避免 grace 错杀.
|
|
596
|
-
if (timedOut) {
|
|
597
|
-
if (sigkillTimer) {
|
|
598
|
-
clearTimeout(sigkillTimer);
|
|
599
|
-
sigkillTimer = null;
|
|
600
|
-
}
|
|
601
|
-
finalize({
|
|
602
|
-
name: check.name,
|
|
603
|
-
command: check.command,
|
|
604
|
-
status: 'timed-out',
|
|
605
|
-
exitCode: code,
|
|
606
|
-
startedAt: start,
|
|
607
|
-
endedAt: end,
|
|
608
|
-
durationMs: end - start,
|
|
609
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
610
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
611
|
-
errorMessage: `timeout after ${timeoutMs}ms (killed, child closed)`,
|
|
612
|
-
});
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
// Sprint 1c-revive-2-D-11-4 review P2 修复: childAborted 优先, 返回 'aborted'
|
|
616
|
-
// 让 caller 知道是外部 signal 触发的 kill, 不是 child 自身崩溃.
|
|
617
|
-
if (childAborted) {
|
|
618
|
-
finalize({
|
|
619
|
-
name: check.name,
|
|
620
|
-
command: check.command,
|
|
621
|
-
status: 'aborted',
|
|
622
|
-
exitCode: code,
|
|
623
|
-
startedAt: start,
|
|
624
|
-
endedAt: end,
|
|
625
|
-
durationMs: end - start,
|
|
626
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
627
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
628
|
-
errorMessage: 'aborted by external signal during execution',
|
|
629
|
-
});
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
// signal 触发 kill → 'spawn-error' (用户主动取消)
|
|
633
|
-
if (signal === 'SIGKILL' || signal === 'SIGTERM') {
|
|
634
|
-
finalize({
|
|
635
|
-
name: check.name,
|
|
636
|
-
command: check.command,
|
|
637
|
-
status: 'spawn-error',
|
|
638
|
-
exitCode: code,
|
|
639
|
-
startedAt: start,
|
|
640
|
-
endedAt: end,
|
|
641
|
-
durationMs: end - start,
|
|
642
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
643
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
644
|
-
errorMessage: `killed by signal ${signal}`,
|
|
645
|
-
});
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
// Sprint D-20.7.7 (2026-06-06): Win32 shell:true 后, "命令不存在" 不再 sync
|
|
649
|
-
// spawn 抛, 而是 shell 跑完调不存在的 binary, exit=1 + stderr 'is not
|
|
650
|
-
// recognized'. 旧代码直接 'failed' 让语义边界乱: POSIX 走得到 'spawn-error',
|
|
651
|
-
// Win32 走不到. 修法: shell 用过的路径 (useShell=true) + 启错文本命中 →
|
|
652
|
-
// 'spawn-error', 跟 POSIX 行为对齐. 非 shell 路径不动 (Linux/macOS 默认
|
|
653
|
-
// shell:false, ENOENT 由 child.on('error') 接, 不会到这条分支).
|
|
654
|
-
//
|
|
655
|
-
// Sprint D-20.7.7.1 (2026-06-06): exitCode 归一为 null. POSIX 同步 spawn 抛
|
|
656
|
-
// (child.on('error') 路径) 不暴露 exit code (line 540+). Win32 shell 路径
|
|
657
|
-
// 现在也归一, 保持两路 shape 一致 — caller 不必区分 "启错 exit=1 vs 启错 sync 抛"
|
|
658
|
-
// 只看 status='spawn-error' + exitCode=null. D-20.7.7 初版留 code 实际值
|
|
659
|
-
// (e.g. 1), 测 expect null fail. 修法: 强制 null.
|
|
660
|
-
if (useShell &&
|
|
661
|
-
code !== 0 &&
|
|
662
|
-
looksLikeSpawnError(stdoutBuf.toString('utf8'), stderrBuf.toString('utf8'), code)) {
|
|
663
|
-
finalize({
|
|
664
|
-
name: check.name,
|
|
665
|
-
command: check.command,
|
|
666
|
-
status: 'spawn-error',
|
|
667
|
-
exitCode: null,
|
|
668
|
-
startedAt: start,
|
|
669
|
-
endedAt: end,
|
|
670
|
-
durationMs: end - start,
|
|
671
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
672
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
673
|
-
errorMessage: `command not found / not callable (shell detected: stderr contains "is not recognized" or "No such file")`,
|
|
674
|
-
});
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const status = code === 0 ? 'passed' : 'failed';
|
|
678
|
-
finalize({
|
|
679
|
-
name: check.name,
|
|
680
|
-
command: check.command,
|
|
681
|
-
status,
|
|
682
|
-
exitCode: code,
|
|
683
|
-
startedAt: start,
|
|
684
|
-
endedAt: end,
|
|
685
|
-
durationMs: end - start,
|
|
686
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
687
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
child.on('error', (e) => {
|
|
691
|
-
const end = Date.now();
|
|
692
|
-
finalize({
|
|
693
|
-
name: check.name,
|
|
694
|
-
command: check.command,
|
|
695
|
-
status: 'spawn-error',
|
|
696
|
-
exitCode: null,
|
|
697
|
-
startedAt: start,
|
|
698
|
-
endedAt: end,
|
|
699
|
-
durationMs: end - start,
|
|
700
|
-
stdoutTail: stdoutBuf.toString('utf8'),
|
|
701
|
-
stderrTail: stderrBuf.toString('utf8'),
|
|
702
|
-
errorMessage: e.message,
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
//# sourceMappingURL=verify-runner.js.map
|