@reglow/reui-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -0
- package/catalog.mjs +158 -0
- package/cli-utils.mjs +29 -0
- package/index.mjs +169 -0
- package/integrations.mjs +273 -0
- package/knowledge.mjs +923 -0
- package/package.json +46 -0
- package/server.mjs +710 -0
- package/setup.mjs +400 -0
- package/site.mjs +155 -0
- package/skills/reui-components/BEST_PRACTICES.md +219 -0
- package/skills/reui-components/COMPONENTS.md +141 -0
- package/skills/reui-components/SKILL.md +172 -0
- package/skills/reui-components/WORKFLOWS.md +264 -0
- package/skills.mjs +197 -0
- package/tar.mjs +115 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# ReUI MCP 工作流(WORKFLOWS)
|
|
2
|
+
|
|
3
|
+
> 本文件说明如何用 reui-mcp 的 7 个工具**确定性地**拿到 ReUI 组件的真实文档与代码,并给出 5 个可直接照做的完整任务示例。
|
|
4
|
+
> 事实来源:`reui/guide.md`、`reui/package.json`、各组件 `README.md`,以及 `reglow-docs/content/components-library/1.reui/`。
|
|
5
|
+
|
|
6
|
+
## 1. MCP 工具概览
|
|
7
|
+
|
|
8
|
+
| 工具 | 用途 | 关键参数 | 典型返回 |
|
|
9
|
+
| ------------------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
10
|
+
| `get_reui_document` | 取组件完整文档 | `component`(可选,不传返回按分类的组件总览)、`category`(过滤分类)、`version`、`source`(`auto` / `local` / `site`) | README 正文、API 表格、示例说明;不传 `component` 时返回分类总览 |
|
|
11
|
+
| `get_reui_code_examples` | 提取可直接复制的 Vue/TS 示例 | `component`、序号(第 N 个)、类型过滤、`version` | 代码块 + 所在示例标题 |
|
|
12
|
+
| `list_component_files` | 列组件源码文件清单 | `component`、`version` | 路径 / 行数 / 大小 |
|
|
13
|
+
| `get_file_code` | 读具体文件内容 | `component`、文件路径、行号区间、`version` | 源码片段 |
|
|
14
|
+
| `get_function_code` | 取某个函数/常量/组件定义的完整实现 | `component`、符号名、`version` | 实现代码;找不到时返回该文件可用符号清单 |
|
|
15
|
+
| `search_reui_docs` | 跨文档与源码关键词检索 | 关键词(支持多个)、`category`、`limit`、`version` | `组件/文件:行号` + 上下文片段 + 文档链接 |
|
|
16
|
+
| `get_reui_guide` | 库级指南 | `topic`:`overview` / `install` / `theme` / `i18n` / `nuxt` / `build` / `conventions` | 库级说明 |
|
|
17
|
+
|
|
18
|
+
**共同参数 `version`**:不传 = 项目本地已安装版本;传值则取该 npm 版本的知识,无网络时降级。
|
|
19
|
+
**兜底行为**:某版本包内缺少源码文档时,工具返回官网文档站(https://reglow.huoli.tech/components-library/reui )内容,**回复中必须标明来源与版本**。
|
|
20
|
+
|
|
21
|
+
## 2. 基础查询流程(四步走)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
① 定位 get_reui_document(可带 category) → 确认组件存在、拿到组件名
|
|
25
|
+
② 详情 get_reui_document { component } → README + API 表格 + slot/event
|
|
26
|
+
③ 示例 get_reui_code_examples { component } → 可复制代码
|
|
27
|
+
④ 下钻 list_component_files → get_file_code → 实现细节(仅②③不够时)
|
|
28
|
+
get_function_code
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
每一步的**判断标准**:
|
|
32
|
+
|
|
33
|
+
| 步骤 | 结果够用的标准 | 不够时的动作 |
|
|
34
|
+
| ------ | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
35
|
+
| ① 定位 | 目标组件出现在总览中,且有中文说明 | 换关键词走 `search_reui_docs`;仍无 → 判定「该组件不存在」,不要造名字 |
|
|
36
|
+
| ② 详情 | 找到目标 prop/event/slot 的**表格行**(含类型与默认值) | 文档没有该项 → 走 ④ 读源码确认,而不是猜测 |
|
|
37
|
+
| ③ 示例 | 有与本需求同场景的示例(如"受控模式") | 取最接近的示例 + 用 ② 的 API 表格自行拼装;拼装时刻意只使用文档中出现的 prop |
|
|
38
|
+
| ④ 下钻 | 在源码中找到该行为的分支实现 | 用 `get_function_code` 取整段函数;仍无 → 说明该行为未实现 |
|
|
39
|
+
|
|
40
|
+
**查询用语建议**:组件名用目录名(`splitter` / `date-picker` / `code-editor`),与 `components/reui/<name>/` 一致;
|
|
41
|
+
中文名("面板分割")留给 `search_reui_docs`。
|
|
42
|
+
|
|
43
|
+
## 3. 完整任务示例
|
|
44
|
+
|
|
45
|
+
### 示例 1:给后台详情页做左右分栏(Splitter 基础 + 受控 + 折叠)
|
|
46
|
+
|
|
47
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
48
|
+
| ---- | ------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| 1 | `get_reui_document` | `{ component: "splitter" }` | 确认 `Splitter` / `SplitterPanel` 两个导出、`orientation`(对齐 `vertical`)、`lazy`、`collapsible`、`destroyOnHidden` 等 prop 存在 |
|
|
50
|
+
| 2 | `get_reui_code_examples` | `{ component: "splitter" }` | 需要"受控模式"与"可折叠示例"两段,判断 `@resize` 回调参数为 `PanelSize[]` |
|
|
51
|
+
| 3 | `get_reui_document` | `{ component: "splitter" }` | 核对 `SplitterPanel` 的 `defaultSize` / `size` / `min` / `max` / `collapsible` 类型 |
|
|
52
|
+
| 4 | `get_function_code` | `{ component: "splitter", name: "handleResize" }` | 仅在需要确认"折叠后 size 为 0"这类细节时调用 |
|
|
53
|
+
|
|
54
|
+
产出(来自 `components/reui/splitter/README.md`,消费方把导入改成 `@reglow/reui`):
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<script setup>
|
|
58
|
+
import { ref } from "vue";
|
|
59
|
+
import { Splitter, SplitterPanel } from "@reglow/reui";
|
|
60
|
+
|
|
61
|
+
const sizes = ref(["30%", "70%"]);
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<Splitter class="h-[400px]" @resize="(s) => (sizes = s)">
|
|
66
|
+
<SplitterPanel :size="sizes[0]" :min="200" collapsible>
|
|
67
|
+
<div class="p-4">左:列表</div>
|
|
68
|
+
</SplitterPanel>
|
|
69
|
+
<SplitterPanel :size="sizes[1]">
|
|
70
|
+
<div class="p-4">右:详情</div>
|
|
71
|
+
</SplitterPanel>
|
|
72
|
+
</Splitter>
|
|
73
|
+
</template>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> 注意:`resize` 事件的回调参数类型是 `PanelSize[]`(`number | string`,数字为 px、字符串支持 `"50%"`)。
|
|
77
|
+
|
|
78
|
+
### 示例 2:列表页表格(PageTable:分页 + 搜索 + 行操作)
|
|
79
|
+
|
|
80
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
81
|
+
| ---- | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| 1 | `get_reui_document` | `{ component: "table" }` | 确认导出名是 **`PageTable`**(组件目录名 `table`,聚合清单里写作"Table(PageTable)") |
|
|
83
|
+
| 2 | `get_reui_code_examples` | `{ component: "table" }` | 取"基本用法""分页""搜索""行操作"四段 |
|
|
84
|
+
| 3 | `get_reui_document` | `{ component: "table" }` | 核对 `rowActions` 的 `RowAction` 结构(`key/label/variant/confirm/onClick/show/tooltip`)与 `v-model:page`、`v-model:page-size` |
|
|
85
|
+
| 4 | `search_reui_docs` | `{ keywords: ["rowActions", "confirm"], limit: 10 }` | 确认删除二次确认是 `confirm: { title, description? }` |
|
|
86
|
+
|
|
87
|
+
产出(来自 `components/reui/table/README.md`):
|
|
88
|
+
|
|
89
|
+
```vue
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
import { ref } from "vue";
|
|
92
|
+
import { PageTable } from "@reglow/reui";
|
|
93
|
+
import type { RowAction } from "@reglow/reui";
|
|
94
|
+
import type { ColumnDef } from "@tanstack/vue-table";
|
|
95
|
+
|
|
96
|
+
interface User {
|
|
97
|
+
id: number;
|
|
98
|
+
name: string;
|
|
99
|
+
email: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const columns: ColumnDef<User>[] = [
|
|
103
|
+
{ accessorKey: "name", header: "姓名" },
|
|
104
|
+
{ accessorKey: "email", header: "邮箱" },
|
|
105
|
+
];
|
|
106
|
+
const data = ref<User[]>([]);
|
|
107
|
+
const page = ref(1);
|
|
108
|
+
const pageSize = ref(20);
|
|
109
|
+
const search = ref("");
|
|
110
|
+
const rowSelection = ref({});
|
|
111
|
+
|
|
112
|
+
const rowActions: RowAction<User>[] = [
|
|
113
|
+
{ key: "edit", label: "编辑", onClick: (row) => console.log(row) },
|
|
114
|
+
{
|
|
115
|
+
key: "delete",
|
|
116
|
+
label: "删除",
|
|
117
|
+
variant: "destructive",
|
|
118
|
+
confirm: { title: "确认删除?", description: "此操作不可撤销" },
|
|
119
|
+
onClick: (row) => console.log("删除", row),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<PageTable
|
|
126
|
+
:columns="columns"
|
|
127
|
+
:data="data"
|
|
128
|
+
:total="100"
|
|
129
|
+
v-model:page="page"
|
|
130
|
+
v-model:page-size="pageSize"
|
|
131
|
+
searchable
|
|
132
|
+
search-placeholder="搜索..."
|
|
133
|
+
v-model:search="search"
|
|
134
|
+
:search-keys="['name', 'email']"
|
|
135
|
+
selectable
|
|
136
|
+
v-model:row-selection="rowSelection"
|
|
137
|
+
:row-actions="rowActions"
|
|
138
|
+
/>
|
|
139
|
+
</template>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 示例 3:全局统一语言与主题(ConfigProvider / createReui)
|
|
143
|
+
|
|
144
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
145
|
+
| ---- | ------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| 1 | `get_reui_guide` | `{ topic: "theme" }` | 拿到 `ThemeConfig` 全部字段、17 个主色、7 个中性色、圆角取值、持久化 key |
|
|
147
|
+
| 2 | `get_reui_guide` | `{ topic: "i18n" }` | 拿到 `setLocale` / `addLocale` / `t` / `tm` 语义与内置三语言 |
|
|
148
|
+
| 3 | `get_reui_document` | `{ component: "config-provider" }` | 核对 `ConfigProvider` 的 props(`locale` / `direction` / `componentSize` / `theme` / `namespace` / `getPopupContainer` / `componentConfig`)与嵌套覆盖规则 |
|
|
149
|
+
| 4 | `get_reui_document` | `{ component: "config-provider", category: "others" }` | 确认文档的分类归属(others),便于回链文档页 |
|
|
150
|
+
|
|
151
|
+
产出(来自 `guide.md` §4.4 / §5 与 `config-provider/README.md`):
|
|
152
|
+
|
|
153
|
+
```vue
|
|
154
|
+
<script setup lang="ts">
|
|
155
|
+
import { ConfigProvider } from "@reglow/reui";
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<template>
|
|
159
|
+
<ConfigProvider
|
|
160
|
+
locale="zh-TW"
|
|
161
|
+
:theme="{ mode: 'dark', primaryColor: 'emerald', radius: '0.5' }"
|
|
162
|
+
>
|
|
163
|
+
<App />
|
|
164
|
+
</ConfigProvider>
|
|
165
|
+
</template>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// 命令式:仅当前会话 / 持久化 / 启动恢复
|
|
170
|
+
import { applyTheme, saveTheme, loadAndApplyTheme } from "@reglow/reui/theme";
|
|
171
|
+
|
|
172
|
+
applyTheme({
|
|
173
|
+
mode: "dark",
|
|
174
|
+
baseColor: "zinc",
|
|
175
|
+
primaryColor: "emerald",
|
|
176
|
+
radius: "0.5",
|
|
177
|
+
});
|
|
178
|
+
saveTheme({
|
|
179
|
+
mode: "dark",
|
|
180
|
+
baseColor: "zinc",
|
|
181
|
+
primaryColor: "emerald",
|
|
182
|
+
radius: "0.5",
|
|
183
|
+
});
|
|
184
|
+
loadAndApplyTheme(); // main.ts 中 mount 前调用,避免白屏闪烁
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 示例 4:新增语言包 + 组件级文案覆盖(i18n)
|
|
188
|
+
|
|
189
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
190
|
+
| ---- | ------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |
|
|
191
|
+
| 1 | `get_reui_guide` | `{ topic: "i18n" }` | 确认 `addLocale(locale, messages)` 支持新增语言与**深合并覆盖** |
|
|
192
|
+
| 2 | `get_reui_document` | `{ component: "date-picker" }` | 找到文案类 prop(`placeholder` 等)与默认值来源 |
|
|
193
|
+
| 3 | `get_file_code` | `{ component: "date-picker", file: "DatePicker.vue" }` | 需要确认 key 路径(如 `reui.datePicker.*`)时读源码 |
|
|
194
|
+
| 4 | `get_reui_guide` | `{ topic: "conventions" }` | 若要把新语言提交回组件库,确认三语言同步与响应式铁律 |
|
|
195
|
+
|
|
196
|
+
产出(来自 `i18n/index.ts` 与 `reglow-docs/.../5.i18n.md`):
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { addLocale } from "@reglow/reui";
|
|
200
|
+
|
|
201
|
+
// 新增日语(key 结构:common.* + reui.<component>.*)
|
|
202
|
+
addLocale("ja-JP", {
|
|
203
|
+
common: { confirm: "確認", cancel: "キャンセル" },
|
|
204
|
+
reui: { modal: { ok: "OK" } },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 覆盖内置英文部分文案(深合并,不丢其它 key)
|
|
208
|
+
addLocale("en-US", { common: { confirm: "OK" } });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```vue
|
|
212
|
+
<!-- prop 优先于 i18n 默认值 -->
|
|
213
|
+
<DatePicker placeholder="Pick a date" />
|
|
214
|
+
<Transfer :title-text="['Source', 'Target']" />
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 示例 5:Nuxt 项目接入 + SSR 排障
|
|
218
|
+
|
|
219
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
220
|
+
| ---- | ------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
221
|
+
| 1 | `get_reui_guide` | `{ topic: "nuxt" }` | 拿到 `modules: ["@reglow/reui/nuxt"]`、`reui: { locale, theme, prefix, injectStyles }` |
|
|
222
|
+
| 2 | `get_reui_guide` | `{ topic: "install" }` | 确认 Vue >= 3.5、Tailwind CSS 4、`@reglow/reui/style.css` 引入时机 |
|
|
223
|
+
| 3 | `get_reui_document` | `{ component: "modal" }` | 核对命令式 API(`Modal.confirm` 等)用法 |
|
|
224
|
+
| 4 | `search_reui_docs` | `{ keywords: ["window", "document", "onMounted"], limit: 20 }` | 定位 SSR 不安全代码,产出"仅客户端调用"的整改建议 |
|
|
225
|
+
|
|
226
|
+
产出(来自 `guide.md` §6 / §7 与 `reglow-docs/.../3.usage-nuxt.md`):
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// nuxt.config.ts
|
|
230
|
+
export default defineNuxtConfig({
|
|
231
|
+
modules: ["@reglow/reui/nuxt"],
|
|
232
|
+
reui: {
|
|
233
|
+
prefix: "R", // <Button> → <RButton>,避免与宿主组件重名
|
|
234
|
+
locale: "zh-CN",
|
|
235
|
+
theme: { mode: "light", primaryColor: "blue", radius: "0.5" },
|
|
236
|
+
injectStyles: true, // 宿主已有 Tailwind 时设 false(仅注入 components.css)
|
|
237
|
+
},
|
|
238
|
+
// file:/软链安装时避免 vue 双实例
|
|
239
|
+
vite: { resolve: { dedupe: ["vue"] } },
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
SSR 注意:`Cropper`、`SlideCaptcha`、`QRCode`、`AudioPlayer`、`VideoPlayer`、`Editor`、`Tour`、`Affix`、`BackTop`、`Watermark`、`MasonryGrid` 服务端只渲染占位、客户端水合后激活;`Modal.info/confirm`、`useNotification`、`useToast` 只能在客户端调用。
|
|
244
|
+
|
|
245
|
+
### 示例 6:文档没写清时的源码下钻(通用兜底套路)
|
|
246
|
+
|
|
247
|
+
以"CodeEditor 的语言下拉可选项怎么配"为例:
|
|
248
|
+
|
|
249
|
+
| 步骤 | 调用 | 参数 | 判断 |
|
|
250
|
+
| ---- | ---------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
251
|
+
| 1 | `get_reui_document` | `{ component: "code-editor" }` | 拿到 `language` / `languages` / `readonly` / `wrap` / `tabSize` / `height` 等 prop |
|
|
252
|
+
| 2 | `list_component_files` | `{ component: "code-editor" }` | 看到文件清单(`CodeEditor.vue` / `highlight.ts` / `index.ts`) |
|
|
253
|
+
| 3 | `get_file_code` | `{ component: "code-editor", file: "CodeEditor.vue", startLine: 1, endLine: 120 }` | 看到 props 声明与工具栏渲染分支 |
|
|
254
|
+
| 4 | `get_function_code` | `{ component: "code-editor", name: "highlightCode" }` | 取到独立可用的高亮函数实现与返回结构 |
|
|
255
|
+
|
|
256
|
+
下钻时若 `get_function_code` 报"未找到",**直接使用返回的可用符号清单**重新挑一个符号名再调,而不是换工具瞎猜。
|
|
257
|
+
|
|
258
|
+
## 4. 反模式(不要这样做)
|
|
259
|
+
|
|
260
|
+
1. 跳过 `get_reui_document` 直接凭记忆写 `props`——ReUI 大量组件对齐 antd 命名(`defaultSize`、`activeKey`、`rowActions`),与 shadcn/reka-ui 原始命名不同。
|
|
261
|
+
2. 用 `get_reui_document` 拿总览后,把"组件存在于总览"当成"API 已知"——分类总览不含 props。
|
|
262
|
+
3. 文档缺某行为时,用"应该支持"来补——必须用 `get_file_code` 验证,验证不了就说明未实现。
|
|
263
|
+
4. 忽略版本:跨版本回答时未声明 `version`,导致 API 与实际安装版本不符。
|
|
264
|
+
5. 兜底内容不标来源:工具从官网站点返回内容时必须写明来源与版本。
|
package/skills.mjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* reui-skills:把 ReUI 的 Skills 知识包安装到项目中的 AI 工具目录
|
|
4
|
+
*
|
|
5
|
+
* 知识包内容(`skills/reui-components/`):
|
|
6
|
+
* - SKILL.md 技能入口(何时使用 + 工具速查 + 硬性规则 + 文件导航)
|
|
7
|
+
* - WORKFLOWS.md MCP 工具工作流与完整任务示例
|
|
8
|
+
* - BEST_PRACTICES.md 引入方式、主题、i18n、Nuxt、扩展与常见陷阱
|
|
9
|
+
* - COMPONENTS.md 按分类的组件选型清单(含官网文档链接)
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* npx -y -p @reglow/reui-mcp reui-skills # 默认装到 .trae/skills
|
|
13
|
+
* npx -y -p @reglow/reui-mcp reui-skills --target cursor # 装到 .cursor/skills
|
|
14
|
+
* npx -y -p @reglow/reui-mcp reui-skills --target .my/skills # 自定义目录
|
|
15
|
+
* npx -y -p @reglow/reui-mcp reui-skills --all # 装到全部已知工具目录
|
|
16
|
+
* npx -y -p @reglow/reui-mcp reui-skills --list # 查看可用目标
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { isEntryModule } from "./cli-utils.mjs";
|
|
23
|
+
import { skillTargetsFromIntegrations } from "./integrations.mjs";
|
|
24
|
+
|
|
25
|
+
const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const SKILL_SOURCE = path.join(PACKAGE_DIR, "skills");
|
|
27
|
+
const SKILL_NAME = "reui-components";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 各 AI 编程工具的 Skills 目录约定
|
|
31
|
+
*
|
|
32
|
+
* 主流工具(Trae / CodeBuddy / Qoder / Cursor / Claude)的目录来自 `integrations.mjs`
|
|
33
|
+
* 中的官方规格;Qwen Code 与 OpenAI Codex CLI 仅提供 Skills 目标。
|
|
34
|
+
*/
|
|
35
|
+
export const SKILL_TARGETS = {
|
|
36
|
+
...skillTargetsFromIntegrations(),
|
|
37
|
+
qwen: { dir: ".qwen/skills", label: "Qwen Code" },
|
|
38
|
+
codex: { dir: ".codex/skills", label: "OpenAI Codex CLI" },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** MCP 配置片段(写入客户端配置的 `mcpServers`) */
|
|
42
|
+
export function mcpConfigSnippet() {
|
|
43
|
+
return {
|
|
44
|
+
mcpServers: {
|
|
45
|
+
"reui-mcp": {
|
|
46
|
+
command: "npx",
|
|
47
|
+
args: ["-y", "@reglow/reui-mcp"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listSkillFiles(dir, base = dir) {
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
56
|
+
const full = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) result.push(...listSkillFiles(full, base));
|
|
58
|
+
else if (entry.isFile()) result.push(path.relative(base, full));
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 安装 Skills 知识包
|
|
65
|
+
*
|
|
66
|
+
* @param {{ cwd?: string, targets?: string[], force?: boolean, dryRun?: boolean, logger?: (m: string) => void }} options
|
|
67
|
+
* @returns {{ installed: { target: string, dir: string, files: string[], skipped: string[] }[] }}
|
|
68
|
+
*/
|
|
69
|
+
export function installSkills(options = {}) {
|
|
70
|
+
const logger = options.logger ?? ((message) => console.log(message));
|
|
71
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
72
|
+
const force = options.force ?? false;
|
|
73
|
+
const dryRun = options.dryRun ?? false;
|
|
74
|
+
const requested = options.targets?.length ? options.targets : ["trae"];
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(SKILL_SOURCE)) {
|
|
77
|
+
throw new Error(`未找到内置 Skills 目录:${SKILL_SOURCE}`);
|
|
78
|
+
}
|
|
79
|
+
// 以技能目录为基准计算相对路径,避免安装后出现 `reui-components/reui-components/` 的重复层级
|
|
80
|
+
const contentDir = path.join(SKILL_SOURCE, SKILL_NAME);
|
|
81
|
+
if (!fs.existsSync(contentDir)) {
|
|
82
|
+
throw new Error(`未找到内置技能目录:${contentDir}`);
|
|
83
|
+
}
|
|
84
|
+
const sourceFiles = listSkillFiles(contentDir);
|
|
85
|
+
const installed = [];
|
|
86
|
+
|
|
87
|
+
for (const requestedTarget of requested) {
|
|
88
|
+
const preset = SKILL_TARGETS[requestedTarget];
|
|
89
|
+
const relativeDir = preset ? preset.dir : requestedTarget;
|
|
90
|
+
const targetRoot = path.resolve(cwd, relativeDir);
|
|
91
|
+
const targetDir = path.join(targetRoot, SKILL_NAME);
|
|
92
|
+
const files = [];
|
|
93
|
+
const skipped = [];
|
|
94
|
+
|
|
95
|
+
for (const file of sourceFiles) {
|
|
96
|
+
const destination = path.join(targetDir, file);
|
|
97
|
+
if (fs.existsSync(destination) && !force) {
|
|
98
|
+
skipped.push(file);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!dryRun) {
|
|
102
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
103
|
+
fs.copyFileSync(path.join(contentDir, file), destination);
|
|
104
|
+
}
|
|
105
|
+
files.push(file);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
installed.push({ target: requestedTarget, dir: targetDir, files, skipped });
|
|
109
|
+
logger(
|
|
110
|
+
`${dryRun ? "[dry-run] " : ""}${preset?.label ?? requestedTarget} → ${path.relative(cwd, targetDir) || targetDir}` +
|
|
111
|
+
`(写入 ${files.length} 个文件${skipped.length ? `,跳过已存在 ${skipped.length} 个(--force 可覆盖)` : ""})`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { installed };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** CLI 入口 */
|
|
119
|
+
export function runSkillsCli(argv = process.argv.slice(2)) {
|
|
120
|
+
if (argv.includes("--list")) {
|
|
121
|
+
console.log("可用目标(--target 支持预设名或自定义目录):");
|
|
122
|
+
for (const [key, value] of Object.entries(SKILL_TARGETS)) {
|
|
123
|
+
console.log(` ${key.padEnd(10)} ${value.dir.padEnd(18)} ${value.label}`);
|
|
124
|
+
}
|
|
125
|
+
console.log("\n示例:npx -y -p @reglow/reui-mcp reui-skills --target trae");
|
|
126
|
+
console.log(
|
|
127
|
+
"需要同时写 MCP 配置:npx -y -p @reglow/reui-mcp reui-setup --tools trae,codebuddy,qoder",
|
|
128
|
+
);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const values = { target: undefined, cwd: undefined };
|
|
133
|
+
const flags = { all: false, force: false, dryRun: false };
|
|
134
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
135
|
+
const arg = argv[i];
|
|
136
|
+
if (arg === "--all") flags.all = true;
|
|
137
|
+
else if (arg === "--force") flags.force = true;
|
|
138
|
+
else if (arg === "--dry-run") flags.dryRun = true;
|
|
139
|
+
else if (arg === "--target" || arg === "-t") values.target = argv[++i];
|
|
140
|
+
else if (arg.startsWith("--target="))
|
|
141
|
+
values.target = arg.slice("--target=".length);
|
|
142
|
+
else if (arg === "--cwd") values.cwd = argv[++i];
|
|
143
|
+
else if (arg.startsWith("--cwd=")) values.cwd = arg.slice("--cwd=".length);
|
|
144
|
+
else if (arg === "--help" || arg === "-h") {
|
|
145
|
+
console.log(
|
|
146
|
+
[
|
|
147
|
+
"reui-skills —— 安装 ReUI 的 Agent Skills 知识包",
|
|
148
|
+
"",
|
|
149
|
+
"用法:npx -y -p @reglow/reui-mcp reui-skills [选项]",
|
|
150
|
+
"",
|
|
151
|
+
"选项:",
|
|
152
|
+
" -t, --target <name|dir> 安装目标:预设名(trae/codebuddy/qoder/cursor/claude/qwen/codex/generic)或自定义目录,默认 trae",
|
|
153
|
+
" --all 安装到全部预设目录",
|
|
154
|
+
" --cwd <dir> 项目根目录,默认当前目录",
|
|
155
|
+
" --force 覆盖已存在的文件",
|
|
156
|
+
" --dry-run 只打印将要写入的文件",
|
|
157
|
+
" --list 列出可用目标",
|
|
158
|
+
" -h, --help 显示帮助",
|
|
159
|
+
"",
|
|
160
|
+
"提示:需要同时写入 MCP 配置时,用 reui-setup(等价于 reui-mcp --setup)。",
|
|
161
|
+
].join("\n"),
|
|
162
|
+
);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const targets = flags.all
|
|
168
|
+
? Object.keys(SKILL_TARGETS)
|
|
169
|
+
: [values.target ?? "trae"];
|
|
170
|
+
const result = installSkills({
|
|
171
|
+
cwd: values.cwd,
|
|
172
|
+
targets,
|
|
173
|
+
force: flags.force,
|
|
174
|
+
dryRun: flags.dryRun,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const total = result.installed.reduce(
|
|
178
|
+
(sum, item) => sum + item.files.length,
|
|
179
|
+
0,
|
|
180
|
+
);
|
|
181
|
+
console.log(`\n已安装 ReUI Skills:${SKILL_NAME}(${total} 个文件)`);
|
|
182
|
+
console.log("\n配套的 MCP 客户端配置(mcpServers):");
|
|
183
|
+
console.log(JSON.stringify(mcpConfigSnippet(), null, 2));
|
|
184
|
+
console.log(
|
|
185
|
+
"\n提示:Skills 负责「怎么用」,MCP 负责「查得到」——建议两者一起配置。",
|
|
186
|
+
);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isEntryModule(import.meta.url)) {
|
|
191
|
+
try {
|
|
192
|
+
process.exitCode = runSkillsCli();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`[reui-skills] 安装失败:${error.message}`);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/tar.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 tar 解包(零依赖,仅支持 npm 包发布用的 tar 格式)
|
|
3
|
+
*
|
|
4
|
+
* 用途:把 npm registry 下载的 `<pkg>.tgz` 解包到本地缓存目录,
|
|
5
|
+
* 以便按指定版本查询组件文档与源码。
|
|
6
|
+
*
|
|
7
|
+
* 支持:普通文件、目录、PAX 扩展头(path 覆盖)、GNU 长文件名(L)、
|
|
8
|
+
* 长链接名(K)—— 覆盖 npm 打包产物的全部形态;其余类型直接跳过。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { gunzipSync } from "node:zlib";
|
|
12
|
+
|
|
13
|
+
const BLOCK = 512;
|
|
14
|
+
|
|
15
|
+
/** 读取 tar 头中的字符串字段 */
|
|
16
|
+
function readString(block, offset, length) {
|
|
17
|
+
const slice = block.subarray(offset, offset + length);
|
|
18
|
+
const end = slice.indexOf(0);
|
|
19
|
+
return slice.subarray(0, end === -1 ? slice.length : end).toString("utf8").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 读取八进制数值字段 */
|
|
23
|
+
function readNumber(block, offset, length) {
|
|
24
|
+
const raw = readString(block, offset, length).replace(/\0/g, "").trim();
|
|
25
|
+
if (!raw) return 0;
|
|
26
|
+
const value = Number.parseInt(raw, 8);
|
|
27
|
+
return Number.isFinite(value) ? value : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 解析 PAX 扩展头内容(`<len> <key>=<value>\n` 重复) */
|
|
31
|
+
function parsePax(text) {
|
|
32
|
+
const result = {};
|
|
33
|
+
let index = 0;
|
|
34
|
+
while (index < text.length) {
|
|
35
|
+
const space = text.indexOf(" ", index);
|
|
36
|
+
if (space === -1) break;
|
|
37
|
+
const length = Number.parseInt(text.slice(index, space), 10);
|
|
38
|
+
if (!Number.isFinite(length) || length <= 0) break;
|
|
39
|
+
const record = text.slice(space + 1, index + length - 1);
|
|
40
|
+
const eq = record.indexOf("=");
|
|
41
|
+
if (eq > 0) result[record.slice(0, eq)] = record.slice(eq + 1);
|
|
42
|
+
index += length;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 解包 tar 数据(未压缩)
|
|
49
|
+
*
|
|
50
|
+
* @param {Buffer} buffer tar 字节
|
|
51
|
+
* @param {(entry: { path: string, type: string, data: Buffer }) => void} [onEntry] 逐条回调
|
|
52
|
+
* @returns {{ path: string, type: string, data: Buffer }[]} 文件条目(不含目录)
|
|
53
|
+
*/
|
|
54
|
+
export function extractTar(buffer, onEntry) {
|
|
55
|
+
const files = [];
|
|
56
|
+
let offset = 0;
|
|
57
|
+
let pendingPax = null;
|
|
58
|
+
let pendingLongName = null;
|
|
59
|
+
|
|
60
|
+
while (offset + BLOCK <= buffer.length) {
|
|
61
|
+
const header = buffer.subarray(offset, offset + BLOCK);
|
|
62
|
+
// 连续两个全零块表示归档结束
|
|
63
|
+
if (header.every((byte) => byte === 0)) {
|
|
64
|
+
offset += BLOCK;
|
|
65
|
+
const next = buffer.subarray(offset, offset + BLOCK);
|
|
66
|
+
if (next.length === 0 || next.every((byte) => byte === 0)) break;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const size = readNumber(header, 124, 12);
|
|
71
|
+
const typeFlag = String.fromCharCode(header[156] || 48);
|
|
72
|
+
let name = readString(header, 0, 100);
|
|
73
|
+
const prefix = readString(header, 345, 155);
|
|
74
|
+
if (prefix) name = `${prefix}/${name}`;
|
|
75
|
+
const dataStart = offset + BLOCK;
|
|
76
|
+
const data = buffer.subarray(dataStart, dataStart + size);
|
|
77
|
+
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
78
|
+
|
|
79
|
+
if (typeFlag === "x" || typeFlag === "X") {
|
|
80
|
+
pendingPax = parsePax(data.toString("utf8"));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeFlag === "L") {
|
|
84
|
+
pendingLongName = data.toString("utf8").replace(/\0+$/, "");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeFlag === "K" || typeFlag === "g") continue;
|
|
88
|
+
|
|
89
|
+
if (pendingPax?.path) name = pendingPax.path;
|
|
90
|
+
if (pendingLongName) {
|
|
91
|
+
name = pendingLongName;
|
|
92
|
+
pendingLongName = null;
|
|
93
|
+
}
|
|
94
|
+
const pax = pendingPax;
|
|
95
|
+
pendingPax = null;
|
|
96
|
+
|
|
97
|
+
if (typeFlag === "0" || typeFlag === "\0" || typeFlag === "7" || typeFlag === "") {
|
|
98
|
+
const entry = { path: name.replace(/^\.\//, ""), type: "file", data, mode: pax?.mode };
|
|
99
|
+
files.push(entry);
|
|
100
|
+
onEntry?.(entry);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return files;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 解包 .tgz 数据
|
|
109
|
+
*
|
|
110
|
+
* @param {Buffer} buffer gzip 压缩的 tar
|
|
111
|
+
* @param {(entry: { path: string, type: string, data: Buffer }) => void} [onEntry]
|
|
112
|
+
*/
|
|
113
|
+
export function extractTarball(buffer, onEntry) {
|
|
114
|
+
return extractTar(gunzipSync(buffer), onEntry);
|
|
115
|
+
}
|