@dsh-workbench/module-tools 1.1.5 → 1.1.7

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/README.md CHANGED
@@ -17,10 +17,10 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
17
17
  维护者确认要真发时跑 `pnpm exec dsh-wb-module release`:全门禁(build / check / design / typecheck / test / selfcheck)、
18
18
  验收必须全勾、预检(登录 / license / git 远端)、按 `--target` 发到团队源 / 公网 npm / 安装包 / git,
19
19
  最后从出口侧核包(`--dry-run` 只预览,`--verify` 只核已发布的包)。配了可写 git 索引源时,
20
- 发完还会更新 `market.json`(`--index auto|none|<目录|git 地址>`,`artifacts-in-repo` 连产物一起入库,
20
+ 发完还会写索引条目(`--index auto|none|<目录|git 地址>`,`artifacts-in-repo` 连产物一起入库,
21
21
  `catalog-only` 只更索引);索引没更上不影响包已发布这件事,结果里会如实写。
22
22
  发布可选 `--index-source <id>` 指定发到哪个索引源;不指定时用唯一可发布的源,有多个可发布源会要求指定。
23
- 空仓库先跑 `dsh-wb-module market-init <git 地址>`:生成 `market.json` 与 README 并推首个 commit
23
+ 空仓库先跑 `dsh-wb-module market-init <git 地址>`:生成索引目录 `market/` 与 README 并推首个 commit
24
24
  (用仓库声明的默认分支,读不到时用 `--ref` 指定)。在工作台里配了可写索引源之后,
25
25
  `release` 默认出口就是 `market`:产物与索引一起进那个仓库。
26
26
  发布过程中每一步都写进 `~/.dsh/workbench/releases/<unit>.json`;中断后带 `--resume` 接着发
@@ -33,10 +33,11 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
33
33
  模块的 `package.json` 可用 `dsh.workbench.market` 声明 `category` 与 `labels`(字符串数组);
34
34
  不写就不带分类和标签,索引里只留 `metadata.workbench: true` 这条身份标记。
35
35
  要换署名时带 `--author "姓名 <邮箱>"`(或只给姓名):它写回 `package.json`,索引、产物与提交都用它,
36
- `--refresh` 时也一样。一个包名归第一次发布的人(`market.json` 里的 `owner`):换署名发同名会被拒,
36
+ `--refresh` 时也一样。**发布本身不改市集里的标题 / 署名 / 说明**:这些元数据只由 `--refresh`(或显式 `--author`)写,
37
+ 避免一台还没更新的机器发新版时把别人刚改的元数据盖回去。一个包名归第一次发布的人(条目文件里的 `owner`):换署名发同名会被拒,
37
38
  `--refresh` 也一样 —— 真要换人,先改那一条的 `owner`。
38
39
  同一个市场里,一个模块 id(`dsh.workbench.unit`)只能被一个条目使用;产物目录按包名走
39
- (`packages/<包名>/`):换一个包名沿用旧 unit、或两个条目撞同一个 id / 目录,都会被拒。
40
+ (`artifacts/<包名>/`):换一个包名沿用旧 unit、或两个条目撞同一个 id / 目录,都会被拒。
40
41
 
41
42
  骨架自带脚本,日常不用记长命令:`build` · `typecheck` · `test` · `selfcheck` · `check` · `lint` · `design`。
42
43
 
@@ -45,7 +46,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
45
46
  | 命令 | 作用 |
46
47
  |---|---|
47
48
  | `dsh-wb-module init <unit> [--dir <目录>] [--template minimal\|diy] [--description "一句话"]` | 建模块骨架 |
48
- | `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market.json` + README,推首个 commit |
49
+ | `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market/` + README,推首个 commit |
49
50
  | `dsh-wb-module build` | 出 `dist/host.js`(单文件 ESM)与 `client.js`(套 loader 壳) |
50
51
  | `dsh-wb-module check [--only <判据>]` | 十三条判据;`--only` 单跑,也当查询口用;`--list [--json\|--markdown]` 只列判据元数据 |
51
52
  | `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
@@ -73,7 +73,7 @@ const MARKET_INIT_HELP = `
73
73
  market-init <git 地址|目录> [--ref <分支>] [--path <索引路径>] [--no-readme] [--json]
74
74
 
75
75
  空仓库:用仓库自己声明的默认分支,读不到时用 --ref 指定(github.com 兜底 main)。
76
- 已有 commit 的仓库:只补 market.json / README.md,不改已有文件。`;
76
+ 已有 commit 的仓库:只补索引目录 / README.md,不改已有文件。`;
77
77
 
78
78
  const CHANGELOG_HELP = `
79
79
  用法:
@@ -86,7 +86,7 @@ const COMMANDS = {
86
86
  build: '出 dist/host.js(单文件 ESM)与 client.js(套 loader 壳)',
87
87
  check: '十三条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
88
88
  init: '建模块骨架(--dir / --template minimal|diy / --description)',
89
- 'market-init': '初始化市场索引仓库(写 market.json + README,推首个 commit)',
89
+ 'market-init': '初始化市场索引仓库(写 market/ 与 README,推首个 commit)',
90
90
  design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
91
91
  publish: '发货前清单:门禁 + files + npm pack(只 dry run)',
92
92
  release: '真发布:全门禁 + 验收闸 + 预检接管 + 发到出口 + 发后核包 + 更新市场索引(中断可 --resume)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "semver": "^7.8.5",
34
34
  "typescript": "5.9.3",
35
35
  "typescript-eslint": "^8.71.0",
36
- "@dsh-workbench/shared": "1.1.5"
36
+ "@dsh-workbench/shared": "1.1.7"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
@@ -1,19 +1,29 @@
1
- import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { randomBytes } from 'node:crypto';
2
+ import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
3
  import { dirname, join, resolve } from 'node:path';
3
4
  import { isDeepStrictEqual } from 'node:util';
4
5
 
5
6
  import {
7
+ MARKET_ENTRIES_README_LINES,
6
8
  MARKET_INDEX_README_LINES,
7
- MARKET_INDEX_SEED,
9
+ assembleMarketIndex,
8
10
  claimOf,
11
+ collectMarketEntryFiles,
9
12
  gitUrlOk,
10
13
  marketArtifactPath,
14
+ marketArtifactPathFor,
15
+ marketArtifactRoot,
16
+ marketEntryFilesOfLegacy,
17
+ marketEntryPath,
18
+ marketMetaDocOf,
19
+ marketMetaPath,
11
20
  marketSourceKey,
21
+ marketVersionDocOf,
12
22
  parseMarketIndexJson,
13
- upsertMarketEntry,
14
23
  } from '@dsh-workbench/shared/host';
15
24
 
16
- const MARKET_FILE = 'market.json';
25
+ const MARKET_DIR = 'market';
26
+ const LEGACY_FILE = 'market.json';
17
27
  const SAFE_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
18
28
  const WORKSPACE_PREFIX = 'workspace:';
19
29
  const ARTIFACT_DIRS = ['dist'];
@@ -108,7 +118,7 @@ export function claimProblemText(name, claim, unit) {
108
118
  }
109
119
  if (claim.state === 'path-taken') {
110
120
  return (
111
- `市场仓库里的 ${marketArtifactPath(name)} 已经被 ${claim.entry} 占了 —— 一个产物目录只能放一个包。` +
121
+ `市场仓库里的 ${marketArtifactRoot(name)} 已经被 ${claim.entry} 占了 —— 一个产物目录只能放一个包。` +
112
122
  '换一个包名再发;那个条目如果已经废弃,让索引维护者先处理掉。'
113
123
  );
114
124
  }
@@ -128,17 +138,180 @@ function unitOf(manifest) {
128
138
  return typeof unit === 'string' ? unit.trim() : '';
129
139
  }
130
140
 
131
- function claimOfIndexFile(workdir, indexPath, input) {
132
- const file = join(workdir, indexPath);
133
- if (!existsSync(file)) return { state: 'new' };
134
- const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
141
+ function indexPathOf(plan) {
142
+ const asked = (plan.source.path ?? '').trim();
143
+ const base = asked === '' ? MARKET_DIR : asked.replace(/^\/+/, '');
144
+ return base.replace(/\/+$/, '') || MARKET_DIR;
145
+ }
146
+
147
+ function readWorktreeIndex(workdir, dir) {
148
+ const root = join(workdir, dir);
149
+ if (existsSync(root)) {
150
+ const assembled = assembleMarketIndex(collectMarketEntryFiles(root));
151
+ if (!assembled.ok) {
152
+ throw new MarketIndexError(
153
+ `${dir}/ 里有不合规的条目:${assembled.problems.map((one) => one.detail).join(';')}`,
154
+ 'index-write',
155
+ );
156
+ }
157
+ return assembled.index;
158
+ }
159
+ const legacy = join(workdir, LEGACY_FILE);
160
+ if (!existsSync(legacy)) return { format: 1, plugins: [] };
161
+ const parsed = parseMarketIndexJson(readFileSync(legacy, 'utf8'));
135
162
  if (!parsed.ok) {
136
163
  throw new MarketIndexError(
137
- `${indexPath} 不合 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
164
+ `${LEGACY_FILE} 不合 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
138
165
  'index-write',
139
166
  );
140
167
  }
141
- return claimOf(parsed.index, input);
168
+ return parsed.index;
169
+ }
170
+
171
+ function ensureClaimFree(workdir, dir, input) {
172
+ const claim = claimOf(readWorktreeIndex(workdir, dir), input);
173
+ if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
174
+ throw new MarketIndexError(claimProblemText(input.name, claim, input.unit), 'market-owner');
175
+ }
176
+ }
177
+
178
+ function ensureVersionFree(workdir, dir, input) {
179
+ const entry = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === input.name);
180
+ if (entry?.versions.some((one) => one.version === input.version) === true) {
181
+ throw new MarketIndexError(`索引里已经有 ${input.name}@${input.version} —— 换个版本号再发`, 'market-owner');
182
+ }
183
+ }
184
+
185
+ function writeEntryFile(workdir, dir, entry, version) {
186
+ const doc = marketVersionDocOf(entry, version);
187
+ const rel = marketEntryPath(dir, entry.name, version.version);
188
+ const file = join(workdir, ...rel.split('/'));
189
+ if (existsSync(file)) {
190
+ const current = JSON.parse(readFileSync(file, 'utf8'));
191
+ if (isDeepStrictEqual(current, doc)) return rel;
192
+ throw new MarketIndexError(
193
+ `索引里已经有 ${entry.name}@${version.version}(${rel})—— 换个版本号再发`,
194
+ 'market-owner',
195
+ );
196
+ }
197
+ mkdirSync(dirname(file), { recursive: true });
198
+ writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
199
+ return rel;
200
+ }
201
+
202
+ function metaEpochOf(workdir, dir, name) {
203
+ const root = join(workdir, dir, name);
204
+ if (!existsSync(root)) return 0;
205
+ let epoch = 0;
206
+ for (const item of readdirSync(root)) {
207
+ if (!item.startsWith('meta-') || !item.endsWith('.json')) continue;
208
+ try {
209
+ const doc = JSON.parse(readFileSync(join(root, item), 'utf8'));
210
+ if (typeof doc.epoch === 'number' && doc.epoch > epoch) epoch = doc.epoch;
211
+ } catch {
212
+ continue;
213
+ }
214
+ }
215
+ return epoch;
216
+ }
217
+
218
+ function writeMetaFile(workdir, dir, entry, at) {
219
+ const epoch = metaEpochOf(workdir, dir, entry.name) + 1;
220
+ const suffix = randomBytes(3).toString('hex');
221
+ const doc = marketMetaDocOf(entry, epoch, Math.floor(at / 1000));
222
+ const rel = marketMetaPath(dir, entry.name, epoch, suffix);
223
+ const file = join(workdir, ...rel.split('/'));
224
+ mkdirSync(dirname(file), { recursive: true });
225
+ writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
226
+ return rel;
227
+ }
228
+
229
+ function migrateLegacyIndex(workdir, dir) {
230
+ const legacy = join(workdir, LEGACY_FILE);
231
+ if (!existsSync(legacy)) return [];
232
+ const files = marketEntryFilesOfLegacy(readFileSync(legacy, 'utf8'), dir);
233
+ if (files === undefined) {
234
+ const parsed = parseMarketIndexJson(readFileSync(legacy, 'utf8'));
235
+ throw new MarketIndexError(
236
+ `${LEGACY_FILE} 不合 format 1:${parsed.ok ? '' : parsed.problems.map((one) => one.detail).join(';')}`,
237
+ 'index-write',
238
+ );
239
+ }
240
+ const written = [];
241
+ for (const file of files) {
242
+ const target = join(workdir, ...file.path.split('/'));
243
+ if (existsSync(target)) continue;
244
+ mkdirSync(dirname(target), { recursive: true });
245
+ writeFileSync(target, file.text);
246
+ written.push(file.path);
247
+ }
248
+ const marker = join(workdir, dir, 'README.md');
249
+ if (!existsSync(marker)) {
250
+ mkdirSync(join(workdir, dir), { recursive: true });
251
+ writeFileSync(marker, MARKET_ENTRIES_README_LINES.join('\n'));
252
+ written.push(`${dir}/README.md`);
253
+ }
254
+ rmSync(legacy);
255
+ written.push(LEGACY_FILE);
256
+ return written;
257
+ }
258
+
259
+ function revOf(exec, workdir, ref) {
260
+ const result = runGit(exec, workdir, ['rev-parse', ref], { allowFailure: true });
261
+ return result.status === 0 ? result.stdout.trim() : '';
262
+ }
263
+
264
+ function syncWorktree(exec, workdir, branch) {
265
+ const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
266
+ if (fetched.status !== 0) {
267
+ throw new MarketIndexError(
268
+ `拉不到索引仓库的远端 ${branch}:${tailOf(`${fetched.stdout}\n${fetched.stderr}`)}`,
269
+ 'git-auth',
270
+ `git -C ${workdir} fetch origin ${branch}`,
271
+ );
272
+ }
273
+ const head = revOf(exec, workdir, 'HEAD');
274
+ const remote = revOf(exec, workdir, `origin/${branch}`);
275
+ if (head === '' || remote === '' || head === remote) return;
276
+ const behind = runGit(exec, workdir, ['merge-base', '--is-ancestor', head, `origin/${branch}`], {
277
+ allowFailure: true,
278
+ });
279
+ if (behind.status !== 0) return;
280
+ runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
281
+ }
282
+
283
+ function replicatePush(exec, workdir, branch, base, redo) {
284
+ let last = '';
285
+ for (let attempt = 0; attempt < 3; attempt += 1) {
286
+ const sha = redo();
287
+ const pushed = pushOf(exec, workdir, branch);
288
+ if (pushed.status === 0) return { ok: true, sha };
289
+ last = tailOf(`${pushed.stdout}\n${pushed.stderr}`);
290
+ const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
291
+ if (fetched.status !== 0) return { ok: false, code: 'git-auth', reason: last };
292
+ const remote = revOf(exec, workdir, `origin/${branch}`);
293
+ const ancestor =
294
+ base === ''
295
+ ? true
296
+ : runGit(exec, workdir, ['merge-base', '--is-ancestor', base, `origin/${branch}`], {
297
+ allowFailure: true,
298
+ }).status === 0;
299
+ if (!ancestor) {
300
+ return {
301
+ ok: false,
302
+ code: 'git-market-rewritten',
303
+ reason: `远端 ${branch} 的历史被改写过(本地基点不在它的祖先里)—— 不自动合并:${last}`,
304
+ };
305
+ }
306
+ if (remote === '' || remote === base) {
307
+ return { ok: false, code: 'git-market-protected', reason: last };
308
+ }
309
+ const reset = runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
310
+ if (reset.status !== 0) {
311
+ return { ok: false, code: 'index-write', reason: `同步远端失败:${tailOf(reset.stderr)}` };
312
+ }
313
+ }
314
+ return { ok: false, code: 'index-write', reason: `远端一直在变,重试 3 次没挤进去(${last})—— 过一会儿再发` };
142
315
  }
143
316
 
144
317
  export function identityOf(manifest) {
@@ -218,16 +391,6 @@ export function indexEntryOf(input) {
218
391
  };
219
392
  }
220
393
 
221
- export function mergeIndexText(text, entry) {
222
- const raw = String(text ?? '').trim();
223
- const parsed = raw === '' ? { ok: true, index: { format: 1, plugins: [] } } : parseMarketIndexJson(raw);
224
- if (!parsed.ok) return { ok: false, problems: parsed.problems };
225
- const merged = upsertMarketEntry(parsed.index, entry);
226
- const checked = parseMarketIndexJson(JSON.stringify(merged));
227
- if (!checked.ok) return { ok: false, problems: checked.problems };
228
- return { ok: true, text: `${JSON.stringify(checked.index, null, 2)}\n` };
229
- }
230
-
231
394
  function readSources(file) {
232
395
  try {
233
396
  const parsed = JSON.parse(readFileSync(file, 'utf8'));
@@ -275,7 +438,7 @@ export function resolveIndexPlan({ args = [], home, exec }) {
275
438
  if (plan === undefined) {
276
439
  if (asked === 'auto' || asked === '') return undefined;
277
440
  throw new MarketIndexError(
278
- `这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_FILE}。`,
441
+ `这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_DIR}/。`,
279
442
  'index-write',
280
443
  );
281
444
  }
@@ -427,11 +590,11 @@ function remoteHeadOf(exec, workdir, branch) {
427
590
  return line.split(/\s+/)[0] ?? '';
428
591
  }
429
592
 
430
- function copyArtifacts(root, workdir, name, manifest) {
593
+ function copyArtifacts(root, workdir, name, version, manifest) {
431
594
  if (!SAFE_PACKAGE_NAME.test(name)) {
432
595
  throw new MarketIndexError(`包名不是合法 npm 名,没法当产物目录:${JSON.stringify(name)}`, 'index-write');
433
596
  }
434
- const path = marketArtifactPath(name);
597
+ const path = marketArtifactPathFor(name, version);
435
598
  const dest = join(workdir, path);
436
599
  rmSync(dest, { recursive: true, force: true });
437
600
  mkdirSync(dest, { recursive: true });
@@ -447,37 +610,6 @@ function copyArtifacts(root, workdir, name, manifest) {
447
610
  return path;
448
611
  }
449
612
 
450
- function writeEntry(workdir, indexPath, entry, publisher, unit) {
451
- const file = join(workdir, indexPath);
452
- const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
453
- if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
454
- const claim = claimOfIndexFile(workdir, indexPath, {
455
- name: entry.name,
456
- publisher,
457
- unit,
458
- artifactPath: entry.versions[0]?.source?.type === 'git' ? entry.versions[0].source.path : undefined,
459
- });
460
- if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
461
- throw new MarketIndexError(claimProblemText(entry.name, claim, unit), 'market-owner');
462
- }
463
- const merged = mergeIndexText(current, entry);
464
- if (!merged.ok) {
465
- throw new MarketIndexError(
466
- `${indexPath} 不合 format 1:${merged.problems.map((one) => one.detail).join(';')}`,
467
- 'index-write',
468
- );
469
- }
470
- mkdirSync(dirname(file), { recursive: true });
471
- writeFileSync(file, merged.text);
472
- }
473
-
474
- function resetToRemote(exec, workdir, branch) {
475
- const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
476
- if (fetched.status !== 0) return false;
477
- const reset = runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
478
- return reset.status === 0;
479
- }
480
-
481
613
  function publishSourceOf(publish) {
482
614
  if (publish.git !== undefined) {
483
615
  return {
@@ -497,14 +629,30 @@ function publishSourceOf(publish) {
497
629
 
498
630
  export function marketClaim({ plan, exec, home, name, unit, publisher }) {
499
631
  const workdir = worktreeOf(plan, exec, home);
500
- return claimOfIndexFile(workdir, plan.source.path ?? MARKET_FILE, {
632
+ return claimOf(readWorktreeIndex(workdir, indexPathOf(plan)), {
501
633
  name,
502
634
  publisher,
503
635
  unit,
504
- artifactPath: marketArtifactPath(name),
636
+ artifactPath: marketArtifactRoot(name),
505
637
  });
506
638
  }
507
639
 
640
+ const META_KEYS = ['title', 'description', 'author', 'owner', 'homepage', 'icon', 'metadata'];
641
+
642
+ function withCurrentMeta(entry, current, update) {
643
+ if (current === undefined) return entry;
644
+ const next = { ...entry };
645
+ if (!update) {
646
+ for (const key of META_KEYS) {
647
+ if (key === 'owner') continue;
648
+ if (current[key] === undefined) delete next[key];
649
+ else next[key] = current[key];
650
+ }
651
+ }
652
+ if (current.owner !== undefined) next.owner = current.owner;
653
+ return next;
654
+ }
655
+
508
656
  export function updateMarketIndex(options) {
509
657
  const { plan, exec, home, root, manifest, version, publish, now, resume, onStep } = options;
510
658
  const unit = options.unit ?? unitOf(manifest);
@@ -522,17 +670,21 @@ export function updateMarketIndex(options) {
522
670
  };
523
671
  try {
524
672
  const workdir = worktreeOf(plan, exec, home);
525
- const indexPath = plan.source.path ?? MARKET_FILE;
526
- if (resume !== undefined && plan.source.localPath === undefined) {
527
- runGit(exec, workdir, ['reset', '--hard', 'HEAD'], { allowFailure: true });
528
- runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactPath(manifest.name), indexPath], {
529
- allowFailure: true,
530
- });
673
+ const dir = indexPathOf(plan);
674
+ const branch = plan.source.ref ?? resume?.branch ?? currentBranch(exec, workdir);
675
+ base = { ...base, branch };
676
+ if (resume !== undefined) {
677
+ if (plan.source.localPath === undefined) {
678
+ const fetched = runGit(exec, workdir, ['fetch', 'origin', branch], { allowFailure: true });
679
+ if (fetched.status === 0) runGit(exec, workdir, ['reset', '--hard', `origin/${branch}`], { allowFailure: true });
680
+ runGit(exec, workdir, ['clean', '-fd', '--', marketArtifactPathFor(manifest.name, version), dir], {
681
+ allowFailure: true,
682
+ });
683
+ }
684
+ rmSync(join(workdir, dir, manifest.name, `${version}.json`), { force: true });
531
685
  } else {
532
686
  assertClean(exec, workdir);
533
687
  }
534
- const branch = plan.source.ref ?? resume?.branch ?? currentBranch(exec, workdir);
535
- base = { ...base, branch };
536
688
  if (typeof resume?.indexCommit === 'string' && resume.indexCommit !== '' && resume.indexPushed === true) {
537
689
  const remote = remoteHeadOf(exec, workdir, branch);
538
690
  if (remote === resume.indexCommit) {
@@ -547,34 +699,42 @@ export function updateMarketIndex(options) {
547
699
  }
548
700
  }
549
701
  if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
550
- const claim = claimOfIndexFile(workdir, indexPath, {
702
+ syncWorktree(exec, workdir, branch);
703
+ const baseSha = revOf(exec, workdir, 'HEAD');
704
+ const claimInput = {
551
705
  name: manifest.name,
552
706
  publisher,
553
707
  unit,
554
- artifactPath: marketArtifactPath(manifest.name),
555
- });
556
- if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
557
- throw new MarketIndexError(claimProblemText(manifest.name, claim, unit), 'market-owner');
558
- }
708
+ version,
709
+ artifactPath: marketArtifactRoot(manifest.name),
710
+ };
711
+ ensureClaimFree(workdir, dir, claimInput);
712
+ ensureVersionFree(workdir, dir, claimInput);
559
713
  let source;
560
714
  let artifactPath;
715
+ let artifactSha = '';
716
+ let indexSha = '';
561
717
  if (plan.policy === 'artifacts-in-repo') {
562
718
  report({ phase: 'artifacts' });
563
- artifactPath = copyArtifacts(root, workdir, manifest.name, manifest);
564
- report({ phase: 'artifacts-copied' });
565
- const sha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
566
- report({ phase: 'artifacts-committed', artifactCommit: sha });
567
- const pushed = pushOf(exec, workdir, branch);
568
- if (pushed.status !== 0) {
719
+ const done = replicatePush(exec, workdir, branch, baseSha, () => {
720
+ ensureClaimFree(workdir, dir, claimInput);
721
+ migrateLegacyIndex(workdir, dir);
722
+ artifactPath = copyArtifacts(root, workdir, manifest.name, version, manifest);
723
+ report({ phase: 'artifacts-copied' });
724
+ artifactSha = commitOf(exec, workdir, `chore(release): ${entryName}`, identityOf(manifest));
725
+ report({ phase: 'artifacts-committed', artifactCommit: artifactSha });
726
+ return artifactSha;
727
+ });
728
+ if (!done.ok) {
569
729
  phase = 'artifacts';
570
730
  throw new MarketIndexError(
571
- `产物提交推不上去(分支保护或权限):本地 commit ${sha};去合并/给权限后跟我说,我补索引。${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
572
- 'git-market-protected',
731
+ `产物提交推不上去(分支保护或权限):本地 commit ${artifactSha};去合并/给权限后跟我说,我补索引。${done.reason}`,
732
+ done.code,
573
733
  `git -C ${workdir} push origin HEAD:refs/heads/${branch}`,
574
734
  );
575
735
  }
576
- report({ phase: 'artifacts-pushed', artifactCommit: sha, artifactPushed: true });
577
- source = { type: 'git', url: plan.source.url, commit: sha, path: artifactPath };
736
+ report({ phase: 'artifacts-pushed', artifactCommit: artifactSha, artifactPushed: true });
737
+ source = { type: 'git', url: plan.source.url, commit: artifactSha, path: artifactPath };
578
738
  } else {
579
739
  source = publishSourceOf(publish ?? {});
580
740
  if (source === undefined) throw new MarketIndexError('这个出口没有可写进索引的 source(tarball 不进索引)', 'index-write');
@@ -582,30 +742,28 @@ export function updateMarketIndex(options) {
582
742
  }
583
743
  const entry = indexEntryOf({ manifest, version, source, at: now() });
584
744
  report({ phase: 'index' });
585
- writeEntry(workdir, indexPath, entry, publisher, unit);
586
- let indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
587
- report({ phase: 'index-committed', indexCommit: indexSha });
588
- const pushed = pushOf(exec, workdir, branch);
589
- if (pushed.status !== 0) {
590
- const replayed = resetToRemote(exec, workdir, branch);
591
- if (!replayed) {
592
- phase = 'index';
593
- throw new MarketIndexError(
594
- `模块已进仓库、索引没更新(本地 commit ${indexSha}):${tailOf(`${pushed.stdout}\n${pushed.stderr}`)}`,
595
- );
596
- }
597
- writeEntry(workdir, indexPath, entry, publisher, unit);
745
+ const indexed = replicatePush(exec, workdir, branch, baseSha, () => {
746
+ if (plan.policy !== 'artifacts-in-repo') migrateLegacyIndex(workdir, dir);
747
+ ensureClaimFree(workdir, dir, claimInput);
748
+ const current = readWorktreeIndex(workdir, dir).plugins.find((one) => one.name === manifest.name);
749
+ const written = withCurrentMeta(entry, current, options.metadata === true);
750
+ if (current === undefined || options.metadata === true) writeMetaFile(workdir, dir, written, now());
751
+ writeEntryFile(workdir, dir, written, written.versions[0]);
598
752
  indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
599
753
  report({ phase: 'index-committed', indexCommit: indexSha });
600
- const retried = pushOf(exec, workdir, branch);
601
- if (retried.status !== 0) {
602
- phase = 'index';
603
- throw new MarketIndexError(`模块已进仓库、索引没更新:${tailOf(`${retried.stdout}\n${retried.stderr}`)}`);
604
- }
754
+ return indexSha;
755
+ });
756
+ if (!indexed.ok) {
757
+ phase = 'index';
758
+ throw new MarketIndexError(
759
+ indexed.code === 'git-market-protected' || indexed.code === 'git-auth'
760
+ ? `模块已进仓库、索引没更新(本地 commit ${indexSha}):${indexed.reason}`
761
+ : indexed.reason,
762
+ indexed.code,
763
+ );
605
764
  }
606
765
  report({ phase: 'index-pushed', indexCommit: indexSha, indexPushed: true });
607
- const local = runGit(exec, workdir, ['rev-parse', 'HEAD']);
608
- const headSha = local.status === 0 ? local.stdout.trim() : '';
766
+ const headSha = revOf(exec, workdir, 'HEAD');
609
767
  const remoteHead = remoteHeadOf(exec, workdir, branch);
610
768
  if (headSha === '' || remoteHead !== headSha) {
611
769
  phase = 'remote-check';
@@ -642,24 +800,26 @@ export function refreshMarketEntry(options) {
642
800
  let phase = 'start';
643
801
  try {
644
802
  const workdir = worktreeOf(plan, exec, home);
645
- const indexPath = plan.source.path ?? MARKET_FILE;
803
+ const dir = indexPathOf(plan);
646
804
  if (publisher === '') throw new MarketIndexError(missingPublisherText(), 'market-owner');
647
805
  assertClean(exec, workdir);
648
806
  const branch = plan.source.ref ?? currentBranch(exec, workdir);
649
807
  base = { ...base, branch };
650
- const file = join(workdir, indexPath);
651
- if (!existsSync(file)) throw new MarketIndexError(`索引里还没有 ${indexPath} —— 先正常发一版`, 'index-write');
652
- const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
653
- if (!parsed.ok) {
654
- throw new MarketIndexError(
655
- `${indexPath} 不是 format 1:${parsed.problems.map((one) => one.detail).join(';')}`,
656
- 'index-write',
657
- );
658
- }
659
- const existing = parsed.index.plugins.find((one) => one.name === manifest.name);
808
+ syncWorktree(exec, workdir, branch);
809
+ const index = readWorktreeIndex(workdir, dir);
810
+ const existing = index.plugins.find((one) => one.name === manifest.name);
660
811
  if (existing === undefined) {
661
812
  throw new MarketIndexError(`${manifest.name} 还不在索引里 —— 先正常发一版`, 'index-write');
662
813
  }
814
+ const claim = claimOf(index, {
815
+ name: manifest.name,
816
+ publisher,
817
+ unit,
818
+ artifactPath: marketArtifactRoot(manifest.name),
819
+ });
820
+ if (claim.state === 'conflict' || claim.state === 'unit-taken' || claim.state === 'path-taken') {
821
+ throw new MarketIndexError(claimProblemText(manifest.name, claim, unit), 'market-owner');
822
+ }
663
823
  const latest = existing.versions[0]?.version ?? '';
664
824
  const meta = entryMetaOf(manifest);
665
825
  const next = { ...existing };
@@ -670,23 +830,14 @@ export function refreshMarketEntry(options) {
670
830
  if (isDeepStrictEqual(next, existing)) {
671
831
  return { ...base, updated: false, unchanged: true, version: latest, phase: 'done' };
672
832
  }
673
- writeEntry(workdir, indexPath, next, publisher, unit);
674
- let indexSha = commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
675
- const pushed = pushOf(exec, workdir, branch);
676
- if (pushed.status !== 0) {
677
- const replayed = resetToRemote(exec, workdir, branch);
678
- if (!replayed) {
679
- throw new MarketIndexError(`元数据没推上去(本地 commit ${indexSha}):${tailOf(pushed.stderr)}`, 'index-write');
680
- }
681
- writeEntry(workdir, indexPath, next, publisher, unit);
682
- indexSha = commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
683
- const retried = pushOf(exec, workdir, branch);
684
- if (retried.status !== 0) {
685
- throw new MarketIndexError(`元数据没推上去:${tailOf(retried.stderr)}`, 'index-write');
686
- }
687
- }
688
- const head = runGit(exec, workdir, ['rev-parse', 'HEAD']);
689
- const headSha = head.status === 0 ? head.stdout.trim() : '';
833
+ const at = Date.now();
834
+ const baseSha = revOf(exec, workdir, 'HEAD');
835
+ const done = replicatePush(exec, workdir, branch, baseSha, () => {
836
+ writeMetaFile(workdir, dir, next, at);
837
+ return commitOf(exec, workdir, `chore(market): refresh ${manifest.name}`, identityOf(manifest));
838
+ });
839
+ if (!done.ok) throw new MarketIndexError(done.reason, done.code);
840
+ const headSha = revOf(exec, workdir, 'HEAD');
690
841
  const remoteHead = remoteHeadOf(exec, workdir, branch);
691
842
  if (headSha === '' || remoteHead !== headSha) {
692
843
  phase = 'remote-check';
@@ -695,7 +846,7 @@ export function refreshMarketEntry(options) {
695
846
  'git-market-pushed',
696
847
  );
697
848
  }
698
- return { ...base, updated: true, version: latest, commit: indexSha, phase: 'done' };
849
+ return { ...base, updated: true, version: latest, commit: done.sha, phase: 'done' };
699
850
  } catch (error) {
700
851
  return {
701
852
  ...base,
@@ -733,7 +884,7 @@ function defaultBranchOf(exec, workdir, url, ref) {
733
884
  }
734
885
 
735
886
  export function initializeMarketRepo(options) {
736
- const { exec, home, url, dir, ref, indexPath = MARKET_FILE, readme = true } = options;
887
+ const { exec, home, url, dir, ref, indexPath = MARKET_DIR, readme = true } = options;
737
888
  let workdir;
738
889
  let remote = String(url ?? '').trim();
739
890
  if (dir !== undefined) {
@@ -757,12 +908,13 @@ export function initializeMarketRepo(options) {
757
908
  const switched = runGit(exec, workdir, ['checkout', '-B', branch], { allowFailure: true });
758
909
  if (switched.status !== 0) throw new MarketIndexError(`建分支 ${branch} 没过:${tailOf(switched.stderr)}`, 'index-init');
759
910
  }
760
- const created = [];
761
- const file = join(workdir, indexPath);
762
- if (!existsSync(file)) {
763
- mkdirSync(dirname(file), { recursive: true });
764
- writeFileSync(file, MARKET_INDEX_SEED);
765
- created.push(indexPath);
911
+ const created = migrateLegacyIndex(workdir, indexPath);
912
+ const indexDir = join(workdir, indexPath);
913
+ const marker = join(indexDir, 'README.md');
914
+ if (!existsSync(marker)) {
915
+ mkdirSync(indexDir, { recursive: true });
916
+ writeFileSync(marker, MARKET_ENTRIES_README_LINES.join('\n'));
917
+ created.push(`${indexPath}/README.md`);
766
918
  }
767
919
  const readmeFile = join(workdir, 'README.md');
768
920
  if (readme && !existsSync(readmeFile)) {
@@ -792,11 +944,7 @@ export function initializeMarketRepo(options) {
792
944
  export function marketVersionExists({ plan, exec, home, name, version }) {
793
945
  try {
794
946
  const workdir = worktreeOf(plan, exec, home);
795
- const file = join(workdir, plan.source.path ?? MARKET_FILE);
796
- if (!existsSync(file)) return false;
797
- const parsed = parseMarketIndexJson(readFileSync(file, 'utf8'));
798
- if (!parsed.ok) return false;
799
- const entry = parsed.index.plugins.find((one) => one.name === name);
947
+ const entry = readWorktreeIndex(workdir, indexPathOf(plan)).plugins.find((one) => one.name === name);
800
948
  return entry?.versions.some((one) => one.version === version) === true;
801
949
  } catch {
802
950
  return false;
@@ -823,4 +971,3 @@ export function verifyMarketArtifact({ plan, exec, home, name, path, commit }) {
823
971
  return { path: artifactPath, files };
824
972
  }
825
973
 
826
- export { MARKET_FILE };
@@ -71,7 +71,7 @@ export function human(out, summary) {
71
71
  `(${market.branch === undefined ? '默认分支' : market.branch})`,
72
72
  );
73
73
  out(` 产物:${String(market.path ?? '').replace(/^\/*/, '')} · commit ${shortSha(market.commit)}`);
74
- out(' 提交:产物与 market.json 两个 commit 都已推上去');
74
+ out(' 提交:产物与索引条目两个 commit 都已推上去');
75
75
  if (summary.changes.length > 0) out(` 改了:${summary.changes.join(';')}`);
76
76
  if (summary.files !== undefined) out(` 包内容:${summary.files.join(' · ')}`);
77
77
  out(` 安装:${summary.install}`);
package/src/release.mjs CHANGED
@@ -480,6 +480,7 @@ export async function runRelease({
480
480
  version,
481
481
  publish,
482
482
  publisher: authorTextOf(manifestNow),
483
+ metadata: authorChange !== undefined,
483
484
  exec,
484
485
  home,
485
486
  now: () => Date.now(),
@@ -728,6 +729,7 @@ export async function runRelease({
728
729
  version,
729
730
  publish: {},
730
731
  publisher,
732
+ metadata: authorChange !== undefined,
731
733
  exec,
732
734
  home,
733
735
  now: () => Date.now(),