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