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.
@@ -9,6 +9,20 @@ const { buildContext } = require("../context");
9
9
 
10
10
  const ctx = buildContext(utils);
11
11
 
12
+ // 跨平台打开文件管理器
13
+ function openInExplorer(dirPath) {
14
+ const cmd = process.platform === "win32"
15
+ ? `start "" "${dirPath}"`
16
+ : process.platform === "darwin"
17
+ ? `open "${dirPath}"`
18
+ : `xdg-open "${dirPath}"`;
19
+ execSync(cmd, { stdio: "ignore" });
20
+ }
21
+
22
+ async function pathExists(p) {
23
+ try { await fs.promises.access(p); return true; } catch { return false; }
24
+ }
25
+
12
26
  function requireSource(res) {
13
27
  if (!ctx.skillsSource || !ctx.pkgSkillsSource) {
14
28
  utils.sendJSON(res, { error: "未设置技能目录,请在设置中连接" }, 400);
@@ -17,11 +31,11 @@ function requireSource(res) {
17
31
  return true;
18
32
  }
19
33
 
20
- function cleanPullCache() {
34
+ async function cleanPullCache() {
21
35
  const now = Date.now();
22
36
  for (const [url, entry] of ctx.pullCache) {
23
37
  if (now - entry.createdAt > 10 * 60 * 1000) {
24
- try { fs.rmSync(entry.tmpDir, { recursive: true, force: true }); } catch {}
38
+ try { await fs.promises.rm(entry.tmpDir, { recursive: true, force: true }); } catch {}
25
39
  ctx.pullCache.delete(url);
26
40
  }
27
41
  }
@@ -80,20 +94,20 @@ function register(router) {
80
94
  // ----------------------------------------------------------------
81
95
  router.get("/api/skills", async (req, res) => {
82
96
  if (!requireSource(res)) return;
83
- const pkgSkills = core.getPackageSkills(ctx.skillsSource);
97
+ const pkgSkills = await core.getPackageSkills(ctx.skillsSource);
84
98
  const pkgNames = new Set(pkgSkills.map((s) => s.name));
85
- const userSkills = core.getUserSkills(ctx.claudeDest);
99
+ const userSkills = await core.getUserSkills(ctx.claudeDest);
86
100
  let cleaned = false;
87
101
  for (const us of userSkills) {
88
102
  if (!pkgNames.has(us.name)) {
89
103
  const dest = path.join(ctx.skillsDest, us.name);
90
- if (fs.existsSync(dest)) {
91
- const r = utils.safeRmSync(dest, us.name);
104
+ if (await pathExists(dest)) {
105
+ const r = await utils.safeRm(dest, us.name);
92
106
  if (r.removed) cleaned = true;
93
107
  }
94
108
  }
95
109
  }
96
- const settings = ctx.readSettings();
110
+ const settings = await ctx.readSettings();
97
111
  let settingsChanged = cleaned;
98
112
  for (const name of Object.keys(settings.skills || {})) {
99
113
  if (!pkgNames.has(name)) {
@@ -101,10 +115,10 @@ function register(router) {
101
115
  settingsChanged = true;
102
116
  }
103
117
  }
104
- if (settingsChanged) ctx.writeSettings(settings);
105
- const refreshed = core.getUserSkills(ctx.claudeDest);
118
+ if (settingsChanged) await ctx.writeSettings(settings);
119
+ const refreshed = await core.getUserSkills(ctx.claudeDest);
106
120
  const userMap = new Map(refreshed.map((s) => [s.name, s]));
107
- const pullSource = ctx.readPullSource();
121
+ const pullSource = await ctx.readPullSource();
108
122
  const result = pkgSkills.map((s) => ({
109
123
  ...s,
110
124
  enabled: userMap.has(s.name) ? userMap.get(s.name).enabled : false,
@@ -125,22 +139,22 @@ function register(router) {
125
139
  const src = path.join(ctx.pkgSkillsSource, name);
126
140
  const dest = path.join(ctx.skillsDest, name);
127
141
  if (enabled) {
128
- if (fs.existsSync(src) && !fs.existsSync(dest)) {
129
- utils.copyDirSync(src, dest);
142
+ if (await pathExists(src) && !(await pathExists(dest))) {
143
+ await utils.copyDir(src, dest);
130
144
  }
131
145
  } else {
132
- if (fs.existsSync(dest)) {
133
- const r = utils.safeRmSync(dest, name);
146
+ if (await pathExists(dest)) {
147
+ const r = await utils.safeRm(dest, name);
134
148
  if (!r.removed && r.reason === "locked") {
135
149
  return utils.sendJSON(res, { error: `技能 "${name}" 正被其他程序使用,无法停用` }, 409);
136
150
  }
137
151
  }
138
152
  }
139
- const settings = ctx.readSettings();
153
+ const settings = await ctx.readSettings();
140
154
  if (!settings.skills) settings.skills = {};
141
155
  if (!settings.skills[name]) settings.skills[name] = {};
142
156
  settings.skills[name].enabled = enabled;
143
- ctx.writeSettings(settings);
157
+ await ctx.writeSettings(settings);
144
158
  return utils.sendJSON(res, { ok: true, name, enabled: !!enabled });
145
159
  });
146
160
 
@@ -153,7 +167,7 @@ function register(router) {
153
167
  const { selected } = body;
154
168
  if (!Array.isArray(selected))
155
169
  return utils.sendJSON(res, { error: "参数错误" }, 400);
156
- const result = ctx.installSelectedSkills(selected);
170
+ const result = await ctx.installSelectedSkills(selected);
157
171
  return utils.sendJSON(res, { ok: true, ...result });
158
172
  });
159
173
 
@@ -169,28 +183,28 @@ function register(router) {
169
183
 
170
184
  // ---------- 预览模式 ----------
171
185
  if (!Array.isArray(names) || names.length === 0) {
172
- cleanPullCache();
186
+ await cleanPullCache();
173
187
  const cached = ctx.pullCache.get(url);
174
188
  if (cached) {
175
189
  const skillsDir = path.join(cached.tmpDir, ".claude", "skills");
176
- const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
190
+ const skills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
177
191
  return utils.sendJSON(res, { ok: true, skills, total: skills.length });
178
192
  }
179
193
  const tmpDir = path.join(os.tmpdir(), "mdk-preview-" + Date.now());
180
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
194
+ await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
181
195
  try {
182
- await runAsync(ctx.skillsBin + " add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-pull-preview" });
196
+ await runAsync(ctx.skillsBin + " add " + utils.escapeShellArg(url) + " --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-pull-preview" });
183
197
  } catch (e) {
184
- utils.cleanNpxTemp();
185
- utils.safeRmSync(tmpDir, "临时目录");
198
+ await utils.cleanNpxTemp();
199
+ await utils.safeRm(tmpDir, "临时目录");
186
200
  if (e.killed) return utils.sendJSON(res, { cancelled: true }, 499);
187
201
  return utils.sendJSON(res, { error: "预览失败:" + (e.stderr || e.message) }, 400);
188
202
  }
189
- utils.cleanNpxTemp();
203
+ await utils.cleanNpxTemp();
190
204
  const skillsDir = path.join(tmpDir, ".claude", "skills");
191
- const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
205
+ const skills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
192
206
  if (skills.length === 0) {
193
- utils.safeRmSync(tmpDir, "临时目录");
207
+ await utils.safeRm(tmpDir, "临时目录");
194
208
  return utils.sendJSON(res, { error: "未找到有效技能" }, 400);
195
209
  }
196
210
  ctx.pullCache.set(url, { tmpDir, createdAt: Date.now() });
@@ -208,25 +222,25 @@ function register(router) {
208
222
  const skillsDir = path.join(tmpDir, ".claude", "skills");
209
223
  for (const name of names) {
210
224
  const skillPath = path.join(skillsDir, name);
211
- if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
225
+ if (!(await pathExists(path.join(skillPath, "SKILL.md")))) {
212
226
  skipped.push(name);
213
227
  continue;
214
228
  }
215
229
  const dest = path.join(ctx.pkgSkillsSource, name);
216
- if (fs.existsSync(dest)) {
217
- const r = utils.safeRmSync(dest, name + "(源目录)");
230
+ if (await pathExists(dest)) {
231
+ const r = await utils.safeRm(dest, name + "(源目录)");
218
232
  if (!r.removed && r.reason === "locked") {
219
233
  skipped.push(name);
220
234
  continue;
221
235
  }
222
236
  }
223
- utils.copyDirSync(skillPath, dest);
224
- utils.writeSkillMeta(dest, false);
225
- ctx.addPullSource(name, url);
237
+ await utils.copyDir(skillPath, dest);
238
+ await utils.writeSkillMeta(dest, false);
239
+ await ctx.addPullSource(name, url);
226
240
  imported.push(name);
227
241
  }
228
242
  ctx.pullCache.delete(url);
229
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
243
+ try { await fs.promises.rm(tmpDir, { recursive: true, force: true }); } catch {}
230
244
  if (imported.length === 0) {
231
245
  return utils.sendJSON(res, { error: "所选技能均无效(需包含 SKILL.md)", skipped }, 400);
232
246
  }
@@ -237,9 +251,9 @@ function register(router) {
237
251
  // GET /api/source-names — 获取别名映射
238
252
  // ----------------------------------------------------------------
239
253
  router.get("/api/source-names", async (req, res) => {
240
- const settings = ctx.readSettings();
254
+ const settings = await ctx.readSettings();
241
255
  const sourceNames = settings._sourceNames || {};
242
- const pullSource = ctx.readPullSource();
256
+ const pullSource = await ctx.readPullSource();
243
257
  const knownUrls = [...new Set(Object.values(pullSource).map(v => v.url))].filter(Boolean);
244
258
  return utils.sendJSON(res, { names: sourceNames, knownUrls });
245
259
  });
@@ -247,9 +261,9 @@ function register(router) {
247
261
  // POST /api/source-names — 保存别名映射
248
262
  router.post("/api/source-names", async (req, res) => {
249
263
  const body = await utils.parseBody(req);
250
- const settings = ctx.readSettings();
264
+ const settings = await ctx.readSettings();
251
265
  settings._sourceNames = body.names || {};
252
- ctx.writeSettings(settings);
266
+ await ctx.writeSettings(settings);
253
267
  return utils.sendJSON(res, { ok: true });
254
268
  });
255
269
 
@@ -261,17 +275,17 @@ function register(router) {
261
275
  const body = await utils.parseBody(req);
262
276
  const { names, enabled } = body;
263
277
  if (!Array.isArray(names)) return utils.sendJSON(res, { error: "参数错误" }, 400);
264
- const settings = ctx.readSettings();
278
+ const settings = await ctx.readSettings();
265
279
  if (!settings.skills) settings.skills = {};
266
280
  const locked = [];
267
281
  for (const name of names) {
268
282
  const src = path.join(ctx.pkgSkillsSource, name);
269
283
  const dest = path.join(ctx.skillsDest, name);
270
284
  if (enabled) {
271
- if (fs.existsSync(src) && !fs.existsSync(dest)) utils.copyDirSync(src, dest);
285
+ if (await pathExists(src) && !(await pathExists(dest))) await utils.copyDir(src, dest);
272
286
  } else {
273
- if (fs.existsSync(dest)) {
274
- const r = utils.safeRmSync(dest, name);
287
+ if (await pathExists(dest)) {
288
+ const r = await utils.safeRm(dest, name);
275
289
  if (!r.removed && r.reason === "locked") {
276
290
  locked.push(name);
277
291
  continue;
@@ -286,7 +300,7 @@ function register(router) {
286
300
  settings.skills[name].enabled = false;
287
301
  }
288
302
  }
289
- ctx.writeSettings(settings);
303
+ await ctx.writeSettings(settings);
290
304
  return utils.sendJSON(res, { ok: true, locked });
291
305
  });
292
306
 
@@ -298,14 +312,14 @@ function register(router) {
298
312
  const body = await utils.parseBody(req);
299
313
  const { names } = body;
300
314
  if (!Array.isArray(names) || names.length === 0) return utils.sendJSON(res, { error: "参数错误" }, 400);
301
- const settings = ctx.readSettings();
315
+ const settings = await ctx.readSettings();
302
316
  const locked = [];
303
317
  const deleted = [];
304
318
  for (const name of names) {
305
319
  const sourceDir = path.join(ctx.pkgSkillsSource, name);
306
320
  const destDir = path.join(ctx.skillsDest, name);
307
- const r1 = utils.safeRmSync(sourceDir, name + "(源目录)");
308
- const r2 = utils.safeRmSync(destDir, name + "(项目目录)");
321
+ const r1 = await utils.safeRm(sourceDir, name + "(源目录)");
322
+ const r2 = await utils.safeRm(destDir, name + "(项目目录)");
309
323
  if (r1.reason === "locked" || r2.reason === "locked") {
310
324
  locked.push(name);
311
325
  continue;
@@ -313,7 +327,7 @@ function register(router) {
313
327
  if (settings.skills && settings.skills[name] !== undefined) delete settings.skills[name];
314
328
  deleted.push(name);
315
329
  }
316
- ctx.writeSettings(settings);
330
+ await ctx.writeSettings(settings);
317
331
  return utils.sendJSON(res, { ok: true, deleted, locked });
318
332
  });
319
333
 
@@ -326,7 +340,7 @@ function register(router) {
326
340
  if (url && ctx.pullCache.has(url)) {
327
341
  const entry = ctx.pullCache.get(url);
328
342
  ctx.pullCache.delete(url);
329
- try { fs.rmSync(entry.tmpDir, { recursive: true, force: true }); } catch {}
343
+ try { await fs.promises.rm(entry.tmpDir, { recursive: true, force: true }); } catch {}
330
344
  }
331
345
  return utils.sendJSON(res, { ok: true });
332
346
  });
@@ -337,12 +351,12 @@ function register(router) {
337
351
  router.get("/api/skills/:name/source", async (req, res, params) => {
338
352
  if (!requireSource(res)) return;
339
353
  const name = params.name;
340
- const pullSource = ctx.readPullSource();
354
+ const pullSource = await ctx.readPullSource();
341
355
  if (pullSource[name]) {
342
356
  return utils.sendJSON(res, { type: "remote", ...pullSource[name] });
343
357
  }
344
358
  const skillDir = path.join(ctx.pkgSkillsSource, name);
345
- if (fs.existsSync(skillDir)) {
359
+ if (await pathExists(skillDir)) {
346
360
  return utils.sendJSON(res, { type: "local", path: skillDir });
347
361
  }
348
362
  return utils.sendJSON(res, null);
@@ -354,58 +368,58 @@ function register(router) {
354
368
  router.post("/api/skills/:name/update", async (req, res, params) => {
355
369
  if (!requireSource(res)) return;
356
370
  const name = params.name;
357
- const pullSource = ctx.readPullSource();
371
+ const pullSource = await ctx.readPullSource();
358
372
  const source = pullSource[name];
359
373
  if (!source) return utils.sendJSON(res, { error: "该技能无远程来源,无法更新" }, 400);
360
374
 
361
375
  const tmpDir = path.join(os.tmpdir(), "mdk-update-" + Date.now());
362
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
376
+ await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
363
377
  try {
364
- await runAsync(ctx.skillsBin + " add \"" + source.url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-update-" + name });
378
+ await runAsync(ctx.skillsBin + " add " + utils.escapeShellArg(source.url) + " --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-update-" + name });
365
379
  } catch (e) {
366
- utils.cleanNpxTemp();
367
- utils.safeRmSync(tmpDir, "临时目录");
380
+ await utils.cleanNpxTemp();
381
+ await utils.safeRm(tmpDir, "临时目录");
368
382
  if (e.killed) return utils.sendJSON(res, { cancelled: true }, 499);
369
383
  return utils.sendJSON(res, { error: "重新拉取失败:" + (e.stderr || e.message) }, 400);
370
384
  }
371
- utils.cleanNpxTemp();
385
+ await utils.cleanNpxTemp();
372
386
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
373
- if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
374
- utils.safeRmSync(tmpDir, "临时目录");
387
+ if (!(await pathExists(path.join(skillPath, "SKILL.md")))) {
388
+ await utils.safeRm(tmpDir, "临时目录");
375
389
  return utils.sendJSON(res, { error: "远程仓库中未找到该技能" }, 400);
376
390
  }
377
391
 
378
392
  const dest = path.join(ctx.pkgSkillsSource, name);
379
- const oldFingerprint = utils.calcSkillFingerprint(dest);
380
- const newFingerprint = utils.calcSkillFingerprint(skillPath);
393
+ const oldFingerprint = await utils.calcSkillFingerprint(dest);
394
+ const newFingerprint = await utils.calcSkillFingerprint(skillPath);
381
395
  if (oldFingerprint === newFingerprint) {
382
- utils.safeRmSync(tmpDir, "临时目录");
396
+ await utils.safeRm(tmpDir, "临时目录");
383
397
  return utils.sendJSON(res, { ok: true, updated: false });
384
398
  }
385
399
 
386
- const rSrc = utils.safeRmSync(dest, name + "(源目录)");
400
+ const rSrc = await utils.safeRm(dest, name + "(源目录)");
387
401
  if (!rSrc.removed && rSrc.reason === "locked") {
388
- utils.safeRmSync(tmpDir, "临时目录");
402
+ await utils.safeRm(tmpDir, "临时目录");
389
403
  return utils.sendJSON(res, { error: `技能"${name}"被其他程序占用,无法更新`, locked: true }, 409);
390
404
  }
391
- utils.copyDirSync(skillPath, dest);
405
+ await utils.copyDir(skillPath, dest);
392
406
 
393
407
  const projectSkill = path.join(ctx.skillsDest, name);
394
- if (fs.existsSync(projectSkill)) {
395
- const rProj = utils.safeRmSync(projectSkill, name + "(项目目录)");
408
+ if (await pathExists(projectSkill)) {
409
+ const rProj = await utils.safeRm(projectSkill, name + "(项目目录)");
396
410
  if (!rProj.removed && rProj.reason === "locked") {
397
- utils.safeRmSync(tmpDir, "临时目录");
411
+ await utils.safeRm(tmpDir, "临时目录");
398
412
  return utils.sendJSON(res, { error: `技能"${name}"项目目录被其他程序占用,无法更新`, locked: true }, 409);
399
413
  }
400
- utils.copyDirSync(skillPath, projectSkill);
414
+ await utils.copyDir(skillPath, projectSkill);
401
415
  }
402
416
 
403
- utils.writeSkillMeta(dest, true);
417
+ await utils.writeSkillMeta(dest, true);
404
418
  source.pulledAt = new Date().toISOString();
405
- ctx.writePullSource(pullSource);
419
+ await ctx.writePullSource(pullSource);
406
420
  const siblings = Object.keys(pullSource).filter(k => k !== name && pullSource[k].url === source.url);
407
421
 
408
- utils.safeRmSync(tmpDir, "临时目录");
422
+ await utils.safeRm(tmpDir, "临时目录");
409
423
  return utils.sendJSON(res, { ok: true, name, updatedAt: source.pulledAt, siblings });
410
424
  });
411
425
 
@@ -439,42 +453,42 @@ function register(router) {
439
453
  if (!repo || !skillName) return utils.sendJSON(res, { error: "参数错误" }, 400);
440
454
 
441
455
  const tmpDir = path.join(os.tmpdir(), "mdk-market-" + Date.now());
442
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
456
+ await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
443
457
  try {
444
- await runAsync(ctx.skillsBin + " add " + repo + " --skill \"" + skillName + "\" --copy -y -a claude-code", {
458
+ await runAsync(ctx.skillsBin + " add " + utils.escapeShellArg(repo) + " --skill " + utils.escapeShellArg(skillName) + " --copy -y -a claude-code", {
445
459
  cwd: tmpDir,
446
460
  taskId: "skills-install-" + skillName,
447
461
  });
448
462
  } catch (e) {
449
- utils.cleanNpxTemp();
450
- utils.safeRmSync(tmpDir, "临时目录");
463
+ await utils.cleanNpxTemp();
464
+ await utils.safeRm(tmpDir, "临时目录");
451
465
  if (e.killed) return utils.sendJSON(res, { cancelled: true }, 499);
452
466
  return utils.sendJSON(res, { error: "安装失败:" + (e.stderr || e.message) }, 400);
453
467
  }
454
- utils.cleanNpxTemp();
468
+ await utils.cleanNpxTemp();
455
469
 
456
470
  const skillPath = path.join(tmpDir, ".claude", "skills", skillName);
457
- if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
458
- utils.safeRmSync(tmpDir, "临时目录");
471
+ if (!(await pathExists(path.join(skillPath, "SKILL.md")))) {
472
+ await utils.safeRm(tmpDir, "临时目录");
459
473
  return utils.sendJSON(res, { error: "未找到技能文件" }, 400);
460
474
  }
461
475
 
462
476
  const dest = path.join(ctx.pkgSkillsSource, skillName);
463
- utils.safeRmSync(dest, skillName + "(源目录)");
464
- utils.copyDirSync(skillPath, dest);
465
- utils.writeSkillMeta(dest, true);
477
+ await utils.safeRm(dest, skillName + "(源目录)");
478
+ await utils.copyDir(skillPath, dest);
479
+ await utils.writeSkillMeta(dest, true);
466
480
 
467
481
  const projectSkill = path.join(ctx.skillsDest, skillName);
468
- utils.safeRmSync(projectSkill, skillName + "(项目目录)");
469
- utils.copyDirSync(skillPath, projectSkill);
470
- const settings = ctx.readSettings();
482
+ await utils.safeRm(projectSkill, skillName + "(项目目录)");
483
+ await utils.copyDir(skillPath, projectSkill);
484
+ const settings = await ctx.readSettings();
471
485
  if (!settings.skills) settings.skills = {};
472
486
  settings.skills[skillName] = { enabled: true };
473
- ctx.writeSettings(settings);
487
+ await ctx.writeSettings(settings);
474
488
 
475
- ctx.addPullSource(skillName, repo);
489
+ await ctx.addPullSource(skillName, repo);
476
490
 
477
- utils.safeRmSync(tmpDir, "临时目录");
491
+ await utils.safeRm(tmpDir, "临时目录");
478
492
  return utils.sendJSON(res, { ok: true, name: skillName });
479
493
  });
480
494
 
@@ -490,16 +504,16 @@ function register(router) {
490
504
  }
491
505
 
492
506
  const tmpDir = path.join(os.tmpdir(), "mdk-batch-" + Date.now());
493
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
507
+ await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
494
508
  try {
495
- await runAsync(ctx.skillsBin + " add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-batch" });
509
+ await runAsync(ctx.skillsBin + " add " + utils.escapeShellArg(url) + " --copy -y -a claude-code", { cwd: tmpDir, taskId: "skills-batch" });
496
510
  } catch (e) {
497
- utils.cleanNpxTemp();
498
- utils.safeRmSync(tmpDir, "临时目录");
511
+ await utils.cleanNpxTemp();
512
+ await utils.safeRm(tmpDir, "临时目录");
499
513
  if (e.killed) return utils.sendJSON(res, { cancelled: true }, 499);
500
514
  return utils.sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
501
515
  }
502
- utils.cleanNpxTemp();
516
+ await utils.cleanNpxTemp();
503
517
 
504
518
  const updated = [];
505
519
  const unchanged = [];
@@ -507,47 +521,47 @@ function register(router) {
507
521
  const locked = [];
508
522
  for (const name of names) {
509
523
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
510
- if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
524
+ if (!(await pathExists(path.join(skillPath, "SKILL.md")))) {
511
525
  notFound.push(name);
512
526
  continue;
513
527
  }
514
528
  const dest = path.join(ctx.pkgSkillsSource, name);
515
- const newFingerprint = utils.calcSkillFingerprint(skillPath);
516
- const oldFingerprint = utils.calcSkillFingerprint(dest);
529
+ const newFingerprint = await utils.calcSkillFingerprint(skillPath);
530
+ const oldFingerprint = await utils.calcSkillFingerprint(dest);
517
531
  if (oldFingerprint === newFingerprint) {
518
532
  unchanged.push(name);
519
533
  continue;
520
534
  }
521
- const rSrc = utils.safeRmSync(dest, name + "(源目录)");
535
+ const rSrc = await utils.safeRm(dest, name + "(源目录)");
522
536
  if (!rSrc.removed && rSrc.reason === "locked") {
523
537
  locked.push(name);
524
538
  continue;
525
539
  }
526
- utils.copyDirSync(skillPath, dest);
540
+ await utils.copyDir(skillPath, dest);
527
541
 
528
542
  const projectSkill = path.join(ctx.skillsDest, name);
529
- if (fs.existsSync(projectSkill)) {
530
- const rProj = utils.safeRmSync(projectSkill, name + "(项目目录)");
543
+ if (await pathExists(projectSkill)) {
544
+ const rProj = await utils.safeRm(projectSkill, name + "(项目目录)");
531
545
  if (!rProj.removed && rProj.reason === "locked") {
532
546
  locked.push(name);
533
547
  continue;
534
548
  }
535
- utils.copyDirSync(skillPath, projectSkill);
549
+ await utils.copyDir(skillPath, projectSkill);
536
550
  }
537
551
 
538
- utils.writeSkillMeta(dest, true);
552
+ await utils.writeSkillMeta(dest, true);
539
553
  updated.push(name);
540
554
  }
541
555
 
542
- const pullSource = ctx.readPullSource();
556
+ const pullSource = await ctx.readPullSource();
543
557
  for (const name of updated) {
544
558
  if (pullSource[name]) {
545
559
  pullSource[name].pulledAt = new Date().toISOString();
546
560
  }
547
561
  }
548
- ctx.writePullSource(pullSource);
562
+ await ctx.writePullSource(pullSource);
549
563
 
550
- utils.safeRmSync(tmpDir, "临时目录");
564
+ await utils.safeRm(tmpDir, "临时目录");
551
565
  return utils.sendJSON(res, { ok: true, updated, unchanged, notFound, locked });
552
566
  });
553
567
 
@@ -558,11 +572,11 @@ function register(router) {
558
572
  if (!requireSource(res)) return;
559
573
  const name = params.name;
560
574
  const skillDir = path.join(ctx.pkgSkillsSource, name);
561
- if (!fs.existsSync(skillDir)) {
575
+ if (!(await pathExists(skillDir))) {
562
576
  return utils.sendJSON(res, { error: "技能目录不存在" }, 404);
563
577
  }
564
578
  try {
565
- execSync("start \"\" \"" + skillDir + "\"", { stdio: "ignore" });
579
+ openInExplorer(skillDir);
566
580
  return utils.sendJSON(res, { ok: true });
567
581
  } catch {
568
582
  return utils.sendJSON(res, { error: "打开目录失败" }, 500);
@@ -576,8 +590,8 @@ function register(router) {
576
590
  if (!requireSource(res)) return;
577
591
  const name = params.name;
578
592
  const dest = path.join(ctx.skillsDest, name);
579
- if (!fs.existsSync(dest)) return utils.sendJSON(res, { ok: true, name, skipped: true });
580
- const r = utils.safeRmSync(dest, name);
593
+ if (!(await pathExists(dest))) return utils.sendJSON(res, { ok: true, name, skipped: true });
594
+ const r = await utils.safeRm(dest, name);
581
595
  if (r.removed) return utils.sendJSON(res, { ok: true, name });
582
596
  if (r.reason === "locked") return utils.sendJSON(res, { error: `技能"${name}"被其他程序占用,无法删除`, locked: true }, 409);
583
597
  return utils.sendJSON(res, { error: `删除"${name}"失败: ${r.message || "未知错误"}` }, 500);
@@ -591,13 +605,13 @@ function register(router) {
591
605
  const name = params.name;
592
606
  const src = path.join(ctx.pkgSkillsSource, name);
593
607
  const dest = path.join(ctx.skillsDest, name);
594
- if (fs.existsSync(dest)) {
595
- const r = utils.safeRmSync(dest, name);
608
+ if (await pathExists(dest)) {
609
+ const r = await utils.safeRm(dest, name);
596
610
  if (!r.removed) return utils.sendJSON(res, { error: `无法覆盖现有目录: ${r.reason}` }, r.reason === "locked" ? 409 : 500);
597
611
  }
598
- if (!fs.existsSync(src)) return utils.sendJSON(res, { error: `源目录不存在: ${name}` }, 404);
612
+ if (!(await pathExists(src))) return utils.sendJSON(res, { error: `源目录不存在: ${name}` }, 404);
599
613
  try {
600
- utils.copyDirSync(src, dest);
614
+ await utils.copyDir(src, dest);
601
615
  return utils.sendJSON(res, { ok: true, name });
602
616
  } catch (err) {
603
617
  return utils.sendJSON(res, { error: `安装"${name}"失败: ${err.message}` }, 500);
@@ -610,7 +624,7 @@ function register(router) {
610
624
  router.post("/api/skills-dest/open", async (req, res) => {
611
625
  if (!requireSource(res)) return;
612
626
  try {
613
- execSync("start \"\" \"" + ctx.skillsDest + "\"", { stdio: "ignore" });
627
+ openInExplorer(ctx.skillsDest);
614
628
  return utils.sendJSON(res, { ok: true });
615
629
  } catch {
616
630
  return utils.sendJSON(res, { error: "打开目录失败" }, 500);
@@ -625,11 +639,11 @@ function register(router) {
625
639
  const name = params.name;
626
640
  const skillDir = path.join(ctx.skillsDest, name);
627
641
  const altDir = path.join(ctx.pkgSkillsSource, name);
628
- const readmePath = [
629
- path.join(skillDir, "SKILL.md"),
630
- path.join(altDir, "SKILL.md"),
631
- ].find((p) => fs.existsSync(p));
632
- const content = readmePath ? fs.readFileSync(readmePath, "utf-8") : null;
642
+ let readmePath = null;
643
+ for (const p of [path.join(skillDir, "SKILL.md"), path.join(altDir, "SKILL.md")]) {
644
+ if (await pathExists(p)) { readmePath = p; break; }
645
+ }
646
+ const content = readmePath ? await fs.promises.readFile(readmePath, "utf-8") : null;
633
647
  return utils.sendJSON(res, { content, found: !!readmePath });
634
648
  });
635
649
 
@@ -640,13 +654,13 @@ function register(router) {
640
654
  if (!requireSource(res)) return;
641
655
  const name = params.name;
642
656
  const skillDir = path.join(ctx.pkgSkillsSource, name);
643
- if (!fs.existsSync(skillDir)) {
657
+ if (!(await pathExists(skillDir))) {
644
658
  return utils.sendJSON(res, { error: "技能不存在" }, 404);
645
659
  }
646
660
  const metaPath = path.join(skillDir, ".meta.json");
647
661
  let meta = {};
648
- if (fs.existsSync(metaPath)) {
649
- try { meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch {}
662
+ if (await pathExists(metaPath)) {
663
+ try { meta = JSON.parse(await fs.promises.readFile(metaPath, "utf-8")); } catch {}
650
664
  }
651
665
  const body = await utils.parseBody(req);
652
666
  if (body.version !== undefined) meta.version = String(body.version);
@@ -654,7 +668,7 @@ function register(router) {
654
668
  if (body.tags !== undefined) {
655
669
  meta.tags = Array.isArray(body.tags) ? body.tags.filter((t) => typeof t === "string") : [];
656
670
  }
657
- fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
671
+ await fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
658
672
  return utils.sendJSON(res, { ok: true, meta });
659
673
  });
660
674
 
@@ -669,11 +683,11 @@ function register(router) {
669
683
  let deleted = [];
670
684
  let locked = false;
671
685
 
672
- const r1 = utils.safeRmSync(sourceDir, name + "(源目录)");
686
+ const r1 = await utils.safeRm(sourceDir, name + "(源目录)");
673
687
  if (r1.removed) deleted.push("源目录");
674
688
  else if (r1.reason === "locked") locked = true;
675
689
 
676
- const r2 = utils.safeRmSync(destDir, name + "(项目目录)");
690
+ const r2 = await utils.safeRm(destDir, name + "(项目目录)");
677
691
  if (r2.removed) deleted.push("项目目录");
678
692
  else if (r2.reason === "locked") locked = true;
679
693
 
@@ -681,10 +695,10 @@ function register(router) {
681
695
  return utils.sendJSON(res, { error: `技能"${name}"被其他程序占用,无法删除`, locked: true }, 409);
682
696
  }
683
697
 
684
- const settings = ctx.readSettings();
698
+ const settings = await ctx.readSettings();
685
699
  if (settings.skills && settings.skills[name] !== undefined) {
686
700
  delete settings.skills[name];
687
- ctx.writeSettings(settings);
701
+ await ctx.writeSettings(settings);
688
702
  }
689
703
 
690
704
  if (deleted.length === 0) {