@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.1

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -177
  3. package/bin/dsh-wb-module.mjs +89 -34
  4. package/package.json +22 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +166 -0
  8. package/src/check-flags.mjs +0 -7
  9. package/src/check.mjs +115 -53
  10. package/src/checks/alias.mjs +0 -9
  11. package/src/checks/arch.mjs +3 -25
  12. package/src/checks/bundles.mjs +2 -34
  13. package/src/checks/cards.mjs +26 -30
  14. package/src/checks/contracts.mjs +72 -219
  15. package/src/checks/kit.mjs +2 -42
  16. package/src/checks/lint.mjs +9 -82
  17. package/src/checks/markers.mjs +48 -17
  18. package/src/checks/primitives.mjs +0 -39
  19. package/src/checks/protocol.mjs +0 -18
  20. package/src/checks/source.mjs +103 -51
  21. package/src/checks/standalone.mjs +4 -14
  22. package/src/checks/style.mjs +84 -230
  23. package/src/css-injection.mjs +0 -11
  24. package/src/css-shell.mjs +3 -31
  25. package/src/datasource-file.mjs +104 -0
  26. package/src/declared-tools.mjs +0 -24
  27. package/src/design.mjs +116 -38
  28. package/src/discover.mjs +2 -10
  29. package/src/font-scale.mjs +1 -61
  30. package/src/init.mjs +2 -17
  31. package/src/lint.mjs +24 -144
  32. package/src/market-index.mjs +569 -0
  33. package/src/market-init.mjs +80 -0
  34. package/src/primitives-surface.mjs +2 -28
  35. package/src/publish.mjs +11 -25
  36. package/src/release-changelog.mjs +29 -0
  37. package/src/release-journal.mjs +131 -0
  38. package/src/release-report.mjs +99 -0
  39. package/src/release-util.mjs +87 -0
  40. package/src/release.mjs +1250 -0
  41. package/src/seed.mjs +0 -25
  42. package/src/shell-type-files.mjs +3 -30
  43. package/src/sync.mjs +41 -24
  44. package/template/AGENTS.md.tpl +33 -0
  45. package/template/CHANGELOG.md.tpl +7 -0
  46. package/template/README.md.tpl +6 -33
  47. package/template/package.json.tpl +2 -2
  48. package/template/src/client.tsx.tpl +1 -18
  49. package/template/src/host.ts.tpl +2 -15
  50. package/template/types/globals.d.ts +1 -14
  51. package/template-diy/AGENTS.md.tpl +33 -0
  52. package/template-diy/CHANGELOG.md.tpl +7 -0
  53. package/template-diy/README.md.tpl +4 -133
  54. package/template-diy/package.json.tpl +2 -2
  55. package/template-diy/src/client.tsx.tpl +1 -29
  56. package/template-diy/src/host.ts.tpl +1 -15
  57. package/template-diy/src/plugin.test.ts.tpl +1 -1
  58. package/template-diy/types/globals.d.ts +1 -14
  59. package/template-diy/types/styles.d.ts +0 -10
@@ -0,0 +1,1250 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir, tmpdir } from 'node:os';
3
+ import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import semver from 'semver';
7
+
8
+ import { DESIGN_FILE, acceptanceOf } from './design.mjs';
9
+ import {
10
+ advanceRun,
11
+ artifactFingerprintOf,
12
+ finishRun,
13
+ interruptRun,
14
+ readState,
15
+ removeState,
16
+ resumableState,
17
+ startRun,
18
+ writeState,
19
+ } from './release-journal.mjs';
20
+ import { emit, installOf, marketInstallOf, stateOf } from './release-report.mjs';
21
+ import { authorChangeOf, changelogGateOf } from './release-changelog.mjs';
22
+ import { NeedsUser, ReleaseError, ResumableError, defaultExec, publishTagOf, tailOf } from './release-util.mjs';
23
+ import {
24
+ MarketIndexError,
25
+ marketVersionExists,
26
+ resolveIndexPlan,
27
+ updateMarketIndex,
28
+ verifyMarketArtifact,
29
+ } from './market-index.mjs';
30
+
31
+ export { NeedsUser, ReleaseError, ResumableError };
32
+
33
+ const CLI = fileURLToPath(new URL('../bin/dsh-wb-module.mjs', import.meta.url));
34
+ const SCRIPTS = ['typecheck', 'test', 'selfcheck'];
35
+ const TARGETS = ['team', 'public', 'tarball', 'git', 'market', 'local'];
36
+ const PUBLIC_REGISTRIES = ['https://registry.npmjs.org/', 'https://registry.npmmirror.com/'];
37
+ const REQUIRED_FILES = ['dist/host.js', 'client.js', 'cordis.patch.yml', 'package.json', 'CHANGELOG.md'];
38
+
39
+ function argValue(args, flag) {
40
+ const at = args.indexOf(flag);
41
+ return at === -1 ? undefined : args[at + 1];
42
+ }
43
+
44
+ function hasFlag(args, flag) {
45
+ return args.includes(flag);
46
+ }
47
+
48
+ function parseJson(text) {
49
+ const raw = String(text ?? '');
50
+ const start = raw.search(/[[{]/);
51
+ if (start === -1) return undefined;
52
+ try {
53
+ return JSON.parse(raw.slice(start));
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ function quote(value) {
60
+ const text = String(value);
61
+ return /[\s"'$`\\]/.test(text) ? `'${text.replace(/'/g, `'\\''`)}'` : text;
62
+ }
63
+
64
+ function readManifest(root) {
65
+ const file = join(root, 'package.json');
66
+ if (!existsSync(file)) throw new ReleaseError(`${root} 里没有 package.json —— 这不是一个包目录`, 2);
67
+ let manifest;
68
+ try {
69
+ manifest = JSON.parse(readFileSync(file, 'utf8'));
70
+ } catch {
71
+ throw new ReleaseError('package.json 不是合法 JSON');
72
+ }
73
+ if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {
74
+ throw new ReleaseError('package.json 不是一个对象');
75
+ }
76
+ return manifest;
77
+ }
78
+
79
+ function writeManifest(root, manifest) {
80
+ writeFileSync(join(root, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
81
+ }
82
+
83
+ function dshHomeOf(home) {
84
+ return home ?? process.env.DSH_HOME?.trim() ?? join(homedir(), '.dsh');
85
+ }
86
+
87
+ function artifactsDirOf(home, unit) {
88
+ return join(dshHomeOf(home), 'workbench', 'artifacts', unit);
89
+ }
90
+
91
+ function normalizeRegistry(url) {
92
+ const value = String(url ?? '').trim();
93
+ if (value === '') return '';
94
+ return value.endsWith('/') ? value : `${value}/`;
95
+ }
96
+
97
+ function isPublicRegistry(registry) {
98
+ return PUBLIC_REGISTRIES.includes(normalizeRegistry(registry));
99
+ }
100
+
101
+ function findNpmrc(root) {
102
+ let dir = resolve(root);
103
+ for (;;) {
104
+ const file = join(dir, '.npmrc');
105
+ if (existsSync(file)) return file;
106
+ const parent = dirname(dir);
107
+ if (parent === dir) return undefined;
108
+ dir = parent;
109
+ }
110
+ }
111
+
112
+ function hasAuth(file) {
113
+ if (file === undefined) return false;
114
+ try {
115
+ return /(_authToken|_auth\s*=|always-auth)/i.test(readFileSync(file, 'utf8'));
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ function registryInNpmrc(file, name) {
122
+ if (file === undefined) return '';
123
+ let general = '';
124
+ const scope = name.startsWith('@') ? name.slice(0, name.indexOf('/')) : '';
125
+ for (const raw of readFileSync(file, 'utf8').split('\n')) {
126
+ const line = raw.trim();
127
+ if (line === '' || line.startsWith('#') || line.startsWith(';')) continue;
128
+ const at = line.indexOf('=');
129
+ if (at === -1) continue;
130
+ const key = line.slice(0, at).trim();
131
+ const value = normalizeRegistry(line.slice(at + 1).trim());
132
+ if (value === '') continue;
133
+ if (scope !== '' && key === `${scope}:registry`) return value;
134
+ if (key === 'registry') general = value;
135
+ }
136
+ return general;
137
+ }
138
+
139
+ function configuredRegistry(root, manifest) {
140
+ const npmrc = findNpmrc(root);
141
+ return (
142
+ normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
143
+ normalizeRegistry(manifest.publishConfig?.registry) ||
144
+ registryInNpmrc(npmrc, String(manifest.name ?? ''))
145
+ );
146
+ }
147
+
148
+ function targetOf(root, manifest, args, indexPlan) {
149
+ const asked = argValue(args, '--target');
150
+ if (asked !== undefined) {
151
+ if (!TARGETS.includes(asked)) throw new ReleaseError(`--target 只认 ${TARGETS.join(' / ')}`);
152
+ return asked;
153
+ }
154
+ if (hasFlag(args, '--public')) return 'public';
155
+ if (hasFlag(args, '--git') || argValue(args, '--git') !== undefined) return 'git';
156
+ if (argValue(args, '--out') !== undefined) return 'tarball';
157
+ const flagged = normalizeRegistry(argValue(args, '--registry'));
158
+ if (flagged !== '') return isPublicRegistry(flagged) ? 'public' : 'team';
159
+ if (indexPlan?.policy === 'artifacts-in-repo') return 'market';
160
+ const configured = configuredRegistry(root, manifest);
161
+ if (configured !== '') return isPublicRegistry(configured) ? 'public' : 'team';
162
+ return 'tarball';
163
+ }
164
+
165
+ function licenseFileOf(root) {
166
+ for (const name of ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING']) {
167
+ if (existsSync(join(root, name))) return name;
168
+ }
169
+ return undefined;
170
+ }
171
+
172
+ function licenseOf(root, manifest, target, args) {
173
+ const given = argValue(args, '--license') ?? manifest.license;
174
+ if (typeof given === 'string' && given.trim() !== '') return given.trim();
175
+ const file = licenseFileOf(root);
176
+ if (file !== undefined) return `SEE LICENSE IN ${file}`;
177
+ if (target === 'public') {
178
+ throw new NeedsUser('license', '公开发布需要一个许可证。用 MIT、Apache-2.0,还是仓库里已有的 LICENSE?', undefined, {
179
+ target,
180
+ });
181
+ }
182
+ if (target === 'team') return 'UNLICENSED';
183
+ return undefined;
184
+ }
185
+
186
+ function repositoryOf(root, exec) {
187
+ const result = exec('git', ['-C', root, 'remote', 'get-url', 'origin'], { capture: true, allowFailure: true });
188
+ const url = result.status === 0 ? result.stdout.trim() : '';
189
+ return url === '' ? undefined : { type: 'git', url };
190
+ }
191
+
192
+ function bumpVersion(version, kind) {
193
+ const next = semver.inc(String(version), kind);
194
+ if (next === null) throw new ReleaseError(`当前版本号读不出来:${String(version)}`);
195
+ return next;
196
+ }
197
+
198
+ function nextVersion(current, asked, exists) {
199
+ if (asked !== undefined) {
200
+ const version = /^(major|minor|patch)$/.test(asked) ? bumpVersion(current, asked) : asked;
201
+ if (semver.valid(version) === null) {
202
+ throw new ReleaseError(`--version 只认 patch / minor / major / x.y.z(收到「${asked}」)`);
203
+ }
204
+ if (exists(version)) throw new ReleaseError(`${version} 已经被占了 —— 换一个版本`);
205
+ return version;
206
+ }
207
+ if (!exists(current)) return current;
208
+ for (const kind of ['patch', 'minor', 'major']) {
209
+ const version = bumpVersion(current, kind);
210
+ if (!exists(version)) return version;
211
+ }
212
+ throw new ReleaseError('版本号没法自动加 —— 手动给 --version');
213
+ }
214
+
215
+ function loginCommandOf(registry) {
216
+ return `npm login --registry ${registry}`;
217
+ }
218
+
219
+ function npmrcArgs(npmrc) {
220
+ return npmrc === undefined ? [] : ['--userconfig', npmrc];
221
+ }
222
+
223
+ function checkLogin(exec, info) {
224
+ const argv = ['whoami', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
225
+ const result = exec('npm', argv, { capture: true, allowFailure: true });
226
+ if (result.status === 0) return;
227
+ const text = `${result.stdout}\n${result.stderr}`;
228
+ if (/E404|404|not found/i.test(text)) return;
229
+ if (/ENEEDAUTH|not logged in|401|Unauthorized|E401|E403|403/i.test(text)) {
230
+ throw new NeedsUser(
231
+ 'login',
232
+ `${info.target === 'public' ? 'npm' : '团队源'}还没登录。请在终端跑 \`${loginCommandOf(info.registry)}\`,走完登录/验证器再跟我说「好了」,我接着发。`,
233
+ loginCommandOf(info.registry),
234
+ { target: info.target, registry: info.registry },
235
+ );
236
+ }
237
+ throw new ReleaseError(`连不上源(${info.registry}):${tailOf(text)}`);
238
+ }
239
+
240
+ function registryInfoOf(root, manifest, args, target, fallbackRegistry) {
241
+ const name = String(manifest.name ?? '');
242
+ const askedNpmrc = argValue(args, '--npmrc');
243
+ const sourceNpmrc = askedNpmrc === undefined ? findNpmrc(root) : resolve(askedNpmrc);
244
+ if (askedNpmrc !== undefined && !existsSync(sourceNpmrc)) {
245
+ throw new ReleaseError(`--npmrc 指的文件不存在:${sourceNpmrc}`);
246
+ }
247
+ const npmrc = askedNpmrc !== undefined ? sourceNpmrc : hasAuth(sourceNpmrc) ? sourceNpmrc : undefined;
248
+ const flagged = normalizeRegistry(argValue(args, '--registry'));
249
+ let registry;
250
+ if (target === 'public') {
251
+ registry = flagged || 'https://registry.npmjs.org/';
252
+ if (!isPublicRegistry(registry)) {
253
+ throw new ReleaseError('public 目标要公网源;团队私有源用 --target team');
254
+ }
255
+ } else {
256
+ registry =
257
+ flagged ||
258
+ normalizeRegistry(process.env.DSH_WB_PUBLISH_REGISTRY) ||
259
+ normalizeRegistry(manifest.publishConfig?.registry) ||
260
+ normalizeRegistry(fallbackRegistry) ||
261
+ registryInNpmrc(sourceNpmrc, name);
262
+ if (registry === '') {
263
+ throw new NeedsUser(
264
+ 'registry',
265
+ '这个工坊还没配团队源。让公司把 registry 和 token 写进 `.npmrc`;或者我先给你打一个安装包。',
266
+ undefined,
267
+ { target },
268
+ );
269
+ }
270
+ if (isPublicRegistry(registry)) {
271
+ throw new ReleaseError('这个源是公共 npm —— 用 --target public 明确公开发布');
272
+ }
273
+ }
274
+ const access = argValue(args, '--access') ?? manifest.publishConfig?.access ?? (target === 'public' ? 'public' : 'restricted');
275
+ if (access !== 'restricted' && access !== 'public') {
276
+ throw new ReleaseError(`--access 只认 restricted / public(收到「${access}」)`);
277
+ }
278
+ if (target === 'public' && access !== 'public') throw new ReleaseError('public 目标只接受 --access public');
279
+ return { target, kind: 'registry', name, registry, access, npmrc };
280
+ }
281
+
282
+ function tarballInfoOf(root, manifest, args, home) {
283
+ const out = resolve(argValue(args, '--out') ?? artifactsDirOf(home, String(manifest.dsh?.workbench?.unit ?? '')));
284
+ const fileNameOf = (version) => `${String(manifest.name).replace(/^@/, '').replace(/[/\\]/g, '-')}-${version}.tgz`;
285
+ const fileOf = (version) => join(out, fileNameOf(version));
286
+ return {
287
+ target: 'tarball',
288
+ kind: 'tarball',
289
+ out,
290
+ fileNameOf,
291
+ fileOf,
292
+ exists: (version) => existsSync(fileOf(version)),
293
+ };
294
+ }
295
+
296
+ function gitInfoOf(root, manifest, args, exec) {
297
+ const unit = String(manifest.dsh?.workbench?.unit ?? '');
298
+ const top = exec('git', ['-C', root, 'rev-parse', '--show-toplevel'], { capture: true, allowFailure: true });
299
+ if (top.status !== 0) {
300
+ throw new NeedsUser(
301
+ 'git-remote',
302
+ '这个模块目录还不是 git 仓库。先给它一个仓库/配好权限,或改打一个安装包。',
303
+ undefined,
304
+ { target: 'git' },
305
+ );
306
+ }
307
+ const toplevel = top.stdout.trim();
308
+ const branchResult = exec('git', ['-C', toplevel, 'rev-parse', '--abbrev-ref', 'HEAD'], {
309
+ capture: true,
310
+ allowFailure: true,
311
+ });
312
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() : '';
313
+ if (branch === '' || branch === 'HEAD') {
314
+ throw new NeedsUser('git-branch', '这个仓库现在不在一个分支上(detached HEAD)。先切回一个分支,再发。', undefined, {
315
+ target: 'git',
316
+ });
317
+ }
318
+ const remote = argValue(args, '--git') ?? 'origin';
319
+ const remoteResult = exec('git', ['-C', toplevel, 'remote', 'get-url', remote], { capture: true, allowFailure: true });
320
+ if (remoteResult.status !== 0 || remoteResult.stdout.trim() === '') {
321
+ throw new NeedsUser('git-remote', `这个仓库没有可推送的远端 ${remote}。先加一个远端,或改打一个安装包。`, undefined, {
322
+ target: 'git',
323
+ remote,
324
+ });
325
+ }
326
+ const url = remoteResult.stdout.trim();
327
+ const packagePath = relative(toplevel, root).split(sep).join('/') || '.';
328
+ const status = exec('git', ['-C', toplevel, 'status', '--porcelain', '--untracked-files=all'], {
329
+ capture: true,
330
+ allowFailure: true,
331
+ });
332
+ if (status.status !== 0) throw new ReleaseError(`读不了 git 状态:${tailOf(status.stderr)}`);
333
+ const prefix = packagePath === '.' ? '' : `${packagePath}/`;
334
+ const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json'];
335
+ const dirty = status.stdout
336
+ .trim()
337
+ .split('\n')
338
+ .filter((line) => line.trim() !== '')
339
+ .map((line) => line.slice(3).trim())
340
+ .filter((path) => {
341
+ if (!path.startsWith(prefix)) return true;
342
+ const rel = path.slice(prefix.length);
343
+ return !allowed.some((one) => rel === one || rel.startsWith(`${one}/`));
344
+ });
345
+ if (dirty.length > 0) {
346
+ throw new NeedsUser(
347
+ 'git-dirty',
348
+ `工作区还有别的未提交改动(${dirty.slice(0, 3).join('、')})。先提交/暂存,再发;或改用安装包。`,
349
+ undefined,
350
+ { target: 'git', paths: dirty.slice(0, 3) },
351
+ );
352
+ }
353
+ const tagOf = (version) => `${unit}@${version}`;
354
+ const exists = (version) =>
355
+ exec('git', ['-C', toplevel, 'rev-parse', '-q', '--verify', `refs/tags/${tagOf(version)}`], {
356
+ capture: true,
357
+ allowFailure: true,
358
+ }).status === 0;
359
+ return { target: 'git', kind: 'git', toplevel, branch, remote, url, packagePath, tagOf, exists };
360
+ }
361
+
362
+ function targetInfoOf(root, manifest, args, home, exec, target, fallbackRegistry) {
363
+ if (target === 'team' || target === 'public') return registryInfoOf(root, manifest, args, target, fallbackRegistry);
364
+ if (target === 'tarball') return tarballInfoOf(root, manifest, args, home);
365
+ if (target === 'git') return gitInfoOf(root, manifest, args, exec);
366
+ return { target: 'local', kind: 'local' };
367
+ }
368
+
369
+ function stage(exec, command, argv, { cwd, capture, err, label }) {
370
+ const result = exec(command, argv, { cwd, capture, allowFailure: true });
371
+ if (result.status === 0) return result;
372
+ if (capture) {
373
+ const text = `${result.stdout}\n${result.stderr}`.trim();
374
+ if (text !== '') err(text.split('\n').slice(-30).join('\n'));
375
+ }
376
+ throw new ReleaseError(`${label} 没过——先修红字,别发`);
377
+ }
378
+
379
+ function runCliStage(exec, root, argv, options) {
380
+ return stage(exec, process.execPath, [CLI, ...argv, '--cwd', root], {
381
+ ...options,
382
+ label: `dsh-wb-module ${argv.join(' ')}`,
383
+ });
384
+ }
385
+
386
+ function runScriptStage(exec, root, script, options) {
387
+ return stage(exec, 'npm', ['run', script], { ...options, label: `npm run ${script}` });
388
+ }
389
+
390
+ function runGate(exec, root, options) {
391
+ runCliStage(exec, root, ['build'], options);
392
+ for (const script of SCRIPTS) runScriptStage(exec, root, script, options);
393
+ }
394
+
395
+ function localFiles(exec, root) {
396
+ const result = exec(
397
+ 'npm',
398
+ ['pack', '--dry-run', '--json', '--ignore-scripts', '--cache', join(tmpdir(), 'dsh-wb-npm-cache')],
399
+ { cwd: root, capture: true, allowFailure: true },
400
+ );
401
+ if (result.status !== 0) throw new ReleaseError(`核不了本地包内容:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
402
+ const parsed = parseJson(result.stdout);
403
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
404
+ const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
405
+ if (files.length === 0) throw new ReleaseError('npm pack 没列出包内容');
406
+ return files;
407
+ }
408
+
409
+ function checkPublished(files) {
410
+ const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
411
+ if (missing.length > 0) throw new ReleaseError(`包里少了 ${missing.join(' / ')} —— 装上会起不来`);
412
+ const leaked = files.filter(
413
+ (one) => one.startsWith('src/') || one.endsWith('.npmrc') || one.startsWith('template') || one.includes('.env'),
414
+ );
415
+ if (leaked.length > 0) throw new ReleaseError(`包里混进了不该发的:${leaked.slice(0, 3).join('、')}`);
416
+ }
417
+
418
+ function checkGitFiles(files) {
419
+ const missing = REQUIRED_FILES.filter((one) => !files.includes(one));
420
+ if (missing.length > 0) throw new ReleaseError(`tag 里少了 ${missing.join(' / ')} —— 装上会起不来`);
421
+ const leaked = files.filter((one) => one.endsWith('.npmrc') || one.includes('.env'));
422
+ if (leaked.length > 0) throw new ReleaseError(`tag 里混进了凭据文件:${leaked.slice(0, 3).join('、')}`);
423
+ }
424
+
425
+ function registryExists(exec, info, name, version) {
426
+ const argv = ['view', `${name}@${version}`, 'version', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)];
427
+ const result = exec('npm', argv, { capture: true, allowFailure: true });
428
+ if (result.status === 0) return true;
429
+ const text = `${result.stdout}\n${result.stderr}`;
430
+ if (/E404|404|No matching version|not found|不存在/i.test(text)) return false;
431
+ throw new ReleaseError(`查团队源失败(${info.registry}):${tailOf(text)}`);
432
+ }
433
+
434
+ function publishArgsOf(info, version) {
435
+ const tag = publishTagOf(version);
436
+ return [
437
+ 'publish',
438
+ '--registry',
439
+ info.registry,
440
+ '--access',
441
+ info.access,
442
+ '--ignore-scripts',
443
+ ...(tag === undefined ? [] : ['--tag', tag]),
444
+ ...npmrcArgs(info.npmrc),
445
+ ];
446
+ }
447
+
448
+ function otpCommandOf(root, info, version) {
449
+ const tag = publishTagOf(version);
450
+ return [
451
+ `cd ${quote(root)}`,
452
+ '&&',
453
+ 'npm',
454
+ 'publish',
455
+ '--registry',
456
+ info.registry,
457
+ '--access',
458
+ info.access,
459
+ '--ignore-scripts',
460
+ ...(tag === undefined ? [] : ['--tag', tag]),
461
+ ...npmrcArgs(info.npmrc),
462
+ ].join(' ');
463
+ }
464
+
465
+ function publishRegistry(exec, root, info, manifest, version) {
466
+ const result = exec('npm', publishArgsOf(info, version), { cwd: root, capture: true, allowFailure: true });
467
+ if (result.status === 0) return;
468
+ const text = `${result.stdout}\n${result.stderr}`;
469
+ if (/EOTP|one-time password|OTP/i.test(text)) {
470
+ throw new NeedsUser(
471
+ 'otp',
472
+ '源要求一次性验证码。构建和门禁都过了,请在终端跑下面这条,按提示输入验证码;完了跟我说「好了」,我来核对。',
473
+ otpCommandOf(root, info, version),
474
+ { target: info.target, name: manifest.name, version, registry: info.registry },
475
+ );
476
+ }
477
+ if (info.target === 'public' && /E403|403|taken|already exists|Forbidden|cannot publish over/i.test(text)) {
478
+ throw new NeedsUser('name', '这个名字在 npm 上已经被占了。换一个:`@<user>/<name>` 或加一个后缀?', undefined, {
479
+ target: info.target,
480
+ name: manifest.name,
481
+ version,
482
+ });
483
+ }
484
+ if (/E403|403|already exists|Forbidden/i.test(text)) {
485
+ throw new NeedsUser('scope', '团队源不让发这个包名(可能被占了,或没有权限)。让管理员确认 scope / 权限后跟我说。', undefined, {
486
+ target: info.target,
487
+ name: manifest.name,
488
+ version,
489
+ });
490
+ }
491
+ if (/E402|402|E404|404|scope|payment|not found/i.test(text)) {
492
+ throw new NeedsUser('scope', '团队源要求包名带 scope。用 `@<scope>/<name>` 吗?', undefined, {
493
+ target: info.target,
494
+ name: manifest.name,
495
+ version,
496
+ });
497
+ }
498
+ throw new ReleaseError(`npm publish 没过:${tailOf(text)}`);
499
+ }
500
+
501
+ function verifyRegistry(exec, info, name, version) {
502
+ const view = exec(
503
+ 'npm',
504
+ ['view', `${name}@${version}`, 'version', 'dist.tarball', '--json', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
505
+ { capture: true, allowFailure: true },
506
+ );
507
+ if (view.status !== 0) {
508
+ throw new ReleaseError(`发完了,但在源上查不到 ${name}@${version} —— 去 registry 确认一下`);
509
+ }
510
+ const packed = exec(
511
+ 'npm',
512
+ ['pack', `${name}@${version}`, '--dry-run', '--json', '--ignore-scripts', '--registry', info.registry, ...npmrcArgs(info.npmrc)],
513
+ { capture: true, allowFailure: true },
514
+ );
515
+ if (packed.status !== 0) throw new ReleaseError(`发完了,但核不了包:${tailOf(`${packed.stdout}\n${packed.stderr}`)}`);
516
+ const parsed = parseJson(packed.stdout);
517
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
518
+ const files = Array.isArray(first?.files) ? first.files.map((one) => String(one.path ?? one)) : [];
519
+ if (files.length === 0) throw new ReleaseError('发完了,但 npm pack 没列出包内容');
520
+ checkPublished(files);
521
+ return files;
522
+ }
523
+
524
+ function packTarball(exec, info, version) {
525
+ mkdirSync(info.out, { recursive: true });
526
+ const result = exec(
527
+ 'npm',
528
+ ['pack', '--json', '--ignore-scripts', '--pack-destination', info.out],
529
+ { capture: true, allowFailure: true },
530
+ );
531
+ if (result.status !== 0) throw new ReleaseError(`npm pack 没过:${tailOf(`${result.stdout}\n${result.stderr}`)}`);
532
+ const parsed = parseJson(result.stdout);
533
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
534
+ const filename = typeof first?.filename === 'string' ? first.filename : info.fileNameOf(version);
535
+ const file = join(info.out, filename);
536
+ if (!existsSync(file)) throw new ReleaseError(`npm pack 说生成了 ${filename},但文件不在 ${info.out}`);
537
+ return file;
538
+ }
539
+
540
+ function gitPathsOf(info) {
541
+ const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
542
+ return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json']
543
+ .map((one) => `${prefix}${one}`)
544
+ .filter((one) => existsSync(join(info.toplevel, one)));
545
+ }
546
+
547
+ function gitCommitOf(exec, info, version, capture, progress = {}) {
548
+ const add = exec('git', ['-C', info.toplevel, 'add', '-f', '--', ...gitPathsOf(info)], {
549
+ capture,
550
+ allowFailure: true,
551
+ });
552
+ if (add.status !== 0) throw new ReleaseError(`git add 没过:${tailOf(`${add.stdout}\n${add.stderr}`)}`);
553
+ const commit = exec(
554
+ 'git',
555
+ ['-C', info.toplevel, 'commit', '-m', `chore(release): ${info.tagOf(version)}`],
556
+ { capture, allowFailure: true },
557
+ );
558
+ if (commit.status !== 0) throw new ReleaseError(`git commit 没过:${tailOf(`${commit.stdout}\n${commit.stderr}`)}`);
559
+ progress.committed = true;
560
+ const tag = exec('git', ['-C', info.toplevel, 'tag', info.tagOf(version)], { capture, allowFailure: true });
561
+ if (tag.status !== 0) throw new ReleaseError(`git tag 没过:${tailOf(`${tag.stdout}\n${tag.stderr}`)}`);
562
+ const head = exec('git', ['-C', info.toplevel, 'rev-parse', 'HEAD'], { capture, allowFailure: true });
563
+ if (head.status !== 0) throw new ReleaseError('读不了刚提交的 commit');
564
+ return head.stdout.trim();
565
+ }
566
+
567
+ function gitPushOf(exec, info, version, capture) {
568
+ const branch = exec(
569
+ 'git',
570
+ ['-C', info.toplevel, 'push', info.remote, `HEAD:refs/heads/${info.branch}`],
571
+ { capture, allowFailure: true },
572
+ );
573
+ if (branch.status !== 0) {
574
+ throw new NeedsUser('git-auth', '推不上去:远端没给这个目录推送权限,或远端有新提交。检查权限/同步后跟我说,我重推。', undefined, {
575
+ target: 'git',
576
+ remote: info.remote,
577
+ tag: info.tagOf(version),
578
+ });
579
+ }
580
+ const tag = exec('git', ['-C', info.toplevel, 'push', info.remote, `refs/tags/${info.tagOf(version)}`], {
581
+ capture,
582
+ allowFailure: true,
583
+ });
584
+ if (tag.status !== 0) {
585
+ throw new NeedsUser('git-auth', '分支推上去了,但 tag 没推上去。检查权限后跟我说,我补推。', undefined, {
586
+ target: 'git',
587
+ remote: info.remote,
588
+ tag: info.tagOf(version),
589
+ });
590
+ }
591
+ }
592
+
593
+ function verifyGit(exec, info, version) {
594
+ const commit = exec('git', ['-C', info.toplevel, 'rev-parse', `${info.tagOf(version)}^{commit}`], {
595
+ capture: true,
596
+ allowFailure: true,
597
+ });
598
+ if (commit.status !== 0) throw new ReleaseError(`发完了,但本地找不到 tag ${info.tagOf(version)}`);
599
+ const tree = exec('git', ['-C', info.toplevel, 'ls-tree', '-r', '--name-only', info.tagOf(version)], {
600
+ capture: true,
601
+ allowFailure: true,
602
+ });
603
+ if (tree.status !== 0) throw new ReleaseError(`核不了 tag ${info.tagOf(version)} 的内容`);
604
+ const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
605
+ const files = tree.stdout
606
+ .split('\n')
607
+ .map((one) => one.trim())
608
+ .filter((one) => one !== '' && one.startsWith(prefix))
609
+ .map((one) => one.slice(prefix.length));
610
+ checkGitFiles(files);
611
+ return { commit: commit.stdout.trim(), files };
612
+ }
613
+
614
+ export async function runRelease({
615
+ root,
616
+ args = [],
617
+ exec = defaultExec,
618
+ home = dshHomeOf(),
619
+ out = console.log,
620
+ err = console.error,
621
+ } = {}) {
622
+ const json = hasFlag(args, '--json');
623
+ const dryRun = hasFlag(args, '--dry-run');
624
+ const verify = hasFlag(args, '--verify');
625
+ const manifest = readManifest(root);
626
+ const unit = manifest?.dsh?.workbench?.unit;
627
+ if (typeof unit !== 'string' || unit === '') throw new ReleaseError('这不是一个 wb 模块包(没有 dsh.workbench.unit)');
628
+ if (typeof manifest.name !== 'string' || manifest.name === '') throw new ReleaseError('package.json 里没有 name');
629
+ if (typeof manifest.version !== 'string' || manifest.version === '') {
630
+ throw new ReleaseError('package.json 里没有 version');
631
+ }
632
+ const earlyState = readState(home, unit);
633
+ const resume = hasFlag(args, '--resume');
634
+ const journal = resume ? resumableState(home, unit) : undefined;
635
+ if (resume && journal === undefined) {
636
+ throw new ReleaseError('没有可续跑的发布记录 —— 去掉 --resume 正常发一次');
637
+ }
638
+ const askedTarget = argValue(args, '--target');
639
+ if (resume && askedTarget !== undefined && askedTarget !== journal?.target) {
640
+ throw new ReleaseError(`这条续跑记录属于出口 ${String(journal?.target)},和 --target ${askedTarget} 不一致`);
641
+ }
642
+ const stateTarget =
643
+ verify && askedTarget === undefined && TARGETS.includes(earlyState?.target) ? earlyState.target : undefined;
644
+ const resumeTarget = resume && askedTarget === undefined ? journal?.target : undefined;
645
+ const needPlan = !verify || stateTarget === 'market' || askedTarget === 'market';
646
+ let indexPlan;
647
+ if (needPlan) {
648
+ try {
649
+ indexPlan = resolveIndexPlan({ args, home, exec });
650
+ } catch (error) {
651
+ if (error instanceof MarketIndexError) {
652
+ throw new NeedsUser(error.step ?? 'index-write', error.message, error.command, {
653
+ target: askedTarget ?? 'market',
654
+ });
655
+ }
656
+ throw error;
657
+ }
658
+ }
659
+ if (resume && indexPlan === undefined && journal?.run?.indexPlan !== undefined) {
660
+ indexPlan = journal.run.indexPlan;
661
+ }
662
+ const target = resumeTarget ?? stateTarget ?? targetOf(root, manifest, args, indexPlan);
663
+ if (target === 'local') {
664
+ const summary = {
665
+ ok: true,
666
+ target: 'local',
667
+ name: manifest.name,
668
+ version: manifest.version,
669
+ changes: [],
670
+ install: '',
671
+ text: '没发,留在本机。',
672
+ };
673
+ emit(out, json, summary);
674
+ return summary;
675
+ }
676
+ if (target === 'market') {
677
+ if (indexPlan === undefined) {
678
+ throw new ReleaseError('没有可写的 git 索引源 —— 用 --index <仓库> 指定,或先在「模块市场 → 发现」里初始化索引');
679
+ }
680
+ if (indexPlan.policy !== 'artifacts-in-repo') {
681
+ throw new ReleaseError('这个索引源是 catalog-only:它只收目录、不放产物。用 --index-policy artifacts-in-repo 或换一个可写仓库');
682
+ }
683
+ }
684
+ const authored = authorChangeOf(root, manifest, exec);
685
+ const authorChange = authored.change;
686
+ if (!verify && !dryRun && authored.author !== undefined) {
687
+ manifest.author = authored.author;
688
+ writeManifest(root, manifest);
689
+ }
690
+ const frozenVersion = resume && typeof journal?.version === 'string' ? journal.version : undefined;
691
+ const fingerprint = artifactFingerprintOf(root);
692
+ const resumeMatchesTree = resume && frozenVersion !== undefined && journal?.run?.fingerprint === fingerprint;
693
+ const tracking = !verify && !dryRun;
694
+ if (tracking) {
695
+ startRun(home, unit, {
696
+ target,
697
+ name: manifest.name,
698
+ resume,
699
+ ...(frozenVersion === undefined ? {} : { version: frozenVersion }),
700
+ fields: {
701
+ fingerprint,
702
+ ...(indexPlan === undefined
703
+ ? {}
704
+ : { indexPlan: { source: indexPlan.source, policy: indexPlan.policy } }),
705
+ },
706
+ });
707
+ }
708
+ const info =
709
+ target === 'market'
710
+ ? { target: 'market', kind: 'market', url: indexPlan.source.url }
711
+ : targetInfoOf(root, manifest, args, home, exec, target, earlyState?.registry);
712
+ const indexPreviewOf = (name, version) =>
713
+ indexPlan === undefined
714
+ ? undefined
715
+ : {
716
+ kind: 'git',
717
+ url: indexPlan.source.url,
718
+ policy: indexPlan.policy,
719
+ entry: `${name}@${version}`,
720
+ updated: false,
721
+ planned: true,
722
+ };
723
+ const indexOf = (manifestNow, version, publish) => {
724
+ if (indexPlan === undefined) return undefined;
725
+ if (target === 'tarball' || target === 'local') {
726
+ return {
727
+ kind: 'git',
728
+ url: indexPlan.source.url,
729
+ entry: `${manifestNow.name}@${version}`,
730
+ updated: false,
731
+ reason: `${target} 不进市场索引`,
732
+ };
733
+ }
734
+ return updateMarketIndex({
735
+ plan: indexPlan,
736
+ root,
737
+ manifest: manifestNow,
738
+ unit,
739
+ version,
740
+ publish,
741
+ exec,
742
+ home,
743
+ now: () => Date.now(),
744
+ });
745
+ };
746
+ const changesOf = (version) => [
747
+ ...(authorChange === undefined ? [] : [authorChange]),
748
+ ...(version === manifest.version ? [] : [`version → ${version}`]),
749
+ ];
750
+ const gateOf = (version) => {
751
+ if (verify || dryRun) return;
752
+ const gate = changelogGateOf(root, version);
753
+ if (gate.ok !== true) throw new NeedsUser('changelog', gate.reason, gate.command, { target, version });
754
+ };
755
+ const askedVersion = argValue(args, '--version');
756
+ const targetVersionOf = () => {
757
+ if (info.kind === 'tarball' || info.kind === 'git') {
758
+ return frozenVersion ?? nextVersion(manifest.version, askedVersion, info.exists);
759
+ }
760
+ if (info.kind === 'registry') {
761
+ return frozenVersion ?? nextVersion(manifest.version, askedVersion, (one) => registryExists(exec, info, manifest.name, one));
762
+ }
763
+ if (info.kind === 'market' && indexPlan !== undefined) {
764
+ return (
765
+ frozenVersion ??
766
+ nextVersion(manifest.version, askedVersion, (one) =>
767
+ marketVersionExists({ plan: indexPlan, exec, home, name: manifest.name, version: one }),
768
+ )
769
+ );
770
+ }
771
+ return frozenVersion ?? nextVersion(manifest.version, askedVersion, () => false);
772
+ };
773
+ if (!verify && !dryRun) gateOf(targetVersionOf());
774
+ if (verify) {
775
+ const state = earlyState;
776
+ const version = argValue(args, '--version') ?? state?.version ?? manifest.version;
777
+ if (info.kind === 'market') {
778
+ if (indexPlan === undefined) throw new ReleaseError('没有可写的 git 索引源,核不了这次的产物');
779
+ const commit = typeof state?.commit === 'string' ? state.commit : '';
780
+ if (commit === '') throw new ReleaseError('没有记录上次发布的 commit —— 先正常发一次再核');
781
+ let checked;
782
+ try {
783
+ checked = verifyMarketArtifact({ plan: indexPlan, exec, home, unit, commit });
784
+ } catch (error) {
785
+ throw new ReleaseError(error instanceof Error ? error.message : String(error));
786
+ }
787
+ const summary = {
788
+ ok: true,
789
+ verified: true,
790
+ target,
791
+ name: manifest.name,
792
+ version,
793
+ files: checked.files,
794
+ market: {
795
+ kind: 'git',
796
+ url: indexPlan.source.url,
797
+ branch: state?.branch,
798
+ commit,
799
+ path: checked.path,
800
+ entry: `${manifest.name}@${version}`,
801
+ updated: true,
802
+ },
803
+ changes: [],
804
+ install: marketInstallOf(indexPlan.source.url, commit, checked.path),
805
+ };
806
+ writeState(home, unit, { ...state, status: 'verified' });
807
+ emit(out, json, summary);
808
+ return summary;
809
+ }
810
+ if (info.kind === 'registry') {
811
+ const files = verifyRegistry(exec, info, manifest.name, version);
812
+ const summary = {
813
+ ok: true,
814
+ verified: true,
815
+ target,
816
+ name: manifest.name,
817
+ version,
818
+ registry: info.registry,
819
+ files,
820
+ changes: [],
821
+ install: installOf(info, manifest.name, version),
822
+ };
823
+ writeState(home, unit, { ...stateOf(info, manifest, version, 'verified') });
824
+ emit(out, json, summary);
825
+ return summary;
826
+ }
827
+ if (info.kind === 'tarball') {
828
+ const file = state?.file ?? info.fileOf(version);
829
+ if (!existsSync(file)) throw new ReleaseError(`找不到安装包 ${file}`);
830
+ const summary = {
831
+ ok: true,
832
+ verified: true,
833
+ target,
834
+ name: manifest.name,
835
+ version,
836
+ file,
837
+ files: [],
838
+ changes: [],
839
+ install: installOf(info, manifest.name, version),
840
+ };
841
+ writeState(home, unit, { ...stateOf(info, manifest, version, 'verified') });
842
+ emit(out, json, summary);
843
+ return summary;
844
+ }
845
+ const checked = verifyGit(exec, info, version);
846
+ const summary = {
847
+ ok: true,
848
+ verified: true,
849
+ target,
850
+ name: manifest.name,
851
+ version,
852
+ files: checked.files,
853
+ git: { url: info.url, tag: info.tagOf(version), commit: checked.commit },
854
+ changes: [],
855
+ install: installOf(info, manifest.name, version),
856
+ };
857
+ writeState(home, unit, { ...stateOf(info, manifest, version, 'verified') });
858
+ emit(out, json, summary);
859
+ return summary;
860
+ }
861
+ const existing = earlyState;
862
+ if (
863
+ !dryRun &&
864
+ argValue(args, '--version') === undefined &&
865
+ info.kind === 'registry' &&
866
+ existing?.status === 'published' &&
867
+ existing.target === target &&
868
+ existing.name === manifest.name &&
869
+ existing.version === manifest.version
870
+ ) {
871
+ const version = manifest.version;
872
+ const files = verifyRegistry(exec, info, manifest.name, version);
873
+ writeState(home, unit, stateOf(info, manifest, version, 'verified'));
874
+ if (tracking) finishRun(home, unit, { version });
875
+ const summary = {
876
+ ok: true,
877
+ verified: true,
878
+ target,
879
+ name: manifest.name,
880
+ version,
881
+ registry: info.registry,
882
+ changes: [],
883
+ files,
884
+ install: installOf(info, manifest.name, version),
885
+ market: indexOf(manifest, version, { registry: info.registry }),
886
+ };
887
+ emit(out, json, summary);
888
+ return summary;
889
+ }
890
+ if (
891
+ !dryRun &&
892
+ argValue(args, '--version') === undefined &&
893
+ target === 'git' &&
894
+ existing?.target === 'git' &&
895
+ existing.status === 'committed' &&
896
+ existing.tag === info.tagOf(String(existing.version ?? ''))
897
+ ) {
898
+ const version = String(existing.version);
899
+ gitPushOf(exec, info, version, json);
900
+ const checked = verifyGit(exec, info, version);
901
+ writeState(home, unit, { ...stateOf(info, manifest, version, 'verified'), commit: checked.commit });
902
+ if (tracking) finishRun(home, unit, { version });
903
+ const summary = {
904
+ ok: true,
905
+ verified: true,
906
+ target,
907
+ name: manifest.name,
908
+ version,
909
+ files: checked.files,
910
+ changes: [],
911
+ git: { url: info.url, tag: info.tagOf(version), commit: checked.commit },
912
+ install: installOf(info, manifest.name, version),
913
+ market: indexOf(manifest, version, {
914
+ git: { url: info.url, commit: checked.commit, path: info.packagePath },
915
+ }),
916
+ };
917
+ emit(out, json, summary);
918
+ return summary;
919
+ }
920
+ if (!dryRun && !hasFlag(args, '--yes')) {
921
+ throw new ReleaseError('还没发:确认后加 --yes 真发;只想预览就加 --dry-run');
922
+ }
923
+ if (!resumeMatchesTree) {
924
+ if (!existsSync(join(root, DESIGN_FILE))) {
925
+ throw new NeedsUser('acceptance', '还没有 design.json。先在真卡片上逐条走通,再 design accept。', undefined, {
926
+ target,
927
+ });
928
+ }
929
+ const acceptance = acceptanceOf(root);
930
+ if (acceptance.total === 0) {
931
+ throw new NeedsUser('acceptance', '验收清单是空的。先在真卡片上逐条走通,再 design accept。', undefined, { target });
932
+ }
933
+ if (acceptance.checked < acceptance.total) {
934
+ throw new NeedsUser(
935
+ 'acceptance',
936
+ `还有 ${acceptance.total - acceptance.checked} 条验收没打勾(${acceptance.checked}/${acceptance.total})。先在真卡片上走完,确认没问题我再发。`,
937
+ undefined,
938
+ { target },
939
+ );
940
+ }
941
+ }
942
+ const earlyLicense = info.kind === 'registry' ? licenseOf(root, manifest, target, args) : undefined;
943
+ if (info.kind === 'registry') checkLogin(exec, info);
944
+ const capture = json;
945
+ const stageOptions = { cwd: root, capture, err };
946
+ if (!resumeMatchesTree) runGate(exec, root, stageOptions);
947
+ if (tracking) advanceRun(home, unit, { phase: 'gates' });
948
+
949
+ if (info.kind === 'market') {
950
+ const exists = (one) =>
951
+ marketVersionExists({ plan: indexPlan, exec, home, name: manifest.name, version: one });
952
+ const version =
953
+ frozenVersion ?? nextVersion(manifest.version, argValue(args, '--version'), exists);
954
+ const files = localFiles(exec, root);
955
+ checkPublished(files);
956
+ const summary = {
957
+ ok: true,
958
+ dryRun,
959
+ target,
960
+ name: manifest.name,
961
+ version,
962
+ files,
963
+ changes: changesOf(version),
964
+ install: '',
965
+ market: indexPreviewOf(manifest.name, version),
966
+ };
967
+ if (dryRun) {
968
+ emit(out, json, summary);
969
+ return summary;
970
+ }
971
+ if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
972
+ if (tracking) advanceRun(home, unit, { phase: 'version', version });
973
+ const next = { ...manifest, version };
974
+ const index = updateMarketIndex({
975
+ plan: indexPlan,
976
+ root,
977
+ manifest: next,
978
+ unit,
979
+ version,
980
+ publish: {},
981
+ exec,
982
+ home,
983
+ now: () => Date.now(),
984
+ ...(journal?.run === undefined ? {} : { resume: journal.run }),
985
+ ...(tracking ? { onStep: (step) => advanceRun(home, unit, step) } : {}),
986
+ });
987
+ if (index.updated !== true || index.commit === undefined || index.commit === '') {
988
+ const failure = {
989
+ class: index.code === 'git-market-protected' ? 'human' : 'transient',
990
+ code: index.code ?? 'index-write',
991
+ text: index.reason ?? '没拿到产物 commit',
992
+ phase: index.phase ?? 'index',
993
+ };
994
+ if (tracking) interruptRun(home, unit, failure);
995
+ throw new ResumableError(`发布中断在第 ${String(index.phase ?? 'index')} 步:${failure.text}`, {
996
+ unit,
997
+ target,
998
+ version,
999
+ phase: index.phase ?? 'index',
1000
+ ...(index.branch === undefined ? {} : { branch: index.branch }),
1001
+ });
1002
+ }
1003
+ writeManifest(root, next);
1004
+ const path = index.path ?? `packages/${unit}`;
1005
+ const install = marketInstallOf(index.url, index.commit, path);
1006
+ const journalNow = tracking ? readState(home, unit)?.run : undefined;
1007
+ writeState(home, unit, {
1008
+ target: 'market',
1009
+ name: next.name,
1010
+ version,
1011
+ status: 'verified',
1012
+ at: new Date().toISOString(),
1013
+ url: index.url,
1014
+ branch: index.branch,
1015
+ commit: index.commit,
1016
+ path,
1017
+ ...(tracking
1018
+ ? { run: { ...(journalNow ?? {}), state: 'done', phase: 'done', updatedAt: new Date().toISOString() } }
1019
+ : {}),
1020
+ });
1021
+ summary.market = { ...index, commits: 2 };
1022
+ summary.install = install;
1023
+ emit(out, json, summary);
1024
+ return summary;
1025
+ }
1026
+
1027
+ if (info.kind === 'tarball') {
1028
+ const version = frozenVersion ?? nextVersion(manifest.version, argValue(args, '--version'), info.exists);
1029
+ const files = localFiles(exec, root);
1030
+ checkPublished(files);
1031
+ const summary = {
1032
+ ok: true,
1033
+ dryRun,
1034
+ target,
1035
+ name: manifest.name,
1036
+ version,
1037
+ file: info.fileOf(version),
1038
+ files,
1039
+ changes: changesOf(version),
1040
+ install: installOf(info, manifest.name, version),
1041
+ };
1042
+ if (dryRun) {
1043
+ emit(out, json, summary);
1044
+ return summary;
1045
+ }
1046
+ if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
1047
+ const next = { ...manifest, version };
1048
+ writeManifest(root, next);
1049
+ if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
1050
+ writeState(home, unit, stateOf(info, next, version, 'intent'));
1051
+ let packed = false;
1052
+ try {
1053
+ const file = packTarball(exec, info, version);
1054
+ packed = true;
1055
+ writeState(home, unit, { ...stateOf(info, next, version, 'published'), file });
1056
+ const packedFiles = localFiles(exec, root);
1057
+ checkPublished(packedFiles);
1058
+ writeState(home, unit, { ...stateOf(info, next, version, 'verified'), file });
1059
+ if (tracking) finishRun(home, unit, { version });
1060
+ summary.file = file;
1061
+ summary.files = packedFiles;
1062
+ summary.market = indexOf(next, version, {});
1063
+ emit(out, json, summary);
1064
+ return summary;
1065
+ } catch (error) {
1066
+ if (!packed) {
1067
+ writeManifest(root, manifest);
1068
+ if (tracking) {
1069
+ interruptRun(home, unit, { code: 'tarball', text: String(error?.message ?? error) });
1070
+ } else {
1071
+ removeState(home, unit);
1072
+ }
1073
+ }
1074
+ throw error;
1075
+ }
1076
+ }
1077
+
1078
+ if (info.kind === 'git') {
1079
+ const version = frozenVersion ?? nextVersion(manifest.version, argValue(args, '--version'), info.exists);
1080
+ const files = localFiles(exec, root);
1081
+ checkPublished(files);
1082
+ const summary = {
1083
+ ok: true,
1084
+ dryRun,
1085
+ target,
1086
+ name: manifest.name,
1087
+ version,
1088
+ files,
1089
+ changes: changesOf(version),
1090
+ git: { url: info.url, tag: info.tagOf(version) },
1091
+ install: installOf(info, manifest.name, version),
1092
+ ...(dryRun ? { market: indexPreviewOf(manifest.name, version) } : {}),
1093
+ };
1094
+ if (dryRun) {
1095
+ emit(out, json, summary);
1096
+ return summary;
1097
+ }
1098
+ if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
1099
+ const next = { ...manifest, version };
1100
+ writeManifest(root, next);
1101
+ if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
1102
+ writeState(home, unit, stateOf(info, next, version, 'intent'));
1103
+ const progress = { committed: false };
1104
+ try {
1105
+ const commit = gitCommitOf(exec, info, version, capture, progress);
1106
+ writeState(home, unit, { ...stateOf(info, next, version, 'committed'), commit });
1107
+ gitPushOf(exec, info, version, capture);
1108
+ const checked = verifyGit(exec, info, version);
1109
+ writeState(home, unit, {
1110
+ ...stateOf(info, next, version, 'verified'),
1111
+ commit: checked.commit,
1112
+ });
1113
+ if (tracking) finishRun(home, unit, { version });
1114
+ summary.files = checked.files;
1115
+ summary.git.commit = checked.commit;
1116
+ summary.market = indexOf(next, version, {
1117
+ git: { url: info.url, commit: checked.commit, path: info.packagePath },
1118
+ });
1119
+ emit(out, json, summary);
1120
+ return summary;
1121
+ } catch (error) {
1122
+ if (!progress.committed) {
1123
+ writeManifest(root, manifest);
1124
+ if (tracking) {
1125
+ interruptRun(home, unit, { code: 'git', text: String(error?.message ?? error) });
1126
+ } else {
1127
+ removeState(home, unit);
1128
+ }
1129
+ }
1130
+ throw error;
1131
+ }
1132
+ }
1133
+
1134
+ const original = globalThis.structuredClone(manifest);
1135
+ const next = { ...manifest };
1136
+ const changes = [];
1137
+ if (authorChange !== undefined) changes.push(authorChange);
1138
+ const license = earlyLicense;
1139
+ if (license !== undefined && next.license !== license) {
1140
+ next.license = license;
1141
+ changes.push(`license → ${license}`);
1142
+ }
1143
+ if (next.repository === undefined) {
1144
+ const repository = repositoryOf(root, exec);
1145
+ if (repository !== undefined) {
1146
+ next.repository = repository;
1147
+ changes.push('repository ← git remote');
1148
+ }
1149
+ }
1150
+ const publishConfig = { ...(next.publishConfig ?? {}), registry: info.registry, access: info.access };
1151
+ if (!isDeepStrictEqual(publishConfig, next.publishConfig ?? {})) {
1152
+ next.publishConfig = publishConfig;
1153
+ changes.push(`publishConfig → ${info.registry}(${info.access})`);
1154
+ }
1155
+ if (next.private === true) {
1156
+ delete next.private;
1157
+ changes.push('摘掉 private');
1158
+ }
1159
+ const version =
1160
+ frozenVersion ?? nextVersion(next.version, argValue(args, '--version'), (one) => registryExists(exec, info, next.name, one));
1161
+ if (version !== next.version) {
1162
+ next.version = version;
1163
+ changes.push(`version → ${version}`);
1164
+ }
1165
+ if (dryRun) {
1166
+ if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
1167
+ const files = localFiles(exec, root);
1168
+ checkPublished(files);
1169
+ const summary = {
1170
+ ok: true,
1171
+ dryRun: true,
1172
+ target,
1173
+ name: next.name,
1174
+ version,
1175
+ registry: info.registry,
1176
+ changes,
1177
+ files,
1178
+ install: installOf(info, next.name, version),
1179
+ market: indexPreviewOf(next.name, version),
1180
+ };
1181
+ emit(out, json, summary);
1182
+ return summary;
1183
+ }
1184
+ writeManifest(root, next);
1185
+ if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
1186
+ let published = false;
1187
+ try {
1188
+ if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
1189
+ writeState(home, unit, stateOf(info, next, version, 'intent'));
1190
+ publishRegistry(exec, root, info, next, version);
1191
+ published = true;
1192
+ const files = verifyRegistry(exec, info, next.name, version);
1193
+ writeState(home, unit, stateOf(info, next, version, 'verified'));
1194
+ if (tracking) finishRun(home, unit, { version });
1195
+ const summary = {
1196
+ ok: true,
1197
+ verified: true,
1198
+ target,
1199
+ name: next.name,
1200
+ version,
1201
+ registry: info.registry,
1202
+ changes,
1203
+ files,
1204
+ install: installOf(info, next.name, version),
1205
+ market: indexOf(next, version, { registry: info.registry }),
1206
+ };
1207
+ emit(out, json, summary);
1208
+ return summary;
1209
+ } catch (error) {
1210
+ if (!published && !(error instanceof NeedsUser && error.step === 'otp')) {
1211
+ writeManifest(root, original);
1212
+ if (tracking) {
1213
+ interruptRun(home, unit, { code: 'registry', text: String(error?.message ?? error) });
1214
+ } else {
1215
+ removeState(home, unit);
1216
+ }
1217
+ }
1218
+ if (published) writeState(home, unit, stateOf(info, next, version, 'published'));
1219
+ throw error;
1220
+ }
1221
+ }
1222
+
1223
+ export async function release({ root, args = [] }) {
1224
+ try {
1225
+ return await runRelease({ root, args });
1226
+ } catch (error) {
1227
+ if (error instanceof NeedsUser) {
1228
+ if (hasFlag(args, '--json')) {
1229
+ process.stdout.write(`${JSON.stringify({ ok: false, target: error.extra.target, needsUser: error.toJSON() })}\n`);
1230
+ } else {
1231
+ console.error(error.text);
1232
+ }
1233
+ process.exit(3);
1234
+ }
1235
+ if (error instanceof ResumableError) {
1236
+ if (hasFlag(args, '--json')) {
1237
+ process.stdout.write(`${JSON.stringify(error.toJSON())}\n`);
1238
+ } else {
1239
+ console.error(`✖ ${error.message}`);
1240
+ console.error(' 这是可续跑的中断:加 --resume 再跑一次。');
1241
+ }
1242
+ process.exit(4);
1243
+ }
1244
+ if (error instanceof ReleaseError) {
1245
+ console.error(`✖ ${error.message}`);
1246
+ process.exit(error.code);
1247
+ }
1248
+ throw error;
1249
+ }
1250
+ }