Files
dify/web/scripts/fetch-skill-templates.ts
yyh 142b72f435 refactor(skill): remove tags/icons/categories, use kebab-case folder names
Drop CategoryTabs component, SkillTemplateTag type, icon/tags fields,
and UI_CONFIG from the fetch script. Upload folders now use the
kebab-case skill id (e.g. "skill-creator") instead of the display name.
Card shows the human-readable name from SKILL.md frontmatter while the
created folder uses the id for consistent naming.
2026-01-30 16:10:19 +08:00

268 lines
6.5 KiB
TypeScript

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()