@dsh-workbench/module-tools 1.1.13 → 1.1.14-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.
Files changed (64) hide show
  1. package/README.md +36 -23
  2. package/bin/dsh-wb-module.mjs +18 -4
  3. package/package.json +4 -4
  4. package/src/arch.mjs +36 -25
  5. package/src/check.mjs +50 -3
  6. package/src/checks/arch.mjs +2 -2
  7. package/src/checks/contracts.mjs +88 -41
  8. package/src/checks/kit.mjs +3 -2
  9. package/src/checks/lint.mjs +2 -2
  10. package/src/checks/primitives.mjs +2 -2
  11. package/src/discover.mjs +186 -16
  12. package/src/init.mjs +7 -8
  13. package/src/lint.mjs +4 -16
  14. package/src/market-index.mjs +1 -1
  15. package/src/migrate.mjs +69 -0
  16. package/src/release-journal.mjs +3 -1
  17. package/src/release-target.mjs +1 -1
  18. package/src/release-util.mjs +7 -1
  19. package/template-diy/AGENTS.md.tpl +14 -22
  20. package/template-diy/README.md.tpl +61 -65
  21. package/template-diy/design.json.tpl +19 -21
  22. package/template-diy/package.json.tpl +6 -2
  23. package/template-diy/scripts/smoke.assert.mjs.tpl +28 -0
  24. package/template-diy/scripts/smoke.mjs.tpl +150 -30
  25. package/template-diy/src/__tests__/plugin.test.ts.tpl +453 -0
  26. package/template-diy/src/components/NotesSection.tsx.tpl +74 -0
  27. package/template-diy/src/components/OverviewBody.module.css.tpl +5 -0
  28. package/template-diy/src/components/OverviewBody.tsx.tpl +64 -0
  29. package/template-diy/src/components/OverviewCard.module.css.tpl +9 -1
  30. package/template-diy/src/components/OverviewCard.tsx.tpl +108 -67
  31. package/template-diy/src/components/PrefsSection.tsx.tpl +36 -0
  32. package/template-diy/src/components/RunSection.tsx.tpl +69 -0
  33. package/template-diy/src/components/{OverviewCard.test.ts.tpl → __tests__/OverviewCard.test.ts.tpl} +51 -1
  34. package/template-diy/src/declared-tools.ts.tpl +6 -0
  35. package/template-diy/src/host/notes.ts.tpl +92 -0
  36. package/template-diy/src/host/overview.ts.tpl +57 -0
  37. package/template-diy/src/host/sample.ts.tpl +151 -0
  38. package/template-diy/src/host.ts.tpl +88 -66
  39. package/template-diy/src/messages.ts.tpl +28 -0
  40. package/template-diy/src/model/__tests__/flow.test.ts.tpl +36 -0
  41. package/template-diy/src/model/__tests__/notes.test.ts.tpl +49 -0
  42. package/template-diy/src/model/{overview.test.ts.tpl → __tests__/overview.test.ts.tpl} +1 -1
  43. package/template-diy/src/model/__tests__/prefs.test.ts.tpl +59 -0
  44. package/template-diy/src/model/flow.ts.tpl +47 -0
  45. package/template-diy/src/model/notes.ts.tpl +52 -0
  46. package/template-diy/src/model/overview.ts.tpl +3 -1
  47. package/template-diy/src/model/prefs.ts.tpl +61 -0
  48. package/template/AGENTS.md.tpl +0 -33
  49. package/template/CHANGELOG.md.tpl +0 -7
  50. package/template/README.md.tpl +0 -30
  51. package/template/arch.config.mjs +0 -2
  52. package/template/cordis.patch.yml.tpl +0 -3
  53. package/template/eslint.config.mjs +0 -2
  54. package/template/package.json.tpl +0 -50
  55. package/template/scripts/smoke.mjs.tpl +0 -192
  56. package/template/src/client.tsx.tpl +0 -22
  57. package/template/src/components/HelloCard.tsx.tpl +0 -28
  58. package/template/src/host.ts.tpl +0 -21
  59. package/template/src/messages.ts.tpl +0 -12
  60. package/template/src/plugin.test.ts.tpl +0 -56
  61. package/template/tsconfig.json +0 -20
  62. package/template/types/globals.d.ts +0 -14
  63. package/template/vitest.config.ts +0 -14
  64. package/template-diy/src/plugin.test.ts.tpl +0 -159
@@ -170,8 +170,9 @@ export async function run() {
170
170
  if (missing.length > 0) {
171
171
  problems.push(
172
172
  `${name}: 产物用了这份外壳(${shellClient})里没有的 ${missing.join('、')} —— ` +
173
- `顶层解构不会抛(拿到 undefined),卡片首帧渲染才炸 "Element type is invalid",` +
174
- `而外壳没有错误边界 ⇒ 整块面板会跟着没。要么去掉/换掉这些成员,要么把 peer 下限提到真正需要的那版`,
173
+ `顶层解构不会抛(拿到 undefined),卡片首帧渲染才炸 "Element type is invalid";` +
174
+ `外壳会把这张卡收进错误态(只炸这一张,但用户看到的是错误页而不是你的卡)。` +
175
+ `要么去掉/换掉这些成员,要么把 peer 下限提到真正需要的那版`,
175
176
  );
176
177
  }
177
178
  }
@@ -111,7 +111,7 @@ async function loadConfig(pkg) {
111
111
  return {
112
112
  error:
113
113
  `${pkg.manifest.name}: ${CONFIG_FILE} 跑不起来 —— ${messageOf(error)}` +
114
- '(先确认这份配置能 import、没有语法错,再确认它 import 的东西在工坊里解析得到)',
114
+ '(先确认这份配置能 import、没有语法错,再确认它 import 的东西在工作区里解析得到)',
115
115
  };
116
116
  }
117
117
 
@@ -251,7 +251,7 @@ export async function run() {
251
251
 
252
252
  if (problems.length > 0) {
253
253
 
254
- const only = process.env.DSH_WB_ONLY === 'lint';
254
+ const only = process.env.DSH_WB_ONLY === 'lint' || process.env.DSH_WB_PACKAGE_FILTER !== '';
255
255
  const shown = only ? problems : problems.slice(0, MAX_PROBLEMS);
256
256
  const notes = [
257
257
  '红了怎么修:先看位置与规则名,再决定"改代码"还是"就地 eslint-disable-next-line";' +
@@ -115,9 +115,9 @@ function faceOf(modulePackages) {
115
115
  const notes = [
116
116
  `这份工程里没装 ${PRIMITIVES_SPECIFIER} —— 列不出清单。`,
117
117
  ` 它在 @deepseek-ai/dsh-client-ui-primitives 那个包的 lib/types/**/*.d.ts 里(源码不随包发布)。`,
118
- ` 先加一行再 pnpm install:`,
118
+ ` 在模块清单里加这一行:`,
119
119
  ` "devDependencies": { "${PRIMITIVES_SPECIFIER}": "${want}" }`,
120
- ` 两个模板默认带这一行;加完在 dsh.client.external 里也声明 "${PRIMITIVES_SPECIFIER}"(两处都要)。`,
120
+ ` 模板默认带这一行;加完在 dsh.client.external 里也声明 "${PRIMITIVES_SPECIFIER}"(两处都要)。`,
121
121
  ];
122
122
  if (pinned !== undefined) notes.push(` 外壳(${SHELL}${shellVersion === undefined ? '' : `@${shellVersion}`})钉的是 ${pinned} —— 对齐它。`);
123
123
  return { notes, payload: { installed: null, pinned: pinned ?? null, modules, snippet: devDependencySnippet(want) } };
package/src/discover.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
2
+ import { basename, dirname, join, resolve, sep } from 'node:path';
3
3
 
4
4
  function manifestOf(dir) {
5
5
  const file = join(dir, 'package.json');
@@ -11,41 +11,211 @@ function manifestOf(dir) {
11
11
  }
12
12
  }
13
13
 
14
+ function stripComment(line) {
15
+ const at = line.indexOf('#');
16
+ return at === -1 ? line : line.slice(0, at);
17
+ }
18
+
19
+ function unquote(value) {
20
+ return value.trim().replace(/^['"]|['"]$/g, '').trim();
21
+ }
22
+
23
+ function flowPatterns(text) {
24
+ const inner = text.trim().replace(/^\[/, '').replace(/\]$/, '');
25
+ if (inner.trim() === '') return [];
26
+ return inner
27
+ .split(',')
28
+ .map(unquote)
29
+ .filter((one) => one !== '');
30
+ }
31
+
32
+ function patternsFromYaml(text) {
33
+ const lines = text.split(/\r?\n/);
34
+ const patterns = [];
35
+ let state = 'idle';
36
+ for (const raw of lines) {
37
+ const line = stripComment(raw);
38
+ if (line.trim() === '') continue;
39
+ if (state === 'idle') {
40
+ const match = /^packages\s*:\s*(.*)$/.exec(line);
41
+ if (match === null) continue;
42
+ const rest = match[1].trim();
43
+ if (rest.startsWith('[')) {
44
+ const flow = flowPatterns(rest);
45
+ patterns.push(...flow);
46
+ return { declared: true, patterns, empty: flow.length === 0 };
47
+ }
48
+ if (rest !== '') return { declared: true, patterns: flowPatterns(rest) };
49
+ state = 'block';
50
+ continue;
51
+ }
52
+ const item = /^\s*-\s*(.+?)\s*$/.exec(line);
53
+ if (item !== null) {
54
+ const value = unquote(item[1]);
55
+ if (value !== '') patterns.push(value);
56
+ continue;
57
+ }
58
+ return { declared: true, patterns };
59
+ }
60
+ return state === 'block' ? { declared: true, patterns } : { declared: false, patterns: [] };
61
+ }
62
+
63
+ function patternsOf(root) {
64
+ const yaml = join(root, 'pnpm-workspace.yaml');
65
+ if (existsSync(yaml)) {
66
+ const parsed = patternsFromYaml(readFileSync(yaml, 'utf8'));
67
+ if (parsed.declared) {
68
+ if (parsed.patterns.length === 0 && parsed.empty !== true) {
69
+ throw new Error(
70
+ `pnpm-workspace.yaml 里有 packages 但没有解析出任何条目:${yaml} —— ` +
71
+ '支持缩进/不缩进的块列表、行内数组与 ! 排除;花括号、字符类等写法请改成显式目录',
72
+ );
73
+ }
74
+ return parsed;
75
+ }
76
+ }
77
+ const manifest = manifestOf(root);
78
+ const workspaces = manifest?.workspaces;
79
+ if (Array.isArray(workspaces)) {
80
+ return { declared: true, patterns: workspaces.filter((one) => typeof one === 'string') };
81
+ }
82
+ if (workspaces !== null && typeof workspaces === 'object' && Array.isArray(workspaces.packages)) {
83
+ return { declared: true, patterns: workspaces.packages.filter((one) => typeof one === 'string') };
84
+ }
85
+ return { declared: false, patterns: [] };
86
+ }
87
+
14
88
  export function workspaceRootOf(start) {
15
89
  if (process.env.DSH_WB_ROOT) return process.env.DSH_WB_ROOT;
16
90
  let dir = start;
91
+ let legacy;
17
92
  for (;;) {
18
- if (existsSync(join(dir, 'packages')) || existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
93
+ if (existsSync(join(dir, 'pnpm-workspace.yaml'))) return dir;
94
+ const manifest = manifestOf(dir);
95
+ if (manifest !== undefined) {
96
+ const workspaces = manifest.workspaces;
97
+ if (Array.isArray(workspaces) || (workspaces !== null && typeof workspaces === 'object')) return dir;
98
+ }
99
+ if (legacy === undefined && existsSync(join(dir, 'packages'))) legacy = dir;
19
100
  const parent = dirname(dir);
20
- if (parent === dir) return start;
101
+ if (parent === dir) return legacy ?? start;
21
102
  dir = parent;
22
103
  }
23
104
  }
24
105
 
106
+ function isDir(dir) {
107
+ try {
108
+ return statSync(dir).isDirectory();
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function segmentMatches(name, segment) {
115
+ if (/[{}[\]()|+@!]/.test(segment)) {
116
+ throw new Error(`工作区 glob 里这个写法暂不支持:${segment}(改成 *、** 或显式目录)`);
117
+ }
118
+ const pattern = segment
119
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
120
+ .replace(/\*/g, '[^/]*')
121
+ .replace(/\?/g, '[^/]');
122
+ return new RegExp(`^${pattern}$`).test(name);
123
+ }
124
+
125
+ function childDirs(dir) {
126
+ try {
127
+ return readdirSync(dir, { withFileTypes: true })
128
+ .filter((entry) => entry.name !== 'node_modules' && !entry.name.startsWith('.'))
129
+ .map((entry) => join(dir, entry.name))
130
+ .filter((child) => isDir(child));
131
+ } catch {
132
+ return [];
133
+ }
134
+ }
135
+
136
+ function walkDirs(dir, out) {
137
+ out.push(dir);
138
+ for (const child of childDirs(dir)) walkDirs(child, out);
139
+ return out;
140
+ }
141
+
142
+ function expandPattern(root, pattern) {
143
+ if (/[{}[\]()|+]/.test(pattern)) {
144
+ throw new Error(`工作区 glob 里这个写法暂不支持:${pattern}(改成 *、** 或显式目录)`);
145
+ }
146
+ const parts = pattern.replace(/\\/g, '/').split('/').filter((one) => one !== '' && one !== '.');
147
+ let current = [root];
148
+ for (const part of parts) {
149
+ const next = [];
150
+ for (const base of current) {
151
+ if (part === '**') {
152
+ walkDirs(base, next);
153
+ continue;
154
+ }
155
+ if (part.includes('*') || part.includes('?')) {
156
+ for (const child of childDirs(base)) {
157
+ if (segmentMatches(basename(child), part)) next.push(child);
158
+ }
159
+ continue;
160
+ }
161
+ const exact = join(base, part);
162
+ if (isDir(exact)) next.push(exact);
163
+ }
164
+ current = next;
165
+ }
166
+ return current;
167
+ }
168
+
169
+ const cache = new Map();
170
+
171
+ function legacyPackages(root) {
172
+ return childDirs(join(root, 'packages'));
173
+ }
174
+
25
175
  export function discoverPackages({ root = process.cwd() } = {}) {
26
176
  const workspace = workspaceRootOf(root);
27
- const dirs = [];
28
- const packagesDir = join(workspace, 'packages');
29
- if (existsSync(packagesDir)) {
30
- for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
177
+ const parsed = patternsOf(workspace);
178
+ const positive = parsed.patterns.filter((one) => !one.startsWith('!'));
179
+ const negative = parsed.patterns.filter((one) => one.startsWith('!')).map((one) => one.slice(1));
180
+ const key = [workspace, root, parsed.patterns.join('|')].join('\0');
181
+ const cached = cache.get(key);
182
+ if (cached !== undefined) return cached.map((pkg) => ({ ...pkg }));
31
183
 
32
- if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
33
- dirs.push(join(packagesDir, entry.name));
184
+ const dirs = [];
185
+ const push = (dir) => {
186
+ if (existsSync(dir)) dirs.push(dir);
187
+ };
188
+ if (!parsed.declared) {
189
+ for (const dir of legacyPackages(workspace)) push(dir);
190
+ } else {
191
+ const excluded = new Set();
192
+ for (const pattern of negative) {
193
+ for (const dir of expandPattern(workspace, pattern)) excluded.add(realpathSync(dir));
194
+ }
195
+ for (const pattern of positive) {
196
+ for (const dir of expandPattern(workspace, pattern)) {
197
+ if (excluded.has(realpathSync(dir))) continue;
198
+ push(dir);
199
+ }
34
200
  }
35
201
  }
36
- dirs.push(root, workspace);
202
+ const isolated = process.env.DSH_WB_ROOT?.trim();
203
+ const inside = isolated === undefined || isolated === '' || resolve(root).startsWith(resolve(workspace) + sep);
204
+ if (inside) push(root);
205
+ push(workspace);
206
+
37
207
  const seen = new Set();
38
208
  const found = [];
39
209
  for (const dir of dirs) {
40
-
41
- const key = existsSync(dir) ? realpathSync(dir) : dir;
42
- if (seen.has(key)) continue;
43
- seen.add(key);
210
+ const resolved = existsSync(dir) ? realpathSync(dir) : dir;
211
+ if (seen.has(resolved)) continue;
212
+ seen.add(resolved);
44
213
  const manifest = manifestOf(dir);
45
214
  if (manifest === undefined || typeof manifest.name !== 'string') continue;
46
215
  found.push({ dir, manifest });
47
216
  }
48
- return found;
217
+ cache.set(key, found);
218
+ return found.map((pkg) => ({ ...pkg }));
49
219
  }
50
220
 
51
221
  export function discoverModulePackages(options = {}) {
package/src/init.mjs CHANGED
@@ -4,7 +4,6 @@ import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const TEMPLATES = {
7
- minimal: '../template',
8
7
  diy: '../template-diy',
9
8
  };
10
9
  const UNIT_ID = /^[a-z][a-z0-9-]*$/;
@@ -26,7 +25,7 @@ const dshUiVersion = (root) => {
26
25
 
27
26
  }
28
27
  console.error(
29
- `读不到外壳 manifest 里钉的 ${DSH_UI}:模板保持 ${FALLBACK_DSH_UI}(与外壳对账过的那版);在装好依赖的工程里 init 就能盖上当下那版`,
28
+ `读不到外壳 manifest 里钉的 ${DSH_UI}:模板保持 ${FALLBACK_DSH_UI}(与外壳对账过的那版);工程里装过依赖时 init 会盖上当下那版`,
30
29
  );
31
30
  return { version: FALLBACK_DSH_UI, source: '兜底常量(没读到外壳 manifest)' };
32
31
  };
@@ -46,7 +45,7 @@ const shellRange = (root) => {
46
45
  const own = versionIn(fileURLToPath(new URL('../package.json', import.meta.url)));
47
46
  if (own !== undefined) return `^${own}`;
48
47
  console.error(
49
- `读不到 @dsh-workbench/core 与 module-tools 的版本:模板里那三处 @dsh-workbench/* 保持 ${FALLBACK_RANGE}(与以前一样);在装好依赖的工程里 init 就能盖上当下范围`,
48
+ `读不到 @dsh-workbench/core 与 module-tools 的版本:模板里那三处 @dsh-workbench/* 保持 ${FALLBACK_RANGE}(与以前一样);工程里装过依赖时 init 会盖上当下范围`,
50
49
  );
51
50
  return FALLBACK_RANGE;
52
51
  };
@@ -54,13 +53,13 @@ const shellRange = (root) => {
54
53
  export async function init({ root, args = [] }) {
55
54
  const unit = args[0];
56
55
  if (unit === undefined || !UNIT_ID.test(unit)) {
57
- console.error('用法:dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description <文字>]');
56
+ console.error('用法:dsh-wb-module init <unit> [--dir <目录>] [--template diy] [--description <文字>]');
58
57
  console.error(' unit 是小写短横线标识(它同时是单元 id、包名后缀、卡片 class 前缀)');
59
- console.error(' --template diy:带一个真数据源 + 卡片的样板(默认 minimal 是空壳)');
58
+ console.error(' --template diy:唯一模板(真数据源 + 卡片 + 可跑流程/工具),也是默认');
60
59
  process.exit(2);
61
60
  }
62
61
  const picked = args[args.indexOf('--template') + 1];
63
- const which = args.includes('--template') ? picked : 'minimal';
62
+ const which = args.includes('--template') ? picked : 'diy';
64
63
  const template = TEMPLATES[which];
65
64
  if (template === undefined) {
66
65
  console.error(`不认识的模板:${String(which)}(可用:${Object.keys(TEMPLATES).join(' / ')})`);
@@ -68,8 +67,9 @@ export async function init({ root, args = [] }) {
68
67
  }
69
68
  const at = args.indexOf('--dir');
70
69
  const target = at === -1 ? join(root, `module-${unit}`) : args[at + 1];
70
+ const describedAt = args.indexOf('--description');
71
71
  const description =
72
- args[args.indexOf('--description') + 1] ?? `${unit} 模块:独立 dsh 插件包`;
72
+ describedAt === -1 ? `${unit} 模块:独立 dsh 插件包` : (args[describedAt + 1] ?? `${unit} 模块:独立 dsh 插件包`);
73
73
  if (existsSync(target) && readdirSync(target).length > 0) {
74
74
  console.error(`${target} 已经存在且不是空目录,不覆盖`);
75
75
  process.exit(2);
@@ -114,6 +114,5 @@ export async function init({ root, args = [] }) {
114
114
  console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / design / selfcheck 已接工具)`);
115
115
  console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,README 由它生成`);
116
116
  console.log(` 模板默认带了 ${DSH_UI}@${dshUi.version}(${dshUi.source})—— 这是 dsh 原语的类型面 + external 声明两处;`);
117
- console.log(' 装完先在这个 workspace 里跑一次 pnpm install,pnpm typecheck 才认得到它(骨架的判据不依赖它)。');
118
117
  console.log(' 确实用不到就连 devDependencies 与 dsh.client.external 两处一起删,判据不拦。');
119
118
  }
package/src/lint.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import js from '@eslint/js';
2
2
  import react from 'eslint-plugin-react';
3
3
  import reactHooks from 'eslint-plugin-react-hooks';
4
+ import globals from 'globals';
4
5
  import tseslint from 'typescript-eslint';
5
6
 
6
7
  export const FINGERPRINT = 'dsh-wb/module-lint';
@@ -58,23 +59,10 @@ const PLATFORM_READ = {
58
59
  "MemberExpression[object.name='process'][property.name='platform'], " +
59
60
  "MemberExpression[object.object.name='process'][object.property.name='versions'][property.name='electron'], " +
60
61
  "MemberExpression[object.name='window'][property.name='dshDesktop'], " +
61
- "MemberExpression[property.name='hostOs'], " +
62
62
  "MemberExpression[object.name='navigator'][property.name='userAgent']",
63
63
  message:
64
- '平台/外壳差异走能力面(HostSurface / ClientSurface):模块源码不直接读 ' +
65
- 'process.platform / versions.electron / dshDesktop / hostOs / navigator.userAgent',
66
- };
67
-
68
- const NODE_GLOBALS = {
69
- Buffer: 'readonly',
70
- URL: 'readonly',
71
- URLSearchParams: 'readonly',
72
- console: 'readonly',
73
- fetch: 'readonly',
74
- module: 'readonly',
75
- process: 'readonly',
76
- setTimeout: 'readonly',
77
- clearTimeout: 'readonly',
64
+ '平台/外壳差异走能力面:宿主面读 deps.surface(HostSurface),浏览器面读 kit.hooks.usePlatform();' +
65
+ '模块源码不直接读 process.platform / versions.electron / dshDesktop / navigator.userAgent',
78
66
  };
79
67
 
80
68
  const SCRIPT_RULES = {
@@ -174,7 +162,7 @@ export function moduleLint(options = {}) {
174
162
  {
175
163
  name: 'dsh-wb/module-lint/scripts',
176
164
  files: SCRIPT_FILES,
177
- languageOptions: { globals: NODE_GLOBALS },
165
+ languageOptions: { globals: { ...globals.node } },
178
166
  rules: SCRIPT_RULES,
179
167
  },
180
168
  ...overrideRules,
@@ -415,7 +415,7 @@ export function resolveIndexPlan({ args = [], home, exec }) {
415
415
  if (plan === undefined) {
416
416
  if (asked === 'auto' || asked === '') return undefined;
417
417
  throw new MarketIndexError(
418
- `这个工坊还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_DIR}/。`,
418
+ `这台机器还没配可写的 git 索引源。给我一个能推的 git 仓库(或它在本机的 clone 路径),我配成索引源并建一份 ${MARKET_DIR}/。`,
419
419
  'index-write',
420
420
  );
421
421
  }
@@ -0,0 +1,69 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const API_VERSION = 3;
5
+
6
+ const DEFAULT_FACES = { host: 'src/host.ts', client: 'src/client.tsx' };
7
+
8
+ export async function migrate({ root, args = [] }) {
9
+ const dryRun = args.includes('--dry-run');
10
+ const file = join(root, 'package.json');
11
+ if (!existsSync(file)) {
12
+ console.error(`这里没有 package.json:${root}`);
13
+ process.exit(2);
14
+ }
15
+
16
+ let manifest;
17
+ try {
18
+ manifest = JSON.parse(readFileSync(file, 'utf8'));
19
+ } catch (error) {
20
+ console.error(`package.json 不是合法 JSON:${error.message}`);
21
+ process.exit(2);
22
+ }
23
+
24
+ const dsh = manifest.dsh;
25
+ const workbench = dsh?.workbench;
26
+ if (workbench === undefined) {
27
+ console.error('这个包没声明 dsh.workbench:先 cd 到模块目录再跑(或加 --cwd <模块目录>)');
28
+ process.exit(2);
29
+ }
30
+
31
+ const changes = [];
32
+ if (workbench.apiVersion !== API_VERSION) {
33
+ changes.push(`dsh.workbench.apiVersion:${JSON.stringify(workbench.apiVersion)} → ${String(API_VERSION)}`);
34
+ workbench.apiVersion = API_VERSION;
35
+ }
36
+
37
+ if (dsh.workbenchFaces === undefined) {
38
+ changes.push(`dsh.workbenchFaces:补默认 ${DEFAULT_FACES.host} / ${DEFAULT_FACES.client}`);
39
+ dsh.workbenchFaces = { ...DEFAULT_FACES };
40
+ } else {
41
+ for (const [kind, fallback] of Object.entries(DEFAULT_FACES)) {
42
+ if (typeof dsh.workbenchFaces[kind] === 'string' && dsh.workbenchFaces[kind] !== '') continue;
43
+ changes.push(`dsh.workbenchFaces.${kind}:补 ${fallback}`);
44
+ dsh.workbenchFaces[kind] = fallback;
45
+ }
46
+ }
47
+
48
+ for (const [kind, rel] of Object.entries(dsh.workbenchFaces)) {
49
+ if (!existsSync(join(root, rel))) {
50
+ console.warn(`⚠ workbenchFaces.${kind} 指的 ${rel} 不存在 —— 先把这个入口建出来`);
51
+ }
52
+ }
53
+
54
+ if (changes.length === 0) {
55
+ console.log('已经是当前代:清单不用改。');
56
+ } else if (dryRun) {
57
+ console.log(`会改 ${String(changes.length)} 处(--dry-run 没写):`);
58
+ for (const one of changes) console.log(` · ${one}`);
59
+ } else {
60
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
61
+ console.log(`改好 ${String(changes.length)} 处:`);
62
+ for (const one of changes) console.log(` · ${one}`);
63
+ }
64
+
65
+ if (workbench.card === null || workbench.card === undefined) {
66
+ console.log('清单 card 是 null / 缺:如果 client.js 真登记了卡,跑 `check --only cards` 看 —— 补 `"card": { "order": … }` 或去掉那张卡的登记。');
67
+ }
68
+ console.log('源码类迁移不自动改:`check` 会逐条点名(kit 成员、裸控件、apiVersion 之外的协议字段),照提示改源码。');
69
+ }
@@ -2,6 +2,8 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
 
5
+ import { workbenchRootOf } from './release-util.mjs';
6
+
5
7
  const FINGERPRINT_FILES = [
6
8
  'package.json',
7
9
  'design.json',
@@ -13,7 +15,7 @@ const FINGERPRINT_FILES = [
13
15
  ];
14
16
 
15
17
  function statePathOf(home, unit) {
16
- return join(home, 'workbench', 'releases', `${unit}.json`);
18
+ return join(workbenchRootOf(home), 'releases', `${unit}.json`);
17
19
  }
18
20
 
19
21
  export function readState(home, unit) {
@@ -159,7 +159,7 @@ export function registryInfoOf(root, manifest, args, target, fallbackRegistry) {
159
159
  if (registry === '') {
160
160
  throw new NeedsUser(
161
161
  'registry',
162
- '这个工坊还没配团队源。让公司把 registry 和 token 写进 `.npmrc`;或者我先给你打一个安装包。',
162
+ '这台机器还没配团队源。让公司把 registry 和 token 写进 `.npmrc`;或者我先给你打一个安装包。',
163
163
  undefined,
164
164
  { target },
165
165
  );
@@ -80,8 +80,14 @@ export function dshHomeOf(home) {
80
80
  return home ?? process.env.DSH_HOME?.trim() ?? join(homedir(), '.dsh');
81
81
  }
82
82
 
83
+ export function workbenchRootOf(home) {
84
+ const configured = process.env.DSH_WORKBENCH_HOME?.trim();
85
+ if (configured !== undefined && configured !== '') return configured;
86
+ return join(dshHomeOf(home), 'workbench');
87
+ }
88
+
83
89
  export function artifactsDirOf(home, unit) {
84
- return join(dshHomeOf(home), 'workbench', 'artifacts', unit);
90
+ return join(workbenchRootOf(home), 'artifacts', unit);
85
91
  }
86
92
 
87
93
  export function publishTagOf(version) {
@@ -1,33 +1,25 @@
1
1
  # __NAME__ · agent 工作说明
2
2
 
3
- 这个包是工作台模块(单元 `__UNIT__`)。`README.md` 讲模块本身;这份讲 agent 在这份代码里怎么干活。
3
+ 工作台模块(单元 `__UNIT__`)。模块行为与数据看 `README.md`;这份只说工具链怎么用。
4
4
 
5
- ## 命令
5
+ ## 干活循环
6
6
 
7
- ```bash
8
- pnpm install
9
- pnpm build # dist/host.js + client.js
10
- pnpm typecheck
11
- pnpm test
12
- pnpm selfcheck
13
- pnpm check # 十三条模块判据
14
- pnpm lint
15
- pnpm design check
16
- ```
7
+ 改 `src/**` 后:
17
8
 
18
- 发布(维护者确认之后):
9
+ 1. `pnpm build` —— 产出 `dist/host.js` + 套了 loader 壳的 `client.js`;产物由它生成。
10
+ 2. `pnpm check` —— 十三条判据,读的就是上一步的产物。单跑一条用 `pnpm check --only <判据名>`;
11
+ **全绿**才算这件活做完。怎么读红、常见红:`@dsh-workbench/core/skills/workbench-module-diy/REVIEW.md`。
12
+ 3. `pnpm test` —— `src/**/__tests__/` 下、与改动同目录的 `*.test.ts`;新逻辑配对应用例,**全绿**才算完。
19
13
 
20
- ```bash
21
- dsh-wb-module release --target team|public|tarball|git
22
- ```
14
+ `pnpm selfcheck` = build + `design check` + smoke;`design.json` 里的(待填)填完才绿。
23
15
 
24
- `--dry-run` 只预览,`--verify` 只核已发布的包。
16
+ ## 工具链约定
25
17
 
26
- ## 不要
27
-
28
- - 手改 `client.js` / `dist/**`:构建产物,改 `src/**` 后重新 build。
18
+ - 清单是工具名唯一真相:`dsh-wb-module sync` 生成 `src/declared-tools.ts`,代码引 `DECLARED_TOOLS`。
19
+ - 判据读构建产物:改了 `src/**` 先 build 再 check。
20
+ - 发布:`dsh-wb-module release --target team|public|tarball|git`;`--dry-run` 只预览,`--verify` 只核已发布的包。
29
21
 
30
22
  ## 先读
31
23
 
32
- - `README.md`:模块行为与数据源。
33
- - 工作台模块指引:`@dsh-workbench/core/skills/workbench-module-diy/`(dsh / 工作台的约束随外壳出货)。
24
+ - `README.md`:模块行为、数据源、三张表。
25
+ - `@dsh-workbench/core/skills/workbench-module-diy/`:路由 / 工具 / 卡片 / 文案的写法与约定(随外壳出货)。