@dsh-workbench/module-tools 0.1.16 → 0.1.18
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 +57 -3
- package/bin/dsh-wb-module.mjs +3 -2
- package/package.json +3 -2
- package/src/check.mjs +3 -3
- package/src/checks/alias.mjs +1 -1
- package/src/checks/bundles.mjs +1 -1
- package/src/checks/cards.mjs +24 -2
- package/src/checks/contracts.mjs +309 -35
- package/src/checks/kit.mjs +1 -1
- package/src/checks/markers.mjs +1 -1
- package/src/checks/primitives.mjs +1 -1
- package/src/checks/protocol.mjs +57 -0
- package/src/checks/source.mjs +14 -8
- package/src/checks/standalone.mjs +1 -1
- package/src/checks/style.mjs +83 -33
- package/src/declared-tools.mjs +85 -0
- package/src/design.mjs +3 -14
- package/src/publish.mjs +1 -1
- package/src/sync.mjs +51 -0
- package/template/README.md.tpl +10 -0
- package/template/package.json.tpl +6 -2
- package/template/scripts/smoke.mjs.tpl +4 -6
- package/template/src/client.tsx.tpl +2 -22
- package/template/src/host.ts.tpl +5 -25
- package/template/src/plugin.test.ts.tpl +2 -1
- package/template-diy/README.md.tpl +5 -2
- package/template-diy/package.json.tpl +6 -2
- package/template-diy/scripts/smoke.mjs.tpl +4 -6
- package/template-diy/src/client.tsx.tpl +2 -22
- package/template-diy/src/host.ts.tpl +5 -25
- package/template-diy/src/plugin.test.ts.tpl +4 -4
package/src/checks/style.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// CSS Modules 的哈希类名靠"同一份文件、同一个导出"配对,调用点拿到 undefined 时真机渲染成
|
|
6
6
|
// `class="undefined"` —— 元素整片没样式,而且控制台一声不响。
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
9
9
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
10
10
|
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
@@ -22,17 +22,60 @@ const CLASS_TOKEN = /(^|[^-\w])\.?(wb-[a-zA-Z0-9-]+)/g;
|
|
|
22
22
|
// 契约 = 外壳**所有**声明了 `wb-*` 的样式表,不只是那份全局 `board.css`:
|
|
23
23
|
// ADR-0029 要求组件的样式跟组件走,所以 `wb-card` / `wb-rows` / `wb-note` 这些已经搬进
|
|
24
24
|
// `packages/shared/src/components/*.css`(谁渲染谁拥有)。契约跟着搬,模块借用它们才不误报。
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
// 契约按**两种安装形态**解析,两边必须给同一份名单(实测过差别,见 spec §24.4):
|
|
26
|
+
// · 在外壳仓里跑:源码就在隔壁(`workbench/src/shared/styles/board.css` + `shared/src/components/*.css`);
|
|
27
|
+
// · 在模块仓里跑:只能从**已装的外壳**读 —— `@dsh-workbench/core/dist/board.css`(构建时随包出货的那份
|
|
28
|
+
// 契约副本,见 `packages/workbench/scripts/build.mjs`)与 `@dsh-workbench/shared/src/components/*.css`
|
|
29
|
+
// (shared 本来就发源码)。
|
|
30
|
+
// 从前只写死了相对 module-tools 自己的路径 ⇒ 装进模块仓后 `board.css` 找不到,契约退化成"只有 shared 组件
|
|
31
|
+
// 那一份",于是 `board.css` 里的合法借用(例如 `wb-iconButton`)被**误报**;而且借用检查在契约缺失时
|
|
32
|
+
// 会 `contract.has` 抛异常("契约文件不在"那条 note 根本到不了)。
|
|
33
|
+
const SOURCE_BOARD = fileURLToPath(new URL('../../../workbench/src/shared/styles/board.css', import.meta.url));
|
|
34
|
+
const SOURCE_COMPONENTS = fileURLToPath(new URL('../../../shared/src/components', import.meta.url));
|
|
35
|
+
const INSTALLED_BOARD = join('node_modules', '@dsh-workbench', 'core', 'dist', 'board.css');
|
|
36
|
+
const INSTALLED_COMPONENTS = join('node_modules', '@dsh-workbench', 'shared', 'src', 'components');
|
|
37
|
+
|
|
38
|
+
function installedUnder(dir, tail) {
|
|
39
|
+
let at = resolve(dir);
|
|
40
|
+
for (;;) {
|
|
41
|
+
const candidate = join(at, tail);
|
|
42
|
+
if (existsSync(candidate)) return candidate;
|
|
43
|
+
const parent = dirname(at);
|
|
44
|
+
if (parent === at) return undefined;
|
|
45
|
+
at = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cssFilesIn(dir) {
|
|
50
|
+
try {
|
|
51
|
+
return readdirSync(dir)
|
|
52
|
+
.filter((name) => name.endsWith('.css'))
|
|
53
|
+
.map((name) => join(dir, name));
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function contractFilesFor(moduleDir) {
|
|
60
|
+
const board = existsSync(SOURCE_BOARD) ? SOURCE_BOARD : installedUnder(moduleDir, INSTALLED_BOARD);
|
|
61
|
+
const components = existsSync(SOURCE_COMPONENTS)
|
|
62
|
+
? SOURCE_COMPONENTS
|
|
63
|
+
: installedUnder(moduleDir, INSTALLED_COMPONENTS);
|
|
64
|
+
return [
|
|
65
|
+
...(board === undefined ? [] : [board]),
|
|
66
|
+
...(components === undefined ? [] : cssFilesIn(components)),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contractOf(moduleDir) {
|
|
71
|
+
const files = contractFilesFor(moduleDir);
|
|
72
|
+
if (files.length === 0) return null;
|
|
73
|
+
return new Set(
|
|
74
|
+
files.flatMap((file) =>
|
|
75
|
+
Array.from(readFileSync(file, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
36
79
|
|
|
37
80
|
export const title = '样式归属';
|
|
38
81
|
|
|
@@ -51,19 +94,13 @@ function isTestSource(file) {
|
|
|
51
94
|
}
|
|
52
95
|
|
|
53
96
|
export async function run() {
|
|
54
|
-
const contract = CONTRACT_FILES.some((file) => existsSync(file))
|
|
55
|
-
? new Set(
|
|
56
|
-
CONTRACT_FILES.filter((file) => existsSync(file)).flatMap((file) =>
|
|
57
|
-
Array.from(readFileSync(file, 'utf8').matchAll(CLASS_TOKEN), (match) => match[2]),
|
|
58
|
-
),
|
|
59
|
-
)
|
|
60
|
-
: null;
|
|
61
|
-
|
|
62
97
|
const problems = [];
|
|
63
98
|
const borrowed = [];
|
|
64
99
|
const anchors = [];
|
|
65
100
|
const modules = discoverModulePackages({ root: process.cwd() });
|
|
101
|
+
const contracts = new Map(modules.map((pkg) => [pkg.dir, contractOf(pkg.dir)]));
|
|
66
102
|
for (const pkg of modules) {
|
|
103
|
+
const contract = contracts.get(pkg.dir);
|
|
67
104
|
for (const file of walk(pkg.dir)) {
|
|
68
105
|
if (!/\.(css|ts|tsx)$/.test(file)) continue;
|
|
69
106
|
let text;
|
|
@@ -84,15 +121,14 @@ export async function run() {
|
|
|
84
121
|
|
|
85
122
|
if (isTestSource(file)) continue;
|
|
86
123
|
if (!/\.tsx?$/.test(file)) continue;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
124
|
+
lines.forEach((line, index) => {
|
|
125
|
+
if (!BARE_ANCHOR.test(line)) return;
|
|
126
|
+
anchors.push(
|
|
127
|
+
`${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 手写了裸 <a> —— ` +
|
|
128
|
+
`外链一律用 kit 的 Link(新窗口、noopener、自带那个外链小箭头;样式只有这一份):${line.trim().slice(0, 80)}`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
if (contract === null) continue;
|
|
96
132
|
lines.forEach((line, index) => {
|
|
97
133
|
for (const match of line.matchAll(CLASS_TOKEN)) {
|
|
98
134
|
const name = match[2];
|
|
@@ -149,7 +185,8 @@ export async function run() {
|
|
|
149
185
|
if (borrowed.length > 0) {
|
|
150
186
|
notes.push(
|
|
151
187
|
`${borrowed.length} 处是「模块借用了契约里没有的 wb-* 类」:(契约是本仓 board.css:模块只许借它里面有的类。` +
|
|
152
|
-
'没有的那个要么是拼错/改了名漏改,要么本该进 board.css ——
|
|
188
|
+
'没有的那个要么是拼错/改了名漏改,要么本该进 board.css —— 空类名在真机上就是没样式,而且没人会报错。' +
|
|
189
|
+
'名单是**外壳出货的那份**(`@dsh-workbench/core/dist/board.css` + 外壳组件自己的样式表))',
|
|
153
190
|
);
|
|
154
191
|
}
|
|
155
192
|
if (anchors.length > 0) {
|
|
@@ -170,10 +207,23 @@ export async function run() {
|
|
|
170
207
|
};
|
|
171
208
|
}
|
|
172
209
|
|
|
210
|
+
const noContract = modules
|
|
211
|
+
.filter((pkg) => contracts.get(pkg.dir) === null)
|
|
212
|
+
.map((pkg) => pkg.manifest.name);
|
|
213
|
+
if (noContract.length === modules.length) {
|
|
214
|
+
return {
|
|
215
|
+
status: 'skip',
|
|
216
|
+
notes: [
|
|
217
|
+
'找不到外壳出货的那份契约(`@dsh-workbench/core/dist/board.css` 与 `@dsh-workbench/shared` 的组件样式表都不在)' +
|
|
218
|
+
'—— 模块借的 wb-* 类这一项这次一条都没查,不是"没问题"',
|
|
219
|
+
],
|
|
220
|
+
problems: [],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
173
223
|
const contractNote =
|
|
174
|
-
|
|
175
|
-
? '
|
|
176
|
-
:
|
|
224
|
+
noContract.length === 0
|
|
225
|
+
? '借用的类都在契约里(外壳出货的 board.css + 外壳组件自己的样式表)'
|
|
226
|
+
: `${noContract.join(' / ')} 那一边找不到外壳的契约,借用这一项没查`;
|
|
177
227
|
return {
|
|
178
228
|
status: 'pass',
|
|
179
229
|
notes: [`${modules.length} 个模块都没有定义外壳前缀(wb-*)的 CSS 规则;${contractNote}`],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// 工具名只有**一处写者**:清单(`dsh.workbench.tools[].name`)。
|
|
2
|
+
//
|
|
3
|
+
// 代码里注册一个工具必须报它的名字,所以从前那份字面量在清单与代码里各写一遍 —— 只能靠判据对账
|
|
4
|
+
// (`check:contracts` 的 reconcileTools 就是这么干的)。`tools[].key` + `dsh-wb-module sync` 把
|
|
5
|
+
// 代码那一半**生成**出来:
|
|
6
|
+
//
|
|
7
|
+
// dsh-wb-module sync → src/declared-tools.ts
|
|
8
|
+
// export const DECLARED_TOOLS = {
|
|
9
|
+
// sync: { name: 'workbench_argocd_sync', writes: true },
|
|
10
|
+
// } as const;
|
|
11
|
+
//
|
|
12
|
+
// 于是:改名只动清单一处;代码引用的是**键**(`DECLARED_TOOLS.sync.name`),键写错是编译错;
|
|
13
|
+
// 生成物落后于清单由判据当场判红。
|
|
14
|
+
//
|
|
15
|
+
// 方向是**单向**的:绝不回写 `package.json`(ADR-0038 §2.5 已经拒了"生成式回写":`package.json`
|
|
16
|
+
// 有两个写者(人与生成器)就会静默覆盖)。
|
|
17
|
+
//
|
|
18
|
+
// 输出与比对都按"各家 prettier 都可能重排"来设计:字面量用单引号(本生态的 TS 风格),
|
|
19
|
+
// 比对时**忽略空白、把两种引号当同一件事** —— 重排不算过期,值变了才算。
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
export const DECLARED_TOOLS_FILE = 'src/declared-tools.ts';
|
|
25
|
+
|
|
26
|
+
export function declaredToolsPathOf(dir) {
|
|
27
|
+
return join(dir, DECLARED_TOOLS_FILE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolsOf(raw) {
|
|
31
|
+
return Array.isArray(raw?.tools) ? raw.tools : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function keyOf(tool) {
|
|
35
|
+
return typeof tool?.key === 'string' && tool.key.trim() !== '' ? tool.key.trim() : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 一个 key 都没写的模块不适用(那是不用生成物那一档,判据也不找它)。 */
|
|
39
|
+
export function declaredToolsApplies(raw) {
|
|
40
|
+
return toolsOf(raw).some((tool) => keyOf(tool) !== '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 还没写 key 的那些工具(有 key 就得每个都有:半写会让生成物少几条,而代码里还在写字面量)。 */
|
|
44
|
+
export function declaredToolsWithoutKey(raw) {
|
|
45
|
+
return toolsOf(raw)
|
|
46
|
+
.filter((tool) => keyOf(tool) === '')
|
|
47
|
+
.map((tool) => JSON.stringify(tool?.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 生成物的正文。**不给注释**:它落在模块的 `src/**`(零注释那一条管到这里),改它无效、过期会红。 */
|
|
51
|
+
export function renderDeclaredTools(raw) {
|
|
52
|
+
const rows = toolsOf(raw).map(
|
|
53
|
+
(tool) =>
|
|
54
|
+
` ${keyOf(tool)}: { name: '${tool.name}', writes: ${tool.writes === true} },`,
|
|
55
|
+
);
|
|
56
|
+
return [
|
|
57
|
+
'export const DECLARED_TOOLS = {',
|
|
58
|
+
...rows,
|
|
59
|
+
'} as const;',
|
|
60
|
+
'',
|
|
61
|
+
'export type DeclaredToolKey = keyof typeof DECLARED_TOOLS;',
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 生成物与清单对不上的原因(`undefined` = 新鲜,或不适用)。 */
|
|
67
|
+
export function declaredToolsProblemOf(dir, raw) {
|
|
68
|
+
if (!declaredToolsApplies(raw)) return undefined;
|
|
69
|
+
const missing = declaredToolsWithoutKey(raw);
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
return `清单里 ${missing.join(' / ')} 没有 key —— 有 key 就走生成物那一档,那就每个工具都得有一个`;
|
|
72
|
+
}
|
|
73
|
+
const at = declaredToolsPathOf(dir);
|
|
74
|
+
if (!existsSync(at)) {
|
|
75
|
+
return `${DECLARED_TOOLS_FILE} 不在 —— 清单里给了 key 就该有生成物,跑 \`dsh-wb-module sync\` 生成`;
|
|
76
|
+
}
|
|
77
|
+
const strip = (text) => text.replace(/\s+/g, '').replace(/"/g, "'");
|
|
78
|
+
if (strip(readFileSync(at, 'utf8')) !== strip(renderDeclaredTools(raw))) {
|
|
79
|
+
return (
|
|
80
|
+
`${DECLARED_TOOLS_FILE} 与清单对不上(生成物过期,或有人手改了它)—— ` +
|
|
81
|
+
'跑 `dsh-wb-module sync` 刷新。别手改:它是生成的'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
package/src/design.mjs
CHANGED
|
@@ -41,8 +41,7 @@ const ROWS = {
|
|
|
41
41
|
mayBeEmpty: true,
|
|
42
42
|
},
|
|
43
43
|
capabilities: {
|
|
44
|
-
fields: ['name', 'oneLine', 'shape', 'approval'
|
|
45
|
-
choices: { scope: ['always', 'workspace'] },
|
|
44
|
+
fields: ['name', 'oneLine', 'shape', 'approval'],
|
|
46
45
|
optional: ['approvalText'],
|
|
47
46
|
mayBeEmpty: true,
|
|
48
47
|
},
|
|
@@ -68,15 +67,6 @@ function persistenceLabel(value) {
|
|
|
68
67
|
return cell(raw);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
/** 一条能力在哪些项目里出现:`always` = 每个项目都该看见;`workspace` = 只在某些项目里有意义,
|
|
72
|
-
* 代码里必须真的用 `gate.deniedTools(workspace)` 把它收窄,否则表上写着窄、实际到处都在。 */
|
|
73
|
-
function scopeLabel(value) {
|
|
74
|
-
const raw = text(value);
|
|
75
|
-
if (raw === 'always') return '哪个项目都能用';
|
|
76
|
-
if (raw === 'workspace') return '只在某些项目里出现(按项目收窄)';
|
|
77
|
-
return cell(raw);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
70
|
function datasourceRowsOf(read) {
|
|
81
71
|
return [
|
|
82
72
|
'| 项 | 值(照抄,别转述) |',
|
|
@@ -264,16 +254,15 @@ export function renderDesign(design) {
|
|
|
264
254
|
'### 表 2 agent:他能替用户做什么、哪几件要点头',
|
|
265
255
|
'',
|
|
266
256
|
tableOf(
|
|
267
|
-
'能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么"
|
|
257
|
+
'能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么"',
|
|
268
258
|
capabilities.length === 0
|
|
269
|
-
? [['(这份模块故意没有能力)', '—', '—', '—', '—'
|
|
259
|
+
? [['(这份模块故意没有能力)', '—', '—', '—', '—']]
|
|
270
260
|
: capabilities.map((one) => [
|
|
271
261
|
cell(one?.name),
|
|
272
262
|
cell(one?.oneLine),
|
|
273
263
|
cell(one?.shape),
|
|
274
264
|
cell(one?.approval),
|
|
275
265
|
soft(one?.approvalText),
|
|
276
|
-
scopeLabel(one?.scope),
|
|
277
266
|
]),
|
|
278
267
|
),
|
|
279
268
|
'',
|
package/src/publish.mjs
CHANGED
|
@@ -69,7 +69,7 @@ export async function publish({ root }) {
|
|
|
69
69
|
if (existsSync(join(root, DESIGN_FILE))) runCli(root, ['design', 'check']);
|
|
70
70
|
else console.log('○ 没有 design.json(三张表还没落成机器形状)—— 建议先 dsh-wb-module design init');
|
|
71
71
|
} else {
|
|
72
|
-
console.log('○ 这不是一个 wb 模块包(没有 dsh.workbench.unit)——
|
|
72
|
+
console.log('○ 这不是一个 wb 模块包(没有 dsh.workbench.unit)—— 跳过判据,只查打包清单');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const packed = packList(root);
|
package/src/sync.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// `dsh-wb-module sync`:按清单生成 `src/declared-tools.ts`(单向,清单 → 代码)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它、生成物长什么样、为什么绝不回写 package.json:见 `declared-tools.mjs` 开头与
|
|
4
|
+
// ADR-0038 的 `tools[].key` 一节。
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
DECLARED_TOOLS_FILE,
|
|
10
|
+
declaredToolsApplies,
|
|
11
|
+
declaredToolsPathOf,
|
|
12
|
+
declaredToolsProblemOf,
|
|
13
|
+
declaredToolsWithoutKey,
|
|
14
|
+
renderDeclaredTools,
|
|
15
|
+
} from './declared-tools.mjs';
|
|
16
|
+
|
|
17
|
+
export async function sync({ root }) {
|
|
18
|
+
const manifestPath = join(root, 'package.json');
|
|
19
|
+
if (!existsSync(manifestPath)) throw new Error(`${manifestPath} 不在 —— sync 要在模块包目录里跑`);
|
|
20
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
21
|
+
const raw = manifest.dsh?.workbench;
|
|
22
|
+
const unit = typeof raw?.unit === 'string' ? raw.unit : manifest.name;
|
|
23
|
+
if (raw === undefined) throw new Error(`${unit}: package.json 里没有 dsh.workbench —— 这不是模块包`);
|
|
24
|
+
|
|
25
|
+
const tools = Array.isArray(raw.tools) ? raw.tools : [];
|
|
26
|
+
if (tools.length === 0) {
|
|
27
|
+
console.log(`${unit}: 清单里没有工具,不用生成物`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const missing = declaredToolsWithoutKey(raw);
|
|
32
|
+
if (missing.length > 0) {
|
|
33
|
+
const hint = declaredToolsApplies(raw)
|
|
34
|
+
? '有 key 就走生成物那一档,那就每个工具都得有一个'
|
|
35
|
+
: '给每个工具加一个 lowerCamelCase 的 key,代码里就不再写工具名的字面量';
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${unit}: 清单里 ${missing.join(' / ')} 没有 key —— ${hint}。例如:\n` +
|
|
38
|
+
`${tools.map((tool) => ` "key": "…", // ${JSON.stringify(tool.name)}`).join('\n')}\n` +
|
|
39
|
+
'(key 是代码里那个短名:`DECLARED_TOOLS.<key>.name`。见 ADR-0038 的 `tools[].key` 一节)',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const at = declaredToolsPathOf(root);
|
|
44
|
+
const had = existsSync(at);
|
|
45
|
+
const stale = declaredToolsProblemOf(root, raw);
|
|
46
|
+
writeFileSync(at, renderDeclaredTools(raw));
|
|
47
|
+
console.log(
|
|
48
|
+
`${unit}: 写了 ${DECLARED_TOOLS_FILE}(${tools.length} 个工具,按清单生成)` +
|
|
49
|
+
(had && stale !== undefined ? ' —— 原来那份和清单对不上,已刷新' : ''),
|
|
50
|
+
);
|
|
51
|
+
}
|
package/template/README.md.tpl
CHANGED
|
@@ -23,6 +23,16 @@ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍,看
|
|
|
23
23
|
dsh plugin --profile workbench add <本目录>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
要加能力:`package.json` 的 `dsh.workbench.tools[]` 里每个工具写 `name` / `writes` / `key`
|
|
27
|
+
(`key` 是代码里那个短名,lowerCamelCase),然后跑一次
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh-wb-module sync # 按清单生成 src/declared-tools.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
代码里写 `DECLARED_TOOLS.<key>.name` —— **工具名的字面量只留在清单这一处**。生成物过期、半写 `key`、
|
|
34
|
+
或源码里又写了一遍工具名字面量,`check` 都会红(`sync` 会告诉你缺哪个工具的 `key`)。
|
|
35
|
+
|
|
26
36
|
面板只读、写入只走 agent 工具;共享代码从 `@dsh-workbench/shared` 拿,
|
|
27
37
|
单例机制(hooks / 容器组件 / 读缝)只能从 `@dsh-workbench/core/client` 的 kit 拿。
|
|
28
38
|
dsh 已有的组件与样式**先从 kit 拿**,别从 DOM + CSS 重画。
|
|
@@ -24,9 +24,13 @@
|
|
|
24
24
|
"external": ["@dsh-workbench/core/client"]
|
|
25
25
|
},
|
|
26
26
|
"workbench": {
|
|
27
|
+
"apiVersion": 1,
|
|
27
28
|
"unit": "__UNIT__",
|
|
28
|
-
"
|
|
29
|
-
|
|
29
|
+
"card": { "titleKey": "__UNIT__.cardTitle", "order": 90 },
|
|
30
|
+
"tools": [],
|
|
31
|
+
"host": { "inject": [] }
|
|
32
|
+
},
|
|
33
|
+
"workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" }
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": { "@dsh-workbench/core": "^0.1.0", "react": "*" },
|
|
32
36
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
@@ -108,14 +108,12 @@ const ctx = {
|
|
|
108
108
|
|
|
109
109
|
pluginExports.apply(ctx);
|
|
110
110
|
check('卡片真的注册进了外壳', faces.length === 1, `注册了 ${faces.length} 个`);
|
|
111
|
-
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.
|
|
111
|
+
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.unit === UNIT_ID, faces[0]?.unit);
|
|
112
112
|
check('卡片是个能画的组件', typeof faces[0]?.card === 'function');
|
|
113
113
|
check(
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
typeof faces[0]?.manifest.card?.order === 'number',
|
|
118
|
-
JSON.stringify(faces[0]?.manifest.card),
|
|
114
|
+
'面只交 unit(标题与顺序住在 package.json 的 dsh.workbench 里,由外壳按 unit 交给浏览器)',
|
|
115
|
+
faces[0]?.manifest === undefined,
|
|
116
|
+
JSON.stringify(faces[0]?.manifest),
|
|
119
117
|
);
|
|
120
118
|
check(
|
|
121
119
|
'卡片在空数据下也画得出来(不许抛)',
|
|
@@ -4,23 +4,11 @@ import { messages } from './messages';
|
|
|
4
4
|
|
|
5
5
|
const UNIT_ID = '__UNIT__';
|
|
6
6
|
|
|
7
|
-
interface UnitManifest {
|
|
8
|
-
readonly id: string;
|
|
9
|
-
readonly kind: string;
|
|
10
|
-
readonly card: {
|
|
11
|
-
readonly titleKey: string;
|
|
12
|
-
readonly order: number;
|
|
13
|
-
readonly enabledByDefault: boolean;
|
|
14
|
-
} | null;
|
|
15
|
-
readonly tools: readonly unknown[];
|
|
16
|
-
readonly host: { readonly inject: readonly string[] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
7
|
type Rendered = ReturnType<typeof h>;
|
|
20
8
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
21
9
|
|
|
22
10
|
interface UnitClientFace {
|
|
23
|
-
readonly
|
|
11
|
+
readonly unit: string;
|
|
24
12
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
25
13
|
readonly card: (props: { readonly t: Translate; readonly onRefresh: () => void }) => Rendered;
|
|
26
14
|
}
|
|
@@ -34,16 +22,8 @@ interface ClientContext {
|
|
|
34
22
|
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
35
23
|
}
|
|
36
24
|
|
|
37
|
-
const manifest: UnitManifest = {
|
|
38
|
-
id: UNIT_ID,
|
|
39
|
-
kind: 'board',
|
|
40
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
41
|
-
tools: [],
|
|
42
|
-
host: { inject: [] },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
25
|
const face: UnitClientFace = {
|
|
46
|
-
|
|
26
|
+
unit: UNIT_ID,
|
|
47
27
|
messages,
|
|
48
28
|
card: (props) => {
|
|
49
29
|
const { Card } = kit.components;
|
package/template/src/host.ts.tpl
CHANGED
|
@@ -4,25 +4,13 @@ export const name = '__NAME__';
|
|
|
4
4
|
|
|
5
5
|
export const inject = ['workbenchUnits'];
|
|
6
6
|
|
|
7
|
-
interface
|
|
8
|
-
readonly
|
|
9
|
-
readonly kind: string;
|
|
10
|
-
readonly card: {
|
|
11
|
-
readonly titleKey: string;
|
|
12
|
-
readonly order: number;
|
|
13
|
-
readonly enabledByDefault: boolean;
|
|
14
|
-
} | null;
|
|
15
|
-
readonly tools: readonly unknown[];
|
|
16
|
-
readonly host: { readonly inject: readonly string[] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface UnitHostFace {
|
|
20
|
-
readonly manifest: UnitManifest;
|
|
7
|
+
interface UnitModuleFace {
|
|
8
|
+
readonly unit: string;
|
|
21
9
|
mount(): { routes: Record<string, never>; registerTools(): Array<() => void> };
|
|
22
10
|
}
|
|
23
11
|
|
|
24
12
|
interface UnitRegistry {
|
|
25
|
-
register(face:
|
|
13
|
+
register(face: UnitModuleFace): () => void;
|
|
26
14
|
}
|
|
27
15
|
|
|
28
16
|
interface HostContext {
|
|
@@ -31,16 +19,8 @@ interface HostContext {
|
|
|
31
19
|
logger?: { warn?(message: string): void } | undefined;
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
kind: 'board',
|
|
37
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
38
|
-
tools: [],
|
|
39
|
-
host: { inject: [] },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const face: UnitHostFace = {
|
|
43
|
-
manifest,
|
|
22
|
+
const face: UnitModuleFace = {
|
|
23
|
+
unit: UNIT_ID,
|
|
44
24
|
mount: () => ({ routes: {}, registerTools: () => [] }),
|
|
45
25
|
};
|
|
46
26
|
|
|
@@ -39,7 +39,7 @@ describe('__NAME__ 的面', () => {
|
|
|
39
39
|
});
|
|
40
40
|
apply(ctx as never);
|
|
41
41
|
expect(faces).toHaveLength(1);
|
|
42
|
-
expect((faces[0] as {
|
|
42
|
+
expect((faces[0] as { unit: string }).unit).toBe('__UNIT__');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('浏览器半侧把自己的卡片注册进外壳', () => {
|
|
@@ -51,5 +51,6 @@ describe('__NAME__ 的面', () => {
|
|
|
51
51
|
});
|
|
52
52
|
applyClient(ctx as never);
|
|
53
53
|
expect(faces).toHaveLength(1);
|
|
54
|
+
expect((faces[0] as { unit: string }).unit).toBe('__UNIT__');
|
|
54
55
|
});
|
|
55
56
|
});
|
|
@@ -58,7 +58,7 @@ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 调
|
|
|
58
58
|
|
|
59
59
|
| 能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么" |
|
|
60
60
|
|---|---|---|---|---|
|
|
61
|
-
| (这份骨架故意没有能力:`
|
|
61
|
+
| (这份骨架故意没有能力:`dsh.workbench.tools` 是空的。要加就照这张表填,只读的不点头、动东西的要点头) | | | | |
|
|
62
62
|
|
|
63
63
|
### 表 3 联动:agent 动完,这个模块哪一格变
|
|
64
64
|
|
|
@@ -155,8 +155,11 @@ GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
|
|
|
155
155
|
|
|
156
156
|
### 会写:清单里标出来 + 认领审批
|
|
157
157
|
|
|
158
|
+
> 每个工具都写一个 `key`(代码里那个短名,lowerCamelCase),然后 `dsh-wb-module sync` 生成
|
|
159
|
+
> `src/declared-tools.ts`:代码写 `DECLARED_TOOLS.rebuild.name` —— 工具名的字面量只留在清单里。
|
|
160
|
+
|
|
158
161
|
```ts
|
|
159
|
-
tools: [{ name: 'workbench___UNIT___rebuild', writes: true }],
|
|
162
|
+
tools: [{ name: 'workbench___UNIT___rebuild', writes: true, key: 'rebuild' }],
|
|
160
163
|
|
|
161
164
|
needsApproval: (call) => ({
|
|
162
165
|
kind: 'ask',
|
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
"external": ["@dsh-workbench/core/client"]
|
|
26
26
|
},
|
|
27
27
|
"workbench": {
|
|
28
|
+
"apiVersion": 1,
|
|
28
29
|
"unit": "__UNIT__",
|
|
29
|
-
"
|
|
30
|
-
|
|
30
|
+
"card": { "titleKey": "__UNIT__.cardTitle", "order": 90 },
|
|
31
|
+
"tools": [],
|
|
32
|
+
"host": { "inject": [] }
|
|
33
|
+
},
|
|
34
|
+
"workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" }
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": { "@dsh-workbench/core": "^0.1.0", "react": "*" },
|
|
33
37
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
@@ -115,14 +115,12 @@ const ctx = {
|
|
|
115
115
|
|
|
116
116
|
pluginExports.apply(ctx);
|
|
117
117
|
check('卡片真的注册进了外壳', faces.length === 1, `注册了 ${faces.length} 个`);
|
|
118
|
-
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.
|
|
118
|
+
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.unit === UNIT_ID, faces[0]?.unit);
|
|
119
119
|
check('卡片是个能画的组件', typeof faces[0]?.card === 'function');
|
|
120
120
|
check(
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
typeof faces[0]?.manifest.card?.order === 'number',
|
|
125
|
-
JSON.stringify(faces[0]?.manifest.card),
|
|
121
|
+
'面只交 unit(标题与顺序住在 package.json 的 dsh.workbench 里,由外壳按 unit 交给浏览器)',
|
|
122
|
+
faces[0]?.manifest === undefined,
|
|
123
|
+
JSON.stringify(faces[0]?.manifest),
|
|
126
124
|
);
|
|
127
125
|
check(
|
|
128
126
|
'卡片在空数据下也画得出来(不许抛)',
|
|
@@ -10,18 +10,6 @@ const KB = 1024;
|
|
|
10
10
|
const { Card, MockStrip, CenteredState, EmptyState, StateBlock, IconPanelScreen } = kit.components;
|
|
11
11
|
const { useApiResource, useFixturePick } = kit.hooks;
|
|
12
12
|
|
|
13
|
-
interface UnitManifest {
|
|
14
|
-
readonly id: string;
|
|
15
|
-
readonly kind: string;
|
|
16
|
-
readonly card: {
|
|
17
|
-
readonly titleKey: string;
|
|
18
|
-
readonly order: number;
|
|
19
|
-
readonly enabledByDefault: boolean;
|
|
20
|
-
} | null;
|
|
21
|
-
readonly tools: readonly unknown[];
|
|
22
|
-
readonly host: { readonly inject: readonly string[] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
13
|
type Rendered = ReturnType<typeof h>;
|
|
26
14
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
27
15
|
|
|
@@ -37,7 +25,7 @@ interface BoardProps {
|
|
|
37
25
|
}
|
|
38
26
|
|
|
39
27
|
interface UnitClientFace {
|
|
40
|
-
readonly
|
|
28
|
+
readonly unit: string;
|
|
41
29
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
42
30
|
readonly card: (props: BoardProps) => Rendered;
|
|
43
31
|
}
|
|
@@ -146,14 +134,6 @@ export function overviewStateOf(input: OverviewInput): OverviewState {
|
|
|
146
134
|
return { kind: 'loading' };
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
const manifest: UnitManifest = {
|
|
150
|
-
id: UNIT_ID,
|
|
151
|
-
kind: 'board',
|
|
152
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
153
|
-
tools: [],
|
|
154
|
-
host: { inject: [] },
|
|
155
|
-
};
|
|
156
|
-
|
|
157
137
|
function bodyOf(state: OverviewState, t: Translate): Rendered {
|
|
158
138
|
if (state.kind === 'idle') {
|
|
159
139
|
return h(CenteredState, {
|
|
@@ -207,7 +187,7 @@ function bodyOf(state: OverviewState, t: Translate): Rendered {
|
|
|
207
187
|
}
|
|
208
188
|
|
|
209
189
|
const face: UnitClientFace = {
|
|
210
|
-
|
|
190
|
+
unit: UNIT_ID,
|
|
211
191
|
messages,
|
|
212
192
|
card: (props) => {
|
|
213
193
|
const workspace = props.world.current.workspace;
|