Skip to content

Commit fb2370a

Browse files
committed
feat: skills-repo
1 parent 330c414 commit fb2370a

File tree

4 files changed

+593
-0
lines changed

4 files changed

+593
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
.env.local
77
# Package publish backup files
88
**/package.json.backup
9+
.skills-repo-output

scripts/build-skills-repo.mjs

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Build Agent Skills Repository Script
5+
* Collects all agent skills from config/.claude/skills/ and outputs them
6+
* to .skill-repo-output/skills/ for publishing to a separate repository.
7+
*/
8+
9+
import fs from "fs";
10+
import path from "path";
11+
import { fileURLToPath } from "url";
12+
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = path.dirname(__filename);
15+
const projectRoot = path.resolve(__dirname, "..");
16+
17+
// Color definitions
18+
const colors = {
19+
RED: "\x1b[0;31m",
20+
GREEN: "\x1b[0;32m",
21+
YELLOW: "\x1b[1;33m",
22+
BLUE: "\x1b[0;34m",
23+
NC: "\x1b[0m", // No Color
24+
};
25+
26+
// Configuration
27+
const SKILLS_SOURCE_DIR = "config/.claude/skills";
28+
const OUTPUT_DIR = ".skills-repo-output";
29+
const SKILLS_OUTPUT_DIR = path.join(OUTPUT_DIR, "skills");
30+
const README_TEMPLATE_PATH = path.join(
31+
__dirname,
32+
"skills-repo-template",
33+
"readme-template.md",
34+
);
35+
const ADDITIONAL_SKILLS_DIR = path.join(
36+
__dirname,
37+
"skills-repo-template",
38+
"cloudbase-guidelines",
39+
);
40+
41+
/**
42+
* Parse SKILL.md frontmatter
43+
* @param {string} skillPath - Path to skill directory
44+
* @returns {{name: string, description: string}|null} Skill metadata
45+
*/
46+
function parseSkillMetadata(skillPath) {
47+
const skillFile = path.join(skillPath, "SKILL.md");
48+
if (!fs.existsSync(skillFile)) {
49+
return null;
50+
}
51+
52+
try {
53+
const content = fs.readFileSync(skillFile, "utf8");
54+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
55+
if (!frontmatterMatch) {
56+
return null;
57+
}
58+
59+
const frontmatter = frontmatterMatch[1];
60+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
61+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
62+
63+
return {
64+
name: nameMatch ? nameMatch[1].trim() : null,
65+
description: descMatch ? descMatch[1].trim() : null,
66+
};
67+
} catch (error) {
68+
return null;
69+
}
70+
}
71+
72+
/**
73+
* Generate README.md content from template
74+
* @param {Array} skills - Array of skill information
75+
* @returns {string} README content
76+
*/
77+
function generateREADME(skills) {
78+
// Read template file
79+
if (!fs.existsSync(README_TEMPLATE_PATH)) {
80+
console.log(
81+
`${colors.RED}❌ 错误: README 模板文件不存在: ${README_TEMPLATE_PATH}${colors.NC}`,
82+
);
83+
throw new Error(`README template not found: ${README_TEMPLATE_PATH}`);
84+
}
85+
86+
const template = fs.readFileSync(README_TEMPLATE_PATH, "utf8");
87+
88+
// Generate skills list
89+
const skillsList = skills
90+
.map((skill) => {
91+
const metadata = skill.metadata;
92+
const name = metadata?.name || skill.name;
93+
const description = metadata?.description || "No description available";
94+
return `- **${skill.name}** (${name})\n ${description}`;
95+
})
96+
.join("\n\n");
97+
98+
// Replace placeholders
99+
const timestamp = new Date().toISOString().split("T")[0];
100+
const content = template
101+
.replace(/\{\{LAST_UPDATED\}\}/g, timestamp)
102+
.replace(/\{\{SKILLS_COUNT\}\}/g, skills.length.toString())
103+
.replace(/\{\{SKILLS_LIST\}\}/g, skillsList);
104+
105+
return content;
106+
}
107+
108+
/**
109+
* Copy directory recursively
110+
* @param {string} srcDir - Source directory
111+
* @param {string} destDir - Destination directory
112+
* @returns {{files: number, errors: number}} Copy statistics
113+
*/
114+
function copyDirectoryRecursive(srcDir, destDir) {
115+
let filesCount = 0;
116+
let errorsCount = 0;
117+
118+
function copyRecursive(src, dest) {
119+
if (!fs.existsSync(src)) {
120+
return;
121+
}
122+
123+
// Ensure destination directory exists
124+
if (!fs.existsSync(dest)) {
125+
fs.mkdirSync(dest, { recursive: true });
126+
}
127+
128+
const entries = fs.readdirSync(src, { withFileTypes: true });
129+
130+
for (const entry of entries) {
131+
const srcPath = path.join(src, entry.name);
132+
const destPath = path.join(dest, entry.name);
133+
134+
if (entry.isDirectory()) {
135+
copyRecursive(srcPath, destPath);
136+
} else if (entry.isFile()) {
137+
try {
138+
// Remove existing file if it exists
139+
if (fs.existsSync(destPath)) {
140+
fs.unlinkSync(destPath);
141+
}
142+
// Copy file
143+
fs.copyFileSync(srcPath, destPath);
144+
filesCount++;
145+
} catch (error) {
146+
console.log(
147+
` ${colors.RED}❌ 无法复制文件: ${entry.name} - ${error.message}${colors.NC}`,
148+
);
149+
errorsCount++;
150+
}
151+
}
152+
}
153+
}
154+
155+
copyRecursive(srcDir, destDir);
156+
157+
return { files: filesCount, errors: errorsCount };
158+
}
159+
160+
/**
161+
* Build skills repository
162+
*/
163+
async function buildSkillsRepo() {
164+
console.log(
165+
`${colors.BLUE}🔧 CloudBase AI Agent Skills Repository Builder${colors.NC}`,
166+
);
167+
console.log("==================================================");
168+
169+
const sourcePath = path.join(projectRoot, SKILLS_SOURCE_DIR);
170+
const outputPath = path.join(projectRoot, OUTPUT_DIR);
171+
const skillsOutputPath = path.join(projectRoot, SKILLS_OUTPUT_DIR);
172+
173+
// Check if source directory exists
174+
if (!fs.existsSync(sourcePath)) {
175+
console.log(
176+
`${colors.RED}❌ 错误: 源目录 ${SKILLS_SOURCE_DIR} 不存在${colors.NC}`,
177+
);
178+
process.exit(1);
179+
}
180+
181+
console.log(`${colors.GREEN}✅ 源目录存在: ${SKILLS_SOURCE_DIR}${colors.NC}`);
182+
183+
// Clean output directory if it exists
184+
if (fs.existsSync(outputPath)) {
185+
console.log(`${colors.YELLOW}🧹 清理输出目录: ${OUTPUT_DIR}${colors.NC}`);
186+
fs.rmSync(outputPath, { recursive: true, force: true });
187+
}
188+
189+
// Create output directory
190+
console.log(`${colors.BLUE}📁 创建输出目录: ${OUTPUT_DIR}${colors.NC}`);
191+
fs.mkdirSync(outputPath, { recursive: true });
192+
193+
// Get all skill directories
194+
console.log(
195+
`${colors.YELLOW}🔍 扫描技能目录: ${SKILLS_SOURCE_DIR}${colors.NC}`,
196+
);
197+
198+
const skillDirs = fs
199+
.readdirSync(sourcePath, { withFileTypes: true })
200+
.filter((entry) => entry.isDirectory())
201+
.map((entry) => entry.name);
202+
203+
console.log(
204+
`${colors.BLUE}📋 找到 ${skillDirs.length} 个技能目录${colors.NC}`,
205+
);
206+
207+
let totalFiles = 0;
208+
let totalErrors = 0;
209+
const processedSkills = [];
210+
211+
// Process each skill directory
212+
for (let i = 0; i < skillDirs.length; i++) {
213+
const skillDir = skillDirs[i];
214+
console.log(
215+
`\n[${i + 1}/${skillDirs.length}] ${colors.BLUE}处理技能: ${skillDir}${colors.NC}`,
216+
);
217+
218+
const skillSourcePath = path.join(sourcePath, skillDir);
219+
const skillOutputPath = path.join(skillsOutputPath, skillDir);
220+
221+
// Parse skill metadata
222+
const metadata = parseSkillMetadata(skillSourcePath);
223+
224+
const stats = copyDirectoryRecursive(skillSourcePath, skillOutputPath);
225+
226+
totalFiles += stats.files;
227+
totalErrors += stats.errors;
228+
229+
if (stats.errors === 0) {
230+
console.log(
231+
` ${colors.GREEN}✅ 成功复制 ${stats.files} 个文件${colors.NC}`,
232+
);
233+
processedSkills.push({
234+
name: skillDir,
235+
files: stats.files,
236+
metadata: metadata,
237+
});
238+
} else {
239+
console.log(
240+
` ${colors.YELLOW}⚠️ 复制了 ${stats.files} 个文件,${stats.errors} 个错误${colors.NC}`,
241+
);
242+
processedSkills.push({
243+
name: skillDir,
244+
files: stats.files,
245+
metadata: metadata,
246+
});
247+
}
248+
}
249+
250+
// Process additional skills from template directory
251+
if (fs.existsSync(ADDITIONAL_SKILLS_DIR)) {
252+
console.log(
253+
`\n${colors.BLUE}📦 处理额外技能: cloudbase-guidelines${colors.NC}`,
254+
);
255+
256+
const skillDir = "cloudbase-guidelines";
257+
const skillSourcePath = ADDITIONAL_SKILLS_DIR;
258+
const skillOutputPath = path.join(skillsOutputPath, skillDir);
259+
260+
// Parse skill metadata
261+
const metadata = parseSkillMetadata(skillSourcePath);
262+
263+
const stats = copyDirectoryRecursive(skillSourcePath, skillOutputPath);
264+
265+
totalFiles += stats.files;
266+
totalErrors += stats.errors;
267+
268+
if (stats.errors === 0) {
269+
console.log(
270+
` ${colors.GREEN}✅ 成功复制 ${stats.files} 个文件${colors.NC}`,
271+
);
272+
processedSkills.push({
273+
name: skillDir,
274+
files: stats.files,
275+
metadata: metadata,
276+
});
277+
} else {
278+
console.log(
279+
` ${colors.YELLOW}⚠️ 复制了 ${stats.files} 个文件,${stats.errors} 个错误${colors.NC}`,
280+
);
281+
processedSkills.push({
282+
name: skillDir,
283+
files: stats.files,
284+
metadata: metadata,
285+
});
286+
}
287+
}
288+
289+
// Print summary
290+
console.log(`\n${colors.BLUE}📊 构建完成统计:${colors.NC}`);
291+
console.log(
292+
`${colors.GREEN}✅ 成功处理: ${processedSkills.length} 个技能${colors.NC}`,
293+
);
294+
console.log(`${colors.GREEN}✅ 总共复制: ${totalFiles} 个文件${colors.NC}`);
295+
if (totalErrors > 0) {
296+
console.log(`${colors.RED}❌ 复制失败: ${totalErrors} 个文件${colors.NC}`);
297+
}
298+
299+
console.log(`\n${colors.BLUE}📋 处理的技能列表:${colors.NC}`);
300+
for (const skill of processedSkills) {
301+
console.log(
302+
` ${colors.GREEN}${colors.NC} ${skill.name} (${skill.files} 个文件)`,
303+
);
304+
}
305+
306+
// Generate README.md
307+
console.log(`\n${colors.BLUE}📝 生成 README.md...${colors.NC}`);
308+
const readmePath = path.join(outputPath, "README.md");
309+
const readmeContent = generateREADME(processedSkills);
310+
fs.writeFileSync(readmePath, readmeContent, "utf8");
311+
console.log(
312+
`${colors.GREEN}✅ README.md 已生成到: ${OUTPUT_DIR}/README.md${colors.NC}`,
313+
);
314+
315+
console.log(
316+
`\n${colors.GREEN}✨ 技能仓库构建完成!输出目录: ${OUTPUT_DIR}${colors.NC}`,
317+
);
318+
}
319+
320+
/**
321+
* Main function
322+
*/
323+
async function main() {
324+
try {
325+
await buildSkillsRepo();
326+
} catch (error) {
327+
console.error(
328+
`\n${colors.RED}❌ 脚本执行失败: ${error.message}${colors.NC}`,
329+
);
330+
console.error(error.stack);
331+
process.exit(1);
332+
}
333+
}
334+
335+
// Run main function
336+
main().catch(console.error);

0 commit comments

Comments
 (0)