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/scripts/core.js CHANGED
@@ -1,63 +1,80 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
 
4
- // 时间戳格式:YYYY-MM-DD HH:mm:ss
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
- * 追加日志到 .claude/.install.log
20
- * 需要传入 logFile 路径
21
- */
22
- function appendLog(logFile, level, message) {
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 (!fs.existsSync(dir)) {
25
- fs.mkdirSync(dir, { recursive: true });
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.appendFileSync(logFile, line, "utf-8");
46
+ await fs.promises.appendFile(logFile, line, "utf-8");
29
47
  }
30
48
 
31
- /**
32
- * 备份已有目录到 dir/backups/<timestamp>/
33
- * 排除 backups/ 自身,清理超过 3 份的旧备份
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 (!fs.existsSync(backupsDir)) {
40
- fs.mkdirSync(backupsDir, { recursive: true });
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.mkdirSync(bakPath, { recursive: true });
61
+ await fs.promises.mkdir(bakPath, { recursive: true });
46
62
 
47
- for (const item of fs.readdirSync(dir)) {
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
- if (fs.statSync(srcPath).isDirectory()) {
52
- copyDirSync(srcPath, destPath);
68
+ const stat = await fs.promises.stat(srcPath);
69
+ if (stat.isDirectory()) {
70
+ await copyDir(srcPath, destPath);
53
71
  } else {
54
- fs.copyFileSync(srcPath, destPath);
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.rmSync(oldPath, { recursive: true, force: true });
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
- function isPlainObject(val) {
76
- return Object.prototype.toString.call(val) === "[object Object]";
77
- }
92
+ // ---------- 递归拷贝目录(异步,绕开 Windows fs.cpSync 中文路径 bug)----------
78
93
 
79
- /**
80
- * 手动递归拷贝目录,绕开 Windows 中文路径下 fs.cpSync({ recursive: true }) 死锁的 bug
81
- */
82
- function copyDirSync(src, dest) {
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
- if (fs.statSync(srcPath).isDirectory()) {
88
- copyDirSync(srcPath, destPath);
100
+ const stat = await fs.promises.stat(srcPath);
101
+ if (stat.isDirectory()) {
102
+ await copyDir(srcPath, destPath);
89
103
  } else {
90
- fs.copyFileSync(srcPath, destPath);
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
- for (const key of Object.keys(pkgObj)) {
102
- if (!(key in result)) {
103
- result[key] = pkgObj[key];
104
- } else if (isPlainObject(pkgObj[key]) && isPlainObject(result[key])) {
105
- result[key] = deepMerge(result[key], pkgObj[key]);
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 (!fs.existsSync(mdPath)) return null;
128
+ if (!(await pathExists(mdPath))) return null;
130
129
  let content;
131
- try { content = fs.readFileSync(mdPath, "utf-8"); } catch { return null; }
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
- * 优先从 .meta.json 读取;没有或解析失败时,从 SKILL.md frontmatter 兜底。
163
- */
164
- function readMeta(skillDir) {
159
+ // ---------- 读取技能元信息 ----------
160
+
161
+ async function readMeta(skillDir) {
165
162
  const metaPath = path.join(skillDir, ".meta.json");
166
- if (fs.existsSync(metaPath)) {
163
+ if (await pathExists(metaPath)) {
167
164
  try {
168
- return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
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
- function installSkills(skillsSource, skillsDest, logFile) {
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 (!fs.existsSync(skillsDest)) {
189
- fs.mkdirSync(skillsDest, { recursive: true });
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 (!fs.existsSync(destSkill)) {
202
- copyDirSync(srcSkill, destSkill);
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
- copyDirSync(destSkill, bakPath);
213
- fs.rmSync(destSkill, { recursive: true, force: true });
214
- copyDirSync(srcSkill, destSkill);
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
- * 安装 settings.json,深度合并
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 (!fs.existsSync(src)) continue;
235
+ if (!(await pathExists(src))) continue;
241
236
 
242
- if (fs.existsSync(dest)) {
243
- const userData = JSON.parse(fs.readFileSync(dest, "utf-8"));
244
- const pkgData = JSON.parse(fs.readFileSync(src, "utf-8"));
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.writeFileSync(dest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
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.copyFileSync(src, dest);
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
- return listSkillDirs(skillsSource).map((name) => {
261
- const meta = readMeta(path.join(skillsSource, name));
262
- return {
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
- * settings.json 的 _skill_source 字段读取用户指定的本地技能目录
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 (fs.existsSync(settingsPath)) {
273
+ if (await pathExists(settingsPath)) {
280
274
  try {
281
- const s = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
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
- * 获取用户已安装的技能及其 settings 中的 enabled 状态
290
- */
291
- function getUserSkills(claudeDest) {
282
+ // ---------- 获取用户已安装技能 ----------
283
+
284
+ async function getUserSkills(claudeDest) {
292
285
  const skillsDest = path.join(claudeDest, "skills");
293
- if (!fs.existsSync(skillsDest)) return [];
286
+ if (!(await pathExists(skillsDest))) return [];
294
287
 
295
288
  const settingsPath = path.join(claudeDest, "settings.json");
296
289
  let settings = {};
297
- if (fs.existsSync(settingsPath)) {
290
+ if (await pathExists(settingsPath)) {
298
291
  try {
299
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
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
- return listSkillDirs(skillsDest).map((name) => {
308
- const meta = readMeta(path.join(skillsDest, name));
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
- return {
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
- copyDirSync,
320
+ copyDir,
326
321
  deepMerge,
327
322
  listSkillDirs,
328
323
  readMeta,