@dsh-workbench/module-tools 1.1.14-beta.1 → 1.1.14

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
@@ -8,7 +8,7 @@
8
8
  ```bash
9
9
  pnpm exec dsh-wb-module init my-unit # 建骨架(唯一模板,已默认)
10
10
  pnpm build # dist/host.js + 套了 loader 壳的 client.js
11
- pnpm check # 十三条判据
11
+ pnpm check # 十四条判据
12
12
  pnpm design check # 设计底稿有没有空格、README 有没有落后
13
13
  ```
14
14
 
@@ -50,7 +50,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
50
50
  | `dsh-wb-module init <unit> [--dir <目录>] [--template diy] [--description "一句话"]` | 建模块骨架(只有 `diy` 一个模板,也是默认) |
51
51
  | `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market/` + README,推首个 commit |
52
52
  | `dsh-wb-module build` | 出 `dist/host.js`(单文件 ESM)与 `client.js`(套 loader 壳) |
53
- | `dsh-wb-module check [--only <判据>] [--package <包名或 unit>]` | 十三条判据;`--only` 单跑,也当查询口用;`--package` 只报这个模块的问题(见下);`--list [--json\|--markdown]` 只列判据元数据 |
53
+ | `dsh-wb-module check [--only <判据>] [--package <包名或 unit>]` | 十四条判据;`--only` 单跑,也当查询口用;`--package` 只报这个模块的问题(见下);`--list [--json\|--markdown]` 只列判据元数据 |
54
54
  | `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
55
55
  | `dsh-wb-module sync` | 按清单生成 `src/declared-tools.ts`,按 `design.json` 的 `key + recipe` 生成 `src/datasource.ts` |
56
56
  | `dsh-wb-module design <init\|check\|render\|accept>` | 设计底稿:填、查空格、生成 README、验收打勾 |
@@ -68,7 +68,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
68
68
  「我这个模块绿」不等于「工作区绿」。门禁 / 发布 / release 不要带它。要只让判据看见一个包,
69
69
  用环境变量 `DSH_WB_ROOT=<包目录>`(工具链自己的自检就是这么隔离的)。
70
70
 
71
- ## 十三条判据
71
+ ## 十四条判据
72
72
 
73
73
  `dsh-wb-module check` 一次跑完,三态输出:`✔ 通过` / `○ 不适用` / `✖ 不过`;有任何不过就退出 1。
74
74
 
@@ -81,6 +81,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
81
81
  | `protocol` | manifest | dsh.workbench 的 apiVersion、静态半形状与未知键 |
82
82
  | `bundles` | build | 宿主产物不含浏览器侧、浏览器产物不内联外壳 CSS 与夹具 |
83
83
  | `source` | source | 零注释、包与运行时入口分开、不手写进程/锁/原子写 |
84
+ | `seam` | source | 子进程只走 deps.seam 门面;不取原始 shell / 不裸 child_process;清单 writes 与请求一致 |
84
85
  | `arch` | source | 模块自己的层序、运行时边界与设计系统配置 |
85
86
  | `lint` | source | 模块自己的 eslint.config.mjs 与规则集 |
86
87
  | `primitives` | build | dsh 包只从 kit 取,模块自己声明 external |
@@ -101,6 +102,22 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
101
102
  现存违规收在各自的豁免表里,是棘轮:迁一处删一行,表里已经不再命中的行也会当场报红。
102
103
  判据**故意窄** —— 模块自己用 div 拼一张不借槽类的表单不判,判宽了就是每次改界面都先撞门禁。
103
104
 
105
+ ### `seam`:模块只走执行缝门面
106
+
107
+ 模块在宿主侧起子进程,只能走外壳放进 `mount(deps)` 的门面 `deps.seam`。判据查三件事:
108
+
109
+ - 宿主侧可达的源码里不许出现 `runShell` / `shellOf`、`ctx.get('shell')` / `ctx.get('workbenchShell')` /
110
+ `ctx.get('seam')`、`node:child_process` / `child_process`;
111
+ - `dsh.workbench.host.writes` 只认 `"unit"` / `"workspace"` 两档(`protocol` 判据也查形状,这里再兜一次);
112
+ - 源码里以 `scope: 'unit'` / `scope: 'workspace'` 请求的档,清单 `host.writes` 里必须有。
113
+
114
+ 修法:清单里写 `"host": { "inject": [], "writes": ["unit"] }`,代码里
115
+ `deps.seam?.run({ scope: 'unit', command })`。`unit` 档可写 `<运行时根>/units/<unit>/`(模块自己的数据目录,
116
+ `HOME` 与 npm/pnpm 缓存也注入在那里),`workspace` 档写当前会话工作区、还要带上会话对象;
117
+ 不写 `writes` = 不给门面(`deps.seam` 是 `undefined`)。模块宿主代码与外壳同进程 —— 这层是
118
+ **约定 + 门禁 + 归因**,不是安全边界。完整写法见
119
+ [BUILD.md](../core/skills/workbench-module-diy/BUILD.md) §5。
120
+
104
121
  ### `arch`:模块包的分层方言
105
122
 
106
123
  卡片一复杂就往 `src/client.tsx` 里堆,宿主侧顺手 import 一个组件(`dist/host.js` 在 Node 里
@@ -79,6 +79,8 @@ const RELEASE_HELP = `
79
79
  --index auto|none|<目录|git 地址> 发布后要更新的索引源
80
80
  --index-source <id> --index-ref <分支> --index-path <索引路径> --index-policy artifacts-in-repo|catalog-only
81
81
  --resume 上次中断后接着发(用记录里的出口与版本)
82
+ --workspace-only 只做写工作区那几步(构建/检查/提交),索引与产物流给 --index-only
83
+ --index-only 接着 --workspace-only 的记录只做产品侧(索引/发布/产物)
82
84
  --dry-run --verify --yes --json`;
83
85
 
84
86
  const MARKET_INIT_HELP = `
@@ -97,7 +99,7 @@ release --changelog-note "这一版改了什么"(文件不在会先建)。`;
97
99
 
98
100
  const COMMANDS = {
99
101
  build: '出 dist/host.js(单文件 ESM)与 client.js(套 loader 壳)',
100
- check: '十三条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
102
+ check: '十四条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
101
103
  init: '建模块骨架(--dir / --template diy / --description;只有 diy 一个模板)',
102
104
  'market-init': '初始化市场索引仓库(写 market/ 与 README,推首个 commit)',
103
105
  design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.1.14-beta.1",
3
+ "version": "1.1.14",
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.14-beta.1"
36
+ "@dsh-workbench/shared": "1.1.14"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
package/src/check.mjs CHANGED
@@ -25,6 +25,12 @@ export const CHECKS = {
25
25
  layer: 'source',
26
26
  load: () => import('./checks/source.mjs'),
27
27
  },
28
+ seam: {
29
+ title: '模块执行缝(写入域)',
30
+ summary: '子进程只走 deps.seam 门面;不取原始 shell / 不裸 child_process;清单 writes 与请求一致',
31
+ layer: 'source',
32
+ load: () => import('./checks/seam.mjs'),
33
+ },
28
34
  arch: {
29
35
  title: '模块架构(分层 / 边界 / 设计系统)',
30
36
  summary: '模块自己的层序、运行时边界与设计系统配置',
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { relative } from 'node:path';
3
+
4
+ import { discoverModulePackages } from '../discover.mjs';
5
+ import { reachableFrom } from './source.mjs';
6
+
7
+ export const title = '模块执行缝(写入域)';
8
+
9
+ const SCOPE_USE = /scope\s*:\s*['"](unit|workspace)['"]/g;
10
+
11
+ const RAW_SEAM = [
12
+ { pattern: /\brunShell\b/, what: '用了原始 shell 的 runShell(导入或调用)' },
13
+ { pattern: /\bshellOf\b/, what: '用了原始 shell 的 shellOf(导入或调用)' },
14
+ {
15
+ pattern: /ctx\s*\.\s*get\s*\(\s*['"`](shell|workbenchShell|seam)['"`]/,
16
+ what: '从外壳服务里取了原始执行缝',
17
+ },
18
+ { pattern: /['"](?:node:)?child_process['"]/, what: '直接引了 child_process' },
19
+ { pattern: /['"]execa['"]/, what: '直接引了 execa(模块不许自己起进程)' },
20
+ { pattern: /kind\s*:\s*['"]host['"]/, what: '请求了宿主写入域(模块只有 unit / workspace 两档)' },
21
+ ];
22
+
23
+ const SEAM_FIX =
24
+ '模块起子进程只走门面:mount(deps) 里的 `deps.seam.run({ scope: "unit" | "workspace", command })`,' +
25
+ '清单里用 `dsh.workbench.host.writes` 声明要哪几档(写法见 `@dsh-workbench/core` 的 ' +
26
+ '`skills/workbench-module-diy/BUILD.md`)';
27
+
28
+ const WRITE_SCOPES = ['unit', 'workspace'];
29
+
30
+ function lineOf(text, index) {
31
+ return text.slice(0, index).split('\n').length;
32
+ }
33
+
34
+ function isRecord(value) {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ export function rawSeamProblemsOf(name, rel, text) {
39
+ const problems = [];
40
+ for (const entry of RAW_SEAM) {
41
+ const match = entry.pattern.exec(text);
42
+ if (match === null) continue;
43
+ problems.push(`${name}: ${rel}:${lineOf(text, match.index)} ${entry.what} —— ${SEAM_FIX}`);
44
+ }
45
+ return problems;
46
+ }
47
+
48
+ export function declaredWritesOf(workbench) {
49
+ const host = isRecord(workbench) ? workbench.host : undefined;
50
+ const writes = isRecord(host) ? host.writes : undefined;
51
+ return new Set(Array.isArray(writes) ? writes.filter((scope) => WRITE_SCOPES.includes(scope)) : []);
52
+ }
53
+
54
+ export function declaredWritesProblemsOf(name, workbench) {
55
+ const host = isRecord(workbench) ? workbench.host : undefined;
56
+ if (!isRecord(host) || host.writes === undefined) return [];
57
+ const writes = host.writes;
58
+ if (!Array.isArray(writes)) {
59
+ return [
60
+ `${name}: dsh.workbench.host.writes 要是一串写入域("unit" / "workspace"),` +
61
+ `现在是 ${JSON.stringify(writes)}`,
62
+ ];
63
+ }
64
+ const outside = writes.filter((scope) => !WRITE_SCOPES.includes(scope));
65
+ if (outside.length === 0) return [];
66
+ return [
67
+ `${name}: dsh.workbench.host.writes 里有不认的写入域 ` +
68
+ `${outside.map((scope) => JSON.stringify(scope)).join(' / ')} —— ` +
69
+ '只认 "unit"(模块自己的数据目录,<运行时根>/units/<unit>)与 "workspace"(当前会话工作区)两档;' +
70
+ '模块拿不到产品域与任意路径,别写别的名字(不写 = 不给执行缝门面)',
71
+ ];
72
+ }
73
+
74
+ export function writesUseProblemsOf(name, rel, text, declared) {
75
+ if (!/\bseam\b/.test(text)) return [];
76
+ const problems = [];
77
+ const seen = new Set();
78
+ for (const match of text.matchAll(SCOPE_USE)) {
79
+ const scope = match[1];
80
+ if (seen.has(scope) || declared.has(scope)) continue;
81
+ seen.add(scope);
82
+ problems.push(
83
+ `${name}: ${rel}:${lineOf(text, match.index)} 请求了 ${scope} 档,但清单 ` +
84
+ `dsh.workbench.host.writes 没声明它 —— 外壳不会把门面交给这个模块(deps.seam 是 undefined):` +
85
+ `清单里补 "writes": ["${scope}"],或这一档别请求`,
86
+ );
87
+ }
88
+ return problems;
89
+ }
90
+
91
+ export async function run() {
92
+ const modulePackages = discoverModulePackages({ root: process.cwd() });
93
+ const problems = [];
94
+ let scanned = 0;
95
+ let scannedUnits = 0;
96
+ let seamUnits = 0;
97
+
98
+ for (const pkg of modulePackages) {
99
+ const name = pkg.manifest.name;
100
+ const workbench = pkg.manifest.dsh?.workbench;
101
+ problems.push(...declaredWritesProblemsOf(name, workbench));
102
+ const declared = declaredWritesOf(workbench);
103
+ if (declared.size > 0) seamUnits += 1;
104
+ const hostFace = pkg.manifest.dsh?.workbenchFaces?.host;
105
+ if (typeof hostFace !== 'string') continue;
106
+ const unit = typeof workbench?.unit === 'string' ? workbench.unit : '';
107
+ scannedUnits += 1;
108
+ for (const file of reachableFrom(pkg.dir, unit, hostFace)) {
109
+ const text = readFileSync(file, 'utf8');
110
+ const rel = relative(pkg.dir, file);
111
+ scanned += 1;
112
+ problems.push(...rawSeamProblemsOf(name, rel, text));
113
+ problems.push(...writesUseProblemsOf(name, rel, text, declared));
114
+ }
115
+ }
116
+
117
+ if (modulePackages.length === 0) {
118
+ return {
119
+ status: 'skip',
120
+ notes: ['没有模块包(没有声明 dsh.workbench 的包)—— 没有执行缝用法要查'],
121
+ problems: [],
122
+ };
123
+ }
124
+ if (problems.length > 0) return { status: 'fail', notes: [], problems };
125
+ return {
126
+ status: 'pass',
127
+ notes: [
128
+ `${modulePackages.length} 个模块包 · ${scannedUnits} 个宿主面 · ${scanned} 个宿主侧源文件 · ` +
129
+ `${seamUnits} 个模块声明了写入域(不取原始 shell / 不裸 child_process;` +
130
+ '请求的写入域都在清单里)',
131
+ ],
132
+ problems: [],
133
+ };
134
+ }
@@ -173,7 +173,7 @@ function resolveLocal(pkgDir, unit, fromFile, specifier) {
173
173
  return undefined;
174
174
  }
175
175
 
176
- function reachableFrom(pkgDir, unit, entry) {
176
+ export function reachableFrom(pkgDir, unit, entry) {
177
177
  const seen = new Set();
178
178
  const start = join(pkgDir, entry);
179
179
  if (!existsSync(start)) return seen;
@@ -81,6 +81,8 @@ export function dshHomeOf(home) {
81
81
  }
82
82
 
83
83
  export function workbenchRootOf(home) {
84
+ const staging = process.env.DSH_WB_RELEASE_HOME?.trim();
85
+ if (staging !== undefined && staging !== '') return staging;
84
86
  const configured = process.env.DSH_WORKBENCH_HOME?.trim();
85
87
  if (configured !== undefined && configured !== '') return configured;
86
88
  return join(dshHomeOf(home), 'workbench');
package/src/release.mjs CHANGED
@@ -151,6 +151,27 @@ function titleIn(segment, key) {
151
151
  return match === null ? '' : match[1].trim();
152
152
  }
153
153
 
154
+ function writeStateKeepingRun(home, unit, value) {
155
+ const existing = readState(home, unit);
156
+ const valueRun = value !== null && typeof value === 'object' ? value.run : undefined;
157
+ const run = valueRun ?? existing?.run;
158
+ writeState(home, unit, run === undefined ? value : { ...value, run });
159
+ }
160
+
161
+ function stopSummaryOf(target, manifest, version, changes, changelog, labels) {
162
+ return {
163
+ ok: true,
164
+ stop: 'workspace-only',
165
+ target,
166
+ name: manifest.name,
167
+ version,
168
+ changes,
169
+ ...(labels.length === 0 ? {} : { labels }),
170
+ ...(changelog === undefined ? {} : { changelog }),
171
+ install: '',
172
+ };
173
+ }
174
+
154
175
  export function cardTitleFromMessages(root, unit) {
155
176
  if (unit === '') return undefined;
156
177
  const key = `${unit}.cardTitle`;
@@ -254,6 +275,9 @@ export async function runRelease({
254
275
  const json = hasFlag(args, '--json');
255
276
  const dryRun = hasFlag(args, '--dry-run');
256
277
  const verify = hasFlag(args, '--verify');
278
+ const workspaceOnly = hasFlag(args, '--workspace-only');
279
+ const indexOnly = hasFlag(args, '--index-only');
280
+ if (workspaceOnly && indexOnly) throw new ReleaseError('--workspace-only 与 --index-only 不能同时用');
257
281
  const askedAuthor = argValue(args, '--author');
258
282
  const explicitAuthor = askedAuthor === undefined ? undefined : askedAuthor.trim();
259
283
  if (askedAuthor !== undefined && (explicitAuthor === '' || /[\r\n]/u.test(explicitAuthor))) {
@@ -277,9 +301,12 @@ export async function runRelease({
277
301
  throw new ReleaseError('package.json 里没有 version');
278
302
  }
279
303
  const earlyState = readState(home, unit);
280
- const resume = hasFlag(args, '--resume');
281
- const journal = resume ? resumableState(home, unit) : undefined;
282
- if (resume && journal === undefined) {
304
+ const resume = hasFlag(args, '--resume') || indexOnly;
305
+ const journal = indexOnly ? readState(home, unit) : resume ? resumableState(home, unit) : undefined;
306
+ if (indexOnly && (journal === undefined || journal.run === undefined)) {
307
+ throw new ReleaseError('没有可续跑的发布记录 —— 先正常发一次');
308
+ }
309
+ if (resume && !indexOnly && journal === undefined) {
283
310
  throw new ReleaseError('没有可续跑的发布记录 —— 去掉 --resume 正常发一次');
284
311
  }
285
312
  const askedTarget = argValue(args, '--target');
@@ -291,7 +318,9 @@ export async function runRelease({
291
318
  const resumeTarget = resume && askedTarget === undefined ? journal?.target : undefined;
292
319
  const needPlan = !verify || stateTarget === 'market' || askedTarget === 'market';
293
320
  let indexPlan;
294
- if (needPlan) {
321
+ if (indexOnly) {
322
+ indexPlan = journal?.run?.indexPlan;
323
+ } else if (needPlan) {
295
324
  try {
296
325
  indexPlan = resolveIndexPlan({ args, home, exec });
297
326
  } catch (error) {
@@ -306,10 +335,17 @@ export async function runRelease({
306
335
  if (resume && indexPlan === undefined && journal?.run?.indexPlan !== undefined) {
307
336
  indexPlan = journal.run.indexPlan;
308
337
  }
309
- const target = resumeTarget ?? stateTarget ?? targetOf(root, manifest, args, indexPlan);
338
+ const target = indexOnly
339
+ ? typeof journal?.target === 'string'
340
+ ? journal.target
341
+ : (() => {
342
+ throw new ReleaseError('发布记录里没有出口 —— 重新发一次');
343
+ })()
344
+ : (resumeTarget ?? stateTarget ?? targetOf(root, manifest, args, indexPlan));
310
345
  if (target === 'local') {
311
346
  const summary = {
312
347
  ok: true,
348
+ ...(workspaceOnly ? { stop: 'workspace-only' } : {}),
313
349
  target: 'local',
314
350
  name: manifest.name,
315
351
  version: manifest.version,
@@ -324,7 +360,7 @@ export async function runRelease({
324
360
  const labelsForIndex = labels.length > 0 ? labels : labelsOfManifest(manifest);
325
361
  const labelsChange = verify || labels.length === 0 ? undefined : `标签 ← ${labels.join(' · ')}`;
326
362
  const explicitApplied = explicitAuthor !== undefined && !verify && !dryRun;
327
- if (explicitApplied) {
363
+ if (explicitApplied && !indexOnly) {
328
364
  manifest.author = explicitAuthor;
329
365
  writeManifest(root, manifest);
330
366
  }
@@ -396,13 +432,13 @@ export async function runRelease({
396
432
  return summary;
397
433
  }
398
434
  const authorChange = explicitAuthor === undefined ? authored.change : 'author ← --author';
399
- if (!explicitApplied && !verify && !dryRun && authored.author !== undefined) {
435
+ if (!explicitApplied && !indexOnly && !verify && !dryRun && authored.author !== undefined) {
400
436
  manifest.author = authored.author;
401
437
  writeManifest(root, manifest);
402
438
  }
403
439
  const publisher = authorTextOf(manifest);
404
440
  const writesIndex = indexPlan !== undefined && target !== 'local' && target !== 'tarball';
405
- if (!verify && !dryRun && writesIndex) {
441
+ if (!workspaceOnly && !verify && !dryRun && writesIndex) {
406
442
  if (publisher === '') throw new ReleaseError(missingPublisherText());
407
443
  let claim;
408
444
  try {
@@ -417,7 +453,7 @@ export async function runRelease({
417
453
  throw new ReleaseError(claimProblemText(manifest.name, claim, unit));
418
454
  }
419
455
  }
420
- const frozenVersion = resume && typeof journal?.version === 'string' ? journal.version : undefined;
456
+ const frozenVersion = (resume || indexOnly) && typeof journal?.version === 'string' ? journal.version : undefined;
421
457
  const info =
422
458
  target === 'market'
423
459
  ? { target: 'market', kind: 'market', url: indexPlan.source.url }
@@ -440,18 +476,20 @@ export async function runRelease({
440
476
  }
441
477
  return frozenVersion ?? nextVersion(manifest.version, askedVersion, () => false);
442
478
  };
443
- const releaseVersion = verify ? undefined : targetVersionOf();
444
- const changelogProblem = verify || dryRun ? undefined : changelogProblemOf(root, releaseVersion);
445
- if (changelogProblem !== undefined && changelogNote === '') {
479
+ const releaseVersion = indexOnly ? frozenVersion : verify ? undefined : targetVersionOf();
480
+ if (indexOnly && releaseVersion === undefined) throw new ReleaseError('发布记录里没有版本号 —— 重新发一次');
481
+ const changelogProblem = verify || dryRun || indexOnly ? undefined : changelogProblemOf(root, releaseVersion);
482
+ if (!indexOnly && changelogProblem !== undefined && changelogNote === '') {
446
483
  throw new NeedsUser('changelog', changelogProblem.reason, changelogProblem.command, {
447
484
  target,
448
485
  version: releaseVersion,
449
486
  });
450
487
  }
451
488
  const fingerprint = artifactFingerprintOf(root);
452
- const resumeMatchesTree = resume && frozenVersion !== undefined && journal?.run?.fingerprint === fingerprint;
489
+ const resumeMatchesTree =
490
+ resume && frozenVersion !== undefined && (indexOnly || journal?.run?.fingerprint === fingerprint);
453
491
  const tracking = !verify && !dryRun;
454
- if (tracking) {
492
+ if (tracking && !indexOnly) {
455
493
  startRun(home, unit, {
456
494
  target,
457
495
  name: manifest.name,
@@ -686,7 +724,7 @@ export async function runRelease({
686
724
  }
687
725
  const earlyLicense = info.kind === 'registry' ? licenseOf(root, manifest, target, args) : undefined;
688
726
  if (info.kind === 'registry') checkLogin(exec, info);
689
- const changelog = dryRun
727
+ const changelog = dryRun || indexOnly
690
728
  ? undefined
691
729
  : applyChangelog(root, {
692
730
  version: releaseVersion,
@@ -694,7 +732,7 @@ export async function runRelease({
694
732
  note: changelogNote,
695
733
  date: changelogDate(),
696
734
  });
697
- if (!dryRun && changelog === undefined) {
735
+ if (!indexOnly && !dryRun && changelog === undefined) {
698
736
  const problem = changelogProblemOf(root, releaseVersion);
699
737
  throw new NeedsUser(
700
738
  'changelog',
@@ -739,6 +777,13 @@ export async function runRelease({
739
777
  const next = { ...manifest, version };
740
778
  const cardTitle = cardTitleFromMessages(root, unit);
741
779
  const titled = cardTitle === undefined ? next : { ...next, title: cardTitle };
780
+ if (workspaceOnly) {
781
+ writeManifest(root, next);
782
+ if (tracking) advanceRun(home, unit, { phase: 'workspace', fingerprint: artifactFingerprintOf(root) });
783
+ const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels);
784
+ emit(out, json, stopSummary);
785
+ return stopSummary;
786
+ }
742
787
  const index = updateMarketIndex({
743
788
  plan: indexPlan,
744
789
  root,
@@ -782,11 +827,11 @@ export async function runRelease({
782
827
  ...(index.branch === undefined ? {} : { branch: index.branch }),
783
828
  });
784
829
  }
785
- writeManifest(root, next);
830
+ if (!indexOnly) writeManifest(root, next);
786
831
  const path = index.path ?? marketArtifactPath(manifest.name);
787
832
  const install = marketInstallOf(index.url, index.commit, path);
788
833
  const journalNow = tracking ? readState(home, unit)?.run : undefined;
789
- writeState(home, unit, {
834
+ writeStateKeepingRun(home, unit, {
790
835
  target: 'market',
791
836
  name: next.name,
792
837
  version,
@@ -828,17 +873,22 @@ export async function runRelease({
828
873
  }
829
874
  if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
830
875
  const next = { ...manifest, version };
831
- writeManifest(root, next);
876
+ if (!indexOnly) writeManifest(root, next);
832
877
  if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
833
- writeState(home, unit, stateOf(info, next, version, 'intent'));
878
+ writeStateKeepingRun(home, unit, stateOf(info, next, version, 'intent'));
879
+ if (workspaceOnly) {
880
+ const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels);
881
+ emit(out, json, stopSummary);
882
+ return stopSummary;
883
+ }
834
884
  let packed = false;
835
885
  try {
836
886
  const file = packTarball(exec, info, version);
837
887
  packed = true;
838
- writeState(home, unit, { ...stateOf(info, next, version, 'published'), file });
888
+ writeStateKeepingRun(home, unit, { ...stateOf(info, next, version, 'published'), file });
839
889
  const packedFiles = localFiles(exec, root);
840
890
  checkPublished(packedFiles);
841
- writeState(home, unit, { ...stateOf(info, next, version, 'verified'), file });
891
+ writeStateKeepingRun(home, unit, { ...stateOf(info, next, version, 'verified'), file });
842
892
  if (tracking) finishRun(home, unit, { version });
843
893
  summary.file = file;
844
894
  summary.files = packedFiles;
@@ -847,7 +897,7 @@ export async function runRelease({
847
897
  return summary;
848
898
  } catch (error) {
849
899
  if (!packed) {
850
- writeManifest(root, manifest);
900
+ if (!indexOnly) writeManifest(root, manifest);
851
901
  if (tracking) {
852
902
  interruptRun(home, unit, { code: 'tarball', text: String(error?.message ?? error) });
853
903
  } else {
@@ -881,19 +931,46 @@ export async function runRelease({
881
931
  }
882
932
  if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
883
933
  const next = { ...manifest, version };
884
- writeManifest(root, next);
934
+ if (!indexOnly) writeManifest(root, next);
885
935
  if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
886
- writeState(home, unit, stateOf(info, next, version, 'intent'));
936
+ writeStateKeepingRun(home, unit, stateOf(info, next, version, 'intent'));
937
+ if (indexOnly) {
938
+ const state = readState(home, unit);
939
+ const checked = verifyGit(exec, info, version);
940
+ summary.files = checked.files;
941
+ summary.git.commit = checked.commit;
942
+ summary.market = indexOf(next, version, {
943
+ git: { url: info.url, commit: checked.commit, path: info.packagePath },
944
+ });
945
+ writeStateKeepingRun(home, unit, {
946
+ ...stateOf(info, next, version, 'verified'),
947
+ commit: checked.commit,
948
+ ...(tracking
949
+ ? { run: { ...(state?.run ?? {}), state: 'done', phase: 'done', updatedAt: new Date().toISOString() } }
950
+ : {}),
951
+ });
952
+ emit(out, json, summary);
953
+ return summary;
954
+ }
887
955
  const progress = { committed: false };
888
956
  try {
889
957
  const commit = gitCommitOf(exec, info, version, capture, progress, identityOf(next));
890
- writeState(home, unit, { ...stateOf(info, next, version, 'committed'), commit });
958
+ writeStateKeepingRun(home, unit, { ...stateOf(info, next, version, 'committed'), commit });
891
959
  gitPushOf(exec, info, version, capture);
892
960
  const checked = verifyGit(exec, info, version);
893
- writeState(home, unit, {
961
+ writeStateKeepingRun(home, unit, {
894
962
  ...stateOf(info, next, version, 'verified'),
895
963
  commit: checked.commit,
896
964
  });
965
+ if (workspaceOnly) {
966
+ const stopSummary = {
967
+ ...stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels),
968
+ files: checked.files,
969
+ git: { url: info.url, tag: info.tagOf(version), commit: checked.commit },
970
+ };
971
+ emit(out, json, stopSummary);
972
+ return stopSummary;
973
+ }
897
974
  if (tracking) finishRun(home, unit, { version });
898
975
  summary.files = checked.files;
899
976
  summary.git.commit = checked.commit;
@@ -965,12 +1042,17 @@ export async function runRelease({
965
1042
  emit(out, json, summary);
966
1043
  return summary;
967
1044
  }
968
- writeManifest(root, next);
1045
+ if (!indexOnly) writeManifest(root, next);
969
1046
  if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
970
1047
  let published = false;
971
1048
  try {
972
1049
  if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
973
- writeState(home, unit, stateOf(info, next, version, 'intent'));
1050
+ writeStateKeepingRun(home, unit, stateOf(info, next, version, 'intent'));
1051
+ if (workspaceOnly) {
1052
+ const stopSummary = stopSummaryOf(target, next, version, changes, changelogReport, labels);
1053
+ emit(out, json, stopSummary);
1054
+ return stopSummary;
1055
+ }
974
1056
  publishRegistry(exec, root, info, next, version);
975
1057
  published = true;
976
1058
  const files = verifyRegistry(exec, info, next.name, version);
@@ -993,14 +1075,14 @@ export async function runRelease({
993
1075
  return summary;
994
1076
  } catch (error) {
995
1077
  if (!published && !(error instanceof NeedsUser && error.step === 'otp')) {
996
- writeManifest(root, original);
1078
+ if (!indexOnly) writeManifest(root, original);
997
1079
  if (tracking) {
998
1080
  interruptRun(home, unit, { code: 'registry', text: String(error?.message ?? error) });
999
1081
  } else {
1000
1082
  removeState(home, unit);
1001
1083
  }
1002
1084
  }
1003
- if (published) writeState(home, unit, stateOf(info, next, version, 'published'));
1085
+ if (published) writeStateKeepingRun(home, unit, stateOf(info, next, version, 'published'));
1004
1086
  throw error;
1005
1087
  }
1006
1088
  }