@dsh-workbench/module-tools 0.1.14 → 0.1.16

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/src/design.mjs ADDED
@@ -0,0 +1,424 @@
1
+ // 三张表的**唯一真相**:`design.json`。
2
+ //
3
+ // ── 为什么要有这个文件 ────────────────────────────────────────────────────────
4
+ //
5
+ // 在这之前,三张表只活在 README 的散文里:SKILL.md 说"空着的格就是还没定的事,别带着空格往下做",
6
+ // 可**没有一条判据看得见那些格**。于是它靠 agent 自觉:忘了填、填了一半、只留在对话里 —— 都没人拦。
7
+ // README 是给人读的落点,不是给机器判的形状。
8
+ //
9
+ // 现在的分工:
10
+ // · `design.json` —— 机器可读的需求底稿(三张表 + 状态矩阵 + 数据源记录 + 验收清单);
11
+ // · `README.md` —— 由它**生成**的那一段(夹在 `<!-- dsh-wb:design:start/end -->` 之间);
12
+ // · `dsh-wb-module design check` —— 把**空着的格**列出来(红),并核对 README 没落后于 design.json。
13
+ //
14
+ // 这样"空格"第一次有了形状:填空 → 再往下做;写完 → `design accept` 逐条打勾。
15
+
16
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+
19
+ export const DESIGN_FILE = 'design.json';
20
+ export const START_MARK = '<!-- dsh-wb:design:start -->';
21
+ export const END_MARK = '<!-- dsh-wb:design:end -->';
22
+
23
+ /** 卡片五态 —— 与指引 PANEL.md 的状态矩阵、`check:cards` 的渲染判据同一套。 */
24
+ export const STATE_KEYS = ['idle', 'loading', 'ready', 'empty', 'unreadable'];
25
+ const STATE_LABELS = {
26
+ idle: '待机(还没选东西)',
27
+ loading: '加载',
28
+ ready: '就绪',
29
+ empty: '就绪但空',
30
+ unreadable: '读不到',
31
+ };
32
+ const LETTERS = ['A', 'B', 'C', 'D', 'E'];
33
+ const PENDING = '(待填)';
34
+
35
+ /** 必填的行字段;`optional` 的那些留空不算空。 */
36
+ const ROWS = {
37
+ panel: { fields: ['cell', 'conclusion'], optional: ['link'] },
38
+ config: {
39
+ fields: ['persistence', 'key', 'watches', 'default', 'valuesFrom', 'wrongChoice'],
40
+ choices: { persistence: ['prefs', 'view'] },
41
+ mayBeEmpty: true,
42
+ },
43
+ capabilities: {
44
+ fields: ['name', 'oneLine', 'shape', 'approval', 'scope'],
45
+ choices: { scope: ['always', 'workspace'] },
46
+ optional: ['approvalText'],
47
+ mayBeEmpty: true,
48
+ },
49
+ linkage: { fields: ['who', 'when', 'whatChanges', 'userSees'] },
50
+ acceptance: { fields: ['step'] },
51
+ };
52
+ const DATASOURCE_FIELDS = ['read', 'params', 'credentials', 'notLoggedIn', 'timeout'];
53
+
54
+ /** 一条卡背后可以有多条读(甚至跨站点):规范形状 `reads: [ … ]`,一条一行;
55
+ * `datasource: { … }` 是只有一条读时的写法(老模块都长这样,继续认)。 */
56
+ function readsOf(design) {
57
+ const list = design?.reads;
58
+ if (Array.isArray(list) && list.length > 0) return list;
59
+ const one = design?.datasource;
60
+ return one === undefined || one === null || typeof one !== 'object' ? [] : [one];
61
+ }
62
+
63
+ /** 「他能自己配什么」有两种,分不清就会写成一个不落盘的选择还自称配置: */
64
+ function persistenceLabel(value) {
65
+ const raw = text(value);
66
+ if (raw === 'prefs') return '存起来(按工作区记,刷新还在)';
67
+ if (raw === 'view') return '只在这次看的时候选(刷新回默认)';
68
+ return cell(raw);
69
+ }
70
+
71
+ /** 一条能力在哪些项目里出现:`always` = 每个项目都该看见;`workspace` = 只在某些项目里有意义,
72
+ * 代码里必须真的用 `gate.deniedTools(workspace)` 把它收窄,否则表上写着窄、实际到处都在。 */
73
+ function scopeLabel(value) {
74
+ const raw = text(value);
75
+ if (raw === 'always') return '哪个项目都能用';
76
+ if (raw === 'workspace') return '只在某些项目里出现(按项目收窄)';
77
+ return cell(raw);
78
+ }
79
+
80
+ function datasourceRowsOf(read) {
81
+ return [
82
+ '| 项 | 值(照抄,别转述) |',
83
+ '|---|---|',
84
+ `| 哪条读(命令 / URL) | ${cell(read?.read)} |`,
85
+ `| 参数(缺省 / 范围 / 上限) | ${cell(read?.params)} |`,
86
+ `| 凭据从哪来 | ${cell(read?.credentials)} |`,
87
+ `| 没登录 / 凭据过期时是什么样 | ${cell(read?.notLoggedIn)} |`,
88
+ `| 超时 / 连不上时是什么样 | ${cell(read?.timeout)} |`,
89
+ `| 当初探明用的那次调用(原文) | ${soft(read?.probeCommand)} |`,
90
+ ];
91
+ }
92
+
93
+ function text(value) {
94
+ return typeof value === 'string' ? value.trim() : '';
95
+ }
96
+
97
+ function pending(value) {
98
+ return text(value) === '' || text(value) === PENDING;
99
+ }
100
+
101
+ function asRows(value) {
102
+ return Array.isArray(value) ? value : [];
103
+ }
104
+
105
+ export function readDesign(root) {
106
+ const file = join(root, DESIGN_FILE);
107
+ if (!existsSync(file)) return undefined;
108
+ try {
109
+ return JSON.parse(readFileSync(file, 'utf8'));
110
+ } catch (error) {
111
+ console.error(`✖ ${DESIGN_FILE} 不是合法 JSON:${error.message}`);
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ function skeleton(manifest = {}) {
117
+ return {
118
+ unit: text(manifest?.dsh?.workbench?.unit) || text(manifest.name).replace(/^.*\/module-/, ''),
119
+ name: text(manifest.name),
120
+ oneLine: text(manifest.description),
121
+ panel: [{ cell: '', conclusion: '', link: '' }],
122
+ states: STATE_KEYS.map((key) => ({ key, sees: '', canDo: '', agentCan: '' })),
123
+ config: [],
124
+ capabilities: [],
125
+ linkage: [{ who: '', when: '', whatChanges: '', userSees: '' }],
126
+ datasource: {
127
+ read: '',
128
+ params: '',
129
+ credentials: '',
130
+ notLoggedIn: '',
131
+ timeout: '',
132
+ probeCommand: '',
133
+ },
134
+ acceptance: [{ step: '' }],
135
+ };
136
+ }
137
+
138
+ export function writeDesign(root, value) {
139
+ writeFileSync(join(root, DESIGN_FILE), `${JSON.stringify(value, null, 2)}\n`);
140
+ }
141
+
142
+ function need(cells, value, path) {
143
+ if (pending(value)) cells.push(path);
144
+ }
145
+
146
+ /** 空着的格 —— 这就是"别带着空格往下做"那条纪律的机器形状。 */
147
+ export function emptyCells(design) {
148
+ const cells = [];
149
+ for (const field of ['unit', 'name', 'oneLine']) need(cells, design?.[field], field);
150
+
151
+ for (const [key, rule] of Object.entries(ROWS)) {
152
+ const rows = asRows(design?.[key]);
153
+ if (rows.length === 0) {
154
+ if (rule.mayBeEmpty !== true) cells.push(`${key}(一行都没有)`);
155
+ continue;
156
+ }
157
+ rows.forEach((row, index) => {
158
+ for (const field of rule.fields) need(cells, row?.[field], `${key}[${index}].${field}`);
159
+ for (const [field, allowed] of Object.entries(rule.choices ?? {})) {
160
+ const raw = text(row?.[field]);
161
+ if (raw !== '' && !allowed.includes(raw)) {
162
+ cells.push(`${key}[${index}].${field}(只认 ${allowed.join(' / ')},收到「${raw}」)`);
163
+ }
164
+ }
165
+ });
166
+ }
167
+
168
+ const states = asRows(design?.states);
169
+ const noCapabilities = asRows(design?.capabilities).length === 0;
170
+ for (const [index, key] of STATE_KEYS.entries()) {
171
+ const row = states.find((one) => text(one?.key) === key);
172
+ if (row === undefined) {
173
+ cells.push(`states[${index}](缺 ${key} 这一态)`);
174
+ continue;
175
+ }
176
+ // 这一态对这份模块确实不适用时,写一句**为什么**就行(例:账号级的卡没有"还没选项目"这一态)。
177
+ // 行仍然要占着 —— 五态是矩阵的行,不是"能省则省"的清单;省掉的只是那三个格子。
178
+ if (text(row.na) !== '') continue;
179
+ for (const field of ['sees', 'canDo']) need(cells, row[field], `states.${key}.${field}`);
180
+ // 这份模块故意没有 agent 能力时,`agentCan` 没有内容可写 —— 那几格不必硬填一句同义反复。
181
+ if (!noCapabilities) need(cells, row.agentCan, `states.${key}.agentCan`);
182
+ }
183
+
184
+ const reads = readsOf(design);
185
+ if (reads.length === 0) cells.push('数据源(一条读都没写:datasource 或 reads 至少要有一条)');
186
+ reads.forEach((read, index) => {
187
+ const label = reads.length === 1 ? 'datasource' : `reads[${index}]`;
188
+ for (const field of DATASOURCE_FIELDS) need(cells, read?.[field], `${label}.${field}`);
189
+ });
190
+
191
+ return cells;
192
+ }
193
+
194
+ function cell(value) {
195
+ const raw = text(value);
196
+ return raw === '' ? PENDING : raw.replace(/\|/g, '\\|').replace(/\s*\n\s*/g, ' ');
197
+ }
198
+
199
+ /** 可空的格:空着是**合法**的(例:这张卡没有外链),渲染成 `—` 而不是"待填"。 */
200
+ function soft(value) {
201
+ const raw = text(value);
202
+ return raw === '' || raw === PENDING ? '—' : cell(raw);
203
+ }
204
+
205
+ function tableOf(header, rows) {
206
+ const columns = header.split(' | ');
207
+ const divider = `|${columns.map(() => '---').join('|')}|`;
208
+ return [`| ${header} |`, divider, ...rows.map((row) => `| ${row.join(' | ')} |`)].join('\n');
209
+ }
210
+
211
+ export function renderDesign(design) {
212
+ const panel = asRows(design?.panel);
213
+ const config = asRows(design?.config);
214
+ const capabilities = asRows(design?.capabilities);
215
+ const linkage = asRows(design?.linkage);
216
+ const acceptance = asRows(design?.acceptance);
217
+ const states = asRows(design?.states);
218
+ const reads = readsOf(design);
219
+
220
+ const stateRows = STATE_KEYS.map((key, index) => {
221
+ const row = states.find((one) => text(one?.key) === key) ?? {};
222
+ const na = text(row.na);
223
+ if (na !== '') {
224
+ return [LETTERS[index], STATE_LABELS[key], `(这份模块不适用)${na}`, '—', '—'];
225
+ }
226
+ return [
227
+ LETTERS[index],
228
+ STATE_LABELS[key],
229
+ cell(row.sees),
230
+ cell(row.canDo),
231
+ capabilities.length === 0 ? soft(row.agentCan) : cell(row.agentCan),
232
+ ];
233
+ });
234
+
235
+ const acceptanceRows =
236
+ acceptance.length === 0
237
+ ? ['- [ ] (待填)']
238
+ : acceptance.map((one) => `- [${one?.checked === true ? 'x' : ' '}] ${cell(one?.step)}`);
239
+
240
+ return [
241
+ '### 表 1 面板:他看什么、他能自己配什么',
242
+ '',
243
+ tableOf('这一格 | 一眼能得出什么结论 | 点一下可以去哪', panel.length === 0
244
+ ? [['(待填)', PENDING, PENDING]]
245
+ : panel.map((one) => [cell(one?.cell), cell(one?.conclusion), soft(one?.link)])),
246
+ '',
247
+ tableOf(
248
+ '存不存(刷新还在吗) | 能自己配的 | 管什么 | 默认是什么 | 可选值从哪来 | 选错了会怎样',
249
+ config.length === 0 ? [['(没有可配项)', '—', '—', '—', '—', '—']] : config.map((one) => [
250
+ persistenceLabel(one?.persistence),
251
+ cell(one?.key),
252
+ cell(one?.watches),
253
+ cell(one?.default),
254
+ cell(one?.valuesFrom),
255
+ cell(one?.wrongChoice),
256
+ ]),
257
+ ),
258
+ '',
259
+ '状态矩阵(每种状态:他看到什么 / 他能做什么 / agent 能接着做什么)——',
260
+ '图号对着「卡片画哪五态」那一节:',
261
+ '',
262
+ tableOf('# | 状态 | 他看到什么 | 他能做什么 | agent 能接着做什么', stateRows),
263
+ '',
264
+ '### 表 2 agent:他能替用户做什么、哪几件要点头',
265
+ '',
266
+ tableOf(
267
+ '能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么" | 在哪些项目里出现',
268
+ capabilities.length === 0
269
+ ? [['(这份模块故意没有能力)', '—', '—', '—', '—', '—']]
270
+ : capabilities.map((one) => [
271
+ cell(one?.name),
272
+ cell(one?.oneLine),
273
+ cell(one?.shape),
274
+ cell(one?.approval),
275
+ soft(one?.approvalText),
276
+ scopeLabel(one?.scope),
277
+ ]),
278
+ ),
279
+ '',
280
+ '### 表 3 联动:agent 动完,这个模块哪一格变',
281
+ '',
282
+ tableOf('谁在动 | 面板什么时候变 | 哪一格变(投影 + 最窄那个参数) | 用户看到什么', linkage.length === 0
283
+ ? [['(待填)', PENDING, PENDING, PENDING]]
284
+ : linkage.map((one) => [cell(one?.who), cell(one?.when), cell(one?.whatChanges), cell(one?.userSees)])),
285
+ '',
286
+ '### 数据源探明记录',
287
+ '',
288
+ ...(reads.length <= 1
289
+ ? [...datasourceRowsOf(reads[0]), '']
290
+ : reads.flatMap((one, index) => [`#### 读 ${String(index + 1)}`, '', ...datasourceRowsOf(one), ''])),
291
+ '### 验收(做完逐条打勾:`dsh-wb-module design accept`)',
292
+ '',
293
+ ...acceptanceRows,
294
+ '',
295
+ `> 这一段由 \`${DESIGN_FILE}\` 生成(\`dsh-wb-module design render\`),别手改。`,
296
+ ].join('\n');
297
+ }
298
+
299
+ function regionOf(readme) {
300
+ const start = readme.indexOf(START_MARK);
301
+ const end = readme.indexOf(END_MARK);
302
+ if (start === -1 || end === -1 || end < start) return undefined;
303
+ return { start, end };
304
+ }
305
+
306
+ export function applyToReadme(root, markdown) {
307
+ const file = join(root, 'README.md');
308
+ const block = `${START_MARK}\n${markdown}\n${END_MARK}`;
309
+ if (!existsSync(file)) {
310
+ writeFileSync(file, `# ${text(manifestOf(root).name) || '模块'}\n\n${block}\n`);
311
+ return 'created';
312
+ }
313
+ const text0 = readFileSync(file, 'utf8');
314
+ const at = regionOf(text0);
315
+ if (at !== undefined) {
316
+ const next = `${text0.slice(0, at.start)}${block}${text0.slice(at.end + END_MARK.length)}`;
317
+ if (next === text0) return 'same';
318
+ writeFileSync(file, next);
319
+ return 'updated';
320
+ }
321
+ const heading = text0.indexOf('## 需求底稿');
322
+ const insertAt = (() => {
323
+ if (heading === -1) return text0.length;
324
+ const after = text0.indexOf('\n## ', heading + 1);
325
+ return after === -1 ? text0.length : after + 1;
326
+ })();
327
+ const before = text0.slice(0, insertAt).replace(/\s*$/, '');
328
+ const after = text0.slice(insertAt);
329
+ writeFileSync(file, `${before}\n\n${block}\n${after === '' ? '' : `\n${after}`}`);
330
+ return 'inserted';
331
+ }
332
+
333
+ function manifestOf(root) {
334
+ try {
335
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
336
+ } catch {
337
+ return {};
338
+ }
339
+ }
340
+
341
+ function checkDesign(root) {
342
+ const design = readDesign(root);
343
+ if (design === undefined) {
344
+ console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
345
+ process.exit(1);
346
+ }
347
+ const cells = emptyCells(design);
348
+ const rows = (key) => asRows(design[key]).length;
349
+ const acceptance = asRows(design.acceptance);
350
+ const accepted = acceptance.filter((one) => one?.checked === true).length;
351
+
352
+ const readme = existsSync(join(root, 'README.md')) ? readFileSync(join(root, 'README.md'), 'utf8') : '';
353
+ const at = regionOf(readme);
354
+ const fresh = at === undefined ? false : readme.slice(at.start, at.end) === `${START_MARK}\n${renderDesign(design)}\n`;
355
+
356
+ console.log(`design.json:面板 ${rows('panel')} 行 · 状态 ${STATE_KEYS.length} 态 · 配置 ${rows('config')} 项 · 能力 ${rows('capabilities')} 条 · 联动 ${rows('linkage')} 行 · 验收 ${accepted}/${acceptance.length}`);
357
+ if (cells.length > 0) {
358
+ console.error(`✖ 还空着 ${cells.length} 格("空着的格就是还没定的事",别带着空格往下做):`);
359
+ for (const one of cells) console.error(` · ${one}`);
360
+ }
361
+ if (!fresh) {
362
+ console.error('✖ README 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render');
363
+ }
364
+ if (cells.length === 0 && fresh) {
365
+ console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(用户点头后跑 design accept)`}`);
366
+ }
367
+ process.exit(cells.length === 0 && fresh ? 0 : 1);
368
+ }
369
+
370
+ export async function design({ root, args = [] }) {
371
+ const action = args[0] ?? 'check';
372
+
373
+ if (action === 'init') {
374
+ if (existsSync(join(root, DESIGN_FILE))) {
375
+ console.error(`✖ ${DESIGN_FILE} 已经在了;要重来先删掉它`);
376
+ process.exit(1);
377
+ }
378
+ const design0 = skeleton(manifestOf(root));
379
+ writeDesign(root, design0);
380
+ const how = applyToReadme(root, renderDesign(design0));
381
+ console.log(`✔ 写好 ${DESIGN_FILE}(三张表 + 状态矩阵 + 数据源记录 + 验收清单的空骨架)`);
382
+ console.log(` README 的那一段:${how === 'created' ? '新建' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
383
+ console.log(' 接着把每一格填成"用户看得见的东西",然后 dsh-wb-module design check');
384
+ return;
385
+ }
386
+
387
+ if (action === 'render') {
388
+ const design0 = readDesign(root);
389
+ if (design0 === undefined) {
390
+ console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
391
+ process.exit(1);
392
+ }
393
+ const how = applyToReadme(root, renderDesign(design0));
394
+ console.log(`✔ README 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : how === 'inserted' ? '插进「需求底稿」那一节' : '建了 README'}`);
395
+ return;
396
+ }
397
+
398
+ if (action === 'accept') {
399
+ const design0 = readDesign(root);
400
+ if (design0 === undefined) {
401
+ console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
402
+ process.exit(1);
403
+ }
404
+ const acceptance = asRows(design0.acceptance);
405
+ if (acceptance.length === 0) {
406
+ console.error('✖ 验收清单是空的 —— 先写清"打开哪 → 看什么 → 点什么 → 应该看到什么"');
407
+ process.exit(1);
408
+ }
409
+ design0.acceptance = acceptance.map((one) => ({ ...one, checked: true }));
410
+ design0.acceptedAt = new Date().toISOString();
411
+ writeDesign(root, design0);
412
+ applyToReadme(root, renderDesign(design0));
413
+ console.log(`✔ ${acceptance.length} 条验收全部打勾(只在他真的逐条看过之后再跑这条)`);
414
+ return;
415
+ }
416
+
417
+ if (action === 'check') {
418
+ checkDesign(root);
419
+ return;
420
+ }
421
+
422
+ console.error(`不认识的 design 动作:${action}(可用:init / check / render / accept)`);
423
+ process.exit(2);
424
+ }
package/src/discover.mjs CHANGED
@@ -12,7 +12,7 @@ function manifestOf(dir) {
12
12
  }
13
13
  }
14
14
 
15
- function workspaceRootOf(start) {
15
+ export function workspaceRootOf(start) {
16
16
  if (process.env.DSH_WB_ROOT) return process.env.DSH_WB_ROOT;
17
17
  let dir = start;
18
18
  for (;;) {
package/src/init.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { DESIGN_FILE, applyToReadme, readDesign, renderDesign } from './design.mjs';
2
3
  import { join } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
 
@@ -60,5 +61,9 @@ export async function init({ root, args = [] }) {
60
61
  }
61
62
  };
62
63
  walk(target);
63
- console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / selfcheck 已接工具)`);
64
+ const declared = readDesign(target);
65
+ if (declared !== undefined) applyToReadme(target, renderDesign(declared));
66
+ const tables = existsSync(join(target, DESIGN_FILE)) ? `${DESIGN_FILE} 里那份三张表` : '还没有三张表(dsh-wb-module design init 生成)';
67
+ console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / design / selfcheck 已接工具)`);
68
+ console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,README 由它生成`);
64
69
  }
@@ -0,0 +1,134 @@
1
+ // 发货前那一张清单:门禁 + 三张表 + `npm pack --dry-run` 的清单。
2
+ //
3
+ // ── 为什么要有这个命令 ────────────────────────────────────────────────────────
4
+ //
5
+ // 打包这件事有两条**静默**的坑,都真发生过:
6
+ // · `files` 里漏一个目录(`template-diy/` 那次):在仓里跑永远看不出问题,装出来的那份
7
+ // `init --template diy` 直接报"模板不存在";
8
+ // · 产物/声明对不上(`exports` 指向的文件没被 `files` 带上):装上去加载器找不到入口。
9
+ // 两条都不会在开发时冒头,只会在"别人装到那一刻"炸。所以把它们变成发货前的一条命令。
10
+ //
11
+ // 这条命令**只做 dry run**:真发布要凭据、要人点头,不在这里做。它跑完会打印你该跑的那一条。
12
+
13
+ import { execFileSync } from 'node:child_process';
14
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { join, relative } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ import { DESIGN_FILE } from './design.mjs';
20
+
21
+ /** 门禁那几条命令自己会 `process.exit`(它们本来就是 CLI),所以这里**走子进程** ——
22
+ * 直接 import 进来调,成功那次也会把整条 publish 结束掉(实测:清单还没打印就退出了)。 */
23
+ const CLI = fileURLToPath(new URL('../bin/dsh-wb-module.mjs', import.meta.url));
24
+
25
+ function runCli(root, argv) {
26
+ try {
27
+ execFileSync(process.execPath, [CLI, ...argv, '--cwd', root], { stdio: 'inherit' });
28
+ } catch {
29
+ console.error(`✖ ${argv.join(' ')} 没过 —— 清单先不打印了,把上面那些红字处理掉`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ function readManifest(root) {
35
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
36
+ }
37
+
38
+ function packList(root) {
39
+ const cache = join(tmpdir(), 'dsh-wb-pack-cache');
40
+ // `--ignore-scripts`:`prepack`(= build)会往 stdout 打自己的日志,和 `--json` 的输出混在一起
41
+ // 就没法 parse 了(实测)。反正上面已经自己 build 过一遍,这里不需要 npm 再跑一次。
42
+ const raw = execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts', '--cache', cache], {
43
+ cwd: root,
44
+ encoding: 'utf8',
45
+ stdio: ['ignore', 'pipe', 'pipe'],
46
+ });
47
+ const start = raw.search(/[[{]/);
48
+ if (start === -1) throw new Error(`npm pack 没有输出 JSON:${raw.slice(0, 200)}`);
49
+ const parsed = JSON.parse(raw.slice(start));
50
+ return Array.isArray(parsed) ? parsed[0] : parsed;
51
+ }
52
+
53
+ const normalize = (one) => String(one).replace(/^\.\//, '');
54
+
55
+ export async function publish({ root }) {
56
+ if (!existsSync(join(root, 'package.json'))) {
57
+ console.error(`✖ ${root} 里没有 package.json —— 这不是一个包目录`);
58
+ process.exit(2);
59
+ }
60
+ const manifest = readManifest(root);
61
+ const unit = manifest?.dsh?.workbench?.unit;
62
+ const isModule = typeof unit === 'string' && unit !== '';
63
+
64
+ if (isModule) {
65
+ // 先构建:清单评的是**要发出去的那份**,而 `npm pack` 自己也会跑 prepack(= build)——
66
+ // 不先构建就会拿"还没构建"的半成品去跑门禁,红得莫名其妙。
67
+ runCli(root, ['build']);
68
+ runCli(root, ['check']);
69
+ if (existsSync(join(root, DESIGN_FILE))) runCli(root, ['design', 'check']);
70
+ else console.log('○ 没有 design.json(三张表还没落成机器形状)—— 建议先 dsh-wb-module design init');
71
+ } else {
72
+ console.log('○ 这不是一个 wb 模块包(没有 dsh.workbench.unit)—— 跳过十条判据,只查打包清单');
73
+ }
74
+
75
+ const packed = packList(root);
76
+ const paths = (packed.files ?? []).map((one) => normalize(one.path));
77
+ const declared = Array.isArray(manifest.files) ? manifest.files.map(normalize) : [];
78
+ const problems = [];
79
+
80
+ const artifacts = Object.values(manifest.exports ?? {})
81
+ .filter((one) => typeof one === 'string')
82
+ .map(normalize)
83
+ .filter((one) => one !== '' && !one.endsWith('.css'));
84
+ for (const one of artifacts) {
85
+ if (!paths.includes(one)) problems.push(`exports 里的 ${one} 不在包里(files 少带了它)`);
86
+ }
87
+
88
+ if (isModule && !paths.includes('cordis.patch.yml')) {
89
+ problems.push('cordis.patch.yml 不在包里 —— 模块是靠它插进 dsh 组合的');
90
+ }
91
+
92
+ const templateDirs = readdirSync(root, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('template'))
94
+ .map((entry) => entry.name);
95
+ for (const one of templateDirs) {
96
+ if (!declared.includes(one)) problems.push(`模板目录 ${one}/ 不在 files 里(init --template 会当场找不到它)`);
97
+ }
98
+
99
+ const readmeOnDisk = existsSync(join(root, 'README.md'));
100
+ if (readmeOnDisk && !paths.includes('README.md')) problems.push('README.md 在盘上却没进包');
101
+ if (!readmeOnDisk) console.log('○ 包根下没有 README.md —— 发出去的包在源上会是一页空白,建议补一份');
102
+
103
+ // `.tpl` 在 `template*/` 里是**本体**(init 拷过去才剥壳),只有在别处冒出来才算混进脏东西。
104
+ const inTemplate = (one) => templateDirs.some((dir) => one.startsWith(`${dir}/`));
105
+ const leftovers = paths.filter((one) => !inTemplate(one) && (one.endsWith('.tpl') || one.includes('.init-')));
106
+ if (leftovers.length > 0) problems.push(`包里混进了不该发的东西:${leftovers.slice(0, 3).join('、')}`);
107
+
108
+ for (const one of declared) {
109
+ const hit = paths.some((path0) => path0 === one || path0.startsWith(`${one}/`));
110
+ if (!hit) problems.push(`files 里声明了 ${one},但包里没有它(声明与产物不一致)`);
111
+ }
112
+
113
+ const listed = packed.entryCount ?? paths.length;
114
+ console.log(
115
+ `打包清单:${listed} 个文件 · ${(packed.size / 1024).toFixed(1)} KB(解包 ${(packed.unpackedSize / 1024).toFixed(1)} KB)`,
116
+ );
117
+ for (const one of paths.slice(0, 12)) console.log(` · ${one}`);
118
+ if (listed > 12) console.log(` … 还有 ${listed - 12} 个`);
119
+
120
+ if (problems.length > 0) {
121
+ console.error(`✖ 发货前清单 ${problems.length} 条不过:`);
122
+ for (const one of problems) console.error(` · ${one}`);
123
+ process.exit(1);
124
+ }
125
+
126
+ if (manifest.private === true) {
127
+ console.log('✔ 清单干净。包是 `private: true` —— **不会**发到任何源(DIY 的模块都该这样)。');
128
+ console.log(` 要给别人用:摘掉 private + 写上发布目标,再自己跑 \`npm publish\`(这条命令不会替你发)。`);
129
+ return;
130
+ }
131
+ console.log('✔ 清单干净。真要发就自己跑(凭据在你手上):');
132
+ console.log(` npm publish${manifest.publishConfig?.registry === undefined ? '' : ` --registry ${manifest.publishConfig.registry}`}`);
133
+ console.log(` 包目录:${relative(process.cwd(), root) === '' ? '.' : relative(process.cwd(), root)}`);
134
+ }