mdk-skills 2.4.19 → 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 -130
- package/scripts/core.js +131 -136
- package/scripts/web-ui/server/context.js +62 -58
- package/scripts/web-ui/server/routes/others.js +29 -19
- package/scripts/web-ui/server/routes/profiles.js +16 -12
- package/scripts/web-ui/server/routes/skills.js +139 -125
- package/scripts/web-ui/server/routes/source.js +52 -48
- package/scripts/web-ui/server/utils.js +56 -52
- package/scripts/web-ui/server.js +40 -16
package/scripts/core.js
CHANGED
|
@@ -1,63 +1,80 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// ---------- 纯函数(无需 I/O,保持同步)----------
|
|
5
|
+
|
|
5
6
|
function timestamp() {
|
|
6
7
|
const d = new Date();
|
|
7
8
|
const pad = (n) => String(n).padStart(2, "0");
|
|
8
9
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
// 紧凑时间戳用于备份目录名:YYYYMMDD.HHmmss
|
|
12
12
|
function backupTimestamp() {
|
|
13
13
|
const d = new Date();
|
|
14
14
|
const pad = (n) => String(n).padStart(2, "0");
|
|
15
15
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}.${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function
|
|
18
|
+
function isPlainObject(val) {
|
|
19
|
+
return Object.prototype.toString.call(val) === "[object Object]";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function deepMerge(userObj, pkgObj) {
|
|
23
|
+
const result = { ...userObj };
|
|
24
|
+
for (const key of Object.keys(pkgObj)) {
|
|
25
|
+
if (!(key in result)) {
|
|
26
|
+
result[key] = pkgObj[key];
|
|
27
|
+
} else if (isPlainObject(pkgObj[key]) && isPlainObject(result[key])) {
|
|
28
|
+
result[key] = deepMerge(result[key], pkgObj[key]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function pathExists(p) {
|
|
35
|
+
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------- 日志 ----------
|
|
39
|
+
|
|
40
|
+
async function appendLog(logFile, level, message) {
|
|
23
41
|
const dir = path.dirname(logFile);
|
|
24
|
-
if (!
|
|
25
|
-
fs.
|
|
42
|
+
if (!(await pathExists(dir))) {
|
|
43
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
26
44
|
}
|
|
27
45
|
const line = `[${timestamp()}] [${level}] ${message}\n`;
|
|
28
|
-
fs.
|
|
46
|
+
await fs.promises.appendFile(logFile, line, "utf-8");
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function backupDir(dir) {
|
|
36
|
-
if (!fs.existsSync(dir)) return null;
|
|
49
|
+
// ---------- 备份 ----------
|
|
50
|
+
|
|
51
|
+
async function backupDir(dir) {
|
|
52
|
+
if (!(await pathExists(dir))) return null;
|
|
37
53
|
|
|
38
54
|
const backupsDir = path.join(dir, "backups");
|
|
39
|
-
if (!
|
|
40
|
-
fs.
|
|
55
|
+
if (!(await pathExists(backupsDir))) {
|
|
56
|
+
await fs.promises.mkdir(backupsDir, { recursive: true });
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
const ts = backupTimestamp();
|
|
44
60
|
const bakPath = path.join(backupsDir, ts);
|
|
45
|
-
fs.
|
|
61
|
+
await fs.promises.mkdir(bakPath, { recursive: true });
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
const items = await fs.promises.readdir(dir);
|
|
64
|
+
for (const item of items) {
|
|
48
65
|
if (item === "backups") continue;
|
|
49
66
|
const srcPath = path.join(dir, item);
|
|
50
67
|
const destPath = path.join(bakPath, item);
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
const stat = await fs.promises.stat(srcPath);
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
await copyDir(srcPath, destPath);
|
|
53
71
|
} else {
|
|
54
|
-
fs.
|
|
72
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
// 清理旧备份,只保留最近 3 份
|
|
59
|
-
const backups = fs
|
|
60
|
-
.readdirSync(backupsDir)
|
|
77
|
+
const backups = (await fs.promises.readdir(backupsDir))
|
|
61
78
|
.filter((name) => /^\d{8}\.\d{6}$/.test(name))
|
|
62
79
|
.sort()
|
|
63
80
|
.reverse();
|
|
@@ -65,70 +82,52 @@ function backupDir(dir) {
|
|
|
65
82
|
if (backups.length > 3) {
|
|
66
83
|
for (const old of backups.slice(3)) {
|
|
67
84
|
const oldPath = path.join(backupsDir, old);
|
|
68
|
-
fs.
|
|
85
|
+
await fs.promises.rm(oldPath, { recursive: true, force: true });
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
return bakPath;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
return Object.prototype.toString.call(val) === "[object Object]";
|
|
77
|
-
}
|
|
92
|
+
// ---------- 递归拷贝目录(异步,绕开 Windows fs.cpSync 中文路径 bug)----------
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
84
|
-
for (const item of fs.readdirSync(src)) {
|
|
94
|
+
async function copyDir(src, dest) {
|
|
95
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
96
|
+
const items = await fs.promises.readdir(src);
|
|
97
|
+
for (const item of items) {
|
|
85
98
|
const srcPath = path.join(src, item);
|
|
86
99
|
const destPath = path.join(dest, item);
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
const stat = await fs.promises.stat(srcPath);
|
|
101
|
+
if (stat.isDirectory()) {
|
|
102
|
+
await copyDir(srcPath, destPath);
|
|
89
103
|
} else {
|
|
90
|
-
fs.
|
|
104
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
* 递归深合并,用户值优先,目标新增字段补充
|
|
97
|
-
*/
|
|
98
|
-
function deepMerge(userObj, pkgObj) {
|
|
99
|
-
const result = { ...userObj };
|
|
109
|
+
// ---------- 列出技能目录 ----------
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
async function listSkillDirs(skillsPath) {
|
|
112
|
+
if (!(await pathExists(skillsPath))) return [];
|
|
113
|
+
const names = await fs.promises.readdir(skillsPath);
|
|
114
|
+
const result = [];
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
if (name.startsWith(".")) continue;
|
|
117
|
+
const fullPath = path.join(skillsPath, name);
|
|
118
|
+
const stat = await fs.promises.stat(fullPath);
|
|
119
|
+
if (stat.isDirectory()) result.push(name);
|
|
107
120
|
}
|
|
108
|
-
|
|
109
121
|
return result;
|
|
110
122
|
}
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
* 列出技能目录下的有效技能目录(排除非目录项和隐藏目录)
|
|
114
|
-
*/
|
|
115
|
-
function listSkillDirs(skillsPath) {
|
|
116
|
-
if (!fs.existsSync(skillsPath)) return [];
|
|
117
|
-
return fs.readdirSync(skillsPath).filter((name) => {
|
|
118
|
-
const fullPath = path.join(skillsPath, name);
|
|
119
|
-
return fs.statSync(fullPath).isDirectory() && !name.startsWith(".");
|
|
120
|
-
});
|
|
121
|
-
}
|
|
124
|
+
// ---------- 解析 SKILL.md frontmatter ----------
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
* 解析 SKILL.md 的 YAML frontmatter
|
|
125
|
-
* 返回 { name?, description?, version?, tags? },解析失败返回 null
|
|
126
|
-
*/
|
|
127
|
-
function parseFrontmatter(skillDir) {
|
|
126
|
+
async function parseFrontmatter(skillDir) {
|
|
128
127
|
const mdPath = path.join(skillDir, "SKILL.md");
|
|
129
|
-
if (!
|
|
128
|
+
if (!(await pathExists(mdPath))) return null;
|
|
130
129
|
let content;
|
|
131
|
-
try { content = fs.
|
|
130
|
+
try { content = await fs.promises.readFile(mdPath, "utf-8"); } catch { return null; }
|
|
132
131
|
content = content.replace(/\r\n/g, "\n");
|
|
133
132
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
134
133
|
if (!match) return null;
|
|
@@ -157,18 +156,16 @@ function parseFrontmatter(skillDir) {
|
|
|
157
156
|
return result;
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*/
|
|
164
|
-
function readMeta(skillDir) {
|
|
159
|
+
// ---------- 读取技能元信息 ----------
|
|
160
|
+
|
|
161
|
+
async function readMeta(skillDir) {
|
|
165
162
|
const metaPath = path.join(skillDir, ".meta.json");
|
|
166
|
-
if (
|
|
163
|
+
if (await pathExists(metaPath)) {
|
|
167
164
|
try {
|
|
168
|
-
return JSON.parse(fs.
|
|
165
|
+
return JSON.parse(await fs.promises.readFile(metaPath, "utf-8"));
|
|
169
166
|
} catch {}
|
|
170
167
|
}
|
|
171
|
-
const fm = parseFrontmatter(skillDir);
|
|
168
|
+
const fm = await parseFrontmatter(skillDir);
|
|
172
169
|
if (fm) {
|
|
173
170
|
return {
|
|
174
171
|
version: fm.version || "1.0.0",
|
|
@@ -179,43 +176,42 @@ function readMeta(skillDir) {
|
|
|
179
176
|
return null;
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!fs.existsSync(skillsSource)) return [];
|
|
179
|
+
// ---------- 安装技能 ----------
|
|
180
|
+
|
|
181
|
+
async function installSkills(skillsSource, skillsDest, logFile) {
|
|
182
|
+
if (!(await pathExists(skillsSource))) return [];
|
|
187
183
|
|
|
188
|
-
if (!
|
|
189
|
-
fs.
|
|
184
|
+
if (!(await pathExists(skillsDest))) {
|
|
185
|
+
await fs.promises.mkdir(skillsDest, { recursive: true });
|
|
190
186
|
}
|
|
191
187
|
|
|
192
|
-
const skillDirs = listSkillDirs(skillsSource);
|
|
188
|
+
const skillDirs = await listSkillDirs(skillsSource);
|
|
193
189
|
const results = [];
|
|
194
190
|
|
|
195
191
|
for (const dir of skillDirs) {
|
|
196
192
|
const srcSkill = path.join(skillsSource, dir);
|
|
197
193
|
const destSkill = path.join(skillsDest, dir);
|
|
198
|
-
const meta = readMeta(srcSkill);
|
|
194
|
+
const meta = await readMeta(srcSkill);
|
|
199
195
|
const version = meta ? meta.version : "?";
|
|
200
196
|
|
|
201
|
-
if (!
|
|
202
|
-
|
|
203
|
-
appendLog(logFile, "SKILL", `${dir} (${version}) → installed`);
|
|
197
|
+
if (!(await pathExists(destSkill))) {
|
|
198
|
+
await copyDir(srcSkill, destSkill);
|
|
199
|
+
await appendLog(logFile, "SKILL", `${dir} (${version}) → installed`);
|
|
204
200
|
results.push({ name: dir, version, action: "installed" });
|
|
205
201
|
} else {
|
|
206
|
-
const destMeta = readMeta(destSkill);
|
|
202
|
+
const destMeta = await readMeta(destSkill);
|
|
207
203
|
const needUpdate = meta && destMeta && meta.version !== destMeta.version;
|
|
208
204
|
|
|
209
205
|
if (needUpdate) {
|
|
210
206
|
const bakName = `${dir}.bak.${backupTimestamp()}`;
|
|
211
207
|
const bakPath = path.join(skillsDest, bakName);
|
|
212
|
-
|
|
213
|
-
fs.
|
|
214
|
-
|
|
215
|
-
appendLog(logFile, "SKILL", `${dir} (${destMeta.version} → ${meta.version}) → updated`);
|
|
208
|
+
await copyDir(destSkill, bakPath);
|
|
209
|
+
await fs.promises.rm(destSkill, { recursive: true, force: true });
|
|
210
|
+
await copyDir(srcSkill, destSkill);
|
|
211
|
+
await appendLog(logFile, "SKILL", `${dir} (${destMeta.version} → ${meta.version}) → updated`);
|
|
216
212
|
results.push({ name: dir, version: meta.version, action: "updated" });
|
|
217
213
|
} else {
|
|
218
|
-
appendLog(logFile, "SKILL", `${dir} (${version}) → skipped (exists)`);
|
|
214
|
+
await appendLog(logFile, "SKILL", `${dir} (${version}) → skipped (exists)`);
|
|
219
215
|
results.push({ name: dir, version, action: "skipped" });
|
|
220
216
|
}
|
|
221
217
|
}
|
|
@@ -224,10 +220,9 @@ function installSkills(skillsSource, skillsDest, logFile) {
|
|
|
224
220
|
return results;
|
|
225
221
|
}
|
|
226
222
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
function installSettings(sourceDir, destDir, logFile) {
|
|
223
|
+
// ---------- 安装 settings.json ----------
|
|
224
|
+
|
|
225
|
+
async function installSettings(sourceDir, destDir, logFile) {
|
|
231
226
|
const settingsSource = path.join(sourceDir, "settings.json");
|
|
232
227
|
const settingsDest = path.join(destDir, "settings.json");
|
|
233
228
|
const localSource = path.join(sourceDir, "settings.local.json");
|
|
@@ -237,84 +232,84 @@ function installSettings(sourceDir, destDir, logFile) {
|
|
|
237
232
|
{ src: settingsSource, dest: settingsDest, name: "settings.json" },
|
|
238
233
|
{ src: localSource, dest: localDest, name: "settings.local.json" },
|
|
239
234
|
]) {
|
|
240
|
-
if (!
|
|
235
|
+
if (!(await pathExists(src))) continue;
|
|
241
236
|
|
|
242
|
-
if (
|
|
243
|
-
const userData = JSON.parse(fs.
|
|
244
|
-
const pkgData = JSON.parse(fs.
|
|
237
|
+
if (await pathExists(dest)) {
|
|
238
|
+
const userData = JSON.parse(await fs.promises.readFile(dest, "utf-8"));
|
|
239
|
+
const pkgData = JSON.parse(await fs.promises.readFile(src, "utf-8"));
|
|
245
240
|
const merged = deepMerge(userData, pkgData);
|
|
246
|
-
fs.
|
|
247
|
-
appendLog(logFile, "SETTINGS", `${name} merged`);
|
|
241
|
+
await fs.promises.writeFile(dest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
242
|
+
await appendLog(logFile, "SETTINGS", `${name} merged`);
|
|
248
243
|
} else {
|
|
249
|
-
fs.
|
|
250
|
-
appendLog(logFile, "SETTINGS", `${name} created`);
|
|
244
|
+
await fs.promises.copyFile(src, dest);
|
|
245
|
+
await appendLog(logFile, "SETTINGS", `${name} created`);
|
|
251
246
|
}
|
|
252
247
|
}
|
|
253
248
|
}
|
|
254
249
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
function getPackageSkills(packageDir) {
|
|
250
|
+
// ---------- 获取包内技能列表 ----------
|
|
251
|
+
|
|
252
|
+
async function getPackageSkills(packageDir) {
|
|
259
253
|
const skillsSource = path.join(packageDir, ".claude", "skills");
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
const names = await listSkillDirs(skillsSource);
|
|
255
|
+
const result = [];
|
|
256
|
+
for (const name of names) {
|
|
257
|
+
const meta = await readMeta(path.join(skillsSource, name));
|
|
258
|
+
result.push({
|
|
263
259
|
name,
|
|
264
260
|
version: meta ? meta.version : "?",
|
|
265
261
|
description: meta ? meta.description : "",
|
|
266
262
|
tags: meta ? meta.tags : [],
|
|
267
263
|
_updateCount: meta ? (meta._updateCount || 0) : 0,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
* 没有设置则返回 null
|
|
276
|
-
*/
|
|
277
|
-
function getSkillsSource(projectRoot) {
|
|
269
|
+
// ---------- 获取技能源路径 ----------
|
|
270
|
+
|
|
271
|
+
async function getSkillsSource(projectRoot) {
|
|
278
272
|
const settingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
279
|
-
if (
|
|
273
|
+
if (await pathExists(settingsPath)) {
|
|
280
274
|
try {
|
|
281
|
-
const s = JSON.parse(fs.
|
|
275
|
+
const s = JSON.parse(await fs.promises.readFile(settingsPath, "utf-8"));
|
|
282
276
|
if (s._skill_source) return s._skill_source;
|
|
283
277
|
} catch {}
|
|
284
278
|
}
|
|
285
279
|
return null;
|
|
286
280
|
}
|
|
287
281
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
function getUserSkills(claudeDest) {
|
|
282
|
+
// ---------- 获取用户已安装技能 ----------
|
|
283
|
+
|
|
284
|
+
async function getUserSkills(claudeDest) {
|
|
292
285
|
const skillsDest = path.join(claudeDest, "skills");
|
|
293
|
-
if (!
|
|
286
|
+
if (!(await pathExists(skillsDest))) return [];
|
|
294
287
|
|
|
295
288
|
const settingsPath = path.join(claudeDest, "settings.json");
|
|
296
289
|
let settings = {};
|
|
297
|
-
if (
|
|
290
|
+
if (await pathExists(settingsPath)) {
|
|
298
291
|
try {
|
|
299
|
-
settings = JSON.parse(fs.
|
|
292
|
+
settings = JSON.parse(await fs.promises.readFile(settingsPath, "utf-8"));
|
|
300
293
|
} catch {
|
|
301
294
|
settings = {};
|
|
302
295
|
}
|
|
303
296
|
}
|
|
304
297
|
|
|
305
298
|
const skillConfigs = settings.skills || {};
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
299
|
+
const names = await listSkillDirs(skillsDest);
|
|
300
|
+
const result = [];
|
|
301
|
+
for (const name of names) {
|
|
302
|
+
const meta = await readMeta(path.join(skillsDest, name));
|
|
309
303
|
const config = skillConfigs[name] || {};
|
|
310
|
-
|
|
304
|
+
result.push({
|
|
311
305
|
name,
|
|
312
306
|
version: meta ? meta.version : "?",
|
|
313
307
|
description: meta ? meta.description : config.description || "",
|
|
314
308
|
tags: meta ? meta.tags : [],
|
|
315
309
|
enabled: config.enabled !== false,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
318
313
|
}
|
|
319
314
|
|
|
320
315
|
module.exports = {
|
|
@@ -322,7 +317,7 @@ module.exports = {
|
|
|
322
317
|
backupTimestamp,
|
|
323
318
|
appendLog,
|
|
324
319
|
backupDir,
|
|
325
|
-
|
|
320
|
+
copyDir,
|
|
326
321
|
deepMerge,
|
|
327
322
|
listSkillDirs,
|
|
328
323
|
readMeta,
|