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 +1 -1
- package/scripts/cli.js +44 -62
- package/scripts/copy-skills.js +3 -24
- package/scripts/core.js +30 -0
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
279
|
-
if (removedCount > 0) parts.push(
|
|
280
|
-
const summary = parts.length > 0 ?
|
|
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(
|
|
264
|
+
console.log(` ✅ 完成${summary}\n`);
|
|
283
265
|
}
|
|
284
266
|
|
|
285
267
|
// ---------- 主入口 ----------
|
package/scripts/copy-skills.js
CHANGED
|
@@ -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
|
-
|
|
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("💡
|
|
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 = {
|