OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / scripts / generate-skill.js

309 lines · javascript ref:
#!/usr/bin/env node

/**
 * Generate Skill - Auto-generate skill from repo documentation
 *
 * Usage:
 *   node scripts/generate-skill.js <repo-name>
 *   node scripts/generate-skill.js <repo-name> --force   # Overwrite existing
 *   node scripts/generate-skill.js <repo-name> --dry-run # Preview without writing
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const KB_ROOT = join(__dirname, '..')
const SKILLS_DIR = join(KB_ROOT, '.claude', 'skills')
const HOLEPUNCHTO_DIR = join(KB_ROOT, 'holepunchto')

// Load semantic data
function loadJSON(filename) {
  const path = join(HOLEPUNCHTO_DIR, filename)
  if (!existsSync(path)) {
    console.error(`❌ File not found: ${filename}`)
    return null
  }
  return JSON.parse(readFileSync(path, 'utf-8'))
}

const REPO_MAP = loadJSON('repo_map.json')
const CATEGORIES = loadJSON('semantic-categories.json')
const DEPENDENCIES = loadJSON('dependency-graph.json')
const PATTERNS = loadJSON('pattern-map.json')

// Find repo in repo_map
function findRepo(repoName) {
  if (!REPO_MAP || !REPO_MAP.repos) return null
  return REPO_MAP.repos.find(r => r.name === repoName)
}

// Get category info
function getCategory(repoName) {
  if (!CATEGORIES) return { category: 'infrastructure', subcategory: 'miscellaneous' }
  const cat = CATEGORIES[repoName]
  if (!cat) return { category: 'infrastructure', subcategory: 'miscellaneous' }
  return {
    category: cat.category.toLowerCase().replace(/\s+/g, '-'),
    subcategory: cat.subcategory || 'general'
  }
}

// Get dependencies
function getDependencies(repoName) {
  if (!DEPENDENCIES || !DEPENDENCIES[repoName]) return []
  const deps = DEPENDENCIES[repoName].dependencies || {}
  // Filter to only holepunchto packages
  return Object.keys(deps).filter(d =>
    REPO_MAP && REPO_MAP.repos && REPO_MAP.repos.find(r => r.name === d)
  )
}

// Get patterns
function getPatterns(repoName) {
  if (!PATTERNS || !PATTERNS.repoPatterns || !PATTERNS.repoPatterns[repoName]) return []
  return PATTERNS.repoPatterns[repoName].implements || []
}

// Determine priority from category and stars
function determinePriority(repo, category) {
  if (!repo) return 'medium'

  const stars = repo.stars || 0
  const cat = category.category

  // Critical infrastructure
  if (cat === 'building-blocks') return 'critical'
  if (cat === 'helpers' && stars > 100) return 'high'
  if (cat === 'pear-modules' && stars > 50) return 'high'

  // High usage
  if (stars > 500) return 'critical'
  if (stars > 200) return 'high'
  if (stars > 50) return 'medium'

  return 'low'
}

// Read existing holepunchto skills.md if available
function readHolepunchtoDoc(repoName) {
  const docPath = join(HOLEPUNCHTO_DIR, 'skills', repoName, 'skills.md')
  if (!existsSync(docPath)) return null
  return readFileSync(docPath, 'utf-8')
}

// Extract examples from holepunchto docs
function extractExamples(doc) {
  if (!doc) return []

  const examples = []
  const codeBlockRegex = /```(?:javascript|js)?\n([\s\S]*?)```/g
  let match

  while ((match = codeBlockRegex.exec(doc)) !== null) {
    const code = match[1].trim()
    if (code.length > 20 && code.length < 500) { // Reasonable example size
      examples.push(code)
    }
  }

  return examples.slice(0, 3) // Max 3 examples
}

// Generate "When to Use" section
function generateWhenToUse(repo, category, patterns) {
  const uses = []

  // Based on category
  if (category.category === 'building-blocks') {
    uses.push('Building P2P applications requiring distributed data structures')
  }
  if (category.category === 'helpers') {
    uses.push('Simplifying common P2P development tasks')
  }
  if (category.category === 'bare-modules') {
    uses.push('Using Bare runtime for P2P applications')
  }
  if (category.category === 'pear-modules') {
    uses.push('Building Pear applications')
  }

  // Based on patterns
  if (patterns.includes('append-only-log')) {
    uses.push('Implementing append-only data structures')
  }
  if (patterns.includes('replication')) {
    uses.push('Syncing data between peers')
  }
  if (patterns.includes('dht')) {
    uses.push('P2P peer discovery and routing')
  }
  if (patterns.includes('encryption')) {
    uses.push('Securing P2P connections')
  }

  // Fallback based on description
  if (uses.length === 0 && repo && repo.description) {
    const desc = repo.description.toLowerCase()
    if (desc.includes('test')) uses.push('Testing P2P applications')
    if (desc.includes('util')) uses.push('Common utility operations')
    if (desc.includes('stream')) uses.push('Working with data streams')
    if (desc.includes('encode')) uses.push('Encoding and decoding data')
  }

  // Generic fallback
  if (uses.length === 0) {
    uses.push('Building P2P applications with Holepunchto ecosystem')
  }

  return uses.slice(0, 5) // Max 5 use cases
}

// Generate skill content
function generateSkillContent(repoName, options = {}) {
  const repo = findRepo(repoName)
  if (!repo) {
    console.error(`❌ Repo not found in repo_map.json: ${repoName}`)
    return null
  }

  const category = getCategory(repoName)
  const dependencies = getDependencies(repoName)
  const patterns = getPatterns(repoName)
  const priority = determinePriority(repo, category)
  const holepunchtoDoc = readHolepunchtoDoc(repoName)
  const examples = extractExamples(holepunchtoDoc)
  const whenToUse = generateWhenToUse(repo, category, patterns)

  // Build skill markdown
  let content = `---
name: ${repoName}
description: ${repo.description || 'Holepunchto module'}
category: ${category.category}
priority: ${priority}
stars: ${repo.stars || 0}
---

# ${repoName.charAt(0).toUpperCase() + repoName.slice(1)} Skill

${repo.description || 'Part of the Holepunchto P2P ecosystem.'}

## When to Use

Use ${repoName} when you need:

`

  whenToUse.forEach(use => {
    content += `- **${use.split(' ').slice(0, 3).join(' ')}** - ${use}\n`
  })

  content += '\n## Quick Examples\n\n'

  if (examples.length > 0) {
    examples.forEach((example, i) => {
      content += `### Example ${i + 1}\n\n\`\`\`javascript\n${example}\n\`\`\`\n\n`
    })
  } else {
    content += `\`\`\`javascript
import ${repoName.replace(/-/g, '')} from '${repoName}'

// Basic usage
// See complete documentation for API details
\`\`\`

`
  }

  // Integration section
  if (dependencies.length > 0) {
    content += '## Integration with Other Skills\n\n'
    content += `${repoName} commonly works with:\n\n`
    dependencies.slice(0, 5).forEach(dep => {
      content += `- **${dep}** - Dependency\n`
    })
    content += '\n'
  }

  // Patterns section
  if (patterns.length > 0) {
    content += '## P2P Patterns\n\n'
    content += `This module implements:\n\n`
    patterns.forEach(pattern => {
      content += `- ${pattern.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}\n`
    })
    content += '\n'
  }

  // Documentation link
  content += '## Complete Documentation\n\n'
  content += `- **GitHub**: https://github.com/holepunchto/${repoName}\n`
  content += `- **Holepunchto Skills**: See \`holepunchto/skills/${repoName}/skills.md\` for complete API\n`

  return content
}

// Write skill file
function writeSkill(repoName, content, force = false) {
  const skillDir = join(SKILLS_DIR, repoName)
  const skillFile = join(skillDir, 'SKILL.md')

  // Check if exists
  if (existsSync(skillFile) && !force) {
    console.error(`❌ Skill already exists: ${repoName}`)
    console.error('Use --force to overwrite')
    return false
  }

  // Create directory
  if (!existsSync(skillDir)) {
    mkdirSync(skillDir, { recursive: true })
  }

  // Write file
  writeFileSync(skillFile, content, 'utf-8')
  return true
}

// Main
function main() {
  const args = process.argv.slice(2)

  if (args.length === 0 || args[0] === '--help') {
    console.log('Usage:')
    console.log('  node generate-skill.js <repo-name>')
    console.log('  node generate-skill.js <repo-name> --force')
    console.log('  node generate-skill.js <repo-name> --dry-run')
    process.exit(args.length === 0 ? 1 : 0)
  }

  const repoName = args[0]
  const force = args.includes('--force')
  const dryRun = args.includes('--dry-run')

  console.log(`Generating skill for: ${repoName}`)

  const content = generateSkillContent(repoName)
  if (!content) {
    process.exit(1)
  }

  if (dryRun) {
    console.log('\n--- Preview (dry run) ---\n')
    console.log(content)
    console.log('\n--- End Preview ---\n')
    return
  }

  const success = writeSkill(repoName, content, force)
  if (success) {
    console.log(`✅ Generated skill: ${repoName}`)
    console.log(`   File: .claude/skills/${repoName}/SKILL.md`)
  } else {
    process.exit(1)
  }
}

main()