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
|
@@ -6,21 +6,25 @@ const { buildContext } = require("../context");
|
|
|
6
6
|
|
|
7
7
|
const ctx = buildContext(utils);
|
|
8
8
|
|
|
9
|
+
async function pathExists(p) {
|
|
10
|
+
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
function register(router) {
|
|
10
14
|
|
|
11
15
|
// ----------------------------------------------------------------
|
|
12
16
|
// GET /api/status — 当前状态总览
|
|
13
17
|
// ----------------------------------------------------------------
|
|
14
18
|
router.get("/api/status", async (req, res) => {
|
|
15
|
-
const settings = ctx.readSettings();
|
|
16
|
-
const userSkills = core.getUserSkills(ctx.claudeDest);
|
|
17
|
-
const pkgSkills = ctx.skillsSource ? core.getPackageSkills(ctx.skillsSource) : [];
|
|
18
|
-
const profiles = ctx.skillsSource ? ctx.loadProfiles() : null;
|
|
19
|
+
const settings = await ctx.readSettings();
|
|
20
|
+
const userSkills = await core.getUserSkills(ctx.claudeDest);
|
|
21
|
+
const pkgSkills = ctx.skillsSource ? await core.getPackageSkills(ctx.skillsSource) : [];
|
|
22
|
+
const profiles = ctx.skillsSource ? await ctx.loadProfiles() : null;
|
|
19
23
|
const activeProfile = profiles?.find(
|
|
20
24
|
(p) => p.id === settings._active_profile,
|
|
21
25
|
);
|
|
22
26
|
const enabledCount = userSkills.filter((s) => s.enabled).length;
|
|
23
|
-
const mdExists =
|
|
27
|
+
const mdExists = await pathExists(path.join(ctx.projectRoot, "CLAUDE.md"));
|
|
24
28
|
return utils.sendJSON(res, {
|
|
25
29
|
pkgSkills,
|
|
26
30
|
enabledCount,
|
|
@@ -37,7 +41,7 @@ function register(router) {
|
|
|
37
41
|
// GET /api/source — 获取技能目录信息
|
|
38
42
|
// ----------------------------------------------------------------
|
|
39
43
|
router.get("/api/source", async (req, res) => {
|
|
40
|
-
const settings = ctx.readSettings();
|
|
44
|
+
const settings = await ctx.readSettings();
|
|
41
45
|
const sourcePath = settings._skill_source || null;
|
|
42
46
|
|
|
43
47
|
let needsInit = false;
|
|
@@ -45,17 +49,17 @@ function register(router) {
|
|
|
45
49
|
const missingSkillDocs = [];
|
|
46
50
|
if (sourcePath) {
|
|
47
51
|
const claudeDir = path.join(sourcePath, ".claude");
|
|
48
|
-
if (
|
|
49
|
-
if (!
|
|
52
|
+
if (await pathExists(claudeDir)) {
|
|
53
|
+
if (!(await pathExists(path.join(claudeDir, "profiles.json")))) {
|
|
50
54
|
missingConfig.push("profiles.json");
|
|
51
55
|
}
|
|
52
|
-
if (!
|
|
56
|
+
if (!(await pathExists(path.join(claudeDir, "settings.json")))) {
|
|
53
57
|
missingConfig.push("settings.json");
|
|
54
58
|
}
|
|
55
59
|
const skillsDir = path.join(claudeDir, "skills");
|
|
56
|
-
if (
|
|
57
|
-
for (const name of core.listSkillDirs(skillsDir)) {
|
|
58
|
-
if (!
|
|
60
|
+
if (await pathExists(skillsDir)) {
|
|
61
|
+
for (const name of await core.listSkillDirs(skillsDir)) {
|
|
62
|
+
if (!(await pathExists(path.join(skillsDir, name, "SKILL.md")))) {
|
|
59
63
|
missingSkillDocs.push(name);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
@@ -70,8 +74,8 @@ function register(router) {
|
|
|
70
74
|
if (sourcePath) {
|
|
71
75
|
const skillsDir = path.join(sourcePath, ".claude", "skills");
|
|
72
76
|
readmeStatus = {
|
|
73
|
-
root:
|
|
74
|
-
skills:
|
|
77
|
+
root: await pathExists(path.join(sourcePath, "README.md")),
|
|
78
|
+
skills: await pathExists(path.join(skillsDir, "README.md")),
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -93,25 +97,25 @@ function register(router) {
|
|
|
93
97
|
const repoPath = body.path;
|
|
94
98
|
if (!repoPath) return utils.sendJSON(res, { error: "缺少目录路径" }, 400);
|
|
95
99
|
const resolved = path.resolve(repoPath);
|
|
96
|
-
if (!
|
|
100
|
+
if (!(await pathExists(path.join(resolved, ".claude", "skills")))) {
|
|
97
101
|
return utils.sendJSON(
|
|
98
102
|
res,
|
|
99
103
|
{ error: `路径 "${resolved}" 下没有 .claude/skills/` },
|
|
100
104
|
400,
|
|
101
105
|
);
|
|
102
106
|
}
|
|
103
|
-
const settings = ctx.readSettings();
|
|
107
|
+
const settings = await ctx.readSettings();
|
|
104
108
|
settings._skill_source = resolved;
|
|
105
|
-
ctx.writeSettings(settings);
|
|
109
|
+
await ctx.writeSettings(settings);
|
|
106
110
|
// 复制 CLAUDE.md(不存在才装)
|
|
107
111
|
const mdSource = path.join(resolved, "CLAUDE.md");
|
|
108
112
|
const mdDest = path.join(ctx.projectRoot, "CLAUDE.md");
|
|
109
|
-
if (
|
|
110
|
-
fs.
|
|
113
|
+
if (await pathExists(mdSource) && !(await pathExists(mdDest))) {
|
|
114
|
+
await fs.promises.copyFile(mdSource, mdDest);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
// 清理幽灵技能引用
|
|
114
|
-
const cleanResult = utils.cleanGhostSkills(resolved);
|
|
118
|
+
const cleanResult = await utils.cleanGhostSkills(resolved);
|
|
115
119
|
if (cleanResult.removed.length > 0) {
|
|
116
120
|
console.log(`[清理幽灵技能] ${cleanResult.removed.join(", ")}`);
|
|
117
121
|
}
|
|
@@ -124,12 +128,12 @@ function register(router) {
|
|
|
124
128
|
// POST /api/source/clear — 清除技能目录
|
|
125
129
|
// ----------------------------------------------------------------
|
|
126
130
|
router.post("/api/source/clear", async (req, res) => {
|
|
127
|
-
const settings = ctx.readSettings();
|
|
131
|
+
const settings = await ctx.readSettings();
|
|
128
132
|
if (!settings._skill_source) {
|
|
129
133
|
return utils.sendJSON(res, { error: "当前未设置技能目录" }, 400);
|
|
130
134
|
}
|
|
131
135
|
delete settings._skill_source;
|
|
132
|
-
ctx.writeSettings(settings);
|
|
136
|
+
await ctx.writeSettings(settings);
|
|
133
137
|
ctx.refreshSource();
|
|
134
138
|
return utils.sendJSON(res, { ok: true });
|
|
135
139
|
});
|
|
@@ -138,34 +142,34 @@ function register(router) {
|
|
|
138
142
|
// POST /api/source/sync — 同步到本地源
|
|
139
143
|
// ----------------------------------------------------------------
|
|
140
144
|
router.post("/api/source/sync", async (req, res) => {
|
|
141
|
-
const settings = ctx.readSettings();
|
|
145
|
+
const settings = await ctx.readSettings();
|
|
142
146
|
const sourcePath = settings._skill_source;
|
|
143
147
|
if (!sourcePath) return utils.sendJSON(res, { error: "尚未绑定技能源" }, 400);
|
|
144
|
-
if (!
|
|
148
|
+
if (!(await pathExists(path.join(sourcePath, ".claude")))) {
|
|
145
149
|
return utils.sendJSON(res, { error: "绑定的路径已失效" }, 400);
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
const repoClaude = path.join(sourcePath, ".claude");
|
|
149
|
-
core.backupDir(repoClaude);
|
|
153
|
+
await core.backupDir(repoClaude);
|
|
150
154
|
|
|
151
155
|
// 技能反推
|
|
152
156
|
const repoSkills = path.join(sourcePath, ".claude", "skills");
|
|
153
|
-
if (!
|
|
154
|
-
fs.
|
|
155
|
-
if (
|
|
156
|
-
for (const name of core.listSkillDirs(ctx.skillsDest)) {
|
|
157
|
+
if (!(await pathExists(repoSkills)))
|
|
158
|
+
await fs.promises.mkdir(repoSkills, { recursive: true });
|
|
159
|
+
if (await pathExists(ctx.skillsDest)) {
|
|
160
|
+
for (const name of await core.listSkillDirs(ctx.skillsDest)) {
|
|
157
161
|
const src = path.join(ctx.skillsDest, name);
|
|
158
162
|
const dest = path.join(repoSkills, name);
|
|
159
|
-
if (
|
|
160
|
-
utils.
|
|
161
|
-
utils.
|
|
163
|
+
if (await pathExists(dest))
|
|
164
|
+
await utils.safeRm(dest, name);
|
|
165
|
+
await utils.copyDir(src, dest);
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// profiles.json 反推
|
|
166
170
|
const projectProfiles = path.join(ctx.claudeDest, "profiles.json");
|
|
167
|
-
if (
|
|
168
|
-
fs.
|
|
171
|
+
if (await pathExists(projectProfiles)) {
|
|
172
|
+
await fs.promises.copyFile(
|
|
169
173
|
projectProfiles,
|
|
170
174
|
path.join(sourcePath, ".claude", "profiles.json"),
|
|
171
175
|
);
|
|
@@ -175,7 +179,7 @@ function register(router) {
|
|
|
175
179
|
const cleanSettings = { ...settings };
|
|
176
180
|
delete cleanSettings._skill_source;
|
|
177
181
|
delete cleanSettings._active_profile;
|
|
178
|
-
fs.
|
|
182
|
+
await fs.promises.writeFile(
|
|
179
183
|
path.join(sourcePath, ".claude", "settings.json"),
|
|
180
184
|
JSON.stringify(cleanSettings, null, 2) + "\n",
|
|
181
185
|
"utf-8",
|
|
@@ -183,8 +187,8 @@ function register(router) {
|
|
|
183
187
|
|
|
184
188
|
// CLAUDE.md 反推
|
|
185
189
|
const projectMd = path.join(ctx.projectRoot, "CLAUDE.md");
|
|
186
|
-
if (
|
|
187
|
-
fs.
|
|
190
|
+
if (await pathExists(projectMd)) {
|
|
191
|
+
await fs.promises.copyFile(projectMd, path.join(sourcePath, "CLAUDE.md"));
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
return utils.sendJSON(res, { ok: true });
|
|
@@ -194,7 +198,7 @@ function register(router) {
|
|
|
194
198
|
// POST /api/source/init — 初始化本地源骨架文件
|
|
195
199
|
// ----------------------------------------------------------------
|
|
196
200
|
router.post("/api/source/init", async (req, res) => {
|
|
197
|
-
const settings = ctx.readSettings();
|
|
201
|
+
const settings = await ctx.readSettings();
|
|
198
202
|
const sourcePath = settings._skill_source;
|
|
199
203
|
if (!sourcePath) return utils.sendJSON(res, { error: "未绑定本地源" }, 400);
|
|
200
204
|
|
|
@@ -206,8 +210,8 @@ function register(router) {
|
|
|
206
210
|
const created = { profiles: false, settings: false, metaFiles: 0 };
|
|
207
211
|
|
|
208
212
|
// 1. profiles.json
|
|
209
|
-
if (!
|
|
210
|
-
fs.
|
|
213
|
+
if (!(await pathExists(profilesPath))) {
|
|
214
|
+
await fs.promises.writeFile(
|
|
211
215
|
profilesPath,
|
|
212
216
|
JSON.stringify(
|
|
213
217
|
{
|
|
@@ -230,8 +234,8 @@ function register(router) {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
// 2. settings.json
|
|
233
|
-
if (!
|
|
234
|
-
fs.
|
|
237
|
+
if (!(await pathExists(settingsPath))) {
|
|
238
|
+
await fs.promises.writeFile(
|
|
235
239
|
settingsPath,
|
|
236
240
|
JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
|
|
237
241
|
"utf-8",
|
|
@@ -240,20 +244,20 @@ function register(router) {
|
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
// 3. 每个 skill 目录的 .meta.json
|
|
243
|
-
if (
|
|
244
|
-
for (const name of core.listSkillDirs(skillsDir)) {
|
|
247
|
+
if (await pathExists(skillsDir)) {
|
|
248
|
+
for (const name of await core.listSkillDirs(skillsDir)) {
|
|
245
249
|
const skillDir = path.join(skillsDir, name);
|
|
246
250
|
const metaPath = path.join(skillDir, ".meta.json");
|
|
247
|
-
if (!
|
|
248
|
-
utils.writeSkillMeta(skillDir, false);
|
|
251
|
+
if (!(await pathExists(metaPath))) {
|
|
252
|
+
await utils.writeSkillMeta(skillDir, false);
|
|
249
253
|
created.metaFiles++;
|
|
250
254
|
}
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
const readmeStatus = {
|
|
255
|
-
root:
|
|
256
|
-
skills:
|
|
259
|
+
root: await pathExists(path.join(sourcePath, "README.md")),
|
|
260
|
+
skills: await pathExists(path.join(skillsDir, "README.md")),
|
|
257
261
|
};
|
|
258
262
|
|
|
259
263
|
return utils.sendJSON(res, { ok: true, ...created, readmeStatus });
|
|
@@ -4,6 +4,12 @@ const crypto = require("crypto");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const core = require("../../core");
|
|
6
6
|
|
|
7
|
+
// ===================== 异步路径检查 =====================
|
|
8
|
+
|
|
9
|
+
async function pathExists(p) {
|
|
10
|
+
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
// ===================== HTTP 工具 =====================
|
|
8
14
|
|
|
9
15
|
function sendJSON(res, data, status = 200) {
|
|
@@ -25,14 +31,13 @@ function parseBody(req) {
|
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
// =====================
|
|
34
|
+
// ===================== 文件系统(异步)======================
|
|
29
35
|
|
|
30
36
|
// 安全删除目录/文件,处理 Windows 文件锁定(EBUSY)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!fs.existsSync(dir)) return { removed: false, reason: "not_found" };
|
|
37
|
+
async function safeRm(dir, label) {
|
|
38
|
+
if (!(await pathExists(dir))) return { removed: false, reason: "not_found" };
|
|
34
39
|
try {
|
|
35
|
-
fs.
|
|
40
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
36
41
|
return { removed: true };
|
|
37
42
|
} catch (err) {
|
|
38
43
|
if (err.code === "EBUSY") {
|
|
@@ -44,42 +49,30 @@ function safeRmSync(dir, label) {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
//
|
|
48
|
-
function
|
|
49
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
50
|
-
for (const item of fs.readdirSync(src)) {
|
|
51
|
-
const srcPath = path.join(src, item);
|
|
52
|
-
const destPath = path.join(dest, item);
|
|
53
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
54
|
-
copyDirSync(srcPath, destPath);
|
|
55
|
-
} else {
|
|
56
|
-
fs.copyFileSync(srcPath, destPath);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// npx skills 会在系统临时目录留下 skills-* 工作目录,统一清理
|
|
62
|
-
function cleanNpxTemp() {
|
|
52
|
+
// 清理 npx 临时目录
|
|
53
|
+
async function cleanNpxTemp() {
|
|
63
54
|
const tmp = os.tmpdir();
|
|
64
|
-
|
|
55
|
+
let names;
|
|
56
|
+
try { names = await fs.promises.readdir(tmp); } catch { return; }
|
|
57
|
+
for (const name of names) {
|
|
65
58
|
if (/^skills-[A-Za-z0-9]+$/.test(name)) {
|
|
66
59
|
const p = path.join(tmp, name);
|
|
67
|
-
try { fs.
|
|
60
|
+
try { await fs.promises.rm(p, { recursive: true, force: true }); } catch {}
|
|
68
61
|
}
|
|
69
62
|
}
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
// Windows 下获取可用盘符
|
|
73
|
-
function getWindowsDrives() {
|
|
66
|
+
async function getWindowsDrives() {
|
|
74
67
|
const drives = [];
|
|
75
68
|
for (let i = 65; i <= 90; i++) {
|
|
76
69
|
const letter = String.fromCharCode(i);
|
|
77
70
|
const root = letter + ":\\";
|
|
78
71
|
try {
|
|
79
|
-
if (
|
|
72
|
+
if (await pathExists(root)) {
|
|
80
73
|
drives.push({ name: root, isDir: true });
|
|
81
74
|
}
|
|
82
|
-
} catch {
|
|
75
|
+
} catch {}
|
|
83
76
|
}
|
|
84
77
|
return drives;
|
|
85
78
|
}
|
|
@@ -131,35 +124,40 @@ function parseSkillsFindOutput(stdout) {
|
|
|
131
124
|
|
|
132
125
|
// ===================== 指纹 & 元信息 =====================
|
|
133
126
|
|
|
134
|
-
//
|
|
135
|
-
function calcSkillFingerprint(dir) {
|
|
136
|
-
if (!
|
|
127
|
+
// 计算技能目录的文件指纹(MD5,排除 .meta.json)
|
|
128
|
+
async function calcSkillFingerprint(dir) {
|
|
129
|
+
if (!(await pathExists(dir))) return "";
|
|
137
130
|
const hash = crypto.createHash("md5");
|
|
138
|
-
const walk = (d) => {
|
|
139
|
-
|
|
131
|
+
const walk = async (d) => {
|
|
132
|
+
let items;
|
|
133
|
+
try { items = await fs.promises.readdir(d); } catch { return; }
|
|
134
|
+
items.sort();
|
|
135
|
+
for (const item of items) {
|
|
140
136
|
const p = path.join(d, item);
|
|
141
137
|
if (item === ".meta.json") continue;
|
|
142
|
-
|
|
138
|
+
let stat;
|
|
139
|
+
try { stat = await fs.promises.stat(p); } catch { continue; }
|
|
143
140
|
if (stat.isDirectory()) {
|
|
144
|
-
walk(p);
|
|
141
|
+
await walk(p);
|
|
145
142
|
} else {
|
|
146
143
|
hash.update(item);
|
|
147
|
-
|
|
144
|
+
const content = await fs.promises.readFile(p);
|
|
145
|
+
hash.update(content);
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
148
|
};
|
|
151
|
-
walk(dir);
|
|
149
|
+
await walk(dir);
|
|
152
150
|
return hash.digest("hex");
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
//
|
|
156
|
-
function writeSkillMeta(dest, wasUpdated) {
|
|
157
|
-
const fm = core.parseFrontmatter(dest);
|
|
153
|
+
// 写入技能元信息(异步版)
|
|
154
|
+
async function writeSkillMeta(dest, wasUpdated) {
|
|
155
|
+
const fm = await core.parseFrontmatter(dest);
|
|
158
156
|
if (!fm) return;
|
|
159
157
|
const metaPath = path.join(dest, ".meta.json");
|
|
160
158
|
let oldMeta = {};
|
|
161
|
-
if (
|
|
162
|
-
try { oldMeta = JSON.parse(fs.
|
|
159
|
+
if (await pathExists(metaPath)) {
|
|
160
|
+
try { oldMeta = JSON.parse(await fs.promises.readFile(metaPath, "utf-8")); } catch {}
|
|
163
161
|
}
|
|
164
162
|
const hasUpstream = !!fm.version;
|
|
165
163
|
const meta = {
|
|
@@ -174,24 +172,24 @@ function writeSkillMeta(dest, wasUpdated) {
|
|
|
174
172
|
} else {
|
|
175
173
|
meta._updateCount = oldMeta._updateCount || 0;
|
|
176
174
|
}
|
|
177
|
-
fs.
|
|
175
|
+
await fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
178
176
|
}
|
|
179
177
|
|
|
180
178
|
// ===================== 幽灵技能清理 =====================
|
|
181
179
|
|
|
182
|
-
function cleanGhostSkills(sourcePath) {
|
|
180
|
+
async function cleanGhostSkills(sourcePath) {
|
|
183
181
|
const claudeDir = path.join(sourcePath, ".claude");
|
|
184
182
|
const skillsDir = path.join(claudeDir, "skills");
|
|
185
|
-
if (!
|
|
183
|
+
if (!(await pathExists(skillsDir))) return { cleaned: false, reason: "skills 目录不存在" };
|
|
186
184
|
|
|
187
|
-
const actualSkills =
|
|
185
|
+
const actualSkills = await pathExists(skillsDir) ? await core.listSkillDirs(skillsDir) : [];
|
|
188
186
|
const skillSet = new Set(actualSkills);
|
|
189
187
|
const cleaned = { profiles: 0, alwaysApply: 0, settings: 0, removed: [] };
|
|
190
188
|
|
|
191
189
|
const profilesPath = path.join(claudeDir, "profiles.json");
|
|
192
|
-
if (
|
|
190
|
+
if (await pathExists(profilesPath)) {
|
|
193
191
|
try {
|
|
194
|
-
const profilesData = JSON.parse(fs.
|
|
192
|
+
const profilesData = JSON.parse(await fs.promises.readFile(profilesPath, "utf-8"));
|
|
195
193
|
let changed = false;
|
|
196
194
|
for (const profile of profilesData.profiles || []) {
|
|
197
195
|
if (!profile.skills || !Array.isArray(profile.skills)) continue;
|
|
@@ -204,15 +202,15 @@ function cleanGhostSkills(sourcePath) {
|
|
|
204
202
|
}
|
|
205
203
|
if (changed) {
|
|
206
204
|
cleaned.profiles++;
|
|
207
|
-
fs.
|
|
205
|
+
await fs.promises.writeFile(profilesPath, JSON.stringify(profilesData, null, 2) + "\n", "utf-8");
|
|
208
206
|
}
|
|
209
207
|
} catch {}
|
|
210
208
|
}
|
|
211
209
|
|
|
212
210
|
const settingsPathInRepo = path.join(claudeDir, "settings.json");
|
|
213
|
-
if (
|
|
211
|
+
if (await pathExists(settingsPathInRepo)) {
|
|
214
212
|
try {
|
|
215
|
-
const settings = JSON.parse(fs.
|
|
213
|
+
const settings = JSON.parse(await fs.promises.readFile(settingsPathInRepo, "utf-8"));
|
|
216
214
|
let changed = false;
|
|
217
215
|
if (Array.isArray(settings.always_apply_skills)) {
|
|
218
216
|
const filtered = settings.always_apply_skills.filter((s) => {
|
|
@@ -233,7 +231,7 @@ function cleanGhostSkills(sourcePath) {
|
|
|
233
231
|
}
|
|
234
232
|
if (changed) {
|
|
235
233
|
cleaned.settings++;
|
|
236
|
-
fs.
|
|
234
|
+
await fs.promises.writeFile(settingsPathInRepo, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
237
235
|
}
|
|
238
236
|
} catch {}
|
|
239
237
|
}
|
|
@@ -242,17 +240,23 @@ function cleanGhostSkills(sourcePath) {
|
|
|
242
240
|
}
|
|
243
241
|
|
|
244
242
|
module.exports = {
|
|
243
|
+
// HTTP
|
|
245
244
|
sendJSON,
|
|
246
245
|
parseBody,
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
// 文件系统
|
|
247
|
+
pathExists,
|
|
248
|
+
safeRm,
|
|
249
|
+
copyDir: core.copyDir,
|
|
249
250
|
cleanNpxTemp,
|
|
250
251
|
getWindowsDrives,
|
|
252
|
+
// 技能解析
|
|
251
253
|
stripAnsi,
|
|
252
254
|
escapeShellArg,
|
|
253
255
|
parseInstallCount,
|
|
254
256
|
parseSkillsFindOutput,
|
|
257
|
+
// 指纹 & 元信息
|
|
255
258
|
calcSkillFingerprint,
|
|
256
259
|
writeSkillMeta,
|
|
260
|
+
// 幽灵清理
|
|
257
261
|
cleanGhostSkills,
|
|
258
262
|
};
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs = require("fs");
|
|
|
8
8
|
|
|
9
9
|
const { Router } = require("./server/router");
|
|
10
10
|
const utils = require("./server/utils");
|
|
11
|
+
const { refreshSource } = require("./server/context");
|
|
11
12
|
|
|
12
13
|
// ===================== 路由注册 =====================
|
|
13
14
|
|
|
@@ -71,14 +72,30 @@ function startProduction(distDir) {
|
|
|
71
72
|
}
|
|
72
73
|
const urlPath = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
|
73
74
|
const filePath = path.join(distDir, urlPath);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
|
|
76
|
+
fs.stat(filePath, (err, stat) => {
|
|
77
|
+
if (!err && stat.isFile()) {
|
|
78
|
+
const ext = path.extname(filePath);
|
|
79
|
+
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
|
|
80
|
+
const stream = fs.createReadStream(filePath);
|
|
81
|
+
stream.pipe(res);
|
|
82
|
+
stream.on("error", () => { res.end(); });
|
|
83
|
+
} else {
|
|
84
|
+
// SPA fallback: 返回 index.html
|
|
85
|
+
const indexPath = path.join(distDir, "index.html");
|
|
86
|
+
fs.stat(indexPath, (err2, stat2) => {
|
|
87
|
+
if (err2) {
|
|
88
|
+
res.writeHead(404);
|
|
89
|
+
res.end("Not Found");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
93
|
+
const stream = fs.createReadStream(indexPath);
|
|
94
|
+
stream.pipe(res);
|
|
95
|
+
stream.on("error", () => { res.end(); });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
82
99
|
});
|
|
83
100
|
server.listen(3344, () => {
|
|
84
101
|
console.log(`\n 🖥️ mdk-skills Web UI`);
|
|
@@ -120,12 +137,19 @@ async function startDev() {
|
|
|
120
137
|
|
|
121
138
|
// ===================== 启动 =====================
|
|
122
139
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
140
|
+
async function start() {
|
|
141
|
+
await refreshSource();
|
|
142
|
+
const distDir = path.join(__dirname, "dist");
|
|
143
|
+
let stats;
|
|
144
|
+
try { stats = await fs.promises.stat(distDir); } catch {}
|
|
145
|
+
if (stats) {
|
|
146
|
+
startProduction(distDir);
|
|
147
|
+
} else {
|
|
148
|
+
await startDev();
|
|
149
|
+
}
|
|
131
150
|
}
|
|
151
|
+
|
|
152
|
+
start().catch((err) => {
|
|
153
|
+
console.error("启动失败:", err);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
});
|