@xihan-ui/cli 0.0.0-stage → 4.2.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/bin/xihan-ui.mjs +4 -0
- package/data/changelog.md +669 -0
- package/data/components.json +1 -0
- package/data/demos.json +1 -0
- package/data/design.md +251 -0
- package/data/meta.json +4 -0
- package/data/pages.json +1 -0
- package/data/tokens.json +1 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1540 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -3
- package/skills/xihan-ui/SKILL.md +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
//#region src/knowledge.ts
|
|
8
|
+
const FRAMEWORKS = [
|
|
9
|
+
"vue",
|
|
10
|
+
"react",
|
|
11
|
+
"web-components"
|
|
12
|
+
];
|
|
13
|
+
/** 包内 data/ 目录:src/ 与 dist/ 同在包根下一层。 */
|
|
14
|
+
function defaultDataDir() {
|
|
15
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "data");
|
|
16
|
+
}
|
|
17
|
+
function lazy(load) {
|
|
18
|
+
let cached;
|
|
19
|
+
return () => {
|
|
20
|
+
cached ??= { value: load() };
|
|
21
|
+
return cached.value;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** 从数据目录读出全部知识;大文件在首次用到时才读。 */
|
|
25
|
+
function loadKnowledge(dataDir = defaultDataDir()) {
|
|
26
|
+
const text = (file) => readFileSync(join(dataDir, file), "utf8");
|
|
27
|
+
const json = (file) => JSON.parse(text(file));
|
|
28
|
+
let meta;
|
|
29
|
+
try {
|
|
30
|
+
meta = json("meta.json");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`读不到 @xihan-ui/cli 的离线数据(${dataDir}):${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
const { categories, components } = json("components.json");
|
|
35
|
+
return {
|
|
36
|
+
meta,
|
|
37
|
+
categories,
|
|
38
|
+
components,
|
|
39
|
+
pages: lazy(() => json("pages.json")),
|
|
40
|
+
demos: lazy(() => json("demos.json")),
|
|
41
|
+
tokens: lazy(() => json("tokens.json")),
|
|
42
|
+
designMd: lazy(() => text("design.md")),
|
|
43
|
+
changelog: lazy(() => text("changelog.md"))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** kebab-case 或带 Xh 前缀的 PascalCase 名字统一成组件标识。 */
|
|
47
|
+
function normalize(name) {
|
|
48
|
+
return name.trim().replace(/^<|\/?>$/g, "").replace(/^xh-/i, "").replace(/^Xh(?=[A-Z])/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
/** 按标识、PascalCase 名、自定义元素标签、部件组件名或中文名找组件。 */
|
|
51
|
+
function findComponent(knowledge, name) {
|
|
52
|
+
const raw = name.trim();
|
|
53
|
+
if (!raw) return void 0;
|
|
54
|
+
const id = normalize(raw);
|
|
55
|
+
const exact = knowledge.components.find((c) => c.id === id || c.name === raw);
|
|
56
|
+
if (exact) return exact;
|
|
57
|
+
const exported = /^Xh[A-Z]/.test(raw) ? raw : `Xh${raw}`;
|
|
58
|
+
return knowledge.components.find((c) => c.artifacts.components.includes(exported));
|
|
59
|
+
}
|
|
60
|
+
/** 两个字符串的编辑距离。 */
|
|
61
|
+
function distance(a, b) {
|
|
62
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
63
|
+
for (let i = 1; i <= a.length; i++) {
|
|
64
|
+
let previous = row[0];
|
|
65
|
+
row[0] = i;
|
|
66
|
+
for (let j = 1; j <= b.length; j++) {
|
|
67
|
+
const current = row[j];
|
|
68
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
69
|
+
previous = current;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return row[b.length];
|
|
73
|
+
}
|
|
74
|
+
/** 找不到组件时的候选:名字互相包含或编辑距离不超过 2。 */
|
|
75
|
+
function suggestComponents(knowledge, name, limit = 5) {
|
|
76
|
+
const id = normalize(name);
|
|
77
|
+
if (!id) return [];
|
|
78
|
+
return knowledge.components.map((c) => ({
|
|
79
|
+
c,
|
|
80
|
+
score: c.id.includes(id) || id.includes(c.id) || c.name.includes(name.trim()) ? 0 : distance(c.id, id)
|
|
81
|
+
})).filter((x) => x.score <= 2).sort((a, b) => a.score - b.score || a.c.id.localeCompare(b.c.id)).slice(0, limit).map((x) => x.c);
|
|
82
|
+
}
|
|
83
|
+
/** 按站内路径找文档页:完整路径优先,其次末段同名的唯一一页。 */
|
|
84
|
+
function findPage(knowledge, path) {
|
|
85
|
+
const key = path.trim().replace(/^\/+|\/+$/g, "").replace(/\.md$/, "").replace(/\/index$/, "");
|
|
86
|
+
const pages = knowledge.pages();
|
|
87
|
+
const exact = pages.find((p) => p.path === key);
|
|
88
|
+
if (exact) return exact;
|
|
89
|
+
const tail = pages.filter((p) => p.path.split("/").pop() === key);
|
|
90
|
+
return tail.length === 1 ? tail[0] : void 0;
|
|
91
|
+
}
|
|
92
|
+
/** 把 vue / react / wc / web-components 等写法统一成框架标识。 */
|
|
93
|
+
function parseFramework(value) {
|
|
94
|
+
if (value === void 0) return void 0;
|
|
95
|
+
const v = value.trim().toLowerCase();
|
|
96
|
+
if (v === "vue") return "vue";
|
|
97
|
+
if (v === "react" || v === "tsx") return "react";
|
|
98
|
+
if (v === "wc" || v === "web-components" || v === "webcomponents" || v === "html") return "web-components";
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/query.ts
|
|
102
|
+
/** 查询失败:带上可直接展示给使用者的说明与候选。 */
|
|
103
|
+
var QueryError = class extends Error {
|
|
104
|
+
suggestions;
|
|
105
|
+
constructor(message, suggestions = []) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "QueryError";
|
|
108
|
+
this.suggestions = suggestions;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/** 找组件,找不到时抛出带候选的 QueryError。 */
|
|
112
|
+
function requireComponent(knowledge, name) {
|
|
113
|
+
const entry = findComponent(knowledge, name);
|
|
114
|
+
if (entry) return entry;
|
|
115
|
+
const near = suggestComponents(knowledge, name).map((c) => `${c.title}(${c.id})`);
|
|
116
|
+
throw new QueryError(`没有名为「${name}」的组件。用 list 查看全部组件。`, near);
|
|
117
|
+
}
|
|
118
|
+
/** 按分类列组件,可按分类标识 / 名称与关键字过滤。 */
|
|
119
|
+
function listComponents(knowledge, options = {}) {
|
|
120
|
+
const category = options.category?.trim();
|
|
121
|
+
const query = options.query?.trim().toLowerCase();
|
|
122
|
+
if (category && !knowledge.categories.some((c) => c.id === category || c.label === category)) throw new QueryError(`没有名为「${category}」的分类。`, knowledge.categories.map((c) => `${c.id}(${c.label})`));
|
|
123
|
+
return knowledge.categories.filter((c) => !category || c.id === category || c.label === category).map((c) => ({
|
|
124
|
+
...c,
|
|
125
|
+
components: knowledge.components.filter((entry) => entry.category === c.id && (!query || entry.id.includes(query) || entry.title.toLowerCase().includes(query) || entry.name.includes(query) || (entry.overview ?? "").toLowerCase().includes(query)))
|
|
126
|
+
})).filter((c) => c.components.length > 0);
|
|
127
|
+
}
|
|
128
|
+
const FENCE_LANG = {
|
|
129
|
+
"vue": "vue",
|
|
130
|
+
"react": "tsx",
|
|
131
|
+
"web-components": "html"
|
|
132
|
+
};
|
|
133
|
+
const FRAMEWORK_LABEL$1 = {
|
|
134
|
+
"vue": "Vue",
|
|
135
|
+
"react": "React",
|
|
136
|
+
"web-components": "Web Components"
|
|
137
|
+
};
|
|
138
|
+
/** 把页面里的 <XhDemo src="…" /> 换成所选写法的示例源码。 */
|
|
139
|
+
function inlineDemos(knowledge, markdown, framework) {
|
|
140
|
+
return markdown.replace(/<XhDemo\s+src="([^"]+)"\s*\/>/g, (_, src) => {
|
|
141
|
+
const source = knowledge.demos()[src]?.[framework];
|
|
142
|
+
if (source === void 0) return `> 示例 \`${src}\` 没有 ${FRAMEWORK_LABEL$1[framework]} 写法。`;
|
|
143
|
+
return `\`\`\`${FENCE_LANG[framework]}\n${source}\n\`\`\``;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** 组件或文档页的完整 Markdown,示例按所选写法内联。 */
|
|
147
|
+
function resolveDoc(knowledge, name, framework) {
|
|
148
|
+
const entry = findComponent(knowledge, name);
|
|
149
|
+
const page = entry ? findPage(knowledge, `components/${entry.id}`) : findPage(knowledge, name);
|
|
150
|
+
if (!page) {
|
|
151
|
+
const near = [...suggestComponents(knowledge, name).map((c) => c.id), ...knowledge.pages().filter((p) => p.path.includes(name.trim().toLowerCase())).map((p) => p.path)].slice(0, 8);
|
|
152
|
+
throw new QueryError(`没有名为「${name}」的组件或文档页。组件写标识(如 select),文档页写站内路径(如 guide/theme)。`, near);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
path: page.path,
|
|
156
|
+
title: page.title,
|
|
157
|
+
framework,
|
|
158
|
+
markdown: inlineDemos(knowledge, page.markdown, framework)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** 除组件页外的全部文档页,按路径排序。 */
|
|
162
|
+
function listPages(knowledge) {
|
|
163
|
+
return knowledge.pages().filter((p) => !p.path.startsWith("components/")).sort((a, b) => a.path.localeCompare(b.path));
|
|
164
|
+
}
|
|
165
|
+
/** 按序号、文件基名或标题找组件的某个示例。 */
|
|
166
|
+
function findDemo(entry, name) {
|
|
167
|
+
const key = name.trim();
|
|
168
|
+
if (/^\d+$/.test(key)) return entry.demos[Number(key) - 1];
|
|
169
|
+
return entry.demos.find((demo) => {
|
|
170
|
+
const base = demo.src.slice(demo.src.indexOf("/") + 1);
|
|
171
|
+
return base === key || base.replace(/^\d+-/, "") === key || demo.title === key;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** 一个示例在所选写法下的源码。 */
|
|
175
|
+
function demoSource(knowledge, entry, name, framework) {
|
|
176
|
+
const demo = findDemo(entry, name);
|
|
177
|
+
if (!demo) throw new QueryError(`${entry.title} 没有名为「${name}」的示例。`, entry.demos.map((d, i) => `${i + 1}. ${d.src.slice(d.src.indexOf("/") + 1)}(${d.title})`));
|
|
178
|
+
const source = knowledge.demos()[demo.src]?.[framework];
|
|
179
|
+
if (source === void 0) {
|
|
180
|
+
const available = Object.keys(knowledge.demos()[demo.src] ?? {});
|
|
181
|
+
throw new QueryError(`示例 ${demo.src} 没有 ${FRAMEWORK_LABEL$1[framework]} 写法。`, available);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
component: entry,
|
|
185
|
+
framework,
|
|
186
|
+
demo,
|
|
187
|
+
source
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** 令牌族:`--xh-<族>-…` 的第一段。 */
|
|
191
|
+
function tokenFamily(name) {
|
|
192
|
+
return name.replace(/^--xh-/, "").split("-")[0] ?? "";
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 查令牌:给组件时列它的覆盖槽,给关键字时按名字匹配全局令牌,都不给时列令牌族。
|
|
196
|
+
* 关键字可写完整名、前缀或片段,如 `bg-brand`、`--xh-shape`。
|
|
197
|
+
*/
|
|
198
|
+
function queryTokens(knowledge, options = {}) {
|
|
199
|
+
if (options.component) {
|
|
200
|
+
const entry = requireComponent(knowledge, options.component);
|
|
201
|
+
return {
|
|
202
|
+
kind: "component",
|
|
203
|
+
component: entry,
|
|
204
|
+
tokens: entry.componentTokens
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const data = knowledge.tokens();
|
|
208
|
+
const query = options.query?.trim();
|
|
209
|
+
if (!query) {
|
|
210
|
+
const counts = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const token of data.tokens) counts.set(tokenFamily(token.name), (counts.get(tokenFamily(token.name)) ?? 0) + 1);
|
|
212
|
+
return {
|
|
213
|
+
kind: "families",
|
|
214
|
+
families: [...counts].map(([family, count]) => ({
|
|
215
|
+
family,
|
|
216
|
+
count
|
|
217
|
+
})).sort((a, b) => a.family.localeCompare(b.family))
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const needle = query.replace(/^--xh-/, "").replace(/^--/, "").toLowerCase();
|
|
221
|
+
const tokens = data.tokens.filter((t) => t.name.slice(5).toLowerCase().includes(needle));
|
|
222
|
+
const tone = data.tone.filter((name) => name.slice(5).includes(needle));
|
|
223
|
+
if (tokens.length === 0 && tone.length === 0) throw new QueryError(`没有名字含「${query}」的令牌。不带关键字查询可列出全部令牌族。`);
|
|
224
|
+
return {
|
|
225
|
+
kind: "global",
|
|
226
|
+
query,
|
|
227
|
+
tokens,
|
|
228
|
+
tone
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** 组件的样式钩子:部件选择器、数据属性、状态取值与覆盖槽。 */
|
|
232
|
+
function componentParts(knowledge, name) {
|
|
233
|
+
return { component: requireComponent(knowledge, name) };
|
|
234
|
+
}
|
|
235
|
+
/** 把更新日志切成按版本的段。 */
|
|
236
|
+
function parseChangelog(markdown) {
|
|
237
|
+
const out = [];
|
|
238
|
+
const parts = markdown.split(/^## /m).slice(1);
|
|
239
|
+
for (const part of parts) {
|
|
240
|
+
const newline = part.indexOf("\n");
|
|
241
|
+
const [version = "", ...rest] = (newline === -1 ? part : part.slice(0, newline)).trim().split(/\s+/);
|
|
242
|
+
if (!version) continue;
|
|
243
|
+
out.push({
|
|
244
|
+
version: version.replace(/^v/, ""),
|
|
245
|
+
date: rest.join(" ").replace(/^\(|\)$/g, ""),
|
|
246
|
+
body: newline === -1 ? "" : part.slice(newline + 1).trim()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
/** 语义化版本比较:预发布排在同号正式版之前。 */
|
|
252
|
+
function compareVersions(a, b) {
|
|
253
|
+
const split = (v) => {
|
|
254
|
+
const [core = "", pre = ""] = v.replace(/^v/, "").split("-", 2);
|
|
255
|
+
return [core.split(".").map((n) => Number(n) || 0), pre];
|
|
256
|
+
};
|
|
257
|
+
const [ca, pa] = split(a);
|
|
258
|
+
const [cb, pb] = split(b);
|
|
259
|
+
for (let i = 0; i < Math.max(ca.length, cb.length); i++) {
|
|
260
|
+
const diff = (ca[i] ?? 0) - (cb[i] ?? 0);
|
|
261
|
+
if (diff !== 0) return diff;
|
|
262
|
+
}
|
|
263
|
+
if (pa === pb) return 0;
|
|
264
|
+
if (!pa) return 1;
|
|
265
|
+
if (!pb) return -1;
|
|
266
|
+
return pa.localeCompare(pb, "en", { numeric: true });
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 取更新日志:只给 from 时取这一版,给 from 与 to 时取 (from, to] 区间,
|
|
270
|
+
* 都不给时取最近一版;keyword 只保留含该词的条目。
|
|
271
|
+
*/
|
|
272
|
+
function queryChangelog(knowledge, options = {}) {
|
|
273
|
+
const sections = parseChangelog(knowledge.changelog());
|
|
274
|
+
if (sections.length === 0) throw new QueryError("更新日志为空。");
|
|
275
|
+
let picked;
|
|
276
|
+
if (options.from && options.to) {
|
|
277
|
+
const [low, high] = compareVersions(options.from, options.to) <= 0 ? [options.from, options.to] : [options.to, options.from];
|
|
278
|
+
picked = sections.filter((s) => compareVersions(s.version, low) > 0 && compareVersions(s.version, high) <= 0);
|
|
279
|
+
} else if (options.from) {
|
|
280
|
+
const version = options.from.replace(/^v/, "");
|
|
281
|
+
picked = sections.filter((s) => s.version === version);
|
|
282
|
+
if (picked.length === 0) throw new QueryError(`更新日志里没有 ${options.from}。`, sections.slice(0, 8).map((s) => s.version));
|
|
283
|
+
} else picked = sections.slice(0, 1);
|
|
284
|
+
const keyword = options.keyword?.trim();
|
|
285
|
+
if (!keyword) return picked;
|
|
286
|
+
const needles = keywordVariants(knowledge, keyword);
|
|
287
|
+
return picked.map((s) => ({
|
|
288
|
+
...s,
|
|
289
|
+
body: s.body.split("\n").filter((line) => /^\s*[-*]\s/.test(line) && needles.some((n) => line.toLowerCase().includes(n))).join("\n")
|
|
290
|
+
})).filter((s) => s.body);
|
|
291
|
+
}
|
|
292
|
+
/** 关键字是组件名时,连同它的 PascalCase 名与中文名一起匹配。 */
|
|
293
|
+
function keywordVariants(knowledge, keyword) {
|
|
294
|
+
const entry = findComponent(knowledge, keyword);
|
|
295
|
+
const words = entry ? [
|
|
296
|
+
entry.id,
|
|
297
|
+
entry.title,
|
|
298
|
+
entry.name,
|
|
299
|
+
keyword
|
|
300
|
+
] : [keyword];
|
|
301
|
+
return [...new Set(words.map((w) => w.toLowerCase()))];
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/render.ts
|
|
305
|
+
const code = (s) => `\`${s.replace(/`/g, "")}\``;
|
|
306
|
+
const cell = (s) => s.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
|
|
307
|
+
const codeCell = (s) => cell(code(s));
|
|
308
|
+
function table(head, rows) {
|
|
309
|
+
return [
|
|
310
|
+
`| ${head.join(" | ")} |`,
|
|
311
|
+
`| ${head.map(() => "---").join(" | ")} |`,
|
|
312
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
/** Vue / React 的根组件名:Root 优先,其次同名组件。 */
|
|
316
|
+
function rootComponent(entry) {
|
|
317
|
+
const names = entry.artifacts.components;
|
|
318
|
+
return names.find((n) => n === `Xh${entry.title}Root`) ?? names.find((n) => n === `Xh${entry.title}`) ?? names[0];
|
|
319
|
+
}
|
|
320
|
+
function renderList(knowledge, groups) {
|
|
321
|
+
const total = groups.reduce((sum, g) => sum + g.components.length, 0);
|
|
322
|
+
const L = [
|
|
323
|
+
`# XiHan.UI 组件 · v${knowledge.meta.version}`,
|
|
324
|
+
"",
|
|
325
|
+
`共 ${total} 个。查看某个组件用 info,完整文档用 doc。`,
|
|
326
|
+
""
|
|
327
|
+
];
|
|
328
|
+
for (const group of groups) {
|
|
329
|
+
L.push(`## ${group.label}(${group.id})`, "");
|
|
330
|
+
if (group.description) L.push(group.description, "");
|
|
331
|
+
L.push(...table([
|
|
332
|
+
"组件",
|
|
333
|
+
"名称",
|
|
334
|
+
"Vue / React 根组件",
|
|
335
|
+
"自定义元素",
|
|
336
|
+
"状态"
|
|
337
|
+
], group.components.map((c) => [
|
|
338
|
+
`${c.title}(${code(c.id)})`,
|
|
339
|
+
cell(c.name),
|
|
340
|
+
rootComponent(c) ? code(rootComponent(c)) : "—",
|
|
341
|
+
c.artifacts.tag ? code(`<${c.artifacts.tag}>`) : "—",
|
|
342
|
+
c.status ?? ""
|
|
343
|
+
])), "");
|
|
344
|
+
}
|
|
345
|
+
return L.join("\n");
|
|
346
|
+
}
|
|
347
|
+
function fieldTable(rows) {
|
|
348
|
+
return table([
|
|
349
|
+
"属性",
|
|
350
|
+
"类型",
|
|
351
|
+
"必填",
|
|
352
|
+
"说明"
|
|
353
|
+
], rows.map((r) => [
|
|
354
|
+
codeCell(r.name),
|
|
355
|
+
codeCell(r.type),
|
|
356
|
+
r.optional ? "" : "是",
|
|
357
|
+
cell(r.doc)
|
|
358
|
+
]));
|
|
359
|
+
}
|
|
360
|
+
function renderInfo(knowledge, entry) {
|
|
361
|
+
const a = entry.artifacts;
|
|
362
|
+
const L = [`# ${entry.title} ${entry.name}`, ""];
|
|
363
|
+
if (entry.status) L.push(`状态:${code(entry.status)}`, "");
|
|
364
|
+
if (entry.overview) L.push(entry.overview, "");
|
|
365
|
+
L.push(`分类:${entry.categoryLabel}(${entry.category})· 文档:${knowledge.meta.site}/components/${entry.id}`, "");
|
|
366
|
+
L.push("## 产物", "");
|
|
367
|
+
const artifactRows = [];
|
|
368
|
+
if (a.components.length) artifactRows.push(["Vue / React 组件", a.components.map(code).join(" ")]);
|
|
369
|
+
if (a.composable) artifactRows.push(["Vue 组合式函数", code(a.composable)]);
|
|
370
|
+
if (a.tag) artifactRows.push(["自定义元素", code(`<${a.tag}>`)]);
|
|
371
|
+
artifactRows.push(["状态机", a.machine ? code(a.machine) : "无"]);
|
|
372
|
+
if (a.skin) artifactRows.push(["皮肤", code(a.skin)]);
|
|
373
|
+
L.push(...table(["层", "值"], artifactRows), "");
|
|
374
|
+
const required = new Set(entry.requiredParts);
|
|
375
|
+
L.push("## 部件", "", `${code(`data-scope="${entry.id}"`)}:${entry.parts.map((p) => required.has(p) ? `**${code(p)}**` : code(p)).join(" · ")}(加粗为必需部件)`, "");
|
|
376
|
+
if (entry.props.length) L.push("## Props", "", ...fieldTable(entry.props), "");
|
|
377
|
+
for (const node of entry.nodes) L.push(`### ${node.name}`, "", `${code(node.via)} 的元素。`, "", ...fieldTable(node.fields), "");
|
|
378
|
+
if (entry.events.length) {
|
|
379
|
+
L.push("## 事件", "", "自定义元素的载荷在 `detail`;Vue 为同名 emit。", "");
|
|
380
|
+
L.push(...table([
|
|
381
|
+
"事件",
|
|
382
|
+
"载荷",
|
|
383
|
+
"说明"
|
|
384
|
+
], entry.events.map((e) => [
|
|
385
|
+
codeCell(e.name),
|
|
386
|
+
e.type ? codeCell(e.type) : "",
|
|
387
|
+
cell(e.description)
|
|
388
|
+
])), "");
|
|
389
|
+
}
|
|
390
|
+
if (entry.slots.length) {
|
|
391
|
+
L.push("## Vue 插槽(带载荷)", "");
|
|
392
|
+
L.push(...table([
|
|
393
|
+
"组件",
|
|
394
|
+
"插槽",
|
|
395
|
+
"载荷",
|
|
396
|
+
"说明"
|
|
397
|
+
], entry.slots.map((s) => [
|
|
398
|
+
code(s.component),
|
|
399
|
+
code(s.name),
|
|
400
|
+
s.payload ? codeCell(s.payload) : "—",
|
|
401
|
+
cell(s.doc)
|
|
402
|
+
])), "");
|
|
403
|
+
}
|
|
404
|
+
if (entry.reactProps.length) {
|
|
405
|
+
L.push("## React 适配器 props", "", "只列 React 组件自己声明的属性,DOM 属性与上面 Props 表里同名的不重复列。", "");
|
|
406
|
+
L.push(...table([
|
|
407
|
+
"组件",
|
|
408
|
+
"属性",
|
|
409
|
+
"类型",
|
|
410
|
+
"必填",
|
|
411
|
+
"说明"
|
|
412
|
+
], entry.reactProps.map((r) => [
|
|
413
|
+
code(r.component),
|
|
414
|
+
codeCell(r.name),
|
|
415
|
+
codeCell(r.type),
|
|
416
|
+
r.optional ? "" : "是",
|
|
417
|
+
cell(r.doc)
|
|
418
|
+
])), "");
|
|
419
|
+
}
|
|
420
|
+
const states = entry.dataAttributes.filter((d) => d.attr === "data-state");
|
|
421
|
+
if (states.length) L.push("## 状态", "", ...table(["部件", "data-state"], states.map((s) => [code(s.part), cell(s.value)])), "");
|
|
422
|
+
if (entry.api.length) {
|
|
423
|
+
L.push("## connect API", "", "`getXxxProps()` 返回对应部件的宿主属性,组合式函数与自定义布局用它。", "");
|
|
424
|
+
L.push(...table([
|
|
425
|
+
"成员",
|
|
426
|
+
"类型",
|
|
427
|
+
"说明"
|
|
428
|
+
], entry.api.map((x) => [
|
|
429
|
+
codeCell(x.name),
|
|
430
|
+
codeCell(x.type),
|
|
431
|
+
cell(x.doc)
|
|
432
|
+
])), "");
|
|
433
|
+
}
|
|
434
|
+
L.push("## 键盘", "");
|
|
435
|
+
if (entry.keyboard.rows.length) L.push(...table([
|
|
436
|
+
"按键",
|
|
437
|
+
"生效条件",
|
|
438
|
+
"行为"
|
|
439
|
+
], entry.keyboard.rows.map((r) => [
|
|
440
|
+
r.keys.map(code).join(" / "),
|
|
441
|
+
cell(r.when),
|
|
442
|
+
cell(r.does)
|
|
443
|
+
])), "");
|
|
444
|
+
else L.push("无键盘交互。", "");
|
|
445
|
+
for (const title of [
|
|
446
|
+
"何时使用",
|
|
447
|
+
"何时不用",
|
|
448
|
+
"反模式"
|
|
449
|
+
]) {
|
|
450
|
+
const text = entry.guidance[title];
|
|
451
|
+
if (text) L.push(`## ${title}`, "", text, "");
|
|
452
|
+
}
|
|
453
|
+
if (entry.demos.length) L.push("## 示例", "", ...entry.demos.map((d, i) => `${i + 1}. ${d.title}${d.description ? `:${d.description}` : ""}(${code(d.src)})`), "");
|
|
454
|
+
return L.join("\n");
|
|
455
|
+
}
|
|
456
|
+
function renderParts(entry) {
|
|
457
|
+
const L = [`# ${entry.title} 的样式钩子`, ""];
|
|
458
|
+
L.push("皮肤按部件选择器书写;覆盖样式写进 `@layer xihan.overrides`(无层皮肤时用不低于 `[data-scope][data-part]` 的特异性),", "颜色、间距、圆角等只取语义令牌或下面的组件覆盖槽,不写散值。", "");
|
|
459
|
+
L.push("## 部件选择器", "");
|
|
460
|
+
const required = new Set(entry.requiredParts);
|
|
461
|
+
L.push(...table([
|
|
462
|
+
"部件",
|
|
463
|
+
"选择器",
|
|
464
|
+
"必需"
|
|
465
|
+
], entry.parts.map((p) => [
|
|
466
|
+
code(p),
|
|
467
|
+
code(`[data-scope="${entry.id}"][data-part="${p}"]`),
|
|
468
|
+
required.has(p) ? "是" : ""
|
|
469
|
+
])), "");
|
|
470
|
+
if (entry.dataAttributes.length) {
|
|
471
|
+
L.push("## 数据属性", "", "由 connect 写入;条件不成立时不输出。", "");
|
|
472
|
+
L.push(...table([
|
|
473
|
+
"部件",
|
|
474
|
+
"属性",
|
|
475
|
+
"值"
|
|
476
|
+
], entry.dataAttributes.map((d) => [
|
|
477
|
+
code(d.part),
|
|
478
|
+
code(d.attr),
|
|
479
|
+
cell(d.value)
|
|
480
|
+
])), "");
|
|
481
|
+
}
|
|
482
|
+
if (entry.aria.length) {
|
|
483
|
+
L.push("## ARIA", "", "由 connect 写入,不要手写。", "");
|
|
484
|
+
L.push(...table([
|
|
485
|
+
"部件",
|
|
486
|
+
"属性",
|
|
487
|
+
"值"
|
|
488
|
+
], entry.aria.map((d) => [
|
|
489
|
+
code(d.part),
|
|
490
|
+
code(d.attr),
|
|
491
|
+
cell(d.value)
|
|
492
|
+
])), "");
|
|
493
|
+
}
|
|
494
|
+
if (entry.componentTokens.length) {
|
|
495
|
+
L.push("## 组件覆盖槽", "", "在组件根或任意祖先上设置即可覆盖,缺省值取后面的令牌。", "");
|
|
496
|
+
L.push(...table([
|
|
497
|
+
"变量",
|
|
498
|
+
"部件",
|
|
499
|
+
"属性",
|
|
500
|
+
"状态",
|
|
501
|
+
"缺省"
|
|
502
|
+
], entry.componentTokens.map((t) => [
|
|
503
|
+
code(t.name),
|
|
504
|
+
t.part.map(code).join(" "),
|
|
505
|
+
t.property.map(code).join(" "),
|
|
506
|
+
t.state.map(code).join(" "),
|
|
507
|
+
t.defaultToken.map(code).join(" ")
|
|
508
|
+
])), "");
|
|
509
|
+
} else L.push("## 组件覆盖槽", "", "本组件没有专属覆盖槽,外观由语义令牌决定。", "");
|
|
510
|
+
return L.join("\n");
|
|
511
|
+
}
|
|
512
|
+
function renderDoc(doc) {
|
|
513
|
+
return doc.markdown;
|
|
514
|
+
}
|
|
515
|
+
function renderPages(knowledge, pages) {
|
|
516
|
+
const L = [
|
|
517
|
+
`# XiHan.UI 文档页 · v${knowledge.meta.version}`,
|
|
518
|
+
"",
|
|
519
|
+
"组件页用组件标识读取(如 `select`),其余页面用下列路径读取。",
|
|
520
|
+
""
|
|
521
|
+
];
|
|
522
|
+
L.push(...table([
|
|
523
|
+
"路径",
|
|
524
|
+
"标题",
|
|
525
|
+
"摘要"
|
|
526
|
+
], pages.map((p) => [
|
|
527
|
+
code(p.path),
|
|
528
|
+
cell(p.title),
|
|
529
|
+
cell(p.summary)
|
|
530
|
+
])), "");
|
|
531
|
+
return L.join("\n");
|
|
532
|
+
}
|
|
533
|
+
const FRAMEWORK_LABEL = {
|
|
534
|
+
"vue": "Vue",
|
|
535
|
+
"react": "React",
|
|
536
|
+
"web-components": "Web Components"
|
|
537
|
+
};
|
|
538
|
+
function renderDemoList(entry) {
|
|
539
|
+
if (entry.demos.length === 0) return `# ${entry.title} 的示例\n\n本组件还没有示例。`;
|
|
540
|
+
const L = [
|
|
541
|
+
`# ${entry.title} 的示例`,
|
|
542
|
+
"",
|
|
543
|
+
"按序号或文件基名取源码,写法可选 Vue、React、Web Components。",
|
|
544
|
+
""
|
|
545
|
+
];
|
|
546
|
+
L.push(...table([
|
|
547
|
+
"#",
|
|
548
|
+
"示例",
|
|
549
|
+
"标题",
|
|
550
|
+
"说明"
|
|
551
|
+
], entry.demos.map((d, i) => [
|
|
552
|
+
String(i + 1),
|
|
553
|
+
code(d.src.slice(d.src.indexOf("/") + 1)),
|
|
554
|
+
cell(d.title),
|
|
555
|
+
cell(d.description)
|
|
556
|
+
])), "");
|
|
557
|
+
return L.join("\n");
|
|
558
|
+
}
|
|
559
|
+
function renderDemo(result) {
|
|
560
|
+
const lang = result.framework === "vue" ? "vue" : result.framework === "react" ? "tsx" : "html";
|
|
561
|
+
const L = [`# ${result.component.title} · ${result.demo.title}(${FRAMEWORK_LABEL[result.framework]})`, ""];
|
|
562
|
+
if (result.demo.description) L.push(result.demo.description, "");
|
|
563
|
+
L.push(`\`\`\`${lang}`, result.source, "```", "");
|
|
564
|
+
return L.join("\n");
|
|
565
|
+
}
|
|
566
|
+
function renderTokens(result) {
|
|
567
|
+
if (result.kind === "families") {
|
|
568
|
+
const L = [
|
|
569
|
+
"# 设计令牌族",
|
|
570
|
+
"",
|
|
571
|
+
"用关键字查具体令牌(如 `bg-brand`、`shape`、`control-h`),用 component 查某个组件的覆盖槽。",
|
|
572
|
+
""
|
|
573
|
+
];
|
|
574
|
+
L.push(...table([
|
|
575
|
+
"族",
|
|
576
|
+
"令牌数",
|
|
577
|
+
"示例前缀"
|
|
578
|
+
], result.families.map((f) => [
|
|
579
|
+
code(f.family),
|
|
580
|
+
String(f.count),
|
|
581
|
+
code(`--xh-${f.family}-*`)
|
|
582
|
+
])), "");
|
|
583
|
+
return L.join("\n");
|
|
584
|
+
}
|
|
585
|
+
if (result.kind === "component") {
|
|
586
|
+
const entry = result.component;
|
|
587
|
+
if (result.tokens.length === 0) return `# ${entry.title} 的覆盖槽\n\n本组件没有专属覆盖槽,外观由语义令牌决定。`;
|
|
588
|
+
const L = [`# ${entry.title} 的覆盖槽`, ""];
|
|
589
|
+
L.push(...table([
|
|
590
|
+
"变量",
|
|
591
|
+
"部件",
|
|
592
|
+
"属性",
|
|
593
|
+
"状态",
|
|
594
|
+
"缺省",
|
|
595
|
+
"说明"
|
|
596
|
+
], result.tokens.map((t) => [
|
|
597
|
+
code(t.name),
|
|
598
|
+
t.part.map(code).join(" "),
|
|
599
|
+
t.property.map(code).join(" "),
|
|
600
|
+
t.state.map(code).join(" "),
|
|
601
|
+
t.defaultToken.map(code).join(" "),
|
|
602
|
+
cell(t.description)
|
|
603
|
+
])), "");
|
|
604
|
+
return L.join("\n");
|
|
605
|
+
}
|
|
606
|
+
const L = [
|
|
607
|
+
`# 令牌 · ${result.query}`,
|
|
608
|
+
"",
|
|
609
|
+
"取值是缺省档(浅色 · 舒适 · 基础对比度)下的值;「改写档」是还重新给过它的令牌源(深色、紧凑、高对比度、减弱动效、打印等)。",
|
|
610
|
+
""
|
|
611
|
+
];
|
|
612
|
+
if (result.tokens.length) L.push(...table([
|
|
613
|
+
"令牌",
|
|
614
|
+
"类型",
|
|
615
|
+
"取值",
|
|
616
|
+
"展开后",
|
|
617
|
+
"改写档"
|
|
618
|
+
], result.tokens.map((t) => [
|
|
619
|
+
code(t.name),
|
|
620
|
+
t.type,
|
|
621
|
+
codeCell(t.value),
|
|
622
|
+
t.resolved === t.value ? "" : codeCell(t.resolved),
|
|
623
|
+
t.layers.join(" / ") || "—"
|
|
624
|
+
])), "");
|
|
625
|
+
if (result.tone.length) {
|
|
626
|
+
L.push("## 语气族", "", "只在写了 `data-tone` 的节点及其后代里有取值。", "");
|
|
627
|
+
L.push(...result.tone.map((name) => `- ${code(name)}`), "");
|
|
628
|
+
}
|
|
629
|
+
return L.join("\n");
|
|
630
|
+
}
|
|
631
|
+
function renderChangelog(sections) {
|
|
632
|
+
if (sections.length === 0) return "没有匹配的更新条目。";
|
|
633
|
+
return sections.map((s) => `## v${s.version}${s.date ? ` (${s.date})` : ""}\n\n${s.body}`).join("\n\n");
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/tools.ts
|
|
637
|
+
/** 参数校验失败。 */
|
|
638
|
+
var ArgumentError = class extends Error {
|
|
639
|
+
constructor(message) {
|
|
640
|
+
super(message);
|
|
641
|
+
this.name = "ArgumentError";
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function optionalString(args, key) {
|
|
645
|
+
const value = args[key];
|
|
646
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
647
|
+
if (typeof value !== "string") throw new ArgumentError(`参数 ${key} 必须是字符串`);
|
|
648
|
+
return value;
|
|
649
|
+
}
|
|
650
|
+
function requiredString(args, key) {
|
|
651
|
+
const value = optionalString(args, key);
|
|
652
|
+
if (value === void 0) throw new ArgumentError(`缺少参数 ${key}`);
|
|
653
|
+
return value;
|
|
654
|
+
}
|
|
655
|
+
function frameworkArg(args, context) {
|
|
656
|
+
const raw = optionalString(args, "framework");
|
|
657
|
+
if (raw === void 0) return context.framework;
|
|
658
|
+
const framework = parseFramework(raw);
|
|
659
|
+
if (!framework) throw new ArgumentError(`framework 只能是 ${FRAMEWORKS.join(" / ")},收到 ${raw}`);
|
|
660
|
+
return framework;
|
|
661
|
+
}
|
|
662
|
+
const frameworkProperty = {
|
|
663
|
+
type: "string",
|
|
664
|
+
enum: [...FRAMEWORKS],
|
|
665
|
+
description: "示例写法:vue、react 或 web-components。缺省取项目里装的适配器,识别不到时为 vue。"
|
|
666
|
+
};
|
|
667
|
+
const componentProperty = {
|
|
668
|
+
type: "string",
|
|
669
|
+
description: "组件名:标识(date-picker)、PascalCase(DatePicker)、导出名(XhDatePickerRoot)、标签(xh-date-picker)或中文名均可。"
|
|
670
|
+
};
|
|
671
|
+
const TOOLS = [
|
|
672
|
+
{
|
|
673
|
+
name: "xihan_ui_list",
|
|
674
|
+
title: "列出组件",
|
|
675
|
+
description: "按分类列出 XiHan.UI 的全部组件:标识、中文名、Vue / React 根组件名、自定义元素标签与状态。不确定该用哪个组件时先调它。",
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
category: {
|
|
680
|
+
type: "string",
|
|
681
|
+
description: "只列某个分类,写分类标识或中文名,如 form、数据录入。"
|
|
682
|
+
},
|
|
683
|
+
query: {
|
|
684
|
+
type: "string",
|
|
685
|
+
description: "按名称或概述里的关键字过滤。"
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
additionalProperties: false
|
|
689
|
+
},
|
|
690
|
+
run: (args, { knowledge }) => renderList(knowledge, listComponents(knowledge, {
|
|
691
|
+
category: optionalString(args, "category"),
|
|
692
|
+
query: optionalString(args, "query")
|
|
693
|
+
}))
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: "xihan_ui_info",
|
|
697
|
+
title: "组件 API",
|
|
698
|
+
description: "取一个组件的完整 API:产物(导出名、标签、皮肤)、部件、Props 与条目数据结构、事件、插槽、React 专有属性、状态、connect API 与键盘规格。写组件代码前必须先查,不要凭记忆写属性名。",
|
|
699
|
+
inputSchema: {
|
|
700
|
+
type: "object",
|
|
701
|
+
properties: { component: componentProperty },
|
|
702
|
+
required: ["component"],
|
|
703
|
+
additionalProperties: false
|
|
704
|
+
},
|
|
705
|
+
run: (args, { knowledge }) => renderInfo(knowledge, requireComponent(knowledge, requiredString(args, "component")))
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: "xihan_ui_doc",
|
|
709
|
+
title: "完整文档",
|
|
710
|
+
description: "取组件页或指南页的完整 Markdown,示例按所选写法内联为源码。组件写标识(select),指南写站内路径(guide/theme、installation、adapters/react);不传 name 时列出全部指南页。",
|
|
711
|
+
inputSchema: {
|
|
712
|
+
type: "object",
|
|
713
|
+
properties: {
|
|
714
|
+
name: {
|
|
715
|
+
type: "string",
|
|
716
|
+
description: "组件名或文档页路径。"
|
|
717
|
+
},
|
|
718
|
+
framework: frameworkProperty
|
|
719
|
+
},
|
|
720
|
+
additionalProperties: false
|
|
721
|
+
},
|
|
722
|
+
run: (args, context) => {
|
|
723
|
+
const name = optionalString(args, "name");
|
|
724
|
+
if (!name) return renderPages(context.knowledge, listPages(context.knowledge));
|
|
725
|
+
return renderDoc(resolveDoc(context.knowledge, name, frameworkArg(args, context)));
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "xihan_ui_demo",
|
|
730
|
+
title: "示例源码",
|
|
731
|
+
description: "取组件的可运行示例。不传 name 时列出该组件的全部示例;传序号、文件基名或标题时返回所选写法的完整源码。",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: {
|
|
735
|
+
component: componentProperty,
|
|
736
|
+
name: {
|
|
737
|
+
type: "string",
|
|
738
|
+
description: "示例序号(1 起)、文件基名(01-basic 或 basic)或标题。"
|
|
739
|
+
},
|
|
740
|
+
framework: frameworkProperty
|
|
741
|
+
},
|
|
742
|
+
required: ["component"],
|
|
743
|
+
additionalProperties: false
|
|
744
|
+
},
|
|
745
|
+
run: (args, context) => {
|
|
746
|
+
const entry = requireComponent(context.knowledge, requiredString(args, "component"));
|
|
747
|
+
const name = optionalString(args, "name");
|
|
748
|
+
if (!name) return renderDemoList(entry);
|
|
749
|
+
return renderDemo(demoSource(context.knowledge, entry, name, frameworkArg(args, context)));
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "xihan_ui_token",
|
|
754
|
+
title: "设计令牌",
|
|
755
|
+
description: "查设计令牌。传 query 按名字片段查全局令牌(如 bg-brand、shape、control-h、motion-duration)及其缺省取值;传 component 列该组件的覆盖槽;都不传时列出令牌族。写样式时只用这里查得到的令牌,不写散值。",
|
|
756
|
+
inputSchema: {
|
|
757
|
+
type: "object",
|
|
758
|
+
properties: {
|
|
759
|
+
query: {
|
|
760
|
+
type: "string",
|
|
761
|
+
description: "令牌名或名字片段,可带 --xh- 前缀。"
|
|
762
|
+
},
|
|
763
|
+
component: componentProperty
|
|
764
|
+
},
|
|
765
|
+
additionalProperties: false
|
|
766
|
+
},
|
|
767
|
+
run: (args, { knowledge }) => renderTokens(queryTokens(knowledge, {
|
|
768
|
+
query: optionalString(args, "query"),
|
|
769
|
+
component: optionalString(args, "component")
|
|
770
|
+
}))
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "xihan_ui_parts",
|
|
774
|
+
title: "样式钩子",
|
|
775
|
+
description: "取组件的样式钩子:每个部件的 [data-scope][data-part] 选择器、connect 写入的数据属性与状态取值、ARIA、组件覆盖槽及其缺省令牌。改组件外观前先查它。",
|
|
776
|
+
inputSchema: {
|
|
777
|
+
type: "object",
|
|
778
|
+
properties: { component: componentProperty },
|
|
779
|
+
required: ["component"],
|
|
780
|
+
additionalProperties: false
|
|
781
|
+
},
|
|
782
|
+
run: (args, { knowledge }) => renderParts(componentParts(knowledge, requiredString(args, "component")).component)
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: "xihan_ui_design_md",
|
|
786
|
+
title: "设计语言",
|
|
787
|
+
description: "取 XiHan.UI 的 design.md:缺省主题的颜色、排版、间距、圆角与组件样式令牌,以及视觉规则与禁忌。生成整页界面或判断视觉取舍时读它。",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {},
|
|
791
|
+
additionalProperties: false
|
|
792
|
+
},
|
|
793
|
+
run: (_, { knowledge }) => knowledge.designMd()
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "xihan_ui_changelog",
|
|
797
|
+
title: "更新日志",
|
|
798
|
+
description: "查更新日志。只传 from 取这一版;传 from 与 to 取两版之间(不含 from、含 to)的全部变更,用于升级迁移;keyword 只保留提到它的条目(可写组件名)。都不传时取最近一版。",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: {
|
|
802
|
+
from: {
|
|
803
|
+
type: "string",
|
|
804
|
+
description: "版本号,如 4.0.0。"
|
|
805
|
+
},
|
|
806
|
+
to: {
|
|
807
|
+
type: "string",
|
|
808
|
+
description: "版本号,如 4.1.0。"
|
|
809
|
+
},
|
|
810
|
+
keyword: {
|
|
811
|
+
type: "string",
|
|
812
|
+
description: "关键字或组件名。"
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
additionalProperties: false
|
|
816
|
+
},
|
|
817
|
+
run: (args, { knowledge }) => renderChangelog(queryChangelog(knowledge, {
|
|
818
|
+
from: optionalString(args, "from"),
|
|
819
|
+
to: optionalString(args, "to"),
|
|
820
|
+
keyword: optionalString(args, "keyword")
|
|
821
|
+
}))
|
|
822
|
+
}
|
|
823
|
+
];
|
|
824
|
+
/** 包内 skills/xihan-ui/SKILL.md 的正文,去掉 frontmatter。 */
|
|
825
|
+
function skillBody() {
|
|
826
|
+
return readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "skills", "xihan-ui", "SKILL.md"), "utf8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim();
|
|
827
|
+
}
|
|
828
|
+
const PROMPTS = [{
|
|
829
|
+
name: "xihan-ui-expert",
|
|
830
|
+
title: "XiHan.UI 专家",
|
|
831
|
+
description: "以 XiHan.UI 专家身份工作:先查 API 再写代码,样式只走部件选择器与令牌。",
|
|
832
|
+
arguments: [],
|
|
833
|
+
render: (_, { framework }) => [
|
|
834
|
+
`你是 XiHan.UI 的专家。当前项目的写法是 ${framework}。以下是使用规范,回答与写代码都按它执行;`,
|
|
835
|
+
"涉及具体组件时先调 xihan_ui_info / xihan_ui_doc / xihan_ui_demo 查证,不要凭记忆写组件名、属性或部件。",
|
|
836
|
+
"",
|
|
837
|
+
skillBody()
|
|
838
|
+
].join("\n")
|
|
839
|
+
}, {
|
|
840
|
+
name: "xihan-ui-page-generator",
|
|
841
|
+
title: "用 XiHan.UI 搭页面",
|
|
842
|
+
description: "按描述用 XiHan.UI 组件搭一整页界面。",
|
|
843
|
+
arguments: [{
|
|
844
|
+
name: "description",
|
|
845
|
+
description: "页面要做什么:用途、区块、数据与交互。",
|
|
846
|
+
required: true
|
|
847
|
+
}, {
|
|
848
|
+
name: "framework",
|
|
849
|
+
description: "写法:vue、react 或 web-components,缺省取项目里的适配器。",
|
|
850
|
+
required: false
|
|
851
|
+
}],
|
|
852
|
+
render: (args, context) => {
|
|
853
|
+
return [
|
|
854
|
+
`用 XiHan.UI 的 ${parseFramework(args.framework) ?? context.framework} 写法实现下面这个页面:`,
|
|
855
|
+
"",
|
|
856
|
+
args.description ?? "",
|
|
857
|
+
"",
|
|
858
|
+
"步骤:",
|
|
859
|
+
"1. 调 xihan_ui_design_md 了解视觉规则,调 xihan_ui_list 选定要用的组件;",
|
|
860
|
+
"2. 对每个选定的组件调 xihan_ui_info 核对导出名、部件与 Props,需要时调 xihan_ui_demo 参考示例;",
|
|
861
|
+
"3. 页面布局用 Layout / Flex / Grid 等布局组件与间距令牌,颜色、间距、圆角只取 xihan_ui_token 查得到的令牌;",
|
|
862
|
+
"4. 交付完整可运行的代码,并列出用到的组件与需要额外安装的包。"
|
|
863
|
+
].join("\n");
|
|
864
|
+
}
|
|
865
|
+
}];
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/mcp.ts
|
|
868
|
+
/** 按新到旧排列的受支持协议版本。 */
|
|
869
|
+
const PROTOCOL_VERSIONS = [
|
|
870
|
+
"2025-11-25",
|
|
871
|
+
"2025-06-18",
|
|
872
|
+
"2025-03-26",
|
|
873
|
+
"2024-11-05"
|
|
874
|
+
];
|
|
875
|
+
const PARSE_ERROR = -32700;
|
|
876
|
+
const INVALID_REQUEST = -32600;
|
|
877
|
+
const METHOD_NOT_FOUND = -32601;
|
|
878
|
+
const INVALID_PARAMS = -32602;
|
|
879
|
+
var RpcError = class extends Error {
|
|
880
|
+
code;
|
|
881
|
+
constructor(code, message) {
|
|
882
|
+
super(message);
|
|
883
|
+
this.code = code;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
function asRecord(value) {
|
|
887
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
888
|
+
}
|
|
889
|
+
/** 处理一条请求;通知(不带 id)返回 undefined。 */
|
|
890
|
+
function handleMessage(message, options) {
|
|
891
|
+
const record = asRecord(message);
|
|
892
|
+
const id = record.id;
|
|
893
|
+
const validId = typeof id === "string" || typeof id === "number";
|
|
894
|
+
if (record.jsonrpc !== "2.0" || typeof record.method !== "string") return {
|
|
895
|
+
jsonrpc: "2.0",
|
|
896
|
+
id: validId ? id : null,
|
|
897
|
+
error: {
|
|
898
|
+
code: INVALID_REQUEST,
|
|
899
|
+
message: "不是合法的 JSON-RPC 2.0 请求"
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
const request = record;
|
|
903
|
+
const isNotification = !("id" in record);
|
|
904
|
+
try {
|
|
905
|
+
const result = dispatch(request, options);
|
|
906
|
+
if (isNotification) return void 0;
|
|
907
|
+
return {
|
|
908
|
+
jsonrpc: "2.0",
|
|
909
|
+
id: validId ? id : null,
|
|
910
|
+
result: result ?? {}
|
|
911
|
+
};
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (isNotification) return void 0;
|
|
914
|
+
const code = error instanceof RpcError ? error.code : -32603;
|
|
915
|
+
return {
|
|
916
|
+
jsonrpc: "2.0",
|
|
917
|
+
id: validId ? id : null,
|
|
918
|
+
error: {
|
|
919
|
+
code,
|
|
920
|
+
message: error.message
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function dispatch(request, options) {
|
|
926
|
+
const params = asRecord(request.params);
|
|
927
|
+
switch (request.method) {
|
|
928
|
+
case "initialize": {
|
|
929
|
+
const requested = params.protocolVersion;
|
|
930
|
+
return {
|
|
931
|
+
protocolVersion: PROTOCOL_VERSIONS.find((v) => v === requested) ?? PROTOCOL_VERSIONS[0],
|
|
932
|
+
capabilities: {
|
|
933
|
+
tools: { listChanged: false },
|
|
934
|
+
prompts: { listChanged: false }
|
|
935
|
+
},
|
|
936
|
+
serverInfo: {
|
|
937
|
+
name: "xihan-ui",
|
|
938
|
+
title: "XiHan.UI",
|
|
939
|
+
version: options.version
|
|
940
|
+
},
|
|
941
|
+
instructions: options.instructions
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
case "notifications/initialized":
|
|
945
|
+
case "notifications/cancelled": return;
|
|
946
|
+
case "ping": return {};
|
|
947
|
+
case "tools/list": return { tools: TOOLS.map((tool) => ({
|
|
948
|
+
name: tool.name,
|
|
949
|
+
title: tool.title,
|
|
950
|
+
description: tool.description,
|
|
951
|
+
inputSchema: tool.inputSchema,
|
|
952
|
+
annotations: {
|
|
953
|
+
readOnlyHint: true,
|
|
954
|
+
openWorldHint: false
|
|
955
|
+
}
|
|
956
|
+
})) };
|
|
957
|
+
case "tools/call": return callTool(params, options.context);
|
|
958
|
+
case "prompts/list": return { prompts: PROMPTS.map((prompt) => ({
|
|
959
|
+
name: prompt.name,
|
|
960
|
+
title: prompt.title,
|
|
961
|
+
description: prompt.description,
|
|
962
|
+
arguments: prompt.arguments
|
|
963
|
+
})) };
|
|
964
|
+
case "prompts/get": return getPrompt(params, options.context);
|
|
965
|
+
default: throw new RpcError(METHOD_NOT_FOUND, `不支持的方法:${request.method}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function callTool(params, context) {
|
|
969
|
+
const tool = TOOLS.find((t) => t.name === params.name);
|
|
970
|
+
if (!tool) throw new RpcError(INVALID_PARAMS, `没有名为 ${String(params.name)} 的工具`);
|
|
971
|
+
try {
|
|
972
|
+
return { content: [{
|
|
973
|
+
type: "text",
|
|
974
|
+
text: tool.run(asRecord(params.arguments), context)
|
|
975
|
+
}] };
|
|
976
|
+
} catch (error) {
|
|
977
|
+
if (error instanceof QueryError || error instanceof ArgumentError) {
|
|
978
|
+
const near = error instanceof QueryError && error.suggestions.length ? `\n\n候选:\n${error.suggestions.map((s) => `- ${s}`).join("\n")}` : "";
|
|
979
|
+
return {
|
|
980
|
+
content: [{
|
|
981
|
+
type: "text",
|
|
982
|
+
text: `${error.message}${near}`
|
|
983
|
+
}],
|
|
984
|
+
isError: true
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function getPrompt(params, context) {
|
|
991
|
+
const prompt = PROMPTS.find((p) => p.name === params.name);
|
|
992
|
+
if (!prompt) throw new RpcError(INVALID_PARAMS, `没有名为 ${String(params.name)} 的提示词`);
|
|
993
|
+
const raw = asRecord(params.arguments);
|
|
994
|
+
const args = {};
|
|
995
|
+
for (const [key, value] of Object.entries(raw)) if (typeof value === "string") args[key] = value;
|
|
996
|
+
const missing = prompt.arguments.filter((a) => a.required && !args[a.name]);
|
|
997
|
+
if (missing.length) throw new RpcError(INVALID_PARAMS, `缺少参数:${missing.map((a) => a.name).join("、")}`);
|
|
998
|
+
return {
|
|
999
|
+
description: prompt.description,
|
|
1000
|
+
messages: [{
|
|
1001
|
+
role: "user",
|
|
1002
|
+
content: {
|
|
1003
|
+
type: "text",
|
|
1004
|
+
text: prompt.render(args, context)
|
|
1005
|
+
}
|
|
1006
|
+
}]
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
/** 处理一行输入:单条请求或批量数组,返回要写出的那一行(没有响应时为 undefined)。 */
|
|
1010
|
+
function handleLine(line, options) {
|
|
1011
|
+
let message;
|
|
1012
|
+
try {
|
|
1013
|
+
message = JSON.parse(line);
|
|
1014
|
+
} catch {
|
|
1015
|
+
return JSON.stringify({
|
|
1016
|
+
jsonrpc: "2.0",
|
|
1017
|
+
id: null,
|
|
1018
|
+
error: {
|
|
1019
|
+
code: PARSE_ERROR,
|
|
1020
|
+
message: "无法解析的 JSON"
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (Array.isArray(message)) {
|
|
1025
|
+
const responses = message.map((m) => handleMessage(m, options)).filter((r) => r !== void 0);
|
|
1026
|
+
return responses.length ? JSON.stringify(responses) : void 0;
|
|
1027
|
+
}
|
|
1028
|
+
const response = handleMessage(message, options);
|
|
1029
|
+
return response ? JSON.stringify(response) : void 0;
|
|
1030
|
+
}
|
|
1031
|
+
/** 在给定的输入输出流上运行服务端,输入结束时返回。 */
|
|
1032
|
+
function serveStdio(options, input = process.stdin, output = process.stdout) {
|
|
1033
|
+
const reader = createInterface({
|
|
1034
|
+
input,
|
|
1035
|
+
crlfDelay: Infinity
|
|
1036
|
+
});
|
|
1037
|
+
reader.on("line", (line) => {
|
|
1038
|
+
if (!line.trim()) return;
|
|
1039
|
+
const reply = handleLine(line, options);
|
|
1040
|
+
if (reply !== void 0) output.write(`${reply}\n`);
|
|
1041
|
+
});
|
|
1042
|
+
return new Promise((resolve) => reader.once("close", () => resolve()));
|
|
1043
|
+
}
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/project.ts
|
|
1046
|
+
const ADAPTER_PACKAGES = [
|
|
1047
|
+
["vue", "@xihan-ui/vue"],
|
|
1048
|
+
["react", "@xihan-ui/react"],
|
|
1049
|
+
["web-components", "@xihan-ui/web-components"]
|
|
1050
|
+
];
|
|
1051
|
+
function readJson(path) {
|
|
1052
|
+
try {
|
|
1053
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
1054
|
+
} catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/** 向上找第一个含 .git 的目录。 */
|
|
1059
|
+
function gitRoot(start) {
|
|
1060
|
+
let dir = start;
|
|
1061
|
+
for (;;) {
|
|
1062
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
1063
|
+
const parent = dirname(dir);
|
|
1064
|
+
if (parent === dir) return void 0;
|
|
1065
|
+
dir = parent;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/** 向上找最近的 package.json 所在目录,不越过 stop。 */
|
|
1069
|
+
function nearestPackageDir(start, stop) {
|
|
1070
|
+
let dir = start;
|
|
1071
|
+
for (;;) {
|
|
1072
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
1073
|
+
const parent = dirname(dir);
|
|
1074
|
+
if (parent === dir || dir === stop) return void 0;
|
|
1075
|
+
dir = parent;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function declaredDependencies(dir) {
|
|
1079
|
+
const manifest = readJson(join(dir, "package.json")) ?? {};
|
|
1080
|
+
return {
|
|
1081
|
+
...manifest.dependencies,
|
|
1082
|
+
...manifest.devDependencies
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function declaresXihanUi(dir) {
|
|
1086
|
+
return Object.keys(declaredDependencies(dir)).some((name) => name.startsWith("@xihan-ui/"));
|
|
1087
|
+
}
|
|
1088
|
+
/** 仓库根的一级子目录里第一个依赖 XiHan.UI 的,如前后端分仓结构里的 frontend/。 */
|
|
1089
|
+
function xihanUiChild(repo) {
|
|
1090
|
+
return readdirSync(repo, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort().map((name) => join(repo, name)).find((dir) => existsSync(join(dir, "package.json")) && declaresXihanUi(dir));
|
|
1091
|
+
}
|
|
1092
|
+
/** 从 start 向上逐级找 node_modules/<name>/package.json 里的版本。 */
|
|
1093
|
+
function installedVersionOf(start, name) {
|
|
1094
|
+
let dir = start;
|
|
1095
|
+
for (;;) {
|
|
1096
|
+
const manifest = readJson(join(dir, "node_modules", name, "package.json"));
|
|
1097
|
+
if (typeof manifest?.version === "string") return manifest.version;
|
|
1098
|
+
const parent = dirname(dir);
|
|
1099
|
+
if (parent === dir) return void 0;
|
|
1100
|
+
dir = parent;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function detectProject(cwd = process.cwd()) {
|
|
1104
|
+
const repo = gitRoot(cwd);
|
|
1105
|
+
let packageDir = nearestPackageDir(cwd, repo);
|
|
1106
|
+
if (repo && (!packageDir || !declaresXihanUi(packageDir))) packageDir = xihanUiChild(repo) ?? packageDir;
|
|
1107
|
+
const root = repo ?? packageDir;
|
|
1108
|
+
if (!packageDir) return {
|
|
1109
|
+
root,
|
|
1110
|
+
packageDir: void 0,
|
|
1111
|
+
framework: void 0,
|
|
1112
|
+
installedVersion: void 0
|
|
1113
|
+
};
|
|
1114
|
+
const declared = declaredDependencies(packageDir);
|
|
1115
|
+
const adapter = ADAPTER_PACKAGES.find(([, name]) => name in declared);
|
|
1116
|
+
const probe = adapter?.[1] ?? "@xihan-ui/core";
|
|
1117
|
+
return {
|
|
1118
|
+
root,
|
|
1119
|
+
packageDir,
|
|
1120
|
+
framework: adapter?.[0],
|
|
1121
|
+
installedVersion: installedVersionOf(packageDir, probe)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
/** 项目装的版本与 CLI 数据版本不一致时的提示;一致或未安装时返回 undefined。 */
|
|
1125
|
+
function versionNotice(project, dataVersion) {
|
|
1126
|
+
if (!project.installedVersion || project.installedVersion === dataVersion) return void 0;
|
|
1127
|
+
return `当前项目装的是 XiHan.UI ${project.installedVersion},这份 CLI 的数据对应 ${dataVersion}。要按项目版本查询,改用 npx -y @xihan-ui/cli@${project.installedVersion}。`;
|
|
1128
|
+
}
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/setup.ts
|
|
1131
|
+
const CLIENTS = [
|
|
1132
|
+
"claude",
|
|
1133
|
+
"cursor",
|
|
1134
|
+
"vscode",
|
|
1135
|
+
"codex"
|
|
1136
|
+
];
|
|
1137
|
+
const SETUP_MODES = [
|
|
1138
|
+
"mcp",
|
|
1139
|
+
"skill",
|
|
1140
|
+
"both"
|
|
1141
|
+
];
|
|
1142
|
+
/** MCP 配置里的服务名。 */
|
|
1143
|
+
const SERVER_NAME = "xihan-ui";
|
|
1144
|
+
/** 启动服务端的命令;Windows 上 Claude Code 需要经 cmd /c 调 npx。 */
|
|
1145
|
+
function serverCommand(client, version, platform) {
|
|
1146
|
+
const args = [
|
|
1147
|
+
"-y",
|
|
1148
|
+
`@xihan-ui/cli@${version}`,
|
|
1149
|
+
"mcp"
|
|
1150
|
+
];
|
|
1151
|
+
if (client === "claude" && platform === "win32") return {
|
|
1152
|
+
command: "cmd",
|
|
1153
|
+
args: [
|
|
1154
|
+
"/c",
|
|
1155
|
+
"npx",
|
|
1156
|
+
...args
|
|
1157
|
+
]
|
|
1158
|
+
};
|
|
1159
|
+
return {
|
|
1160
|
+
command: "npx",
|
|
1161
|
+
args
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function skillSource() {
|
|
1165
|
+
return readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "skills", "xihan-ui", "SKILL.md"), "utf8");
|
|
1166
|
+
}
|
|
1167
|
+
/** 按客户端与模式算出要做的事,不碰磁盘。 */
|
|
1168
|
+
function planSetup(options) {
|
|
1169
|
+
const actions = [];
|
|
1170
|
+
const { command, args } = serverCommand(options.client, options.version, options.platform);
|
|
1171
|
+
if (options.mode !== "skill") switch (options.client) {
|
|
1172
|
+
case "claude":
|
|
1173
|
+
actions.push({
|
|
1174
|
+
kind: "json",
|
|
1175
|
+
path: join(options.root, ".mcp.json"),
|
|
1176
|
+
key: "mcpServers",
|
|
1177
|
+
entry: {
|
|
1178
|
+
command,
|
|
1179
|
+
args
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
break;
|
|
1183
|
+
case "cursor":
|
|
1184
|
+
actions.push({
|
|
1185
|
+
kind: "json",
|
|
1186
|
+
path: join(options.root, ".cursor", "mcp.json"),
|
|
1187
|
+
key: "mcpServers",
|
|
1188
|
+
entry: {
|
|
1189
|
+
command,
|
|
1190
|
+
args
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
break;
|
|
1194
|
+
case "vscode":
|
|
1195
|
+
actions.push({
|
|
1196
|
+
kind: "json",
|
|
1197
|
+
path: join(options.root, ".vscode", "mcp.json"),
|
|
1198
|
+
key: "servers",
|
|
1199
|
+
entry: {
|
|
1200
|
+
type: "stdio",
|
|
1201
|
+
command,
|
|
1202
|
+
args
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
break;
|
|
1206
|
+
case "codex":
|
|
1207
|
+
actions.push({
|
|
1208
|
+
kind: "print",
|
|
1209
|
+
target: "~/.codex/config.toml",
|
|
1210
|
+
content: [
|
|
1211
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
1212
|
+
`command = "${command}"`,
|
|
1213
|
+
`args = [${args.map((a) => `"${a}"`).join(", ")}]`
|
|
1214
|
+
].join("\n")
|
|
1215
|
+
});
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
if (options.mode !== "mcp") {
|
|
1219
|
+
const dir = options.client === "claude" ? join(".claude", "skills") : join(".agents", "skills");
|
|
1220
|
+
actions.push({
|
|
1221
|
+
kind: "file",
|
|
1222
|
+
path: join(options.root, dir, SERVER_NAME, "SKILL.md"),
|
|
1223
|
+
content: skillSource()
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
return actions;
|
|
1227
|
+
}
|
|
1228
|
+
function readConfig(path) {
|
|
1229
|
+
if (!existsSync(path)) return {};
|
|
1230
|
+
const text = readFileSync(path, "utf8");
|
|
1231
|
+
if (!text.trim()) return {};
|
|
1232
|
+
try {
|
|
1233
|
+
const value = JSON.parse(text);
|
|
1234
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("顶层不是对象");
|
|
1235
|
+
return value;
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
throw new Error(`无法解析 ${path}(${error.message}),文件未改动;请按 --dry-run 打印的片段手动合并`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/** 一条动作的说明,--dry-run 与执行后都打印它。 */
|
|
1241
|
+
function describeAction(action) {
|
|
1242
|
+
if (action.kind === "json") return `在 ${action.path} 的 ${action.key} 里写入 ${SERVER_NAME}:\n${JSON.stringify({ [action.key]: { [SERVER_NAME]: action.entry } }, null, 2)}`;
|
|
1243
|
+
if (action.kind === "file") return `写入 ${action.path}`;
|
|
1244
|
+
return `把下面这段加进 ${action.target}:\n${action.content}`;
|
|
1245
|
+
}
|
|
1246
|
+
/** 执行动作:JSON 配置只改 xihan-ui 一项,其余内容原样保留。 */
|
|
1247
|
+
function applyAction(action) {
|
|
1248
|
+
if (action.kind === "print") return;
|
|
1249
|
+
mkdirSync(dirname(action.path), { recursive: true });
|
|
1250
|
+
if (action.kind === "file") {
|
|
1251
|
+
writeFileSync(action.path, action.content);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const config = readConfig(action.path);
|
|
1255
|
+
const servers = config[action.key] !== null && typeof config[action.key] === "object" ? config[action.key] : {};
|
|
1256
|
+
config[action.key] = {
|
|
1257
|
+
...servers,
|
|
1258
|
+
[SERVER_NAME]: action.entry
|
|
1259
|
+
};
|
|
1260
|
+
writeFileSync(action.path, `${JSON.stringify(config, null, 2)}\n`);
|
|
1261
|
+
}
|
|
1262
|
+
/** 核对动作是否已经生效;返回未生效的说明。 */
|
|
1263
|
+
function checkAction(action) {
|
|
1264
|
+
if (action.kind === "print") return void 0;
|
|
1265
|
+
if (!existsSync(action.path)) return `缺少 ${action.path}`;
|
|
1266
|
+
if (action.kind === "file") return readFileSync(action.path, "utf8") === action.content ? void 0 : `${action.path} 与当前版本的技能不一致`;
|
|
1267
|
+
const current = readConfig(action.path)[action.key]?.[SERVER_NAME];
|
|
1268
|
+
if (!current) return `${action.path} 里没有 ${SERVER_NAME}`;
|
|
1269
|
+
return JSON.stringify(current) === JSON.stringify(action.entry) ? void 0 : `${action.path} 里的 ${SERVER_NAME} 与期望不一致`;
|
|
1270
|
+
}
|
|
1271
|
+
//#endregion
|
|
1272
|
+
//#region src/cli.ts
|
|
1273
|
+
const HELP = `xihan-ui —— XiHan.UI 的离线知识库与 MCP 服务端
|
|
1274
|
+
|
|
1275
|
+
用法:xihan-ui <命令> [参数] [选项]
|
|
1276
|
+
|
|
1277
|
+
查询
|
|
1278
|
+
list [--category <分类>] [--query <关键字>] 按分类列出组件
|
|
1279
|
+
info <组件> 组件 API:产物、部件、Props、事件、插槽、状态、键盘
|
|
1280
|
+
doc [<组件|页面>] 完整文档,示例按写法内联;不带参数列出指南页
|
|
1281
|
+
demo <组件> [<示例>] 列出示例或输出示例源码
|
|
1282
|
+
token [<关键字>] [--component <组件>] 设计令牌与组件覆盖槽
|
|
1283
|
+
parts <组件> 部件选择器、数据属性与覆盖槽
|
|
1284
|
+
design.md 设计语言文档
|
|
1285
|
+
changelog [<版本> [<版本>]] [--keyword <词>] 更新日志,两个版本时取区间
|
|
1286
|
+
|
|
1287
|
+
集成
|
|
1288
|
+
mcp 以 stdio 启动 MCP 服务端
|
|
1289
|
+
setup --client <${CLIENTS.join("|")}> 写入 MCP 配置与技能
|
|
1290
|
+
[--mode <${SETUP_MODES.join("|")}>] [--pin <版本>] [--dry-run] [--check]
|
|
1291
|
+
|
|
1292
|
+
选项
|
|
1293
|
+
--framework <vue|react|web-components> 示例写法,缺省取项目里装的适配器
|
|
1294
|
+
--format <markdown|json> 输出格式,缺省 markdown
|
|
1295
|
+
-v, --version 打印版本
|
|
1296
|
+
-h, --help 打印本帮助
|
|
1297
|
+
|
|
1298
|
+
组件名可写标识(date-picker)、PascalCase(DatePicker)、导出名(XhDatePickerRoot)、标签(xh-date-picker)或中文名。`;
|
|
1299
|
+
var UsageError = class extends Error {};
|
|
1300
|
+
const defaultEnvironment = {
|
|
1301
|
+
stdout: (text) => process.stdout.write(`${text}\n`),
|
|
1302
|
+
stderr: (text) => process.stderr.write(`${text}\n`)
|
|
1303
|
+
};
|
|
1304
|
+
function requireArg(value, usage) {
|
|
1305
|
+
if (!value) throw new UsageError(`缺少参数。用法:xihan-ui ${usage}`);
|
|
1306
|
+
return value;
|
|
1307
|
+
}
|
|
1308
|
+
function pickFramework(value, fallback) {
|
|
1309
|
+
if (value === void 0) return fallback;
|
|
1310
|
+
const framework = parseFramework(value);
|
|
1311
|
+
if (!framework) throw new UsageError(`--framework 只能是 ${FRAMEWORKS.join(" / ")},收到 ${value}`);
|
|
1312
|
+
return framework;
|
|
1313
|
+
}
|
|
1314
|
+
function query(command, positionals, values, knowledge, framework) {
|
|
1315
|
+
const str = (key) => typeof values[key] === "string" ? values[key] : void 0;
|
|
1316
|
+
switch (command) {
|
|
1317
|
+
case "list": {
|
|
1318
|
+
const groups = listComponents(knowledge, {
|
|
1319
|
+
category: str("category"),
|
|
1320
|
+
query: str("query")
|
|
1321
|
+
});
|
|
1322
|
+
return {
|
|
1323
|
+
markdown: renderList(knowledge, groups),
|
|
1324
|
+
json: groups
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
case "info": {
|
|
1328
|
+
const entry = requireComponent(knowledge, requireArg(positionals[0], "info <组件>"));
|
|
1329
|
+
return {
|
|
1330
|
+
markdown: renderInfo(knowledge, entry),
|
|
1331
|
+
json: entry
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
case "doc": {
|
|
1335
|
+
if (!positionals[0]) {
|
|
1336
|
+
const pages = listPages(knowledge).map(({ markdown: _, ...page }) => page);
|
|
1337
|
+
return {
|
|
1338
|
+
markdown: renderPages(knowledge, listPages(knowledge)),
|
|
1339
|
+
json: pages
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const doc = resolveDoc(knowledge, positionals[0], framework);
|
|
1343
|
+
return {
|
|
1344
|
+
markdown: renderDoc(doc),
|
|
1345
|
+
json: doc
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
case "demo": {
|
|
1349
|
+
const entry = requireComponent(knowledge, requireArg(positionals[0], "demo <组件> [<示例>]"));
|
|
1350
|
+
if (!positionals[1]) return {
|
|
1351
|
+
markdown: renderDemoList(entry),
|
|
1352
|
+
json: entry.demos
|
|
1353
|
+
};
|
|
1354
|
+
const demo = demoSource(knowledge, entry, positionals[1], framework);
|
|
1355
|
+
return {
|
|
1356
|
+
markdown: renderDemo(demo),
|
|
1357
|
+
json: {
|
|
1358
|
+
component: entry.id,
|
|
1359
|
+
framework: demo.framework,
|
|
1360
|
+
...demo.demo,
|
|
1361
|
+
source: demo.source
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
case "token": {
|
|
1366
|
+
const result = queryTokens(knowledge, {
|
|
1367
|
+
query: positionals[0],
|
|
1368
|
+
component: str("component")
|
|
1369
|
+
});
|
|
1370
|
+
return {
|
|
1371
|
+
markdown: renderTokens(result),
|
|
1372
|
+
json: result.kind === "component" ? {
|
|
1373
|
+
kind: result.kind,
|
|
1374
|
+
component: result.component.id,
|
|
1375
|
+
tokens: result.tokens
|
|
1376
|
+
} : result
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
case "parts": {
|
|
1380
|
+
const { component } = componentParts(knowledge, requireArg(positionals[0], "parts <组件>"));
|
|
1381
|
+
const { id, parts, requiredParts, dataAttributes, aria, componentTokens } = component;
|
|
1382
|
+
return {
|
|
1383
|
+
markdown: renderParts(component),
|
|
1384
|
+
json: {
|
|
1385
|
+
id,
|
|
1386
|
+
parts,
|
|
1387
|
+
requiredParts,
|
|
1388
|
+
dataAttributes,
|
|
1389
|
+
aria,
|
|
1390
|
+
componentTokens
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
case "design.md":
|
|
1395
|
+
case "design-md": {
|
|
1396
|
+
const markdown = knowledge.designMd();
|
|
1397
|
+
return {
|
|
1398
|
+
markdown,
|
|
1399
|
+
json: { markdown }
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
case "changelog": {
|
|
1403
|
+
const sections = queryChangelog(knowledge, {
|
|
1404
|
+
from: positionals[0],
|
|
1405
|
+
to: positionals[1],
|
|
1406
|
+
keyword: str("keyword")
|
|
1407
|
+
});
|
|
1408
|
+
return {
|
|
1409
|
+
markdown: renderChangelog(sections),
|
|
1410
|
+
json: sections
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
default: throw new UsageError(`未知命令:${command}。xihan-ui --help 查看全部命令`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function runSetup(values, knowledge, out) {
|
|
1417
|
+
const client = values.client;
|
|
1418
|
+
if (typeof client !== "string" || !CLIENTS.includes(client)) throw new UsageError(`--client 只能是 ${CLIENTS.join(" / ")}`);
|
|
1419
|
+
const mode = typeof values.mode === "string" ? values.mode : "both";
|
|
1420
|
+
if (!SETUP_MODES.includes(mode)) throw new UsageError(`--mode 只能是 ${SETUP_MODES.join(" / ")}`);
|
|
1421
|
+
const project = detectProject(out.cwd);
|
|
1422
|
+
if (!project.root) throw new UsageError("当前目录不在 git 仓库里,向上也找不到 package.json,在项目根目录里运行 setup");
|
|
1423
|
+
const version = typeof values.pin === "string" ? values.pin : project.installedVersion ?? knowledge.meta.version;
|
|
1424
|
+
const actions = planSetup({
|
|
1425
|
+
client,
|
|
1426
|
+
mode,
|
|
1427
|
+
root: project.root,
|
|
1428
|
+
version,
|
|
1429
|
+
platform: process.platform
|
|
1430
|
+
});
|
|
1431
|
+
if (values.check) {
|
|
1432
|
+
const problems = actions.map(checkAction).filter((p) => p !== void 0);
|
|
1433
|
+
if (problems.length) {
|
|
1434
|
+
for (const problem of problems) out.stderr(`✗ ${problem}`);
|
|
1435
|
+
return 1;
|
|
1436
|
+
}
|
|
1437
|
+
out.stdout(`✓ ${client} 的 XiHan.UI 配置已就绪(@xihan-ui/cli@${version})`);
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
1440
|
+
for (const action of actions) {
|
|
1441
|
+
if (!values["dry-run"]) applyAction(action);
|
|
1442
|
+
out.stdout(`${values["dry-run"] || action.kind === "print" ? "" : "✓ "}${describeAction(action)}`);
|
|
1443
|
+
}
|
|
1444
|
+
return 0;
|
|
1445
|
+
}
|
|
1446
|
+
/** 运行一次命令行,返回退出码;mcp 命令在输入流结束后返回。 */
|
|
1447
|
+
async function runCli(argv, environment = {}) {
|
|
1448
|
+
const out = {
|
|
1449
|
+
...defaultEnvironment,
|
|
1450
|
+
...environment
|
|
1451
|
+
};
|
|
1452
|
+
let parsed;
|
|
1453
|
+
try {
|
|
1454
|
+
parsed = parseArgs({
|
|
1455
|
+
args: [...argv],
|
|
1456
|
+
allowPositionals: true,
|
|
1457
|
+
strict: true,
|
|
1458
|
+
options: {
|
|
1459
|
+
"category": { type: "string" },
|
|
1460
|
+
"query": { type: "string" },
|
|
1461
|
+
"component": { type: "string" },
|
|
1462
|
+
"keyword": { type: "string" },
|
|
1463
|
+
"framework": { type: "string" },
|
|
1464
|
+
"format": { type: "string" },
|
|
1465
|
+
"client": { type: "string" },
|
|
1466
|
+
"mode": { type: "string" },
|
|
1467
|
+
"pin": { type: "string" },
|
|
1468
|
+
"dry-run": { type: "boolean" },
|
|
1469
|
+
"check": { type: "boolean" },
|
|
1470
|
+
"version": {
|
|
1471
|
+
type: "boolean",
|
|
1472
|
+
short: "v"
|
|
1473
|
+
},
|
|
1474
|
+
"help": {
|
|
1475
|
+
type: "boolean",
|
|
1476
|
+
short: "h"
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
out.stderr(`${error.message}\n\n${HELP}`);
|
|
1482
|
+
return 2;
|
|
1483
|
+
}
|
|
1484
|
+
const { values, positionals } = parsed;
|
|
1485
|
+
const [command, ...rest] = positionals;
|
|
1486
|
+
if (values.help || !command && !values.version) {
|
|
1487
|
+
out.stdout(HELP);
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
const knowledge = loadKnowledge(out.dataDir);
|
|
1492
|
+
if (values.version) {
|
|
1493
|
+
out.stdout(knowledge.meta.version);
|
|
1494
|
+
return 0;
|
|
1495
|
+
}
|
|
1496
|
+
const project = detectProject(out.cwd);
|
|
1497
|
+
const framework = pickFramework(values.framework, project.framework ?? "vue");
|
|
1498
|
+
const notice = versionNotice(project, knowledge.meta.version);
|
|
1499
|
+
if (command === "mcp") {
|
|
1500
|
+
const instructions = [
|
|
1501
|
+
`XiHan.UI ${knowledge.meta.version} 的离线知识库。写 XiHan.UI 代码前先用 xihan_ui_info 核对组件 API,用 xihan_ui_doc / xihan_ui_demo 取文档与示例,样式只用 xihan_ui_parts 与 xihan_ui_token 查得到的部件选择器与令牌。`,
|
|
1502
|
+
`缺省示例写法:${framework}。`,
|
|
1503
|
+
...notice ? [notice] : []
|
|
1504
|
+
].join("\n");
|
|
1505
|
+
if (notice) out.stderr(notice);
|
|
1506
|
+
await serveStdio({
|
|
1507
|
+
context: {
|
|
1508
|
+
knowledge,
|
|
1509
|
+
framework
|
|
1510
|
+
},
|
|
1511
|
+
version: knowledge.meta.version,
|
|
1512
|
+
instructions
|
|
1513
|
+
});
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|
|
1516
|
+
if (command === "setup") return runSetup(values, knowledge, out);
|
|
1517
|
+
const format = values.format ?? "markdown";
|
|
1518
|
+
if (format !== "markdown" && format !== "json") throw new UsageError(`--format 只能是 markdown / json,收到 ${format}`);
|
|
1519
|
+
const result = query(command, rest, values, knowledge, framework);
|
|
1520
|
+
if (notice) out.stderr(notice);
|
|
1521
|
+
out.stdout(format === "json" ? JSON.stringify(result.json, null, 2) : result.markdown);
|
|
1522
|
+
return 0;
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
if (error instanceof QueryError) {
|
|
1525
|
+
out.stderr(error.message);
|
|
1526
|
+
if (error.suggestions.length) out.stderr(`候选:\n${error.suggestions.map((s) => ` ${s}`).join("\n")}`);
|
|
1527
|
+
return 1;
|
|
1528
|
+
}
|
|
1529
|
+
if (error instanceof UsageError || error instanceof ArgumentError) {
|
|
1530
|
+
out.stderr(error.message);
|
|
1531
|
+
return 2;
|
|
1532
|
+
}
|
|
1533
|
+
out.stderr(error.message);
|
|
1534
|
+
return 1;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
//#endregion
|
|
1538
|
+
export { PROMPTS, PROTOCOL_VERSIONS, TOOLS, handleMessage, loadKnowledge, runCli, serveStdio };
|
|
1539
|
+
|
|
1540
|
+
//# sourceMappingURL=index.js.map
|