dk-frontend-skills 1.1.4 → 2.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.
- package/package.json +4 -1
- package/scripts/cli.js +242 -0
- package/scripts/copy-skills.js +15 -221
- package/scripts/core.js +239 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dk-frontend-skills",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "dk-engineer - 幽默沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
|
|
5
5
|
"author": "XiaoMa",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"CLAUDE.md",
|
|
11
11
|
"scripts/"
|
|
12
12
|
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"dk-skills": "./scripts/cli.js"
|
|
15
|
+
},
|
|
13
16
|
"scripts": {
|
|
14
17
|
"postinstall": "node scripts/copy-skills.js"
|
|
15
18
|
}
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
const { getPackageSkills, getUserSkills } = require("./core");
|
|
7
|
+
|
|
8
|
+
// 定位项目根目录和包目录
|
|
9
|
+
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
10
|
+
const packageDir = path.join(__dirname, "..");
|
|
11
|
+
const claudeDest = path.join(projectRoot, ".claude");
|
|
12
|
+
const settingsPath = path.join(claudeDest, "settings.json");
|
|
13
|
+
|
|
14
|
+
const command = process.argv[2];
|
|
15
|
+
const args = process.argv.slice(3);
|
|
16
|
+
|
|
17
|
+
// ---------- 辅助函数 ----------
|
|
18
|
+
|
|
19
|
+
function readSettings() {
|
|
20
|
+
if (!fs.existsSync(settingsPath)) return { skills: {}, always_apply_skills: [] };
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return { skills: {}, always_apply_skills: [] };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeSettings(settings) {
|
|
29
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 保存技能选择结果到 settings.json
|
|
34
|
+
*/
|
|
35
|
+
function saveSkillSelection(choices) {
|
|
36
|
+
const settings = readSettings();
|
|
37
|
+
if (!settings.skills) settings.skills = {};
|
|
38
|
+
|
|
39
|
+
for (const { name, enabled } of choices) {
|
|
40
|
+
if (!settings.skills[name]) {
|
|
41
|
+
settings.skills[name] = {};
|
|
42
|
+
}
|
|
43
|
+
settings.skills[name].enabled = enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
writeSettings(settings);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------- 命令:list ----------
|
|
50
|
+
|
|
51
|
+
function cmdList() {
|
|
52
|
+
const pkgSkills = getPackageSkills(packageDir);
|
|
53
|
+
const userSkills = getUserSkills(claudeDest);
|
|
54
|
+
|
|
55
|
+
// 合并包内和用户已安装的技能状态
|
|
56
|
+
const skillMap = new Map();
|
|
57
|
+
for (const s of userSkills) skillMap.set(s.name, s);
|
|
58
|
+
for (const s of pkgSkills) {
|
|
59
|
+
if (!skillMap.has(s.name)) {
|
|
60
|
+
skillMap.set(s.name, { ...s, enabled: false, installed: false });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const allSkills = [...skillMap.values()];
|
|
65
|
+
|
|
66
|
+
console.log("\n📋 dk-frontend-skills 技能清单\n");
|
|
67
|
+
console.log(" 状态 技能名 版本 描述");
|
|
68
|
+
console.log(" ─────────────────────────────────────────────────────");
|
|
69
|
+
|
|
70
|
+
for (const skill of allSkills) {
|
|
71
|
+
const status = skill.enabled ? "✅ 启用" : "⏸️ 停用";
|
|
72
|
+
const name = skill.name.padEnd(20);
|
|
73
|
+
const ver = skill.version.padEnd(7);
|
|
74
|
+
const desc = skill.description || "(无描述)";
|
|
75
|
+
console.log(` ${status} ${name} ${ver} ${desc}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`\n 💡 运行 npx dk-skills 进入交互选择模式\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- 命令:enable / disable ----------
|
|
82
|
+
|
|
83
|
+
function cmdToggle(enable, names) {
|
|
84
|
+
if (names.length === 0) {
|
|
85
|
+
console.log(` 用法: npx dk-skills ${enable ? "enable" : "disable"} <技能名...>`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pkgSkills = getPackageSkills(packageDir);
|
|
90
|
+
const pkgNames = new Set(pkgSkills.map((s) => s.name));
|
|
91
|
+
const userSkills = getUserSkills(claudeDest);
|
|
92
|
+
const userNames = new Set(userSkills.map((s) => s.name));
|
|
93
|
+
|
|
94
|
+
const notFound = names.filter((n) => !pkgNames.has(n));
|
|
95
|
+
if (notFound.length > 0) {
|
|
96
|
+
console.log(` ⚠️ 未知技能: ${notFound.join(", ")}`);
|
|
97
|
+
console.log(` 可用: ${[...pkgNames].join(", ")}\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const notInstalled = names.filter((n) => !userNames.has(n));
|
|
102
|
+
if (notInstalled.length > 0) {
|
|
103
|
+
console.log(` ⚠️ 以下技能尚未安装: ${notInstalled.join(", ")}`);
|
|
104
|
+
console.log(` 请先执行 npm i dk-frontend-skills 安装完整技能包\n`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const choices = userSkills.map((s) => ({
|
|
109
|
+
name: s.name,
|
|
110
|
+
enabled: names.includes(s.name) ? enable : s.enabled,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
saveSkillSelection(choices);
|
|
114
|
+
|
|
115
|
+
const action = enable ? "启用" : "停用";
|
|
116
|
+
console.log(` ✅ 已${action}: ${names.join(", ")}\n`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------- 交互式 TUI 菜单 ----------
|
|
120
|
+
|
|
121
|
+
function startInteractiveMenu() {
|
|
122
|
+
const pkgSkills = getPackageSkills(packageDir);
|
|
123
|
+
|
|
124
|
+
if (pkgSkills.length === 0) {
|
|
125
|
+
console.log(" ⚠️ 包内没有可用技能\n");
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 读取当前 settings 中的启用状态
|
|
130
|
+
const userSkills = getUserSkills(claudeDest);
|
|
131
|
+
const userMap = new Map(userSkills.map((s) => [s.name, s.enabled]));
|
|
132
|
+
|
|
133
|
+
// choices 只包含包内有的技能
|
|
134
|
+
const skills = pkgSkills.map((s) => ({
|
|
135
|
+
...s,
|
|
136
|
+
enabled: userMap.has(s.name) ? userMap.get(s.name) : false,
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
let cursor = 0;
|
|
140
|
+
const toggled = skills.map((s) => s.enabled);
|
|
141
|
+
const stdin = process.stdin;
|
|
142
|
+
const stdout = process.stdout;
|
|
143
|
+
|
|
144
|
+
function render() {
|
|
145
|
+
stdout.write("\x1Bc"); // 清屏
|
|
146
|
+
stdout.write("╔══════════════════════════════════════════════╗\n");
|
|
147
|
+
stdout.write("║ dk-frontend-skills 技能选择 ║\n");
|
|
148
|
+
stdout.write("╠══════════════════════════════════════════════╣\n");
|
|
149
|
+
stdout.write("║ ↑↓ 导航 ␣ 开关 Enter 确认 q 退出 ║\n");
|
|
150
|
+
stdout.write("╚══════════════════════════════════════════════╝\n\n");
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < skills.length; i++) {
|
|
153
|
+
const s = skills[i];
|
|
154
|
+
const arrow = i === cursor ? "❯" : " ";
|
|
155
|
+
const check = toggled[i] ? "✓" : " ";
|
|
156
|
+
const name = s.name.padEnd(20);
|
|
157
|
+
const desc = s.description || "";
|
|
158
|
+
stdout.write(` ${arrow} [${check}] ${name} ${desc}\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
stdout.write("\n 选中: ");
|
|
162
|
+
const selectedNames = skills.filter((_, i) => toggled[i]).map((s) => s.name);
|
|
163
|
+
if (selectedNames.length === 0) {
|
|
164
|
+
stdout.write("(无)");
|
|
165
|
+
} else if (selectedNames.length <= 3) {
|
|
166
|
+
stdout.write(selectedNames.join(", "));
|
|
167
|
+
} else {
|
|
168
|
+
stdout.write(`${selectedNames.length} 个技能`);
|
|
169
|
+
}
|
|
170
|
+
stdout.write("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function exitMenu(saved) {
|
|
174
|
+
stdin.setRawMode(false);
|
|
175
|
+
stdin.pause();
|
|
176
|
+
if (saved) {
|
|
177
|
+
console.log("\n ✅ 技能选择已保存\n");
|
|
178
|
+
}
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render();
|
|
183
|
+
|
|
184
|
+
readline.emitKeypressEvents(stdin);
|
|
185
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
186
|
+
stdin.resume();
|
|
187
|
+
|
|
188
|
+
stdin.on("keypress", (str, key) => {
|
|
189
|
+
if (!key) return;
|
|
190
|
+
|
|
191
|
+
if (key.name === "up") {
|
|
192
|
+
cursor = Math.max(0, cursor - 1);
|
|
193
|
+
render();
|
|
194
|
+
} else if (key.name === "down") {
|
|
195
|
+
cursor = Math.min(skills.length - 1, cursor + 1);
|
|
196
|
+
render();
|
|
197
|
+
} else if (key.name === "space") {
|
|
198
|
+
toggled[cursor] = !toggled[cursor];
|
|
199
|
+
render();
|
|
200
|
+
} else if (key.name === "return") {
|
|
201
|
+
const choices = skills.map((s, i) => ({ name: s.name, enabled: toggled[i] }));
|
|
202
|
+
saveSkillSelection(choices);
|
|
203
|
+
exitMenu(true);
|
|
204
|
+
} else if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
205
|
+
exitMenu(false);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------- 主入口 ----------
|
|
211
|
+
|
|
212
|
+
const COMMANDS = ["list", "enable", "disable"];
|
|
213
|
+
|
|
214
|
+
if (!command) {
|
|
215
|
+
// 无参数 → 启动交互菜单
|
|
216
|
+
startInteractiveMenu();
|
|
217
|
+
} else if (command === "--help" || command === "-h") {
|
|
218
|
+
console.log(`
|
|
219
|
+
dk-frontend-skills CLI
|
|
220
|
+
|
|
221
|
+
用法:
|
|
222
|
+
npx dk-skills 交互式技能选择菜单
|
|
223
|
+
npx dk-skills list 查看技能清单
|
|
224
|
+
npx dk-skills enable <技能名...> 启用指定技能
|
|
225
|
+
npx dk-skills disable <技能名...> 停用指定技能
|
|
226
|
+
npx dk-skills --help 显示帮助
|
|
227
|
+
|
|
228
|
+
示例:
|
|
229
|
+
npx dk-skills list
|
|
230
|
+
npx dk-skills enable vue fe-biz-patterns
|
|
231
|
+
npx dk-skills disable moai-framework-electron
|
|
232
|
+
`);
|
|
233
|
+
} else if (command === "list") {
|
|
234
|
+
cmdList();
|
|
235
|
+
} else if (command === "enable") {
|
|
236
|
+
cmdToggle(true, args);
|
|
237
|
+
} else if (command === "disable") {
|
|
238
|
+
cmdToggle(false, args);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(` 未知命令: ${command}\n 可用: ${COMMANDS.join(", ")}\n`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
package/scripts/copy-skills.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
backupTimestamp,
|
|
5
|
+
appendLog,
|
|
6
|
+
backupDir,
|
|
7
|
+
installSkills,
|
|
8
|
+
installSettings,
|
|
9
|
+
} = require("./core");
|
|
3
10
|
|
|
4
11
|
// 获取用户项目根目录
|
|
5
12
|
const projectRoot = path.resolve(__dirname, "..", "..", "..");
|
|
@@ -16,214 +23,6 @@ const mdDest = path.join(projectRoot, "CLAUDE.md");
|
|
|
16
23
|
// 日志文件
|
|
17
24
|
const logFile = path.join(claudeDest, ".install.log");
|
|
18
25
|
|
|
19
|
-
// 时间戳格式:YYYY-MM-DD HH:mm:ss
|
|
20
|
-
function timestamp() {
|
|
21
|
-
const d = new Date();
|
|
22
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
23
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 紧凑时间戳用于备份目录名:YYYYMMDD.HHmmss
|
|
27
|
-
function backupTimestamp() {
|
|
28
|
-
const d = new Date();
|
|
29
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
30
|
-
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}.${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 追加日志到 .claude/.install.log
|
|
35
|
-
* 如果 .claude 目录还不存在,先创建
|
|
36
|
-
*/
|
|
37
|
-
function appendLog(level, message) {
|
|
38
|
-
const dir = path.dirname(logFile);
|
|
39
|
-
if (!fs.existsSync(dir)) {
|
|
40
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
const line = `[${timestamp()}] [${level}] ${message}\n`;
|
|
43
|
-
fs.appendFileSync(logFile, line, "utf-8");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 备份已有目录到 .claude/backups/<timestamp>/
|
|
48
|
-
* 注意:排除 backups/ 自身,避免复制到子目录
|
|
49
|
-
* 清理超过 3 份的旧备份
|
|
50
|
-
*/
|
|
51
|
-
function backupDir(dir) {
|
|
52
|
-
if (!fs.existsSync(dir)) return null;
|
|
53
|
-
|
|
54
|
-
const backupsDir = path.join(dir, "backups");
|
|
55
|
-
if (!fs.existsSync(backupsDir)) {
|
|
56
|
-
fs.mkdirSync(backupsDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ts = backupTimestamp();
|
|
60
|
-
const bakPath = path.join(backupsDir, ts);
|
|
61
|
-
fs.mkdirSync(bakPath, { recursive: true });
|
|
62
|
-
|
|
63
|
-
// 逐项复制,排除 backups/ 避免自引用
|
|
64
|
-
for (const item of fs.readdirSync(dir)) {
|
|
65
|
-
if (item === "backups") continue;
|
|
66
|
-
fs.cpSync(path.join(dir, item), path.join(bakPath, item), { recursive: true });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
appendLog("BACKUP", `backups/${ts}/ created`);
|
|
70
|
-
console.log(` 💾 备份 → .claude/backups/${ts}/`);
|
|
71
|
-
|
|
72
|
-
// 清理旧备份,只保留最近 3 份
|
|
73
|
-
const backups = fs
|
|
74
|
-
.readdirSync(backupsDir)
|
|
75
|
-
.filter((name) => /^\d{8}\.\d{6}$/.test(name))
|
|
76
|
-
.sort()
|
|
77
|
-
.reverse();
|
|
78
|
-
|
|
79
|
-
if (backups.length > 3) {
|
|
80
|
-
for (const old of backups.slice(3)) {
|
|
81
|
-
const oldPath = path.join(backupsDir, old);
|
|
82
|
-
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
83
|
-
appendLog("CLEAN", `清除旧备份 backups/${old}/`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return bakPath;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 递归深合并
|
|
92
|
-
* 用户值优先(原样保留),目标新增字段补充
|
|
93
|
-
* 只有纯对象才递归合并,数组和原始值直接取用户的值
|
|
94
|
-
*/
|
|
95
|
-
function deepMerge(userObj, pkgObj) {
|
|
96
|
-
const result = { ...userObj };
|
|
97
|
-
|
|
98
|
-
for (const key of Object.keys(pkgObj)) {
|
|
99
|
-
if (!(key in result)) {
|
|
100
|
-
// 用户没有这个 key,直接用包的
|
|
101
|
-
result[key] = pkgObj[key];
|
|
102
|
-
} else if (
|
|
103
|
-
isPlainObject(pkgObj[key]) &&
|
|
104
|
-
isPlainObject(result[key])
|
|
105
|
-
) {
|
|
106
|
-
// 双方都是纯对象,递归合并
|
|
107
|
-
result[key] = deepMerge(result[key], pkgObj[key]);
|
|
108
|
-
}
|
|
109
|
-
// 其他情况:用户值优先,不动
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return result;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isPlainObject(val) {
|
|
116
|
-
return Object.prototype.toString.call(val) === "[object Object]";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 读取技能目录下的 .meta.json
|
|
121
|
-
* 没有则返回 null
|
|
122
|
-
*/
|
|
123
|
-
function readMeta(skillDir) {
|
|
124
|
-
const metaPath = path.join(skillDir, ".meta.json");
|
|
125
|
-
if (fs.existsSync(metaPath)) {
|
|
126
|
-
try {
|
|
127
|
-
return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 安装技能
|
|
137
|
-
* 逐个复制技能目录,补缺不覆盖,记录日志
|
|
138
|
-
*/
|
|
139
|
-
function installSkills(skillsSource, skillsDest) {
|
|
140
|
-
if (!fs.existsSync(skillsSource)) return [];
|
|
141
|
-
|
|
142
|
-
if (!fs.existsSync(skillsDest)) {
|
|
143
|
-
fs.mkdirSync(skillsDest, { recursive: true });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const skillDirs = fs.readdirSync(skillsSource);
|
|
147
|
-
const results = [];
|
|
148
|
-
|
|
149
|
-
for (const dir of skillDirs) {
|
|
150
|
-
const srcSkill = path.join(skillsSource, dir);
|
|
151
|
-
const destSkill = path.join(skillsDest, dir);
|
|
152
|
-
const meta = readMeta(srcSkill);
|
|
153
|
-
const version = meta ? meta.version : "?";
|
|
154
|
-
|
|
155
|
-
if (!fs.existsSync(destSkill)) {
|
|
156
|
-
// 新安装
|
|
157
|
-
fs.cpSync(srcSkill, destSkill, { recursive: true });
|
|
158
|
-
appendLog("SKILL", `${dir} (${version}) → installed`);
|
|
159
|
-
results.push({ name: dir, version, action: "installed" });
|
|
160
|
-
} else {
|
|
161
|
-
// 补版本:检测元信息,有更新才覆盖
|
|
162
|
-
const destMeta = readMeta(destSkill);
|
|
163
|
-
const needUpdate = meta && destMeta && meta.version !== destMeta.version;
|
|
164
|
-
|
|
165
|
-
if (needUpdate) {
|
|
166
|
-
// 先备份旧技能
|
|
167
|
-
const bakName = `${dir}.bak.${backupTimestamp()}`;
|
|
168
|
-
const bakPath = path.join(skillsDest, bakName);
|
|
169
|
-
fs.cpSync(destSkill, bakPath, { recursive: true });
|
|
170
|
-
|
|
171
|
-
// 删旧的,复制新的
|
|
172
|
-
fs.rmSync(destSkill, { recursive: true, force: true });
|
|
173
|
-
fs.cpSync(srcSkill, destSkill, { recursive: true });
|
|
174
|
-
|
|
175
|
-
appendLog("SKILL", `${dir} (${destMeta.version} → ${meta.version}) → updated`);
|
|
176
|
-
results.push({ name: dir, version: meta.version, action: "updated" });
|
|
177
|
-
} else {
|
|
178
|
-
appendLog("SKILL", `${dir} (${version}) → skipped (exists)`);
|
|
179
|
-
results.push({ name: dir, version, action: "skipped" });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return results;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* 安装 settings.json
|
|
189
|
-
* 用深度合并策略,用户自定义优先
|
|
190
|
-
*/
|
|
191
|
-
function installSettings(sourceDir, destDir) {
|
|
192
|
-
const settingsSource = path.join(sourceDir, "settings.json");
|
|
193
|
-
const settingsDest = path.join(destDir, "settings.json");
|
|
194
|
-
const localSource = path.join(sourceDir, "settings.local.json");
|
|
195
|
-
const localDest = path.join(destDir, "settings.local.json");
|
|
196
|
-
|
|
197
|
-
if (fs.existsSync(settingsSource)) {
|
|
198
|
-
if (fs.existsSync(settingsDest)) {
|
|
199
|
-
// 深度合并
|
|
200
|
-
const userSettings = JSON.parse(fs.readFileSync(settingsDest, "utf-8"));
|
|
201
|
-
const pkgSettings = JSON.parse(fs.readFileSync(settingsSource, "utf-8"));
|
|
202
|
-
const merged = deepMerge(userSettings, pkgSettings);
|
|
203
|
-
fs.writeFileSync(settingsDest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
204
|
-
appendLog("SETTINGS", "settings.json merged");
|
|
205
|
-
} else {
|
|
206
|
-
// 用户没有,直接复制
|
|
207
|
-
fs.copyFileSync(settingsSource, settingsDest);
|
|
208
|
-
appendLog("SETTINGS", "settings.json created");
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// settings.local.json 同样处理
|
|
213
|
-
if (fs.existsSync(localSource)) {
|
|
214
|
-
if (fs.existsSync(localDest)) {
|
|
215
|
-
const userLocal = JSON.parse(fs.readFileSync(localDest, "utf-8"));
|
|
216
|
-
const pkgLocal = JSON.parse(fs.readFileSync(localSource, "utf-8"));
|
|
217
|
-
const merged = deepMerge(userLocal, pkgLocal);
|
|
218
|
-
fs.writeFileSync(localDest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
219
|
-
appendLog("SETTINGS", "settings.local.json merged");
|
|
220
|
-
} else {
|
|
221
|
-
fs.copyFileSync(localSource, localDest);
|
|
222
|
-
appendLog("SETTINGS", "settings.local.json created");
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
26
|
// ===================== 主流程 =====================
|
|
228
27
|
|
|
229
28
|
console.log("\n📦 dk-frontend-skills 技能包安装开始\n");
|
|
@@ -231,30 +30,28 @@ console.log("\n📦 dk-frontend-skills 技能包安装开始\n");
|
|
|
231
30
|
if (fs.existsSync(claudeSource)) {
|
|
232
31
|
if (fs.existsSync(claudeDest)) {
|
|
233
32
|
// .claude 已存在 → 备份后再操作
|
|
234
|
-
appendLog("INFO", "Install started (upgrade)");
|
|
33
|
+
appendLog(logFile, "INFO", "Install started (upgrade)");
|
|
235
34
|
backupDir(claudeDest);
|
|
236
35
|
|
|
237
36
|
const skillsSource = path.join(claudeSource, "skills");
|
|
238
37
|
const skillsDest = path.join(claudeDest, "skills");
|
|
239
38
|
|
|
240
|
-
installSkills(skillsSource, skillsDest);
|
|
241
|
-
installSettings(claudeSource, claudeDest);
|
|
39
|
+
installSkills(skillsSource, skillsDest, logFile);
|
|
40
|
+
installSettings(claudeSource, claudeDest, logFile);
|
|
242
41
|
} else {
|
|
243
42
|
// 全新安装
|
|
244
|
-
appendLog("INFO", "Install started (fresh)");
|
|
43
|
+
appendLog(logFile, "INFO", "Install started (fresh)");
|
|
245
44
|
fs.cpSync(claudeSource, claudeDest, { recursive: true });
|
|
246
|
-
appendLog("INFO", `.claude/ directory created`);
|
|
45
|
+
appendLog(logFile, "INFO", `.claude/ directory created`);
|
|
247
46
|
}
|
|
248
47
|
}
|
|
249
48
|
|
|
250
49
|
// 安装 CLAUDE.md
|
|
251
|
-
// 用户已有则只备份不覆盖,防止冲掉自定义配置
|
|
252
50
|
if (fs.existsSync(mdSource)) {
|
|
253
51
|
if (!fs.existsSync(mdDest)) {
|
|
254
52
|
fs.copyFileSync(mdSource, mdDest);
|
|
255
|
-
appendLog("INFO", "CLAUDE.md installed");
|
|
53
|
+
appendLog(logFile, "INFO", "CLAUDE.md installed");
|
|
256
54
|
} else {
|
|
257
|
-
// 仅备份用户版本到 .claude/backups/,方便查阅
|
|
258
55
|
const backupsDir = path.join(claudeDest, "backups");
|
|
259
56
|
if (!fs.existsSync(backupsDir)) {
|
|
260
57
|
fs.mkdirSync(backupsDir, { recursive: true });
|
|
@@ -262,14 +59,13 @@ if (fs.existsSync(mdSource)) {
|
|
|
262
59
|
const bakName = `CLAUDE.md.${backupTimestamp()}`;
|
|
263
60
|
const bakPath = path.join(backupsDir, bakName);
|
|
264
61
|
fs.copyFileSync(mdDest, bakPath);
|
|
265
|
-
appendLog("BACKUP", `backups/${bakName} (CLAUDE.md user version preserved)`);
|
|
62
|
+
appendLog(logFile, "BACKUP", `backups/${bakName} (CLAUDE.md user version preserved)`);
|
|
266
63
|
}
|
|
267
64
|
}
|
|
268
65
|
|
|
269
66
|
// 输出摘要
|
|
270
67
|
console.log("✅ dk-frontend-skills 安装完成!\n");
|
|
271
68
|
|
|
272
|
-
// 读取日志输出最后几行
|
|
273
69
|
if (fs.existsSync(logFile)) {
|
|
274
70
|
const logs = fs.readFileSync(logFile, "utf-8").trim().split("\n");
|
|
275
71
|
const recent = logs.slice(-10);
|
|
@@ -280,6 +76,4 @@ if (fs.existsSync(logFile)) {
|
|
|
280
76
|
console.log("");
|
|
281
77
|
}
|
|
282
78
|
|
|
283
|
-
|
|
284
|
-
console.log("💡 dk-frontend-skills 安装包仍留在 node_modules 中");
|
|
285
|
-
console.log(" 如需移除,可手动执行: npm uninstall dk-frontend-skills\n");
|
|
79
|
+
console.log("💡 提示:运行 npx dk-skills 可交互选择启用/禁用技能\n");
|
package/scripts/core.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// 时间戳格式:YYYY-MM-DD HH:mm:ss
|
|
5
|
+
function timestamp() {
|
|
6
|
+
const d = new Date();
|
|
7
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 紧凑时间戳用于备份目录名:YYYYMMDD.HHmmss
|
|
12
|
+
function backupTimestamp() {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
15
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}.${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 追加日志到 .claude/.install.log
|
|
20
|
+
* 需要传入 logFile 路径
|
|
21
|
+
*/
|
|
22
|
+
function appendLog(logFile, level, message) {
|
|
23
|
+
const dir = path.dirname(logFile);
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
const line = `[${timestamp()}] [${level}] ${message}\n`;
|
|
28
|
+
fs.appendFileSync(logFile, line, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 备份已有目录到 dir/backups/<timestamp>/
|
|
33
|
+
* 排除 backups/ 自身,清理超过 3 份的旧备份
|
|
34
|
+
*/
|
|
35
|
+
function backupDir(dir) {
|
|
36
|
+
if (!fs.existsSync(dir)) return null;
|
|
37
|
+
|
|
38
|
+
const backupsDir = path.join(dir, "backups");
|
|
39
|
+
if (!fs.existsSync(backupsDir)) {
|
|
40
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ts = backupTimestamp();
|
|
44
|
+
const bakPath = path.join(backupsDir, ts);
|
|
45
|
+
fs.mkdirSync(bakPath, { recursive: true });
|
|
46
|
+
|
|
47
|
+
for (const item of fs.readdirSync(dir)) {
|
|
48
|
+
if (item === "backups") continue;
|
|
49
|
+
fs.cpSync(path.join(dir, item), path.join(bakPath, item), { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 清理旧备份,只保留最近 3 份
|
|
53
|
+
const backups = fs
|
|
54
|
+
.readdirSync(backupsDir)
|
|
55
|
+
.filter((name) => /^\d{8}\.\d{6}$/.test(name))
|
|
56
|
+
.sort()
|
|
57
|
+
.reverse();
|
|
58
|
+
|
|
59
|
+
if (backups.length > 3) {
|
|
60
|
+
for (const old of backups.slice(3)) {
|
|
61
|
+
const oldPath = path.join(backupsDir, old);
|
|
62
|
+
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return bakPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPlainObject(val) {
|
|
70
|
+
return Object.prototype.toString.call(val) === "[object Object]";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 递归深合并,用户值优先,目标新增字段补充
|
|
75
|
+
*/
|
|
76
|
+
function deepMerge(userObj, pkgObj) {
|
|
77
|
+
const result = { ...userObj };
|
|
78
|
+
|
|
79
|
+
for (const key of Object.keys(pkgObj)) {
|
|
80
|
+
if (!(key in result)) {
|
|
81
|
+
result[key] = pkgObj[key];
|
|
82
|
+
} else if (isPlainObject(pkgObj[key]) && isPlainObject(result[key])) {
|
|
83
|
+
result[key] = deepMerge(result[key], pkgObj[key]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 读取技能目录下的 .meta.json,没有则返回 null
|
|
92
|
+
*/
|
|
93
|
+
function readMeta(skillDir) {
|
|
94
|
+
const metaPath = path.join(skillDir, ".meta.json");
|
|
95
|
+
if (fs.existsSync(metaPath)) {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
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
|
+
/**
|
|
151
|
+
* 安装 settings.json,深度合并
|
|
152
|
+
*/
|
|
153
|
+
function installSettings(sourceDir, destDir, logFile) {
|
|
154
|
+
const settingsSource = path.join(sourceDir, "settings.json");
|
|
155
|
+
const settingsDest = path.join(destDir, "settings.json");
|
|
156
|
+
const localSource = path.join(sourceDir, "settings.local.json");
|
|
157
|
+
const localDest = path.join(destDir, "settings.local.json");
|
|
158
|
+
|
|
159
|
+
for (const { src, dest, name } of [
|
|
160
|
+
{ src: settingsSource, dest: settingsDest, name: "settings.json" },
|
|
161
|
+
{ src: localSource, dest: localDest, name: "settings.local.json" },
|
|
162
|
+
]) {
|
|
163
|
+
if (!fs.existsSync(src)) continue;
|
|
164
|
+
|
|
165
|
+
if (fs.existsSync(dest)) {
|
|
166
|
+
const userData = JSON.parse(fs.readFileSync(dest, "utf-8"));
|
|
167
|
+
const pkgData = JSON.parse(fs.readFileSync(src, "utf-8"));
|
|
168
|
+
const merged = deepMerge(userData, pkgData);
|
|
169
|
+
fs.writeFileSync(dest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
170
|
+
appendLog(logFile, "SETTINGS", `${name} merged`);
|
|
171
|
+
} else {
|
|
172
|
+
fs.copyFileSync(src, dest);
|
|
173
|
+
appendLog(logFile, "SETTINGS", `${name} created`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 获取包内的技能列表(带元信息)
|
|
180
|
+
*/
|
|
181
|
+
function getPackageSkills(packageDir) {
|
|
182
|
+
const skillsSource = path.join(packageDir, ".claude", "skills");
|
|
183
|
+
if (!fs.existsSync(skillsSource)) return [];
|
|
184
|
+
|
|
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
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 获取用户已安装的技能及其 settings 中的 enabled 状态
|
|
198
|
+
*/
|
|
199
|
+
function getUserSkills(claudeDest) {
|
|
200
|
+
const skillsDest = path.join(claudeDest, "skills");
|
|
201
|
+
if (!fs.existsSync(skillsDest)) return [];
|
|
202
|
+
|
|
203
|
+
const settingsPath = path.join(claudeDest, "settings.json");
|
|
204
|
+
let settings = {};
|
|
205
|
+
if (fs.existsSync(settingsPath)) {
|
|
206
|
+
try {
|
|
207
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
208
|
+
} catch {
|
|
209
|
+
settings = {};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const skillConfigs = settings.skills || {};
|
|
214
|
+
|
|
215
|
+
return fs.readdirSync(skillsDest).map((name) => {
|
|
216
|
+
const meta = readMeta(path.join(skillsDest, name));
|
|
217
|
+
const config = skillConfigs[name] || {};
|
|
218
|
+
return {
|
|
219
|
+
name,
|
|
220
|
+
version: meta ? meta.version : "?",
|
|
221
|
+
description: meta ? meta.description : config.description || "",
|
|
222
|
+
tags: meta ? meta.tags : [],
|
|
223
|
+
enabled: config.enabled !== false,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
timestamp,
|
|
230
|
+
backupTimestamp,
|
|
231
|
+
appendLog,
|
|
232
|
+
backupDir,
|
|
233
|
+
deepMerge,
|
|
234
|
+
readMeta,
|
|
235
|
+
installSkills,
|
|
236
|
+
installSettings,
|
|
237
|
+
getPackageSkills,
|
|
238
|
+
getUserSkills,
|
|
239
|
+
};
|