@dsh-workbench/module-tools 1.1.18 → 1.1.19-beta.0

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
@@ -31,8 +31,10 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
31
31
  `package.json` 缺 `files` 里的 `CHANGELOG.md` 一样拦下;`author` 没写时从本机 `git config` 补进 manifest,索引与产物都带上。
32
32
  标签只住在市集条目上(`metadata.labels`),**不写进 `package.json`**:清单的键是协议定死的
33
33
  (`apiVersion` / `unit` / `title` / `card` / `tools` / `host` / `needs`),往里塞私货 check 会判红。
34
- 发布时带 `--label <短词>`(可重复,最多两个):它并进索引条目的 `metadata.labels`;
34
+ 标签可选:`--label <短词>`(可重复,最多两个)并进索引条目的 `metadata.labels`;
35
35
  不传就沿用老清单里已经写过的(`dsh.workbench.market.labels`,只在读那一侧认)。
36
+ 分类 `--category <短词>` 是市场发布的要求:条目还没有分类时必须给一个(工作台的发布审批会拦),
37
+ 已有分类可以不带;它写进 `metadata.category`,发现页的分类 chip 就是按它汇总的,新模块带一个新分类,页面自动多一个。
36
38
  标题 / 署名 / 说明仍保持索引里现有的那份不动。
37
39
  要换署名时带 `--author "姓名 <邮箱>"`(或只给姓名):它写回 `package.json`,索引、产物与提交都用它,
38
40
  `--refresh` 时也一样。**发布本身不改市集里的标题 / 署名 / 说明**:这些元数据只由 `--refresh`(或显式 `--author`)写,
@@ -57,7 +59,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
57
59
  | `dsh-wb-module migrate [--dry-run]` | 把模块清单迁到当前代:`apiVersion` → 3、缺/空的 `workbenchFaces` 补默认;源码类问题只给提示(check 会点名) |
58
60
  | `dsh-wb-module changelog check [版本]` | 查这一版有没有记录;发布时用 `release --changelog-note` 写进去 |
59
61
  | `dsh-wb-module publish` | 发货前清单:门禁 + `files` + `npm pack`(只 dry run) |
60
- | `dsh-wb-module release` | 真发布:team / public / tarball / git;全门禁 + 验收闸 + 预检接管 + 发后核包 + 更新市场索引(`--dry-run` / `--verify`) |
62
+ | `dsh-wb-module release` | 真发布:team / public / tarball / git / market / local;全门禁 + 验收闸 + 预检接管 + 发后核包 + 更新市场索引(`--dry-run` / `--verify`) |
61
63
 
62
64
  `--cwd <目录>` 指定**工作目录**,默认当前目录。注意判据仍按**工作区**发现模块:往上找
63
65
  `pnpm-workspace.yaml`(或 `package.json` 的 `workspaces`),按它声明的 glob 展开包目录
@@ -2,6 +2,8 @@
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { Command } from 'commander';
5
+ import { WORKBENCH_API_VERSION } from '@dsh-workbench/shared/host';
6
+ import { RELEASE_TARGETS } from '@dsh-workbench/shared/contract/protocol/release';
5
7
  import { build } from '../src/build.mjs';
6
8
  import { CHECKS, check } from '../src/check.mjs';
7
9
  import { changelog } from '../src/changelog.mjs';
@@ -63,17 +65,18 @@ const MIGRATE_HELP = `
63
65
  用法:
64
66
  migrate [--cwd <模块目录>] [--dry-run]
65
67
 
66
- 把模块清单迁到当前代:apiVersion → 3、缺/空的 workbenchFaces 补默认入口。
68
+ 把模块清单迁到当前代:apiVersion → ${WORKBENCH_API_VERSION}、缺/空的 workbenchFaces 补默认入口。
67
69
  源码类问题(kit 成员移除、裸控件、card: null 对账)不自动改 —— check 会逐条点名。`;
68
70
 
69
71
  const RELEASE_HELP = `
70
72
  选项:
71
- --target team|public|tarball|git|market|local 出口;省略按配置默认(可写索引优先 market)
73
+ --target ${RELEASE_TARGETS.join('|')} 出口;省略按配置默认(可写索引优先 market)
72
74
  --version patch|minor|major|x.y.z 版本;省略时已存在就自动 patch
73
75
  --registry <url> --access restricted|public --npmrc <文件>
74
76
  --license <id> --out <目录> --git <remote>
75
77
  --author <署名> 换署名(姓名,或 姓名 <邮箱>);写回 package.json
76
- --label <短词> 给市集条目打标签(1–2 个,可重复);写回 package.json
78
+ --label <短词> 给市集条目打标签(1–2 个,可重复);只进索引条目
79
+ --category <短词> 给市集条目定分类(一个短词);只进索引条目,不传沿用已有的
77
80
  --changelog-note <文字> --changelog-type added|changed|fixed|removed|security|deprecated
78
81
  这一版还没有更新日志时写进 CHANGELOG.md(文件不在先建)
79
82
  --index auto|none|<目录|git 地址> 发布后要更新的索引源
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.1.18",
3
+ "version": "1.1.19-beta.0",
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.18"
36
+ "@dsh-workbench/shared": "1.1.19-beta.0"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
package/src/arch.mjs CHANGED
@@ -13,9 +13,7 @@ import {
13
13
  import { createRule } from '@arch-guard/core';
14
14
  import { DECLARED_TOOLS_FILE } from './declared-tools.mjs';
15
15
  import { SEED_SPECIFIERS } from './seed.mjs';
16
-
17
- const HOST_FACE = 'src/host.ts';
18
- const CLIENT_FACE = 'src/client.tsx';
16
+ import { CLIENT_FACE, HOST_FACE } from './faces.mjs';
19
17
 
20
18
  const LAYERS = { model: 1, components: 2, host: 10 };
21
19
 
package/src/build.mjs CHANGED
@@ -5,6 +5,7 @@ import { isAbsolute, join, relative } from 'node:path';
5
5
  import { build as esbuild } from 'esbuild';
6
6
  import { SEED_SPECIFIERS } from './seed.mjs';
7
7
  import { cssInjectionCode } from './css-injection.mjs';
8
+ import { CLIENT_ENTRY_CANDIDATES, HOST_ENTRY_CANDIDATES } from './faces.mjs';
8
9
 
9
10
  const SHELL_CLIENT = '@dsh-workbench/core/client';
10
11
  const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.json'];
@@ -113,7 +114,7 @@ export async function build({ root }) {
113
114
  const plugins = [aliasResolver({ root, unit })];
114
115
  const nodeBuiltins = [...builtinModules, ...builtinModules.map((name) => `node:${name}`)];
115
116
 
116
- const hostEntry = pickEntry(root, ['src/plugin.host.ts', 'src/host.ts']);
117
+ const hostEntry = pickEntry(root, HOST_ENTRY_CANDIDATES);
117
118
  if (hostEntry !== undefined) {
118
119
  await esbuild({
119
120
  absWorkingDir: root,
@@ -130,8 +131,8 @@ export async function build({ root }) {
130
131
  });
131
132
  }
132
133
 
133
- const clientEntry = pickEntry(root, ['src/plugin.client.ts', 'src/client.tsx']);
134
- if (clientEntry === undefined) throw new Error('找不到浏览器侧入口(src/plugin.client.ts 或 src/client.tsx)');
134
+ const clientEntry = pickEntry(root, CLIENT_ENTRY_CANDIDATES);
135
+ if (clientEntry === undefined) throw new Error(`找不到浏览器侧入口(${CLIENT_ENTRY_CANDIDATES.join(' 或 ')})`);
135
136
  const client = await esbuild({
136
137
  absWorkingDir: root,
137
138
  entryPoints: [clientEntry],
@@ -4,6 +4,8 @@ import { homedir } from 'node:os';
4
4
  import { join, relative } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import {
7
+ OPENCLI_CATALOG_ENV,
8
+ OPENCLI_NPM_PACKAGE,
7
9
  buildSupplyIndex,
8
10
  createHostKit,
9
11
  hostSurface,
@@ -18,6 +20,7 @@ import {
18
20
  } from '../declared-tools.mjs';
19
21
  import { discoverPackages, workspaceRootOf } from '../discover.mjs';
20
22
  import { readDesign } from '../design.mjs';
23
+ import { DEFAULT_FACES } from '../faces.mjs';
21
24
  import { datasourceProblemOf } from '../datasource-file.mjs';
22
25
  import { SEED_SHORT } from '../seed.mjs';
23
26
 
@@ -65,9 +68,6 @@ async function unitFaceOf(dir) {
65
68
  }
66
69
  }
67
70
 
68
- const OPENCLI_CATALOG_ENV = 'DSH_WB_OPENCLI_CATALOG';
69
- const OPENCLI_NPM_PACKAGE = '@jackwener/opencli';
70
-
71
71
  function workbenchRootOf() {
72
72
  const configured = process.env.DSH_WORKBENCH_HOME?.trim();
73
73
  if (configured !== undefined && configured !== '') return configured;
@@ -540,7 +540,7 @@ export async function run() {
540
540
  problems.push(
541
541
  `${name}: dsh.workbenchFaces.${face} 没声明 —— 这两个源码入口是工具链字段` +
542
542
  `(不在 dsh.workbench 协议里):source 判据靠它们判"哪一半",缺了它判不了` +
543
- `(\`dsh-wb-module migrate\` 会补默认的 ${face === 'host' ? 'src/host.ts' : 'src/client.tsx'})`,
543
+ `(\`dsh-wb-module migrate\` 会补默认的 ${DEFAULT_FACES[face]})`,
544
544
  );
545
545
  } else if (!existsSync(join(pkg.dir, rel))) {
546
546
  problems.push(`${name}: dsh.workbenchFaces.${face} 指的文件不存在 —— ${rel}`);
@@ -289,6 +289,7 @@ export async function run() {
289
289
  const anchorHits = new Set();
290
290
  const modules = discoverModulePackages({ root: process.cwd() });
291
291
  const contracts = new Map(modules.map((pkg) => [pkg.dir, contractOf(pkg.dir)]));
292
+ let dshTokens = null;
292
293
  if (modules.length > 0) {
293
294
  const searched = [
294
295
  process.cwd(),
@@ -297,6 +298,7 @@ export async function run() {
297
298
  join(dshHome(), 'workbench', 'diy'),
298
299
  ];
299
300
  const tokens = readTokens(searched);
301
+ dshTokens = tokens.dsw;
300
302
  const dswNames = (tokens.dsw?.entries ?? []).map((one) => one.name);
301
303
  const wbNames = (tokens.wb?.entries ?? []).map((one) => one.name);
302
304
  const known = new Set([...dswNames, ...wbNames]);
@@ -675,11 +677,11 @@ export async function run() {
675
677
  ? '没有手写的整张表单(没有裸 <form>,也没有借表单槽类手摆行)'
676
678
  : `表单这一层还有 ${formHits.size} 个文件在 FORM_EXEMPT 里等外壳发版后迁(棘轮:迁一处删一行)`;
677
679
 
678
- const fontScale = fontScaleOf(SHELL_FONT_SCALE_FILES);
680
+ const fontScale = fontScaleOf(SHELL_FONT_SCALE_FILES, dshTokens);
679
681
  const fontNotes = fontScaleNotes(
680
682
  auditFontSizes({ roots: fontRootsIn(modules), scale: fontScale }),
681
683
  fontScale,
682
- { doc: FONT_DOC },
684
+ { doc: FONT_DOC, dshMissing: dshTokens === null },
683
685
  );
684
686
  return {
685
687
  status: 'pass',
package/src/faces.mjs ADDED
@@ -0,0 +1,7 @@
1
+ export const HOST_FACE = 'src/host.ts';
2
+ export const CLIENT_FACE = 'src/client.tsx';
3
+
4
+ export const DEFAULT_FACES = { host: HOST_FACE, client: CLIENT_FACE };
5
+
6
+ export const HOST_ENTRY_CANDIDATES = ['src/plugin.host.ts', HOST_FACE];
7
+ export const CLIENT_ENTRY_CANDIDATES = ['src/plugin.client.ts', CLIENT_FACE];
@@ -1,8 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join, relative } from 'node:path';
3
3
 
4
- export const DSH_ALIAS_FONT_SIZES = [11, 12, 13, 14, 16, 20, 24];
5
-
6
4
  const SHELL_FONT_TOKEN = /--wb-font-[a-z0-9-]+\s*:\s*([0-9]+(?:\.[0-9]+)?)px/g;
7
5
 
8
6
  export function shellFontScaleIn(files) {
@@ -16,8 +14,26 @@ export function shellFontScaleIn(files) {
16
14
  return sizes;
17
15
  }
18
16
 
19
- export function fontScaleOf(shellTokenFiles = []) {
20
- return new Set([...DSH_ALIAS_FONT_SIZES, ...shellFontScaleIn(shellTokenFiles)]);
17
+ const TOKEN_FONT_SIZE = /([0-9]*\.?[0-9]+)(px|rem)/g;
18
+
19
+ export function tokenFontSizesIn(source) {
20
+ const sizes = new Set();
21
+ if (source === null || source === undefined) return sizes;
22
+ for (const entry of source.entries ?? []) {
23
+ if (!entry.name.includes('font-size')) continue;
24
+ for (const value of [entry.values?.light, entry.values?.dark]) {
25
+ if (typeof value !== 'string') continue;
26
+ for (const hit of value.matchAll(TOKEN_FONT_SIZE)) {
27
+ const number = Number(hit[1]);
28
+ sizes.add(hit[2] === 'rem' ? number * 16 : number);
29
+ }
30
+ }
31
+ }
32
+ return sizes;
33
+ }
34
+
35
+ export function fontScaleOf(shellTokenFiles = [], dshTokens = null) {
36
+ return new Set([...shellFontScaleIn(shellTokenFiles), ...tokenFontSizesIn(dshTokens)]);
21
37
  }
22
38
 
23
39
  const CSS_FONT_SIZE = /(?:^|[;{\s])font-size\s*:\s*([^;{}]+)/g;
@@ -131,10 +147,13 @@ function detailOf(hit, verdict) {
131
147
  : `· ${head} —— ${formatSize(hit.px)} 在刻度上,可收进令牌(但不是错)`;
132
148
  }
133
149
 
134
- export function fontScaleNotes(report, scale, { doc }) {
150
+ export function fontScaleNotes(report, scale, { doc, dshMissing = false }) {
135
151
  const sizes = [...scale].sort((a, b) => a - b).map(formatSize).join(' / ');
152
+ const source = dshMissing
153
+ ? '外壳 --wb-font-* 的定义(dsh 主题没读到,--dsw-font-* 未纳入)'
154
+ : '外壳 --wb-font-* 与 dsh --dsw-font-* 的定义';
136
155
  const head =
137
- `字号口径(黄,不判红):刻度 ${sizes}(从 --dsw-font-* 与 --wb-font-* 的**定义**读出来,` +
156
+ `字号口径(黄,不判红):刻度 ${sizes}(从 ${source}读出来,` +
138
157
  `出处见 ${doc});引用令牌 ${report.refs.length} 处 / ${report.refFiles.size} 文件(合规,不列);` +
139
158
  `刻度外字面值 ${report.offScale.length} 处 / ${report.litFiles.size} 文件;` +
140
159
  `刻度上字面值 ${report.onScale.length} 处;相对单位(em/%)判不了 ${report.relative.length} 处`;
@@ -1,6 +1,7 @@
1
1
  import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
+ import { MARKET_INDEX_DIR, MARKET_INDEX_PATH } from '@dsh-workbench/shared/contract/protocol/market-index';
4
5
 
5
6
  import {
6
7
  MARKET_ENTRIES_README_LINES,
@@ -18,8 +19,8 @@ import {
18
19
  sortMarketVersions,
19
20
  } from '@dsh-workbench/shared/host';
20
21
 
21
- const MARKET_DIR = 'market';
22
- const LEGACY_FILE = 'market.json';
22
+ const MARKET_DIR = MARKET_INDEX_DIR;
23
+ const LEGACY_FILE = MARKET_INDEX_PATH;
23
24
  const SAFE_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
24
25
  const WORKSPACE_PREFIX = 'workspace:';
25
26
  const ARTIFACT_DIRS = ['dist'];
@@ -321,6 +322,19 @@ export function mergeEntryLabels(entry, labels) {
321
322
  return { ...entry, metadata: { ...(entry.metadata ?? {}), labels: wanted } };
322
323
  }
323
324
 
325
+ export function categoryOfManifest(manifest) {
326
+ const dsh = recordOf(recordOf(manifest).dsh);
327
+ const workbench = recordOf(dsh.workbench);
328
+ const category = recordOf(workbench.market).category;
329
+ return typeof category === 'string' ? category.trim() : '';
330
+ }
331
+
332
+ export function mergeEntryCategory(entry, category) {
333
+ if (typeof category !== 'string' || category === '') return entry;
334
+ if (entry.metadata?.category === category) return entry;
335
+ return { ...entry, metadata: { ...(entry.metadata ?? {}), category } };
336
+ }
337
+
324
338
  export function entryMetaOf(manifest) {
325
339
  const author = authorOf(manifest);
326
340
  const homepage = typeof manifest.homepage === 'string' ? manifest.homepage.trim() : '';
@@ -741,6 +755,7 @@ export function updateMarketIndex(options) {
741
755
  }
742
756
  }
743
757
  next = mergeEntryLabels(next, options.labels);
758
+ next = mergeEntryCategory(next, options.category);
744
759
  writeEntryFile(workdir, dir, next);
745
760
  indexSha = commitOf(exec, workdir, `chore(market): ${entryName}`, identityOf(manifest));
746
761
  report({ phase: 'index-committed', indexCommit: indexSha });
@@ -815,6 +830,18 @@ export function refreshMarketEntry(options) {
815
830
  }
816
831
  const latest = existing.versions[0]?.version ?? '';
817
832
  const meta = entryMetaOf(manifest);
833
+ const keptLabels = Array.isArray(options.labels) && options.labels.length > 0
834
+ ? [...options.labels]
835
+ : (existing.metadata?.labels ?? []);
836
+ const keptCategory = typeof options.category === 'string' && options.category !== ''
837
+ ? options.category
838
+ : (existing.metadata?.category ?? '');
839
+ const metadata = { ...(meta.metadata ?? {}) };
840
+ if (keptLabels.length === 0) delete metadata.labels;
841
+ else metadata.labels = keptLabels;
842
+ if (keptCategory === '') delete metadata.category;
843
+ else metadata.category = keptCategory;
844
+ meta.metadata = metadata;
818
845
  const next = { ...existing };
819
846
  for (const key of ['title', 'description', 'author', 'homepage', 'metadata']) {
820
847
  if (Object.hasOwn(meta, key)) next[key] = meta[key];
package/src/migrate.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { WORKBENCH_API_VERSION } from '@dsh-workbench/shared/host';
4
+ import { DEFAULT_FACES } from './faces.mjs';
3
5
 
4
- const API_VERSION = 3;
5
-
6
- const DEFAULT_FACES = { host: 'src/host.ts', client: 'src/client.tsx' };
6
+ const API_VERSION = WORKBENCH_API_VERSION;
7
7
 
8
8
  export async function migrate({ root, args = [] }) {
9
9
  const dryRun = args.includes('--dry-run');
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { RELEASE_TARGETS } from '@dsh-workbench/shared/contract/protocol/release';
2
3
  import { dirname, join, relative, resolve, sep } from 'node:path';
3
4
 
4
5
  import {
@@ -11,7 +12,7 @@ import {
11
12
  tailOf,
12
13
  } from './release-util.mjs';
13
14
 
14
- const TARGETS = ['team', 'public', 'tarball', 'git', 'market', 'local'];
15
+ const TARGETS = RELEASE_TARGETS;
15
16
  const PUBLIC_REGISTRIES = ['https://registry.npmjs.org/', 'https://registry.npmmirror.com/'];
16
17
 
17
18
  function isPublicRegistry(registry) {
package/src/release.mjs CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  import {
52
52
  MarketIndexError,
53
53
  authorTextOf,
54
+ categoryOfManifest,
54
55
  claimProblemText,
55
56
  identityOf,
56
57
  labelsOfManifest,
@@ -158,7 +159,7 @@ function writeStateKeepingRun(home, unit, value) {
158
159
  writeState(home, unit, run === undefined ? value : { ...value, run });
159
160
  }
160
161
 
161
- function stopSummaryOf(target, manifest, version, changes, changelog, labels) {
162
+ function stopSummaryOf(target, manifest, version, changes, changelog, labels, category = '') {
162
163
  return {
163
164
  ok: true,
164
165
  stop: 'workspace-only',
@@ -167,6 +168,7 @@ function stopSummaryOf(target, manifest, version, changes, changelog, labels) {
167
168
  version,
168
169
  changes,
169
170
  ...(labels.length === 0 ? {} : { labels }),
171
+ ...(category === '' ? {} : { category }),
170
172
  ...(changelog === undefined ? {} : { changelog }),
171
173
  install: '',
172
174
  };
@@ -293,6 +295,10 @@ export async function runRelease({
293
295
  throw new ReleaseError('--label 只认一行短词:不能是空的');
294
296
  }
295
297
  if (askedLabels.length > 2) throw new ReleaseError('--label 最多两个 —— 一个模块的标签是 1–2 个短词');
298
+ const askedCategory = (argValue(args, '--category') ?? '').trim();
299
+ if (askedCategory !== '' && /[\r\n]/u.test(askedCategory)) {
300
+ throw new ReleaseError('--category 只认一个短词:不能换行');
301
+ }
296
302
  const manifest = readManifest(root);
297
303
  const unit = manifest?.dsh?.workbench?.unit;
298
304
  if (typeof unit !== 'string' || unit === '') throw new ReleaseError('这不是一个 wb 模块包(没有 dsh.workbench.unit)');
@@ -359,6 +365,8 @@ export async function runRelease({
359
365
  const labels = askedLabels;
360
366
  const labelsForIndex = labels.length > 0 ? labels : labelsOfManifest(manifest);
361
367
  const labelsChange = verify || labels.length === 0 ? undefined : `标签 ← ${labels.join(' · ')}`;
368
+ const categoryForIndex = askedCategory !== '' ? askedCategory : categoryOfManifest(manifest);
369
+ const categoryChange = verify || askedCategory === '' ? undefined : `分类 ← ${askedCategory}`;
362
370
  const explicitApplied = explicitAuthor !== undefined && !verify && !dryRun;
363
371
  if (explicitApplied && !indexOnly) {
364
372
  manifest.author = explicitAuthor;
@@ -397,7 +405,15 @@ export async function runRelease({
397
405
  const cardTitle = cardTitleFromMessages(root, unit);
398
406
  const titled = cardTitle === undefined ? manifest : { ...manifest, title: cardTitle };
399
407
  const publisher = authorTextOf(manifest) || authored.author || '';
400
- const result = refreshMarketEntry({ plan: indexPlan, exec, home, manifest: titled, publisher });
408
+ const result = refreshMarketEntry({
409
+ plan: indexPlan,
410
+ exec,
411
+ home,
412
+ manifest: titled,
413
+ publisher,
414
+ ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
415
+ ...(categoryForIndex === '' ? {} : { category: categoryForIndex }),
416
+ });
401
417
  if (result.updated !== true && result.reason !== undefined) {
402
418
  throw new ReleaseError(`刷新市集元数据没走通:${result.reason}`);
403
419
  }
@@ -419,9 +435,11 @@ export async function runRelease({
419
435
  '市集元数据已刷新',
420
436
  ...(explicitAuthor === undefined ? [] : ['author ← --author']),
421
437
  ...(labelsChange === undefined ? [] : [labelsChange]),
438
+ ...(categoryChange === undefined ? [] : [categoryChange]),
422
439
  ]
423
440
  : [],
424
441
  ...(labels.length === 0 ? {} : { labels }),
442
+ ...(categoryForIndex === '' ? {} : { category: categoryForIndex }),
425
443
  install: '',
426
444
  text:
427
445
  result.updated === true
@@ -535,6 +553,7 @@ export async function runRelease({
535
553
  publisher: authorTextOf(manifestNow),
536
554
  metadata: authorChange !== undefined,
537
555
  ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
556
+ ...(categoryForIndex === '' ? {} : { category: categoryForIndex }),
538
557
  exec,
539
558
  home,
540
559
  now: () => Date.now(),
@@ -544,6 +563,7 @@ export async function runRelease({
544
563
  const changesOf = (version) => [
545
564
  ...(authorChange === undefined ? [] : [authorChange]),
546
565
  ...(labelsChange === undefined ? [] : [labelsChange]),
566
+ ...(categoryChange === undefined ? [] : [categoryChange]),
547
567
  ...(version === manifest.version ? [] : [`version → ${version}`]),
548
568
  ...(changelogChange === undefined ? [] : [changelogChange]),
549
569
  ];
@@ -764,6 +784,7 @@ export async function runRelease({
764
784
  files,
765
785
  changes: changesOf(version),
766
786
  ...(labels.length === 0 ? {} : { labels }),
787
+ ...(categoryForIndex === '' ? {} : { category: categoryForIndex }),
767
788
  ...(changelogReport === undefined ? {} : { changelog: changelogReport }),
768
789
  install: '',
769
790
  market: indexPreviewOf(manifest.name, version),
@@ -780,7 +801,7 @@ export async function runRelease({
780
801
  if (workspaceOnly) {
781
802
  writeManifest(root, next);
782
803
  if (tracking) advanceRun(home, unit, { phase: 'workspace', fingerprint: artifactFingerprintOf(root) });
783
- const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels);
804
+ const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels, categoryForIndex);
784
805
  emit(out, json, stopSummary);
785
806
  return stopSummary;
786
807
  }
@@ -794,6 +815,7 @@ export async function runRelease({
794
815
  publisher,
795
816
  metadata: authorChange !== undefined,
796
817
  ...(labelsForIndex.length === 0 ? {} : { labels: labelsForIndex }),
818
+ ...(categoryForIndex === '' ? {} : { category: categoryForIndex }),
797
819
  exec,
798
820
  home,
799
821
  now: () => Date.now(),
@@ -877,7 +899,7 @@ export async function runRelease({
877
899
  if (tracking) advanceRun(home, unit, { phase: 'version', version, fingerprint: artifactFingerprintOf(root) });
878
900
  writeStateKeepingRun(home, unit, stateOf(info, next, version, 'intent'));
879
901
  if (workspaceOnly) {
880
- const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels);
902
+ const stopSummary = stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels, categoryForIndex);
881
903
  emit(out, json, stopSummary);
882
904
  return stopSummary;
883
905
  }
@@ -964,7 +986,7 @@ export async function runRelease({
964
986
  });
965
987
  if (workspaceOnly) {
966
988
  const stopSummary = {
967
- ...stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels),
989
+ ...stopSummaryOf(target, next, version, changesOf(version), changelogReport, labels, categoryForIndex),
968
990
  files: checked.files,
969
991
  git: { url: info.url, tag: info.tagOf(version), commit: checked.commit },
970
992
  };
@@ -1049,7 +1071,7 @@ export async function runRelease({
1049
1071
  if (!resumeMatchesTree) runCliStage(exec, root, ['check'], stageOptions);
1050
1072
  writeStateKeepingRun(home, unit, stateOf(info, next, version, 'intent'));
1051
1073
  if (workspaceOnly) {
1052
- const stopSummary = stopSummaryOf(target, next, version, changes, changelogReport, labels);
1074
+ const stopSummary = stopSummaryOf(target, next, version, changes, changelogReport, labels, categoryForIndex);
1053
1075
  emit(out, json, stopSummary);
1054
1076
  return stopSummary;
1055
1077
  }
@@ -7,7 +7,7 @@
7
7
  改 `src/**` 后:
8
8
 
9
9
  1. `pnpm build` —— 产出 `dist/host.js` + 套了 loader 壳的 `client.js`;产物由它生成。
10
- 2. `pnpm check` —— 十三条判据,读的就是上一步的产物。单跑一条用 `pnpm check --only <判据名>`;
10
+ 2. `pnpm check` —— 十四条判据,读的就是上一步的产物。单跑一条用 `pnpm check --only <判据名>`;
11
11
  **全绿**才算这件活做完。怎么读红、常见红:`@dsh-workbench/core/skills/workbench-module-diy/REVIEW.md`。
12
12
  3. `pnpm test` —— `src/**/__tests__/` 下、与改动同目录的 `*.test.ts`;新逻辑配对应用例,**全绿**才算完。
13
13
 
@@ -9,8 +9,8 @@
9
9
  ```bash
10
10
  dsh-wb-module init <unit> --dir <目录> # 生成这份骨架(只有这一个模板)
11
11
  dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
12
- dsh-wb-module check # 十三条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
13
- dsh-wb-module check --only cards # 十三条可以单跑
12
+ dsh-wb-module check # 十四条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 执行缝 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
13
+ dsh-wb-module check --only cards # 十四条可以单跑
14
14
  ```
15
15
 
16
16
  包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
@@ -1,7 +1,7 @@
1
1
  // 这份模块自己的真读断言:换数据源时改这一份,不要删 smoke.mjs 里那一整段。
2
2
  //
3
3
  // `assertModule()` 只在这里拿到真东西(路由表与 check),想验什么就写什么:
4
- // 卡片画的数从哪来,这里真的调一次 —— `check` 的十三条判据一条都不碰这条。
4
+ // 卡片画的数从哪来,这里真的调一次 —— `check` 的十四条判据一条都不碰这条。
5
5
 
6
6
  import { readdirSync } from 'node:fs';
7
7
  import { dirname } from 'node:path';
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // ★ 这份脚本验的是**形状**:它认不出成员名,所以换数据源、换卡片内容都不用改它。
8
8
  // 模板自己的"真读"与"真跑"在 `scripts/smoke.assert.mjs` 里:换数据源时改那一份,
9
- // 不要删这里的调用 —— 卡片画的数从哪来,`check` 的十三条判据一条都不碰,只有这里会真调一次。
9
+ // 不要删这里的调用 —— 卡片画的数从哪来,`check` 的十四条判据一条都不碰,只有这里会真调一次。
10
10
  //
11
11
  // 负例在最后一段:服务不在的时候必须**报出来**,不许静默通过 —— 静默的模块是最难查的那种坏。
12
12
 
@@ -0,0 +1,71 @@
1
+ import { createElement as h } from 'react';
2
+ import { kit } from '@dsh-workbench/core/client';
3
+
4
+ const UNIT_ID = '__UNIT__';
5
+ const SETUP_ROUTE = `${UNIT_ID}/setup`;
6
+
7
+ const { SetupShell, TextField } = kit.components;
8
+ const { useUnitSetup } = kit.hooks;
9
+
10
+ interface SetupPayload {
11
+ readonly configured?: boolean;
12
+ readonly values?: { readonly name?: string };
13
+ }
14
+
15
+ export interface SetupFlowProps {
16
+ readonly t: (key: string) => string;
17
+ readonly path: string;
18
+ readonly onDone: () => void;
19
+ }
20
+
21
+ export function SetupFlow({ t, path, onDone }: SetupFlowProps): ReturnType<typeof h> {
22
+ const setup = useUnitSetup({
23
+ steps: [
24
+ {
25
+ id: 'name',
26
+ title: t(`${UNIT_ID}.setup.title`),
27
+ render: (ctx) =>
28
+ h(TextField, {
29
+ value: String(ctx.values['name'] ?? ''),
30
+ placeholder: t(`${UNIT_ID}.setup.namePlaceholder`),
31
+ disabled: ctx.busy,
32
+ onChange: (event) => {
33
+ ctx.setValues({ ...ctx.values, name: event.target.value });
34
+ },
35
+ }),
36
+ submit: async (values) => {
37
+ const response = await kit.api.apiPost<{ ok: boolean }>(SETUP_ROUTE, {
38
+ path,
39
+ name: String(values['name'] ?? ''),
40
+ });
41
+ return response.ok ? { ok: true } : { ok: false, message: response.error.message };
42
+ },
43
+ },
44
+ ],
45
+ read: async () => {
46
+ const response = await kit.api.apiGet<SetupPayload>(SETUP_ROUTE, { path });
47
+ if (!response.ok) return { configured: false, values: {} };
48
+ return {
49
+ configured: response.configured === true,
50
+ values: { name: response.values?.name ?? '' },
51
+ };
52
+ },
53
+ onReady: () => {
54
+ onDone();
55
+ },
56
+ });
57
+ return h(SetupShell, {
58
+ setup,
59
+ labels: {
60
+ back: t(`${UNIT_ID}.setup.back`),
61
+ next: t(`${UNIT_ID}.setup.next`),
62
+ finish: t(`${UNIT_ID}.setup.finish`),
63
+ save: t(`${UNIT_ID}.setup.save`),
64
+ saving: t(`${UNIT_ID}.setup.saving`),
65
+ cancel: t(`${UNIT_ID}.setup.cancel`),
66
+ reading: t(`${UNIT_ID}.setup.reading`),
67
+ failed: t(`${UNIT_ID}.setup.failed`),
68
+ },
69
+ renderHome: () => null,
70
+ });
71
+ }
@@ -24,6 +24,16 @@ export const messages = {
24
24
  '__UNIT__.prefsLabel': '最大的几个文件',
25
25
  '__UNIT__.prefsHint': '这个选择按项目存(刷新还在);只影响卡片显示,不改读到的数据',
26
26
  '__UNIT__.prefsUnit': ' 个',
27
+ '__UNIT__.setup.title': '显示名',
28
+ '__UNIT__.setup.namePlaceholder': '给这个模块起个显示名',
29
+ '__UNIT__.setup.back': '上一步',
30
+ '__UNIT__.setup.next': '下一步',
31
+ '__UNIT__.setup.finish': '完成',
32
+ '__UNIT__.setup.save': '保存',
33
+ '__UNIT__.setup.saving': '保存中',
34
+ '__UNIT__.setup.cancel': '取消',
35
+ '__UNIT__.setup.reading': '正在读配置…',
36
+ '__UNIT__.setup.failed': '读不到配置',
27
37
  },
28
38
  en: {
29
39
  '__UNIT__.cardTitle': 'Directory overview',
@@ -50,5 +60,15 @@ export const messages = {
50
60
  '__UNIT__.prefsLabel': 'Largest files shown',
51
61
  '__UNIT__.prefsHint': 'Stored per project (survives refresh); affects display only, not the data',
52
62
  '__UNIT__.prefsUnit': '',
63
+ '__UNIT__.setup.title': 'Display name',
64
+ '__UNIT__.setup.namePlaceholder': 'Give this module a display name',
65
+ '__UNIT__.setup.back': 'Back',
66
+ '__UNIT__.setup.next': 'Next',
67
+ '__UNIT__.setup.finish': 'Finish',
68
+ '__UNIT__.setup.save': 'Save',
69
+ '__UNIT__.setup.saving': 'Saving',
70
+ '__UNIT__.setup.cancel': 'Cancel',
71
+ '__UNIT__.setup.reading': 'Reading config…',
72
+ '__UNIT__.setup.failed': 'Cannot read config',
53
73
  },
54
74
  };