mirror of
https://github.com/langgenius/dify.git
synced 2026-02-11 10:01:30 -05:00
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.
268 lines
6.5 KiB
TypeScript
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()
|