import { execSync } from 'node:child_process' import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' import { dirname, extname, join, relative } from 'node:path' import { fileURLToPath } from 'node:url' const REPO_URL = 'https://github.com/anthropics/skills.git' const __dirname = dirname(fileURLToPath(import.meta.url)) const CLONE_DIR = join(process.env.TMPDIR || '/tmp', 'anthropic-skills') const OUTPUT_DIR = join( __dirname, '../app/components/workflow/skill/start-tab/templates', ) const SKILLS_OUTPUT_DIR = join(OUTPUT_DIR, 'skills') const TEXT_EXTENSIONS = new Set([ '.md', '.py', '.sh', '.txt', '.xml', '.json', '.html', '.js', '.css', '.ts', '.tsx', '.jsx', '.yaml', '.yml', '.toml', '.cfg', '.ini', '.conf', '.env', '.gitignore', '.editorconfig', '.prettierrc', '.eslintrc', '.svg', '.csv', '.tsv', '.log', '.rst', '.tex', '.r', '.rb', '.lua', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h', '.hpp', '.bat', '.cmd', '.ps1', '.zsh', '.bash', '.fish', ]) const SKIP_FILES = new Set(['LICENSE.txt']) type FileEntry = { name: string node_type: 'file' content: string encoding?: 'base64' } type FolderEntry = { name: string node_type: 'folder' children: TreeEntry[] } type TreeEntry = FileEntry | FolderEntry type SkillMeta = { id: string name: string description: string fileCount: number } function isTextFile(filePath: string): boolean { const ext = extname(filePath).toLowerCase() if (TEXT_EXTENSIONS.has(ext)) return true if (ext === '') return true return false } function countFiles(entries: TreeEntry[]): number { let count = 0 for (const entry of entries) { if (entry.node_type === 'file') count++ else count += countFiles(entry.children) } return count } function readDirectoryTree(dirPath: string): TreeEntry[] { const entries: TreeEntry[] = [] const items = readdirSync(dirPath).sort() for (const item of items) { if (SKIP_FILES.has(item)) continue const fullPath = join(dirPath, item) const stat = statSync(fullPath) if (stat.isDirectory()) { entries.push({ name: item, node_type: 'folder', children: readDirectoryTree(fullPath), }) } else if (stat.isFile()) { if (isTextFile(fullPath)) { entries.push({ name: item, node_type: 'file', content: readFileSync(fullPath, 'utf-8'), }) } else { entries.push({ name: item, node_type: 'file', content: readFileSync(fullPath).toString('base64'), encoding: 'base64', }) } } } return entries } function parseFrontmatter(content: string): { name: string, description: string } { const match = content.match(/^---\n([\s\S]*?)\n---/) if (!match) return { name: '', description: '' } const yaml = match[1] let name = '' let description = '' for (const line of yaml.split('\n')) { const nameMatch = line.match(/^name:\s*(.+)/) if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '') const descMatch = line.match(/^description:\s*(.+)/) if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, '') } return { name, description } } function generateSkillFile(id: string, children: TreeEntry[]): string { const lines: string[] = [] lines.push('// AUTO-GENERATED — DO NOT EDIT') lines.push('// Source: https://github.com/anthropics/skills') lines.push('import type { SkillTemplateNode } from \'../types\'') lines.push('') lines.push(`const children: SkillTemplateNode[] = ${JSON.stringify(children, null, 2)}`) lines.push('') lines.push('export default children') lines.push('') return lines.join('\n') } function sq(value: string): string { return `'${value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'` } function generateRegistryFile(metas: SkillMeta[]): string { const lines: string[] = [] lines.push('// AUTO-GENERATED — DO NOT EDIT') lines.push('// Source: https://github.com/anthropics/skills') lines.push('import type { SkillTemplateEntry } from \'./types\'') lines.push('') lines.push('export const SKILL_TEMPLATES: SkillTemplateEntry[] = [') for (const meta of metas) { lines.push(' {') lines.push(` id: ${sq(meta.id)},`) lines.push(` name: ${sq(meta.name)},`) lines.push(` description: ${sq(meta.description)},`) lines.push(` fileCount: ${meta.fileCount},`) lines.push(` loadContent: () => import(${sq(`./skills/${meta.id}`)}).then(m => m.default),`) lines.push(' },') } lines.push(']') lines.push('') return lines.join('\n') } function main() { console.log('Cloning anthropics/skills...') if (existsSync(CLONE_DIR)) rmSync(CLONE_DIR, { recursive: true }) execSync(`git clone --depth 1 ${REPO_URL} ${CLONE_DIR}`, { stdio: 'inherit' }) const skillsDir = join(CLONE_DIR, 'skills') if (!existsSync(skillsDir)) { console.error('Error: skills/ directory not found in cloned repo') process.exit(1) } const skillDirs = readdirSync(skillsDir) .filter(name => statSync(join(skillsDir, name)).isDirectory()) .sort() console.log(`Found ${skillDirs.length} skills: ${skillDirs.join(', ')}`) if (!existsSync(SKILLS_OUTPUT_DIR)) mkdirSync(SKILLS_OUTPUT_DIR, { recursive: true }) const metas: SkillMeta[] = [] for (const skillId of skillDirs) { const skillPath = join(skillsDir, skillId) const children = readDirectoryTree(skillPath) const fileCount = countFiles(children) const skillMdPath = join(skillPath, 'SKILL.md') let meta: { name: string, description: string } = { name: skillId, description: '' } if (existsSync(skillMdPath)) meta = parseFrontmatter(readFileSync(skillMdPath, 'utf-8')) if (!meta.name) meta.name = skillId metas.push({ id: skillId, name: meta.name, description: meta.description, fileCount, }) const outputPath = join(SKILLS_OUTPUT_DIR, `${skillId}.ts`) writeFileSync(outputPath, generateSkillFile(skillId, children)) console.log(` Generated ${relative(OUTPUT_DIR, outputPath)} (${fileCount} files)`) } const registryPath = join(OUTPUT_DIR, 'registry.ts') writeFileSync(registryPath, generateRegistryFile(metas)) console.log(`Generated registry.ts with ${metas.length} entries`) console.log('Cleaning up...') rmSync(CLONE_DIR, { recursive: true }) console.log('Done!') } main()