mdk-skills 2.2.22 → 2.2.23

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.
Files changed (134) hide show
  1. package/.claude/.install.log +3 -0
  2. package/.claude/backups/20260511.145306/.install.log +39 -0
  3. package/.claude/backups/20260511.145306/profiles.json +22 -0
  4. package/.claude/backups/20260511.145306/settings.json +11 -0
  5. package/.claude/backups/CLAUDE.md.20260511.145306 +131 -0
  6. package/.claude/profiles.json +10 -61
  7. package/.claude/settings.json +9 -27
  8. package/package.json +9 -4
  9. package/scripts/core.js +49 -4
  10. package/scripts/web-ui/dist/assets/index-B3SLlTdd.css +1 -0
  11. package/scripts/web-ui/dist/assets/index-DanvMbHP.js +7440 -0
  12. package/scripts/web-ui/dist/index.html +13 -0
  13. package/scripts/web-ui/server.js +1013 -860
  14. package/scripts/web-ui/src/App.vue +148 -125
  15. package/scripts/web-ui/src/api/skills.js +20 -0
  16. package/scripts/web-ui/src/components/SkillCard.vue +9 -0
  17. package/scripts/web-ui/src/components/StatusBar.vue +13 -0
  18. package/scripts/web-ui/src/styles/main.css +95 -0
  19. package/scripts/web-ui/src/utils/usage.js +46 -0
  20. package/scripts/web-ui/src/views/Dashboard.vue +389 -20
  21. package/scripts/web-ui/src/views/SceneSwitch.vue +4 -0
  22. package/scripts/web-ui/src/views/Settings.vue +44 -2
  23. package/scripts/web-ui/vite.config.js +11 -0
  24. package/.claude/backups/20260510.153300/.install.log +0 -10
  25. package/.claude/backups/20260510.153300/profiles.json +0 -67
  26. package/.claude/backups/20260510.153300/settings.json +0 -29
  27. package/.claude/backups/20260510.153300/skills/frontend-code-review/.meta.json +0 -6
  28. package/.claude/backups/20260510.153300/skills/frontend-code-review/SKILL.md +0 -167
  29. package/.claude/backups/20260510.153300/skills/frontend-code-review/references/checklist.md +0 -298
  30. package/.claude/backups/20260510.153300/skills/frontend-design/.meta.json +0 -6
  31. package/.claude/backups/20260510.153300/skills/frontend-design/LICENSE.txt +0 -177
  32. package/.claude/backups/20260510.153300/skills/frontend-design/SKILL.md +0 -42
  33. package/.claude/backups/20260510.153300/skills/skill-creator/.meta.json +0 -6
  34. package/.claude/backups/20260510.153300/skills/skill-creator/SKILL.md +0 -356
  35. package/.claude/backups/20260510.153300/skills/skill-creator/references/output-patterns.md +0 -82
  36. package/.claude/backups/20260510.153300/skills/skill-creator/references/workflows.md +0 -28
  37. package/.claude/backups/20260510.153300/skills/skill-creator/scripts/init_skill.py +0 -303
  38. package/.claude/backups/20260510.153300/skills/skill-creator/scripts/package_skill.py +0 -110
  39. package/.claude/backups/20260510.153300/skills/skill-creator/scripts/quick_validate.py +0 -95
  40. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/.meta.json +0 -6
  41. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/SKILL.md +0 -228
  42. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/charts.csv +0 -26
  43. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/colors.csv +0 -97
  44. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/landing.csv +0 -31
  45. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/products.csv +0 -97
  46. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/prompts.csv +0 -24
  47. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  48. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  49. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  50. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  51. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  52. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  53. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/react.csv +0 -54
  54. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  55. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  56. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  57. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/styles.csv +0 -59
  58. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/typography.csv +0 -58
  59. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  60. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/scripts/core.py +0 -238
  61. package/.claude/backups/20260510.153300/skills/ui-ux-pro-max/scripts/search.py +0 -61
  62. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/.meta.json +0 -6
  63. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/SKILL.md +0 -26
  64. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/references/infinite-scroll.md +0 -292
  65. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/references/pinia-store.md +0 -174
  66. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/references/service-layer.md +0 -198
  67. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/references/tab-anchor.md +0 -1125
  68. package/.claude/backups/20260510.153300/skills/v3-fe-biz-patterns/references/use-loading.md +0 -114
  69. package/.claude/backups/20260510.153300/skills/vue/.meta.json +0 -6
  70. package/.claude/backups/20260510.153300/skills/vue/SKILL.md +0 -103
  71. package/.claude/backups/20260510.153300/skills/vue/references/components.md +0 -323
  72. package/.claude/backups/20260510.153300/skills/vue/references/composables.md +0 -358
  73. package/.claude/backups/20260510.153300/skills/vue/references/directives.md +0 -225
  74. package/.claude/backups/20260510.153300/skills/vue/references/gotchas.md +0 -438
  75. package/.claude/backups/20260510.153300/skills/vue/references/provide-inject.md +0 -174
  76. package/.claude/backups/20260510.153300/skills/vue/references/reactivity.md +0 -289
  77. package/.claude/backups/20260510.153300/skills/vue/references/router.md +0 -181
  78. package/.claude/backups/20260510.153300/skills/vue/references/testing.md +0 -294
  79. package/.claude/backups/20260510.153300/skills/vue/references/typescript.md +0 -172
  80. package/.claude/backups/20260510.153300/skills/vue/references/utils-client.md +0 -156
  81. package/.claude/skills/frontend-code-review/.meta.json +0 -6
  82. package/.claude/skills/frontend-code-review/SKILL.md +0 -167
  83. package/.claude/skills/frontend-code-review/references/checklist.md +0 -298
  84. package/.claude/skills/frontend-design/.meta.json +0 -6
  85. package/.claude/skills/frontend-design/LICENSE.txt +0 -177
  86. package/.claude/skills/frontend-design/SKILL.md +0 -42
  87. package/.claude/skills/skill-creator/.meta.json +0 -6
  88. package/.claude/skills/skill-creator/SKILL.md +0 -356
  89. package/.claude/skills/skill-creator/references/output-patterns.md +0 -82
  90. package/.claude/skills/skill-creator/references/workflows.md +0 -28
  91. package/.claude/skills/skill-creator/scripts/init_skill.py +0 -303
  92. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -110
  93. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -95
  94. package/.claude/skills/ui-ux-pro-max/.meta.json +0 -6
  95. package/.claude/skills/ui-ux-pro-max/SKILL.md +0 -228
  96. package/.claude/skills/ui-ux-pro-max/data/charts.csv +0 -26
  97. package/.claude/skills/ui-ux-pro-max/data/colors.csv +0 -97
  98. package/.claude/skills/ui-ux-pro-max/data/landing.csv +0 -31
  99. package/.claude/skills/ui-ux-pro-max/data/products.csv +0 -97
  100. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +0 -24
  101. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  102. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  103. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  104. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  105. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  106. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  107. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +0 -54
  108. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  109. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  110. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  111. package/.claude/skills/ui-ux-pro-max/data/styles.csv +0 -59
  112. package/.claude/skills/ui-ux-pro-max/data/typography.csv +0 -58
  113. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  114. package/.claude/skills/ui-ux-pro-max/scripts/core.py +0 -238
  115. package/.claude/skills/ui-ux-pro-max/scripts/search.py +0 -61
  116. package/.claude/skills/v3-fe-biz-patterns/.meta.json +0 -6
  117. package/.claude/skills/v3-fe-biz-patterns/SKILL.md +0 -26
  118. package/.claude/skills/v3-fe-biz-patterns/references/infinite-scroll.md +0 -292
  119. package/.claude/skills/v3-fe-biz-patterns/references/pinia-store.md +0 -174
  120. package/.claude/skills/v3-fe-biz-patterns/references/service-layer.md +0 -198
  121. package/.claude/skills/v3-fe-biz-patterns/references/tab-anchor.md +0 -1125
  122. package/.claude/skills/v3-fe-biz-patterns/references/use-loading.md +0 -114
  123. package/.claude/skills/vue/.meta.json +0 -6
  124. package/.claude/skills/vue/SKILL.md +0 -103
  125. package/.claude/skills/vue/references/components.md +0 -323
  126. package/.claude/skills/vue/references/composables.md +0 -358
  127. package/.claude/skills/vue/references/directives.md +0 -225
  128. package/.claude/skills/vue/references/gotchas.md +0 -438
  129. package/.claude/skills/vue/references/provide-inject.md +0 -174
  130. package/.claude/skills/vue/references/reactivity.md +0 -289
  131. package/.claude/skills/vue/references/router.md +0 -181
  132. package/.claude/skills/vue/references/testing.md +0 -294
  133. package/.claude/skills/vue/references/typescript.md +0 -172
  134. package/.claude/skills/vue/references/utils-client.md +0 -156
@@ -1,860 +1,1013 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { createServer: createViteServer } = require("vite");
4
- const vue = require("@vitejs/plugin-vue");
5
- const core = require("../core");
6
-
7
- // ---------- 路径(与 cli.js 保持一致) ----------
8
-
9
- const projectRoot = (() => {
10
- try {
11
- return fs.realpathSync.native(process.cwd());
12
- } catch {
13
- return process.cwd();
14
- }
15
- })();
16
- const packageDir = path.join(__dirname, "..", "..");
17
- let skillsSource = core.getSkillsSource(projectRoot, packageDir);
18
- const claudeDest = path.join(projectRoot, ".claude");
19
- const skillsDest = path.join(claudeDest, "skills");
20
- const settingsPath = path.join(claudeDest, "settings.json");
21
- let pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
22
-
23
- // 连接/断开本地源后刷新技能源路径
24
- function refreshSource() {
25
- skillsSource = core.getSkillsSource(projectRoot, packageDir);
26
- pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
27
- }
28
-
29
- // ---------- 工具函数 ----------
30
-
31
- function readSettings() {
32
- if (!fs.existsSync(settingsPath))
33
- return { skills: {}, always_apply_skills: [] };
34
- try {
35
- return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
36
- } catch {
37
- return { skills: {}, always_apply_skills: [] };
38
- }
39
- }
40
-
41
- function writeSettings(settings) {
42
- if (!fs.existsSync(claudeDest)) {
43
- fs.mkdirSync(claudeDest, { recursive: true });
44
- }
45
- fs.writeFileSync(
46
- settingsPath,
47
- JSON.stringify(settings, null, 2) + "\n",
48
- "utf-8",
49
- );
50
- }
51
-
52
- function sendJSON(res, data, status = 200) {
53
- res.writeHead(status, { "Content-Type": "application/json" });
54
- res.end(JSON.stringify(data));
55
- }
56
-
57
- function parseBody(req) {
58
- return new Promise((resolve) => {
59
- let body = "";
60
- req.on("data", (chunk) => (body += chunk));
61
- req.on("end", () => {
62
- try {
63
- resolve(JSON.parse(body));
64
- } catch {
65
- resolve({});
66
- }
67
- });
68
- });
69
- }
70
-
71
- // 手动递归拷贝目录
72
- function copyDirSync(src, dest) {
73
- fs.mkdirSync(dest, { recursive: true });
74
- for (const item of fs.readdirSync(src)) {
75
- const srcPath = path.join(src, item);
76
- const destPath = path.join(dest, item);
77
- if (fs.statSync(srcPath).isDirectory()) {
78
- copyDirSync(srcPath, destPath);
79
- } else {
80
- fs.copyFileSync(srcPath, destPath);
81
- }
82
- }
83
- }
84
-
85
- // 读取 profiles.json
86
- function loadProfiles() {
87
- const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
88
- if (!fs.existsSync(profilesPath)) return null;
89
- try {
90
- return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
91
- } catch {
92
- return null;
93
- }
94
- }
95
-
96
- // 应用场景
97
- function applyProfile(profile) {
98
- const pkgSkills = core.listSkillDirs(pkgSkillsSource);
99
- const settings = readSettings();
100
-
101
- let selected;
102
- if (profile.skills === null) {
103
- return { selected: [], message: "自定义场景,不做自动切换" };
104
- } else if (profile.skills.length === 0) {
105
- selected = [];
106
- } else {
107
- selected = profile.skills.filter((s) => pkgSkills.includes(s));
108
- }
109
-
110
- // 安装/删除技能
111
- if (!fs.existsSync(skillsDest)) {
112
- fs.mkdirSync(skillsDest, { recursive: true });
113
- }
114
- for (const name of pkgSkills) {
115
- const src = path.join(pkgSkillsSource, name);
116
- const dest = path.join(skillsDest, name);
117
- if (selected.includes(name)) {
118
- if (!fs.existsSync(dest)) {
119
- try {
120
- copyDirSync(src, dest);
121
- } catch (err) {
122
- console.error(`安装技能 "${name}" 失败:`, err.message);
123
- }
124
- }
125
- } else {
126
- if (fs.existsSync(dest)) {
127
- fs.rmSync(dest, { recursive: true, force: true });
128
- }
129
- }
130
- }
131
-
132
- // 更新 settings
133
- if (!settings.skills) settings.skills = {};
134
- for (const name of pkgSkills) {
135
- const enabled = selected.includes(name);
136
- if (!settings.skills[name]) settings.skills[name] = {};
137
- settings.skills[name].enabled = enabled;
138
- }
139
-
140
- if (profile.always_apply) {
141
- settings.always_apply_skills = profile.always_apply;
142
- }
143
-
144
- settings._active_profile = profile.id;
145
- writeSettings(settings);
146
-
147
- // 复制 profiles.json
148
- const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
149
- const profilesDest = path.join(claudeDest, "profiles.json");
150
- if (fs.existsSync(profilesSource)) {
151
- fs.copyFileSync(profilesSource, profilesDest);
152
- }
153
-
154
- // 复制 CLAUDE.md(不存在才装)
155
- const mdSource = path.join(skillsSource, "CLAUDE.md");
156
- const mdDest = path.join(projectRoot, "CLAUDE.md");
157
- if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
158
- fs.copyFileSync(mdSource, mdDest);
159
- }
160
-
161
- return {
162
- selected,
163
- enabled: selected.length,
164
- disabled: pkgSkills.length - selected.length,
165
- };
166
- }
167
-
168
- // 安装勾选的技能
169
- function installSelectedSkills(selectedNames) {
170
- if (!fs.existsSync(pkgSkillsSource)) return { installed: 0 };
171
-
172
- if (!fs.existsSync(skillsDest)) {
173
- fs.mkdirSync(skillsDest, { recursive: true });
174
- }
175
-
176
- const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
177
- let installedCount = 0;
178
-
179
- for (const name of allPkgSkills) {
180
- const src = path.join(pkgSkillsSource, name);
181
- const dest = path.join(skillsDest, name);
182
-
183
- if (selectedNames.includes(name)) {
184
- if (!fs.existsSync(dest)) {
185
- try {
186
- copyDirSync(src, dest);
187
- installedCount++;
188
- } catch (err) {
189
- console.error(`安装技能 "${name}" 失败:`, err.message);
190
- }
191
- }
192
- } else {
193
- if (fs.existsSync(dest)) {
194
- fs.rmSync(dest, { recursive: true, force: true });
195
- }
196
- }
197
- }
198
-
199
- const settings = readSettings();
200
- if (!settings.skills) settings.skills = {};
201
- for (const name of allPkgSkills) {
202
- const enabled = selectedNames.includes(name);
203
- if (!settings.skills[name]) settings.skills[name] = {};
204
- settings.skills[name].enabled = enabled;
205
- }
206
- writeSettings(settings);
207
-
208
- // 复制 CLAUDE.md(不存在才装)
209
- const mdSource = path.join(skillsSource, "CLAUDE.md");
210
- const mdDest = path.join(projectRoot, "CLAUDE.md");
211
- if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
212
- fs.copyFileSync(mdSource, mdDest);
213
- }
214
-
215
- return { installed: installedCount };
216
- }
217
-
218
- // ---------- API 路由 ----------
219
-
220
- async function handleApi(req, res) {
221
- const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
222
- const pathname = url.pathname;
223
- const method = req.method;
224
-
225
- // CORS & 防缓存
226
- res.setHeader("Access-Control-Allow-Origin", "*");
227
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
228
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
229
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
230
-
231
- if (method === "OPTIONS") {
232
- res.writeHead(204);
233
- res.end();
234
- return;
235
- }
236
-
237
- try {
238
- // GET /api/skills — 获取所有技能列表(含启用状态),同时清理已失效的技能
239
- if (method === "GET" && pathname === "/api/skills") {
240
- const pkgSkills = core.getPackageSkills(skillsSource);
241
- const pkgNames = new Set(pkgSkills.map((s) => s.name));
242
- const userSkills = core.getUserSkills(claudeDest);
243
- // 清理:源目录已删除的技能,项目中也一并移除
244
- let cleaned = false;
245
- for (const us of userSkills) {
246
- if (!pkgNames.has(us.name)) {
247
- const dest = path.join(skillsDest, us.name);
248
- if (fs.existsSync(dest)) {
249
- fs.rmSync(dest, { recursive: true, force: true });
250
- cleaned = true;
251
- }
252
- }
253
- }
254
- if (cleaned) {
255
- const settings = readSettings();
256
- for (const name of Object.keys(settings.skills || {})) {
257
- if (!pkgNames.has(name)) delete settings.skills[name];
258
- }
259
- writeSettings(settings);
260
- }
261
- const refreshed = core.getUserSkills(claudeDest);
262
- const userMap = new Map(refreshed.map((s) => [s.name, s]));
263
- const result = pkgSkills.map((s) => ({
264
- ...s,
265
- enabled: userMap.has(s.name) ? userMap.get(s.name).enabled : false,
266
- installed: userMap.has(s.name),
267
- }));
268
- return sendJSON(res, result);
269
- }
270
-
271
- // POST /api/skills/toggle — 开关技能(启用时自动安装)
272
- if (method === "POST" && pathname === "/api/skills/toggle") {
273
- const body = await parseBody(req);
274
- const { name, enabled } = body;
275
- if (!name) return sendJSON(res, { error: "缺少技能名" }, 400);
276
- if (enabled) {
277
- const src = path.join(pkgSkillsSource, name);
278
- const dest = path.join(skillsDest, name);
279
- if (fs.existsSync(src) && !fs.existsSync(dest)) {
280
- copyDirSync(src, dest);
281
- }
282
- }
283
- const settings = readSettings();
284
- if (!settings.skills) settings.skills = {};
285
- if (!settings.skills[name]) settings.skills[name] = {};
286
- settings.skills[name].enabled = !!enabled;
287
- writeSettings(settings);
288
- return sendJSON(res, { ok: true, name, enabled: !!enabled });
289
- }
290
-
291
- // POST /api/skills/install — 自定义勾选安装
292
- if (method === "POST" && pathname === "/api/skills/install") {
293
- const body = await parseBody(req);
294
- const { selected } = body;
295
- if (!Array.isArray(selected))
296
- return sendJSON(res, { error: "参数错误" }, 400);
297
- const result = installSelectedSkills(selected);
298
- return sendJSON(res, { ok: true, ...result });
299
- }
300
-
301
- // GET /api/readme获取根目录 README.md
302
- if (method === "GET" && pathname === "/api/readme") {
303
- const p = path.join(skillsSource, "README.md");
304
- const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
305
- return sendJSON(res, { content, found: !!content });
306
- }
307
-
308
- // GET /api/skills-readme 获取技能目录 README.md
309
- if (method === "GET" && pathname === "/api/skills-readme") {
310
- const p = path.join(skillsSource, ".claude", "skills", "README.md");
311
- const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
312
- return sendJSON(res, { content, found: !!content });
313
- }
314
-
315
- // GET /api/claudemd — 获取 CLAUDE.md 内容
316
- if (method === "GET" && pathname === "/api/claudemd") {
317
- const paths = [
318
- path.join(skillsSource, "CLAUDE.md"),
319
- path.join(projectRoot, "CLAUDE.md"),
320
- ];
321
- let content = null;
322
- for (const p of paths) {
323
- if (fs.existsSync(p)) {
324
- content = fs.readFileSync(p, "utf-8");
325
- break;
326
- }
327
- }
328
- return sendJSON(res, { content, found: !!content });
329
- }
330
-
331
- // GET /api/skills/:name/readme 获取技能的 SKILL.md
332
- const readmeMatch = pathname.match(/^\/api\/skills\/([^/]+)\/readme$/);
333
- if (method === "GET" && readmeMatch) {
334
- const name = readmeMatch[1];
335
- const skillDir = path.join(skillsDest, name);
336
- const altDir = path.join(pkgSkillsSource, name);
337
- const readmePath = [
338
- path.join(skillDir, "SKILL.md"),
339
- path.join(altDir, "SKILL.md"),
340
- ].find((p) => fs.existsSync(p));
341
- const content = readmePath ? fs.readFileSync(readmePath, "utf-8") : null;
342
- return sendJSON(res, { content, found: !!readmePath });
343
- }
344
-
345
- // GET /api/profiles — 获取场景列表(含只读状态)
346
- if (method === "GET" && pathname === "/api/profiles") {
347
- const profiles = loadProfiles();
348
- const settings = readSettings();
349
- return sendJSON(res, {
350
- profiles: profiles || [],
351
- activeProfile: settings._active_profile || null,
352
- readonly: !settings._skill_source,
353
- });
354
- }
355
-
356
- // POST /api/profiles/apply — 应用场景
357
- if (method === "POST" && pathname === "/api/profiles/apply") {
358
- const body = await parseBody(req);
359
- const { profileId } = body;
360
- if (!profileId) return sendJSON(res, { error: "缺少场景 ID" }, 400);
361
- const profiles = loadProfiles();
362
- const profile = profiles?.find((p) => p.id === profileId);
363
- if (!profile)
364
- return sendJSON(res, { error: "场景不存在: " + profileId }, 404);
365
- const result = applyProfile(profile);
366
- return sendJSON(res, { ok: true, ...result });
367
- }
368
-
369
- // POST /api/profiles/save 新增/编辑场景(仅本地源模式)
370
- if (method === "POST" && pathname === "/api/profiles/save") {
371
- const body = await parseBody(req);
372
- const settings = readSettings();
373
- if (!settings._skill_source)
374
- return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
375
-
376
- const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
377
- let data = { version: 1, profiles: [] };
378
- if (fs.existsSync(profilesPath)) {
379
- try {
380
- data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
381
- } catch {}
382
- }
383
-
384
- const { id, name, description, skills, always_apply } = body;
385
- if (!name) return sendJSON(res, { error: "场景名称不能为空" }, 400);
386
-
387
- if (id) {
388
- const index = data.profiles.findIndex((p) => p.id === id);
389
- if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
390
- data.profiles[index] = {
391
- ...data.profiles[index],
392
- name,
393
- description: description || "",
394
- skills: skills || [],
395
- always_apply: always_apply || [],
396
- };
397
- } else {
398
- const newId = "profile_" + Date.now().toString(36);
399
- data.profiles.push({
400
- id: newId,
401
- name,
402
- description: description || "",
403
- skills: skills || [],
404
- always_apply: always_apply || [],
405
- });
406
- }
407
-
408
- fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
409
-
410
- // 同步到项目 .claude
411
- const projectProfilesPath = path.join(claudeDest, "profiles.json");
412
- fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
413
-
414
- return sendJSON(res, { ok: true, profiles: data.profiles });
415
- }
416
-
417
- // POST /api/profiles/delete 删除场景(仅本地源模式)
418
- if (method === "POST" && pathname === "/api/profiles/delete") {
419
- const body = await parseBody(req);
420
- const settings = readSettings();
421
- if (!settings._skill_source)
422
- return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
423
-
424
- const { id } = body;
425
- if (!id) return sendJSON(res, { error: "缺少场景 ID" }, 400);
426
- if (id === "custom")
427
- return sendJSON(res, { error: "不能删除内置场景" }, 403);
428
-
429
- const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
430
- if (!fs.existsSync(profilesPath))
431
- return sendJSON(res, { error: "场景文件不存在" }, 404);
432
-
433
- const data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
434
- const index = data.profiles.findIndex((p) => p.id === id);
435
- if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
436
-
437
- data.profiles.splice(index, 1);
438
- fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
439
-
440
- // 同步到项目 .claude
441
- const projectProfilesPath = path.join(claudeDest, "profiles.json");
442
- fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
443
-
444
- return sendJSON(res, { ok: true });
445
- }
446
-
447
- // GET /api/status — 当前状态总览
448
- if (method === "GET" && pathname === "/api/status") {
449
- const settings = readSettings();
450
- const userSkills = core.getUserSkills(claudeDest);
451
- const pkgSkills = core.getPackageSkills(skillsSource);
452
- const profiles = loadProfiles();
453
- const activeProfile = profiles?.find(
454
- (p) => p.id === settings._active_profile,
455
- );
456
- const enabledCount = userSkills.filter((s) => s.enabled).length;
457
- const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
458
- return sendJSON(res, {
459
- pkgSkills,
460
- enabledCount,
461
- totalCount: pkgSkills.length,
462
- activeProfile: activeProfile
463
- ? { id: activeProfile.id, name: activeProfile.name }
464
- : null,
465
- sourcePath: settings._skill_source || null,
466
- hasClaudeMd: mdExists,
467
- });
468
- }
469
-
470
- // GET /api/source 获取本地源信息(含初始化检测)
471
- if (method === "GET" && pathname === "/api/source") {
472
- const settings = readSettings();
473
- const sourcePath = settings._skill_source || null;
474
-
475
- let needsInit = false;
476
- if (sourcePath) {
477
- const claudeDir = path.join(sourcePath, ".claude");
478
- if (fs.existsSync(claudeDir)) {
479
- // 检查 profiles.json
480
- const hasProfiles = fs.existsSync(path.join(claudeDir, "profiles.json"));
481
- // 检查 settings.json
482
- const hasSettings = fs.existsSync(path.join(claudeDir, "settings.json"));
483
- // 检查每个 skill 目录的 .meta.json
484
- const skillsDir = path.join(claudeDir, "skills");
485
- let allMeta = true;
486
- if (fs.existsSync(skillsDir)) {
487
- for (const name of core.listSkillDirs(skillsDir)) {
488
- if (!fs.existsSync(path.join(skillsDir, name, ".meta.json"))) {
489
- allMeta = false;
490
- break;
491
- }
492
- }
493
- }
494
- needsInit = !hasProfiles || !hasSettings || !allMeta;
495
- } else {
496
- needsInit = true;
497
- }
498
- }
499
-
500
- // README.md 缺失状态(独立于骨架文件)
501
- let readmeStatus = null;
502
- if (sourcePath) {
503
- const skillsDir = path.join(sourcePath, ".claude", "skills");
504
- readmeStatus = {
505
- root: fs.existsSync(path.join(sourcePath, "README.md")),
506
- skills: fs.existsSync(path.join(skillsDir, "README.md")),
507
- };
508
- }
509
-
510
- return sendJSON(res, {
511
- connected: !!sourcePath,
512
- path: sourcePath,
513
- needsInit,
514
- readmeStatus,
515
- });
516
- }
517
-
518
- // POST /api/source/connect — 绑定本地源
519
- if (method === "POST" && pathname === "/api/source/connect") {
520
- const body = await parseBody(req);
521
- const repoPath = body.path;
522
- if (!repoPath) return sendJSON(res, { error: "缺少仓库路径" }, 400);
523
- const resolved = path.resolve(repoPath);
524
- if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
525
- return sendJSON(
526
- res,
527
- { error: `路径 "${resolved}" 下没有 .claude/skills/` },
528
- 400,
529
- );
530
- }
531
- const settings = readSettings();
532
- settings._skill_source = resolved;
533
- writeSettings(settings);
534
- // 复制 CLAUDE.md(不存在才装)
535
- const mdSource = path.join(resolved, "CLAUDE.md");
536
- const mdDest = path.join(projectRoot, "CLAUDE.md");
537
- if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
538
- fs.copyFileSync(mdSource, mdDest);
539
- }
540
-
541
- refreshSource();
542
- return sendJSON(res, { ok: true, path: resolved });
543
- }
544
-
545
- // POST /api/source/disconnect 解绑本地源
546
- if (method === "POST" && pathname === "/api/source/disconnect") {
547
- const settings = readSettings();
548
- if (!settings._skill_source) {
549
- return sendJSON(res, { error: "当前未绑定任何技能源" }, 400);
550
- }
551
- delete settings._skill_source;
552
- writeSettings(settings);
553
- refreshSource();
554
- return sendJSON(res, { ok: true });
555
- }
556
-
557
- // POST /api/source/sync — 同步到本地源
558
- if (method === "POST" && pathname === "/api/source/sync") {
559
- const settings = readSettings();
560
- const sourcePath = settings._skill_source;
561
- if (!sourcePath) return sendJSON(res, { error: "尚未绑定技能源" }, 400);
562
- if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
563
- return sendJSON(res, { error: "绑定的路径已失效" }, 400);
564
- }
565
-
566
- const repoClaude = path.join(sourcePath, ".claude");
567
- core.backupDir(repoClaude);
568
-
569
- // 技能反推
570
- const repoSkills = path.join(sourcePath, ".claude", "skills");
571
- if (!fs.existsSync(repoSkills))
572
- fs.mkdirSync(repoSkills, { recursive: true });
573
- if (fs.existsSync(skillsDest)) {
574
- for (const name of core.listSkillDirs(skillsDest)) {
575
- const src = path.join(skillsDest, name);
576
- const dest = path.join(repoSkills, name);
577
- if (fs.existsSync(dest))
578
- fs.rmSync(dest, { recursive: true, force: true });
579
- copyDirSync(src, dest);
580
- }
581
- }
582
-
583
- // profiles.json 反推
584
- const projectProfiles = path.join(claudeDest, "profiles.json");
585
- if (fs.existsSync(projectProfiles)) {
586
- fs.copyFileSync(
587
- projectProfiles,
588
- path.join(sourcePath, ".claude", "profiles.json"),
589
- );
590
- }
591
-
592
- // settings.json 反推
593
- const cleanSettings = { ...settings };
594
- delete cleanSettings._skill_source;
595
- delete cleanSettings._active_profile;
596
- fs.writeFileSync(
597
- path.join(sourcePath, ".claude", "settings.json"),
598
- JSON.stringify(cleanSettings, null, 2) + "\n",
599
- "utf-8",
600
- );
601
-
602
- // CLAUDE.md 反推
603
- const projectMd = path.join(projectRoot, "CLAUDE.md");
604
- if (fs.existsSync(projectMd)) {
605
- fs.copyFileSync(projectMd, path.join(sourcePath, "CLAUDE.md"));
606
- }
607
-
608
- return sendJSON(res, { ok: true });
609
- }
610
-
611
- // POST /api/source/init — 初始化本地源骨架文件(缺啥补啥)
612
- if (method === "POST" && pathname === "/api/source/init") {
613
- const settings = readSettings();
614
- const sourcePath = settings._skill_source;
615
- if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
616
-
617
- const claudeDir = path.join(sourcePath, ".claude");
618
- const skillsDir = path.join(claudeDir, "skills");
619
- const profilesPath = path.join(claudeDir, "profiles.json");
620
- const settingsPath = path.join(claudeDir, "settings.json");
621
-
622
- const created = { profiles: false, settings: false, metaFiles: 0 };
623
-
624
- // 1. profiles.json
625
- if (!fs.existsSync(profilesPath)) {
626
- fs.writeFileSync(
627
- profilesPath,
628
- JSON.stringify(
629
- {
630
- version: 1,
631
- profiles: [
632
- {
633
- id: "custom",
634
- name: "自定义技能组合",
635
- description: "自由勾选技能项,按需个性化配置",
636
- skills: null,
637
- },
638
- ],
639
- },
640
- null,
641
- 2,
642
- ) + "\n",
643
- "utf-8",
644
- );
645
- created.profiles = true;
646
- }
647
-
648
- // 2. settings.json
649
- if (!fs.existsSync(settingsPath)) {
650
- fs.writeFileSync(
651
- settingsPath,
652
- JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
653
- "utf-8",
654
- );
655
- created.settings = true;
656
- }
657
-
658
- // 3. 每个 skill 目录的 .meta.json
659
- if (fs.existsSync(skillsDir)) {
660
- for (const name of core.listSkillDirs(skillsDir)) {
661
- const skillDir = path.join(skillsDir, name);
662
- const metaPath = path.join(skillDir, ".meta.json");
663
- if (!fs.existsSync(metaPath)) {
664
- fs.writeFileSync(
665
- metaPath,
666
- JSON.stringify(
667
- {
668
- version: "1.0.0",
669
- description: name,
670
- tags: [],
671
- },
672
- null,
673
- 2,
674
- ) + "\n",
675
- "utf-8",
676
- );
677
- created.metaFiles++;
678
- }
679
- }
680
- }
681
-
682
- // README.md 缺失状态
683
- const readmeStatus = {
684
- root: fs.existsSync(path.join(sourcePath, "README.md")),
685
- skills: fs.existsSync(path.join(skillsDir, "README.md")),
686
- };
687
-
688
- return sendJSON(res, { ok: true, ...created, readmeStatus });
689
- }
690
-
691
- // POST /api/readme/create — 创建 README.md 文档
692
- if (method === "POST" && pathname === "/api/readme/create") {
693
- const body = await parseBody(req);
694
- const settings = readSettings();
695
- const sourcePath = settings._skill_source;
696
- if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
697
-
698
- const { root, skills, projectName, teamName } = body;
699
- const name = projectName || "我的技能仓库";
700
- const team = teamName || "未设定";
701
- const created = [];
702
- const skipped = [];
703
-
704
- // 获取技能列表
705
- const skillsDir = path.join(sourcePath, ".claude", "skills");
706
- const skillNames = fs.existsSync(skillsDir)
707
- ? core.listSkillDirs(skillsDir)
708
- : [];
709
- const skillsList = skillNames.length
710
- ? skillNames.map((s) => `- \`${s}\``).join("\n")
711
- : "<!-- 暂无技能 -->";
712
-
713
- // 根目录 README.md
714
- if (root) {
715
- const rootPath = path.join(sourcePath, "README.md");
716
- if (!fs.existsSync(rootPath)) {
717
- const tmpl = `# ${name}
718
-
719
- > 维护团队:${team}
720
-
721
- ## 简介
722
-
723
- <!--
724
- 在这里写这个技能仓库的简介。
725
-
726
- 示例:
727
- 本仓库包含面向 mdk-engineer 的前端开发技能集,涵盖 Vue3 组件开发、Chrome DevTools 使用、性能优化等方向。
728
- -->
729
-
730
- ## 技能列表
731
-
732
- ${skillsList}
733
-
734
- ## 使用方式
735
-
736
- 本仓库通过 mdk-skills 管理技能,连接本地源后即可在 Web UI 中查看和安装技能。
737
-
738
- ## License
739
-
740
- <!-- 许可证信息 -->
741
- `;
742
- fs.writeFileSync(rootPath, tmpl, "utf-8");
743
- created.push("README.md");
744
- } else {
745
- skipped.push("README.md");
746
- }
747
- }
748
-
749
- // skills/README.md
750
- if (skills) {
751
- const skillsReadmePath = path.join(skillsDir, "README.md");
752
- if (!fs.existsSync(skillsReadmePath)) {
753
- const tmpl = `# 技能说明
754
-
755
- ## 快速开始
756
-
757
- <!--
758
- 安装 mdk-skills 后连接本仓库,即可在 Web UI 中查看所有技能。
759
- -->
760
-
761
- ## 场景建议
762
-
763
- <!--
764
- 这里可以写哪些技能适合一起使用,不同开发场景下的推荐组合。
765
-
766
- 示例:
767
- - 日常开发:启用代码格式化、ESLint 校验、Git 辅助
768
- - 性能优化:启用 Lighthouse、Bundle Analysis
769
- -->
770
-
771
- ## 注意事项
772
-
773
- <!--
774
- 技能使用过程中的注意事项。
775
- -->
776
- `;
777
- fs.writeFileSync(skillsReadmePath, tmpl, "utf-8");
778
- created.push("skills/README.md");
779
- } else {
780
- skipped.push("skills/README.md");
781
- }
782
- }
783
-
784
- return sendJSON(res, { ok: true, created, skipped });
785
- }
786
-
787
- // GET /api/diagnose 健康检查
788
- if (method === "GET" && pathname === "/api/diagnose") {
789
- const results = [];
790
- const pkgSkills = core.getPackageSkills(skillsSource);
791
- for (const skill of pkgSkills) {
792
- const skillDir = path.join(skillsDest, skill.name);
793
- const issues = [];
794
- if (!fs.existsSync(skillDir)) {
795
- issues.push("未安装");
796
- } else {
797
- const metaPath = path.join(skillDir, ".meta.json");
798
- if (!fs.existsSync(metaPath)) {
799
- issues.push("缺少 .meta.json");
800
- } else {
801
- try {
802
- const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
803
- if (!meta.version) issues.push("版本号缺失");
804
- } catch {
805
- issues.push(".meta.json 格式错误");
806
- }
807
- }
808
- }
809
- results.push({
810
- name: skill.name,
811
- healthy: issues.length === 0,
812
- issues,
813
- });
814
- }
815
- return sendJSON(res, results);
816
- }
817
-
818
- // 404
819
- sendJSON(res, { error: "Not Found: " + pathname }, 404);
820
- } catch (err) {
821
- console.error("API 错误:", err);
822
- sendJSON(res, { error: err.message }, 500);
823
- }
824
- }
825
-
826
- // ---------- 启动 ----------
827
-
828
- async function startUI() {
829
- const vite = await createViteServer({
830
- root: __dirname,
831
- server: { port: 3344, open: false },
832
- plugins: [
833
- vue(),
834
- {
835
- name: "api-routes",
836
- configureServer(viteServer) {
837
- viteServer.middlewares.use((req, res, next) => {
838
- if (req.url.startsWith("/api")) {
839
- handleApi(req, res);
840
- } else {
841
- next();
842
- }
843
- });
844
- },
845
- },
846
- ],
847
- });
848
-
849
- await vite.listen();
850
- const port = vite.config.server.port;
851
- console.log(`\n 🖥️ mdk-skills Web UI`);
852
- console.log(` ─────────────────────`);
853
- console.log(` 地址: http://localhost:${port}\n`);
854
- console.log(` 按 Ctrl+C 停止服务\n`);
855
- }
856
-
857
- startUI().catch((err) => {
858
- console.error("启动失败:", err);
859
- process.exit(1);
860
- });
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const http = require("http");
4
+ const { execSync } = require("child_process");
5
+ const os = require("os");
6
+ const core = require("../core");
7
+
8
+ // ---------- 路径(与 cli.js 保持一致) ----------
9
+
10
+ const projectRoot = (() => {
11
+ try {
12
+ return fs.realpathSync.native(process.cwd());
13
+ } catch {
14
+ return process.cwd();
15
+ }
16
+ })();
17
+ const packageDir = path.join(__dirname, "..", "..");
18
+ let skillsSource = core.getSkillsSource(projectRoot, packageDir);
19
+ const claudeDest = path.join(projectRoot, ".claude");
20
+ const skillsDest = path.join(claudeDest, "skills");
21
+ const settingsPath = path.join(claudeDest, "settings.json");
22
+ let pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
23
+
24
+ // 连接/断开本地源后刷新技能源路径
25
+ function refreshSource() {
26
+ skillsSource = core.getSkillsSource(projectRoot, packageDir);
27
+ pkgSkillsSource = path.join(skillsSource, ".claude", "skills");
28
+ }
29
+
30
+ // ---------- 工具函数 ----------
31
+
32
+ function readSettings() {
33
+ if (!fs.existsSync(settingsPath))
34
+ return { skills: {}, always_apply_skills: [] };
35
+ try {
36
+ return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
37
+ } catch {
38
+ return { skills: {}, always_apply_skills: [] };
39
+ }
40
+ }
41
+
42
+ function writeSettings(settings) {
43
+ if (!fs.existsSync(claudeDest)) {
44
+ fs.mkdirSync(claudeDest, { recursive: true });
45
+ }
46
+ fs.writeFileSync(
47
+ settingsPath,
48
+ JSON.stringify(settings, null, 2) + "\n",
49
+ "utf-8",
50
+ );
51
+ }
52
+
53
+ function sendJSON(res, data, status = 200) {
54
+ res.writeHead(status, { "Content-Type": "application/json" });
55
+ res.end(JSON.stringify(data));
56
+ }
57
+
58
+ function parseBody(req) {
59
+ return new Promise((resolve) => {
60
+ let body = "";
61
+ req.on("data", (chunk) => (body += chunk));
62
+ req.on("end", () => {
63
+ try {
64
+ resolve(JSON.parse(body));
65
+ } catch {
66
+ resolve({});
67
+ }
68
+ });
69
+ });
70
+ }
71
+
72
+ // 手动递归拷贝目录
73
+ function copyDirSync(src, dest) {
74
+ fs.mkdirSync(dest, { recursive: true });
75
+ for (const item of fs.readdirSync(src)) {
76
+ const srcPath = path.join(src, item);
77
+ const destPath = path.join(dest, item);
78
+ if (fs.statSync(srcPath).isDirectory()) {
79
+ copyDirSync(srcPath, destPath);
80
+ } else {
81
+ fs.copyFileSync(srcPath, destPath);
82
+ }
83
+ }
84
+ }
85
+
86
+ // 读取 profiles.json
87
+ function loadProfiles() {
88
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
89
+ if (!fs.existsSync(profilesPath)) return null;
90
+ try {
91
+ return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ // 应用场景
98
+ function applyProfile(profile) {
99
+ const pkgSkills = core.listSkillDirs(pkgSkillsSource);
100
+ const settings = readSettings();
101
+
102
+ let selected;
103
+ if (profile.skills === null) {
104
+ return { selected: [], message: "自定义场景,不做自动切换" };
105
+ } else if (profile.skills.length === 0) {
106
+ selected = [];
107
+ } else {
108
+ selected = profile.skills.filter((s) => pkgSkills.includes(s));
109
+ }
110
+
111
+ // 安装/删除技能
112
+ if (!fs.existsSync(skillsDest)) {
113
+ fs.mkdirSync(skillsDest, { recursive: true });
114
+ }
115
+ for (const name of pkgSkills) {
116
+ const src = path.join(pkgSkillsSource, name);
117
+ const dest = path.join(skillsDest, name);
118
+ if (selected.includes(name)) {
119
+ if (!fs.existsSync(dest)) {
120
+ try {
121
+ copyDirSync(src, dest);
122
+ } catch (err) {
123
+ console.error(`安装技能 "${name}" 失败:`, err.message);
124
+ }
125
+ }
126
+ } else {
127
+ if (fs.existsSync(dest)) {
128
+ fs.rmSync(dest, { recursive: true, force: true });
129
+ }
130
+ }
131
+ }
132
+
133
+ // 更新 settings
134
+ if (!settings.skills) settings.skills = {};
135
+ for (const name of pkgSkills) {
136
+ const enabled = selected.includes(name);
137
+ if (!settings.skills[name]) settings.skills[name] = {};
138
+ settings.skills[name].enabled = enabled;
139
+ }
140
+
141
+ if (profile.always_apply) {
142
+ settings.always_apply_skills = profile.always_apply;
143
+ }
144
+
145
+ settings._active_profile = profile.id;
146
+ writeSettings(settings);
147
+
148
+ // 复制 profiles.json
149
+ const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
150
+ const profilesDest = path.join(claudeDest, "profiles.json");
151
+ if (fs.existsSync(profilesSource)) {
152
+ fs.copyFileSync(profilesSource, profilesDest);
153
+ }
154
+
155
+ // 复制 CLAUDE.md(不存在才装)
156
+ const mdSource = path.join(skillsSource, "CLAUDE.md");
157
+ const mdDest = path.join(projectRoot, "CLAUDE.md");
158
+ if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
159
+ fs.copyFileSync(mdSource, mdDest);
160
+ }
161
+
162
+ return {
163
+ selected,
164
+ enabled: selected.length,
165
+ disabled: pkgSkills.length - selected.length,
166
+ };
167
+ }
168
+
169
+ // 安装勾选的技能
170
+ function installSelectedSkills(selectedNames) {
171
+ if (!fs.existsSync(pkgSkillsSource)) return { installed: 0 };
172
+
173
+ if (!fs.existsSync(skillsDest)) {
174
+ fs.mkdirSync(skillsDest, { recursive: true });
175
+ }
176
+
177
+ const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
178
+ let installedCount = 0;
179
+
180
+ for (const name of allPkgSkills) {
181
+ const src = path.join(pkgSkillsSource, name);
182
+ const dest = path.join(skillsDest, name);
183
+
184
+ if (selectedNames.includes(name)) {
185
+ if (!fs.existsSync(dest)) {
186
+ try {
187
+ copyDirSync(src, dest);
188
+ installedCount++;
189
+ } catch (err) {
190
+ console.error(`安装技能 "${name}" 失败:`, err.message);
191
+ }
192
+ }
193
+ } else {
194
+ if (fs.existsSync(dest)) {
195
+ fs.rmSync(dest, { recursive: true, force: true });
196
+ }
197
+ }
198
+ }
199
+
200
+ const settings = readSettings();
201
+ if (!settings.skills) settings.skills = {};
202
+ for (const name of allPkgSkills) {
203
+ const enabled = selectedNames.includes(name);
204
+ if (!settings.skills[name]) settings.skills[name] = {};
205
+ settings.skills[name].enabled = enabled;
206
+ }
207
+ writeSettings(settings);
208
+
209
+ // 复制 CLAUDE.md(不存在才装)
210
+ const mdSource = path.join(skillsSource, "CLAUDE.md");
211
+ const mdDest = path.join(projectRoot, "CLAUDE.md");
212
+ if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
213
+ fs.copyFileSync(mdSource, mdDest);
214
+ }
215
+
216
+ return { installed: installedCount };
217
+ }
218
+
219
+ // ---------- API 路由 ----------
220
+
221
+ async function handleApi(req, res) {
222
+ const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
223
+ const pathname = url.pathname;
224
+ const method = req.method;
225
+
226
+ // CORS & 防缓存
227
+ res.setHeader("Access-Control-Allow-Origin", "*");
228
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
229
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
230
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
231
+
232
+ if (method === "OPTIONS") {
233
+ res.writeHead(204);
234
+ res.end();
235
+ return;
236
+ }
237
+
238
+ try {
239
+ // GET /api/skills — 获取所有技能列表(含启用状态),同时清理已失效的技能
240
+ if (method === "GET" && pathname === "/api/skills") {
241
+ const pkgSkills = core.getPackageSkills(skillsSource);
242
+ const pkgNames = new Set(pkgSkills.map((s) => s.name));
243
+ const userSkills = core.getUserSkills(claudeDest);
244
+ // 清理:源目录已删除的技能,项目中也一并移除
245
+ let cleaned = false;
246
+ for (const us of userSkills) {
247
+ if (!pkgNames.has(us.name)) {
248
+ const dest = path.join(skillsDest, us.name);
249
+ if (fs.existsSync(dest)) {
250
+ fs.rmSync(dest, { recursive: true, force: true });
251
+ cleaned = true;
252
+ }
253
+ }
254
+ }
255
+ if (cleaned) {
256
+ const settings = readSettings();
257
+ for (const name of Object.keys(settings.skills || {})) {
258
+ if (!pkgNames.has(name)) delete settings.skills[name];
259
+ }
260
+ writeSettings(settings);
261
+ }
262
+ const refreshed = core.getUserSkills(claudeDest);
263
+ const userMap = new Map(refreshed.map((s) => [s.name, s]));
264
+ const result = pkgSkills.map((s) => ({
265
+ ...s,
266
+ enabled: userMap.has(s.name) ? userMap.get(s.name).enabled : false,
267
+ installed: userMap.has(s.name),
268
+ }));
269
+ return sendJSON(res, result);
270
+ }
271
+
272
+ // POST /api/skills/toggle — 开关技能(启用时自动安装)
273
+ if (method === "POST" && pathname === "/api/skills/toggle") {
274
+ const body = await parseBody(req);
275
+ const { name, enabled } = body;
276
+ if (!name) return sendJSON(res, { error: "缺少技能名" }, 400);
277
+ if (enabled) {
278
+ const src = path.join(pkgSkillsSource, name);
279
+ const dest = path.join(skillsDest, name);
280
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
281
+ copyDirSync(src, dest);
282
+ }
283
+ }
284
+ const settings = readSettings();
285
+ if (!settings.skills) settings.skills = {};
286
+ if (!settings.skills[name]) settings.skills[name] = {};
287
+ settings.skills[name].enabled = !!enabled;
288
+ writeSettings(settings);
289
+ return sendJSON(res, { ok: true, name, enabled: !!enabled });
290
+ }
291
+
292
+ // POST /api/skills/install — 自定义勾选安装
293
+ if (method === "POST" && pathname === "/api/skills/install") {
294
+ const body = await parseBody(req);
295
+ const { selected } = body;
296
+ if (!Array.isArray(selected))
297
+ return sendJSON(res, { error: "参数错误" }, 400);
298
+ const result = installSelectedSkills(selected);
299
+ return sendJSON(res, { ok: true, ...result });
300
+ }
301
+ // POST /api/skills/pull从远程仓库拉取技能
302
+ if (method === "POST" && pathname === "/api/skills/pull") {
303
+ const body = await parseBody(req);
304
+ const url = (body.url || "").trim();
305
+ if (!url) return sendJSON(res, { error: "请输入仓库地址" }, 400);
306
+ const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
307
+ try {
308
+ execSync("git clone --depth 1 \"" + url + "\" \"" + tmpDir + "\"", {
309
+ stdio: "pipe",
310
+ timeout: 120000,
311
+ });
312
+ } catch {
313
+ if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
314
+ return sendJSON(res, { error: "仓库克隆失败,请检查地址是否正确" }, 400);
315
+ }
316
+ let skillsDir = null;
317
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
318
+ const skills = path.join(tmpDir, "skills");
319
+ if (fs.existsSync(claudeSkills)) {
320
+ skillsDir = claudeSkills;
321
+ } else if (fs.existsSync(skills)) {
322
+ skillsDir = skills;
323
+ }
324
+ if (!skillsDir) {
325
+ fs.rmSync(tmpDir, { recursive: true, force: true });
326
+ return sendJSON(res, { error: "仓库中未找到 .claude/skills 或 skills 目录" }, 400);
327
+ }
328
+ const imported = [];
329
+ const skipped = [];
330
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
331
+ for (const entry of entries) {
332
+ if (!entry.isDirectory()) continue;
333
+ const skillPath = path.join(skillsDir, entry.name);
334
+ if (fs.existsSync(path.join(skillPath, "SKILL.md"))) {
335
+ const dest = path.join(pkgSkillsSource, entry.name);
336
+ if (fs.existsSync(dest)) {
337
+ fs.rmSync(dest, { recursive: true, force: true });
338
+ }
339
+ fs.cpSync(skillPath, dest, { recursive: true });
340
+ const fm = core.parseFrontmatter(dest);
341
+ if (fm) {
342
+ fs.writeFileSync(path.join(dest, ".meta.json"), JSON.stringify({
343
+ version: fm.version || "1.0.0",
344
+ description: fm.description || "",
345
+ tags: fm.tags || [],
346
+ }, null, 2) + "\n", "utf-8");
347
+ }
348
+ imported.push(entry.name);
349
+ } else {
350
+ skipped.push(entry.name);
351
+ }
352
+ }
353
+ fs.rmSync(tmpDir, { recursive: true, force: true });
354
+ if (imported.length === 0) {
355
+ return sendJSON(res, { error: "未找到有效的技能(目录需包含 SKILL.md)", skipped }, 400);
356
+ }
357
+ return sendJSON(res, { ok: true, imported, skipped });
358
+ }
359
+
360
+ // GET /api/readme 获取根目录 README.md
361
+ if (method === "GET" && pathname === "/api/readme") {
362
+ const p = path.join(skillsSource, "README.md");
363
+ const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
364
+ return sendJSON(res, { content, found: !!content });
365
+ }
366
+
367
+ // GET /api/skills-readme — 获取技能目录 README.md
368
+ if (method === "GET" && pathname === "/api/skills-readme") {
369
+ const p = path.join(skillsSource, ".claude", "skills", "README.md");
370
+ const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
371
+ return sendJSON(res, { content, found: !!content });
372
+ }
373
+
374
+ // GET /api/claudemd 获取 CLAUDE.md 内容
375
+ if (method === "GET" && pathname === "/api/claudemd") {
376
+ const paths = [
377
+ path.join(skillsSource, "CLAUDE.md"),
378
+ path.join(projectRoot, "CLAUDE.md"),
379
+ ];
380
+ let content = null;
381
+ for (const p of paths) {
382
+ if (fs.existsSync(p)) {
383
+ content = fs.readFileSync(p, "utf-8");
384
+ break;
385
+ }
386
+ }
387
+ return sendJSON(res, { content, found: !!content });
388
+ }
389
+
390
+ // GET /api/skills/:name/readme — 获取技能的 SKILL.md
391
+ const readmeMatch = pathname.match(/^\/api\/skills\/([^/]+)\/readme$/);
392
+ if (method === "GET" && readmeMatch) {
393
+ const name = readmeMatch[1];
394
+ const skillDir = path.join(skillsDest, name);
395
+ const altDir = path.join(pkgSkillsSource, name);
396
+ const readmePath = [
397
+ path.join(skillDir, "SKILL.md"),
398
+ path.join(altDir, "SKILL.md"),
399
+ ].find((p) => fs.existsSync(p));
400
+ const content = readmePath ? fs.readFileSync(readmePath, "utf-8") : null;
401
+ return sendJSON(res, { content, found: !!readmePath });
402
+ }
403
+
404
+ // PATCH /api/skills/:name/meta — 更新技能元信息
405
+ const metaMatch = pathname.match(/^\/api\/skills\/([^/]+)\/meta$/);
406
+ if (method === "PATCH" && metaMatch) {
407
+ const name = metaMatch[1];
408
+ const skillDir = path.join(pkgSkillsSource, name);
409
+ if (!fs.existsSync(skillDir)) {
410
+ return sendJSON(res, { error: "技能不存在" }, 404);
411
+ }
412
+ const metaPath = path.join(skillDir, ".meta.json");
413
+ let meta = {};
414
+ if (fs.existsSync(metaPath)) {
415
+ try { meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch {}
416
+ }
417
+ const body = await parseBody(req);
418
+ if (body.version !== undefined) meta.version = String(body.version);
419
+ if (body.description !== undefined) meta.description = String(body.description);
420
+ if (body.tags !== undefined) {
421
+ meta.tags = Array.isArray(body.tags) ? body.tags.filter((t) => typeof t === "string") : [];
422
+ }
423
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
424
+ return sendJSON(res, { ok: true, meta });
425
+ }
426
+
427
+ // DELETE /api/skills/:name 删除技能(源目录 + 项目目录)
428
+ const deleteMatch = pathname.match(/^\/api\/skills\/([^/]+)$/);
429
+ if (method === "DELETE" && deleteMatch) {
430
+ const name = deleteMatch[1];
431
+ const sourceDir = path.join(pkgSkillsSource, name);
432
+ const destDir = path.join(skillsDest, name);
433
+ let deleted = [];
434
+
435
+ if (fs.existsSync(sourceDir)) {
436
+ fs.rmSync(sourceDir, { recursive: true, force: true });
437
+ deleted.push("源目录");
438
+ }
439
+ if (fs.existsSync(destDir)) {
440
+ fs.rmSync(destDir, { recursive: true, force: true });
441
+ deleted.push("项目目录");
442
+ }
443
+
444
+ // 清理 settings 中的启用状态
445
+ const settings = readSettings();
446
+ if (settings.skills && settings.skills[name] !== undefined) {
447
+ delete settings.skills[name];
448
+ writeSettings(settings);
449
+ }
450
+
451
+ if (deleted.length === 0) {
452
+ return sendJSON(res, { error: "技能不存在" }, 404);
453
+ }
454
+ return sendJSON(res, { ok: true, name, deleted });
455
+ }
456
+
457
+ // GET /api/profiles 获取场景列表(含只读状态)
458
+ if (method === "GET" && pathname === "/api/profiles") {
459
+ const profiles = loadProfiles();
460
+ const settings = readSettings();
461
+ return sendJSON(res, {
462
+ profiles: profiles || [],
463
+ activeProfile: settings._active_profile || null,
464
+ readonly: !settings._skill_source,
465
+ });
466
+ }
467
+
468
+ // POST /api/profiles/apply — 应用场景
469
+ if (method === "POST" && pathname === "/api/profiles/apply") {
470
+ const body = await parseBody(req);
471
+ const { profileId } = body;
472
+ if (!profileId) return sendJSON(res, { error: "缺少场景 ID" }, 400);
473
+ const profiles = loadProfiles();
474
+ const profile = profiles?.find((p) => p.id === profileId);
475
+ if (!profile)
476
+ return sendJSON(res, { error: "场景不存在: " + profileId }, 404);
477
+ const result = applyProfile(profile);
478
+ return sendJSON(res, { ok: true, ...result });
479
+ }
480
+
481
+ // POST /api/profiles/save — 新增/编辑场景(仅本地源模式)
482
+ if (method === "POST" && pathname === "/api/profiles/save") {
483
+ const body = await parseBody(req);
484
+ const settings = readSettings();
485
+ if (!settings._skill_source)
486
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
487
+
488
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
489
+ let data = { version: 1, profiles: [] };
490
+ if (fs.existsSync(profilesPath)) {
491
+ try {
492
+ data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
493
+ } catch {}
494
+ }
495
+
496
+ const { id, name, description, skills, always_apply } = body;
497
+ if (!name) return sendJSON(res, { error: "场景名称不能为空" }, 400);
498
+
499
+ if (id) {
500
+ const index = data.profiles.findIndex((p) => p.id === id);
501
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
502
+ data.profiles[index] = {
503
+ ...data.profiles[index],
504
+ name,
505
+ description: description || "",
506
+ skills: skills || [],
507
+ always_apply: always_apply || [],
508
+ };
509
+ } else {
510
+ const newId = "profile_" + Date.now().toString(36);
511
+ data.profiles.push({
512
+ id: newId,
513
+ name,
514
+ description: description || "",
515
+ skills: skills || [],
516
+ always_apply: always_apply || [],
517
+ });
518
+ }
519
+
520
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
521
+
522
+ // 同步到项目 .claude
523
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
524
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
525
+
526
+ return sendJSON(res, { ok: true, profiles: data.profiles });
527
+ }
528
+
529
+ // POST /api/profiles/delete — 删除场景(仅本地源模式)
530
+ if (method === "POST" && pathname === "/api/profiles/delete") {
531
+ const body = await parseBody(req);
532
+ const settings = readSettings();
533
+ if (!settings._skill_source)
534
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
535
+
536
+ const { id } = body;
537
+ if (!id) return sendJSON(res, { error: "缺少场景 ID" }, 400);
538
+ if (id === "custom")
539
+ return sendJSON(res, { error: "不能删除内置场景" }, 403);
540
+
541
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
542
+ if (!fs.existsSync(profilesPath))
543
+ return sendJSON(res, { error: "场景文件不存在" }, 404);
544
+
545
+ const data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
546
+ const index = data.profiles.findIndex((p) => p.id === id);
547
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
548
+
549
+ data.profiles.splice(index, 1);
550
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
551
+
552
+ // 同步到项目 .claude
553
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
554
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
555
+
556
+ return sendJSON(res, { ok: true });
557
+ }
558
+
559
+ // GET /api/status — 当前状态总览
560
+ if (method === "GET" && pathname === "/api/status") {
561
+ const settings = readSettings();
562
+ const userSkills = core.getUserSkills(claudeDest);
563
+ const pkgSkills = core.getPackageSkills(skillsSource);
564
+ const profiles = loadProfiles();
565
+ const activeProfile = profiles?.find(
566
+ (p) => p.id === settings._active_profile,
567
+ );
568
+ const enabledCount = userSkills.filter((s) => s.enabled).length;
569
+ const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
570
+ return sendJSON(res, {
571
+ pkgSkills,
572
+ enabledCount,
573
+ totalCount: pkgSkills.length,
574
+ activeProfile: activeProfile
575
+ ? { id: activeProfile.id, name: activeProfile.name }
576
+ : null,
577
+ sourcePath: settings._skill_source || null,
578
+ hasClaudeMd: mdExists,
579
+ });
580
+ }
581
+
582
+ // GET /api/source — 获取本地源信息(含初始化检测)
583
+ if (method === "GET" && pathname === "/api/source") {
584
+ const settings = readSettings();
585
+ const sourcePath = settings._skill_source || null;
586
+
587
+ let needsInit = false;
588
+ const missingConfig = [];
589
+ const missingSkillDocs = [];
590
+ if (sourcePath) {
591
+ const claudeDir = path.join(sourcePath, ".claude");
592
+ if (fs.existsSync(claudeDir)) {
593
+ if (!fs.existsSync(path.join(claudeDir, "profiles.json"))) {
594
+ missingConfig.push("profiles.json");
595
+ }
596
+ if (!fs.existsSync(path.join(claudeDir, "settings.json"))) {
597
+ missingConfig.push("settings.json");
598
+ }
599
+ const skillsDir = path.join(claudeDir, "skills");
600
+ if (fs.existsSync(skillsDir)) {
601
+ for (const name of core.listSkillDirs(skillsDir)) {
602
+ if (!fs.existsSync(path.join(skillsDir, name, "SKILL.md"))) {
603
+ missingSkillDocs.push(name);
604
+ }
605
+ }
606
+ }
607
+ needsInit = missingConfig.length > 0 || missingSkillDocs.length > 0;
608
+ } else {
609
+ needsInit = true;
610
+ }
611
+ }
612
+
613
+ // README.md 缺失状态(独立于骨架文件)
614
+ let readmeStatus = null;
615
+ if (sourcePath) {
616
+ const skillsDir = path.join(sourcePath, ".claude", "skills");
617
+ readmeStatus = {
618
+ root: fs.existsSync(path.join(sourcePath, "README.md")),
619
+ skills: fs.existsSync(path.join(skillsDir, "README.md")),
620
+ };
621
+ }
622
+
623
+ return sendJSON(res, {
624
+ connected: !!sourcePath,
625
+ path: sourcePath,
626
+ needsInit,
627
+ missingConfig,
628
+ missingSkillDocs,
629
+ readmeStatus,
630
+ });
631
+ }
632
+
633
+ // POST /api/source/connect — 绑定本地源
634
+ if (method === "POST" && pathname === "/api/source/connect") {
635
+ const body = await parseBody(req);
636
+ const repoPath = body.path;
637
+ if (!repoPath) return sendJSON(res, { error: "缺少仓库路径" }, 400);
638
+ const resolved = path.resolve(repoPath);
639
+ if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
640
+ return sendJSON(
641
+ res,
642
+ { error: `路径 "${resolved}" 下没有 .claude/skills/` },
643
+ 400,
644
+ );
645
+ }
646
+ const settings = readSettings();
647
+ settings._skill_source = resolved;
648
+ writeSettings(settings);
649
+ // 复制 CLAUDE.md(不存在才装)
650
+ const mdSource = path.join(resolved, "CLAUDE.md");
651
+ const mdDest = path.join(projectRoot, "CLAUDE.md");
652
+ if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
653
+ fs.copyFileSync(mdSource, mdDest);
654
+ }
655
+
656
+ refreshSource();
657
+ return sendJSON(res, { ok: true, path: resolved });
658
+ }
659
+
660
+ // POST /api/source/disconnect 解绑本地源
661
+ if (method === "POST" && pathname === "/api/source/disconnect") {
662
+ const settings = readSettings();
663
+ if (!settings._skill_source) {
664
+ return sendJSON(res, { error: "当前未绑定任何技能源" }, 400);
665
+ }
666
+ delete settings._skill_source;
667
+ writeSettings(settings);
668
+ refreshSource();
669
+ return sendJSON(res, { ok: true });
670
+ }
671
+
672
+ // POST /api/source/sync — 同步到本地源
673
+ if (method === "POST" && pathname === "/api/source/sync") {
674
+ const settings = readSettings();
675
+ const sourcePath = settings._skill_source;
676
+ if (!sourcePath) return sendJSON(res, { error: "尚未绑定技能源" }, 400);
677
+ if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
678
+ return sendJSON(res, { error: "绑定的路径已失效" }, 400);
679
+ }
680
+
681
+ const repoClaude = path.join(sourcePath, ".claude");
682
+ core.backupDir(repoClaude);
683
+
684
+ // 技能反推
685
+ const repoSkills = path.join(sourcePath, ".claude", "skills");
686
+ if (!fs.existsSync(repoSkills))
687
+ fs.mkdirSync(repoSkills, { recursive: true });
688
+ if (fs.existsSync(skillsDest)) {
689
+ for (const name of core.listSkillDirs(skillsDest)) {
690
+ const src = path.join(skillsDest, name);
691
+ const dest = path.join(repoSkills, name);
692
+ if (fs.existsSync(dest))
693
+ fs.rmSync(dest, { recursive: true, force: true });
694
+ copyDirSync(src, dest);
695
+ }
696
+ }
697
+
698
+ // profiles.json 反推
699
+ const projectProfiles = path.join(claudeDest, "profiles.json");
700
+ if (fs.existsSync(projectProfiles)) {
701
+ fs.copyFileSync(
702
+ projectProfiles,
703
+ path.join(sourcePath, ".claude", "profiles.json"),
704
+ );
705
+ }
706
+
707
+ // settings.json 反推
708
+ const cleanSettings = { ...settings };
709
+ delete cleanSettings._skill_source;
710
+ delete cleanSettings._active_profile;
711
+ fs.writeFileSync(
712
+ path.join(sourcePath, ".claude", "settings.json"),
713
+ JSON.stringify(cleanSettings, null, 2) + "\n",
714
+ "utf-8",
715
+ );
716
+
717
+ // CLAUDE.md 反推
718
+ const projectMd = path.join(projectRoot, "CLAUDE.md");
719
+ if (fs.existsSync(projectMd)) {
720
+ fs.copyFileSync(projectMd, path.join(sourcePath, "CLAUDE.md"));
721
+ }
722
+
723
+ return sendJSON(res, { ok: true });
724
+ }
725
+
726
+ // POST /api/source/init — 初始化本地源骨架文件(缺啥补啥)
727
+ if (method === "POST" && pathname === "/api/source/init") {
728
+ const settings = readSettings();
729
+ const sourcePath = settings._skill_source;
730
+ if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
731
+
732
+ const claudeDir = path.join(sourcePath, ".claude");
733
+ const skillsDir = path.join(claudeDir, "skills");
734
+ const profilesPath = path.join(claudeDir, "profiles.json");
735
+ const settingsPath = path.join(claudeDir, "settings.json");
736
+
737
+ const created = { profiles: false, settings: false, metaFiles: 0 };
738
+
739
+ // 1. profiles.json
740
+ if (!fs.existsSync(profilesPath)) {
741
+ fs.writeFileSync(
742
+ profilesPath,
743
+ JSON.stringify(
744
+ {
745
+ version: 1,
746
+ profiles: [
747
+ {
748
+ id: "custom",
749
+ name: "自定义技能组合",
750
+ description: "自由勾选技能项,按需个性化配置",
751
+ skills: null,
752
+ },
753
+ ],
754
+ },
755
+ null,
756
+ 2,
757
+ ) + "\n",
758
+ "utf-8",
759
+ );
760
+ created.profiles = true;
761
+ }
762
+
763
+ // 2. settings.json
764
+ if (!fs.existsSync(settingsPath)) {
765
+ fs.writeFileSync(
766
+ settingsPath,
767
+ JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
768
+ "utf-8",
769
+ );
770
+ created.settings = true;
771
+ }
772
+
773
+ // 3. 每个 skill 目录的 .meta.json(优先从 SKILL.md frontmatter 读取)
774
+ if (fs.existsSync(skillsDir)) {
775
+ for (const name of core.listSkillDirs(skillsDir)) {
776
+ const skillDir = path.join(skillsDir, name);
777
+ const metaPath = path.join(skillDir, ".meta.json");
778
+ if (!fs.existsSync(metaPath)) {
779
+ const fm = core.parseFrontmatter(skillDir);
780
+ const meta = {
781
+ version: fm?.version || "1.0.0",
782
+ description: fm?.description || name,
783
+ tags: fm?.tags || [],
784
+ };
785
+ fs.writeFileSync(
786
+ metaPath,
787
+ JSON.stringify(meta, null, 2) + "\n",
788
+ "utf-8",
789
+ );
790
+ created.metaFiles++;
791
+ }
792
+ }
793
+ }
794
+
795
+ // README.md 缺失状态
796
+ const readmeStatus = {
797
+ root: fs.existsSync(path.join(sourcePath, "README.md")),
798
+ skills: fs.existsSync(path.join(skillsDir, "README.md")),
799
+ };
800
+
801
+ return sendJSON(res, { ok: true, ...created, readmeStatus });
802
+ }
803
+
804
+ // POST /api/readme/create — 创建 README.md 文档
805
+ if (method === "POST" && pathname === "/api/readme/create") {
806
+ const body = await parseBody(req);
807
+ const settings = readSettings();
808
+ const sourcePath = settings._skill_source;
809
+ if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
810
+
811
+ const { root, skills, projectName, teamName } = body;
812
+ const name = projectName || "我的技能仓库";
813
+ const team = teamName || "未设定";
814
+ const created = [];
815
+ const skipped = [];
816
+
817
+ // 获取技能列表
818
+ const skillsDir = path.join(sourcePath, ".claude", "skills");
819
+ const skillNames = fs.existsSync(skillsDir)
820
+ ? core.listSkillDirs(skillsDir)
821
+ : [];
822
+ const skillsList = skillNames.length
823
+ ? skillNames.map((s) => `- \`${s}\``).join("\n")
824
+ : "<!-- 暂无技能 -->";
825
+
826
+ // 根目录 README.md
827
+ if (root) {
828
+ const rootPath = path.join(sourcePath, "README.md");
829
+ if (!fs.existsSync(rootPath)) {
830
+ const tmpl = `# ${name}
831
+
832
+ > 维护团队:${team}
833
+
834
+ ## 简介
835
+
836
+ <!--
837
+ 在这里写这个技能仓库的简介。
838
+
839
+ 示例:
840
+ 本仓库包含面向 mdk-engineer 的前端开发技能集,涵盖 Vue3 组件开发、Chrome DevTools 使用、性能优化等方向。
841
+ -->
842
+
843
+ ## 技能列表
844
+
845
+ ${skillsList}
846
+
847
+ ## 使用方式
848
+
849
+ 本仓库通过 mdk-skills 管理技能,连接本地源后即可在 Web UI 中查看和安装技能。
850
+
851
+ ## License
852
+
853
+ <!-- 许可证信息 -->
854
+ `;
855
+ fs.writeFileSync(rootPath, tmpl, "utf-8");
856
+ created.push("README.md");
857
+ } else {
858
+ skipped.push("README.md");
859
+ }
860
+ }
861
+
862
+ // skills/README.md
863
+ if (skills) {
864
+ const skillsReadmePath = path.join(skillsDir, "README.md");
865
+ if (!fs.existsSync(skillsReadmePath)) {
866
+ const tmpl = `# 技能说明
867
+
868
+ ## 快速开始
869
+
870
+ <!--
871
+ 安装 mdk-skills 后连接本仓库,即可在 Web UI 中查看所有技能。
872
+ -->
873
+
874
+ ## 场景建议
875
+
876
+ <!--
877
+ 这里可以写哪些技能适合一起使用,不同开发场景下的推荐组合。
878
+
879
+ 示例:
880
+ - 日常开发:启用代码格式化、ESLint 校验、Git 辅助
881
+ - 性能优化:启用 Lighthouse、Bundle Analysis
882
+ -->
883
+
884
+ ## 注意事项
885
+
886
+ <!--
887
+ 技能使用过程中的注意事项。
888
+ -->
889
+ `;
890
+ fs.writeFileSync(skillsReadmePath, tmpl, "utf-8");
891
+ created.push("skills/README.md");
892
+ } else {
893
+ skipped.push("skills/README.md");
894
+ }
895
+ }
896
+
897
+ return sendJSON(res, { ok: true, created, skipped });
898
+ }
899
+
900
+ // GET /api/diagnose — 健康检查
901
+ if (method === "GET" && pathname === "/api/diagnose") {
902
+ const results = [];
903
+ const pkgSkills = core.getPackageSkills(skillsSource);
904
+ for (const skill of pkgSkills) {
905
+ const skillDir = path.join(skillsDest, skill.name);
906
+ const issues = [];
907
+ if (!fs.existsSync(skillDir)) {
908
+ issues.push("未安装");
909
+ } else {
910
+ if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) {
911
+ issues.push("缺少 SKILL.md");
912
+ } else if (!core.parseFrontmatter(skillDir)) {
913
+ issues.push("SKILL.md frontmatter 格式异常");
914
+ }
915
+ // .meta.json 为可选项,缺失不报错
916
+ }
917
+ results.push({
918
+ name: skill.name,
919
+ healthy: issues.length === 0,
920
+ issues,
921
+ });
922
+ }
923
+ return sendJSON(res, results);
924
+ }
925
+
926
+ // 404
927
+ sendJSON(res, { error: "Not Found: " + pathname }, 404);
928
+ } catch (err) {
929
+ console.error("API 错误:", err);
930
+ sendJSON(res, { error: err.message }, 500);
931
+ }
932
+ }
933
+
934
+ // ---------- 启动 ----------
935
+
936
+ const MIME_TYPES = {
937
+ ".html": "text/html",
938
+ ".js": "application/javascript",
939
+ ".css": "text/css",
940
+ ".png": "image/png",
941
+ ".jpg": "image/jpeg",
942
+ ".svg": "image/svg+xml",
943
+ ".ico": "image/x-icon",
944
+ ".json": "application/json",
945
+ ".woff2": "font/woff2",
946
+ ".woff": "font/woff",
947
+ };
948
+
949
+ function startProduction(distDir) {
950
+ const server = http.createServer((req, res) => {
951
+ if (req.url.startsWith("/api")) {
952
+ handleApi(req, res);
953
+ return;
954
+ }
955
+ const urlPath = req.url === "/" ? "/index.html" : req.url.split("?")[0];
956
+ const filePath = path.join(distDir, urlPath);
957
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
958
+ const ext = path.extname(filePath);
959
+ res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
960
+ res.end(fs.readFileSync(filePath));
961
+ } else {
962
+ res.writeHead(200, { "Content-Type": "text/html" });
963
+ res.end(fs.readFileSync(path.join(distDir, "index.html")));
964
+ }
965
+ });
966
+ server.listen(3344, () => {
967
+ console.log(`\n 🖥️ mdk-skills Web UI`);
968
+ console.log(` ─────────────────────`);
969
+ console.log(` 地址: http://localhost:3344\n`);
970
+ console.log(` 按 Ctrl+C 停止服务\n`);
971
+ });
972
+ }
973
+
974
+ async function startDev() {
975
+ const { createServer: createViteServer } = require("vite");
976
+ const vue = require("@vitejs/plugin-vue");
977
+ const vite = await createViteServer({
978
+ root: __dirname,
979
+ server: { port: 3344, open: false },
980
+ plugins: [
981
+ vue(),
982
+ {
983
+ name: "api-routes",
984
+ configureServer(viteServer) {
985
+ viteServer.middlewares.use((req, res, next) => {
986
+ if (req.url.startsWith("/api")) {
987
+ handleApi(req, res);
988
+ } else {
989
+ next();
990
+ }
991
+ });
992
+ },
993
+ },
994
+ ],
995
+ });
996
+ await vite.listen();
997
+ const port = vite.config.server.port;
998
+ console.log(`\n 🖥️ mdk-skills Web UI`);
999
+ console.log(` ─────────────────────`);
1000
+ console.log(` 地址: http://localhost:${port}\n`);
1001
+ console.log(` 按 Ctrl+C 停止服务\n`);
1002
+ }
1003
+
1004
+ // 自动选择模式:有构建产物就走生产模式,否则走 Vite dev
1005
+ const distDir = path.join(__dirname, "dist");
1006
+ if (fs.existsSync(distDir)) {
1007
+ startProduction(distDir);
1008
+ } else {
1009
+ startDev().catch((err) => {
1010
+ console.error("启动失败:", err);
1011
+ process.exit(1);
1012
+ });
1013
+ }