dsh-any-agent 0.6.2 → 0.6.3
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 +60 -1
- package/README.md +47 -6
- package/README.zh.md +48 -7
- package/SECURITY.md +46 -0
- package/USAGE-NOTES.en.md +6 -2
- package/USAGE-NOTES.md +10 -2
- package/bin/dsh-any-agent.mjs +2 -1
- package/docs/en/architecture.md +12 -0
- package/docs/en/limits.md +6 -0
- package/docs/en/live-view-design.md +177 -0
- package/docs/en/reference.md +1 -0
- package/docs/en/release.md +21 -14
- package/docs/en/tests.md +6 -5
- package/docs/en/usage.md +2 -1
- package/docs/zh/architecture.md +12 -0
- package/docs/zh/limits.md +6 -0
- package/docs/zh/live-view-design.md +11 -6
- package/docs/zh/reference.md +1 -0
- package/docs/zh/release.md +20 -11
- package/docs/zh/tests.md +6 -5
- package/docs/zh/usage.md +2 -1
- package/package.json +6 -5
- package/src/doctor.mjs +63 -2
- package/src/live-cli.mjs +52 -15
- package/src/live-dashboard.html +191 -79
- package/src/live-view.mjs +111 -24
- package/DESIGN.md +0 -484
- package/docs/brand/README.md +0 -58
- package/docs/brand/header.jpg +0 -0
- package/scripts/check-release.mjs +0 -73
- package/scripts/commit.mjs +0 -122
- package/scripts/detect-dsh.mjs +0 -55
- package/scripts/lint.mjs +0 -595
- package/scripts/sync-test-counts.mjs +0 -699
|
@@ -1,699 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* 把测试报告里的检查数写回文档:中英文 tests.md 的套件表与总数段落,
|
|
4
|
-
* 以及 .github/workflows/tests.yml 范围注释里的检查数。零依赖。
|
|
5
|
-
*
|
|
6
|
-
* 为什么要有它:这些数字原先全靠手抄,一改测试就漂(failure-terminal 文档写 11、实际 10;
|
|
7
|
-
* dispatch-tools 写 59、实际 58)。现在数字只来自 test/run.mjs 写出的 JSON 报告
|
|
8
|
-
* (见 test/report.mjs),也就是各套件自己打印的汇总行。
|
|
9
|
-
*
|
|
10
|
-
* 用法:
|
|
11
|
-
* node test/run.mjs --all --report .state/test-report.json # 先产出报告(--all 才含需要模型的套件)
|
|
12
|
-
* npm run docs:counts # 写回文档
|
|
13
|
-
* npm run docs:counts:check # 只检查,不一致就 exit 1 并打印差异
|
|
14
|
-
*
|
|
15
|
-
* --report <path> 报告文件,可重复,按顺序逐份应用(同一格后面的覆盖前面的;总数段落里的
|
|
16
|
-
* "本机实测"取最后一份含离线套件的报告)。例如:模拟 CI 环境的报告 + 本机的 --all 报告。
|
|
17
|
-
* 默认 .state/test-report.json
|
|
18
|
-
* --check 不写文件;文档与报告不一致时 exit 1 并打印差异
|
|
19
|
-
* --portable 只处理 CI 上量得准的离线套件(CI 用):与环境无关的套件,以及只取决于
|
|
20
|
-
* "有没有 DSH、有没有客户端配置"的套件(smoke、config-safety,且这次量到的正是
|
|
21
|
-
* runner 那一格);随平台、文件系统变化的套件(dispatch-tools、workspace-titles)
|
|
22
|
-
* 和总数段落不处理
|
|
23
|
-
* --root <dir> 仓库根(测试用)
|
|
24
|
-
*
|
|
25
|
-
* 退出码:0 一致/已写回;1 文档与报告不一致,或有需要人工补的东西(例如某个套件在表里没有一行);
|
|
26
|
-
* 2 用法或报告有问题(报告缺失、报告里有失败的套件 —— 失败的套件检查数不可信)。
|
|
27
|
-
*
|
|
28
|
-
* 两类数字必须"保持为真":
|
|
29
|
-
* - 随环境变化的套件(有没有 DSH、平台、装了几个客户端配置、文件系统是否区分大小写)
|
|
30
|
-
* 写成「主数字(另一环境的数字)」。本次只更新量到的那一格,其余格子原样保留;
|
|
31
|
-
* 这一格由报告里的环境决定(见下面的 VARIANTS)。
|
|
32
|
-
* - 报告里没有需要模型的套件时(没带 --all),模型套件的数字原样保留。
|
|
33
|
-
* - 报告里标记为 skipped 的套件(没有 DSH 的 shared-process、没有 IDA 的 acceptance …)不动。
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
37
|
-
import { dirname, join, resolve } from 'node:path';
|
|
38
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
|
-
|
|
40
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
41
|
-
const DEFAULT_ROOT = resolve(HERE, '..');
|
|
42
|
-
|
|
43
|
-
export const DOC_FILES = { zh: 'docs/zh/tests.md', en: 'docs/en/tests.md' };
|
|
44
|
-
export const WORKFLOW_FILE = '.github/workflows/tests.yml';
|
|
45
|
-
export const RUN_FILE = 'test/run.mjs';
|
|
46
|
-
export const DEFAULT_REPORT = '.state/test-report.json';
|
|
47
|
-
|
|
48
|
-
const BLOCK_BEGIN_RE = /<!--\s*counts:begin[^>]*-->/;
|
|
49
|
-
const BLOCK_END = '<!-- counts:end -->';
|
|
50
|
-
|
|
51
|
-
// ── 随环境变化的套件 ──────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 检查数随环境变化的套件。每一项说明:
|
|
55
|
-
* head 主数字对应的环境(写在单元格最前面)
|
|
56
|
-
* slots 其余环境的格子(label 是单元格里的写法,zh/en 各一种)
|
|
57
|
-
* fixtures 按"客户端配置数 K/N"分格(每个 K 一格,K=N 时就是主数字)
|
|
58
|
-
* context 由报告里的这个套件算出"这次量到的是哪一格",算不出来返回 null(不更新)
|
|
59
|
-
* ci GitHub 托管 runner 上落在哪一格(参数是平台);null = runner 上整套跳过
|
|
60
|
-
* portable true = 在 runner 那一格量到时,--portable 也核对它(只取决于 DSH/客户端配置的套件);
|
|
61
|
-
* 缺省 = --portable 完全不碰(随平台/文件系统变化,我们没法在三个平台上各量一遍)
|
|
62
|
-
* yml slot 在 tests.yml 注释里的写法(缺省用 en)
|
|
63
|
-
*
|
|
64
|
-
* 没出现在这里的套件视为与环境无关:只有一个数字,CI 也要核对它(--portable)。
|
|
65
|
-
*/
|
|
66
|
-
export const VARIANTS = {
|
|
67
|
-
// 没有 DSH 时跳过会话段:33(有 DSH)/ 20(没有)。
|
|
68
|
-
smoke: {
|
|
69
|
-
head: 'full',
|
|
70
|
-
slots: [{ key: 'nodsh', zh: '无 DSH 时', en: 'without DSH' }],
|
|
71
|
-
context: (s) => (s.meta.dsh === '1' || (s.meta.dsh == null && s.hasDsh) ? 'full' : 'nodsh'),
|
|
72
|
-
ci: () => 'nodsh',
|
|
73
|
-
portable: true,
|
|
74
|
-
},
|
|
75
|
-
// PowerShell 哨兵的真机执行只在 Windows 上跑(多 3 项)。
|
|
76
|
-
'dispatch-tools': {
|
|
77
|
-
head: 'win32',
|
|
78
|
-
slots: [{ key: 'other', zh: '非 Windows', en: 'off Windows' }],
|
|
79
|
-
context: (s) => (s.platform === 'win32' ? 'win32' : 'other'),
|
|
80
|
-
ci: (platform) => (platform === 'win32' ? 'win32' : 'other'),
|
|
81
|
-
},
|
|
82
|
-
// 两条断言只在特定环境下才有:「改大小写也认得出家目录」只在不区分大小写的文件系统上,
|
|
83
|
-
// 「文案指出 temp 就在工作区里」只在 TEMP 落在家目录内时(Windows 默认如此)。
|
|
84
|
-
// 两个条件由套件自己探测并写进报告:键 = 文件系统(i 不区分/s 区分) + TEMP(h 在家目录内/o 在外)。
|
|
85
|
-
// GitHub 的 macos-latest runner 是区分大小写的(见 workspace-titles.mjs 里的注释)。
|
|
86
|
-
'workspace-titles': {
|
|
87
|
-
head: 'ih',
|
|
88
|
-
slots: [
|
|
89
|
-
{ key: 'io', zh: 'TEMP 在家目录外时', en: 'with TEMP outside the home directory' },
|
|
90
|
-
{ key: 'sh', zh: '区分大小写的文件系统上', en: 'on a case-sensitive filesystem' },
|
|
91
|
-
{ key: 'so', zh: '区分大小写且 TEMP 在家目录外时', en: 'case-sensitive with TEMP outside home', yml: 'off Windows' },
|
|
92
|
-
],
|
|
93
|
-
context: (s) => {
|
|
94
|
-
const fsCase = s.meta['fs-case'];
|
|
95
|
-
const tempInHome = s.meta['temp-in-home'];
|
|
96
|
-
if (!fsCase || tempInHome == null) return null;
|
|
97
|
-
return (fsCase === 'insensitive' ? 'i' : 's') + (tempInHome === '1' ? 'h' : 'o');
|
|
98
|
-
},
|
|
99
|
-
ci: (platform) => (platform === 'win32' ? 'ih' : 'so'),
|
|
100
|
-
},
|
|
101
|
-
// 真实 HOME 只读回归里"全部已存在客户端配置未变"那一条,只在装了客户端时才有。
|
|
102
|
-
'config-safety': {
|
|
103
|
-
head: 'with',
|
|
104
|
-
slots: [{ key: 'none', zh: '无客户端配置时', en: 'without client configs' }],
|
|
105
|
-
context: (s) => (s.meta['real-clients'] == null ? null : Number(s.meta['real-clients']) > 0 ? 'with' : 'none'),
|
|
106
|
-
ci: () => 'none',
|
|
107
|
-
portable: true,
|
|
108
|
-
},
|
|
109
|
-
// 每多一个客户端配置多一组往返;runner 上没有任何配置,整套跳过。
|
|
110
|
-
'real-configs': {
|
|
111
|
-
head: 'full',
|
|
112
|
-
fixtures: true,
|
|
113
|
-
context: (s) => (/^\d+\/\d+$/.test(s.meta.fixtures ?? '') ? s.meta.fixtures : null),
|
|
114
|
-
ci: () => null,
|
|
115
|
-
},
|
|
116
|
-
// 下面两个只是"有条件才跑",数字本身与环境无关;标成变体是为了不让 CI 的 --portable 去核对它们。
|
|
117
|
-
'shared-process': { head: 'full', slots: [], context: () => 'full', ci: () => null },
|
|
118
|
-
acceptance: { head: 'full', slots: [], context: () => 'full', ci: () => null },
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const FIX_KEY_RE = /^(\d+)\/(\d+)$/;
|
|
122
|
-
|
|
123
|
-
const isHeadKey = (spec, key) => {
|
|
124
|
-
if (!spec) return true;
|
|
125
|
-
if (spec.fixtures) {
|
|
126
|
-
const m = FIX_KEY_RE.exec(key);
|
|
127
|
-
return m ? m[1] === m[2] : key === spec.head;
|
|
128
|
-
}
|
|
129
|
-
return key === spec.head;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// ── 套件清单(从 test/run.mjs 的源码里取,不 import 它:import 会真的把测试跑起来)──
|
|
133
|
-
|
|
134
|
-
export function parseSuiteLists(source) {
|
|
135
|
-
const grab = (name) => {
|
|
136
|
-
const m = new RegExp(`const\\s+${name}\\s*=\\s*\\[([\\s\\S]*?)\\n\\];`).exec(source);
|
|
137
|
-
if (!m) return [];
|
|
138
|
-
// 先去掉注释:注释里可能写着 'xxx.mjs' 字样。
|
|
139
|
-
const body = m[1].replace(/\/\/[^\n]*/g, '');
|
|
140
|
-
return [...body.matchAll(/'([^']+?)\.mjs'/g)].map((x) => x[1]);
|
|
141
|
-
};
|
|
142
|
-
return { offline: grab('OFFLINE'), model: grab('MODEL') };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── 报告 ──────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
export function readReport(path) {
|
|
148
|
-
let data;
|
|
149
|
-
try {
|
|
150
|
-
data = JSON.parse(readFileSync(path, 'utf8'));
|
|
151
|
-
} catch (e) {
|
|
152
|
-
throw new UsageError(`cannot read report ${path}: ${e.message}`);
|
|
153
|
-
}
|
|
154
|
-
if (data?.schema !== 1 || !Array.isArray(data.suites)) {
|
|
155
|
-
throw new UsageError(`${path} is not a test report (schema 1 expected)`);
|
|
156
|
-
}
|
|
157
|
-
return data;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
class UsageError extends Error {}
|
|
161
|
-
|
|
162
|
-
// ── 单元格:「主数字(其它环境的数字)」─────────────────────────────
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 解析 `59(非 Windows 56)` / `59 (56 off Windows)` / `84(7/9 配置时 66,5/9 配置时 48)` /
|
|
166
|
-
* `8(无 DSH 时 skip)`。认不出的括号内片段原样保留(raw),不丢信息。解析不了整格返回 null。
|
|
167
|
-
*/
|
|
168
|
-
export function parseCell(text, lang, spec) {
|
|
169
|
-
const m = /^\s*(\d+)\s*(?:[((]\s*([\s\S]*?)\s*[))])?\s*$/.exec(text);
|
|
170
|
-
if (!m) return null;
|
|
171
|
-
const model = { head: Number(m[1]), slots: new Map(), raw: [] };
|
|
172
|
-
if (!m[2]) return model;
|
|
173
|
-
for (const seg of m[2].split(lang === 'zh' ? /[,,]\s*/ : /,\s*/)) {
|
|
174
|
-
const part = seg.trim();
|
|
175
|
-
if (!part) continue;
|
|
176
|
-
// 中文是「标签 数字」,英文是「数字 标签」;没有数字的片段(如「无 DSH 时 skip」)原样保留。
|
|
177
|
-
const mm = lang === 'zh' ? /^(.*?)\s*(\d+)$/.exec(part) : /^(\d+)\s+(.*)$/.exec(part);
|
|
178
|
-
const label = mm ? (lang === 'zh' ? mm[1] : mm[2]).trim() : null;
|
|
179
|
-
const n = mm ? Number(lang === 'zh' ? mm[2] : mm[1]) : null;
|
|
180
|
-
const key = label == null ? null : slotKeyOf(spec, label, lang);
|
|
181
|
-
if (key == null) model.raw.push(part);
|
|
182
|
-
else model.slots.set(key, n);
|
|
183
|
-
}
|
|
184
|
-
return model;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function slotKeyOf(spec, label, lang) {
|
|
188
|
-
if (!spec) return null;
|
|
189
|
-
for (const slot of spec.slots ?? []) if (slot[lang] === label) return slot.key;
|
|
190
|
-
if (spec.fixtures) {
|
|
191
|
-
const m = /(\d+)\s*\/\s*(\d+)/.exec(label);
|
|
192
|
-
if (m) return `${m[1]}/${m[2]}`;
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const fmtPart = (lang, label, n) => (lang === 'zh' ? `${label} ${n}` : `${n} ${label}`);
|
|
198
|
-
|
|
199
|
-
export function renderCell(model, lang, spec) {
|
|
200
|
-
const parts = [];
|
|
201
|
-
if (spec) {
|
|
202
|
-
for (const slot of spec.slots ?? []) {
|
|
203
|
-
if (model.slots.has(slot.key)) parts.push(fmtPart(lang, slot[lang], model.slots.get(slot.key)));
|
|
204
|
-
}
|
|
205
|
-
if (spec.fixtures) {
|
|
206
|
-
const keys = [...model.slots.keys()]
|
|
207
|
-
.filter((k) => FIX_KEY_RE.test(k))
|
|
208
|
-
.sort((a, b) => Number(b.split('/')[0]) - Number(a.split('/')[0]));
|
|
209
|
-
for (const k of keys) {
|
|
210
|
-
parts.push(fmtPart(lang, lang === 'zh' ? `${k} 配置时` : `with ${k} fixtures`, model.slots.get(k)));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
parts.push(...model.raw);
|
|
215
|
-
if (!parts.length) return String(model.head);
|
|
216
|
-
return lang === 'zh' ? `${model.head}(${parts.join(',')})` : `${model.head} (${parts.join(', ')})`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** 把一次量到的数写进格子模型。 */
|
|
220
|
-
export function setMeasured(model, spec, key, n) {
|
|
221
|
-
if (isHeadKey(spec, key)) model.head = n;
|
|
222
|
-
else model.slots.set(key, n);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** 某个环境在格子里记的数;没记过返回 undefined。 */
|
|
226
|
-
export function valueAt(model, spec, key) {
|
|
227
|
-
if (isHeadKey(spec, key)) return model.head;
|
|
228
|
-
return model.slots.get(key);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ── 要写什么 ──────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* 哪些套件、量到哪一格、是多少。返回 { items, notes, problems }。
|
|
235
|
-
* 报告按顺序逐份处理:items 里同一格的后一项会盖掉前一项。
|
|
236
|
-
*/
|
|
237
|
-
export function planMeasurements(reports, { portable }) {
|
|
238
|
-
const items = [];
|
|
239
|
-
const notes = [];
|
|
240
|
-
const problems = [];
|
|
241
|
-
for (const report of reports) {
|
|
242
|
-
for (const raw of report.suites) {
|
|
243
|
-
// 每个套件带上自己报告的环境:同一套件在不同报告里可以落在不同的格子。
|
|
244
|
-
const s = { ...raw, meta: raw.meta ?? {}, platform: report.platform, hasDsh: Boolean(report.dsh) };
|
|
245
|
-
if (s.exitCode !== 0 || s.failed > 0) {
|
|
246
|
-
problems.push(`${s.suite} did not pass (exit ${s.exitCode}, ${s.failed} failed): its count is not trustworthy`);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (s.skipped) {
|
|
250
|
-
notes.push(`${s.suite}: skipped in the report, its number is left as is`);
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (s.counted === 'none') {
|
|
254
|
-
notes.push(`${s.suite}: no summary line and no check marks in its output, left as is`);
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const spec = VARIANTS[s.suite];
|
|
258
|
-
let key = 'head';
|
|
259
|
-
if (spec) {
|
|
260
|
-
key = spec.context(s);
|
|
261
|
-
if (key == null) {
|
|
262
|
-
notes.push(`${s.suite}: the report does not say which environment it ran in, left as is`);
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (portable && spec && !(spec.portable && key === spec.ci(s.platform))) continue;
|
|
267
|
-
items.push({ name: s.suite, kind: s.kind, spec, key, n: s.passed });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return { items, notes: [...new Set(notes)], problems: [...new Set(problems)] };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// ── 文档 ──────────────────────────────────────────────────────────
|
|
274
|
-
|
|
275
|
-
const ROW_RE = /^(\|\s*`([^`]+)`\s*\|\s*)([^|]*?)(\s*\|.*)$/;
|
|
276
|
-
|
|
277
|
-
const PLATFORM_LABEL = { win32: 'Windows', linux: 'Linux', darwin: 'macOS' };
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* 重写一份 tests.md:套件表里的数字 + 总数段落。
|
|
281
|
-
* @returns {{ text:string, missing:string[], notes:string[] }}
|
|
282
|
-
*/
|
|
283
|
-
export function rewriteDoc(text, lang, plan, ctx) {
|
|
284
|
-
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
285
|
-
const lines = text.split(/\r?\n/);
|
|
286
|
-
const rows = new Map();
|
|
287
|
-
lines.forEach((line, i) => {
|
|
288
|
-
const m = ROW_RE.exec(line);
|
|
289
|
-
if (m && !rows.has(m[2])) rows.set(m[2], i);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const missing = [];
|
|
293
|
-
const notes = [];
|
|
294
|
-
for (const it of plan.items) {
|
|
295
|
-
const idx = rows.get(it.name);
|
|
296
|
-
if (idx == null) {
|
|
297
|
-
missing.push(it.name);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
const m = ROW_RE.exec(lines[idx]);
|
|
301
|
-
const model = parseCell(m[3], lang, it.spec);
|
|
302
|
-
if (!model) {
|
|
303
|
-
notes.push(`${DOC_FILES[lang]}: the count cell of \`${it.name}\` is not a number ("${m[3]}"), left as is`);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
setMeasured(model, it.spec, it.key, it.n);
|
|
307
|
-
lines[idx] = m[1] + renderCell(model, lang, it.spec) + m[4];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// 套件表里的格子模型(后面算总数用;只看当前这份文档里的数)。
|
|
311
|
-
const cells = new Map();
|
|
312
|
-
for (const [name, idx] of rows) {
|
|
313
|
-
const m = ROW_RE.exec(lines[idx]);
|
|
314
|
-
const model = parseCell(m[3], lang, VARIANTS[name]);
|
|
315
|
-
if (model) cells.set(name, model);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let out = lines.join(eol);
|
|
319
|
-
if (!ctx.portable) {
|
|
320
|
-
const block = renderBlock(lang, cells, ctx);
|
|
321
|
-
if (block) {
|
|
322
|
-
const replaced = replaceBlock(out, block, eol);
|
|
323
|
-
if (replaced == null) notes.push(`${DOC_FILES[lang]}: counts:begin/counts:end markers not found, totals not rewritten`);
|
|
324
|
-
else out = replaced;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return { text: out, missing, notes, rows };
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function replaceBlock(text, block, eol) {
|
|
331
|
-
const b = BLOCK_BEGIN_RE.exec(text);
|
|
332
|
-
if (!b) return null;
|
|
333
|
-
const start = b.index + b[0].length;
|
|
334
|
-
const end = text.indexOf(BLOCK_END, start);
|
|
335
|
-
if (end < 0) return null;
|
|
336
|
-
return text.slice(0, start) + eol + block.join(eol) + eol + text.slice(end);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const REGISTRAR = ['setup', 'removal-scope', 'config-safety', 'real-configs'];
|
|
340
|
-
|
|
341
|
-
/** 总数段落(markdown 行)。数据不全时返回 null,保留原文。 */
|
|
342
|
-
function renderBlock(lang, cells, ctx) {
|
|
343
|
-
const { report, lists } = ctx;
|
|
344
|
-
if (!report) return null;
|
|
345
|
-
const offlineSuites = report.suites.filter((s) => s.kind === 'offline');
|
|
346
|
-
const checks = offlineSuites.reduce((n, s) => n + s.passed, 0);
|
|
347
|
-
|
|
348
|
-
const head = (name) => cells.get(name)?.head;
|
|
349
|
-
const measured = (name) => {
|
|
350
|
-
const s = report.suites.find((x) => x.suite === name);
|
|
351
|
-
if (!s) return head(name);
|
|
352
|
-
return s.skipped ? 0 : s.passed;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
// 客户端注册器套件:优先取这份报告量到的数。
|
|
356
|
-
const registrar = REGISTRAR.map((name) => ({ name, n: measured(name), skipped: report.suites.find((x) => x.suite === name)?.skipped }));
|
|
357
|
-
const registrarOk = registrar.every((r) => r.n != null);
|
|
358
|
-
const registrarSum = registrar.reduce((n, r) => n + (r.n ?? 0), 0);
|
|
359
|
-
|
|
360
|
-
// 需要模型的套件:名义合计(取各自表里的主数字)。
|
|
361
|
-
const modelNames = lists.model;
|
|
362
|
-
const modelNums = modelNames.map((name) => head(name));
|
|
363
|
-
const modelOk = modelNames.length > 0 && modelNums.every((n) => n != null);
|
|
364
|
-
const modelSum = modelNums.reduce((n, v) => n + (v ?? 0), 0);
|
|
365
|
-
const smokeCell = cells.get('smoke');
|
|
366
|
-
const smokeExtra = smokeCell && smokeCell.slots.has('nodsh') ? smokeCell.head - smokeCell.slots.get('nodsh') : null;
|
|
367
|
-
|
|
368
|
-
// CI 离线 job 的名义总数:runner 没有 DSH、没有客户端配置。
|
|
369
|
-
const ciTotal = (platform) => {
|
|
370
|
-
let sum = 0;
|
|
371
|
-
for (const name of lists.offline) {
|
|
372
|
-
const model = cells.get(name);
|
|
373
|
-
if (!model) return null;
|
|
374
|
-
const spec = VARIANTS[name];
|
|
375
|
-
if (!spec) {
|
|
376
|
-
sum += model.head;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
const key = spec.ci(platform);
|
|
380
|
-
if (key == null) continue;
|
|
381
|
-
const v = valueAt(model, spec, key);
|
|
382
|
-
if (v === undefined) return null;
|
|
383
|
-
sum += v;
|
|
384
|
-
}
|
|
385
|
-
return sum;
|
|
386
|
-
};
|
|
387
|
-
const ciLinux = ciTotal('linux');
|
|
388
|
-
const ciWin = ciTotal('win32');
|
|
389
|
-
|
|
390
|
-
// 不写日期、Node 版本、DSH 版本:检查数不依赖它们,写进去只会让 `docs:counts:check`
|
|
391
|
-
// 在每次重跑、每次升级之后都报"不一致"(日期交给 git 历史)。只留真正会改变总数的环境因素。
|
|
392
|
-
const platform = PLATFORM_LABEL[report.platform] ?? report.platform;
|
|
393
|
-
const fx = report.fixtures?.clientConfigs;
|
|
394
|
-
const realSkipped = report.suites.find((s) => s.suite === 'real-configs')?.skipped;
|
|
395
|
-
|
|
396
|
-
const zh = lang === 'zh';
|
|
397
|
-
const dshText = report.dsh ? (zh ? '装了 DSH' : 'DSH installed') : zh ? '没有 DSH' : 'no DSH';
|
|
398
|
-
const fxText = fx
|
|
399
|
-
? zh ? `,${fx.present}/${fx.total} 个客户端配置 fixture` : `, ${fx.present}/${fx.total} client config fixtures`
|
|
400
|
-
: realSkipped ? (zh ? ',没有客户端配置 fixture' : ', no client config fixtures') : '';
|
|
401
|
-
const ciText = ciLinux == null
|
|
402
|
-
? null
|
|
403
|
-
: ciWin == null || ciWin === ciLinux
|
|
404
|
-
? String(ciLinux)
|
|
405
|
-
: zh ? `Linux/macOS ${ciLinux},Windows ${ciWin}` : `${ciLinux} on Linux/macOS, ${ciWin} on Windows`;
|
|
406
|
-
|
|
407
|
-
const lines = [];
|
|
408
|
-
if (zh) {
|
|
409
|
-
lines.push(
|
|
410
|
-
`**\`node test/run.mjs\` 在本机(${platform},${dshText}${fxText})跑了 ${offlineSuites.length} 个离线套件、${checks} 项检查,全部通过。**表中带括号的数字随环境变化(有没有 DSH、哪个平台、文件系统是否区分大小写、本机装了几个客户端配置),套件总数也随之变化;这一行记录的是上次同步所用报告的环境。`,
|
|
411
|
-
);
|
|
412
|
-
if (registrarOk) {
|
|
413
|
-
const list = registrar.map((r) => (r.skipped ? `\`${r.name}\` 已跳过` : `\`${r.name}\` ${r.n}`)).join('、');
|
|
414
|
-
lines.push('', `其中 **${registrarSum} 项属于客户端注册器套件**(${list})—— 0.3 的活其实都在这里。`);
|
|
415
|
-
}
|
|
416
|
-
if (modelOk) {
|
|
417
|
-
const list = modelNames.map((name, i) => `\`${name}\` ${modelNums[i]}`).join('、');
|
|
418
|
-
const extra = smokeExtra ? `;有 DSH 时 \`smoke\` 还会多跑 ${smokeExtra} 项会话段` : '';
|
|
419
|
-
lines.push(
|
|
420
|
-
'',
|
|
421
|
-
`需要模型的套件(${list})名义合计 **${modelSum} 项**${extra}。它们要可用的 DSH 与 provider,发版前请自己重跑一遍:\`node test/run.mjs --all\`。\`acceptance\` 在没有 IDA MCP(\`mcp__ida__*\`)时会明确 **skip**;\`shared-process\` 没有 DSH 时也会 skip。`,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
if (ciText != null && modelOk) {
|
|
425
|
-
lines.push(
|
|
426
|
-
'',
|
|
427
|
-
`> 三个总数是刻意分开的:本机 \`node test/run.mjs\`(${checks})、CI 离线 job(名义 ${ciText};hosted runner 没有 DSH 也没有客户端 fixture,\`smoke\` 跳过会话段、\`real-configs\` 整体跳过)、需要模型的集合(名义 ${modelSum})。数字取各套件自己打印的汇总,由 \`npm run docs:counts\` 从报告写回,不在这里手抄;不要把一个数字套到另一个 runner 上。`,
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
} else {
|
|
431
|
-
lines.push(
|
|
432
|
-
`**\`node test/run.mjs\` ran ${checks} checks in ${offlineSuites.length} offline suites on this machine (${platform}, ${dshText}${fxText}), all passing.** Parenthesised numbers in the table depend on the environment (DSH present or not, platform, filesystem case sensitivity, how many client configs the machine has), so the total moves with it; this line records the environment of the report used for the last sync.`,
|
|
433
|
-
);
|
|
434
|
-
if (registrarOk) {
|
|
435
|
-
const list = registrar.map((r) => (r.skipped ? `\`${r.name}\` skipped` : `\`${r.name}\` ${r.n}`)).join(', ');
|
|
436
|
-
lines.push('', `That is **${registrarSum} assertions in the client-registration suites alone** (${list}), which is where the 0.3 work actually is.`);
|
|
437
|
-
}
|
|
438
|
-
if (modelOk) {
|
|
439
|
-
const list = modelNames.map((name, i) => `\`${name}\` ${modelNums[i]}`).join(', ');
|
|
440
|
-
const extra = smokeExtra ? `; with DSH present \`smoke\` runs ${smokeExtra} more checks in its session sections` : '';
|
|
441
|
-
lines.push(
|
|
442
|
-
'',
|
|
443
|
-
`The model-backed suites (${list}) total **${modelSum} checks** nominally${extra}. They need a working DSH and provider, so re-run them yourself before trusting a release: \`node test/run.mjs --all\`. \`acceptance\` **skips explicitly** when the DSH profile has no IDA MCP (\`mcp__ida__*\`); \`shared-process\` also skips without DSH.`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
if (ciText != null && modelOk) {
|
|
447
|
-
lines.push(
|
|
448
|
-
'',
|
|
449
|
-
`> Three different totals are intentional: \`node test/run.mjs\` on this machine (${checks}), the CI offline job (nominal ${ciText}; hosted runners have no DSH and no client fixtures, so \`smoke\` skips its session sections and \`real-configs\` skips entirely), and the model-backed set (${modelSum} nominal). Counts are the numbers each suite prints itself, written back from the report by \`npm run docs:counts\`; don't copy them by hand and don't use one number for a different runner.`,
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return lines;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// ── tests.yml 的范围注释 ──────────────────────────────────────────
|
|
457
|
-
|
|
458
|
-
const YML_LINE_RE = /^(#\s+)([a-z][a-z0-9-]*)(\s+.*)$/;
|
|
459
|
-
const YML_COUNT_RE = /(\d+) checks(?: \((\d+) off Windows\))?/;
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* 重写 workflow 注释里的 `N checks`(及 `(M off Windows)`)。
|
|
463
|
-
* 数字取 CI 环境对应的那一格:runner 没有 DSH、没有客户端配置。
|
|
464
|
-
*/
|
|
465
|
-
export function rewriteWorkflow(text, docModels, offlineNames, portable) {
|
|
466
|
-
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
467
|
-
const lines = text.split(/\r?\n/);
|
|
468
|
-
const notes = [];
|
|
469
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
470
|
-
const m = YML_LINE_RE.exec(lines[i]);
|
|
471
|
-
if (!m) continue;
|
|
472
|
-
const name = m[2];
|
|
473
|
-
if (!offlineNames.has(name)) continue;
|
|
474
|
-
if (!YML_COUNT_RE.test(lines[i])) continue;
|
|
475
|
-
const spec = VARIANTS[name];
|
|
476
|
-
// --portable 不碰随平台/文件系统变化的套件,它们的注释行也不动。
|
|
477
|
-
if (portable && spec && !spec.portable) continue;
|
|
478
|
-
const model = docModels.get(name);
|
|
479
|
-
if (!model) {
|
|
480
|
-
notes.push(`${WORKFLOW_FILE}: \`${name}\` has no row in the docs table, line left as is`);
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
const text2 = ymlCount(name, spec, model);
|
|
484
|
-
if (text2 == null) continue;
|
|
485
|
-
lines[i] = lines[i].replace(YML_COUNT_RE, text2);
|
|
486
|
-
}
|
|
487
|
-
return { text: lines.join(eol), notes };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function ymlCount(name, spec, model) {
|
|
491
|
-
if (!spec) return `${model.head} checks`;
|
|
492
|
-
const win = spec.ci('win32');
|
|
493
|
-
const other = spec.ci('linux');
|
|
494
|
-
if (win == null && other == null) return null;
|
|
495
|
-
const vWin = valueAt(model, spec, win);
|
|
496
|
-
const vOther = valueAt(model, spec, other);
|
|
497
|
-
if (vWin === undefined || vOther === undefined) return null;
|
|
498
|
-
if (vWin === vOther) return `${vWin} checks`;
|
|
499
|
-
// 只有「Windows 是主数字、其它平台是一个 slot」时才写得成 `18 checks (16 off Windows)`。
|
|
500
|
-
const slot = spec.slots.find((s) => s.key === other);
|
|
501
|
-
const label = slot?.yml ?? slot?.en;
|
|
502
|
-
return label === 'off Windows' && win === spec.head ? `${vWin} checks (${vOther} off Windows)` : null;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// ── 差异显示 ──────────────────────────────────────────────────────
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* 行级差异:先去掉共同的首尾,再对中间做 LCS。文档才几百行,O(n*m) 足够。
|
|
509
|
-
* 不能只做"逐行配对":总数段落的行数会变,行数一变,后面所有行都会错位成一大片假差异。
|
|
510
|
-
* 返回 [{ line(旧文件里的行号), removed:[], added:[] }]。
|
|
511
|
-
*/
|
|
512
|
-
function diffHunks(before, after) {
|
|
513
|
-
const a = before.split('\n');
|
|
514
|
-
const b = after.split('\n');
|
|
515
|
-
let s = 0;
|
|
516
|
-
while (s < a.length && s < b.length && a[s] === b[s]) s += 1;
|
|
517
|
-
let ea = a.length;
|
|
518
|
-
let eb = b.length;
|
|
519
|
-
while (ea > s && eb > s && a[ea - 1] === b[eb - 1]) {
|
|
520
|
-
ea -= 1;
|
|
521
|
-
eb -= 1;
|
|
522
|
-
}
|
|
523
|
-
const x = a.slice(s, ea);
|
|
524
|
-
const y = b.slice(s, eb);
|
|
525
|
-
const lcs = Array.from({ length: x.length + 1 }, () => new Uint32Array(y.length + 1));
|
|
526
|
-
for (let i = x.length - 1; i >= 0; i -= 1) {
|
|
527
|
-
for (let j = y.length - 1; j >= 0; j -= 1) {
|
|
528
|
-
lcs[i][j] = x[i] === y[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const hunks = [];
|
|
532
|
-
let cur = null;
|
|
533
|
-
const flush = () => {
|
|
534
|
-
if (cur) hunks.push(cur);
|
|
535
|
-
cur = null;
|
|
536
|
-
};
|
|
537
|
-
let i = 0;
|
|
538
|
-
let j = 0;
|
|
539
|
-
while (i < x.length || j < y.length) {
|
|
540
|
-
if (i < x.length && j < y.length && x[i] === y[j]) {
|
|
541
|
-
flush();
|
|
542
|
-
i += 1;
|
|
543
|
-
j += 1;
|
|
544
|
-
} else if (j < y.length && (i === x.length || lcs[i][j + 1] >= lcs[i + 1][j])) {
|
|
545
|
-
cur ??= { line: s + i + 1, removed: [], added: [] };
|
|
546
|
-
cur.added.push(y[j]);
|
|
547
|
-
j += 1;
|
|
548
|
-
} else {
|
|
549
|
-
cur ??= { line: s + i + 1, removed: [], added: [] };
|
|
550
|
-
cur.removed.push(x[i]);
|
|
551
|
-
i += 1;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
flush();
|
|
555
|
-
return hunks;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const clip = (s) => (s.length > 170 ? s.slice(0, 167) + '...' : s);
|
|
559
|
-
|
|
560
|
-
// ── 入口 ──────────────────────────────────────────────────────────
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* @param {string[]} argv
|
|
564
|
-
* @param {{root?:string, out?:(s:string)=>void, err?:(s:string)=>void}} io
|
|
565
|
-
* @returns {number} exit code
|
|
566
|
-
*/
|
|
567
|
-
export function main(argv, io = {}) {
|
|
568
|
-
const out = io.out ?? ((s) => console.log(s));
|
|
569
|
-
const err = io.err ?? ((s) => console.error(s));
|
|
570
|
-
let root = io.root ?? DEFAULT_ROOT;
|
|
571
|
-
const reportPaths = [];
|
|
572
|
-
let check = false;
|
|
573
|
-
let portable = false;
|
|
574
|
-
try {
|
|
575
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
576
|
-
const a = argv[i];
|
|
577
|
-
const value = () => {
|
|
578
|
-
const v = argv[i + 1];
|
|
579
|
-
if (!v || v.startsWith('--')) throw new UsageError(`${a} needs a value`);
|
|
580
|
-
i += 1;
|
|
581
|
-
return v;
|
|
582
|
-
};
|
|
583
|
-
if (a === '--check') check = true;
|
|
584
|
-
else if (a === '--portable') portable = true;
|
|
585
|
-
else if (a === '--report') reportPaths.push(resolve(value()));
|
|
586
|
-
else if (a.startsWith('--report=')) reportPaths.push(resolve(a.slice('--report='.length)));
|
|
587
|
-
else if (a === '--root') root = resolve(value());
|
|
588
|
-
else if (a === '--help' || a === '-h') {
|
|
589
|
-
out('usage: node scripts/sync-test-counts.mjs [--report <path>]... [--check] [--portable]');
|
|
590
|
-
return 0;
|
|
591
|
-
} else throw new UsageError(`unknown argument ${a}`);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (!reportPaths.length) {
|
|
595
|
-
const def = join(root, DEFAULT_REPORT);
|
|
596
|
-
if (!existsSync(def)) {
|
|
597
|
-
throw new UsageError(`no report at ${def}; run \`node test/run.mjs\` (add --all for model suites) first, or pass --report <path>`);
|
|
598
|
-
}
|
|
599
|
-
reportPaths.push(def);
|
|
600
|
-
}
|
|
601
|
-
const reports = reportPaths.map(readReport);
|
|
602
|
-
|
|
603
|
-
const lists = parseSuiteLists(readFileSync(join(root, RUN_FILE), 'utf8'));
|
|
604
|
-
if (!lists.offline.length) throw new UsageError(`could not read the OFFLINE list from ${RUN_FILE}`);
|
|
605
|
-
|
|
606
|
-
const plan = planMeasurements(reports, { portable });
|
|
607
|
-
if (plan.problems.length) {
|
|
608
|
-
for (const p of plan.problems) err(`docs:counts: ${p}`);
|
|
609
|
-
err('docs:counts: refusing to use a report with failing suites');
|
|
610
|
-
return 2;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// 总数段落里的"本机实测"取最后一份含离线套件的报告。
|
|
614
|
-
const contextReport = [...reports].reverse().find((r) => r.suites.some((s) => s.kind === 'offline')) ?? null;
|
|
615
|
-
const ctx = { report: contextReport, lists, portable };
|
|
616
|
-
const issues = [];
|
|
617
|
-
const diffs = [];
|
|
618
|
-
const docModels = new Map();
|
|
619
|
-
|
|
620
|
-
for (const lang of ['zh', 'en']) {
|
|
621
|
-
const file = join(root, DOC_FILES[lang]);
|
|
622
|
-
if (!existsSync(file)) {
|
|
623
|
-
issues.push(`${DOC_FILES[lang]} not found`);
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
const before = readFileSync(file, 'utf8');
|
|
627
|
-
const res = rewriteDoc(before, lang, plan, ctx);
|
|
628
|
-
for (const name of res.missing) issues.push(`${DOC_FILES[lang]}: no table row for suite \`${name}\`; add one by hand`);
|
|
629
|
-
for (const n of res.notes) issues.push(n);
|
|
630
|
-
// 文档里有行、但 run.mjs 的两个清单里都没有的套件:多半是删了套件没删行。
|
|
631
|
-
const known = new Set([...lists.offline, ...lists.model]);
|
|
632
|
-
for (const name of res.rows.keys()) {
|
|
633
|
-
if (!known.has(name)) issues.push(`${DOC_FILES[lang]}: table row \`${name}\` is not in the ${RUN_FILE} OFFLINE/MODEL lists`);
|
|
634
|
-
}
|
|
635
|
-
// 写 tests.yml 注释时用中文文档里(已更新过的)格子;两份文档的数字本来就该一致。
|
|
636
|
-
if (lang === 'zh') {
|
|
637
|
-
const outLines = res.text.split(/\r?\n/);
|
|
638
|
-
for (const [name, idx] of res.rows) {
|
|
639
|
-
const model = parseCell(ROW_RE.exec(outLines[idx])[3], lang, VARIANTS[name]);
|
|
640
|
-
if (model) docModels.set(name, model);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
if (res.text !== before) diffs.push({ file: DOC_FILES[lang], before, after: res.text, path: file });
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const ymlFile = join(root, WORKFLOW_FILE);
|
|
647
|
-
if (existsSync(ymlFile)) {
|
|
648
|
-
const before = readFileSync(ymlFile, 'utf8');
|
|
649
|
-
const res = rewriteWorkflow(before, docModels, new Set(lists.offline), portable);
|
|
650
|
-
for (const n of res.notes) issues.push(n);
|
|
651
|
-
if (res.text !== before) diffs.push({ file: WORKFLOW_FILE, before, after: res.text, path: ymlFile });
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
for (const n of plan.notes) out(`docs:counts: ${n}`);
|
|
655
|
-
const hasModel = reports.some((r) => r.suites.some((s) => s.kind === 'model'));
|
|
656
|
-
if (!hasModel && !portable) {
|
|
657
|
-
out('docs:counts: the report has no model-backed suites (run `node test/run.mjs --all`), so their numbers were left untouched');
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (check) {
|
|
661
|
-
if (diffs.length) {
|
|
662
|
-
for (const d of diffs) {
|
|
663
|
-
err(`docs:counts: ${d.file} disagrees with the report:`);
|
|
664
|
-
for (const h of diffHunks(d.before, d.after)) {
|
|
665
|
-
err(` ${d.file}:${h.line}`);
|
|
666
|
-
for (const l of h.removed) err(` - ${clip(l)}`);
|
|
667
|
-
for (const l of h.added) err(` + ${clip(l)}`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
err('docs:counts: run `npm run docs:counts` to rewrite them from the report');
|
|
671
|
-
}
|
|
672
|
-
for (const i of issues) err(`docs:counts: ${i}`);
|
|
673
|
-
if (!diffs.length && !issues.length) out(`docs:counts: docs agree with the report${portable ? ' (portable suites only)' : ''}`);
|
|
674
|
-
return diffs.length || issues.length ? 1 : 0;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
for (const d of diffs) {
|
|
678
|
-
writeFileSync(d.path, d.after, 'utf8');
|
|
679
|
-
out(`docs:counts: updated ${d.file}`);
|
|
680
|
-
for (const h of diffHunks(d.before, d.after)) {
|
|
681
|
-
for (const l of h.removed) out(` - ${clip(l)}`);
|
|
682
|
-
for (const l of h.added) out(` + ${clip(l)}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (!diffs.length) out('docs:counts: docs already match the report');
|
|
686
|
-
for (const i of issues) err(`docs:counts: ${i}`);
|
|
687
|
-
return issues.length ? 1 : 0;
|
|
688
|
-
} catch (e) {
|
|
689
|
-
if (e instanceof UsageError) {
|
|
690
|
-
err(`docs:counts: ${e.message}`);
|
|
691
|
-
return 2;
|
|
692
|
-
}
|
|
693
|
-
throw e;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
698
|
-
process.exitCode = main(process.argv.slice(2));
|
|
699
|
-
}
|