dk-frontend-skills 2.1.2 → 3.0.0

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 (64) hide show
  1. package/package.json +11 -5
  2. package/scripts/build-skills.js +89 -0
  3. package/scripts/cli.js +126 -37
  4. package/scripts/copy-skills.js +20 -21
  5. package/scripts/core.js +71 -60
  6. package/skills-index.json +99 -0
  7. package/.claude/skills/agentation/.meta.json +0 -6
  8. package/.claude/skills/agentation/SKILL.md +0 -107
  9. package/.claude/skills/fe-biz-patterns/.meta.json +0 -6
  10. package/.claude/skills/fe-biz-patterns/SKILL.md +0 -26
  11. package/.claude/skills/fe-biz-patterns/references/infinite-scroll.md +0 -292
  12. package/.claude/skills/fe-biz-patterns/references/pinia-store.md +0 -174
  13. package/.claude/skills/fe-biz-patterns/references/service-layer.md +0 -198
  14. package/.claude/skills/fe-biz-patterns/references/tab-anchor.md +0 -1125
  15. package/.claude/skills/fe-biz-patterns/references/use-loading.md +0 -114
  16. package/.claude/skills/frontend-code-review/.meta.json +0 -6
  17. package/.claude/skills/frontend-code-review/SKILL.md +0 -167
  18. package/.claude/skills/frontend-code-review/references/checklist.md +0 -298
  19. package/.claude/skills/frontend-design/.meta.json +0 -6
  20. package/.claude/skills/frontend-design/LICENSE.txt +0 -177
  21. package/.claude/skills/frontend-design/SKILL.md +0 -42
  22. package/.claude/skills/moai-framework-electron/.meta.json +0 -6
  23. package/.claude/skills/moai-framework-electron/SKILL.md +0 -328
  24. package/.claude/skills/skill-creator/.meta.json +0 -6
  25. package/.claude/skills/skill-creator/SKILL.md +0 -356
  26. package/.claude/skills/skill-creator/references/output-patterns.md +0 -82
  27. package/.claude/skills/skill-creator/references/workflows.md +0 -28
  28. package/.claude/skills/skill-creator/scripts/init_skill.py +0 -303
  29. package/.claude/skills/skill-creator/scripts/package_skill.py +0 -110
  30. package/.claude/skills/skill-creator/scripts/quick_validate.py +0 -95
  31. package/.claude/skills/ui-ux-pro-max/.meta.json +0 -6
  32. package/.claude/skills/ui-ux-pro-max/SKILL.md +0 -228
  33. package/.claude/skills/ui-ux-pro-max/data/charts.csv +0 -26
  34. package/.claude/skills/ui-ux-pro-max/data/colors.csv +0 -97
  35. package/.claude/skills/ui-ux-pro-max/data/landing.csv +0 -31
  36. package/.claude/skills/ui-ux-pro-max/data/products.csv +0 -97
  37. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +0 -24
  38. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  39. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  40. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  41. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  42. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  43. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  44. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +0 -54
  45. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  46. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  47. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  48. package/.claude/skills/ui-ux-pro-max/data/styles.csv +0 -59
  49. package/.claude/skills/ui-ux-pro-max/data/typography.csv +0 -58
  50. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  51. package/.claude/skills/ui-ux-pro-max/scripts/core.py +0 -238
  52. package/.claude/skills/ui-ux-pro-max/scripts/search.py +0 -61
  53. package/.claude/skills/vue/.meta.json +0 -6
  54. package/.claude/skills/vue/SKILL.md +0 -103
  55. package/.claude/skills/vue/references/components.md +0 -323
  56. package/.claude/skills/vue/references/composables.md +0 -358
  57. package/.claude/skills/vue/references/directives.md +0 -225
  58. package/.claude/skills/vue/references/gotchas.md +0 -438
  59. package/.claude/skills/vue/references/provide-inject.md +0 -174
  60. package/.claude/skills/vue/references/reactivity.md +0 -289
  61. package/.claude/skills/vue/references/router.md +0 -181
  62. package/.claude/skills/vue/references/testing.md +0 -294
  63. package/.claude/skills/vue/references/typescript.md +0 -172
  64. package/.claude/skills/vue/references/utils-client.md +0 -156
package/package.json CHANGED
@@ -1,23 +1,29 @@
1
1
  {
2
2
  "name": "dk-frontend-skills",
3
- "version": "2.1.2",
3
+ "version": "3.0.0",
4
4
  "description": "dk-engineer - 幽默沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "files": [
9
- ".claude/",
10
9
  "CLAUDE.md",
11
- "scripts/"
10
+ "scripts/",
11
+ "skills-index.json",
12
+ ".claude/settings.json",
13
+ ".claude/settings.local.json"
12
14
  ],
13
15
  "bin": {
14
- "dk-skills": "./scripts/cli.js"
16
+ "dk-skills": "./scripts/cli.js",
17
+ "dk-frontend-skills": "./scripts/cli.js"
15
18
  },
16
19
  "scripts": {
20
+ "build-skills": "node scripts/build-skills.js",
17
21
  "postinstall": "node scripts/copy-skills.js"
18
22
  },
19
23
  "dependencies": {
20
24
  "@inquirer/prompts": "^8.4.2",
21
- "chalk": "^5.6.2"
25
+ "chalk": "^5.6.2",
26
+ "cli-progress": "^3.12.0",
27
+ "tar": "^7.5.15"
22
28
  }
23
29
  }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 技能打包脚本
5
+ * 将 skills/<name>/ 目录打包成 dist/skills/<name>-<version>.tar.gz
6
+ * 并更新 skills-index.json 中的版本和文件大小
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const tar = require("tar");
12
+ const { execSync } = require("child_process");
13
+
14
+ const repoRoot = path.join(__dirname, "..");
15
+ const skillsDir = path.join(repoRoot, "skills");
16
+ const distDir = path.join(repoRoot, "dist", "skills");
17
+ const indexPath = path.join(repoRoot, "skills-index.json");
18
+
19
+ async function build() {
20
+ // 读取已有的索引
21
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
22
+ const indexSkills = index.skills;
23
+
24
+ // 确保输出目录存在
25
+ fs.mkdirSync(distDir, { recursive: true });
26
+
27
+ const skillNames = fs.readdirSync(skillsDir);
28
+
29
+ for (const name of skillNames) {
30
+ const skillPath = path.join(skillsDir, name);
31
+ if (!fs.statSync(skillPath).isDirectory()) continue;
32
+
33
+ // 读取 .meta.json 获取版本
34
+ const metaPath = path.join(skillPath, ".meta.json");
35
+ if (!fs.existsSync(metaPath)) {
36
+ console.log(` ⚠️ ${name}: 缺少 .meta.json,跳过`);
37
+ continue;
38
+ }
39
+
40
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
41
+ const version = meta.version || "0.0.0";
42
+ const fileName = `${name}-${version}.tar.gz`;
43
+ const outputPath = path.join(distDir, fileName);
44
+
45
+ console.log(` 📦 ${name} (${version}) → ${fileName}`);
46
+
47
+ // 打包:进入 skills/ 目录,只打包当前技能子目录
48
+ await tar.c(
49
+ {
50
+ gzip: true,
51
+ file: outputPath,
52
+ cwd: skillsDir,
53
+ portable: true,
54
+ },
55
+ [name],
56
+ );
57
+
58
+ // 获取文件大小
59
+ const stat = fs.statSync(outputPath);
60
+
61
+ // 更新索引
62
+ if (indexSkills[name]) {
63
+ indexSkills[name].version = version;
64
+ indexSkills[name].fileName = fileName;
65
+ indexSkills[name].size = stat.size;
66
+ } else {
67
+ indexSkills[name] = {
68
+ version,
69
+ description: meta.description || "",
70
+ fileName,
71
+ tags: meta.tags || [],
72
+ size: stat.size,
73
+ };
74
+ }
75
+ }
76
+
77
+ // 更新索引的更新时间
78
+ index.updatedAt = new Date().toISOString().slice(0, 10);
79
+
80
+ // 写回索引
81
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + "\n", "utf-8");
82
+ console.log(`\n ✅ skills-index.json 已更新`);
83
+ console.log(` 📂 压缩包输出目录: dist/skills/\n`);
84
+ }
85
+
86
+ build().catch((err) => {
87
+ console.error("打包失败:", err);
88
+ process.exit(1);
89
+ });
package/scripts/cli.js CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
- const { getPackageSkills, getUserSkills } = require("./core");
5
+ const tar = require("tar");
6
+ const {
7
+ getPackageSkills,
8
+ getUserSkills,
9
+ readSkillsIndex,
10
+ downloadFile,
11
+ } = require("./core");
6
12
 
7
13
  // npx 运行时:process.cwd() 是用户项目目录
8
14
  // __dirname 是包内 scripts/ 目录
@@ -18,12 +24,12 @@ const packageDir = path.join(__dirname, "..");
18
24
  const claudeDest = path.join(projectRoot, ".claude");
19
25
  const skillsDest = path.join(claudeDest, "skills");
20
26
  const settingsPath = path.join(claudeDest, "settings.json");
21
- const pkgSkillsSource = path.join(packageDir, ".claude", "skills");
22
27
 
23
28
  // ---------- 文件写入 ----------
24
29
 
25
30
  function readSettings() {
26
- if (!fs.existsSync(settingsPath)) return { skills: {}, always_apply_skills: [] };
31
+ if (!fs.existsSync(settingsPath))
32
+ return { skills: {}, always_apply_skills: [] };
27
33
  try {
28
34
  return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
29
35
  } catch {
@@ -35,36 +41,117 @@ function writeSettings(settings) {
35
41
  if (!fs.existsSync(claudeDest)) {
36
42
  fs.mkdirSync(claudeDest, { recursive: true });
37
43
  }
38
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
44
+ fs.writeFileSync(
45
+ settingsPath,
46
+ JSON.stringify(settings, null, 2) + "\n",
47
+ "utf-8",
48
+ );
39
49
  }
40
50
 
41
- // ---------- 安装勾选的技能 ----------
51
+ // ---------- 远程下载与安装 ----------
52
+
53
+ async function downloadAndInstallSkill(name, index) {
54
+ const skillInfo = index.skills[name];
55
+ if (!skillInfo) {
56
+ console.error(` ❌ 技能 "${name}" 不在索引中`);
57
+ return false;
58
+ }
59
+
60
+ const destDir = path.join(skillsDest, name);
61
+
62
+ // 检查本地是否已安装且版本一致
63
+ const localMetaPath = path.join(destDir, ".meta.json");
64
+ if (fs.existsSync(localMetaPath)) {
65
+ try {
66
+ const localMeta = JSON.parse(fs.readFileSync(localMetaPath, "utf-8"));
67
+ if (localMeta.version === skillInfo.version) {
68
+ return true; // 已是最新,跳过
69
+ }
70
+ } catch {
71
+ // 元信息损坏,重新下载
72
+ }
73
+ }
74
+
75
+ // 构造下载地址
76
+ const url = `${index.baseUrl}/${skillInfo.fileName}`;
77
+ const tempDir = path.join(claudeDest, ".temp");
78
+ if (!fs.existsSync(tempDir)) {
79
+ fs.mkdirSync(tempDir, { recursive: true });
80
+ }
81
+ const tempFile = path.join(tempDir, skillInfo.fileName);
82
+
83
+ // 下载带进度条
84
+ const { SingleBar, Presets } = await import("cli-progress");
85
+ const bar = new SingleBar(
86
+ {
87
+ format: ` 📥 ${name} |{bar}| {percentage}% | {value}/{total} bytes`,
88
+ barCompleteChar: "\u2588",
89
+ barIncompleteChar: "\u2591",
90
+ hideCursor: true,
91
+ },
92
+ Presets.shades_classic,
93
+ );
42
94
 
43
- function installSelectedSkills(selectedNames) {
44
- // 创建 .claude/ 和 skills/ 目录
95
+ try {
96
+ console.log(` 正在下载 ${name} (${skillInfo.version})...`);
97
+ bar.start(skillInfo.size || 100, 0);
98
+ await downloadFile(url, tempFile, (downloaded, total) => {
99
+ bar.setTotal(total || skillInfo.size || 100);
100
+ bar.update(downloaded);
101
+ });
102
+ bar.stop();
103
+
104
+ // 创建目标目录,解压
105
+ if (fs.existsSync(destDir)) {
106
+ fs.rmSync(destDir, { recursive: true, force: true });
107
+ }
108
+ fs.mkdirSync(destDir, { recursive: true });
109
+
110
+ await tar.x({
111
+ file: tempFile,
112
+ C: destDir,
113
+ });
114
+
115
+ // 清理临时文件
116
+ fs.rmSync(tempFile, { force: true });
117
+
118
+ console.log(` ✅ ${name} (${skillInfo.version}) 安装成功`);
119
+ return true;
120
+ } catch (err) {
121
+ bar.stop();
122
+ // 清理残留
123
+ if (fs.existsSync(tempFile)) fs.rmSync(tempFile, { force: true });
124
+ if (fs.existsSync(destDir))
125
+ fs.rmSync(destDir, { recursive: true, force: true });
126
+ console.error(` ❌ 下载 "${name}" 失败: ${err.message}`);
127
+ return false;
128
+ }
129
+ }
130
+
131
+ async function installSelectedSkills(selectedNames) {
132
+ // 创建目录
45
133
  if (!fs.existsSync(skillsDest)) {
46
134
  fs.mkdirSync(skillsDest, { recursive: true });
47
135
  }
48
136
 
49
- const allPkgSkills = fs.readdirSync(pkgSkillsSource);
137
+ const index = readSkillsIndex(packageDir);
138
+ if (!index) {
139
+ console.error(" ❌ skills-index.json 读取失败");
140
+ return 0;
141
+ }
142
+
143
+ const indexSkills = index.skills;
50
144
  let installedCount = 0;
51
145
 
52
- for (const name of allPkgSkills) {
53
- const src = path.join(pkgSkillsSource, name);
146
+ for (const name of Object.keys(indexSkills)) {
54
147
  const dest = path.join(skillsDest, name);
55
148
 
56
149
  if (selectedNames.includes(name)) {
57
- // 勾选了的 → 安装(不存在才装)
58
- if (!fs.existsSync(dest)) {
59
- try {
60
- fs.cpSync(src, dest, { recursive: true });
61
- installedCount++;
62
- } catch (err) {
63
- console.error(` ❌ 安装技能 "${name}" 失败:`, err.message);
64
- }
65
- }
150
+ // 勾选了的 → 下载/更新
151
+ const ok = await downloadAndInstallSkill(name, index);
152
+ if (ok) installedCount++;
66
153
  } else {
67
- // 没勾选的 → 如果之前装了,删掉
154
+ // 没勾选的 → 移除
68
155
  if (fs.existsSync(dest)) {
69
156
  fs.rmSync(dest, { recursive: true, force: true });
70
157
  }
@@ -75,7 +162,7 @@ function installSelectedSkills(selectedNames) {
75
162
  const settings = readSettings();
76
163
  if (!settings.skills) settings.skills = {};
77
164
 
78
- for (const name of allPkgSkills) {
165
+ for (const name of Object.keys(indexSkills)) {
79
166
  const enabled = selectedNames.includes(name);
80
167
  if (!settings.skills[name]) {
81
168
  settings.skills[name] = {};
@@ -90,7 +177,7 @@ function installSelectedSkills(selectedNames) {
90
177
  const mdDest = path.join(projectRoot, "CLAUDE.md");
91
178
  if (fs.existsSync(mdSource) && !fs.existsSync(mdDest)) {
92
179
  fs.copyFileSync(mdSource, mdDest);
93
- console.log(" 📝 CLAUDE.md 已创建\n");
180
+ console.log(" 📝 CLAUDE.md 已创建");
94
181
  }
95
182
 
96
183
  return installedCount;
@@ -119,13 +206,15 @@ async function cmdList() {
119
206
  const allSkills = [...skillMap.values()];
120
207
 
121
208
  console.log(`\n${chalk.bold("📋 dk-frontend-skills 技能清单")}\n`);
122
- console.log(` ${chalk.dim("状态 技能名 版本 描述")}`);
123
- console.log(` ${chalk.dim("─────────────────────────────────────────────────────")}`);
209
+ console.log(
210
+ ` ${chalk.dim("状态 技能名 版本 描述")}`,
211
+ );
212
+ console.log(
213
+ ` ${chalk.dim("─────────────────────────────────────────────────────")}`,
214
+ );
124
215
 
125
216
  for (const skill of allSkills) {
126
- const status = skill.enabled
127
- ? chalk.green("● 启用")
128
- : chalk.gray("○ 停用");
217
+ const status = skill.enabled ? chalk.green("● 启用") : chalk.gray("○ 停用");
129
218
  const name = chalk.white(skill.name.padEnd(20));
130
219
  const ver = chalk.dim(skill.version.padEnd(7));
131
220
  const desc = skill.description
@@ -134,19 +223,15 @@ async function cmdList() {
134
223
  console.log(` ${status} ${name} ${ver} ${desc}`);
135
224
  }
136
225
 
137
- console.log(`\n ${chalk.cyan("💡")} ${chalk.dim("运行 npx dk-frontend-skills 进入交互选择模式")}\n`);
226
+ console.log(
227
+ `\n ${chalk.cyan("💡")} ${chalk.dim("运行 npx dk-frontend-skills 进入交互选择模式")}\n`,
228
+ );
138
229
  }
139
230
 
140
231
  // ---------- 交互式菜单(选完即装) ----------
141
232
 
142
233
  async function startInteractiveMenu() {
143
234
  const { checkbox } = await import("@inquirer/prompts");
144
-
145
- if (!fs.existsSync(pkgSkillsSource)) {
146
- console.log(" ⚠️ 包内没有找到技能文件\n");
147
- return;
148
- }
149
-
150
235
  const pkgSkills = getPackageSkills(packageDir);
151
236
 
152
237
  if (pkgSkills.length === 0) {
@@ -183,9 +268,11 @@ async function startInteractiveMenu() {
183
268
  });
184
269
 
185
270
  const newCount = selected.filter((name) => !installedSet.has(name)).length;
186
- const removedCount = [...installedSet].filter((name) => !selected.includes(name)).length;
271
+ const removedCount = [...installedSet].filter(
272
+ (name) => !selected.includes(name),
273
+ ).length;
187
274
 
188
- const installed = installSelectedSkills(selected);
275
+ const installed = await installSelectedSkills(selected);
189
276
 
190
277
  const parts = [];
191
278
  if (newCount > 0) parts.push(`新装 ${newCount} 个`);
@@ -223,7 +310,9 @@ async function main() {
223
310
  } else if (command === "list") {
224
311
  await cmdList();
225
312
  } else if (command === "enable" || command === "disable") {
226
- console.log(` ⚠️ 该命令已废弃,请直接运行 npx dk-frontend-skills 使用交互菜单\n`);
313
+ console.log(
314
+ ` ⚠️ 该命令已废弃,请直接运行 npx dk-frontend-skills 使用交互菜单\n`,
315
+ );
227
316
  } else {
228
317
  console.log(` 未知命令: ${command}\n 可用: ${COMMANDS.join(", ")}\n`);
229
318
  process.exit(1);
@@ -3,14 +3,13 @@ const path = require("path");
3
3
  const {
4
4
  backupTimestamp,
5
5
  appendLog,
6
- backupDir,
7
- installSkills,
8
6
  installSettings,
9
7
  } = require("./core");
10
8
 
11
9
  // 获取用户项目根目录
12
10
  // npm lifecycle 脚本会设置 INIT_CWD 为执行命令时的目录,比从 __dirname 爬三级更可靠
13
- const projectRoot = process.env.INIT_CWD || path.resolve(__dirname, "..", "..", "..");
11
+ const projectRoot =
12
+ process.env.INIT_CWD || path.resolve(__dirname, "..", "..", "..");
14
13
  const packageDir = path.join(__dirname, "..");
15
14
 
16
15
  // 源路径
@@ -28,25 +27,21 @@ const logFile = path.join(claudeDest, ".install.log");
28
27
 
29
28
  console.log("\n📦 dk-frontend-skills 技能包安装开始\n");
30
29
 
31
- if (fs.existsSync(claudeSource)) {
32
- if (fs.existsSync(claudeDest)) {
33
- // .claude 已存在 备份后再操作
34
- appendLog(logFile, "INFO", "Install started (upgrade)");
35
- backupDir(claudeDest);
36
-
37
- const skillsSource = path.join(claudeSource, "skills");
38
- const skillsDest = path.join(claudeDest, "skills");
30
+ // 确保 .claude/ 目录存在
31
+ if (!fs.existsSync(claudeDest)) {
32
+ fs.mkdirSync(claudeDest, { recursive: true });
33
+ appendLog(logFile, "INFO", `.claude/ directory created`);
34
+ }
39
35
 
40
- installSkills(skillsSource, skillsDest, logFile);
41
- installSettings(claudeSource, claudeDest, logFile);
42
- } else {
43
- // 全新安装
44
- appendLog(logFile, "INFO", "Install started (fresh)");
45
- fs.cpSync(claudeSource, claudeDest, { recursive: true });
46
- appendLog(logFile, "INFO", `.claude/ directory created`);
47
- }
36
+ // 确保 .claude/skills/ 目录存在(不覆盖已有技能)
37
+ const skillsDest = path.join(claudeDest, "skills");
38
+ if (!fs.existsSync(skillsDest)) {
39
+ fs.mkdirSync(skillsDest, { recursive: true });
48
40
  }
49
41
 
42
+ // 安装 settings.json(深度合并)
43
+ installSettings(claudeSource, claudeDest, logFile);
44
+
50
45
  // 安装 CLAUDE.md
51
46
  if (fs.existsSync(mdSource)) {
52
47
  if (!fs.existsSync(mdDest)) {
@@ -60,7 +55,11 @@ if (fs.existsSync(mdSource)) {
60
55
  const bakName = `CLAUDE.md.${backupTimestamp()}`;
61
56
  const bakPath = path.join(backupsDir, bakName);
62
57
  fs.copyFileSync(mdDest, bakPath);
63
- appendLog(logFile, "BACKUP", `backups/${bakName} (CLAUDE.md user version preserved)`);
58
+ appendLog(
59
+ logFile,
60
+ "BACKUP",
61
+ `backups/${bakName} (CLAUDE.md user version preserved)`,
62
+ );
64
63
  }
65
64
  }
66
65
 
@@ -77,4 +76,4 @@ if (fs.existsSync(logFile)) {
77
76
  console.log("");
78
77
  }
79
78
 
80
- console.log("💡 提示:运行 npx dk-skills 可交互选择启用/禁用技能\n");
79
+ console.log("💡 提示:运行 npx dk-skills 可交互选择安装/启用/禁用技能\n");
package/scripts/core.js CHANGED
@@ -46,7 +46,7 @@ function backupDir(dir) {
46
46
 
47
47
  for (const item of fs.readdirSync(dir)) {
48
48
  if (item === "backups") continue;
49
- fs.cpSync(path.join(dir, item), path.join(bakPath, item), { recursive: true });
49
+ copyDirSync(path.join(dir, item), path.join(bakPath, item));
50
50
  }
51
51
 
52
52
  // 清理旧备份,只保留最近 3 份
@@ -70,6 +70,22 @@ function isPlainObject(val) {
70
70
  return Object.prototype.toString.call(val) === "[object Object]";
71
71
  }
72
72
 
73
+ /**
74
+ * 手动递归拷贝目录,绕开 Windows 中文路径下 fs.cpSync({ recursive: true }) 死锁的 bug
75
+ */
76
+ function copyDirSync(src, dest) {
77
+ fs.mkdirSync(dest, { recursive: true });
78
+ for (const item of fs.readdirSync(src)) {
79
+ const srcPath = path.join(src, item);
80
+ const destPath = path.join(dest, item);
81
+ if (fs.statSync(srcPath).isDirectory()) {
82
+ copyDirSync(srcPath, destPath);
83
+ } else {
84
+ fs.copyFileSync(srcPath, destPath);
85
+ }
86
+ }
87
+ }
88
+
73
89
  /**
74
90
  * 递归深合并,用户值优先,目标新增字段补充
75
91
  */
@@ -102,51 +118,6 @@ function readMeta(skillDir) {
102
118
  return null;
103
119
  }
104
120
 
105
- /**
106
- * 安装技能:逐个复制,补缺不覆盖
107
- */
108
- function installSkills(skillsSource, skillsDest, logFile) {
109
- if (!fs.existsSync(skillsSource)) return [];
110
-
111
- if (!fs.existsSync(skillsDest)) {
112
- fs.mkdirSync(skillsDest, { recursive: true });
113
- }
114
-
115
- const skillDirs = fs.readdirSync(skillsSource);
116
- const results = [];
117
-
118
- for (const dir of skillDirs) {
119
- const srcSkill = path.join(skillsSource, dir);
120
- const destSkill = path.join(skillsDest, dir);
121
- const meta = readMeta(srcSkill);
122
- const version = meta ? meta.version : "?";
123
-
124
- if (!fs.existsSync(destSkill)) {
125
- fs.cpSync(srcSkill, destSkill, { recursive: true });
126
- appendLog(logFile, "SKILL", `${dir} (${version}) → installed`);
127
- results.push({ name: dir, version, action: "installed" });
128
- } else {
129
- const destMeta = readMeta(destSkill);
130
- const needUpdate = meta && destMeta && meta.version !== destMeta.version;
131
-
132
- if (needUpdate) {
133
- const bakName = `${dir}.bak.${backupTimestamp()}`;
134
- const bakPath = path.join(skillsDest, bakName);
135
- fs.cpSync(destSkill, bakPath, { recursive: true });
136
- fs.rmSync(destSkill, { recursive: true, force: true });
137
- fs.cpSync(srcSkill, destSkill, { recursive: true });
138
- appendLog(logFile, "SKILL", `${dir} (${destMeta.version} → ${meta.version}) → updated`);
139
- results.push({ name: dir, version: meta.version, action: "updated" });
140
- } else {
141
- appendLog(logFile, "SKILL", `${dir} (${version}) → skipped (exists)`);
142
- results.push({ name: dir, version, action: "skipped" });
143
- }
144
- }
145
- }
146
-
147
- return results;
148
- }
149
-
150
121
  /**
151
122
  * 安装 settings.json,深度合并
152
123
  */
@@ -176,21 +147,31 @@ function installSettings(sourceDir, destDir, logFile) {
176
147
  }
177
148
 
178
149
  /**
179
- * 获取包内的技能列表(带元信息)
150
+ * 读取 skills-index.json 获取包内技能列表
180
151
  */
181
- function getPackageSkills(packageDir) {
182
- const skillsSource = path.join(packageDir, ".claude", "skills");
183
- if (!fs.existsSync(skillsSource)) return [];
152
+ function readSkillsIndex(packageDir) {
153
+ const indexPath = path.join(packageDir, "skills-index.json");
154
+ if (!fs.existsSync(indexPath)) return null;
155
+ try {
156
+ return JSON.parse(fs.readFileSync(indexPath, "utf-8"));
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
184
161
 
185
- return fs.readdirSync(skillsSource).map((name) => {
186
- const meta = readMeta(path.join(skillsSource, name));
187
- return {
188
- name,
189
- version: meta ? meta.version : "?",
190
- description: meta ? meta.description : "",
191
- tags: meta ? meta.tags : [],
192
- };
193
- });
162
+ /**
163
+ * 获取包的技能列表(从 skills-index.json 读取)
164
+ */
165
+ function getPackageSkills(packageDir) {
166
+ const index = readSkillsIndex(packageDir);
167
+ if (!index) return [];
168
+
169
+ return Object.entries(index.skills).map(([name, info]) => ({
170
+ name,
171
+ version: info.version,
172
+ description: info.description || "",
173
+ tags: info.tags || [],
174
+ }));
194
175
  }
195
176
 
196
177
  /**
@@ -225,15 +206,45 @@ function getUserSkills(claudeDest) {
225
206
  });
226
207
  }
227
208
 
209
+ /**
210
+ * 从远程下载文件到本地临时路径
211
+ */
212
+ async function downloadFile(url, destPath, onProgress) {
213
+ const response = await fetch(url);
214
+ if (!response.ok) {
215
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
216
+ }
217
+
218
+ const total = parseInt(response.headers.get("content-length") || "0", 10);
219
+ let downloaded = 0;
220
+
221
+ const reader = response.body.getReader();
222
+ const writer = fs.createWriteStream(destPath);
223
+
224
+ try {
225
+ while (true) {
226
+ const { done, value } = await reader.read();
227
+ if (done) break;
228
+ downloaded += value.length;
229
+ if (onProgress) onProgress(downloaded, total);
230
+ writer.write(value);
231
+ }
232
+ } finally {
233
+ writer.close();
234
+ }
235
+ }
236
+
228
237
  module.exports = {
229
238
  timestamp,
230
239
  backupTimestamp,
231
240
  appendLog,
232
241
  backupDir,
242
+ copyDirSync,
233
243
  deepMerge,
234
244
  readMeta,
235
- installSkills,
236
245
  installSettings,
246
+ readSkillsIndex,
237
247
  getPackageSkills,
238
248
  getUserSkills,
249
+ downloadFile,
239
250
  };