UNPKG

@vibe-dev-kit/cli

Version:

Advanced Command-line toolkit that analyzes your codebase and deploys project-aware rules, memories, commands and agents to any AI coding assistant - VDK is the world's first Vibe Development Kit

1,476 lines (1,298 loc) 81.9 kB
/** * RuleGenerator.js * * Unified rule generator implementing both local template generation and IDE-aware capabilities. * Creates IDE-specific rules with schema validation and remote repository integration. * * Features: * - Local Handlebars template generation (fallback) * - Remote .mdc rule fetching from repository (primary) * - IDE-specific rule adaptation * - YAML frontmatter schema validation (VDK v2.1.0) * - Multi-platform AI assistant support */ import chalk from 'chalk'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import path from 'path'; import { fileURLToPath } from 'url'; import { downloadRule, fetchRuleList } from '../../blueprints-client.js'; import { createIntegrationManager } from '../../integrations/index.js'; import { validateBlueprint } from '../../utils/schema-validator.js'; import { applyLightTemplating, prepareTemplateVariables } from '../utils/light-templating.js'; import { ClaudeCodeAdapter } from './ClaudeCodeAdapter.js'; import { RuleAdapter } from './RuleAdapter.js'; // Ensure fetch is available (Node.js 18+ has it built-in) if (typeof globalThis.fetch === 'undefined') { // Fallback for older Node.js versions would go here console.warn('fetch not available, some features may not work'); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); export class RuleGenerator { constructor(outputPath = './.ai/rules', template = 'default', overwrite = false, options = {}) { // Support both (outputPath, template, overwrite) and (options) constructor signatures if (typeof outputPath === 'object') { options = outputPath; outputPath = options.outputPath || './.ai/rules'; template = options.template || 'default'; overwrite = options.overwrite || false; } this.verbose = options.verbose || false; this.outputPath = outputPath; this.template = template; this.overwrite = overwrite; this.templatesDir = options.templatesDir || path.join(__dirname, '../templates'); this.generatedFiles = []; // IDE-aware specific properties this.integrationManager = null; this.detectedIntegrations = []; this.rulesByIDE = {}; this.ruleAdapters = {}; // VDK Ecosystem configuration this.ecosystemVersion = '2.1.0'; this.hubEndpoint = options.hubEndpoint || 'https://vdk.tools'; this.repositoryEndpoint = options.repositoryEndpoint || 'https://api.github.com/repos/entro314-labs/VDK-Blueprints'; this.enableRemoteFetch = options.enableRemoteFetch !== false; this.enableAnalytics = options.enableAnalytics !== false; this.schemaValidation = options.schemaValidation !== false; this.ideIntegration = options.ideIntegration !== false; // Initialize rule adapters this.initializeRuleAdapters(options); } /** * Initialize VDK-compatible rule adapters for different IDEs */ initializeRuleAdapters(options = {}) { const adapterOptions = { verbose: options.verbose, projectPath: options.projectPath || process.cwd(), ecosystemVersion: this.ecosystemVersion, }; // Claude Code adapter with sophisticated memory hierarchy this.ruleAdapters.claude = new ClaudeCodeAdapter({ ...adapterOptions, ruleGenerator: this }); // Generic adapters for other IDEs this.ruleAdapters.cursor = new RuleAdapter({ ...adapterOptions, targetIDE: 'cursor' }); this.ruleAdapters.windsurf = new RuleAdapter({ ...adapterOptions, targetIDE: 'windsurf' }); this.ruleAdapters.vscode = new RuleAdapter({ ...adapterOptions, targetIDE: 'vscode' }); this.ruleAdapters.copilot = new RuleAdapter({ ...adapterOptions, targetIDE: 'copilot' }); // Set default adapter for VDK ecosystem this.ruleAdapter = this.ruleAdapters.claude; // Claude Code as primary } /** * Enhanced rule generation with IDE-native formats * @param {Object} analysisData - Combined analysis results * @param {Object} categoryFilter - Category filtering options for command fetching * @returns {Object} Generated rules organized by IDE */ async generateIDESpecificRules(analysisData, categoryFilter = null) { if (this.verbose) { console.log(chalk.gray('Starting IDE-aware rule generation...')); } // Initialize integration manager this.integrationManager = createIntegrationManager( analysisData.projectStructure?.root || process.cwd() ); // Detect active integrations this.detectedIntegrations = await this.detectActiveIntegrations(); if (this.verbose) { console.log( chalk.gray( `Detected integrations: ${this.detectedIntegrations.map((i) => i.name).join(', ')}` ) ); } // First, try to fetch remote blueprints let standardizedRules = []; if (this.enableRemoteFetch) { standardizedRules = await this.fetchFromRulesRepository(analysisData); if (this.verbose && standardizedRules.length > 0) { console.log(chalk.gray(`Fetched ${standardizedRules.length} remote blueprints`)); } } // Fallback to local rules if remote fetch failed or returned no results if (standardizedRules.length === 0) { standardizedRules = await this.loadStandardizedRules(analysisData); if (this.verbose) { console.log(chalk.gray(`Using ${standardizedRules.length} local fallback rules`)); } } const results = { generatedRules: {}, summary: { totalFiles: 0, integrations: this.detectedIntegrations.length, formats: [], }, }; // Generate rules for each detected integration using RuleAdapter for (const integration of this.detectedIntegrations) { if (this.verbose) { console.log(chalk.gray(`Adapting rules for ${integration.name}...`)); } try { let adaptedRules; // For Claude Code, use the enhanced adapter with category filtering support if (integration.name === 'Claude Code') { adaptedRules = await this.ruleAdapters.claude.adaptForClaude( standardizedRules, analysisData, categoryFilter ); } else { // For other IDEs, use the standard adapter adaptedRules = await this.ruleAdapter.adaptRules( standardizedRules, this.mapIntegrationToIDE(integration.name), analysisData ); } if (this.verbose && process.env.VDK_DEBUG) { console.log('Debug adaptedRules structure:', JSON.stringify(adaptedRules, null, 2)); } // Write adapted files to disk await this.writeAdaptedRules(adaptedRules); results.generatedRules[integration.name] = adaptedRules; results.summary.totalFiles += adaptedRules.files.length; // Track unique formats const format = this.getIDEFormat(integration.name); if (!results.summary.formats.includes(format)) { results.summary.formats.push(format); } } catch (error) { console.error(chalk.red(`Failed to adapt rules for ${integration.name}: ${error.message}`)); } } // Generate fallback universal rules if no specific integrations detected if (this.detectedIntegrations.length === 0) { if (this.verbose) { console.log( chalk.yellow('No specific IDE integrations detected, generating universal rules...') ); } const universalRules = await this.generateUniversalRules(analysisData); results.generatedRules['Universal'] = universalRules; results.summary.totalFiles += universalRules.length; results.summary.formats.push('md'); } return results; } /** * Detect which IDE integrations are actively being used * @returns {Array} List of active integrations */ async detectActiveIntegrations() { const activeIntegrations = []; const allIntegrations = this.integrationManager.getAllIntegrations(); for (const integration of allIntegrations) { const detection = integration.detectUsage(); // Consider integration active if confidence is medium or high if (detection.confidence === 'medium' || detection.confidence === 'high') { activeIntegrations.push({ name: integration.name, confidence: detection.confidence, integration: integration, }); } } return activeIntegrations; } /** * Load standardized rules from the rules directory with light templating * @param {Object} analysisData - Analysis data for template variables * @returns {Array} Array of rule objects with frontmatter and templated content */ async loadStandardizedRules(analysisData = {}) { const rulesDir = path.resolve(__dirname, '../../../.ai/rules'); const rules = []; try { const ruleFiles = await this.findRuleFiles(rulesDir); for (const filePath of ruleFiles) { try { const rawContent = await fs.readFile(filePath, 'utf8'); const frontmatter = this.parseFrontmatter(rawContent); // Fix framework field if it's 'vdk' if (frontmatter.framework === 'vdk') { frontmatter.framework = this.inferFrameworkFromFile(filePath, rawContent); } // Apply light templating to content (replaces ${variable} patterns) const templateVariables = prepareTemplateVariables(analysisData); const templatedContent = applyLightTemplating(rawContent, templateVariables); if (this.verbose && rawContent !== templatedContent) { console.log(chalk.gray(`Applied light templating to ${path.basename(filePath)}`)); } rules.push({ filePath, frontmatter, content: templatedContent, }); } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Failed to load rule file ${filePath}: ${error.message}`)); } } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Failed to load rules directory: ${error.message}`)); } } return rules; } /** * Recursively find all .mdc rule files * @param {string} dir - Directory to search * @returns {Array} Array of file paths */ async findRuleFiles(dir) { const files = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await this.findRuleFiles(fullPath))); } else if (entry.name.endsWith('.mdc')) { files.push(fullPath); } } } catch (error) { // Ignore directory access errors } return files; } /** * Parse YAML frontmatter from rule content * @param {string} content - File content * @returns {Object} Parsed frontmatter */ parseFrontmatter(content) { if (!content.startsWith('---')) { return {}; } const endIndex = content.indexOf('---', 3); if (endIndex === -1) { return {}; } const frontmatterText = content.slice(3, endIndex).trim(); try { // Use proper YAML parser for accurate parsing const parsed = yaml.load(frontmatterText); return parsed || {}; } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Failed to parse frontmatter: ${error.message}`)); } return {}; } } /** * Infer actual framework from file path and content * @param {string} filePath - Path to the rule file * @param {string} content - File content * @returns {string} Inferred framework */ inferFrameworkFromFile(filePath, content) { const fileName = path.basename(filePath, '.mdc').toLowerCase(); // Check filename for framework indicators if (fileName.includes('nextjs') || fileName.includes('next')) return 'nextjs'; if (fileName.includes('react')) return 'react'; if (fileName.includes('vue')) return 'vue'; if (fileName.includes('angular')) return 'angular'; if (fileName.includes('django')) return 'django'; if (fileName.includes('node') || fileName.includes('express')) return 'nodejs'; if (fileName.includes('typescript')) return 'typescript'; if (fileName.includes('python')) return 'python'; if (fileName.includes('swift')) return 'swift'; if (fileName.includes('kotlin')) return 'kotlin'; // Check content for framework indicators const contentLower = content.toLowerCase(); if (contentLower.includes('next.js') || contentLower.includes('nextjs')) return 'nextjs'; if (contentLower.includes('react')) return 'react'; if (contentLower.includes('vue.js') || contentLower.includes('vue 3')) return 'vue'; if (contentLower.includes('angular')) return 'angular'; if (contentLower.includes('django')) return 'django'; if (contentLower.includes('express') || contentLower.includes('node.js')) return 'nodejs'; return 'general'; } /** * Map integration name to IDE identifier for RuleAdapter * @param {string} integrationName - Integration name * @returns {string} IDE identifier */ mapIntegrationToIDE(integrationName) { const mapping = { 'Cursor AI': 'cursor', Windsurf: 'windsurf', 'Claude Code': 'claude', 'GitHub Copilot': 'github-copilot', }; return mapping[integrationName] || 'generic'; } /** * Write adapted rules to disk * @param {Object} adaptedRules - Adapted rules from RuleAdapter */ async writeAdaptedRules(adaptedRules) { if (!adaptedRules || !adaptedRules.files) { console.error( chalk.red('No files to write - adaptedRules or adaptedRules.files is undefined') ); return; } // Handle ClaudeCodeAdapter structure (files are already written, just paths returned) if (typeof adaptedRules.files[0] === 'string') { if (this.verbose) { console.log( chalk.gray(`Claude Code files already written: ${adaptedRules.files.length} files`) ); adaptedRules.files.forEach((filePath) => { console.log(chalk.gray(` - ${filePath}`)); }); } return; } // Handle standard RuleAdapter structure (file objects with path and content) for (const file of adaptedRules.files) { try { if (!file.path) { console.error(chalk.red(`Failed to write file: path is undefined`), file); continue; } // Ensure directory exists await fs.mkdir(path.dirname(file.path), { recursive: true }); // Write file await fs.writeFile(file.path, file.content, 'utf8'); if (this.verbose) { console.log(chalk.gray(`Written: ${file.path}`)); } } catch (error) { console.error(chalk.red(`Failed to write ${file.path || 'undefined'}: ${error.message}`)); } } } /** * Generate universal rules when no specific IDE is detected * @param {Object} analysisData - Analysis data * @returns {Array} Generated file paths */ async generateUniversalRules(analysisData) { // Set output to .ai/rules for universal compatibility const originalOutputPath = this.outputPath; this.outputPath = path.join( analysisData.projectStructure?.root || process.cwd(), '.ai', 'rules' ); try { // Generate basic fallback rules with standard templates return await this.generateBasicUniversalRules(analysisData); } finally { this.outputPath = originalOutputPath; } } /** * Get the appropriate output directory for an IDE * @param {string} ideName - IDE name * @param {Object} configPaths - IDE config paths * @param {Object} analysisData - Analysis data * @returns {string} Output directory path */ getIDEOutputDirectory(ideName, _configPaths, analysisData) { const projectRoot = analysisData.projectStructure?.root || process.cwd(); switch (ideName) { case 'Cursor AI': return path.join(projectRoot, '.cursor', 'rules'); case 'Windsurf': return path.join(projectRoot, '.windsurf', 'rules'); case 'Claude Code': // Claude Code uses project root for memory files return projectRoot; case 'GitHub Copilot': return path.join(projectRoot, '.github', 'copilot'); default: return path.join(projectRoot, '.ai', 'rules'); } } /** * Get the file format used by an IDE * @param {string} ideName - IDE name * @returns {string} File format (md, mdc, etc.) */ getIDEFormat(ideName) { switch (ideName) { case 'Cursor AI': return 'mdc'; // MDC format with YAML frontmatter case 'Windsurf': return 'md'; // Markdown with XML tags case 'Claude Code': return 'md'; // Pure markdown for memory case 'GitHub Copilot': return 'json'; // JSON configuration default: return 'md'; } } /** * Enhanced rule generation implementing VDK ecosystem architecture * Primary workflow: CLI → Rules Repository → IDE/AI Tools → Hub Analytics */ async generateEnhancedRules(analysisData) { if (this.verbose) { console.log(chalk.blue('🚀 Starting VDK ecosystem-aware rule generation...')); } try { // Step 1: Fetch templates from Rules Repository (CLI ↔ Rules Repository) let remoteTemplates = []; if (this.enableRemoteFetch) { remoteTemplates = await this.fetchFromRulesRepository(analysisData); } // Step 2: Generate base rules using standard process const standardRules = await this.generateIDESpecificRules(analysisData); // Step 2.5: Convert remote templates to actual rule files if (remoteTemplates.length > 0) { await this.generateRulesFromRemoteTemplates(remoteTemplates, analysisData); if (this.verbose) { console.log( chalk.green(`📝 Generated ${remoteTemplates.length} rules from remote templates`) ); } } // Step 3: Apply VDK schema validation (only for remote templates) if (this.schemaValidation) { await this.validateRulesWithVDKSchema(standardRules, remoteTemplates); } // Step 4: Generate VDK ecosystem manifest await this.generateVDKManifest(analysisData, remoteTemplates, standardRules); // Step 5: Send analytics to Hub (CLI → Hub) if (this.enableAnalytics) { await this.sendAnalyticsToHub(analysisData, standardRules); } return { ...standardRules, ecosystem: { version: this.ecosystemVersion, remoteTemplates: remoteTemplates.length, validated: this.schemaValidation, analyticsEnabled: this.enableAnalytics, }, }; } catch (error) { if (this.verbose) { console.error(chalk.red(`❌ VDK ecosystem generation failed: ${error.message}`)); } throw error; } } /** * Fetch content from VDK Blueprints Repository (GitHub) with light templating * @param {Object} analysisData - Project analysis data * @param {string} contentType - Type of content to fetch ('rules', 'commands', 'docs', 'schemas') * @param {string} platform - Platform/IDE specific directory (for commands) * @param {Object} categoryFilter - Category filtering options * @returns {Array} Fetched templates/content */ async fetchFromRepository( analysisData, contentType = 'rules', platform = null, categoryFilter = null ) { if (!this.enableRemoteFetch) return []; try { if (this.verbose) { console.log(chalk.gray(`📚 Fetching ${contentType} from VDK Blueprints Repository...`)); console.log(chalk.gray(` Repository endpoint: ${this.repositoryEndpoint}`)); } const projectSignature = this.generateProjectSignature(analysisData); // Include analysisData in signature for templating projectSignature.analysisData = analysisData; const templates = await this.fetchRepositoryContent( projectSignature, contentType, platform, categoryFilter ); if (this.verbose) { console.log(chalk.green(`📚 Fetched ${templates.length} ${contentType} items`)); } return templates; } catch (error) { if (this.verbose) { console.log(chalk.yellow(`⚠️ Could not fetch remote ${contentType}: ${error.message}`)); } return []; } } /** * Fetch rules from repository */ async fetchFromRulesRepository(analysisData) { return this.fetchFromRepository(analysisData, 'rules'); } /** * Generate project signature for template matching */ generateProjectSignature(analysisData) { // Handle both techStack and technologyData structure for backward compatibility const techData = analysisData.techStack || analysisData.technologyData || {}; const signature = { languages: techData.primaryLanguages || [], frameworks: techData.frameworks || [], libraries: techData.libraries?.slice(0, 10) || [], testingFrameworks: techData.testingFrameworks || [], buildTools: techData.buildTools || [], patterns: Object.keys(analysisData.patterns?.architecturalPatterns || {}), projectSize: this.categorizeProjectSize(analysisData.projectStructure), complexity: this.assessComplexity(analysisData), ecosystemVersion: this.ecosystemVersion, }; if (this.verbose) { console.log(chalk.gray(`🎯 Project signature for template matching:`)); console.log(chalk.gray(` Languages: ${signature.languages.join(', ')}`)); console.log(chalk.gray(` Frameworks: ${signature.frameworks.join(', ')}`)); console.log(chalk.gray(` Libraries: ${signature.libraries.slice(0, 5).join(', ')}`)); } return signature; } /** * Fetch content from GitHub VDK-Blueprints Repository * @param {Object} projectSignature - Project signature for filtering * @param {string} contentType - Type of content ('rules', 'commands', 'docs', 'schemas') * @param {string} platform - Platform/IDE specific directory (for commands) * @param {Object} categoryFilter - Category filtering options */ async fetchRepositoryContent( projectSignature, contentType = 'rules', platform = null, categoryFilter = null ) { try { // Build the API URL based on content type // For commands, we fetch from .ai/commands/ root and filter by platform later const apiUrl = `${this.repositoryEndpoint}/contents/.ai/${contentType}`; const headers = { Accept: 'application/vnd.github.v3+json', 'User-Agent': `VDK-CLI/${this.ecosystemVersion}`, }; // Use GitHub token if available to avoid rate limiting if (process.env.VDK_GITHUB_TOKEN) { headers['Authorization'] = `token ${process.env.VDK_GITHUB_TOKEN}`; } const response = await fetch(apiUrl, { headers }); if (!response.ok) { if (this.verbose) { console.log(chalk.yellow(`⚠️ Repository fetch failed: ${response.status} for ${apiUrl}`)); } throw new Error(`Repository API failed: ${response.status}`); } if (this.verbose) { console.log(chalk.gray(`✅ Repository fetch successful from: ${apiUrl}`)); } const contents = await response.json(); if (this.verbose) { console.log(chalk.gray(`📁 Found ${contents.length} items in repository`)); } if (this.verbose) { console.log( chalk.gray( `📁 Repository contents: ${contents.map((item) => `${item.name}(${item.type})`).join(', ')}` ) ); } // For commands, we need to handle platform-specific directory structure and dynamic categories let allTemplates; if (contentType === 'commands' && platform) { allTemplates = await this.expandRepositoryDirectoriesWithCategories( contents, projectSignature, contentType, platform, categoryFilter ); } else { // First expand directories to get all template files allTemplates = await this.expandRepositoryDirectories( contents, projectSignature, contentType ); } if (this.verbose) { console.log( chalk.gray( `📁 After directory expansion: ${allTemplates.length} ${contentType} files found` ) ); if (categoryFilter && categoryFilter.categories) { console.log( chalk.gray(`🎯 Category filter applied: ${categoryFilter.categories.join(', ')}`) ); } } const relevantTemplates = this.filterRelevantTemplates( allTemplates, projectSignature, contentType ); if (this.verbose) { console.log( chalk.gray(`🎯 Relevant templates after filtering: ${relevantTemplates.length}`) ); relevantTemplates.slice(0, 3).forEach((template) => { console.log(chalk.gray(` - ${template.name} (${template.path})`)); }); } const templates = []; // For commands, allow more templates since they're all potentially useful // For rules, allow more templates to include technology-specific rules // Increased limits to include more technology-specific rules const limit = contentType === 'commands' ? 25 : contentType === 'rules' ? 20 : 10; for (const template of relevantTemplates.slice(0, limit)) { // Allow more rules for better coverage try { const rawTemplateContent = await this.fetchTemplateContent(template.download_url); // Apply light templating to remote content (replaces ${variable} patterns) const templateVariables = prepareTemplateVariables(projectSignature.analysisData || {}); const templatedContent = applyLightTemplating(rawTemplateContent, templateVariables); if (this.verbose && rawTemplateContent !== templatedContent) { console.log(chalk.gray(`Applied light templating to remote template ${template.name}`)); } templates.push({ name: template.name, content: templatedContent, source: 'repository', path: template.path, relevanceScore: template.relevanceScore, }); } catch (err) { if (this.verbose) { console.log(chalk.yellow(`Could not fetch template ${template.name}: ${err.message}`)); } } } return templates; } catch (error) { if (this.verbose) { console.log(chalk.yellow(`Repository fetch failed: ${error.message}`)); } return []; } } /** * Expand directories to get all template files recursively * @param {Array} contents - Directory contents from GitHub API * @param {Object} projectSignature - Project signature * @param {string} contentType - Type of content being fetched * @param {number} maxDepth - Maximum recursion depth * @param {number} currentDepth - Current recursion depth */ async expandRepositoryDirectories( contents, projectSignature, contentType = 'rules', maxDepth = 2, currentDepth = 0 ) { const allTemplates = []; // Determine file extensions based on content type const fileExtensions = this.getFileExtensions(contentType); if (this.verbose && currentDepth === 0) { console.log( chalk.gray( `🔍 Looking for ${contentType} files with extensions: ${fileExtensions.join(', ')}` ) ); } for (const item of contents) { if (item.type === 'file' && fileExtensions.some((ext) => item.name.endsWith(ext))) { // Direct file - add to templates allTemplates.push(item); if (this.verbose) { console.log(chalk.gray(` ✅ Found ${contentType} file: ${item.name}`)); } } else if (item.type === 'dir' && currentDepth < maxDepth) { if (this.verbose) { console.log(chalk.gray(` 📁 Exploring directory: ${item.name}`)); } try { // Fetch subdirectory contents const headers = { Accept: 'application/vnd.github.v3+json', 'User-Agent': `VDK-CLI/${this.ecosystemVersion}`, }; // Use GitHub token if available if (process.env.VDK_GITHUB_TOKEN) { headers['Authorization'] = `token ${process.env.VDK_GITHUB_TOKEN}`; } const subdirResponse = await fetch(item.url, { headers }); if (subdirResponse.ok) { const subdirContents = await subdirResponse.json(); const subdirTemplates = await this.expandRepositoryDirectories( subdirContents, projectSignature, contentType, maxDepth, currentDepth + 1 ); allTemplates.push(...subdirTemplates); if (this.verbose && subdirTemplates.length > 0) { console.log( chalk.gray(` 📁 Found ${subdirTemplates.length} templates in ${item.name}/`) ); } } else { if (this.verbose) { console.log( chalk.yellow( ` ⚠️ Failed to fetch ${item.name}: ${subdirResponse.status} ${subdirResponse.statusText}` ) ); } } } catch (error) { if (this.verbose) { console.log( chalk.yellow(`Could not fetch subdirectory ${item.name}: ${error.message}`) ); } } } } return allTemplates; } /** * Expand repository directories with dynamic category discovery and filtering * This method discovers available categories dynamically from the repository * @param {Array} contents - Repository contents * @param {Object} projectSignature - Project signature for filtering * @param {string} contentType - Type of content ('commands') * @param {string} platform - Platform directory (e.g., 'claude-code') * @param {Object} categoryFilter - Category filtering options * @returns {Array} Array of filtered templates */ async expandRepositoryDirectoriesWithCategories( contents, projectSignature, contentType, platform, categoryFilter ) { const allTemplates = []; const fileExtensions = this.getFileExtensions(contentType); // Find the platform directory (e.g., 'claude-code') const platformDir = contents.find((item) => item.type === 'dir' && item.name === platform); if (!platformDir) { if (this.verbose) { console.log(chalk.yellow(`⚠️ Platform directory '${platform}' not found`)); } return []; } try { // Fetch platform directory contents to discover categories dynamically const platformResponse = await this.fetchWithAuth(platformDir.url); if (!platformResponse.ok) { throw new Error(`Failed to fetch platform directory: ${platformResponse.status}`); } const platformContents = await platformResponse.json(); const discoveredCategories = platformContents .filter((item) => item.type === 'dir') .map((item) => item.name); if (this.verbose) { console.log(chalk.cyan(`🔍 Discovered categories: ${discoveredCategories.join(', ')}`)); } // Update the static category list with discovered categories await this.updateDiscoveredCategories(discoveredCategories); // Determine which categories to fetch let categoriesToFetch; if (categoryFilter && categoryFilter.categories && categoryFilter.categories.length > 0) { // Use specified categories, but validate they exist categoriesToFetch = categoryFilter.categories.filter((cat) => discoveredCategories.includes(cat) ); if (categoriesToFetch.length !== categoryFilter.categories.length) { const missing = categoryFilter.categories.filter( (cat) => !discoveredCategories.includes(cat) ); if (this.verbose) { console.log( chalk.yellow(`⚠️ Categories not found in repository: ${missing.join(', ')}`) ); } } } else { // Fetch all discovered categories categoriesToFetch = discoveredCategories; } if (this.verbose && categoriesToFetch.length > 0) { console.log(chalk.green(`📂 Fetching from categories: ${categoriesToFetch.join(', ')}`)); } // Fetch commands from each selected category for (const category of categoriesToFetch) { const categoryDir = platformContents.find( (item) => item.name === category && item.type === 'dir' ); if (!categoryDir) continue; try { const categoryResponse = await this.fetchWithAuth(categoryDir.url); if (categoryResponse.ok) { const categoryContents = await categoryResponse.json(); // Filter for command files and add category metadata const categoryTemplates = categoryContents .filter( (item) => item.type === 'file' && fileExtensions.some((ext) => item.name.endsWith(ext)) ) .map((template) => ({ ...template, category: category, // Add category metadata path: `${platform}/${category}/${template.name}`, // Add full path })); allTemplates.push(...categoryTemplates); if (this.verbose && categoryTemplates.length > 0) { console.log( chalk.gray(` 📁 Found ${categoryTemplates.length} commands in ${category}/`) ); } } } catch (error) { if (this.verbose) { console.log(chalk.yellow(`Could not fetch category ${category}: ${error.message}`)); } } } // Apply specific command filtering if provided if (categoryFilter && categoryFilter.specificCommands) { const filteredTemplates = allTemplates.filter((template) => { const commandName = template.name.replace(/\.(md|mdc)$/, ''); return categoryFilter.specificCommands.includes(commandName); }); if (this.verbose) { console.log( chalk.cyan( `🎯 Filtered to specific commands: ${filteredTemplates.length}/${allTemplates.length}` ) ); } return filteredTemplates; } } catch (error) { if (this.verbose) { console.log( chalk.yellow(`Could not expand platform directory ${platform}: ${error.message}`) ); } } return allTemplates; } /** * Helper method to fetch with authentication headers */ async fetchWithAuth(url) { const headers = { Accept: 'application/vnd.github.v3+json', 'User-Agent': `VDK-CLI/${this.ecosystemVersion}`, }; if (process.env.VDK_GITHUB_TOKEN) { headers['Authorization'] = `token ${process.env.VDK_GITHUB_TOKEN}`; } return fetch(url, { headers }); } /** * Update discovered categories in the category selector utility * This keeps the static category list in sync with the repository */ async updateDiscoveredCategories(discoveredCategories) { // This could update a cache file or configuration for the category selector // For now, we'll just log the discovery if (this.verbose) { const newCategories = discoveredCategories.filter( (cat) => !['development', 'quality', 'workflow', 'meta', 'security'].includes(cat) ); if (newCategories.length > 0) { console.log(chalk.magenta(`🆕 New categories discovered: ${newCategories.join(', ')}`)); } } } /** * Get file extensions based on content type * @param {string} contentType - Type of content ('rules', 'commands', 'docs', 'schemas') * @returns {Array} Array of file extensions */ getFileExtensions(contentType) { const extensionMap = { rules: ['.mdc', '.md'], commands: ['.md'], docs: ['.md'], schemas: ['.json'], templates: ['.mdc', '.md', '.hbs'], }; return extensionMap[contentType] || ['.md', '.mdc']; } /** * Filter templates based on project relevance * @param {Array} contents - Content items to filter * @param {Object} projectSignature - Project signature * @param {string} contentType - Type of content being filtered */ filterRelevantTemplates(contents, projectSignature, contentType = 'rules') { const relevantTemplates = []; const fileExtensions = this.getFileExtensions(contentType); for (const item of contents) { if (item.type === 'file' && fileExtensions.some((ext) => item.name.endsWith(ext))) { const relevanceScore = this.calculateTemplateRelevance(item, projectSignature, contentType); if (this.verbose && relevanceScore > 0.05) { console.log( chalk.gray(` 💯 ${contentType}: ${item.name} - Score: ${relevanceScore.toFixed(2)}`) ); } // For commands, use a lower threshold since they're all generally useful const threshold = contentType === 'commands' ? 0.05 : 0.1; if (relevanceScore > threshold) { relevantTemplates.push({ ...item, relevanceScore, }); } } } return relevantTemplates.sort((a, b) => b.relevanceScore - a.relevanceScore); } /** * Calculate template relevance score * @param {Object} template - Template object from GitHub API * @param {Object} projectSignature - Project signature * @param {string} contentType - Type of content being scored */ calculateTemplateRelevance(template, projectSignature, contentType = 'rules') { let score = 0; const fileName = template.name.toLowerCase(); const filePath = (template.path || '').toLowerCase(); // Content-type specific base scoring if (contentType === 'commands') { // All commands get a baseline score since they're generally useful score += 0.1; // Commands get higher relevance for development workflows if ( fileName.includes('develop') || fileName.includes('debug') || fileName.includes('review') ) { score += 0.7; } if (fileName.includes('workflow') || fileName.includes('quality')) { score += 0.6; } if (fileName.includes('git') || fileName.includes('commit') || fileName.includes('pr')) { score += 0.5; } if ( fileName.includes('test') || fileName.includes('security') || fileName.includes('audit') ) { score += 0.4; } } else if (contentType === 'rules') { // Core rules always get high relevance if (fileName.startsWith('0') && fileName.includes('core')) { score += 0.8; } } // MCP configuration is relevant but not overwhelming if (fileName.includes('mcp')) { score += 0.3; } // Common errors and project context are universally relevant if (fileName.includes('common-errors') || fileName.includes('project-context')) { score += 0.6; } // JavaScript/Node.js related files if (fileName.includes('javascript') || fileName.includes('node') || fileName.includes('js')) { score += 0.5; } // Framework matching (highest priority for stack-specific rules) for (const framework of projectSignature.frameworks || []) { const frameworkLower = framework.toLowerCase(); const normalizedFramework = frameworkLower .replace('next.js', 'nextjs') .replace('react.js', 'react') .replace('vue.js', 'vue') .replace('tailwind css', 'tailwind') .replace('shadcn/ui', 'shadcnui') .replace('-', '') .replace('.', '') .replace('/', '') .replace(' ', ''); if ( fileName.includes(normalizedFramework) || filePath.includes(normalizedFramework) || fileName.includes(frameworkLower) || filePath.includes(frameworkLower) ) { score += 0.8; } // Special matching for technology files with version numbers if (frameworkLower.includes('tailwind') && fileName.includes('tailwind')) { score += 0.8; } if (frameworkLower.includes('shadcn') && fileName.includes('shadcn')) { score += 0.8; } // Special handling for stack combinations if ( frameworkLower.includes('supabase') && (fileName.includes('supabase') || filePath.includes('supabase')) ) { score += 0.9; // Even higher for specific integrations } if (frameworkLower.includes('nextjs') && fileName.includes('nextjs')) { score += 0.9; } if (frameworkLower.includes('enterprise') && fileName.includes('enterprise')) { score += 0.7; } } // Apply exclusion rules to prevent irrelevant framework pollution const detectedLanguages = projectSignature.languages || []; const detectedFrameworks = projectSignature.frameworks || []; // CRITICAL: Platform-specific filtering to prevent mobile rules in web projects const isWebProject = detectedFrameworks.some((framework) => { const fw = framework.toLowerCase(); return ( fw.includes('next.js') || fw.includes('nextjs') || (fw.includes('react') && !fw.includes('react native')) || fw.includes('vue.js') || fw.includes('angular') || fw.includes('nuxt') || fw.includes('remix') || fw.includes('gatsby') ); }); const isMobileProject = detectedFrameworks.some((framework) => { const fw = framework.toLowerCase(); return ( fw.includes('react native') || fw.includes('expo') || fw.includes('flutter') || fw.includes('ionic') || fw.includes('capacitor') ); }); // For web projects, completely exclude mobile/native patterns if (isWebProject && !isMobileProject) { if ( fileName.includes('react-native') || filePath.includes('react-native') || fileName.includes('expo') || filePath.includes('expo') || fileName.includes('mobile') || filePath.includes('mobile') || fileName.includes('native') || filePath.includes('native') || fileName.includes('ios') || fileName.includes('android') ) { if (this.verbose) { console.log(chalk.yellow(`🚫 Excluding mobile rule for web project: ${fileName}`)); } return 0; // Completely exclude mobile rules for web projects } } // For mobile projects, prefer mobile-specific rules if (isMobileProject && !isWebProject) { if ( fileName.includes('react-native') || fileName.includes('expo') || fileName.includes('mobile') ) { score += 0.7; // Boost mobile rules for mobile projects } } // If no JavaScript/TypeScript, heavily penalize frontend frameworks if ( !detectedLanguages.some((lang) => ['javascript', 'typescript'].includes(lang.toLowerCase())) ) { if ( fileName.includes('next') || fileName.includes('react') || fileName.includes('vue') || fileName.includes('angular') ) { score = Math.max(0, score - 0.6); // Heavy penalty for frontend frameworks in non-JS projects } } // If no Python, penalize Python-specific rules if (!detectedLanguages.some((lang) => lang.toLowerCase() === 'python')) { if ( fileName.includes('django') || fileName.includes('flask') || fileName.includes('fastapi') || fileName.includes('python') ) { score = Math.max(0, score - 0.6); } } // Comprehensive exclusion for completely irrelevant languages // For web projects (Astro, Next.js, etc.), exclude mobile/backend languages entirely if (isWebProject) { // Exclude mobile development languages if ( fileName.includes('swift') || fileName.includes('kotlin') || fileName.includes('java') || fileName.includes('dart') || fileName.includes('flutter') || fileName.includes('xamarin') ) { if (this.verbose) { console.log( chalk.yellow(`🚫 Excluding mobile language rule for web project: ${fileName}`) ); } return 0; } // Exclude system programming languages if ( fileName.includes('cpp') || fileName.includes('c++') || fileName.includes('rust') || fileName.includes('go') || fileName.includes('c#') || fileName.includes('csharp') ) { if (this.verbose) { console.log( chalk.yellow(`🚫 Excluding system language rule for web project: ${fileName}`) ); } return 0; } } // For documentation/content projects (Astro Starlight), be even more restrictive const isContentProject = detectedFrameworks.some((framework) => { const fw = framework.toLowerCase(); return fw.includes('astro') || fw.includes('starlight') || fw.includes('content'); }); if (isContentProject) { // Only allow Astro, TypeScript, and basic web technologies if ( !fileName.includes('astro') && !fileName.includes('typescript') && !fileName.includes('ts') && !fileName.includes('content') && !fileName.includes('markdown') && !fileName.includes('mcp') && !fileName.includes('common-errors') && !fileName.includes('project-context') && !fileName.startsWith('0') // Core rules ) { // Check if it's a completely different technology stack if ( fileName.includes('nextjs') || fileName.includes('react') || fileName.includes('vue') || fileName.includes('supabase') || fileName.includes('trpc') || fileName.includes('ecommerce') || fileName.includes('enterprise') || fileName.includes('clerk') || fileName.includes('python') || fileName.includes('swift') || fileName.includes('kotlin') || fileName.includes('cpp') || fileName.includes('node') || fileName.includes('express') ) { if (this.verbose) { console.log( chalk.yellow(`🚫 Excluding non-content rule for Astro content project: ${fileName}`) ); } score = Math.max(0, score - 0.8); // Heavy penalty instead of complete exclusion for backward compatibility } } } // Language matching (important for language-specific rules) for (const language of projectSignature.languages || []) { const languageLower = language.toLowerCase(); const normalizedLanguage = languageLower .replace('typescript', 'ts') .replace('javascript', 'js'); if ( fileName.includes(languageLower) || filePath.includes(languageLower) || fileName.includes(normalizedLanguage) || filePath.includes(normalizedLanguage) ) { score += 0.6; } // Special boosting for TypeScript since it's very common if ( languageLower === 'typescript' && (fileName.includes('typescript') || fileName.includes('ts')) ) { score += 0.3; // Extra boost for TypeScript rules } } // Library matching (important for technology-specific rules like shadcn/ui) for (const library of projectSignature.libraries || []) { const libraryLower = library.toLowerCase(); const normalizedLibrary = libraryLower .replace('shadcn/ui', 'shadcnui') .replace('tailwind css', 'tailwind') .replace('-', '') .replace('.', '') .replace('/', '') .replace(' ', ''); if ( fileName.includes(normalizedLibrary) || filePath.includes(normalizedLibrary) || fileName.includes(libraryLower) || filePath.includes(libraryLower) ) { score += 0.7; // High score for library-specific rules } // Special matching for UI libraries if (libraryLower.includes('shadcn') && fileName.includes('shadcn')) { score += 0.8; } if (libraryLower.includes('radix') && fileName.includes('radix')) { score += 0.7; } } // Complexity matching if (fileName.includes(projectSignature.complexity)) { score += 0.1; } // Project size matching if (fileName.includes(projectSignature.projectSize)) { score += 0.1; } // Technology-specific rules get highest priority if ( filePath.includes('technologies/') || filePath.includes('stacks/') || filePath.includes('languages/') ) { score += 0.5; // High priority for technology rules } // Assistant-specific rules - lower priority for technology rule fetching if (filePath.includes('assistants/')) { score += 0.2; // Reduced from 0.4 to prioritize technology rules } // Core tools and tasks get medium relevance if (filePath.includes('tools/') || filePath.includes('tasks/')) { score += 0.3; } return Math.min(score, 1.0); } /** * Fetch template content from URL */ async fetchTemplateContent(downloadUrl) { const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`Template fetch failed: $