dk-frontend-skills 3.0.0 → 3.0.2

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": "dk-frontend-skills",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "dk-engineer - 幽默沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
package/scripts/cli.js CHANGED
@@ -52,80 +52,62 @@ function writeSettings(settings) {
52
52
 
53
53
  async function downloadAndInstallSkill(name, index) {
54
54
  const skillInfo = index.skills[name];
55
- if (!skillInfo) {
56
- console.error(` ❌ 技能 "${name}" 不在索引中`);
57
- return false;
58
- }
55
+ if (!skillInfo) return false;
59
56
 
60
57
  const destDir = path.join(skillsDest, name);
61
58
 
62
- // 检查本地是否已安装且版本一致
59
+ // 检查本地版本
63
60
  const localMetaPath = path.join(destDir, ".meta.json");
64
61
  if (fs.existsSync(localMetaPath)) {
65
62
  try {
66
63
  const localMeta = JSON.parse(fs.readFileSync(localMetaPath, "utf-8"));
67
- if (localMeta.version === skillInfo.version) {
68
- return true; // 已是最新,跳过
69
- }
70
- } catch {
71
- // 元信息损坏,重新下载
72
- }
64
+ if (localMeta.version === skillInfo.version) return true;
65
+ } catch {}
73
66
  }
74
67
 
75
- // 构造下载地址
76
68
  const url = `${index.baseUrl}/${skillInfo.fileName}`;
77
69
  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);
70
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
82
71
 
83
- // 下载带进度条
84
72
  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
- );
73
+ const bar = new SingleBar({
74
+ format: ` ${name} |{bar}| {percentage}%`,
75
+ barCompleteChar: "\u2588",
76
+ barIncompleteChar: "\u2591",
77
+ hideCursor: true,
78
+ }, Presets.shades_classic);
94
79
 
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 });
80
+ const MAX_RETRIES = 3;
81
+
82
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
83
+ const tempFile = path.join(tempDir, skillInfo.fileName);
84
+ try {
85
+ if (attempt > 1) console.log(` ↻ ${name} 重试第 ${attempt} 次...`);
86
+ bar.start(100, 0);
87
+ await downloadFile(url, tempFile, (d, t) => {
88
+ bar.setTotal(t || skillInfo.size || 100);
89
+ bar.update(d);
90
+ });
91
+ bar.stop();
92
+
93
+ if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
94
+ fs.mkdirSync(destDir, { recursive: true });
95
+
96
+ await tar.x({ file: tempFile, C: destDir });
97
+
98
+ fs.rmSync(tempFile, { force: true });
99
+ return true;
100
+ } catch (err) {
101
+ bar.stop();
102
+ if (fs.existsSync(tempFile)) fs.rmSync(tempFile, { force: true });
103
+ if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
104
+ if (attempt === MAX_RETRIES) {
105
+ console.error(` ✗ ${name}: ${err.message}`);
106
+ return false;
107
+ }
107
108
  }
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
109
  }
110
+ return false;
129
111
  }
130
112
 
131
113
  async function installSelectedSkills(selectedNames) {
@@ -275,11 +257,11 @@ async function startInteractiveMenu() {
275
257
  const installed = await installSelectedSkills(selected);
276
258
 
277
259
  const parts = [];
278
- if (newCount > 0) parts.push(`新装 ${newCount} 个`);
279
- if (removedCount > 0) parts.push(`移除 ${removedCount} 个`);
280
- const summary = parts.length > 0 ? `(${parts.join("")})` : "";
260
+ if (newCount > 0) parts.push(`+${newCount}`);
261
+ if (removedCount > 0) parts.push(`-${removedCount}`);
262
+ const summary = parts.length > 0 ? ` ${parts.join(" ")}` : "";
281
263
 
282
- console.log(`\n技能安装完成 ${summary}\n`);
264
+ console.log(`完成${summary}\n`);
283
265
  }
284
266
 
285
267
  // ---------- 主入口 ----------
@@ -7,7 +7,6 @@ const {
7
7
  } = require("./core");
8
8
 
9
9
  // 获取用户项目根目录
10
- // npm lifecycle 脚本会设置 INIT_CWD 为执行命令时的目录,比从 __dirname 爬三级更可靠
11
10
  const projectRoot =
12
11
  process.env.INIT_CWD || path.resolve(__dirname, "..", "..", "..");
13
12
  const packageDir = path.join(__dirname, "..");
@@ -25,15 +24,12 @@ const logFile = path.join(claudeDest, ".install.log");
25
24
 
26
25
  // ===================== 主流程 =====================
27
26
 
28
- console.log("\n📦 dk-frontend-skills 技能包安装开始\n");
29
-
30
- // 确保 .claude/ 目录存在
27
+ // 确保 .claude/ skills/ 目录存在
31
28
  if (!fs.existsSync(claudeDest)) {
32
29
  fs.mkdirSync(claudeDest, { recursive: true });
33
30
  appendLog(logFile, "INFO", `.claude/ directory created`);
34
31
  }
35
32
 
36
- // 确保 .claude/skills/ 目录存在(不覆盖已有技能)
37
33
  const skillsDest = path.join(claudeDest, "skills");
38
34
  if (!fs.existsSync(skillsDest)) {
39
35
  fs.mkdirSync(skillsDest, { recursive: true });
@@ -55,25 +51,8 @@ if (fs.existsSync(mdSource)) {
55
51
  const bakName = `CLAUDE.md.${backupTimestamp()}`;
56
52
  const bakPath = path.join(backupsDir, bakName);
57
53
  fs.copyFileSync(mdDest, bakPath);
58
- appendLog(
59
- logFile,
60
- "BACKUP",
61
- `backups/${bakName} (CLAUDE.md user version preserved)`,
62
- );
63
- }
64
- }
65
-
66
- // 输出摘要
67
- console.log("✅ dk-frontend-skills 安装完成!\n");
68
-
69
- if (fs.existsSync(logFile)) {
70
- const logs = fs.readFileSync(logFile, "utf-8").trim().split("\n");
71
- const recent = logs.slice(-10);
72
- console.log("📋 操作日志:");
73
- for (const line of recent) {
74
- console.log(` ${line}`);
54
+ appendLog(logFile, "BACKUP", `backups/${bakName} (CLAUDE.md user version preserved)`);
75
55
  }
76
- console.log("");
77
56
  }
78
57
 
79
- console.log("💡 提示:运行 npx dk-skills 可交互选择安装/启用/禁用技能\n");
58
+ console.log("💡 npx dk-skills 可交互选择安装技能\n");
package/scripts/core.js CHANGED
@@ -206,6 +206,21 @@ function getUserSkills(claudeDest) {
206
206
  });
207
207
  }
208
208
 
209
+ /**
210
+ * 校验文件是否为有效的 gzip 格式(magic number: 0x1F 0x8B)
211
+ */
212
+ function isValidGzip(filePath) {
213
+ try {
214
+ const fd = fs.openSync(filePath, "r");
215
+ const buf = Buffer.alloc(2);
216
+ fs.readSync(fd, buf, 0, 2, 0);
217
+ fs.closeSync(fd);
218
+ return buf[0] === 0x1f && buf[1] === 0x8b;
219
+ } catch {
220
+ return false;
221
+ }
222
+ }
223
+
209
224
  /**
210
225
  * 从远程下载文件到本地临时路径
211
226
  */
@@ -215,6 +230,15 @@ async function downloadFile(url, destPath, onProgress) {
215
230
  throw new Error(`HTTP ${response.status} ${response.statusText}`);
216
231
  }
217
232
 
233
+ // 检查 Content-Type,如果不是 gzip 大概率是 404 页面
234
+ const contentType = response.headers.get("content-type") || "";
235
+ if (!contentType.includes("gzip") && !contentType.includes("octet-stream") && !contentType.includes("binary")) {
236
+ const text = await response.clone().text();
237
+ if (text.includes("<!doctype") || text.includes("<html")) {
238
+ throw new Error("下载链接无效,请检查 GitHub Release 是否存在");
239
+ }
240
+ }
241
+
218
242
  const total = parseInt(response.headers.get("content-length") || "0", 10);
219
243
  let downloaded = 0;
220
244
 
@@ -232,6 +256,12 @@ async function downloadFile(url, destPath, onProgress) {
232
256
  } finally {
233
257
  writer.close();
234
258
  }
259
+
260
+ // 下载完后校验 gzip 格式
261
+ if (fs.existsSync(destPath) && !isValidGzip(destPath)) {
262
+ fs.rmSync(destPath, { force: true });
263
+ throw new Error("下载的文件不是有效的压缩包,可能是 GitHub Release 尚未创建或链接错误");
264
+ }
235
265
  }
236
266
 
237
267
  module.exports = {