mdk-skills 2.4.20 → 2.4.21
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 +1 -1
- package/scripts/cli.js +104 -116
- package/scripts/core.js +131 -136
- package/scripts/web-ui/server/context.js +14 -14
- package/scripts/web-ui/server/routes/others.js +4 -4
- package/scripts/web-ui/server/routes/skills.js +17 -7
- package/scripts/web-ui/server/routes/source.js +6 -6
- package/scripts/web-ui/server/utils.js +12 -186
- package/scripts/web-ui/server.js +13 -7
|
@@ -31,50 +31,7 @@ function parseBody(req) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// =====================
|
|
35
|
-
|
|
36
|
-
// 安全删除目录/文件,处理 Windows 文件锁定(EBUSY)
|
|
37
|
-
function safeRmSync(dir, label) {
|
|
38
|
-
if (!fs.existsSync(dir)) return { removed: false, reason: "not_found" };
|
|
39
|
-
try {
|
|
40
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
41
|
-
return { removed: true };
|
|
42
|
-
} catch (err) {
|
|
43
|
-
if (err.code === "EBUSY") {
|
|
44
|
-
console.error(`跳过删除 "${label || dir}"(文件被其他程序占用)`);
|
|
45
|
-
return { removed: false, reason: "locked" };
|
|
46
|
-
}
|
|
47
|
-
console.error(`删除 "${label || dir}" 失败:`, err.message);
|
|
48
|
-
return { removed: false, reason: "error", message: err.message };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 手动递归拷贝目录
|
|
53
|
-
function copyDirSync(src, dest) {
|
|
54
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
55
|
-
for (const item of fs.readdirSync(src)) {
|
|
56
|
-
const srcPath = path.join(src, item);
|
|
57
|
-
const destPath = path.join(dest, item);
|
|
58
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
59
|
-
copyDirSync(srcPath, destPath);
|
|
60
|
-
} else {
|
|
61
|
-
fs.copyFileSync(srcPath, destPath);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// npx skills 会在系统临时目录留下 skills-* 工作目录,统一清理(同步版)
|
|
67
|
-
function cleanNpxTempSync() {
|
|
68
|
-
const tmp = os.tmpdir();
|
|
69
|
-
for (const name of fs.readdirSync(tmp)) {
|
|
70
|
-
if (/^skills-[A-Za-z0-9]+$/.test(name)) {
|
|
71
|
-
const p = path.join(tmp, name);
|
|
72
|
-
try { fs.rmSync(p, { recursive: true, force: true }); } catch {}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ===================== 文件系统(异步版 — Web UI Server 使用)=====================
|
|
34
|
+
// ===================== 文件系统(异步)======================
|
|
78
35
|
|
|
79
36
|
// 安全删除目录/文件,处理 Windows 文件锁定(EBUSY)
|
|
80
37
|
async function safeRm(dir, label) {
|
|
@@ -92,23 +49,7 @@ async function safeRm(dir, label) {
|
|
|
92
49
|
}
|
|
93
50
|
}
|
|
94
51
|
|
|
95
|
-
//
|
|
96
|
-
async function copyDir(src, dest) {
|
|
97
|
-
await fs.promises.mkdir(dest, { recursive: true });
|
|
98
|
-
const items = await fs.promises.readdir(src);
|
|
99
|
-
for (const item of items) {
|
|
100
|
-
const srcPath = path.join(src, item);
|
|
101
|
-
const destPath = path.join(dest, item);
|
|
102
|
-
const stat = await fs.promises.stat(srcPath);
|
|
103
|
-
if (stat.isDirectory()) {
|
|
104
|
-
await copyDir(srcPath, destPath);
|
|
105
|
-
} else {
|
|
106
|
-
await fs.promises.copyFile(srcPath, destPath);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 清理 npx 临时目录(异步版)
|
|
52
|
+
// 清理 npx 临时目录
|
|
112
53
|
async function cleanNpxTemp() {
|
|
113
54
|
const tmp = os.tmpdir();
|
|
114
55
|
let names;
|
|
@@ -121,7 +62,7 @@ async function cleanNpxTemp() {
|
|
|
121
62
|
}
|
|
122
63
|
}
|
|
123
64
|
|
|
124
|
-
// Windows
|
|
65
|
+
// Windows 下获取可用盘符
|
|
125
66
|
async function getWindowsDrives() {
|
|
126
67
|
const drives = [];
|
|
127
68
|
for (let i = 65; i <= 90; i++) {
|
|
@@ -131,7 +72,7 @@ async function getWindowsDrives() {
|
|
|
131
72
|
if (await pathExists(root)) {
|
|
132
73
|
drives.push({ name: root, isDir: true });
|
|
133
74
|
}
|
|
134
|
-
} catch {
|
|
75
|
+
} catch {}
|
|
135
76
|
}
|
|
136
77
|
return drives;
|
|
137
78
|
}
|
|
@@ -183,28 +124,7 @@ function parseSkillsFindOutput(stdout) {
|
|
|
183
124
|
|
|
184
125
|
// ===================== 指纹 & 元信息 =====================
|
|
185
126
|
|
|
186
|
-
//
|
|
187
|
-
function calcSkillFingerprintSync(dir) {
|
|
188
|
-
if (!fs.existsSync(dir)) return "";
|
|
189
|
-
const hash = crypto.createHash("md5");
|
|
190
|
-
const walk = (d) => {
|
|
191
|
-
for (const item of fs.readdirSync(d).sort()) {
|
|
192
|
-
const p = path.join(d, item);
|
|
193
|
-
if (item === ".meta.json") continue;
|
|
194
|
-
const stat = fs.statSync(p);
|
|
195
|
-
if (stat.isDirectory()) {
|
|
196
|
-
walk(p);
|
|
197
|
-
} else {
|
|
198
|
-
hash.update(item);
|
|
199
|
-
hash.update(fs.readFileSync(p));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
walk(dir);
|
|
204
|
-
return hash.digest("hex");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 计算技能目录的文件指纹(异步版)
|
|
127
|
+
// 计算技能目录的文件指纹(MD5,排除 .meta.json)
|
|
208
128
|
async function calcSkillFingerprint(dir) {
|
|
209
129
|
if (!(await pathExists(dir))) return "";
|
|
210
130
|
const hash = crypto.createHash("md5");
|
|
@@ -232,7 +152,7 @@ async function calcSkillFingerprint(dir) {
|
|
|
232
152
|
|
|
233
153
|
// 写入技能元信息(异步版)
|
|
234
154
|
async function writeSkillMeta(dest, wasUpdated) {
|
|
235
|
-
const fm = core.parseFrontmatter(dest);
|
|
155
|
+
const fm = await core.parseFrontmatter(dest);
|
|
236
156
|
if (!fm) return;
|
|
237
157
|
const metaPath = path.join(dest, ".meta.json");
|
|
238
158
|
let oldMeta = {};
|
|
@@ -255,103 +175,14 @@ async function writeSkillMeta(dest, wasUpdated) {
|
|
|
255
175
|
await fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
256
176
|
}
|
|
257
177
|
|
|
258
|
-
// 写入技能元信息(同步版)
|
|
259
|
-
function writeSkillMetaSync(dest, wasUpdated) {
|
|
260
|
-
const fm = core.parseFrontmatter(dest);
|
|
261
|
-
if (!fm) return;
|
|
262
|
-
const metaPath = path.join(dest, ".meta.json");
|
|
263
|
-
let oldMeta = {};
|
|
264
|
-
if (fs.existsSync(metaPath)) {
|
|
265
|
-
try { oldMeta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch {}
|
|
266
|
-
}
|
|
267
|
-
const hasUpstream = !!fm.version;
|
|
268
|
-
const meta = {
|
|
269
|
-
version: fm.version || "1.0.0",
|
|
270
|
-
description: fm.description || "",
|
|
271
|
-
tags: fm.tags || [],
|
|
272
|
-
};
|
|
273
|
-
if (hasUpstream) {
|
|
274
|
-
meta._updateCount = 0;
|
|
275
|
-
} else if (wasUpdated) {
|
|
276
|
-
meta._updateCount = (oldMeta._updateCount || 0) + 1;
|
|
277
|
-
} else {
|
|
278
|
-
meta._updateCount = oldMeta._updateCount || 0;
|
|
279
|
-
}
|
|
280
|
-
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
178
|
// ===================== 幽灵技能清理 =====================
|
|
284
179
|
|
|
285
|
-
// 幽灵技能清理(同步版)
|
|
286
|
-
function cleanGhostSkillsSync(sourcePath) {
|
|
287
|
-
const claudeDir = path.join(sourcePath, ".claude");
|
|
288
|
-
const skillsDir = path.join(claudeDir, "skills");
|
|
289
|
-
if (!fs.existsSync(skillsDir)) return { cleaned: false, reason: "skills 目录不存在" };
|
|
290
|
-
|
|
291
|
-
const actualSkills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
|
|
292
|
-
const skillSet = new Set(actualSkills);
|
|
293
|
-
const cleaned = { profiles: 0, alwaysApply: 0, settings: 0, removed: [] };
|
|
294
|
-
|
|
295
|
-
const profilesPath = path.join(claudeDir, "profiles.json");
|
|
296
|
-
if (fs.existsSync(profilesPath)) {
|
|
297
|
-
try {
|
|
298
|
-
const profilesData = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
|
|
299
|
-
let changed = false;
|
|
300
|
-
for (const profile of profilesData.profiles || []) {
|
|
301
|
-
if (!profile.skills || !Array.isArray(profile.skills)) continue;
|
|
302
|
-
const filtered = profile.skills.filter((s) => {
|
|
303
|
-
const exists = skillSet.has(s);
|
|
304
|
-
if (!exists) { cleaned.removed.push(s); changed = true; }
|
|
305
|
-
return exists;
|
|
306
|
-
});
|
|
307
|
-
if (changed) profile.skills = filtered;
|
|
308
|
-
}
|
|
309
|
-
if (changed) {
|
|
310
|
-
cleaned.profiles++;
|
|
311
|
-
fs.writeFileSync(profilesPath, JSON.stringify(profilesData, null, 2) + "\n", "utf-8");
|
|
312
|
-
}
|
|
313
|
-
} catch {}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const settingsPathInRepo = path.join(claudeDir, "settings.json");
|
|
317
|
-
if (fs.existsSync(settingsPathInRepo)) {
|
|
318
|
-
try {
|
|
319
|
-
const settings = JSON.parse(fs.readFileSync(settingsPathInRepo, "utf-8"));
|
|
320
|
-
let changed = false;
|
|
321
|
-
if (Array.isArray(settings.always_apply_skills)) {
|
|
322
|
-
const filtered = settings.always_apply_skills.filter((s) => {
|
|
323
|
-
const exists = skillSet.has(s);
|
|
324
|
-
if (!exists) { if (!cleaned.removed.includes(s)) cleaned.removed.push(s); changed = true; }
|
|
325
|
-
return exists;
|
|
326
|
-
});
|
|
327
|
-
if (changed) settings.always_apply_skills = filtered;
|
|
328
|
-
}
|
|
329
|
-
if (settings.skills && typeof settings.skills === "object") {
|
|
330
|
-
for (const name of Object.keys(settings.skills)) {
|
|
331
|
-
if (!skillSet.has(name)) {
|
|
332
|
-
delete settings.skills[name];
|
|
333
|
-
if (!cleaned.removed.includes(name)) cleaned.removed.push(name);
|
|
334
|
-
changed = true;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (changed) {
|
|
339
|
-
cleaned.settings++;
|
|
340
|
-
fs.writeFileSync(settingsPathInRepo, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
341
|
-
}
|
|
342
|
-
} catch {}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return cleaned;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 幽灵技能清理(异步版)
|
|
349
180
|
async function cleanGhostSkills(sourcePath) {
|
|
350
181
|
const claudeDir = path.join(sourcePath, ".claude");
|
|
351
182
|
const skillsDir = path.join(claudeDir, "skills");
|
|
352
183
|
if (!(await pathExists(skillsDir))) return { cleaned: false, reason: "skills 目录不存在" };
|
|
353
184
|
|
|
354
|
-
const actualSkills = await pathExists(skillsDir) ? core.listSkillDirs(skillsDir) : [];
|
|
185
|
+
const actualSkills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
|
|
355
186
|
const skillSet = new Set(actualSkills);
|
|
356
187
|
const cleaned = { profiles: 0, alwaysApply: 0, settings: 0, removed: [] };
|
|
357
188
|
|
|
@@ -412,25 +243,20 @@ module.exports = {
|
|
|
412
243
|
// HTTP
|
|
413
244
|
sendJSON,
|
|
414
245
|
parseBody,
|
|
415
|
-
//
|
|
416
|
-
safeRmSync,
|
|
417
|
-
copyDirSync,
|
|
418
|
-
cleanNpxTempSync,
|
|
419
|
-
calcSkillFingerprintSync,
|
|
420
|
-
writeSkillMetaSync,
|
|
421
|
-
cleanGhostSkillsSync,
|
|
422
|
-
// 异步版
|
|
246
|
+
// 文件系统
|
|
423
247
|
pathExists,
|
|
424
248
|
safeRm,
|
|
425
|
-
copyDir,
|
|
249
|
+
copyDir: core.copyDir,
|
|
426
250
|
cleanNpxTemp,
|
|
427
251
|
getWindowsDrives,
|
|
428
|
-
//
|
|
252
|
+
// 技能解析
|
|
429
253
|
stripAnsi,
|
|
430
254
|
escapeShellArg,
|
|
431
255
|
parseInstallCount,
|
|
432
256
|
parseSkillsFindOutput,
|
|
257
|
+
// 指纹 & 元信息
|
|
433
258
|
calcSkillFingerprint,
|
|
434
259
|
writeSkillMeta,
|
|
260
|
+
// 幽灵清理
|
|
435
261
|
cleanGhostSkills,
|
|
436
262
|
};
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs = require("fs");
|
|
|
8
8
|
|
|
9
9
|
const { Router } = require("./server/router");
|
|
10
10
|
const utils = require("./server/utils");
|
|
11
|
+
const { refreshSource } = require("./server/context");
|
|
11
12
|
|
|
12
13
|
// ===================== 路由注册 =====================
|
|
13
14
|
|
|
@@ -136,14 +137,19 @@ async function startDev() {
|
|
|
136
137
|
|
|
137
138
|
// ===================== 启动 =====================
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
async function start() {
|
|
141
|
+
await refreshSource();
|
|
142
|
+
const distDir = path.join(__dirname, "dist");
|
|
143
|
+
let stats;
|
|
144
|
+
try { stats = await fs.promises.stat(distDir); } catch {}
|
|
145
|
+
if (stats) {
|
|
142
146
|
startProduction(distDir);
|
|
143
147
|
} else {
|
|
144
|
-
startDev()
|
|
145
|
-
console.error("启动失败:", err);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
});
|
|
148
|
+
await startDev();
|
|
148
149
|
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
start().catch((err) => {
|
|
153
|
+
console.error("启动失败:", err);
|
|
154
|
+
process.exit(1);
|
|
149
155
|
});
|