mdk-skills 2.1.11 → 2.1.13
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/.claude/settings.json +3 -2
- package/package.json +9 -2
- package/scripts/cli.js +16 -2
- package/scripts/web-ui/index.html +12 -0
- package/scripts/web-ui/server.js +461 -0
- package/scripts/web-ui/src/App.vue +98 -0
- package/scripts/web-ui/src/api/skills.js +65 -0
- package/scripts/web-ui/src/components/SkillCard.vue +72 -0
- package/scripts/web-ui/src/components/StatusBar.vue +67 -0
- package/scripts/web-ui/src/main.js +9 -0
- package/scripts/web-ui/src/router/index.js +15 -0
- package/scripts/web-ui/src/styles/main.css +55 -0
- package/scripts/web-ui/src/views/Dashboard.vue +66 -0
- package/scripts/web-ui/src/views/SceneSwitch.vue +181 -0
- package/scripts/web-ui/src/views/Settings.vue +220 -0
package/.claude/settings.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdk-skills",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.13",
|
|
4
4
|
"description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
|
|
5
5
|
"author": "XiaoMa",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@inquirer/prompts": "^8.4.2",
|
|
21
|
-
"
|
|
21
|
+
"@vicons/ionicons5": "^0.13.0",
|
|
22
|
+
"@vicons/material": "^0.13.0",
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.6",
|
|
24
|
+
"chalk": "^5.6.2",
|
|
25
|
+
"naive-ui": "^2.44.1",
|
|
26
|
+
"vite": "^8.0.11",
|
|
27
|
+
"vue": "^3.5.34",
|
|
28
|
+
"vue-router": "^5.0.6"
|
|
22
29
|
}
|
|
23
30
|
}
|
package/scripts/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ const projectRoot = (() => {
|
|
|
15
15
|
}
|
|
16
16
|
})();
|
|
17
17
|
const packageDir = path.join(__dirname, "..");
|
|
18
|
-
|
|
18
|
+
let skillsSource = getSkillsSource(projectRoot, packageDir);
|
|
19
19
|
const claudeDest = path.join(projectRoot, ".claude");
|
|
20
20
|
const skillsDest = path.join(claudeDest, "skills");
|
|
21
21
|
const settingsPath = path.join(claudeDest, "settings.json");
|
|
@@ -487,7 +487,7 @@ function cmdDisconnect() {
|
|
|
487
487
|
|
|
488
488
|
// ---------- 主入口 ----------
|
|
489
489
|
|
|
490
|
-
const COMMANDS = ["list", "connect", "sync", "disconnect"];
|
|
490
|
+
const COMMANDS = ["list", "connect", "sync", "disconnect", "ui"];
|
|
491
491
|
|
|
492
492
|
async function main() {
|
|
493
493
|
const command = process.argv[2];
|
|
@@ -511,18 +511,30 @@ async function main() {
|
|
|
511
511
|
npx mdk-skills connect 绑定本地技能源仓库路径
|
|
512
512
|
npx mdk-skills sync 将项目中的技能修改推送到仓库
|
|
513
513
|
npx mdk-skills disconnect 解绑本地源,恢复使用 npm 包内置技能
|
|
514
|
+
npx mdk-skills ui 启动 Web 管理面板
|
|
515
|
+
npx mdk-skills --connect 绑定本地仓库后直接进入场景选择
|
|
514
516
|
npx mdk-skills --help 显示帮助
|
|
515
517
|
|
|
516
518
|
首次运行选择场景后自动配置技能和 CLAUDE.md
|
|
517
519
|
再次运行可切换场景或进入自定义模式微调
|
|
518
520
|
|
|
519
521
|
示例:
|
|
522
|
+
npx mdk-skills --connect D:/dev/mdk-skills
|
|
520
523
|
npx mdk-skills connect D:/dev/mdk-skills
|
|
521
524
|
npx mdk-skills sync
|
|
522
525
|
npx mdk-skills disconnect
|
|
523
526
|
npx mdk-skills
|
|
524
527
|
npx mdk-skills list
|
|
525
528
|
`);
|
|
529
|
+
} else if (command === "--connect") {
|
|
530
|
+
cmdConnect(args[0]);
|
|
531
|
+
skillsSource = getSkillsSource(projectRoot, packageDir);
|
|
532
|
+
const profiles = loadProfiles();
|
|
533
|
+
if (profiles) {
|
|
534
|
+
await startSceneSelection();
|
|
535
|
+
} else {
|
|
536
|
+
await startInteractiveMenu();
|
|
537
|
+
}
|
|
526
538
|
} else if (command === "list") {
|
|
527
539
|
await cmdList();
|
|
528
540
|
} else if (command === "connect") {
|
|
@@ -531,6 +543,8 @@ async function main() {
|
|
|
531
543
|
cmdSync();
|
|
532
544
|
} else if (command === "disconnect") {
|
|
533
545
|
cmdDisconnect();
|
|
546
|
+
} else if (command === "ui") {
|
|
547
|
+
require("./web-ui/server");
|
|
534
548
|
} else {
|
|
535
549
|
console.log(` 未知命令: ${command}\n 可用: ${COMMANDS.join(", ")}\n`);
|
|
536
550
|
process.exit(1);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>mdk-skills 管理面板</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { createServer: createViteServer } = require("vite");
|
|
4
|
+
const core = require("../core");
|
|
5
|
+
|
|
6
|
+
// ---------- 路径(与 cli.js 保持一致) ----------
|
|
7
|
+
|
|
8
|
+
const projectRoot = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return fs.realpathSync.native(process.cwd());
|
|
11
|
+
} catch {
|
|
12
|
+
return process.cwd();
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
const packageDir = path.join(__dirname, "..", "..");
|
|
16
|
+
const skillsSource = core.getSkillsSource(projectRoot, packageDir);
|
|
17
|
+
const claudeDest = path.join(projectRoot, ".claude");
|
|
18
|
+
const skillsDest = path.join(claudeDest, "skills");
|
|
19
|
+
const settingsPath = path.join(claudeDest, "settings.json");
|
|
20
|
+
const pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
|
|
21
|
+
|
|
22
|
+
// ---------- 工具函数 ----------
|
|
23
|
+
|
|
24
|
+
function readSettings() {
|
|
25
|
+
if (!fs.existsSync(settingsPath))
|
|
26
|
+
return { skills: {}, always_apply_skills: [] };
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return { skills: {}, always_apply_skills: [] };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeSettings(settings) {
|
|
35
|
+
if (!fs.existsSync(claudeDest)) {
|
|
36
|
+
fs.mkdirSync(claudeDest, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
settingsPath,
|
|
40
|
+
JSON.stringify(settings, null, 2) + "\n",
|
|
41
|
+
"utf-8",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sendJSON(res, data, status = 200) {
|
|
46
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
47
|
+
res.end(JSON.stringify(data));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseBody(req) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
let body = "";
|
|
53
|
+
req.on("data", (chunk) => (body += chunk));
|
|
54
|
+
req.on("end", () => {
|
|
55
|
+
try {
|
|
56
|
+
resolve(JSON.parse(body));
|
|
57
|
+
} catch {
|
|
58
|
+
resolve({});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 手动递归拷贝目录
|
|
65
|
+
function copyDirSync(src, dest) {
|
|
66
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
67
|
+
for (const item of fs.readdirSync(src)) {
|
|
68
|
+
const srcPath = path.join(src, item);
|
|
69
|
+
const destPath = path.join(dest, item);
|
|
70
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
71
|
+
copyDirSync(srcPath, destPath);
|
|
72
|
+
} else {
|
|
73
|
+
fs.copyFileSync(srcPath, destPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 读取 profiles.json
|
|
79
|
+
function loadProfiles() {
|
|
80
|
+
const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
|
|
81
|
+
if (!fs.existsSync(profilesPath)) return null;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 应用场景
|
|
90
|
+
function applyProfile(profile) {
|
|
91
|
+
const pkgSkills = fs.existsSync(pkgSkillsSource)
|
|
92
|
+
? fs.readdirSync(pkgSkillsSource)
|
|
93
|
+
: [];
|
|
94
|
+
const settings = readSettings();
|
|
95
|
+
|
|
96
|
+
let selected;
|
|
97
|
+
if (profile.skills === null) {
|
|
98
|
+
return { selected: [], message: "自定义场景,不做自动切换" };
|
|
99
|
+
} else if (profile.skills.length === 0) {
|
|
100
|
+
selected = [];
|
|
101
|
+
} else {
|
|
102
|
+
selected = profile.skills.filter((s) => pkgSkills.includes(s));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 安装/删除技能
|
|
106
|
+
if (!fs.existsSync(skillsDest)) {
|
|
107
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
for (const name of pkgSkills) {
|
|
110
|
+
const src = path.join(pkgSkillsSource, name);
|
|
111
|
+
const dest = path.join(skillsDest, name);
|
|
112
|
+
if (selected.includes(name)) {
|
|
113
|
+
if (!fs.existsSync(dest)) {
|
|
114
|
+
try {
|
|
115
|
+
copyDirSync(src, dest);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error(`安装技能 "${name}" 失败:`, err.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (fs.existsSync(dest)) {
|
|
122
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 更新 settings
|
|
128
|
+
if (!settings.skills) settings.skills = {};
|
|
129
|
+
for (const name of pkgSkills) {
|
|
130
|
+
const enabled = selected.includes(name);
|
|
131
|
+
if (!settings.skills[name]) settings.skills[name] = {};
|
|
132
|
+
settings.skills[name].enabled = enabled;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (profile.always_apply) {
|
|
136
|
+
settings.always_apply_skills = profile.always_apply;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
settings._active_profile = profile.id;
|
|
140
|
+
writeSettings(settings);
|
|
141
|
+
|
|
142
|
+
// 复制 profiles.json
|
|
143
|
+
const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
|
|
144
|
+
const profilesDest = path.join(claudeDest, "profiles.json");
|
|
145
|
+
if (fs.existsSync(profilesSource)) {
|
|
146
|
+
fs.copyFileSync(profilesSource, profilesDest);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
selected,
|
|
151
|
+
enabled: selected.length,
|
|
152
|
+
disabled: pkgSkills.length - selected.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 安装勾选的技能
|
|
157
|
+
function installSelectedSkills(selectedNames) {
|
|
158
|
+
if (!fs.existsSync(pkgSkillsSource)) return { installed: 0 };
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(skillsDest)) {
|
|
161
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const allPkgSkills = fs.readdirSync(pkgSkillsSource);
|
|
165
|
+
let installedCount = 0;
|
|
166
|
+
|
|
167
|
+
for (const name of allPkgSkills) {
|
|
168
|
+
const src = path.join(pkgSkillsSource, name);
|
|
169
|
+
const dest = path.join(skillsDest, name);
|
|
170
|
+
|
|
171
|
+
if (selectedNames.includes(name)) {
|
|
172
|
+
if (!fs.existsSync(dest)) {
|
|
173
|
+
try {
|
|
174
|
+
copyDirSync(src, dest);
|
|
175
|
+
installedCount++;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`安装技能 "${name}" 失败:`, err.message);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
if (fs.existsSync(dest)) {
|
|
182
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const settings = readSettings();
|
|
188
|
+
if (!settings.skills) settings.skills = {};
|
|
189
|
+
for (const name of allPkgSkills) {
|
|
190
|
+
const enabled = selectedNames.includes(name);
|
|
191
|
+
if (!settings.skills[name]) settings.skills[name] = {};
|
|
192
|
+
settings.skills[name].enabled = enabled;
|
|
193
|
+
}
|
|
194
|
+
writeSettings(settings);
|
|
195
|
+
|
|
196
|
+
return { installed: installedCount };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------- API 路由 ----------
|
|
200
|
+
|
|
201
|
+
async function handleApi(req, res) {
|
|
202
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
203
|
+
const pathname = url.pathname;
|
|
204
|
+
const method = req.method;
|
|
205
|
+
|
|
206
|
+
// CORS
|
|
207
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
208
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
209
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
210
|
+
|
|
211
|
+
if (method === "OPTIONS") {
|
|
212
|
+
res.writeHead(204);
|
|
213
|
+
res.end();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// GET /api/skills — 获取所有技能列表(含启用状态)
|
|
219
|
+
if (method === "GET" && pathname === "/api/skills") {
|
|
220
|
+
const pkgSkills = core.getPackageSkills(skillsSource);
|
|
221
|
+
const userSkills = core.getUserSkills(claudeDest);
|
|
222
|
+
const userMap = new Map(userSkills.map((s) => [s.name, s]));
|
|
223
|
+
const result = pkgSkills.map((s) => ({
|
|
224
|
+
...s,
|
|
225
|
+
enabled: userMap.has(s.name) ? userMap.get(s.name).enabled : false,
|
|
226
|
+
installed: userMap.has(s.name),
|
|
227
|
+
}));
|
|
228
|
+
return sendJSON(res, result);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// POST /api/skills/toggle — 开关技能
|
|
232
|
+
if (method === "POST" && pathname === "/api/skills/toggle") {
|
|
233
|
+
const body = await parseBody(req);
|
|
234
|
+
const { name, enabled } = body;
|
|
235
|
+
if (!name) return sendJSON(res, { error: "缺少技能名" }, 400);
|
|
236
|
+
const settings = readSettings();
|
|
237
|
+
if (!settings.skills) settings.skills = {};
|
|
238
|
+
if (!settings.skills[name]) settings.skills[name] = {};
|
|
239
|
+
settings.skills[name].enabled = !!enabled;
|
|
240
|
+
writeSettings(settings);
|
|
241
|
+
return sendJSON(res, { ok: true, name, enabled: !!enabled });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// POST /api/skills/install — 自定义勾选安装
|
|
245
|
+
if (method === "POST" && pathname === "/api/skills/install") {
|
|
246
|
+
const body = await parseBody(req);
|
|
247
|
+
const { selected } = body;
|
|
248
|
+
if (!Array.isArray(selected))
|
|
249
|
+
return sendJSON(res, { error: "参数错误" }, 400);
|
|
250
|
+
const result = installSelectedSkills(selected);
|
|
251
|
+
return sendJSON(res, { ok: true, ...result });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// GET /api/profiles — 获取场景列表
|
|
255
|
+
if (method === "GET" && pathname === "/api/profiles") {
|
|
256
|
+
const profiles = loadProfiles();
|
|
257
|
+
const settings = readSettings();
|
|
258
|
+
return sendJSON(res, {
|
|
259
|
+
profiles: profiles || [],
|
|
260
|
+
activeProfile: settings._active_profile || null,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// POST /api/profiles/apply — 应用场景
|
|
265
|
+
if (method === "POST" && pathname === "/api/profiles/apply") {
|
|
266
|
+
const body = await parseBody(req);
|
|
267
|
+
const { profileId } = body;
|
|
268
|
+
if (!profileId) return sendJSON(res, { error: "缺少场景 ID" }, 400);
|
|
269
|
+
const profiles = loadProfiles();
|
|
270
|
+
const profile = profiles?.find((p) => p.id === profileId);
|
|
271
|
+
if (!profile)
|
|
272
|
+
return sendJSON(res, { error: "场景不存在: " + profileId }, 404);
|
|
273
|
+
const result = applyProfile(profile);
|
|
274
|
+
return sendJSON(res, { ok: true, ...result });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// GET /api/status — 当前状态总览
|
|
278
|
+
if (method === "GET" && pathname === "/api/status") {
|
|
279
|
+
const settings = readSettings();
|
|
280
|
+
const userSkills = core.getUserSkills(claudeDest);
|
|
281
|
+
const profiles = loadProfiles();
|
|
282
|
+
const activeProfile = profiles?.find(
|
|
283
|
+
(p) => p.id === settings._active_profile,
|
|
284
|
+
);
|
|
285
|
+
const enabledCount = userSkills.filter((s) => s.enabled).length;
|
|
286
|
+
const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
|
|
287
|
+
return sendJSON(res, {
|
|
288
|
+
enabledCount,
|
|
289
|
+
totalCount: userSkills.length,
|
|
290
|
+
activeProfile: activeProfile
|
|
291
|
+
? { id: activeProfile.id, name: activeProfile.name }
|
|
292
|
+
: null,
|
|
293
|
+
sourcePath: settings._skill_source || null,
|
|
294
|
+
hasClaudeMd: mdExists,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// GET /api/source — 获取本地源信息
|
|
299
|
+
if (method === "GET" && pathname === "/api/source") {
|
|
300
|
+
const settings = readSettings();
|
|
301
|
+
return sendJSON(res, {
|
|
302
|
+
connected: !!settings._skill_source,
|
|
303
|
+
path: settings._skill_source || null,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// POST /api/source/connect — 绑定本地源
|
|
308
|
+
if (method === "POST" && pathname === "/api/source/connect") {
|
|
309
|
+
const body = await parseBody(req);
|
|
310
|
+
const repoPath = body.path;
|
|
311
|
+
if (!repoPath)
|
|
312
|
+
return sendJSON(res, { error: "缺少仓库路径" }, 400);
|
|
313
|
+
const resolved = path.resolve(repoPath);
|
|
314
|
+
if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
|
|
315
|
+
return sendJSON(res, { error: `路径 "${resolved}" 下没有 .claude/skills/` }, 400);
|
|
316
|
+
}
|
|
317
|
+
const settings = readSettings();
|
|
318
|
+
settings._skill_source = resolved;
|
|
319
|
+
writeSettings(settings);
|
|
320
|
+
return sendJSON(res, { ok: true, path: resolved });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// POST /api/source/disconnect — 解绑本地源
|
|
324
|
+
if (method === "POST" && pathname === "/api/source/disconnect") {
|
|
325
|
+
const settings = readSettings();
|
|
326
|
+
if (!settings._skill_source) {
|
|
327
|
+
return sendJSON(res, { error: "当前未绑定任何技能源" }, 400);
|
|
328
|
+
}
|
|
329
|
+
delete settings._skill_source;
|
|
330
|
+
writeSettings(settings);
|
|
331
|
+
return sendJSON(res, { ok: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// POST /api/source/sync — 同步到本地源
|
|
335
|
+
if (method === "POST" && pathname === "/api/source/sync") {
|
|
336
|
+
const settings = readSettings();
|
|
337
|
+
const sourcePath = settings._skill_source;
|
|
338
|
+
if (!sourcePath)
|
|
339
|
+
return sendJSON(res, { error: "尚未绑定技能源" }, 400);
|
|
340
|
+
if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
|
|
341
|
+
return sendJSON(res, { error: "绑定的路径已失效" }, 400);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const repoClaude = path.join(sourcePath, ".claude");
|
|
345
|
+
core.backupDir(repoClaude);
|
|
346
|
+
|
|
347
|
+
// 技能反推
|
|
348
|
+
const repoSkills = path.join(sourcePath, ".claude", "skills");
|
|
349
|
+
if (!fs.existsSync(repoSkills))
|
|
350
|
+
fs.mkdirSync(repoSkills, { recursive: true });
|
|
351
|
+
if (fs.existsSync(skillsDest)) {
|
|
352
|
+
for (const name of fs.readdirSync(skillsDest)) {
|
|
353
|
+
const src = path.join(skillsDest, name);
|
|
354
|
+
const dest = path.join(repoSkills, name);
|
|
355
|
+
if (fs.existsSync(dest))
|
|
356
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
357
|
+
copyDirSync(src, dest);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// profiles.json 反推
|
|
362
|
+
const projectProfiles = path.join(claudeDest, "profiles.json");
|
|
363
|
+
if (fs.existsSync(projectProfiles)) {
|
|
364
|
+
fs.copyFileSync(
|
|
365
|
+
projectProfiles,
|
|
366
|
+
path.join(sourcePath, ".claude", "profiles.json"),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// settings.json 反推
|
|
371
|
+
const cleanSettings = { ...settings };
|
|
372
|
+
delete cleanSettings._skill_source;
|
|
373
|
+
delete cleanSettings._active_profile;
|
|
374
|
+
fs.writeFileSync(
|
|
375
|
+
path.join(sourcePath, ".claude", "settings.json"),
|
|
376
|
+
JSON.stringify(cleanSettings, null, 2) + "\n",
|
|
377
|
+
"utf-8",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// CLAUDE.md 反推
|
|
381
|
+
const projectMd = path.join(projectRoot, "CLAUDE.md");
|
|
382
|
+
if (fs.existsSync(projectMd)) {
|
|
383
|
+
fs.copyFileSync(projectMd, path.join(sourcePath, "CLAUDE.md"));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return sendJSON(res, { ok: true });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// GET /api/diagnose — 健康检查
|
|
390
|
+
if (method === "GET" && pathname === "/api/diagnose") {
|
|
391
|
+
const results = [];
|
|
392
|
+
const pkgSkills = core.getPackageSkills(skillsSource);
|
|
393
|
+
for (const skill of pkgSkills) {
|
|
394
|
+
const skillDir = path.join(skillsDest, skill.name);
|
|
395
|
+
const issues = [];
|
|
396
|
+
if (!fs.existsSync(skillDir)) {
|
|
397
|
+
issues.push("未安装");
|
|
398
|
+
} else {
|
|
399
|
+
const metaPath = path.join(skillDir, ".meta.json");
|
|
400
|
+
if (!fs.existsSync(metaPath)) {
|
|
401
|
+
issues.push("缺少 .meta.json");
|
|
402
|
+
} else {
|
|
403
|
+
try {
|
|
404
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
405
|
+
if (!meta.version) issues.push("版本号缺失");
|
|
406
|
+
} catch {
|
|
407
|
+
issues.push(".meta.json 格式错误");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
results.push({
|
|
412
|
+
name: skill.name,
|
|
413
|
+
healthy: issues.length === 0,
|
|
414
|
+
issues,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return sendJSON(res, results);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 404
|
|
421
|
+
sendJSON(res, { error: "Not Found: " + pathname }, 404);
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error("API 错误:", err);
|
|
424
|
+
sendJSON(res, { error: err.message }, 500);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---------- 启动 ----------
|
|
429
|
+
|
|
430
|
+
async function startUI() {
|
|
431
|
+
const vite = await createViteServer({
|
|
432
|
+
root: __dirname,
|
|
433
|
+
server: { port: 3344, open: false },
|
|
434
|
+
plugins: [
|
|
435
|
+
{
|
|
436
|
+
name: "api-routes",
|
|
437
|
+
configureServer(viteServer) {
|
|
438
|
+
viteServer.middlewares.use((req, res, next) => {
|
|
439
|
+
if (req.url.startsWith("/api")) {
|
|
440
|
+
handleApi(req, res);
|
|
441
|
+
} else {
|
|
442
|
+
next();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await vite.listen();
|
|
451
|
+
const port = vite.config.server.port;
|
|
452
|
+
console.log(`\n 🖥️ mdk-skills Web UI`);
|
|
453
|
+
console.log(` ─────────────────────`);
|
|
454
|
+
console.log(` 地址: http://localhost:${port}\n`);
|
|
455
|
+
console.log(` 按 Ctrl+C 停止服务\n`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
startUI().catch((err) => {
|
|
459
|
+
console.error("启动失败:", err);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<n-message-provider>
|
|
3
|
+
<n-notification-provider>
|
|
4
|
+
<n-config-provider :theme="theme">
|
|
5
|
+
<n-layout class="app-layout">
|
|
6
|
+
<!-- 导航栏 -->
|
|
7
|
+
<n-layout-header class="app-header" bordered>
|
|
8
|
+
<div class="header-inner">
|
|
9
|
+
<div class="header-left">
|
|
10
|
+
<span class="logo">mdk-skills</span>
|
|
11
|
+
<n-menu
|
|
12
|
+
v-model:value="activeKey"
|
|
13
|
+
mode="horizontal"
|
|
14
|
+
:options="menuOptions"
|
|
15
|
+
@update:value="onMenuChange"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<n-tag v-if="statusData" :type="statusData.sourcePath ? 'success' : 'default'" size="small">
|
|
19
|
+
{{ statusData.sourcePath ? "本地源" : "npm 源" }}
|
|
20
|
+
</n-tag>
|
|
21
|
+
</div>
|
|
22
|
+
</n-layout-header>
|
|
23
|
+
|
|
24
|
+
<!-- 状态条 -->
|
|
25
|
+
<StatusBar v-if="statusData" :data="statusData" />
|
|
26
|
+
|
|
27
|
+
<!-- 内容区 -->
|
|
28
|
+
<n-layout-content class="app-content">
|
|
29
|
+
<router-view @refresh="loadStatus" />
|
|
30
|
+
</n-layout-content>
|
|
31
|
+
</n-layout>
|
|
32
|
+
</n-config-provider>
|
|
33
|
+
</n-notification-provider>
|
|
34
|
+
</n-message-provider>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { h, ref, onMounted, computed } from "vue";
|
|
39
|
+
import { useRouter, useRoute } from "vue-router";
|
|
40
|
+
import { NIcon, useMessage } from "naive-ui";
|
|
41
|
+
import {
|
|
42
|
+
DashboardOutlined,
|
|
43
|
+
SwapOutlined,
|
|
44
|
+
SettingOutlined,
|
|
45
|
+
} from "@vicons/material";
|
|
46
|
+
import StatusBar from "./components/StatusBar.vue";
|
|
47
|
+
import { getStatus } from "./api/skills";
|
|
48
|
+
|
|
49
|
+
const router = useRouter();
|
|
50
|
+
const route = useRoute();
|
|
51
|
+
const message = useMessage();
|
|
52
|
+
|
|
53
|
+
const statusData = ref(null);
|
|
54
|
+
|
|
55
|
+
const theme = ref(null);
|
|
56
|
+
|
|
57
|
+
const menuOptions = [
|
|
58
|
+
{
|
|
59
|
+
label: "仪表盘",
|
|
60
|
+
key: "Dashboard",
|
|
61
|
+
icon: () => h(NIcon, null, { default: () => h(DashboardOutlined) }),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "场景切换",
|
|
65
|
+
key: "Scenes",
|
|
66
|
+
icon: () => h(NIcon, null, { default: () => h(SwapOutlined) }),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "设置",
|
|
70
|
+
key: "Settings",
|
|
71
|
+
icon: () => h(NIcon, null, { default: () => h(SettingOutlined) }),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const activeKey = ref(route.name || "Dashboard");
|
|
76
|
+
|
|
77
|
+
function onMenuChange(key) {
|
|
78
|
+
router.push({ name: key });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
router.afterEach((to) => {
|
|
82
|
+
activeKey.value = to.name;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
async function loadStatus() {
|
|
86
|
+
try {
|
|
87
|
+
statusData.value = await getStatus();
|
|
88
|
+
} catch {
|
|
89
|
+
// 静默失败
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onMounted(loadStatus);
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<style>
|
|
97
|
+
@import "./styles/main.css";
|
|
98
|
+
</style>
|