Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Pick the skills you want; the installer copies them into your coding agent. Re-r
|-------|-------------|
| [writing-specs](./skills/spec-driven/writing-specs) | Write and manage spec files with search, conflict detection, and reporting |
| [writing-tasks](./skills/spec-driven/writing-tasks) | Decompose specs into persistent task files with a dependency graph and progress |
| [writing-flows](./skills/spec-driven/writing-flows) | Write single-scenario Flow docs with a Mermaid diagram, step branches, and source references |
| [implement-with-test](./skills/spec-driven/implement-with-test) | Implement a task with tests; auto-detects the test framework |
| [test-commit-push-pr-clean](./skills/spec-driven/test-commit-push-pr-clean) | Branch-safe finish: lint, test, commit, push, open PR, clean worktrees |

Expand Down
264 changes: 74 additions & 190 deletions scripts/generate-plugin-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,222 +6,106 @@ const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.join(__dirname, '..')

// 카테고리 매핑
const categoryMapping = {
'code-style-plugin': 'code-review',
'code-quality-plugin': 'code-review',
'scaffold-claude-feature': 'code-review',
'git-commit-plugin': 'workflow',
'pr-create-plugin': 'workflow',
'git-worktree-plugin': 'workflow',
'simple-sdd-plugin': 'specification',
'spec-manager-plugin': 'specification',
'session-reporter-plugin': 'utility',
'stop-notification-plugin': 'utility',
'worktrace-plugin': 'utility',
'frontend-plugin': 'code-review',
'smart-commit-plugin': 'workflow',
'test-commit-push-pr-clean-plugin': 'workflow',
'common-mcp-plugin': 'infrastructure'
const categoryMeta = {
'spec-driven': { id: 'spec-driven', name: { ko: '스펙 & 개발', en: 'Spec-Driven Dev' }, icon: '📋' },
'agents': { id: 'agents', name: { ko: '에이전트', en: 'Agents' }, icon: '🤖' },
'browser': { id: 'browser', name: { ko: '브라우저', en: 'Browser' }, icon: '🌐' },
'productivity': { id: 'productivity', name: { ko: '생산성', en: 'Productivity' }, icon: '⚡' },
'misc': { id: 'misc', name: { ko: '기타', en: 'Misc' }, icon: '🛠️' },
}

const categories = {
'code-review': {
id: 'code-review',
name: { ko: '코드 리뷰 & 품질', en: 'Code Review & Quality' },
icon: '🎨'
},
'workflow': {
id: 'workflow',
name: { ko: '개발 워크플로우', en: 'Development Workflow' },
icon: '🔄'
},
'specification': {
id: 'specification',
name: { ko: '사양 & 계획', en: 'Specification & Planning' },
icon: '📋'
},
'utility': {
id: 'utility',
name: { ko: '유틸리티', en: 'Utilities' },
icon: '🛠️'
},
'infrastructure': {
id: 'infrastructure',
name: { ko: '인프라', en: 'Infrastructure' },
icon: '🏗️'
}
}

function getPluginDirectories() {
const items = fs.readdirSync(rootDir)
const pluginDirs = []

for (const item of items) {
const itemPath = path.join(rootDir, item)
const pluginJsonPath = path.join(itemPath, '.claude-plugin', 'plugin.json')

if (fs.statSync(itemPath).isDirectory() && fs.existsSync(pluginJsonPath)) {
pluginDirs.push(item)
}
}

return pluginDirs
}

function parsePluginJson(pluginDir) {
const pluginJsonPath = path.join(rootDir, pluginDir, '.claude-plugin', 'plugin.json')
const content = fs.readFileSync(pluginJsonPath, 'utf-8')
return JSON.parse(content)
}

function getComponents(pluginDir) {
const components = {
skills: [],
commands: [],
hooks: false,
mcpServers: []
}

const pluginPath = path.join(rootDir, pluginDir)

// Check for skills
const skillsDir = path.join(pluginPath, 'skills')
if (fs.existsSync(skillsDir)) {
const skillItems = fs.readdirSync(skillsDir)
for (const item of skillItems) {
const skillPath = path.join(skillsDir, item)
if (fs.statSync(skillPath).isDirectory() &&
fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
components.skills.push(item)
function parseSkillFrontmatter(content) {
const lines = content.split('\n')
if (lines[0].trim() !== '---') return {}

const fm = {}
let i = 1
while (i < lines.length && lines[i].trim() !== '---') {
const line = lines[i]
const colonIdx = line.indexOf(':')
if (colonIdx !== -1) {
const key = line.slice(0, colonIdx).trim()
let value = line.slice(colonIdx + 1).trim()
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1)
}
if (key && value) fm[key] = value
}
i++
}
return fm
}

// Check for commands
const commandsDir = path.join(pluginPath, 'commands')
if (fs.existsSync(commandsDir)) {
const cmdFiles = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'))
components.commands = cmdFiles.map(f => f.replace('.md', ''))
}
function readSkillMeta(skillRelPath) {
const skillMdPath = path.join(rootDir, skillRelPath.replace('./', ''), 'SKILL.md')
if (!fs.existsSync(skillMdPath)) return null

// Check for hooks
const hooksPath = path.join(pluginPath, 'hooks', 'hooks.json')
if (fs.existsSync(hooksPath)) {
components.hooks = true
}
const content = fs.readFileSync(skillMdPath, 'utf-8')
const fm = parseSkillFrontmatter(content)

// Check for MCP servers
const mcpPath = path.join(pluginPath, '.mcp.json')
if (fs.existsSync(mcpPath)) {
try {
const mcpContent = fs.readFileSync(mcpPath, 'utf-8')
const mcpConfig = JSON.parse(mcpContent)
if (mcpConfig.mcpServers) {
components.mcpServers = Object.keys(mcpConfig.mcpServers)
}
} catch (e) {
console.error(`Error parsing MCP config for ${pluginDir}:`, e)
}
}
let desc = fm.description || ''
const useWhenIdx = desc.indexOf(' Use when')
if (useWhenIdx !== -1) desc = desc.slice(0, useWhenIdx)

return components
return { name: fm.name || '', description: desc.trim() }
}

function extractFeatures(pluginDir) {
const readmePath = path.join(rootDir, pluginDir, 'README.md')
const features = []

if (!fs.existsSync(readmePath)) {
return features
}

const content = fs.readFileSync(readmePath, 'utf-8')
const lines = content.split('\n')

// 기능 섹션 찾기
let inFeatureSection = false
for (const line of lines) {
// 기능, Features, 주요 기능 등의 헤더 찾기
if (line.match(/^#+\s*(기능|Features?|주요\s*기능|핵심\s*기능)/i)) {
inFeatureSection = true
function generatePluginData() {
const pluginJson = JSON.parse(
fs.readFileSync(path.join(rootDir, '.claude-plugin', 'plugin.json'), 'utf-8')
)

const skills = []
for (const skillPath of pluginJson.skills) {
// e.g. "./skills/spec-driven/writing-specs"
const parts = skillPath.replace('./', '').split('/')
const category = parts[1]
const skillName = parts[2]

const meta = readSkillMeta(skillPath)
if (!meta) {
console.warn(`Skipping ${skillPath}: SKILL.md not found`)
continue
}

// 다른 헤더를 만나면 종료
if (inFeatureSection && line.match(/^#+\s/)) {
break
}

// 체크마크나 불릿 포인트 찾기
if (inFeatureSection) {
const match = line.match(/^[\s]*[-*✅•]\s*\*?\*?(.+?)\*?\*?\s*$/)
if (match && match[1].trim()) {
const feature = match[1].trim()
.replace(/\*\*/g, '')
.replace(/`/g, '')
if (feature.length > 0 && feature.length < 100) {
features.push(feature)
}
}
}

// 최대 5개까지만
if (features.length >= 5) break
skills.push({
id: `skills/${category}/${skillName}`,
name: skillName,
version: pluginJson.version || '0.1.0',
description: meta.description,
author: 'Stefan Cho',
category,
components: {
skills: [skillName],
commands: [],
hooks: false,
mcpServers: [],
},
features: [],
installCommand: `npx skills@latest add devstefancho/claude-plugins --skill ${skillName} -a claude-code -y`,
uninstallCommand: '',
})
}

return features
}
skills.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name))

function generatePluginData() {
const pluginDirs = getPluginDirectories()
const plugins = []

for (const dir of pluginDirs) {
try {
const metadata = parsePluginJson(dir)
const components = getComponents(dir)
const features = extractFeatures(dir)
const categoryId = categoryMapping[dir] || 'utility'

plugins.push({
id: dir,
name: metadata.name || dir,
version: metadata.version || '1.0.0',
description: metadata.description || '',
author: typeof metadata.author === 'object' ? metadata.author.name : (metadata.author || 'Unknown'),
category: categoryId,
components,
features,
installCommand: `/plugin install ${metadata.name || dir}@devstefancho-claude-plugins`,
uninstallCommand: `/plugin uninstall ${metadata.name || dir}@devstefancho-claude-plugins`,
readmePath: `${dir}/README.md`,
hasReadme: fs.existsSync(path.join(rootDir, dir, 'README.md'))
})
} catch (e) {
console.error(`Error processing plugin ${dir}:`, e)
}
}

// 이름순 정렬
plugins.sort((a, b) => a.name.localeCompare(b.name))
const usedCategoryIds = [...new Set(skills.map(s => s.category))]
const categories = usedCategoryIds.filter(c => categoryMeta[c]).map(c => categoryMeta[c])

const data = {
marketplace: {
name: 'devstefancho-claude-plugins',
name: 'devstefancho-skills',
repo: 'devstefancho/claude-plugins',
installCommand: '/plugin marketplace add devstefancho/claude-plugins'
installCommand: 'npx skills@latest add devstefancho/claude-plugins',
},
plugins,
categories: Object.values(categories)
plugins: skills,
categories,
}

// Write to website/data
const outputPath = path.join(rootDir, 'website', 'data', 'plugins.json')
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2))

console.log(`Generated plugin data for ${plugins.length} plugins`)
console.log(`Output: ${outputPath}`)
console.log(`Generated skill data for ${skills.length} skills → ${outputPath}`)
}

generatePluginData()
6 changes: 3 additions & 3 deletions website/.vitepress/theme/components/InstallBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const props = defineProps<{
lang?: 'ko' | 'en'
}>()

const command = '/plugin marketplace add devstefancho/claude-plugins'
const command = 'npx skills@latest add devstefancho/claude-plugins'
const copied = ref(false)
const dismissed = ref(true)

Expand Down Expand Up @@ -36,13 +36,13 @@ const dismiss = () => {
const labels = {
en: {
prefix: 'First time?',
desc: 'Register the marketplace to get started:',
desc: 'Install all skills into your coding agent:',
copied: 'Copied!',
copy: 'Copy',
},
ko: {
prefix: '처음이신가요?',
desc: '마켓플레이스를 먼저 등록하세요:',
desc: '코딩 에이전트에 스킬을 설치하세요:',
copied: '복사됨!',
copy: '복사',
}
Expand Down
20 changes: 10 additions & 10 deletions website/.vitepress/theme/components/PluginCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ const props = defineProps<{
const copied = ref(false)

const categoryNames = {
'code-review': { ko: '코드 리뷰', en: 'Code Review' },
'workflow': { ko: '워크플로우', en: 'Workflow' },
'specification': { ko: '사양 & 계획', en: 'Specification' },
'utility': { ko: '유틸리티', en: 'Utility' },
'infrastructure': { ko: '인프라', en: 'Infrastructure' }
'spec-driven': { ko: '스펙 & 개발', en: 'Spec-Driven Dev' },
'agents': { ko: '에이전트', en: 'Agents' },
'browser': { ko: '브라우저', en: 'Browser' },
'productivity': { ko: '생산성', en: 'Productivity' },
'misc': { ko: '기타', en: 'Misc' },
}

const copyInstallCommand = async () => {
Expand All @@ -68,11 +68,11 @@ const getCategoryName = (categoryId: string) => {

const getCategoryIcon = (categoryId: string) => {
const icons = {
'code-review': Code2,
'workflow': GitBranch,
'specification': FileText,
'utility': Wrench,
'infrastructure': Server
'spec-driven': FileText,
'agents': Brain,
'browser': Terminal,
'productivity': Wrench,
'misc': Server,
}
return icons[categoryId] || Wrench
}
Expand Down
10 changes: 5 additions & 5 deletions website/.vitepress/theme/components/PluginList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ const categories = pluginData.categories
const plugins = pluginData.plugins as Plugin[]

const categoryNames: Record<string, { ko: string; en: string }> = {
'code-review': { ko: '코드 리뷰 & 품질', en: 'Code Review' },
'workflow': { ko: '워크플로우', en: 'Workflow' },
'specification': { ko: '사양 & 계획', en: 'Specification' },
'utility': { ko: '유틸리티', en: 'Utilities' },
'infrastructure': { ko: '인프라', en: 'Infrastructure' }
'spec-driven': { ko: '스펙 & 개발', en: 'Spec-Driven Dev' },
'agents': { ko: '에이전트', en: 'Agents' },
'browser': { ko: '브라우저', en: 'Browser' },
'productivity': { ko: '생산성', en: 'Productivity' },
'misc': { ko: '기타', en: 'Misc' },
}

const categoryCount = (catId: string) => plugins.filter(p => p.category === catId).length
Expand Down
Loading
Loading