mdk-skills 2.4.18 → 2.4.19
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/package.json +3 -2
- package/scripts/web-ui/server/context.js +273 -0
- package/scripts/web-ui/server/router.js +69 -0
- package/scripts/web-ui/server/routes/others.js +239 -0
- package/scripts/web-ui/server/routes/profiles.js +127 -0
- package/scripts/web-ui/server/routes/skills.js +697 -0
- package/scripts/web-ui/server/routes/source.js +263 -0
- package/scripts/web-ui/server/utils.js +258 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdk-skills",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.19",
|
|
4
4
|
"description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
|
|
5
5
|
"author": "XiaoMa",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"scripts/cli.js",
|
|
12
12
|
"scripts/core.js",
|
|
13
13
|
"scripts/web-ui/server.js",
|
|
14
|
-
"scripts/web-ui/dist/"
|
|
14
|
+
"scripts/web-ui/dist/",
|
|
15
|
+
"scripts/web-ui/server/"
|
|
15
16
|
],
|
|
16
17
|
"bin": {
|
|
17
18
|
"mdk-skills": "./scripts/cli.js"
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const core = require("../../core");
|
|
4
|
+
|
|
5
|
+
// ---------- 路径(与 cli.js 保持一致) ----------
|
|
6
|
+
|
|
7
|
+
const projectRoot = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return fs.realpathSync.native(process.cwd());
|
|
10
|
+
} catch {
|
|
11
|
+
return process.cwd();
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
let skillsSource = core.getSkillsSource(projectRoot);
|
|
16
|
+
const claudeDest = path.join(projectRoot, ".claude");
|
|
17
|
+
const skillsDest = path.join(claudeDest, "skills");
|
|
18
|
+
const settingsPath = path.join(claudeDest, "settings.json");
|
|
19
|
+
let pkgSkillsSource = skillsSource ? path.join(skillsSource, ".claude", "skills") : null;
|
|
20
|
+
|
|
21
|
+
// skills 包二进制路径
|
|
22
|
+
const skillsBin = (() => {
|
|
23
|
+
try {
|
|
24
|
+
const skillsPkg = require.resolve("skills/package.json");
|
|
25
|
+
const nmDir = path.dirname(path.dirname(skillsPkg));
|
|
26
|
+
for (const name of ["skills.cmd", "skills.ps1", "skills"]) {
|
|
27
|
+
const full = path.join(nmDir, ".bin", name);
|
|
28
|
+
if (fs.existsSync(full)) return `"${full}"`;
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
const binDir = path.join(projectRoot, "node_modules", ".bin");
|
|
32
|
+
for (const name of ["skills.cmd", "skills.ps1", "skills"]) {
|
|
33
|
+
const full = path.join(binDir, name);
|
|
34
|
+
if (fs.existsSync(full)) return `"${full}"`;
|
|
35
|
+
}
|
|
36
|
+
return `"${path.join(binDir, process.platform === "win32" ? "skills.cmd" : "skills")}"`;
|
|
37
|
+
})();
|
|
38
|
+
|
|
39
|
+
function refreshSource() {
|
|
40
|
+
skillsSource = core.getSkillsSource(projectRoot);
|
|
41
|
+
pkgSkillsSource = skillsSource ? path.join(skillsSource, ".claude", "skills") : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------- Settings ----------
|
|
45
|
+
|
|
46
|
+
function readSettings() {
|
|
47
|
+
if (!fs.existsSync(settingsPath))
|
|
48
|
+
return { skills: {}, always_apply_skills: [] };
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
51
|
+
} catch {
|
|
52
|
+
return { skills: {}, always_apply_skills: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeSettings(settings) {
|
|
57
|
+
if (!fs.existsSync(claudeDest)) {
|
|
58
|
+
fs.mkdirSync(claudeDest, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
settingsPath,
|
|
62
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
63
|
+
"utf-8",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- pull-source 存储 ----------
|
|
68
|
+
|
|
69
|
+
function getPullSourcePath() {
|
|
70
|
+
return path.join(pkgSkillsSource, ".pull-source.json");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readPullSource() {
|
|
74
|
+
if (!pkgSkillsSource) return {};
|
|
75
|
+
const filePath = getPullSourcePath();
|
|
76
|
+
if (!fs.existsSync(filePath)) return {};
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writePullSource(data) {
|
|
85
|
+
fs.writeFileSync(getPullSourcePath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addPullSource(skillName, url) {
|
|
89
|
+
const data = readPullSource();
|
|
90
|
+
data[skillName] = { url, pulledAt: new Date().toISOString() };
|
|
91
|
+
writePullSource(data);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 检查技能源是否已设置
|
|
95
|
+
function requireSource(res, utils) {
|
|
96
|
+
if (!skillsSource || !pkgSkillsSource) {
|
|
97
|
+
utils.sendJSON(res, { error: "未设置技能目录,请在设置中连接" }, 400);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------- profiles ----------
|
|
104
|
+
|
|
105
|
+
function loadProfiles() {
|
|
106
|
+
const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
|
|
107
|
+
if (!fs.existsSync(profilesPath)) return null;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- 异步 npx 管理 ----------
|
|
116
|
+
|
|
117
|
+
const runningTasks = new Map();
|
|
118
|
+
|
|
119
|
+
// ---------- pull 缓存 ----------
|
|
120
|
+
|
|
121
|
+
const pullCache = new Map();
|
|
122
|
+
|
|
123
|
+
// ---------- 应用场景 / 安装 ----------
|
|
124
|
+
|
|
125
|
+
function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSettings }) {
|
|
126
|
+
const pkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
127
|
+
const failedDelete = [];
|
|
128
|
+
const failedInstall = [];
|
|
129
|
+
|
|
130
|
+
let selected;
|
|
131
|
+
if (profile.skills === null) {
|
|
132
|
+
return { selected: [], message: "自定义场景,不做自动切换" };
|
|
133
|
+
} else if (profile.skills.length === 0) {
|
|
134
|
+
selected = [];
|
|
135
|
+
} else {
|
|
136
|
+
selected = profile.skills.filter((s) => pkgSkills.includes(s));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(skillsDest)) {
|
|
140
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
for (const name of pkgSkills) {
|
|
143
|
+
const src = path.join(pkgSkillsSource, name);
|
|
144
|
+
const dest = path.join(skillsDest, name);
|
|
145
|
+
if (selected.includes(name)) {
|
|
146
|
+
if (!fs.existsSync(dest)) {
|
|
147
|
+
try { copyDirSync(src, dest); } catch (err) {
|
|
148
|
+
console.error(`安装技能 "${name}" 失败:`, err.message);
|
|
149
|
+
failedInstall.push(name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
if (fs.existsSync(dest)) {
|
|
154
|
+
const r = safeRmSync(dest, name);
|
|
155
|
+
if (!r.removed) failedDelete.push(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const hasFailed = failedDelete.length > 0 || failedInstall.length > 0;
|
|
161
|
+
if (!hasFailed) {
|
|
162
|
+
const settings = readSettings();
|
|
163
|
+
if (!settings.skills) settings.skills = {};
|
|
164
|
+
for (const name of selected) {
|
|
165
|
+
if (!settings.skills[name]) settings.skills[name] = {};
|
|
166
|
+
settings.skills[name].enabled = true;
|
|
167
|
+
}
|
|
168
|
+
for (const name of Object.keys(settings.skills)) {
|
|
169
|
+
if (!selected.includes(name) && name[0] !== '_') {
|
|
170
|
+
delete settings.skills[name];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (profile.always_apply) {
|
|
174
|
+
settings.always_apply_skills = profile.always_apply;
|
|
175
|
+
}
|
|
176
|
+
settings._active_profile = profile.id;
|
|
177
|
+
writeSettings(settings);
|
|
178
|
+
|
|
179
|
+
const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
|
|
180
|
+
const profilesDest = path.join(claudeDest, "profiles.json");
|
|
181
|
+
if (fs.existsSync(profilesSource)) {
|
|
182
|
+
fs.copyFileSync(profilesSource, profilesDest);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const mdSource = path.join(skillsSource, "CLAUDE.md");
|
|
186
|
+
const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
187
|
+
if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
|
|
188
|
+
fs.copyFileSync(mdSource, mdDest);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
selected,
|
|
194
|
+
enabled: selected.length,
|
|
195
|
+
disabled: pkgSkills.length - selected.length,
|
|
196
|
+
failedDelete,
|
|
197
|
+
failedInstall,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function installSelectedSkills(selectedNames, { copyDirSync, safeRmSync, readSettings, writeSettings }) {
|
|
202
|
+
if (!fs.existsSync(pkgSkillsSource)) return { installed: 0, locked: [] };
|
|
203
|
+
if (!fs.existsSync(skillsDest)) {
|
|
204
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
207
|
+
let installedCount = 0;
|
|
208
|
+
const locked = [];
|
|
209
|
+
|
|
210
|
+
for (const name of allPkgSkills) {
|
|
211
|
+
const src = path.join(pkgSkillsSource, name);
|
|
212
|
+
const dest = path.join(skillsDest, name);
|
|
213
|
+
if (selectedNames.includes(name)) {
|
|
214
|
+
if (!fs.existsSync(dest)) {
|
|
215
|
+
try { copyDirSync(src, dest); installedCount++; } catch (err) {
|
|
216
|
+
console.error(`安装技能 "${name}" 失败:`, err.message);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if (fs.existsSync(dest)) {
|
|
221
|
+
const r = safeRmSync(dest, name);
|
|
222
|
+
if (!r.removed && r.reason === 'locked') locked.push(name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const settings = readSettings();
|
|
228
|
+
if (!settings.skills) settings.skills = {};
|
|
229
|
+
for (const name of allPkgSkills) {
|
|
230
|
+
const enabled = selectedNames.includes(name);
|
|
231
|
+
if (!settings.skills[name]) settings.skills[name] = {};
|
|
232
|
+
settings.skills[name].enabled = enabled;
|
|
233
|
+
}
|
|
234
|
+
writeSettings(settings);
|
|
235
|
+
|
|
236
|
+
const mdSource = path.join(skillsSource, "CLAUDE.md");
|
|
237
|
+
const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
238
|
+
if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
|
|
239
|
+
fs.copyFileSync(mdSource, mdDest);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { installed: installedCount, locked };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------- 回调函数注册(用于动态依赖注入) ----------
|
|
246
|
+
|
|
247
|
+
// 由于 applyProfile / installSelectedSkills 需要在编译时传入依赖
|
|
248
|
+
// 而在拆分前它们直接引用模块级变量,因此我们在 context 模块级保留接口
|
|
249
|
+
function buildContext(utils) {
|
|
250
|
+
return {
|
|
251
|
+
projectRoot,
|
|
252
|
+
claudeDest,
|
|
253
|
+
skillsDest,
|
|
254
|
+
settingsPath,
|
|
255
|
+
get skillsSource() { return skillsSource; },
|
|
256
|
+
get pkgSkillsSource() { return pkgSkillsSource; },
|
|
257
|
+
skillsBin,
|
|
258
|
+
refreshSource,
|
|
259
|
+
readSettings,
|
|
260
|
+
writeSettings,
|
|
261
|
+
readPullSource,
|
|
262
|
+
writePullSource,
|
|
263
|
+
addPullSource,
|
|
264
|
+
loadProfiles,
|
|
265
|
+
requireSource: (res) => requireSource(res, utils),
|
|
266
|
+
applyProfile: (profile) => applyProfile(profile, utils),
|
|
267
|
+
installSelectedSkills: (names) => installSelectedSkills(names, utils),
|
|
268
|
+
runningTasks,
|
|
269
|
+
pullCache,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { buildContext };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量级路由注册器
|
|
3
|
+
* 支持精确路径和正则路径匹配两种模式
|
|
4
|
+
* XiaoMa
|
|
5
|
+
*/
|
|
6
|
+
class Router {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._routes = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 注册精确路径路由(支持 :name 参数占位符)
|
|
13
|
+
* /api/skills/:name/source → /^\/api\/skills\/([^/]+)\/source$/
|
|
14
|
+
*/
|
|
15
|
+
_register(method, pattern, handler) {
|
|
16
|
+
if (pattern.includes(":")) {
|
|
17
|
+
const paramNames = [];
|
|
18
|
+
const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
|
|
19
|
+
paramNames.push(name);
|
|
20
|
+
return "([^/]+)";
|
|
21
|
+
});
|
|
22
|
+
this._routes.push({
|
|
23
|
+
method,
|
|
24
|
+
regex: new RegExp("^" + regexStr + "$"),
|
|
25
|
+
paramNames,
|
|
26
|
+
handler,
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
this._routes.push({ method, path: pattern, handler });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get(pattern, handler) { this._register("GET", pattern, handler); return this; }
|
|
34
|
+
post(pattern, handler) { this._register("POST", pattern, handler); return this; }
|
|
35
|
+
patch(pattern, handler) { this._register("PATCH", pattern, handler); return this; }
|
|
36
|
+
delete(pattern, handler) { this._register("DELETE", pattern, handler); return this; }
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 匹配路由并执行
|
|
40
|
+
* @returns {boolean} 是否匹配到路由
|
|
41
|
+
*/
|
|
42
|
+
async dispatch(method, pathname, req, res) {
|
|
43
|
+
for (const route of this._routes) {
|
|
44
|
+
if (route.method !== method) continue;
|
|
45
|
+
|
|
46
|
+
if (route.path) {
|
|
47
|
+
// 精确路径匹配
|
|
48
|
+
if (route.path === pathname) {
|
|
49
|
+
await route.handler(req, res);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
} else if (route.regex) {
|
|
53
|
+
// 正则路径匹配(含参数)
|
|
54
|
+
const match = pathname.match(route.regex);
|
|
55
|
+
if (match) {
|
|
56
|
+
const params = {};
|
|
57
|
+
route.paramNames.forEach((name, i) => {
|
|
58
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
59
|
+
});
|
|
60
|
+
await route.handler(req, res, params);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { Router };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const core = require("../../../core");
|
|
4
|
+
const utils = require("../utils");
|
|
5
|
+
const { buildContext } = require("../context");
|
|
6
|
+
|
|
7
|
+
const ctx = buildContext(utils);
|
|
8
|
+
|
|
9
|
+
function register(router) {
|
|
10
|
+
|
|
11
|
+
// ----------------------------------------------------------------
|
|
12
|
+
// GET /api/readme — 获取根目录 README.md
|
|
13
|
+
// ----------------------------------------------------------------
|
|
14
|
+
router.get("/api/readme", async (req, res) => {
|
|
15
|
+
if (!ctx.skillsSource) return utils.sendJSON(res, { content: null, found: false });
|
|
16
|
+
const p = path.join(ctx.skillsSource, "README.md");
|
|
17
|
+
const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
|
|
18
|
+
return utils.sendJSON(res, { content, found: !!content });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ----------------------------------------------------------------
|
|
22
|
+
// GET /api/skills-readme — 获取技能目录 README.md
|
|
23
|
+
// ----------------------------------------------------------------
|
|
24
|
+
router.get("/api/skills-readme", async (req, res) => {
|
|
25
|
+
if (!ctx.skillsSource) return utils.sendJSON(res, { content: null, found: false });
|
|
26
|
+
const p = path.join(ctx.skillsSource, ".claude", "skills", "README.md");
|
|
27
|
+
const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
|
|
28
|
+
return utils.sendJSON(res, { content, found: !!content });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ----------------------------------------------------------------
|
|
32
|
+
// GET /api/claudemd — 获取 CLAUDE.md 内容
|
|
33
|
+
// ----------------------------------------------------------------
|
|
34
|
+
router.get("/api/claudemd", async (req, res) => {
|
|
35
|
+
const paths = ctx.skillsSource
|
|
36
|
+
? [path.join(ctx.skillsSource, "CLAUDE.md"), path.join(ctx.projectRoot, "CLAUDE.md")]
|
|
37
|
+
: [path.join(ctx.projectRoot, "CLAUDE.md")];
|
|
38
|
+
let content = null;
|
|
39
|
+
for (const p of paths) {
|
|
40
|
+
if (fs.existsSync(p)) {
|
|
41
|
+
content = fs.readFileSync(p, "utf-8");
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return utils.sendJSON(res, { content, found: !!content });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ----------------------------------------------------------------
|
|
49
|
+
// POST /api/readme/create — 创建 README.md 文档
|
|
50
|
+
// ----------------------------------------------------------------
|
|
51
|
+
router.post("/api/readme/create", async (req, res) => {
|
|
52
|
+
const body = await utils.parseBody(req);
|
|
53
|
+
const settings = ctx.readSettings();
|
|
54
|
+
const sourcePath = settings._skill_source;
|
|
55
|
+
if (!sourcePath) return utils.sendJSON(res, { error: "未绑定本地源" }, 400);
|
|
56
|
+
|
|
57
|
+
const { root, skills, projectName, teamName } = body;
|
|
58
|
+
const name = projectName || "我的技能仓库";
|
|
59
|
+
const team = teamName || "未设定";
|
|
60
|
+
const created = [];
|
|
61
|
+
const skipped = [];
|
|
62
|
+
|
|
63
|
+
const skillsDir = path.join(sourcePath, ".claude", "skills");
|
|
64
|
+
const skillNames = fs.existsSync(skillsDir)
|
|
65
|
+
? core.listSkillDirs(skillsDir)
|
|
66
|
+
: [];
|
|
67
|
+
const skillsList = skillNames.length
|
|
68
|
+
? skillNames.map((s) => `- \`${s}\``).join("\n")
|
|
69
|
+
: "<!-- 暂无技能 -->";
|
|
70
|
+
|
|
71
|
+
// 根目录 README.md
|
|
72
|
+
if (root) {
|
|
73
|
+
const rootPath = path.join(sourcePath, "README.md");
|
|
74
|
+
if (!fs.existsSync(rootPath)) {
|
|
75
|
+
const tmpl = `# ${name}
|
|
76
|
+
|
|
77
|
+
> 维护团队:${team}
|
|
78
|
+
|
|
79
|
+
## 简介
|
|
80
|
+
|
|
81
|
+
<!--
|
|
82
|
+
在这里写这个技能仓库的简介。
|
|
83
|
+
|
|
84
|
+
示例:
|
|
85
|
+
本仓库包含面向 mdk-engineer 的前端开发技能集,涵盖 Vue3 组件开发、Chrome DevTools 使用、性能优化等方向。
|
|
86
|
+
-->
|
|
87
|
+
|
|
88
|
+
## 技能列表
|
|
89
|
+
|
|
90
|
+
${skillsList}
|
|
91
|
+
|
|
92
|
+
## 使用方式
|
|
93
|
+
|
|
94
|
+
本仓库通过 mdk-skills 管理技能,连接本地源后即可在 Web UI 中查看和安装技能。
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
<!-- 许可证信息 -->
|
|
99
|
+
`;
|
|
100
|
+
fs.writeFileSync(rootPath, tmpl, "utf-8");
|
|
101
|
+
created.push("README.md");
|
|
102
|
+
} else {
|
|
103
|
+
skipped.push("README.md");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// skills/README.md
|
|
108
|
+
if (skills) {
|
|
109
|
+
const skillsReadmePath = path.join(skillsDir, "README.md");
|
|
110
|
+
if (!fs.existsSync(skillsReadmePath)) {
|
|
111
|
+
const tmpl = `# 技能说明
|
|
112
|
+
|
|
113
|
+
## 快速开始
|
|
114
|
+
|
|
115
|
+
<!--
|
|
116
|
+
安装 mdk-skills 后连接本仓库,即可在 Web UI 中查看所有技能。
|
|
117
|
+
-->
|
|
118
|
+
|
|
119
|
+
## 场景建议
|
|
120
|
+
|
|
121
|
+
<!--
|
|
122
|
+
这里可以写哪些技能适合一起使用,不同开发场景下的推荐组合。
|
|
123
|
+
|
|
124
|
+
示例:
|
|
125
|
+
- 日常开发:启用代码格式化、ESLint 校验、Git 辅助
|
|
126
|
+
- 性能优化:启用 Lighthouse、Bundle Analysis
|
|
127
|
+
-->
|
|
128
|
+
|
|
129
|
+
## 注意事项
|
|
130
|
+
|
|
131
|
+
<!--
|
|
132
|
+
技能使用过程中的注意事项。
|
|
133
|
+
-->
|
|
134
|
+
`;
|
|
135
|
+
fs.writeFileSync(skillsReadmePath, tmpl, "utf-8");
|
|
136
|
+
created.push("skills/README.md");
|
|
137
|
+
} else {
|
|
138
|
+
skipped.push("skills/README.md");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return utils.sendJSON(res, { ok: true, created, skipped });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ----------------------------------------------------------------
|
|
146
|
+
// GET /api/diagnose — 健康检查
|
|
147
|
+
// ----------------------------------------------------------------
|
|
148
|
+
router.get("/api/diagnose", async (req, res) => {
|
|
149
|
+
if (!ctx.skillsSource || !ctx.pkgSkillsSource) {
|
|
150
|
+
return utils.sendJSON(res, { error: "未设置技能目录,请在设置中连接" }, 400);
|
|
151
|
+
}
|
|
152
|
+
const results = [];
|
|
153
|
+
const pkgSkills = core.getPackageSkills(ctx.skillsSource);
|
|
154
|
+
for (const skill of pkgSkills) {
|
|
155
|
+
const skillDir = path.join(ctx.skillsDest, skill.name);
|
|
156
|
+
const issues = [];
|
|
157
|
+
if (!fs.existsSync(skillDir)) {
|
|
158
|
+
issues.push("未安装");
|
|
159
|
+
} else {
|
|
160
|
+
if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) {
|
|
161
|
+
issues.push("缺少 SKILL.md");
|
|
162
|
+
} else if (!core.parseFrontmatter(skillDir)) {
|
|
163
|
+
issues.push("SKILL.md frontmatter 格式异常");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
results.push({
|
|
167
|
+
name: skill.name,
|
|
168
|
+
healthy: issues.length === 0,
|
|
169
|
+
issues,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return utils.sendJSON(res, results);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ----------------------------------------------------------------
|
|
176
|
+
// POST /api/tasks/cancel — 取消正在执行的 npx 操作
|
|
177
|
+
// ----------------------------------------------------------------
|
|
178
|
+
router.post("/api/tasks/cancel", async (req, res) => {
|
|
179
|
+
const body = await utils.parseBody(req);
|
|
180
|
+
if (body.taskId) {
|
|
181
|
+
const proc = ctx.runningTasks.get(body.taskId);
|
|
182
|
+
if (proc) { proc.kill(); ctx.runningTasks.delete(body.taskId); }
|
|
183
|
+
}
|
|
184
|
+
return utils.sendJSON(res, { ok: true });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ----------------------------------------------------------------
|
|
188
|
+
// POST /api/fs/list — 列出指定路径下的子目录
|
|
189
|
+
// ----------------------------------------------------------------
|
|
190
|
+
router.post("/api/fs/list", async (req, res) => {
|
|
191
|
+
const body = await utils.parseBody(req);
|
|
192
|
+
const dirPath = body.path || "";
|
|
193
|
+
let entries = [];
|
|
194
|
+
|
|
195
|
+
if (!dirPath || dirPath === "") {
|
|
196
|
+
if (process.platform === "win32") {
|
|
197
|
+
entries = utils.getWindowsDrives();
|
|
198
|
+
} else {
|
|
199
|
+
entries = [{ name: "/", isDir: true }];
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
204
|
+
entries = items
|
|
205
|
+
.filter((item) => item.isDirectory())
|
|
206
|
+
.map((item) => ({ name: item.name, isDir: true }))
|
|
207
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
} catch {
|
|
209
|
+
return utils.sendJSON(res, { ok: false, error: "无法读取目录" }, 400);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return utils.sendJSON(res, { ok: true, entries, path: dirPath });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ----------------------------------------------------------------
|
|
216
|
+
// POST /api/fs/validate — 验证路径是否为有效技能目录
|
|
217
|
+
// ----------------------------------------------------------------
|
|
218
|
+
router.post("/api/fs/validate", async (req, res) => {
|
|
219
|
+
const body = await utils.parseBody(req);
|
|
220
|
+
const dirPath = body.path;
|
|
221
|
+
if (!dirPath) return utils.sendJSON(res, { error: "缺少路径" }, 400);
|
|
222
|
+
|
|
223
|
+
const skillsDir = path.join(dirPath, ".claude", "skills");
|
|
224
|
+
const valid = fs.existsSync(skillsDir);
|
|
225
|
+
let skillCount = 0;
|
|
226
|
+
if (valid) {
|
|
227
|
+
skillCount = core.listSkillDirs(skillsDir).length;
|
|
228
|
+
}
|
|
229
|
+
return utils.sendJSON(res, {
|
|
230
|
+
ok: true,
|
|
231
|
+
valid,
|
|
232
|
+
hasClaudeDir: valid,
|
|
233
|
+
skillCount,
|
|
234
|
+
path: dirPath,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { register };
|