@kira905/dsh-memory-toolkit 0.1.2 → 0.2.0
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/CHANGELOG.md +130 -1
- package/README.en.md +225 -2
- package/README.md +208 -2
- package/cordis.patch.yml +32 -0
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +11 -1
package/lib/deploy-guard.mjs
CHANGED
|
@@ -1,192 +1,318 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 部署前的三道机械闸:参数白名单、逃逸 import 静态扫、按部署位置冒烟 import。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// 同口径的共享实现,供其余 deploy.mjs 复用。
|
|
3
|
+
// 为什么非得有这三道(一次真实事故换来的):
|
|
4
|
+
// ① 某个部署脚本收到一个它不认识的参数,静默忽略了 —— 操作者以为在跑只读检查,
|
|
5
|
+
// 脚本却照旧做了真备份、真写入,还顺手触发了重启。→ 参数必须 fail-closed。
|
|
6
|
+
// ② 同一批被装进去的模块用了相对路径跨包 import:在仓库里层级恰好对得上,装到目标
|
|
7
|
+
// 位置后就指向不存在的邻居包,宿主一启动就报模块找不到,连崩几次只能靠自愈拉起。
|
|
8
|
+
// → `node --check` 只查语法,查不出**解析期**失败,得按部署位置真 import 一遍。
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
// parseArgs(argv, known)
|
|
13
|
-
// findEscapingImports(srcDir, opts)
|
|
14
|
-
// smokeImportAll(entityDir, libSpec)
|
|
10
|
+
// 三个导出(纯函数为主,只有冒烟那一步会起子进程):
|
|
11
|
+
// parseArgs(argv, known) 白名单解析参数,认不出的原样报出来
|
|
12
|
+
// findEscapingImports(srcDir, opts) 静态扫"相对 import 解析后跑出实体根"的写法
|
|
13
|
+
// smokeImportAll(entityDir, libSpec) 以部署位置为解析上下文,逐个模块真 import
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
// 0 = 通过 · 1 = 门禁未过(拒绝部署)· 2 = 用法/参数错误
|
|
15
|
+
// 退出码约定(调用方照抄):0 = 通过 · 1 = 门禁没过 · 2 = 用法/参数错误
|
|
18
16
|
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
import
|
|
17
|
+
// 本文件不写盘、不联网;唯一的外部动作是冒烟时起的那些子进程。
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
20
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
22
21
|
import { spawnSync } from 'node:child_process';
|
|
23
22
|
import { pathToFileURL } from 'node:url';
|
|
24
23
|
|
|
25
|
-
export const EXIT = {
|
|
24
|
+
export const EXIT = {
|
|
25
|
+
OK: 0,
|
|
26
|
+
GATE: 1,
|
|
27
|
+
USAGE: 2,
|
|
28
|
+
};
|
|
26
29
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
'node_modules', '.git', '.hg', '.svn', '_archive', '_tmp',
|
|
31
|
-
'
|
|
30
|
+
// 递归时永远不进的目录名:依赖闭包、版本库元数据、历史与临时产物、构建产物。
|
|
31
|
+
// 依赖目录在这台机器上常常是链接,跟进去会跑到盘外扫几十个包。
|
|
32
|
+
const SKIP_DIR_NAMES = new Set([
|
|
33
|
+
'node_modules', '.git', '.hg', '.svn', '_archive', '_tmp',
|
|
34
|
+
'dist', 'build', 'coverage', '.cache', '.dsh', '.venv',
|
|
35
|
+
'__pycache__', 'snapshots', 'backups',
|
|
32
36
|
]);
|
|
33
|
-
const JS_EXT = new Set(['.mjs', '.js', '.cjs']);
|
|
34
|
-
// 前端产物:浏览器 bundle 本来就不该在 node 里 import(真 import 必报 window is not defined)。
|
|
35
|
-
const DEFAULT_SMOKE_SKIP = [/^client\.js$/, /\.browser\.js$/, /\.min\.js$/];
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// 参与扫描的扩展名
|
|
39
|
+
const CODE_EXTS = new Set(['.mjs', '.js', '.cjs']);
|
|
40
|
+
|
|
41
|
+
// 冒烟时默认跳过的文件名(浏览器 bundle 在 node 里 import 必然报错,不该算部署失败)
|
|
42
|
+
const SKIP_SMOKE_FILES = [
|
|
43
|
+
/^client\.js$/,
|
|
44
|
+
/\.browser\.js$/,
|
|
45
|
+
/\.min\.js$/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// ── 1) 参数白名单 ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
// 编译白名单:字符串 = 布尔开关;对象 = 可取值开关(`--src X` 或 `--src=X` 都认)。
|
|
51
|
+
// 表用无原型对象 —— token 叫 `--constructor` 之类的时候不会撞上继承来的属性。
|
|
52
|
+
function compileSpec(known) {
|
|
53
|
+
const table = Object.create(null);
|
|
54
|
+
for (const item of known) {
|
|
55
|
+
if (typeof item === 'string') {
|
|
56
|
+
table[item] = { wantsValue: false };
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!item || !item.flag) continue;
|
|
60
|
+
const wantsValue = Boolean(item.takesValue);
|
|
61
|
+
table[item.flag] = { wantsValue };
|
|
62
|
+
if (item.alias) table[item.alias] = { wantsValue, aliasOf: item.flag };
|
|
63
|
+
}
|
|
64
|
+
return table;
|
|
65
|
+
}
|
|
38
66
|
|
|
39
67
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {string[]} argv
|
|
42
|
-
* @param {Array<string|{flag:string, takesValue?:boolean, alias?:string}>} known
|
|
43
|
-
* 字符串 = 布尔开关;对象 =
|
|
68
|
+
* 解析命令行参数,只认白名单里的 token。
|
|
69
|
+
* @param {string[]} argv 一般是 process.argv.slice(2)
|
|
70
|
+
* @param {Array<string|{flag:string, takesValue?:boolean, alias?:string}>} known
|
|
71
|
+
* 字符串 = 布尔开关;对象 = 可取值参数(同时可带 alias)
|
|
44
72
|
* @returns {{ok:boolean, unknown:string[], flags:Record<string,boolean>,
|
|
45
73
|
* values:Record<string,string>, positionals:string[]}}
|
|
46
|
-
* ok=false 时 unknown
|
|
74
|
+
* ok=false 时 unknown 里是全部不认识的 token(调用方负责打出来并以 rc=2 退出)
|
|
47
75
|
*/
|
|
48
76
|
export function parseArgs(argv, known = []) {
|
|
49
|
-
const spec =
|
|
50
|
-
for (const k of known) {
|
|
51
|
-
if (typeof k === 'string') spec.set(k, { takesValue: false });
|
|
52
|
-
else if (k && k.flag) {
|
|
53
|
-
spec.set(k.flag, { takesValue: !!k.takesValue });
|
|
54
|
-
if (k.alias) spec.set(k.alias, { takesValue: !!k.takesValue, aliasOf: k.flag });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
77
|
+
const spec = compileSpec(known);
|
|
57
78
|
const flags = {};
|
|
58
79
|
const values = {};
|
|
59
80
|
const unknown = [];
|
|
60
81
|
const positionals = [];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
let at = 0;
|
|
83
|
+
|
|
84
|
+
while (at < argv.length) {
|
|
85
|
+
const token = argv[at];
|
|
86
|
+
|
|
87
|
+
// 首字符不是 '-' 就是位置参数:这几个脚本一个位置参数都不吃,静默吞掉正是事故形态
|
|
88
|
+
if (token.charCodeAt(0) !== 45) {
|
|
89
|
+
positionals.push(token);
|
|
90
|
+
at += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const eq = token.indexOf('=');
|
|
95
|
+
const head = eq > 0 ? token.slice(0, eq) : token;
|
|
96
|
+
const entry = spec[head];
|
|
97
|
+
if (!entry) {
|
|
98
|
+
unknown.push(token);
|
|
99
|
+
at += 1;
|
|
100
|
+
continue;
|
|
78
101
|
}
|
|
102
|
+
|
|
103
|
+
const canonical = entry.aliasOf ?? head;
|
|
104
|
+
|
|
105
|
+
if (entry.wantsValue) {
|
|
106
|
+
let value = eq > 0 ? token.slice(eq + 1) : null;
|
|
107
|
+
if (value === null) {
|
|
108
|
+
value = argv[at + 1] ?? null;
|
|
109
|
+
at += 1; // 取值同样消费掉下一个 token(与老行为一致,别改成"留着继续解析")
|
|
110
|
+
}
|
|
111
|
+
values[canonical] = value;
|
|
112
|
+
flags[canonical] = true;
|
|
113
|
+
at += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (eq > 0) {
|
|
118
|
+
// 布尔开关带 =值 是写错了,要响一声,不能当开关静默放行
|
|
119
|
+
unknown.push(token);
|
|
120
|
+
at += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
flags[canonical] = true;
|
|
125
|
+
at += 1;
|
|
79
126
|
}
|
|
80
|
-
|
|
127
|
+
|
|
81
128
|
unknown.push(...positionals);
|
|
82
129
|
return { ok: unknown.length === 0, unknown, flags, values, positionals };
|
|
83
130
|
}
|
|
84
131
|
|
|
85
|
-
/**
|
|
132
|
+
/** 把不认识的参数渲染成人话(调用方后面自己接"只读模式请用 X"的尾巴) */
|
|
86
133
|
export function describeUnknown(unknown, known) {
|
|
87
|
-
const
|
|
88
|
-
return `不认识的参数:${unknown.join(' ')}(本脚本只认 ${
|
|
134
|
+
const allowed = known.map((item) => (typeof item === 'string' ? item : item.flag)).join(' ');
|
|
135
|
+
return `不认识的参数:${unknown.join(' ')}(本脚本只认 ${allowed})`;
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
//
|
|
138
|
+
// ── 2) 路径归一化 ────────────────────────────────────────────────────────────
|
|
92
139
|
|
|
93
140
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
141
|
+
* 归一化:能解析到的部分取真实路径,解析不到的尾巴接回去。
|
|
142
|
+
* 两边都得归一 —— 工作区里目录链接很常见,只归一一边会把同一棵树比成"越过上级",
|
|
143
|
+
* 于是合法的相邻 import 也被判成逃逸(踩过)。
|
|
144
|
+
* @param {string} target
|
|
145
|
+
* @returns {string}
|
|
97
146
|
*/
|
|
98
|
-
export function normalizePath(
|
|
99
|
-
|
|
100
|
-
|
|
147
|
+
export function normalizePath(target) {
|
|
148
|
+
const unresolved = [];
|
|
149
|
+
let probe = resolve(target);
|
|
150
|
+
|
|
101
151
|
for (;;) {
|
|
152
|
+
let real = null;
|
|
102
153
|
try {
|
|
103
|
-
|
|
104
|
-
return tail.length ? path.join(real, ...tail.reverse()) : real;
|
|
154
|
+
real = realpathSync.native ? realpathSync.native(probe) : realpathSync(probe);
|
|
105
155
|
} catch {
|
|
106
|
-
|
|
107
|
-
if (parent === cur) return path.resolve(p);
|
|
108
|
-
tail.push(path.basename(cur));
|
|
109
|
-
cur = parent;
|
|
156
|
+
real = null;
|
|
110
157
|
}
|
|
158
|
+
if (real !== null) {
|
|
159
|
+
if (!unresolved.length) return real;
|
|
160
|
+
return join(real, ...unresolved.reverse());
|
|
161
|
+
}
|
|
162
|
+
const parent = dirname(probe);
|
|
163
|
+
if (parent === probe) return resolve(target);
|
|
164
|
+
unresolved.push(basename(probe));
|
|
165
|
+
probe = parent;
|
|
111
166
|
}
|
|
112
167
|
}
|
|
113
168
|
|
|
169
|
+
// ── 3) 去注释 ────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
114
171
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
172
|
+
* 把注释抹成空格(换行与列位置都保留),引号里的内容原样留着。
|
|
173
|
+
* 为什么必须抹:注释里常写着示例路径,不抹掉静态扫会把它当真 import。
|
|
174
|
+
* 状态机够用就行:`//`、块注释整段变空格;`'…'` / `"…"` / 反引号串内不动
|
|
175
|
+
* (模板串里的 `${}` 不再细分 —— 漏掉的情况由后面那道真 import 兜底)。
|
|
176
|
+
* @param {string} text
|
|
177
|
+
* @returns {string}
|
|
118
178
|
*/
|
|
119
179
|
export function stripComments(text) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
let
|
|
123
|
-
let
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
const kept = [];
|
|
181
|
+
const length = text.length;
|
|
182
|
+
let pos = 0;
|
|
183
|
+
let mode = 'code';
|
|
184
|
+
let quoteChar = '';
|
|
185
|
+
|
|
186
|
+
while (pos < length) {
|
|
187
|
+
const ch = text[pos];
|
|
188
|
+
|
|
189
|
+
if (mode === 'block') {
|
|
190
|
+
if (ch === '*' && text[pos + 1] === '/') {
|
|
191
|
+
kept.push(' ');
|
|
192
|
+
pos += 2;
|
|
193
|
+
mode = 'code';
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
kept.push(ch === '\n' ? '\n' : ' ');
|
|
197
|
+
pos += 1;
|
|
131
198
|
continue;
|
|
132
199
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
|
|
201
|
+
if (mode === 'line') {
|
|
202
|
+
if (ch === '\n') {
|
|
203
|
+
mode = 'code';
|
|
204
|
+
kept.push(ch);
|
|
205
|
+
pos += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
kept.push(' ');
|
|
209
|
+
pos += 1;
|
|
138
210
|
continue;
|
|
139
211
|
}
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
212
|
+
|
|
213
|
+
if (mode === 'quote') {
|
|
214
|
+
if (ch === '\\') {
|
|
215
|
+
kept.push(ch, text[pos + 1] ?? '');
|
|
216
|
+
pos += 2;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
kept.push(ch);
|
|
220
|
+
pos += 1;
|
|
221
|
+
if (ch === quoteChar) {
|
|
222
|
+
mode = 'code';
|
|
223
|
+
quoteChar = '';
|
|
224
|
+
}
|
|
143
225
|
continue;
|
|
144
226
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
227
|
+
|
|
228
|
+
if (ch === '/' && text[pos + 1] === '*') {
|
|
229
|
+
kept.push(' ');
|
|
230
|
+
pos += 2;
|
|
231
|
+
mode = 'block';
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (ch === '/' && text[pos + 1] === '/') {
|
|
235
|
+
kept.push(' ');
|
|
236
|
+
pos += 2;
|
|
237
|
+
mode = 'line';
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
241
|
+
quoteChar = ch;
|
|
242
|
+
kept.push(ch);
|
|
243
|
+
pos += 1;
|
|
244
|
+
mode = 'quote';
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
kept.push(ch);
|
|
248
|
+
pos += 1;
|
|
148
249
|
}
|
|
149
|
-
|
|
250
|
+
|
|
251
|
+
return kept.join('');
|
|
150
252
|
}
|
|
151
253
|
|
|
152
|
-
//
|
|
153
|
-
|
|
254
|
+
// 只认四种说明符形态:`import/export … from '…'`、`import '…'`、`import('…')`、`require('…')`。
|
|
255
|
+
// 前面那个字符类是为了躲开 `x.import(...)`、`$require(...)` 这类属性名/标识符写法。
|
|
256
|
+
const IMPORT_PATTERNS = [
|
|
154
257
|
/(?:^|[^\w$.])(?:import|export)\b[^;]*?\bfrom\s*(['"])([^'"]+)\1/,
|
|
155
258
|
/(?:^|[^\w$.])import\s*(['"])([^'"]+)\1/,
|
|
156
259
|
/(?:^|[^\w$.])import\s*\(\s*(['"])([^'"]+)\1\s*\)/,
|
|
157
260
|
/(?:^|[^\w$.])require\s*\(\s*(['"])([^'"]+)\1\s*\)/,
|
|
158
261
|
];
|
|
159
262
|
|
|
160
|
-
|
|
263
|
+
// 一行里出现的全部说明符(同一行同一个只记一次,顺序按上面四种形态的先后)
|
|
264
|
+
function moduleSpecsOf(line) {
|
|
265
|
+
const specs = [];
|
|
266
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
267
|
+
const matched = pattern.exec(line);
|
|
268
|
+
if (!matched || !matched[2]) continue;
|
|
269
|
+
if (!specs.includes(matched[2])) specs.push(matched[2]);
|
|
270
|
+
}
|
|
271
|
+
return specs;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 列出目录下全部 js/mjs/cjs(递归、排序、跳过黑名单目录与链接)。
|
|
276
|
+
* @param {string} root
|
|
277
|
+
* @param {{excludeDirs?:Set<string>, exts?:Set<string>}} [opts]
|
|
278
|
+
* @returns {string[]} 绝对路径,字典序
|
|
279
|
+
*/
|
|
161
280
|
export function listJsFiles(root, opts = {}) {
|
|
162
|
-
const { excludeDirs =
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
281
|
+
const { excludeDirs = SKIP_DIR_NAMES, exts = CODE_EXTS } = opts;
|
|
282
|
+
const found = [];
|
|
283
|
+
const pending = [root];
|
|
284
|
+
|
|
285
|
+
while (pending.length > 0) {
|
|
286
|
+
const dir = pending.pop();
|
|
287
|
+
let entries;
|
|
288
|
+
try {
|
|
289
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
290
|
+
} catch {
|
|
291
|
+
continue; // 读不动的目录跳过,不影响其余
|
|
292
|
+
}
|
|
293
|
+
for (const dent of entries) {
|
|
294
|
+
// 链接/junction 一律不跟随:跨盘闭包、盘外目录都在它后面
|
|
295
|
+
if (dent.isSymbolicLink()) continue;
|
|
296
|
+
const full = join(dir, dent.name);
|
|
297
|
+
if (dent.isDirectory()) {
|
|
298
|
+
if (!excludeDirs.has(dent.name)) pending.push(full);
|
|
299
|
+
continue;
|
|
174
300
|
}
|
|
301
|
+
if (exts.has(extname(dent.name))) found.push(full);
|
|
175
302
|
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return found.sort();
|
|
179
306
|
}
|
|
180
307
|
|
|
181
308
|
/**
|
|
182
|
-
* 静态扫:srcDir 下 js/mjs/cjs
|
|
183
|
-
* @param {string} srcDir 扫描起点 ——
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* @param {{root?:string}} [opts] root =
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* 跨目录引用的正解是 `file:///<绝对路径>` URL(绝对路径不计逃逸)。
|
|
309
|
+
* 静态扫:srcDir 下 js/mjs/cjs 里"相对 import 解析后跑出 root"的写法。
|
|
310
|
+
* @param {string} srcDir 扫描起点 —— 只该扫「会被复制进实体的那部分」(通常是 lib/)。
|
|
311
|
+
* 传整个插件根会把部署/回滚脚本一起扫进来,它们向上引用是合法姿势、又根本不进
|
|
312
|
+
* 目标位置,只会自报假警(踩过)。
|
|
313
|
+
* @param {{root?:string}} [opts] root = 逃逸判定的边界,即实体被装好之后的那层目录。
|
|
314
|
+
* 原地部署、整棵树一起搬的插件可以放宽到树根。两侧都走 normalizePath(链接坑)。
|
|
315
|
+
* 跨目录引用的正解是用 `file:///…` 绝对 URL —— 绝对路径不计逃逸。
|
|
190
316
|
* @returns {{root:string, scanned:number, specs:number, absolute:number,
|
|
191
317
|
* bad:Array<{file:string,line:number,spec:string,resolved:string,text:string}>}}
|
|
192
318
|
*/
|
|
@@ -195,197 +321,259 @@ export function findEscapingImports(srcDir, opts = {}) {
|
|
|
195
321
|
const rootReal = normalizePath(root);
|
|
196
322
|
const files = listJsFiles(srcDir, opts);
|
|
197
323
|
const bad = [];
|
|
198
|
-
let
|
|
199
|
-
let
|
|
324
|
+
let relativeSpecs = 0;
|
|
325
|
+
let absoluteSpecs = 0;
|
|
326
|
+
|
|
200
327
|
for (const file of files) {
|
|
201
|
-
let raw
|
|
202
|
-
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
219
|
-
bad.push({
|
|
220
|
-
file: path.relative(srcDir, file),
|
|
221
|
-
line: i + 1,
|
|
222
|
-
spec,
|
|
223
|
-
resolved,
|
|
224
|
-
text: raw.split(/\r?\n/)[i].trim().slice(0, 200),
|
|
225
|
-
});
|
|
328
|
+
let raw;
|
|
329
|
+
try {
|
|
330
|
+
raw = readFileSync(file, 'utf8');
|
|
331
|
+
} catch {
|
|
332
|
+
continue; // 读不动就跳过(权限/竞态),不猜
|
|
333
|
+
}
|
|
334
|
+
const rawLines = raw.split(/\r?\n/);
|
|
335
|
+
const codeLines = stripComments(raw).split(/\r?\n/);
|
|
336
|
+
|
|
337
|
+
for (let index = 0; index < codeLines.length; index += 1) {
|
|
338
|
+
const line = codeLines[index];
|
|
339
|
+
if (!/import|require/.test(line)) continue;
|
|
340
|
+
|
|
341
|
+
for (const spec of moduleSpecsOf(line)) {
|
|
342
|
+
if (isAbsolute(spec)) {
|
|
343
|
+
absoluteSpecs += 1; // 绝对路径是推荐的跨目录姿势,不算逃逸
|
|
344
|
+
continue;
|
|
226
345
|
}
|
|
346
|
+
if (!spec.startsWith('.')) continue; // 裸包名交给 node 自己的解析
|
|
347
|
+
relativeSpecs += 1;
|
|
348
|
+
|
|
349
|
+
const resolved = resolve(dirname(file), spec);
|
|
350
|
+
const inside = relative(rootReal, normalizePath(resolved));
|
|
351
|
+
if (!inside.startsWith('..') && !isAbsolute(inside)) continue;
|
|
352
|
+
|
|
353
|
+
bad.push({
|
|
354
|
+
file: relative(srcDir, file),
|
|
355
|
+
line: index + 1,
|
|
356
|
+
spec,
|
|
357
|
+
resolved,
|
|
358
|
+
text: rawLines[index].trim().slice(0, 200),
|
|
359
|
+
});
|
|
227
360
|
}
|
|
228
361
|
}
|
|
229
362
|
}
|
|
230
|
-
|
|
363
|
+
|
|
364
|
+
return { root, scanned: files.length, specs: relativeSpecs, absolute: absoluteSpecs, bad };
|
|
231
365
|
}
|
|
232
366
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"if (!process.exitCode && process.env.DG_EXIT_AFTER === '1') process.exit(0);",
|
|
262
|
-
"",
|
|
263
|
-
].join('\n');
|
|
264
|
-
|
|
265
|
-
function spawnSmoke(file, timeoutMs, exitAfter) {
|
|
266
|
-
return spawnSync(process.execPath, ['--input-type=module'], {
|
|
267
|
-
input: SMOKE_CODE,
|
|
268
|
-
encoding: 'utf8',
|
|
269
|
-
timeout: timeoutMs,
|
|
270
|
-
windowsHide: true,
|
|
271
|
-
env: {
|
|
272
|
-
...process.env,
|
|
273
|
-
DG_TARGET_URL: pathToFileURL(file).href,
|
|
274
|
-
DG_EXIT_AFTER: exitAfter ? '1' : '0',
|
|
275
|
-
},
|
|
276
|
-
});
|
|
367
|
+
// ── 4) 按部署位置冒烟 import(子进程隔离) ──────────────────────────────────
|
|
368
|
+
|
|
369
|
+
// 子进程里跑的两段脚本:代码走 stdin、路径走环境变量 —— 省掉引号与转义那一堆坑。
|
|
370
|
+
// 批量版一次进程 import 全部模块,结果打成一行、带前缀(为了容忍被 import 的模块自己
|
|
371
|
+
// 往 stdout 打印日志)。
|
|
372
|
+
const BATCH_SNIPPET = `
|
|
373
|
+
const files = JSON.parse(process.env.DG_BATCH_FILES || '[]');
|
|
374
|
+
const report = [];
|
|
375
|
+
for (const item of files) {
|
|
376
|
+
try {
|
|
377
|
+
await import(item.url);
|
|
378
|
+
report.push({ file: item.file, ok: true });
|
|
379
|
+
} catch (cause) {
|
|
380
|
+
report.push({ file: item.file, ok: false, err: String((cause && (cause.message || cause)) || cause) });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
process.stdout.write('\\nDG_RESULT:' + JSON.stringify(report) + '\\n');
|
|
384
|
+
if (process.env.DG_EXIT_AFTER === '1') process.exit(0);
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
// 单模块版:失败时精确定位到文件,并把"解析失败"与"解析成功但进程不退出"分开。
|
|
388
|
+
const SINGLE_SNIPPET = `
|
|
389
|
+
const target = process.env.DG_TARGET_URL;
|
|
390
|
+
try {
|
|
391
|
+
await import(target);
|
|
392
|
+
} catch (cause) {
|
|
393
|
+
process.stderr.write('DG_IMPORT_FAIL: ' + String((cause && (cause.message || cause)) || cause));
|
|
394
|
+
process.exitCode = 1;
|
|
277
395
|
}
|
|
396
|
+
if (!process.exitCode && process.env.DG_EXIT_AFTER === '1') process.exit(0);
|
|
397
|
+
`;
|
|
278
398
|
|
|
279
|
-
|
|
399
|
+
const BATCH_MARKER = 'DG_RESULT:';
|
|
400
|
+
const FAILURE_MARKER = 'DG_IMPORT_FAIL: ';
|
|
401
|
+
|
|
402
|
+
// 起一个子进程跑 stdin 脚本:超时、隐藏窗口、环境变量拼装都收在这一处
|
|
403
|
+
function runChildScript(script, extraEnv, timeoutMs) {
|
|
280
404
|
return spawnSync(process.execPath, ['--input-type=module'], {
|
|
281
|
-
input:
|
|
405
|
+
input: script,
|
|
282
406
|
encoding: 'utf8',
|
|
283
407
|
timeout: timeoutMs,
|
|
284
408
|
windowsHide: true,
|
|
285
|
-
env: {
|
|
286
|
-
...process.env,
|
|
287
|
-
DG_BATCH_FILES: JSON.stringify(items),
|
|
288
|
-
DG_EXIT_AFTER: '1', // 批量模式只回答「能否全部解析」,import 完立刻收尾(免被定时器拖住)
|
|
289
|
-
},
|
|
409
|
+
env: { ...process.env, ...extraEnv },
|
|
290
410
|
});
|
|
291
411
|
}
|
|
292
412
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
413
|
+
function probeModule(file, timeoutMs, exitAfterImport) {
|
|
414
|
+
return runChildScript(SINGLE_SNIPPET, {
|
|
415
|
+
DG_TARGET_URL: pathToFileURL(file).href,
|
|
416
|
+
DG_EXIT_AFTER: exitAfterImport ? '1' : '0',
|
|
417
|
+
}, timeoutMs);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function probeBatch(items, timeoutMs) {
|
|
421
|
+
return runChildScript(BATCH_SNIPPET, {
|
|
422
|
+
DG_BATCH_FILES: JSON.stringify(items),
|
|
423
|
+
DG_EXIT_AFTER: '1', // 批量只回答"能不能全部解析",import 完立刻收尾(别被定时器拖住)
|
|
424
|
+
}, timeoutMs);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 从(可能混着模块日志的)stdout 里取最后一条批量结果
|
|
428
|
+
function readBatchReport(stdout) {
|
|
429
|
+
const text = stdout ?? '';
|
|
430
|
+
const started = text.lastIndexOf(BATCH_MARKER);
|
|
431
|
+
if (started < 0) return null;
|
|
432
|
+
const payload = text.slice(started + BATCH_MARKER.length).split(/\r?\n/)[0];
|
|
433
|
+
let parsed;
|
|
299
434
|
try {
|
|
300
|
-
|
|
301
|
-
return Array.isArray(arr) ? arr : null;
|
|
435
|
+
parsed = JSON.parse(payload);
|
|
302
436
|
} catch {
|
|
303
437
|
return null;
|
|
304
438
|
}
|
|
439
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 从 stderr 里取第一行有效错误信息(截到 300 字,够定位就行)
|
|
443
|
+
function readFailureText(stderr) {
|
|
444
|
+
const text = stderr ?? '';
|
|
445
|
+
const started = text.indexOf(FAILURE_MARKER);
|
|
446
|
+
const body = started >= 0 ? text.slice(started + FAILURE_MARKER.length) : text;
|
|
447
|
+
const firstLine = body.split(/\r?\n/).find((row) => row.trim()) ?? '';
|
|
448
|
+
return firstLine.trim().slice(0, 300);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 快路径:整批 import。判据从严 —— 只有"整批都说 ok"才算通过,否则交给慢路径确认。
|
|
452
|
+
// 理由:批量只会误报失败(模块之间互相干扰),不会误报成功(每个模块都真 import 过)。
|
|
453
|
+
function attemptBatch(files, libDir, timeoutMs) {
|
|
454
|
+
const manifest = files.map((file) => ({
|
|
455
|
+
file: relative(libDir, file),
|
|
456
|
+
url: pathToFileURL(file).href,
|
|
457
|
+
}));
|
|
458
|
+
const run = probeBatch(manifest, timeoutMs);
|
|
459
|
+
const report = readBatchReport(run.stdout);
|
|
460
|
+
if (!report || report.length !== files.length) return null;
|
|
461
|
+
if (!report.every((row) => row.ok)) return null;
|
|
462
|
+
return report.map((row) => ({ file: row.file, ok: true, ms: 0, hung: null }));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 慢路径:一个模块一个进程。三态 —— 通过 / 能解析但把事件循环挂住了 / 解析失败(带原因)
|
|
466
|
+
function inspectModule(file, timeoutMs) {
|
|
467
|
+
const first = probeModule(file, timeoutMs, false);
|
|
468
|
+
if (!(first.error || first.signal)) {
|
|
469
|
+
if (first.status === 0) return { resolved: true, hung: false };
|
|
470
|
+
return {
|
|
471
|
+
resolved: false,
|
|
472
|
+
timeout: false,
|
|
473
|
+
err: readFailureText(first.stderr ?? '') || `exit ${first.status}`,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 第一次没跑完未必是解析失败:模块 import 成功后也可能挂着定时器/监听器。
|
|
478
|
+
// 让它 import 完主动收尾再试一次,两种原因就分开了。
|
|
479
|
+
const second = probeModule(file, timeoutMs, true);
|
|
480
|
+
if (second.status === 0) return { resolved: true, hung: true };
|
|
481
|
+
if (!(second.error || second.signal)) {
|
|
482
|
+
return {
|
|
483
|
+
resolved: false,
|
|
484
|
+
timeout: false,
|
|
485
|
+
err: readFailureText(second.stderr ?? '') || `exit ${second.status}`,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 两次都没跑完:仍按"卡住"报,附带第一次拿到的信号/错误码
|
|
490
|
+
return {
|
|
491
|
+
resolved: false,
|
|
492
|
+
timeout: true,
|
|
493
|
+
err: `${first.error?.code ?? first.signal ?? '超时'}:${timeoutMs}ms 内未结束(import 后进程既不退出也不报错)`,
|
|
494
|
+
};
|
|
305
495
|
}
|
|
306
496
|
|
|
307
497
|
/**
|
|
308
|
-
*
|
|
309
|
-
* 关键点:import
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* @param {string} entityDir
|
|
498
|
+
* 以**部署位置**为解析上下文,把 lib 里的模块逐个 import 一遍。
|
|
499
|
+
* 关键点:import 的是实体目录里的那个文件(file URL),模块自己的相对 import 才会按
|
|
500
|
+
* 部署后的位置解析 —— 这正是"仓库里跑得通、装到目标位置就崩"的暴露面。
|
|
501
|
+
* 每个模块单独一个子进程:模块有副作用或挂着事件循环,也污染不到部署进程。
|
|
502
|
+
* @param {string} entityDir 部署实体目录(必须真实存在;原地部署的插件传源码目录自身)
|
|
313
503
|
* @param {string|{dir?:string, skip?:RegExp[], timeoutMs?:number}} [libSpec='lib']
|
|
314
|
-
* @param {{dir?:string, skip?:RegExp[], timeoutMs?:number, only?:RegExp
|
|
315
|
-
*
|
|
316
|
-
*
|
|
504
|
+
* @param {{dir?:string, skip?:RegExp[], timeoutMs?:number, only?:RegExp,
|
|
505
|
+
* batchTimeoutMs?:number, batch?:boolean}} [opts]
|
|
506
|
+
* @returns {{dir:string, error:string|null, mode:string|null, total:number, skipped:number,
|
|
507
|
+
* bad:Array<{file:string,err:string,timeout:boolean}>,
|
|
508
|
+
* results:Array<{file:string,ok:boolean,ms:number,hung:boolean|null}>}}
|
|
317
509
|
*/
|
|
318
510
|
export function smokeImportAll(entityDir, libSpec = 'lib', opts = {}) {
|
|
319
511
|
const cfg = typeof libSpec === 'string' ? { ...opts, dir: libSpec } : { ...opts, ...libSpec };
|
|
320
512
|
const {
|
|
321
|
-
skip =
|
|
322
|
-
|
|
513
|
+
skip = SKIP_SMOKE_FILES,
|
|
514
|
+
timeoutMs = 20000,
|
|
515
|
+
batchTimeoutMs = 90000,
|
|
516
|
+
batch = true,
|
|
517
|
+
only = null,
|
|
323
518
|
} = cfg;
|
|
324
|
-
|
|
325
|
-
const
|
|
519
|
+
|
|
520
|
+
const entityRoot = resolve(entityDir);
|
|
521
|
+
const requestedDir = cfg.dir ?? 'lib';
|
|
522
|
+
const libDir = isAbsolute(requestedDir) ? requestedDir : join(entityRoot, requestedDir);
|
|
326
523
|
const empty = { dir: libDir, error: null, mode: null, total: 0, skipped: 0, bad: [], results: [] };
|
|
327
524
|
|
|
328
|
-
if (!
|
|
329
|
-
if (!
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
525
|
+
if (!existsSync(entityRoot)) return { ...empty, error: `实体目录不存在:${entityRoot}` };
|
|
526
|
+
if (!existsSync(libDir)) return { ...empty, error: `实体下没有 lib 目录:${libDir}` };
|
|
527
|
+
|
|
528
|
+
// 解析上下文必须落在实体内部:lib 跑到实体外面,等于拿源码目录冒充部署位置
|
|
529
|
+
const containment = relative(normalizePath(entityRoot), normalizePath(libDir));
|
|
530
|
+
if (containment.startsWith('..') || isAbsolute(containment)) {
|
|
531
|
+
return { ...empty, error: `lib 目录不在实体目录内(解析上下文错位):${libDir}` };
|
|
334
532
|
}
|
|
335
533
|
|
|
336
|
-
const
|
|
337
|
-
const targets =
|
|
338
|
-
const skipped =
|
|
339
|
-
const bad = [];
|
|
340
|
-
const results = [];
|
|
534
|
+
const discovered = listJsFiles(libDir).filter((file) => !only || only.test(relative(libDir, file)));
|
|
535
|
+
const targets = discovered.filter((file) => !skip.some((pattern) => pattern.test(relative(libDir, file))));
|
|
536
|
+
const skipped = discovered.length - targets.length;
|
|
341
537
|
|
|
342
|
-
//
|
|
343
|
-
// 判据严格:**只有「批量说全部 ok」才算通过**,否则一律落慢路径逐个确认 —— 批量的假失败
|
|
344
|
-
// (模块间串扰)由慢路径澄清,批量的假成功不存在(每个模块都真 import 过)。
|
|
538
|
+
// 快路径:一个进程 import 全部(模块一多就快一个数量级)
|
|
345
539
|
if (batch && targets.length > 1) {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
batchTimeoutMs,
|
|
349
|
-
);
|
|
350
|
-
const parsed = extractBatchResult(r.stdout ?? '');
|
|
351
|
-
if (parsed && parsed.length === targets.length && parsed.every((x) => x.ok)) {
|
|
540
|
+
const overview = attemptBatch(targets, libDir, batchTimeoutMs);
|
|
541
|
+
if (overview) {
|
|
352
542
|
return {
|
|
353
|
-
dir: libDir,
|
|
354
|
-
|
|
543
|
+
dir: libDir,
|
|
544
|
+
error: null,
|
|
545
|
+
mode: 'batch',
|
|
546
|
+
total: targets.length,
|
|
547
|
+
skipped,
|
|
548
|
+
bad: [],
|
|
549
|
+
results: overview,
|
|
355
550
|
};
|
|
356
551
|
}
|
|
357
552
|
}
|
|
358
553
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (r2.status === 0) { r = r2; timeout = false; hung = true; }
|
|
370
|
-
else if (!(r2.error || r2.signal)) { r = r2; timeout = false; }
|
|
371
|
-
}
|
|
372
|
-
const ms = Date.now() - t0;
|
|
373
|
-
if (!timeout && r.status === 0) {
|
|
374
|
-
results.push({ file: rel, ok: true, ms, hung });
|
|
554
|
+
// 慢路径:逐模块(能精确定位到文件,也能分辨"解析失败"与"import 成功后挂着不退出")
|
|
555
|
+
const bad = [];
|
|
556
|
+
const results = [];
|
|
557
|
+
for (const file of targets) {
|
|
558
|
+
const label = relative(libDir, file);
|
|
559
|
+
const startedAt = Date.now();
|
|
560
|
+
const outcome = inspectModule(file, timeoutMs);
|
|
561
|
+
const spent = Date.now() - startedAt;
|
|
562
|
+
if (outcome.resolved) {
|
|
563
|
+
results.push({ file: label, ok: true, ms: spent, hung: outcome.hung });
|
|
375
564
|
continue;
|
|
376
565
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
: extractSmokeError(r.stderr ?? '') || `exit ${r.status}`;
|
|
380
|
-
bad.push({ file: rel, err, timeout });
|
|
381
|
-
results.push({ file: rel, ok: false, ms, hung: false });
|
|
566
|
+
bad.push({ file: label, err: outcome.err, timeout: outcome.timeout });
|
|
567
|
+
results.push({ file: label, ok: false, ms: spent, hung: false });
|
|
382
568
|
}
|
|
383
|
-
return { dir: libDir, error: null, mode: 'per-module', total: targets.length, skipped, bad, results };
|
|
384
|
-
}
|
|
385
569
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
570
|
+
return {
|
|
571
|
+
dir: libDir,
|
|
572
|
+
error: null,
|
|
573
|
+
mode: 'per-module',
|
|
574
|
+
total: targets.length,
|
|
575
|
+
skipped,
|
|
576
|
+
bad,
|
|
577
|
+
results,
|
|
578
|
+
};
|
|
391
579
|
}
|