mdk-skills 2.4.20 → 2.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/cli.js +104 -116
- package/scripts/core.js +131 -136
- package/scripts/web-ui/server/context.js +14 -14
- package/scripts/web-ui/server/routes/others.js +4 -4
- package/scripts/web-ui/server/routes/skills.js +17 -7
- package/scripts/web-ui/server/routes/source.js +6 -6
- package/scripts/web-ui/server/utils.js +12 -186
- package/scripts/web-ui/server.js +13 -7
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,
|
|
@@ -2,7 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const core = require("../../core");
|
|
4
4
|
|
|
5
|
-
// ---------- 路径(与 cli.js
|
|
5
|
+
// ---------- 路径(与 cli.js 保持一致)----------
|
|
6
6
|
|
|
7
7
|
const projectRoot = (() => {
|
|
8
8
|
try {
|
|
@@ -12,11 +12,12 @@ const projectRoot = (() => {
|
|
|
12
12
|
}
|
|
13
13
|
})();
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// 初始为 null,refreshSource() 执行后赋值
|
|
16
|
+
let skillsSource = null;
|
|
16
17
|
const claudeDest = path.join(projectRoot, ".claude");
|
|
17
18
|
const skillsDest = path.join(claudeDest, "skills");
|
|
18
19
|
const settingsPath = path.join(claudeDest, "settings.json");
|
|
19
|
-
let pkgSkillsSource =
|
|
20
|
+
let pkgSkillsSource = null;
|
|
20
21
|
|
|
21
22
|
// skills 包二进制路径
|
|
22
23
|
const skillsBin = (() => {
|
|
@@ -36,13 +37,17 @@ const skillsBin = (() => {
|
|
|
36
37
|
return `"${path.join(binDir, process.platform === "win32" ? "skills.cmd" : "skills")}"`;
|
|
37
38
|
})();
|
|
38
39
|
|
|
39
|
-
function refreshSource() {
|
|
40
|
-
skillsSource = core.getSkillsSource(projectRoot);
|
|
40
|
+
async function refreshSource() {
|
|
41
|
+
skillsSource = await core.getSkillsSource(projectRoot);
|
|
41
42
|
pkgSkillsSource = skillsSource ? path.join(skillsSource, ".claude", "skills") : null;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// ---------- Settings(异步)----------
|
|
45
46
|
|
|
47
|
+
async function pathExists(p) {
|
|
48
|
+
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
async function readSettings() {
|
|
47
52
|
if (!(await pathExists(settingsPath)))
|
|
48
53
|
return { skills: {}, always_apply_skills: [] };
|
|
@@ -103,6 +108,7 @@ function requireSource(res, utils) {
|
|
|
103
108
|
// ---------- profiles ----------
|
|
104
109
|
|
|
105
110
|
async function loadProfiles() {
|
|
111
|
+
if (!skillsSource) return null;
|
|
106
112
|
const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
|
|
107
113
|
if (!(await pathExists(profilesPath))) return null;
|
|
108
114
|
try {
|
|
@@ -120,16 +126,10 @@ const runningTasks = new Map();
|
|
|
120
126
|
|
|
121
127
|
const pullCache = new Map();
|
|
122
128
|
|
|
123
|
-
// ---------- 路径检查工具 ----------
|
|
124
|
-
|
|
125
|
-
async function pathExists(p) {
|
|
126
|
-
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
129
|
// ---------- 应用场景 / 安装(异步)----------
|
|
130
130
|
|
|
131
131
|
async function applyProfile(profile, { copyDir, safeRm }) {
|
|
132
|
-
const pkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
132
|
+
const pkgSkills = await core.listSkillDirs(pkgSkillsSource);
|
|
133
133
|
const failedDelete = [];
|
|
134
134
|
const failedInstall = [];
|
|
135
135
|
|
|
@@ -209,7 +209,7 @@ async function installSelectedSkills(selectedNames, { copyDir, safeRm }) {
|
|
|
209
209
|
if (!(await pathExists(skillsDest))) {
|
|
210
210
|
await fs.promises.mkdir(skillsDest, { recursive: true });
|
|
211
211
|
}
|
|
212
|
-
const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
|
|
212
|
+
const allPkgSkills = await core.listSkillDirs(pkgSkillsSource);
|
|
213
213
|
let installedCount = 0;
|
|
214
214
|
const locked = [];
|
|
215
215
|
|
|
@@ -274,4 +274,4 @@ function buildContext(utils) {
|
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
module.exports = { buildContext };
|
|
277
|
+
module.exports = { buildContext, refreshSource };
|
|
@@ -72,7 +72,7 @@ function register(router) {
|
|
|
72
72
|
|
|
73
73
|
const skillsDir = path.join(sourcePath, ".claude", "skills");
|
|
74
74
|
const skillNames = await pathExists(skillsDir)
|
|
75
|
-
? core.listSkillDirs(skillsDir)
|
|
75
|
+
? await core.listSkillDirs(skillsDir)
|
|
76
76
|
: [];
|
|
77
77
|
const skillsList = skillNames.length
|
|
78
78
|
? skillNames.map((s) => `- \`${s}\``).join("\n")
|
|
@@ -160,7 +160,7 @@ ${skillsList}
|
|
|
160
160
|
return utils.sendJSON(res, { error: "未设置技能目录,请在设置中连接" }, 400);
|
|
161
161
|
}
|
|
162
162
|
const results = [];
|
|
163
|
-
const pkgSkills = core.getPackageSkills(ctx.skillsSource);
|
|
163
|
+
const pkgSkills = await core.getPackageSkills(ctx.skillsSource);
|
|
164
164
|
for (const skill of pkgSkills) {
|
|
165
165
|
const skillDir = path.join(ctx.skillsDest, skill.name);
|
|
166
166
|
const issues = [];
|
|
@@ -169,7 +169,7 @@ ${skillsList}
|
|
|
169
169
|
} else {
|
|
170
170
|
if (!(await pathExists(path.join(skillDir, "SKILL.md")))) {
|
|
171
171
|
issues.push("缺少 SKILL.md");
|
|
172
|
-
} else if (!core.parseFrontmatter(skillDir)) {
|
|
172
|
+
} else if (!(await core.parseFrontmatter(skillDir))) {
|
|
173
173
|
issues.push("SKILL.md frontmatter 格式异常");
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -234,7 +234,7 @@ ${skillsList}
|
|
|
234
234
|
const valid = await pathExists(skillsDir);
|
|
235
235
|
let skillCount = 0;
|
|
236
236
|
if (valid) {
|
|
237
|
-
skillCount = core.listSkillDirs(skillsDir).length;
|
|
237
|
+
skillCount = (await core.listSkillDirs(skillsDir)).length;
|
|
238
238
|
}
|
|
239
239
|
return utils.sendJSON(res, {
|
|
240
240
|
ok: true,
|
|
@@ -9,6 +9,16 @@ 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
|
+
|
|
12
22
|
async function pathExists(p) {
|
|
13
23
|
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
14
24
|
}
|
|
@@ -84,9 +94,9 @@ function register(router) {
|
|
|
84
94
|
// ----------------------------------------------------------------
|
|
85
95
|
router.get("/api/skills", async (req, res) => {
|
|
86
96
|
if (!requireSource(res)) return;
|
|
87
|
-
const pkgSkills = core.getPackageSkills(ctx.skillsSource);
|
|
97
|
+
const pkgSkills = await core.getPackageSkills(ctx.skillsSource);
|
|
88
98
|
const pkgNames = new Set(pkgSkills.map((s) => s.name));
|
|
89
|
-
const userSkills = core.getUserSkills(ctx.claudeDest);
|
|
99
|
+
const userSkills = await core.getUserSkills(ctx.claudeDest);
|
|
90
100
|
let cleaned = false;
|
|
91
101
|
for (const us of userSkills) {
|
|
92
102
|
if (!pkgNames.has(us.name)) {
|
|
@@ -106,7 +116,7 @@ function register(router) {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
if (settingsChanged) await ctx.writeSettings(settings);
|
|
109
|
-
const refreshed = core.getUserSkills(ctx.claudeDest);
|
|
119
|
+
const refreshed = await core.getUserSkills(ctx.claudeDest);
|
|
110
120
|
const userMap = new Map(refreshed.map((s) => [s.name, s]));
|
|
111
121
|
const pullSource = await ctx.readPullSource();
|
|
112
122
|
const result = pkgSkills.map((s) => ({
|
|
@@ -177,7 +187,7 @@ function register(router) {
|
|
|
177
187
|
const cached = ctx.pullCache.get(url);
|
|
178
188
|
if (cached) {
|
|
179
189
|
const skillsDir = path.join(cached.tmpDir, ".claude", "skills");
|
|
180
|
-
const skills = await pathExists(skillsDir) ? core.listSkillDirs(skillsDir) : [];
|
|
190
|
+
const skills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
|
|
181
191
|
return utils.sendJSON(res, { ok: true, skills, total: skills.length });
|
|
182
192
|
}
|
|
183
193
|
const tmpDir = path.join(os.tmpdir(), "mdk-preview-" + Date.now());
|
|
@@ -192,7 +202,7 @@ function register(router) {
|
|
|
192
202
|
}
|
|
193
203
|
await utils.cleanNpxTemp();
|
|
194
204
|
const skillsDir = path.join(tmpDir, ".claude", "skills");
|
|
195
|
-
const skills = await pathExists(skillsDir) ? core.listSkillDirs(skillsDir) : [];
|
|
205
|
+
const skills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
|
|
196
206
|
if (skills.length === 0) {
|
|
197
207
|
await utils.safeRm(tmpDir, "临时目录");
|
|
198
208
|
return utils.sendJSON(res, { error: "未找到有效技能" }, 400);
|
|
@@ -566,7 +576,7 @@ function register(router) {
|
|
|
566
576
|
return utils.sendJSON(res, { error: "技能目录不存在" }, 404);
|
|
567
577
|
}
|
|
568
578
|
try {
|
|
569
|
-
|
|
579
|
+
openInExplorer(skillDir);
|
|
570
580
|
return utils.sendJSON(res, { ok: true });
|
|
571
581
|
} catch {
|
|
572
582
|
return utils.sendJSON(res, { error: "打开目录失败" }, 500);
|
|
@@ -614,7 +624,7 @@ function register(router) {
|
|
|
614
624
|
router.post("/api/skills-dest/open", async (req, res) => {
|
|
615
625
|
if (!requireSource(res)) return;
|
|
616
626
|
try {
|
|
617
|
-
|
|
627
|
+
openInExplorer(ctx.skillsDest);
|
|
618
628
|
return utils.sendJSON(res, { ok: true });
|
|
619
629
|
} catch {
|
|
620
630
|
return utils.sendJSON(res, { error: "打开目录失败" }, 500);
|
|
@@ -17,8 +17,8 @@ function register(router) {
|
|
|
17
17
|
// ----------------------------------------------------------------
|
|
18
18
|
router.get("/api/status", async (req, res) => {
|
|
19
19
|
const settings = await ctx.readSettings();
|
|
20
|
-
const userSkills = core.getUserSkills(ctx.claudeDest);
|
|
21
|
-
const pkgSkills = ctx.skillsSource ? core.getPackageSkills(ctx.skillsSource) : [];
|
|
20
|
+
const userSkills = await core.getUserSkills(ctx.claudeDest);
|
|
21
|
+
const pkgSkills = ctx.skillsSource ? await core.getPackageSkills(ctx.skillsSource) : [];
|
|
22
22
|
const profiles = ctx.skillsSource ? await ctx.loadProfiles() : null;
|
|
23
23
|
const activeProfile = profiles?.find(
|
|
24
24
|
(p) => p.id === settings._active_profile,
|
|
@@ -58,7 +58,7 @@ function register(router) {
|
|
|
58
58
|
}
|
|
59
59
|
const skillsDir = path.join(claudeDir, "skills");
|
|
60
60
|
if (await pathExists(skillsDir)) {
|
|
61
|
-
for (const name of core.listSkillDirs(skillsDir)) {
|
|
61
|
+
for (const name of await core.listSkillDirs(skillsDir)) {
|
|
62
62
|
if (!(await pathExists(path.join(skillsDir, name, "SKILL.md")))) {
|
|
63
63
|
missingSkillDocs.push(name);
|
|
64
64
|
}
|
|
@@ -150,14 +150,14 @@ function register(router) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
const repoClaude = path.join(sourcePath, ".claude");
|
|
153
|
-
core.backupDir(repoClaude);
|
|
153
|
+
await core.backupDir(repoClaude);
|
|
154
154
|
|
|
155
155
|
// 技能反推
|
|
156
156
|
const repoSkills = path.join(sourcePath, ".claude", "skills");
|
|
157
157
|
if (!(await pathExists(repoSkills)))
|
|
158
158
|
await fs.promises.mkdir(repoSkills, { recursive: true });
|
|
159
159
|
if (await pathExists(ctx.skillsDest)) {
|
|
160
|
-
for (const name of core.listSkillDirs(ctx.skillsDest)) {
|
|
160
|
+
for (const name of await core.listSkillDirs(ctx.skillsDest)) {
|
|
161
161
|
const src = path.join(ctx.skillsDest, name);
|
|
162
162
|
const dest = path.join(repoSkills, name);
|
|
163
163
|
if (await pathExists(dest))
|
|
@@ -245,7 +245,7 @@ function register(router) {
|
|
|
245
245
|
|
|
246
246
|
// 3. 每个 skill 目录的 .meta.json
|
|
247
247
|
if (await pathExists(skillsDir)) {
|
|
248
|
-
for (const name of core.listSkillDirs(skillsDir)) {
|
|
248
|
+
for (const name of await core.listSkillDirs(skillsDir)) {
|
|
249
249
|
const skillDir = path.join(skillsDir, name);
|
|
250
250
|
const metaPath = path.join(skillDir, ".meta.json");
|
|
251
251
|
if (!(await pathExists(metaPath))) {
|