mdk-skills 2.4.19 → 2.4.20

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.4.19",
3
+ "version": "2.4.20",
4
4
  "description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
package/scripts/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const { getPackageSkills, getUserSkills, getSkillsSource, backupDir, listSkillDirs } = require("./core");
5
+ const { getPackageSkills, getUserSkills, getSkillsSource, backupDir, listSkillDirs, copyDirSync } = require("./core");
6
6
 
7
7
  // npx 运行时:process.cwd() 是用户项目目录
8
8
  // __dirname 是包内 scripts/ 目录
@@ -47,20 +47,6 @@ function writeSettings(settings) {
47
47
 
48
48
  // ---------- 安装勾选的技能 ----------
49
49
 
50
- // 手动递归拷贝目录,绕开 Windows 中文路径下 fs.cpSync({ recursive: true }) 死锁的 bug
51
- function copyDirSync(src, dest) {
52
- fs.mkdirSync(dest, { recursive: true });
53
- for (const item of fs.readdirSync(src)) {
54
- const srcPath = path.join(src, item);
55
- const destPath = path.join(dest, item);
56
- if (fs.statSync(srcPath).isDirectory()) {
57
- copyDirSync(srcPath, destPath);
58
- } else {
59
- fs.copyFileSync(srcPath, destPath);
60
- }
61
- }
62
- }
63
-
64
50
  function installSelectedSkills(selectedNames) {
65
51
  // 创建 .claude/ 和 skills/ 目录
66
52
  if (!fs.existsSync(skillsDest)) {
@@ -41,23 +41,23 @@ function refreshSource() {
41
41
  pkgSkillsSource = skillsSource ? path.join(skillsSource, ".claude", "skills") : null;
42
42
  }
43
43
 
44
- // ---------- Settings ----------
44
+ // ---------- Settings(异步)----------
45
45
 
46
- function readSettings() {
47
- if (!fs.existsSync(settingsPath))
46
+ async function readSettings() {
47
+ if (!(await pathExists(settingsPath)))
48
48
  return { skills: {}, always_apply_skills: [] };
49
49
  try {
50
- return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
50
+ return JSON.parse(await fs.promises.readFile(settingsPath, "utf-8"));
51
51
  } catch {
52
52
  return { skills: {}, always_apply_skills: [] };
53
53
  }
54
54
  }
55
55
 
56
- function writeSettings(settings) {
57
- if (!fs.existsSync(claudeDest)) {
58
- fs.mkdirSync(claudeDest, { recursive: true });
56
+ async function writeSettings(settings) {
57
+ if (!(await pathExists(claudeDest))) {
58
+ await fs.promises.mkdir(claudeDest, { recursive: true });
59
59
  }
60
- fs.writeFileSync(
60
+ await fs.promises.writeFile(
61
61
  settingsPath,
62
62
  JSON.stringify(settings, null, 2) + "\n",
63
63
  "utf-8",
@@ -70,25 +70,25 @@ function getPullSourcePath() {
70
70
  return path.join(pkgSkillsSource, ".pull-source.json");
71
71
  }
72
72
 
73
- function readPullSource() {
73
+ async function readPullSource() {
74
74
  if (!pkgSkillsSource) return {};
75
75
  const filePath = getPullSourcePath();
76
- if (!fs.existsSync(filePath)) return {};
76
+ if (!(await pathExists(filePath))) return {};
77
77
  try {
78
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
78
+ return JSON.parse(await fs.promises.readFile(filePath, "utf-8"));
79
79
  } catch {
80
80
  return {};
81
81
  }
82
82
  }
83
83
 
84
- function writePullSource(data) {
85
- fs.writeFileSync(getPullSourcePath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
84
+ async function writePullSource(data) {
85
+ await fs.promises.writeFile(getPullSourcePath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
86
86
  }
87
87
 
88
- function addPullSource(skillName, url) {
89
- const data = readPullSource();
88
+ async function addPullSource(skillName, url) {
89
+ const data = await readPullSource();
90
90
  data[skillName] = { url, pulledAt: new Date().toISOString() };
91
- writePullSource(data);
91
+ await writePullSource(data);
92
92
  }
93
93
 
94
94
  // 检查技能源是否已设置
@@ -102,11 +102,11 @@ function requireSource(res, utils) {
102
102
 
103
103
  // ---------- profiles ----------
104
104
 
105
- function loadProfiles() {
105
+ async function loadProfiles() {
106
106
  const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
107
- if (!fs.existsSync(profilesPath)) return null;
107
+ if (!(await pathExists(profilesPath))) return null;
108
108
  try {
109
- return JSON.parse(fs.readFileSync(profilesPath, "utf-8")).profiles;
109
+ return JSON.parse(await fs.promises.readFile(profilesPath, "utf-8")).profiles;
110
110
  } catch {
111
111
  return null;
112
112
  }
@@ -120,9 +120,15 @@ const runningTasks = new Map();
120
120
 
121
121
  const pullCache = new Map();
122
122
 
123
- // ---------- 应用场景 / 安装 ----------
123
+ // ---------- 路径检查工具 ----------
124
124
 
125
- function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSettings }) {
125
+ async function pathExists(p) {
126
+ try { await fs.promises.access(p); return true; } catch { return false; }
127
+ }
128
+
129
+ // ---------- 应用场景 / 安装(异步)----------
130
+
131
+ async function applyProfile(profile, { copyDir, safeRm }) {
126
132
  const pkgSkills = core.listSkillDirs(pkgSkillsSource);
127
133
  const failedDelete = [];
128
134
  const failedInstall = [];
@@ -136,22 +142,22 @@ function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSet
136
142
  selected = profile.skills.filter((s) => pkgSkills.includes(s));
137
143
  }
138
144
 
139
- if (!fs.existsSync(skillsDest)) {
140
- fs.mkdirSync(skillsDest, { recursive: true });
145
+ if (!(await pathExists(skillsDest))) {
146
+ await fs.promises.mkdir(skillsDest, { recursive: true });
141
147
  }
142
148
  for (const name of pkgSkills) {
143
149
  const src = path.join(pkgSkillsSource, name);
144
150
  const dest = path.join(skillsDest, name);
145
151
  if (selected.includes(name)) {
146
- if (!fs.existsSync(dest)) {
147
- try { copyDirSync(src, dest); } catch (err) {
152
+ if (!(await pathExists(dest))) {
153
+ try { await copyDir(src, dest); } catch (err) {
148
154
  console.error(`安装技能 "${name}" 失败:`, err.message);
149
155
  failedInstall.push(name);
150
156
  }
151
157
  }
152
158
  } else {
153
- if (fs.existsSync(dest)) {
154
- const r = safeRmSync(dest, name);
159
+ if (await pathExists(dest)) {
160
+ const r = await safeRm(dest, name);
155
161
  if (!r.removed) failedDelete.push(name);
156
162
  }
157
163
  }
@@ -159,7 +165,7 @@ function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSet
159
165
 
160
166
  const hasFailed = failedDelete.length > 0 || failedInstall.length > 0;
161
167
  if (!hasFailed) {
162
- const settings = readSettings();
168
+ const settings = await readSettings();
163
169
  if (!settings.skills) settings.skills = {};
164
170
  for (const name of selected) {
165
171
  if (!settings.skills[name]) settings.skills[name] = {};
@@ -174,18 +180,18 @@ function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSet
174
180
  settings.always_apply_skills = profile.always_apply;
175
181
  }
176
182
  settings._active_profile = profile.id;
177
- writeSettings(settings);
183
+ await writeSettings(settings);
178
184
 
179
185
  const profilesSource = path.join(skillsSource, ".claude", "profiles.json");
180
186
  const profilesDest = path.join(claudeDest, "profiles.json");
181
- if (fs.existsSync(profilesSource)) {
182
- fs.copyFileSync(profilesSource, profilesDest);
187
+ if (await pathExists(profilesSource)) {
188
+ await fs.promises.copyFile(profilesSource, profilesDest);
183
189
  }
184
190
 
185
191
  const mdSource = path.join(skillsSource, "CLAUDE.md");
186
192
  const mdDest = path.join(projectRoot, "CLAUDE.md");
187
- if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
188
- fs.copyFileSync(mdSource, mdDest);
193
+ if (await pathExists(mdSource) && !(await pathExists(mdDest))) {
194
+ await fs.promises.copyFile(mdSource, mdDest);
189
195
  }
190
196
  }
191
197
 
@@ -198,10 +204,10 @@ function applyProfile(profile, { copyDirSync, safeRmSync, readSettings, writeSet
198
204
  };
199
205
  }
200
206
 
201
- function installSelectedSkills(selectedNames, { copyDirSync, safeRmSync, readSettings, writeSettings }) {
202
- if (!fs.existsSync(pkgSkillsSource)) return { installed: 0, locked: [] };
203
- if (!fs.existsSync(skillsDest)) {
204
- fs.mkdirSync(skillsDest, { recursive: true });
207
+ async function installSelectedSkills(selectedNames, { copyDir, safeRm }) {
208
+ if (!(await pathExists(pkgSkillsSource))) return { installed: 0, locked: [] };
209
+ if (!(await pathExists(skillsDest))) {
210
+ await fs.promises.mkdir(skillsDest, { recursive: true });
205
211
  }
206
212
  const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
207
213
  let installedCount = 0;
@@ -211,41 +217,39 @@ function installSelectedSkills(selectedNames, { copyDirSync, safeRmSync, readSet
211
217
  const src = path.join(pkgSkillsSource, name);
212
218
  const dest = path.join(skillsDest, name);
213
219
  if (selectedNames.includes(name)) {
214
- if (!fs.existsSync(dest)) {
215
- try { copyDirSync(src, dest); installedCount++; } catch (err) {
220
+ if (!(await pathExists(dest))) {
221
+ try { await copyDir(src, dest); installedCount++; } catch (err) {
216
222
  console.error(`安装技能 "${name}" 失败:`, err.message);
217
223
  }
218
224
  }
219
225
  } else {
220
- if (fs.existsSync(dest)) {
221
- const r = safeRmSync(dest, name);
226
+ if (await pathExists(dest)) {
227
+ const r = await safeRm(dest, name);
222
228
  if (!r.removed && r.reason === 'locked') locked.push(name);
223
229
  }
224
230
  }
225
231
  }
226
232
 
227
- const settings = readSettings();
233
+ const settings = await readSettings();
228
234
  if (!settings.skills) settings.skills = {};
229
235
  for (const name of allPkgSkills) {
230
236
  const enabled = selectedNames.includes(name);
231
237
  if (!settings.skills[name]) settings.skills[name] = {};
232
238
  settings.skills[name].enabled = enabled;
233
239
  }
234
- writeSettings(settings);
240
+ await writeSettings(settings);
235
241
 
236
242
  const mdSource = path.join(skillsSource, "CLAUDE.md");
237
243
  const mdDest = path.join(projectRoot, "CLAUDE.md");
238
- if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
239
- fs.copyFileSync(mdSource, mdDest);
244
+ if (await pathExists(mdSource) && !(await pathExists(mdDest))) {
245
+ await fs.promises.copyFile(mdSource, mdDest);
240
246
  }
241
247
 
242
248
  return { installed: installedCount, locked };
243
249
  }
244
250
 
245
- // ---------- 回调函数注册(用于动态依赖注入) ----------
251
+ // ---------- 回调函数注册 ----------
246
252
 
247
- // 由于 applyProfile / installSelectedSkills 需要在编译时传入依赖
248
- // 而在拆分前它们直接引用模块级变量,因此我们在 context 模块级保留接口
249
253
  function buildContext(utils) {
250
254
  return {
251
255
  projectRoot,
@@ -263,8 +267,8 @@ function buildContext(utils) {
263
267
  addPullSource,
264
268
  loadProfiles,
265
269
  requireSource: (res) => requireSource(res, utils),
266
- applyProfile: (profile) => applyProfile(profile, utils),
267
- installSelectedSkills: (names) => installSelectedSkills(names, utils),
270
+ applyProfile: (profile) => applyProfile(profile, { copyDir: utils.copyDir, safeRm: utils.safeRm }),
271
+ installSelectedSkills: (names) => installSelectedSkills(names, { copyDir: utils.copyDir, safeRm: utils.safeRm }),
268
272
  runningTasks,
269
273
  pullCache,
270
274
  };
@@ -6,6 +6,10 @@ const { buildContext } = require("../context");
6
6
 
7
7
  const ctx = buildContext(utils);
8
8
 
9
+ async function pathExists(p) {
10
+ try { await fs.promises.access(p); return true; } catch { return false; }
11
+ }
12
+
9
13
  function register(router) {
10
14
 
11
15
  // ----------------------------------------------------------------
@@ -14,7 +18,10 @@ function register(router) {
14
18
  router.get("/api/readme", async (req, res) => {
15
19
  if (!ctx.skillsSource) return utils.sendJSON(res, { content: null, found: false });
16
20
  const p = path.join(ctx.skillsSource, "README.md");
17
- const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
21
+ let content = null;
22
+ if (await pathExists(p)) {
23
+ content = await fs.promises.readFile(p, "utf-8");
24
+ }
18
25
  return utils.sendJSON(res, { content, found: !!content });
19
26
  });
20
27
 
@@ -24,7 +31,10 @@ function register(router) {
24
31
  router.get("/api/skills-readme", async (req, res) => {
25
32
  if (!ctx.skillsSource) return utils.sendJSON(res, { content: null, found: false });
26
33
  const p = path.join(ctx.skillsSource, ".claude", "skills", "README.md");
27
- const content = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : null;
34
+ let content = null;
35
+ if (await pathExists(p)) {
36
+ content = await fs.promises.readFile(p, "utf-8");
37
+ }
28
38
  return utils.sendJSON(res, { content, found: !!content });
29
39
  });
30
40
 
@@ -37,8 +47,8 @@ function register(router) {
37
47
  : [path.join(ctx.projectRoot, "CLAUDE.md")];
38
48
  let content = null;
39
49
  for (const p of paths) {
40
- if (fs.existsSync(p)) {
41
- content = fs.readFileSync(p, "utf-8");
50
+ if (await pathExists(p)) {
51
+ content = await fs.promises.readFile(p, "utf-8");
42
52
  break;
43
53
  }
44
54
  }
@@ -50,7 +60,7 @@ function register(router) {
50
60
  // ----------------------------------------------------------------
51
61
  router.post("/api/readme/create", async (req, res) => {
52
62
  const body = await utils.parseBody(req);
53
- const settings = ctx.readSettings();
63
+ const settings = await ctx.readSettings();
54
64
  const sourcePath = settings._skill_source;
55
65
  if (!sourcePath) return utils.sendJSON(res, { error: "未绑定本地源" }, 400);
56
66
 
@@ -61,7 +71,7 @@ function register(router) {
61
71
  const skipped = [];
62
72
 
63
73
  const skillsDir = path.join(sourcePath, ".claude", "skills");
64
- const skillNames = fs.existsSync(skillsDir)
74
+ const skillNames = await pathExists(skillsDir)
65
75
  ? core.listSkillDirs(skillsDir)
66
76
  : [];
67
77
  const skillsList = skillNames.length
@@ -71,7 +81,7 @@ function register(router) {
71
81
  // 根目录 README.md
72
82
  if (root) {
73
83
  const rootPath = path.join(sourcePath, "README.md");
74
- if (!fs.existsSync(rootPath)) {
84
+ if (!(await pathExists(rootPath))) {
75
85
  const tmpl = `# ${name}
76
86
 
77
87
  > 维护团队:${team}
@@ -97,7 +107,7 @@ ${skillsList}
97
107
 
98
108
  <!-- 许可证信息 -->
99
109
  `;
100
- fs.writeFileSync(rootPath, tmpl, "utf-8");
110
+ await fs.promises.writeFile(rootPath, tmpl, "utf-8");
101
111
  created.push("README.md");
102
112
  } else {
103
113
  skipped.push("README.md");
@@ -107,7 +117,7 @@ ${skillsList}
107
117
  // skills/README.md
108
118
  if (skills) {
109
119
  const skillsReadmePath = path.join(skillsDir, "README.md");
110
- if (!fs.existsSync(skillsReadmePath)) {
120
+ if (!(await pathExists(skillsReadmePath))) {
111
121
  const tmpl = `# 技能说明
112
122
 
113
123
  ## 快速开始
@@ -132,7 +142,7 @@ ${skillsList}
132
142
  技能使用过程中的注意事项。
133
143
  -->
134
144
  `;
135
- fs.writeFileSync(skillsReadmePath, tmpl, "utf-8");
145
+ await fs.promises.writeFile(skillsReadmePath, tmpl, "utf-8");
136
146
  created.push("skills/README.md");
137
147
  } else {
138
148
  skipped.push("skills/README.md");
@@ -154,10 +164,10 @@ ${skillsList}
154
164
  for (const skill of pkgSkills) {
155
165
  const skillDir = path.join(ctx.skillsDest, skill.name);
156
166
  const issues = [];
157
- if (!fs.existsSync(skillDir)) {
167
+ if (!(await pathExists(skillDir))) {
158
168
  issues.push("未安装");
159
169
  } else {
160
- if (!fs.existsSync(path.join(skillDir, "SKILL.md"))) {
170
+ if (!(await pathExists(path.join(skillDir, "SKILL.md")))) {
161
171
  issues.push("缺少 SKILL.md");
162
172
  } else if (!core.parseFrontmatter(skillDir)) {
163
173
  issues.push("SKILL.md frontmatter 格式异常");
@@ -194,13 +204,13 @@ ${skillsList}
194
204
 
195
205
  if (!dirPath || dirPath === "") {
196
206
  if (process.platform === "win32") {
197
- entries = utils.getWindowsDrives();
207
+ entries = await utils.getWindowsDrives();
198
208
  } else {
199
209
  entries = [{ name: "/", isDir: true }];
200
210
  }
201
211
  } else {
202
212
  try {
203
- const items = fs.readdirSync(dirPath, { withFileTypes: true });
213
+ const items = await fs.promises.readdir(dirPath, { withFileTypes: true });
204
214
  entries = items
205
215
  .filter((item) => item.isDirectory())
206
216
  .map((item) => ({ name: item.name, isDir: true }))
@@ -221,7 +231,7 @@ ${skillsList}
221
231
  if (!dirPath) return utils.sendJSON(res, { error: "缺少路径" }, 400);
222
232
 
223
233
  const skillsDir = path.join(dirPath, ".claude", "skills");
224
- const valid = fs.existsSync(skillsDir);
234
+ const valid = await pathExists(skillsDir);
225
235
  let skillCount = 0;
226
236
  if (valid) {
227
237
  skillCount = core.listSkillDirs(skillsDir).length;
@@ -5,6 +5,10 @@ const { buildContext } = require("../context");
5
5
 
6
6
  const ctx = buildContext(utils);
7
7
 
8
+ async function pathExists(p) {
9
+ try { await fs.promises.access(p); return true; } catch { return false; }
10
+ }
11
+
8
12
  function register(router) {
9
13
 
10
14
  // ----------------------------------------------------------------
@@ -14,8 +18,8 @@ function register(router) {
14
18
  if (!ctx.skillsSource || !ctx.pkgSkillsSource) {
15
19
  return utils.sendJSON(res, { error: "未设置技能目录,请在设置中连接" }, 400);
16
20
  }
17
- const profiles = ctx.loadProfiles();
18
- const settings = ctx.readSettings();
21
+ const profiles = await ctx.loadProfiles();
22
+ const settings = await ctx.readSettings();
19
23
  return utils.sendJSON(res, {
20
24
  profiles: profiles || [],
21
25
  activeProfile: settings._active_profile || null,
@@ -33,11 +37,11 @@ function register(router) {
33
37
  const body = await utils.parseBody(req);
34
38
  const { profileId } = body;
35
39
  if (!profileId) return utils.sendJSON(res, { error: "缺少场景 ID" }, 400);
36
- const profiles = ctx.loadProfiles();
40
+ const profiles = await ctx.loadProfiles();
37
41
  const profile = profiles?.find((p) => p.id === profileId);
38
42
  if (!profile)
39
43
  return utils.sendJSON(res, { error: "场景不存在: " + profileId }, 404);
40
- const result = ctx.applyProfile(profile);
44
+ const result = await ctx.applyProfile(profile);
41
45
  return utils.sendJSON(res, { ok: true, ...result });
42
46
  });
43
47
 
@@ -52,9 +56,9 @@ function register(router) {
52
56
 
53
57
  const profilesPath = path.join(ctx.skillsSource, ".claude", "profiles.json");
54
58
  let data = { version: 1, profiles: [] };
55
- if (fs.existsSync(profilesPath)) {
59
+ if (await pathExists(profilesPath)) {
56
60
  try {
57
- data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
61
+ data = JSON.parse(await fs.promises.readFile(profilesPath, "utf-8"));
58
62
  } catch {}
59
63
  }
60
64
 
@@ -82,11 +86,11 @@ function register(router) {
82
86
  });
83
87
  }
84
88
 
85
- fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
89
+ await fs.promises.writeFile(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
86
90
 
87
91
  // 同步到项目 .claude
88
92
  const projectProfilesPath = path.join(ctx.claudeDest, "profiles.json");
89
- fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
93
+ await fs.promises.writeFile(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
90
94
 
91
95
  return utils.sendJSON(res, { ok: true, profiles: data.profiles });
92
96
  });
@@ -106,19 +110,19 @@ function register(router) {
106
110
  return utils.sendJSON(res, { error: "不能删除内置场景" }, 403);
107
111
 
108
112
  const profilesPath = path.join(ctx.skillsSource, ".claude", "profiles.json");
109
- if (!fs.existsSync(profilesPath))
113
+ if (!(await pathExists(profilesPath)))
110
114
  return utils.sendJSON(res, { error: "场景文件不存在" }, 404);
111
115
 
112
- const data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
116
+ const data = JSON.parse(await fs.promises.readFile(profilesPath, "utf-8"));
113
117
  const index = data.profiles.findIndex((p) => p.id === id);
114
118
  if (index === -1) return utils.sendJSON(res, { error: "场景不存在" }, 404);
115
119
 
116
120
  data.profiles.splice(index, 1);
117
- fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
121
+ await fs.promises.writeFile(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
118
122
 
119
123
  // 同步到项目 .claude
120
124
  const projectProfilesPath = path.join(ctx.claudeDest, "profiles.json");
121
- fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
125
+ await fs.promises.writeFile(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
122
126
 
123
127
  return utils.sendJSON(res, { ok: true });
124
128
  });