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
|
@@ -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.
|
|
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 (
|
|
91
|
-
const r = utils.
|
|
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 (
|
|
129
|
-
utils.
|
|
142
|
+
if (await pathExists(src) && !(await pathExists(dest))) {
|
|
143
|
+
await utils.copyDir(src, dest);
|
|
130
144
|
}
|
|
131
145
|
} else {
|
|
132
|
-
if (
|
|
133
|
-
const r = utils.
|
|
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 =
|
|
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.
|
|
194
|
+
await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
181
195
|
try {
|
|
182
|
-
await runAsync(ctx.skillsBin + " add
|
|
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.
|
|
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 =
|
|
205
|
+
const skills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
|
|
192
206
|
if (skills.length === 0) {
|
|
193
|
-
utils.
|
|
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 (!
|
|
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 (
|
|
217
|
-
const r = utils.
|
|
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.
|
|
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.
|
|
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 (
|
|
285
|
+
if (await pathExists(src) && !(await pathExists(dest))) await utils.copyDir(src, dest);
|
|
272
286
|
} else {
|
|
273
|
-
if (
|
|
274
|
-
const r = utils.
|
|
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.
|
|
308
|
-
const r2 = utils.
|
|
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.
|
|
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 (
|
|
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.
|
|
376
|
+
await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
363
377
|
try {
|
|
364
|
-
await runAsync(ctx.skillsBin + " add
|
|
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.
|
|
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 (!
|
|
374
|
-
utils.
|
|
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.
|
|
396
|
+
await utils.safeRm(tmpDir, "临时目录");
|
|
383
397
|
return utils.sendJSON(res, { ok: true, updated: false });
|
|
384
398
|
}
|
|
385
399
|
|
|
386
|
-
const rSrc = utils.
|
|
400
|
+
const rSrc = await utils.safeRm(dest, name + "(源目录)");
|
|
387
401
|
if (!rSrc.removed && rSrc.reason === "locked") {
|
|
388
|
-
utils.
|
|
402
|
+
await utils.safeRm(tmpDir, "临时目录");
|
|
389
403
|
return utils.sendJSON(res, { error: `技能"${name}"被其他程序占用,无法更新`, locked: true }, 409);
|
|
390
404
|
}
|
|
391
|
-
utils.
|
|
405
|
+
await utils.copyDir(skillPath, dest);
|
|
392
406
|
|
|
393
407
|
const projectSkill = path.join(ctx.skillsDest, name);
|
|
394
|
-
if (
|
|
395
|
-
const rProj = utils.
|
|
408
|
+
if (await pathExists(projectSkill)) {
|
|
409
|
+
const rProj = await utils.safeRm(projectSkill, name + "(项目目录)");
|
|
396
410
|
if (!rProj.removed && rProj.reason === "locked") {
|
|
397
|
-
utils.
|
|
411
|
+
await utils.safeRm(tmpDir, "临时目录");
|
|
398
412
|
return utils.sendJSON(res, { error: `技能"${name}"项目目录被其他程序占用,无法更新`, locked: true }, 409);
|
|
399
413
|
}
|
|
400
|
-
utils.
|
|
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.
|
|
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.
|
|
456
|
+
await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
443
457
|
try {
|
|
444
|
-
await runAsync(ctx.skillsBin + " add " + repo + " --skill
|
|
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.
|
|
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 (!
|
|
458
|
-
utils.
|
|
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.
|
|
464
|
-
utils.
|
|
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.
|
|
469
|
-
utils.
|
|
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.
|
|
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.
|
|
507
|
+
await fs.promises.mkdir(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
494
508
|
try {
|
|
495
|
-
await runAsync(ctx.skillsBin + " add
|
|
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.
|
|
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 (!
|
|
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.
|
|
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.
|
|
540
|
+
await utils.copyDir(skillPath, dest);
|
|
527
541
|
|
|
528
542
|
const projectSkill = path.join(ctx.skillsDest, name);
|
|
529
|
-
if (
|
|
530
|
-
const rProj = utils.
|
|
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.
|
|
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.
|
|
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 (!
|
|
575
|
+
if (!(await pathExists(skillDir))) {
|
|
562
576
|
return utils.sendJSON(res, { error: "技能目录不存在" }, 404);
|
|
563
577
|
}
|
|
564
578
|
try {
|
|
565
|
-
|
|
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 (!
|
|
580
|
-
const r = utils.
|
|
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 (
|
|
595
|
-
const r = utils.
|
|
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 (!
|
|
612
|
+
if (!(await pathExists(src))) return utils.sendJSON(res, { error: `源目录不存在: ${name}` }, 404);
|
|
599
613
|
try {
|
|
600
|
-
utils.
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const content = readmePath ? fs.
|
|
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 (!
|
|
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 (
|
|
649
|
-
try { meta = JSON.parse(fs.
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|