mdk-skills 2.2.9 → 2.2.11

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/scripts/core.js CHANGED
@@ -109,6 +109,17 @@ function deepMerge(userObj, pkgObj) {
109
109
  return result;
110
110
  }
111
111
 
112
+ /**
113
+ * 列出技能目录下的有效技能目录(排除非目录项和隐藏目录)
114
+ */
115
+ function listSkillDirs(skillsPath) {
116
+ if (!fs.existsSync(skillsPath)) return [];
117
+ return fs.readdirSync(skillsPath).filter((name) => {
118
+ const fullPath = path.join(skillsPath, name);
119
+ return fs.statSync(fullPath).isDirectory() && !name.startsWith(".");
120
+ });
121
+ }
122
+
112
123
  /**
113
124
  * 读取技能目录下的 .meta.json,没有则返回 null
114
125
  */
@@ -134,7 +145,7 @@ function installSkills(skillsSource, skillsDest, logFile) {
134
145
  fs.mkdirSync(skillsDest, { recursive: true });
135
146
  }
136
147
 
137
- const skillDirs = fs.readdirSync(skillsSource);
148
+ const skillDirs = listSkillDirs(skillsSource);
138
149
  const results = [];
139
150
 
140
151
  for (const dir of skillDirs) {
@@ -202,9 +213,7 @@ function installSettings(sourceDir, destDir, logFile) {
202
213
  */
203
214
  function getPackageSkills(packageDir) {
204
215
  const skillsSource = path.join(packageDir, ".claude", "skills");
205
- if (!fs.existsSync(skillsSource)) return [];
206
-
207
- return fs.readdirSync(skillsSource).map((name) => {
216
+ return listSkillDirs(skillsSource).map((name) => {
208
217
  const meta = readMeta(path.join(skillsSource, name));
209
218
  return {
210
219
  name,
@@ -250,7 +259,7 @@ function getUserSkills(claudeDest) {
250
259
 
251
260
  const skillConfigs = settings.skills || {};
252
261
 
253
- return fs.readdirSync(skillsDest).map((name) => {
262
+ return listSkillDirs(skillsDest).map((name) => {
254
263
  const meta = readMeta(path.join(skillsDest, name));
255
264
  const config = skillConfigs[name] || {};
256
265
  return {
@@ -270,6 +279,7 @@ module.exports = {
270
279
  backupDir,
271
280
  copyDirSync,
272
281
  deepMerge,
282
+ listSkillDirs,
273
283
  readMeta,
274
284
  installSkills,
275
285
  installSettings,
@@ -95,9 +95,7 @@ function loadProfiles() {
95
95
 
96
96
  // 应用场景
97
97
  function applyProfile(profile) {
98
- const pkgSkills = fs.existsSync(pkgSkillsSource)
99
- ? fs.readdirSync(pkgSkillsSource)
100
- : [];
98
+ const pkgSkills = core.listSkillDirs(pkgSkillsSource);
101
99
  const settings = readSettings();
102
100
 
103
101
  let selected;
@@ -153,6 +151,13 @@ function applyProfile(profile) {
153
151
  fs.copyFileSync(profilesSource, profilesDest);
154
152
  }
155
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
+
156
161
  return {
157
162
  selected,
158
163
  enabled: selected.length,
@@ -168,7 +173,7 @@ function installSelectedSkills(selectedNames) {
168
173
  fs.mkdirSync(skillsDest, { recursive: true });
169
174
  }
170
175
 
171
- const allPkgSkills = fs.readdirSync(pkgSkillsSource);
176
+ const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
172
177
  let installedCount = 0;
173
178
 
174
179
  for (const name of allPkgSkills) {
@@ -200,6 +205,13 @@ function installSelectedSkills(selectedNames) {
200
205
  }
201
206
  writeSettings(settings);
202
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
+
203
215
  return { installed: installedCount };
204
216
  }
205
217
 
@@ -286,6 +298,50 @@ async function handleApi(req, res) {
286
298
  return sendJSON(res, { ok: true, ...result });
287
299
  }
288
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
+
289
345
  // GET /api/profiles — 获取场景列表(含只读状态)
290
346
  if (method === "GET" && pathname === "/api/profiles") {
291
347
  const profiles = loadProfiles();
@@ -411,12 +467,40 @@ async function handleApi(req, res) {
411
467
  });
412
468
  }
413
469
 
414
- // GET /api/source — 获取本地源信息
470
+ // GET /api/source — 获取本地源信息(含初始化检测)
415
471
  if (method === "GET" && pathname === "/api/source") {
416
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
+
417
500
  return sendJSON(res, {
418
- connected: !!settings._skill_source,
419
- path: settings._skill_source || null,
501
+ connected: !!sourcePath,
502
+ path: sourcePath,
503
+ needsInit,
420
504
  });
421
505
  }
422
506
 
@@ -436,6 +520,13 @@ async function handleApi(req, res) {
436
520
  const settings = readSettings();
437
521
  settings._skill_source = resolved;
438
522
  writeSettings(settings);
523
+ // 复制 CLAUDE.md(不存在才装)
524
+ const mdSource = path.join(resolved, "CLAUDE.md");
525
+ const mdDest = path.join(projectRoot, "CLAUDE.md");
526
+ if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
527
+ fs.copyFileSync(mdSource, mdDest);
528
+ }
529
+
439
530
  refreshSource();
440
531
  return sendJSON(res, { ok: true, path: resolved });
441
532
  }
@@ -469,7 +560,7 @@ async function handleApi(req, res) {
469
560
  if (!fs.existsSync(repoSkills))
470
561
  fs.mkdirSync(repoSkills, { recursive: true });
471
562
  if (fs.existsSync(skillsDest)) {
472
- for (const name of fs.readdirSync(skillsDest)) {
563
+ for (const name of core.listSkillDirs(skillsDest)) {
473
564
  const src = path.join(skillsDest, name);
474
565
  const dest = path.join(repoSkills, name);
475
566
  if (fs.existsSync(dest))
@@ -506,6 +597,80 @@ async function handleApi(req, res) {
506
597
  return sendJSON(res, { ok: true });
507
598
  }
508
599
 
600
+ // POST /api/source/init — 初始化本地源骨架文件(缺啥补啥)
601
+ if (method === "POST" && pathname === "/api/source/init") {
602
+ const settings = readSettings();
603
+ const sourcePath = settings._skill_source;
604
+ if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
605
+
606
+ const claudeDir = path.join(sourcePath, ".claude");
607
+ const skillsDir = path.join(claudeDir, "skills");
608
+ const profilesPath = path.join(claudeDir, "profiles.json");
609
+ const settingsPath = path.join(claudeDir, "settings.json");
610
+
611
+ const created = { profiles: false, settings: false, metaFiles: 0 };
612
+
613
+ // 1. profiles.json
614
+ if (!fs.existsSync(profilesPath)) {
615
+ fs.writeFileSync(
616
+ profilesPath,
617
+ JSON.stringify(
618
+ {
619
+ version: 1,
620
+ profiles: [
621
+ {
622
+ id: "custom",
623
+ name: "自定义技能组合",
624
+ description: "自由勾选技能项,按需个性化配置",
625
+ skills: null,
626
+ },
627
+ ],
628
+ },
629
+ null,
630
+ 2,
631
+ ) + "\n",
632
+ "utf-8",
633
+ );
634
+ created.profiles = true;
635
+ }
636
+
637
+ // 2. settings.json
638
+ if (!fs.existsSync(settingsPath)) {
639
+ fs.writeFileSync(
640
+ settingsPath,
641
+ JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
642
+ "utf-8",
643
+ );
644
+ created.settings = true;
645
+ }
646
+
647
+ // 3. 每个 skill 目录的 .meta.json
648
+ if (fs.existsSync(skillsDir)) {
649
+ for (const name of core.listSkillDirs(skillsDir)) {
650
+ const skillDir = path.join(skillsDir, name);
651
+ const metaPath = path.join(skillDir, ".meta.json");
652
+ if (!fs.existsSync(metaPath)) {
653
+ fs.writeFileSync(
654
+ metaPath,
655
+ JSON.stringify(
656
+ {
657
+ version: "1.0.0",
658
+ description: name,
659
+ tags: [],
660
+ },
661
+ null,
662
+ 2,
663
+ ) + "\n",
664
+ "utf-8",
665
+ );
666
+ created.metaFiles++;
667
+ }
668
+ }
669
+ }
670
+
671
+ return sendJSON(res, { ok: true, ...created });
672
+ }
673
+
509
674
  // GET /api/diagnose — 健康检查
510
675
  if (method === "GET" && pathname === "/api/diagnose") {
511
676
  const results = [];
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <n-message-provider>
3
3
  <n-notification-provider>
4
- <n-config-provider :theme="theme">
4
+ <n-config-provider :theme="theme" :locale="zhCN" :date-locale="dateZhCN">
5
5
  <n-layout class="app-layout">
6
6
  <!-- 导航栏 -->
7
7
  <n-layout-header class="app-header" bordered>
@@ -37,11 +37,12 @@
37
37
  <script setup>
38
38
  import { h, ref, onMounted, computed } from "vue";
39
39
  import { useRouter, useRoute } from "vue-router";
40
- import { NIcon } from "naive-ui";
40
+ import { NIcon, zhCN, dateZhCN } from "naive-ui";
41
41
  import {
42
42
  AppsOutline,
43
43
  SwapHorizontalOutline,
44
44
  CogOutline,
45
+ DocumentTextOutline,
45
46
  } from "@vicons/ionicons5";
46
47
  import StatusBar from "./components/StatusBar.vue";
47
48
  import { getStatus } from "./api/skills";
@@ -69,6 +70,11 @@ const menuOptions = [
69
70
  key: "Settings",
70
71
  icon: () => h(NIcon, null, { default: () => h(CogOutline) }),
71
72
  },
73
+ {
74
+ label: "CLAUDE.md",
75
+ key: "ClaudeMd",
76
+ icon: () => h(NIcon, null, { default: () => h(DocumentTextOutline) }),
77
+ },
72
78
  ];
73
79
 
74
80
  const activeKey = ref(route.name || "Dashboard");
@@ -78,6 +78,26 @@ export function syncSource() {
78
78
  return request("/source/sync", { method: "POST" });
79
79
  }
80
80
 
81
+ export function initSource() {
82
+ return request("/source/init", { method: "POST" });
83
+ }
84
+
81
85
  export function diagnose() {
82
86
  return request("/diagnose");
83
87
  }
88
+
89
+ export function getReadme() {
90
+ return request("/readme");
91
+ }
92
+
93
+ export function getSkillsReadme() {
94
+ return request("/skills-readme");
95
+ }
96
+
97
+ export function getClaudeMd() {
98
+ return request("/claudemd");
99
+ }
100
+
101
+ export function getSkillReadme(name) {
102
+ return request("/skills/" + encodeURIComponent(name) + "/readme");
103
+ }
@@ -1,11 +1,13 @@
1
1
  <template>
2
- <n-card class="skill-card" :title="skill.name" size="small">
2
+ <n-card class="skill-card" :title="skill.name" size="small" @click="emit('click')">
3
3
  <template #header-extra>
4
- <n-switch
5
- :value="skill.enabled"
6
- :loading="loading"
7
- @update:value="onToggle"
8
- />
4
+ <span @click.stop>
5
+ <n-switch
6
+ :value="skill.enabled"
7
+ :loading="loading"
8
+ @update:value="onToggle"
9
+ />
10
+ </span>
9
11
  </template>
10
12
  <div class="skill-meta">
11
13
  <span class="skill-version">v{{ skill.version }}</span>
@@ -26,7 +28,7 @@ const props = defineProps({
26
28
  skill: { type: Object, required: true },
27
29
  });
28
30
 
29
- const emit = defineEmits(["refresh"]);
31
+ const emit = defineEmits(["refresh", "click"]);
30
32
  const message = useMessage();
31
33
  const loading = ref(false);
32
34
 
@@ -49,6 +51,12 @@ async function onToggle(value) {
49
51
  <style scoped>
50
52
  .skill-card {
51
53
  margin-bottom: 12px;
54
+ cursor: pointer;
55
+ transition: box-shadow 0.2s;
56
+ }
57
+
58
+ .skill-card:hover {
59
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
52
60
  }
53
61
 
54
62
  .skill-meta {
@@ -69,4 +77,4 @@ async function onToggle(value) {
69
77
  color: #666;
70
78
  margin: 0;
71
79
  }
72
- </style>
80
+ </style>
@@ -19,7 +19,7 @@
19
19
  </span>
20
20
  </div>
21
21
  <div class="status-divider" />
22
- <div class="status-item">
22
+ <div class="status-item clickable" @click="$router.push({ name: 'ClaudeMd' })">
23
23
  <n-icon size="18" :color="data.hasClaudeMd ? '#18a058' : '#d03050'">
24
24
  <DocumentTextOutline />
25
25
  </n-icon>
@@ -58,6 +58,14 @@ defineProps({
58
58
  color: #555;
59
59
  }
60
60
 
61
+ .status-item.clickable {
62
+ cursor: pointer;
63
+ }
64
+
65
+ .status-item.clickable:hover {
66
+ color: #2080f0;
67
+ }
68
+
61
69
  .status-divider {
62
70
  width: 1px;
63
71
  height: 20px;
@@ -2,6 +2,7 @@ import { createApp } from "vue";
2
2
  import naive from "naive-ui";
3
3
  import App from "./App.vue";
4
4
  import router from "./router";
5
+ import "highlight.js/styles/github.css";
5
6
 
6
7
  const app = createApp(App);
7
8
  app.use(naive);
@@ -2,11 +2,13 @@ import { createRouter, createWebHistory } from "vue-router";
2
2
  import Dashboard from "../views/Dashboard.vue";
3
3
  import SceneSwitch from "../views/SceneSwitch.vue";
4
4
  import Settings from "../views/Settings.vue";
5
+ import ReadmeView from "../views/ReadmeView.vue";
5
6
 
6
7
  const routes = [
7
8
  { path: "/", name: "Dashboard", component: Dashboard, meta: { title: "仪表盘" } },
8
9
  { path: "/scenes", name: "Scenes", component: SceneSwitch, meta: { title: "场景切换" } },
9
10
  { path: "/settings", name: "Settings", component: Settings, meta: { title: "设置" } },
11
+ { path: "/readme", name: "ClaudeMd", component: ReadmeView, meta: { title: "CLAUDE.md" } },
10
12
  ];
11
13
 
12
14
  export default createRouter({
@@ -57,3 +57,99 @@ html, body, #app {
57
57
  width: 100%;
58
58
  margin: 0 auto;
59
59
  }
60
+
61
+ /* ---------- markdown 渲染样式 ---------- */
62
+
63
+ .markdown-content {
64
+ line-height: 1.7;
65
+ font-size: 14px;
66
+ color: #333;
67
+ word-wrap: break-word;
68
+ }
69
+
70
+ .markdown-content h1 { font-size: 22px; font-weight: 700; margin: 20px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #eee; }
71
+ .markdown-content h2 { font-size: 18px; font-weight: 700; margin: 18px 0 10px; padding-bottom: 6px; border-bottom: 1px solid #eee; }
72
+ .markdown-content h3 { font-size: 16px; font-weight: 600; margin: 16px 0 8px; }
73
+ .markdown-content h4 { font-size: 14px; font-weight: 600; margin: 14px 0 6px; }
74
+ .markdown-content h5, .markdown-content h6 { font-size: 13px; font-weight: 600; margin: 12px 0 6px; }
75
+
76
+ .markdown-content p { margin: 8px 0; }
77
+ .markdown-content ul, .markdown-content ol { margin: 8px 0; padding-left: 24px; }
78
+ .markdown-content li { margin: 4px 0; }
79
+ .markdown-content blockquote {
80
+ margin: 10px 0;
81
+ padding: 8px 16px;
82
+ border-left: 4px solid #2080f0;
83
+ background: #f0f7ff;
84
+ color: #555;
85
+ }
86
+ .markdown-content blockquote p { margin: 4px 0; }
87
+
88
+ .markdown-content a { color: #2080f0; text-decoration: none; }
89
+ .markdown-content a:hover { text-decoration: underline; }
90
+
91
+ .markdown-content hr { margin: 16px 0; border: none; border-top: 1px solid #e0e0e0; }
92
+
93
+ .markdown-content table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
94
+ .markdown-content th, .markdown-content td {
95
+ padding: 8px 12px;
96
+ border: 1px solid #e0e0e0;
97
+ text-align: left;
98
+ }
99
+ .markdown-content th { background: #f5f7fa; font-weight: 600; }
100
+ .markdown-content tr:nth-child(even) td { background: #fafafa; }
101
+
102
+ .markdown-content img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
103
+
104
+ .markdown-content code {
105
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
106
+ font-size: 13px;
107
+ padding: 2px 6px;
108
+ background: #f0f0f0;
109
+ border-radius: 3px;
110
+ color: #d63384;
111
+ }
112
+
113
+ .markdown-content pre {
114
+ margin: 12px 0;
115
+ padding: 0;
116
+ border-radius: 6px;
117
+ overflow: hidden;
118
+ border: 1px solid #e8e8e8;
119
+ position: relative;
120
+ }
121
+
122
+ .markdown-content pre code {
123
+ display: block;
124
+ padding: 14px 16px;
125
+ background: #f8f9fa;
126
+ color: #333;
127
+ font-size: 13px;
128
+ line-height: 1.5;
129
+ overflow-x: auto;
130
+ tab-size: 2;
131
+ }
132
+
133
+ .markdown-content pre .copy-btn {
134
+ position: absolute;
135
+ top: 6px;
136
+ right: 6px;
137
+ padding: 3px 8px;
138
+ font-size: 11px;
139
+ color: #999;
140
+ background: #fff;
141
+ border: 1px solid #e0e0e0;
142
+ border-radius: 4px;
143
+ cursor: pointer;
144
+ opacity: 0;
145
+ transition: opacity 0.2s;
146
+ }
147
+
148
+ .markdown-content pre:hover .copy-btn {
149
+ opacity: 1;
150
+ }
151
+
152
+ .markdown-content pre .copy-btn:hover {
153
+ color: #2080f0;
154
+ border-color: #2080f0;
155
+ }