mdk-skills 2.2.7 → 2.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdk-skills",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
@@ -286,13 +286,14 @@ async function handleApi(req, res) {
286
286
  return sendJSON(res, { ok: true, ...result });
287
287
  }
288
288
 
289
- // GET /api/profiles — 获取场景列表
289
+ // GET /api/profiles — 获取场景列表(含只读状态)
290
290
  if (method === "GET" && pathname === "/api/profiles") {
291
291
  const profiles = loadProfiles();
292
292
  const settings = readSettings();
293
293
  return sendJSON(res, {
294
294
  profiles: profiles || [],
295
295
  activeProfile: settings._active_profile || null,
296
+ readonly: !settings._skill_source,
296
297
  });
297
298
  }
298
299
 
@@ -309,6 +310,84 @@ async function handleApi(req, res) {
309
310
  return sendJSON(res, { ok: true, ...result });
310
311
  }
311
312
 
313
+ // POST /api/profiles/save — 新增/编辑场景(仅本地源模式)
314
+ if (method === "POST" && pathname === "/api/profiles/save") {
315
+ const body = await parseBody(req);
316
+ const settings = readSettings();
317
+ if (!settings._skill_source)
318
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
319
+
320
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
321
+ let data = { version: 1, profiles: [] };
322
+ if (fs.existsSync(profilesPath)) {
323
+ try {
324
+ data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
325
+ } catch {}
326
+ }
327
+
328
+ const { id, name, description, skills, always_apply } = body;
329
+ if (!name) return sendJSON(res, { error: "场景名称不能为空" }, 400);
330
+
331
+ if (id) {
332
+ const index = data.profiles.findIndex((p) => p.id === id);
333
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
334
+ data.profiles[index] = {
335
+ ...data.profiles[index],
336
+ name,
337
+ description: description || "",
338
+ skills: skills || [],
339
+ always_apply: always_apply || [],
340
+ };
341
+ } else {
342
+ const newId = "profile_" + Date.now().toString(36);
343
+ data.profiles.push({
344
+ id: newId,
345
+ name,
346
+ description: description || "",
347
+ skills: skills || [],
348
+ always_apply: always_apply || [],
349
+ });
350
+ }
351
+
352
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
353
+
354
+ // 同步到项目 .claude
355
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
356
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
357
+
358
+ return sendJSON(res, { ok: true, profiles: data.profiles });
359
+ }
360
+
361
+ // POST /api/profiles/delete — 删除场景(仅本地源模式)
362
+ if (method === "POST" && pathname === "/api/profiles/delete") {
363
+ const body = await parseBody(req);
364
+ const settings = readSettings();
365
+ if (!settings._skill_source)
366
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
367
+
368
+ const { id } = body;
369
+ if (!id) return sendJSON(res, { error: "缺少场景 ID" }, 400);
370
+ if (id === "custom")
371
+ return sendJSON(res, { error: "不能删除内置场景" }, 403);
372
+
373
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
374
+ if (!fs.existsSync(profilesPath))
375
+ return sendJSON(res, { error: "场景文件不存在" }, 404);
376
+
377
+ const data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
378
+ const index = data.profiles.findIndex((p) => p.id === id);
379
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
380
+
381
+ data.profiles.splice(index, 1);
382
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
383
+
384
+ // 同步到项目 .claude
385
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
386
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
387
+
388
+ return sendJSON(res, { ok: true });
389
+ }
390
+
312
391
  // GET /api/status — 当前状态总览
313
392
  if (method === "GET" && pathname === "/api/status") {
314
393
  const settings = readSettings();
@@ -321,6 +400,7 @@ async function handleApi(req, res) {
321
400
  const enabledCount = userSkills.filter((s) => s.enabled).length;
322
401
  const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
323
402
  return sendJSON(res, {
403
+ pkgSkills,
324
404
  enabledCount,
325
405
  totalCount: pkgSkills.length,
326
406
  activeProfile: activeProfile
@@ -344,11 +424,14 @@ async function handleApi(req, res) {
344
424
  if (method === "POST" && pathname === "/api/source/connect") {
345
425
  const body = await parseBody(req);
346
426
  const repoPath = body.path;
347
- if (!repoPath)
348
- return sendJSON(res, { error: "缺少仓库路径" }, 400);
427
+ if (!repoPath) return sendJSON(res, { error: "缺少仓库路径" }, 400);
349
428
  const resolved = path.resolve(repoPath);
350
429
  if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
351
- return sendJSON(res, { error: `路径 "${resolved}" 下没有 .claude/skills/` }, 400);
430
+ return sendJSON(
431
+ res,
432
+ { error: `路径 "${resolved}" 下没有 .claude/skills/` },
433
+ 400,
434
+ );
352
435
  }
353
436
  const settings = readSettings();
354
437
  settings._skill_source = resolved;
@@ -373,8 +456,7 @@ async function handleApi(req, res) {
373
456
  if (method === "POST" && pathname === "/api/source/sync") {
374
457
  const settings = readSettings();
375
458
  const sourcePath = settings._skill_source;
376
- if (!sourcePath)
377
- return sendJSON(res, { error: "尚未绑定技能源" }, 400);
459
+ if (!sourcePath) return sendJSON(res, { error: "尚未绑定技能源" }, 400);
378
460
  if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
379
461
  return sendJSON(res, { error: "绑定的路径已失效" }, 400);
380
462
  }
@@ -41,6 +41,20 @@ export function applyProfile(profileId) {
41
41
  });
42
42
  }
43
43
 
44
+ export function saveProfile(data) {
45
+ return request("/profiles/save", {
46
+ method: "POST",
47
+ body: JSON.stringify(data),
48
+ });
49
+ }
50
+
51
+ export function deleteProfile(id) {
52
+ return request("/profiles/delete", {
53
+ method: "POST",
54
+ body: JSON.stringify({ id }),
55
+ });
56
+ }
57
+
44
58
  export function getStatus() {
45
59
  return request("/status");
46
60
  }
@@ -2,19 +2,38 @@
2
2
  <div class="scene-switch">
3
3
  <div class="page-header">
4
4
  <h2>场景切换</h2>
5
+ <n-button v-if="!readonly" size="small" type="primary" @click="openCreate">
6
+ <template #icon><n-icon><AddOutline /></n-icon></template>
7
+ 新建场景
8
+ </n-button>
5
9
  </div>
6
10
 
7
11
  <n-spin :show="loading">
8
12
  <div class="scene-grid">
9
13
  <n-card
10
- v-for="profile in profiles"
14
+ v-for="profile in filteredProfiles"
11
15
  :key="profile.id"
12
16
  class="scene-card"
13
17
  :class="{ active: profile.id === activeId }"
14
18
  :title="profile.name"
15
19
  size="small"
16
- hoverable
17
20
  >
21
+ <template #header-extra>
22
+ <n-space v-if="!readonly && profile.id !== 'custom' && profile.skills !== null" :size="4">
23
+ <n-button size="tiny" quaternary @click="openEdit(profile)">
24
+ <template #icon><n-icon size="16"><PencilOutline /></n-icon></template>
25
+ </n-button>
26
+ <n-popconfirm @positive-click="onDelete(profile)">
27
+ <template #trigger>
28
+ <n-button size="tiny" quaternary>
29
+ <template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
30
+ </n-button>
31
+ </template>
32
+ 确定删除场景「{{ profile.name }}」?
33
+ </n-popconfirm>
34
+ </n-space>
35
+ </template>
36
+
18
37
  <p class="scene-desc">{{ profile.description }}</p>
19
38
 
20
39
  <template #footer>
@@ -30,7 +49,7 @@
30
49
  </n-tag>
31
50
 
32
51
  <n-button
33
- v-if="profile.id !== activeId && profile.skills !== null"
52
+ v-if="profile.id !== activeId"
34
53
  size="small"
35
54
  type="primary"
36
55
  :loading="applying === profile.id"
@@ -38,44 +57,90 @@
38
57
  >
39
58
  应用
40
59
  </n-button>
41
- <n-button
42
- v-if="profile.id !== activeId && profile.skills === null"
43
- size="small"
44
- @click="openCustomDialog"
45
- >
46
- 自定义勾选
47
- </n-button>
48
60
  </div>
49
61
  </template>
50
62
  </n-card>
51
63
  </div>
52
64
  </n-spin>
53
65
 
54
- <!-- 自定义勾选弹窗 -->
55
- <n-modal v-model:show="showCustom" title="自定义技能组合" preset="card" style="width: 480px">
56
- <n-checkbox-group v-model:value="customSelected">
57
- <n-space vertical>
58
- <n-checkbox
59
- v-for="skill in allSkills"
60
- :key="skill.name"
61
- :value="skill.name"
62
- :label="skill.name"
66
+ <!-- 编辑弹窗 -->
67
+ <n-modal
68
+ v-model:show="showEditor"
69
+ :title="editingProfile ? '编辑场景' : '新建场景'"
70
+ preset="card"
71
+ style="width: 520px"
72
+ :mask-closable="false"
73
+ >
74
+ <n-form :model="formData" label-placement="top">
75
+ <n-form-item label="场景名称" required>
76
+ <n-input
77
+ v-model:value="formData.name"
78
+ placeholder="给场景起个名字"
79
+ :maxlength="30"
63
80
  />
64
- </n-space>
65
- </n-checkbox-group>
81
+ </n-form-item>
82
+ <n-form-item label="场景描述">
83
+ <n-input
84
+ v-model:value="formData.description"
85
+ type="textarea"
86
+ :rows="2"
87
+ placeholder="简短描述这个场景的用途"
88
+ />
89
+ </n-form-item>
90
+ <n-divider />
91
+ <n-form-item label="包含的技能">
92
+ <n-checkbox-group v-model:value="formData.skills">
93
+ <n-space vertical>
94
+ <n-checkbox
95
+ v-for="skill in allSkills"
96
+ :key="skill.name"
97
+ :value="skill.name"
98
+ :label="skill.name"
99
+ />
100
+ </n-space>
101
+ </n-checkbox-group>
102
+ </n-form-item>
103
+ <n-divider />
104
+ <n-form-item label="始终加载(always_apply)">
105
+ <template v-if="formData.skills.length === 0">
106
+ <span class="hint-text">请先勾选包含的技能</span>
107
+ </template>
108
+ <n-checkbox-group v-else v-model:value="formData.always_apply">
109
+ <n-space vertical>
110
+ <n-checkbox
111
+ v-for="skillName in formData.skills"
112
+ :key="skillName"
113
+ :value="skillName"
114
+ :label="skillName"
115
+ />
116
+ </n-space>
117
+ </n-checkbox-group>
118
+ </n-form-item>
119
+ </n-form>
66
120
  <template #footer>
67
- <n-button type="primary" :loading="customLoading" @click="onApplyCustom">
68
- 确认安装
69
- </n-button>
121
+ <n-space justify="end">
122
+ <n-button @click="showEditor = false">取消</n-button>
123
+ <n-button type="primary" :loading="saving" @click="onSave">
124
+ 保存
125
+ </n-button>
126
+ </n-space>
70
127
  </template>
71
128
  </n-modal>
72
129
  </div>
73
130
  </template>
74
131
 
75
132
  <script setup>
76
- import { ref, onMounted } from "vue";
133
+ import { ref, onMounted, computed } from "vue";
77
134
  import { useMessage } from "naive-ui";
78
- import { getProfiles, applyProfile, getSkills, installSkills } from "../api/skills";
135
+ import { NIcon } from "naive-ui";
136
+ import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
137
+ import {
138
+ getProfiles,
139
+ applyProfile,
140
+ getSkills,
141
+ saveProfile,
142
+ deleteProfile,
143
+ } from "../api/skills";
79
144
 
80
145
  const emit = defineEmits(["refresh"]);
81
146
  const message = useMessage();
@@ -85,10 +150,21 @@ const activeId = ref(null);
85
150
  const allSkills = ref([]);
86
151
  const loading = ref(false);
87
152
  const applying = ref(null);
153
+ const readonly = ref(true);
88
154
 
89
- const showCustom = ref(false);
90
- const customSelected = ref([]);
91
- const customLoading = ref(false);
155
+ // 编辑器
156
+ const showEditor = ref(false);
157
+ const editingProfile = ref(null);
158
+ const saving = ref(false);
159
+ const formData = ref({
160
+ name: "",
161
+ description: "",
162
+ skills: [],
163
+ always_apply: [],
164
+ });
165
+
166
+ // 过滤掉不可见的场景(如已有内置)
167
+ const filteredProfiles = computed(() => profiles.value);
92
168
 
93
169
  async function loadData() {
94
170
  loading.value = true;
@@ -96,47 +172,93 @@ async function loadData() {
96
172
  const res = await getProfiles();
97
173
  profiles.value = res.profiles || [];
98
174
  activeId.value = res.activeProfile;
175
+ readonly.value = res.readonly !== false;
99
176
  allSkills.value = await getSkills();
100
177
  } finally {
101
178
  loading.value = false;
102
179
  }
103
180
  }
104
181
 
105
- async function onApply(profile) {
106
- applying.value = profile.id;
182
+ function openCreate() {
183
+ editingProfile.value = null;
184
+ formData.value = {
185
+ name: "",
186
+ description: "",
187
+ skills: [],
188
+ always_apply: [],
189
+ };
190
+ showEditor.value = true;
191
+ }
192
+
193
+ function openEdit(profile) {
194
+ editingProfile.value = profile;
195
+ formData.value = {
196
+ name: profile.name,
197
+ description: profile.description || "",
198
+ skills: [...(profile.skills || [])],
199
+ always_apply: [...(profile.always_apply || [])],
200
+ };
201
+ showEditor.value = true;
202
+ }
203
+
204
+ async function onSave() {
205
+ if (!formData.value.name.trim()) {
206
+ message.warning("请填写场景名称");
207
+ return;
208
+ }
209
+ saving.value = true;
107
210
  try {
108
- const res = await applyProfile(profile.id);
211
+ const data = {
212
+ id: editingProfile.value?.id,
213
+ name: formData.value.name.trim(),
214
+ description: formData.value.description.trim(),
215
+ skills: formData.value.skills,
216
+ always_apply: formData.value.always_apply,
217
+ };
218
+ const res = await saveProfile(data);
109
219
  if (res.ok) {
110
- activeId.value = profile.id;
111
- message.success(`已切换到「${profile.name}」`);
220
+ profiles.value = res.profiles;
221
+ message.success(editingProfile.value ? "场景已更新" : "场景已创建");
222
+ showEditor.value = false;
112
223
  emit("refresh");
224
+ } else if (res.error) {
225
+ message.error(res.error);
113
226
  }
114
227
  } catch {
115
- message.error("切换失败");
228
+ message.error("保存失败");
116
229
  } finally {
117
- applying.value = null;
230
+ saving.value = false;
118
231
  }
119
232
  }
120
233
 
121
- function openCustomDialog() {
122
- customSelected.value = allSkills.value.filter((s) => s.enabled).map((s) => s.name);
123
- showCustom.value = true;
234
+ async function onDelete(profile) {
235
+ try {
236
+ const res = await deleteProfile(profile.id);
237
+ if (res.ok) {
238
+ profiles.value = profiles.value.filter((p) => p.id !== profile.id);
239
+ message.success(`场景「${profile.name}」已删除`);
240
+ emit("refresh");
241
+ } else if (res.error) {
242
+ message.error(res.error);
243
+ }
244
+ } catch {
245
+ message.error("删除失败");
246
+ }
124
247
  }
125
248
 
126
- async function onApplyCustom() {
127
- customLoading.value = true;
249
+ async function onApply(profile) {
250
+ applying.value = profile.id;
128
251
  try {
129
- const res = await installSkills(customSelected.value);
252
+ const res = await applyProfile(profile.id);
130
253
  if (res.ok) {
131
- message.success(`已安装 ${customSelected.value.length} 个技能`);
132
- activeId.value = "custom";
133
- showCustom.value = false;
254
+ activeId.value = profile.id;
255
+ message.success(`已切换到「${profile.name}」`);
134
256
  emit("refresh");
135
257
  }
136
258
  } catch {
137
- message.error("安装失败");
259
+ message.error("切换失败");
138
260
  } finally {
139
- customLoading.value = false;
261
+ applying.value = null;
140
262
  }
141
263
  }
142
264
 
@@ -145,6 +267,9 @@ onMounted(loadData);
145
267
 
146
268
  <style scoped>
147
269
  .page-header {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: space-between;
148
273
  margin-bottom: 20px;
149
274
  }
150
275
 
@@ -178,4 +303,9 @@ onMounted(loadData);
178
303
  align-items: center;
179
304
  justify-content: space-between;
180
305
  }
306
+
307
+ .hint-text {
308
+ font-size: 12px;
309
+ color: #999;
310
+ }
181
311
  </style>
@@ -1,6 +0,0 @@
1
- {
2
- "name": "test1",
3
- "version": "1.0.0",
4
- "description": "测试技能1",
5
- "tags": ["test", "frontend"]
6
- }