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,240 lines (1,023 loc) 38.1 kB
/** * RuleAdapter.js * * Transforms standardized VDK rules into IDE-native formats and locations. * Each IDE gets rules in the format that makes the most sense for that platform. */ import os from 'node:os' import path from 'node:path' import chalk from 'chalk' export class RuleAdapter { constructor(options = {}) { this.verbose = options.verbose this.projectPath = options.projectPath || process.cwd() // Centralized character limits for each IDE this.characterLimits = { windsurf: { perFile: 6000, totalWorkspace: 12000, global: 6000, }, 'github-copilot': { perGuideline: 600, maxGuidelines: 6, }, cursor: { perFile: Number.POSITIVE_INFINITY, // No specific limit total: Number.POSITIVE_INFINITY, }, claude: { perFile: Number.POSITIVE_INFINITY, // No specific limit perCommand: 10000, // Reasonable limit for commands }, } } /** * Adapt a collection of rules for a specific IDE * @param {Array} rules - Array of rule objects with frontmatter and content * @param {string} targetIDE - Target IDE ('claude', 'cursor', 'windsurf', 'github-copilot') * @param {Object} projectContext - Project analysis context * @returns {Object} Adapted rules with paths and content */ async adaptRules(rules, targetIDE, projectContext = {}) { if (this.verbose) { console.log(chalk.gray(`Adapting ${rules.length} rules for ${targetIDE}...`)) } switch (targetIDE.toLowerCase()) { case 'claude': case 'claude-code': return await this.adaptForClaude(rules, projectContext) case 'cursor': case 'cursor-ai': return await this.adaptForCursor(rules, projectContext) case 'windsurf': return await this.adaptForWindsurf(rules, projectContext) case 'github-copilot': return await this.adaptForGitHubCopilot(rules, projectContext) default: throw new Error(`Unknown IDE: ${targetIDE}`) } } /** * Enforce character limits for content based on IDE type * @param {string} content - Content to check/truncate * @param {string} ideType - IDE type * @param {string} limitType - Type of limit (perFile, perGuideline, etc.) * @returns {Object} Result with content and truncation info */ enforceCharacterLimits(content, ideType, limitType = 'perFile') { const limits = this.characterLimits[ideType.toLowerCase()] if (!limits) { return { content, truncated: false, originalLength: content.length } } const limit = limits[limitType] if (limit === Number.POSITIVE_INFINITY || content.length <= limit) { return { content, truncated: false, originalLength: content.length } } // Smart truncation at sentence boundaries const truncationPoint = this.findSmartTruncationPoint(content, limit) const truncatedContent = content.substring(0, truncationPoint) + (truncationPoint < content.length ? '\n\n*Truncated due to character limit*' : '') if (this.verbose) { console.warn( chalk.yellow( `Content truncated for ${ideType} (${content.length}${truncatedContent.length} chars)` ) ) } return { content: truncatedContent, truncated: true, originalLength: content.length, truncatedLength: truncatedContent.length, } } /** * Find smart truncation point at sentence or paragraph boundaries * @param {string} content - Content to truncate * @param {number} limit - Character limit * @returns {number} Truncation point */ findSmartTruncationPoint(content, limit) { const buffer = Math.max(50, limit * 0.05) // 5% buffer or minimum 50 chars const targetLength = limit - buffer if (content.length <= targetLength) { return content.length } // Try to find good truncation points in order of preference const truncationPoints = [ content.lastIndexOf('\n\n', targetLength), // Paragraph boundary content.lastIndexOf('.\n', targetLength), // Sentence with newline content.lastIndexOf('. ', targetLength), // Sentence boundary content.lastIndexOf('\n', targetLength), // Line boundary targetLength, // Hard truncation ] for (const point of truncationPoints) { if (point > targetLength * 0.7) { // Keep at least 70% of content return point } } return targetLength // Fallback to hard truncation } /** * Adapt rules for Claude Code's memory system with proper hierarchy * @param {Array} rules - Standardized rules * @param {Object} projectContext - Project context * @returns {Object} Claude-native memory files and commands */ async adaptForClaude(rules, projectContext) { const adaptedFiles = [] const _projectName = path.basename(this.projectPath) const globalDir = path.join(os.homedir(), '.claude') const commandsDir = path.join(this.projectPath, '.claude', 'commands') const userCommandsDir = path.join(os.homedir(), '.claude', 'commands') // 1. Global memory (cross-project preferences) - Deploy to ~/.claude/CLAUDE.md const globalRules = rules.filter( (rule) => rule.frontmatter?.category === 'core' && rule.frontmatter?.alwaysApply === true ) if (globalRules.length > 0) { const globalContent = this.generateClaudeGlobalMemory(globalRules) adaptedFiles.push({ path: path.join(globalDir, 'CLAUDE.md'), content: globalContent, type: 'memory', scope: 'global', hierarchyLevel: 'global', }) } // 2. Main project memory (team-shared conventions) - Deploy to ./CLAUDE.md const coreRules = rules.filter( (rule) => rule.frontmatter?.category === 'core' || (rule.frontmatter?.alwaysApply === true && rule.frontmatter?.category !== 'core') ) if (coreRules.length > 0) { const claudeMainContent = this.generateClaudeMainMemory(coreRules, projectContext) adaptedFiles.push({ path: path.join(this.projectPath, 'CLAUDE.md'), content: claudeMainContent, type: 'memory', scope: 'project', hierarchyLevel: 'project', }) } // 3. Technology-specific memory files with import structure const techRules = rules.filter( (rule) => rule.frontmatter?.category === 'technology' || rule.frontmatter?.category === 'framework' || rule.frontmatter?.category === 'language' ) if (techRules.length > 0) { const claudeTechContent = this.generateClaudeTechMemory(techRules, projectContext) adaptedFiles.push({ path: path.join(this.projectPath, 'CLAUDE-patterns.md'), content: claudeTechContent, type: 'memory', scope: 'project', hierarchyLevel: 'technology', }) } // 4. Personal preferences import file const personalPrefsContent = this.generateClaudePersonalPrefsImport() adaptedFiles.push({ path: path.join(this.projectPath, 'CLAUDE-personal.md'), content: personalPrefsContent, type: 'memory', scope: 'import', hierarchyLevel: 'personal', }) // 5. Project commands (namespace: /project:) const taskRules = rules.filter((rule) => rule.frontmatter?.category === 'task') for (const rule of taskRules) { const commandContent = this.generateClaudeSlashCommand(rule, projectContext) const commandName = this.getCommandName(rule.frontmatter?.description) adaptedFiles.push({ path: path.join(commandsDir, `${commandName}.md`), content: commandContent, type: 'command', scope: 'project', namespace: 'project', commandName, }) } // 6. User commands (namespace: /user:) - Deploy to ~/.claude/commands/ const personalRules = rules.filter( (rule) => rule.frontmatter?.category === 'assistant' || rule.frontmatter?.category === 'workflow' ) for (const rule of personalRules) { const commandContent = this.generateClaudeSlashCommand(rule, projectContext) const commandName = this.getCommandName(rule.frontmatter?.description) adaptedFiles.push({ path: path.join(userCommandsDir, `${commandName}.md`), content: commandContent, type: 'command', scope: 'user', namespace: 'user', commandName, }) } return { files: adaptedFiles, summary: { memoryFiles: adaptedFiles.filter((f) => f.type === 'memory').length, commands: adaptedFiles.filter((f) => f.type === 'command').length, totalSize: adaptedFiles.reduce((size, file) => size + file.content.length, 0), }, } } /** * Adapt rules for Cursor's MDC system with proper activation types * @param {Array} rules - Standardized rules * @param {Object} projectContext - Project context * @returns {Object} Cursor-native MDC files */ async adaptForCursor(rules, projectContext) { const adaptedFiles = [] const rulesDir = path.join(this.projectPath, '.cursor', 'rules') // Group rules by activation type for better organization const rulesByActivation = this.groupCursorRulesByActivation(rules) // 1. Always rules - Always included in context for (const rule of rulesByActivation.always) { const mdcContent = this.generateCursorMDC(rule, projectContext) const fileName = this.getCursorFileName(rule) adaptedFiles.push({ path: path.join(rulesDir, `always-${fileName}`), content: mdcContent, type: 'rule', activation: 'always', scope: 'project', priority: 'high', }) } // 2. Auto-attached rules - When files matching globs are referenced for (const rule of rulesByActivation.autoAttached) { const mdcContent = this.generateCursorMDC(rule, projectContext) const fileName = this.getCursorFileName(rule) adaptedFiles.push({ path: path.join(rulesDir, `auto-${fileName}`), content: mdcContent, type: 'rule', activation: 'auto-attached', scope: 'project', globs: rule.frontmatter?.globs || [], priority: 'medium', }) } // 3. Agent-requested rules - AI decides whether to include for (const rule of rulesByActivation.agentRequested) { const mdcContent = this.generateCursorMDC(rule, projectContext) const fileName = this.getCursorFileName(rule) adaptedFiles.push({ path: path.join(rulesDir, `agent-${fileName}`), content: mdcContent, type: 'rule', activation: 'agent-requested', scope: 'project', description: rule.frontmatter?.description, priority: 'medium', }) } // 4. Manual rules - Only when explicitly mentioned using @ruleName for (const rule of rulesByActivation.manual) { const mdcContent = this.generateCursorMDC(rule, projectContext) const fileName = this.getCursorFileName(rule) const ruleName = this.getCursorRuleName(rule) adaptedFiles.push({ path: path.join(rulesDir, `manual-${fileName}`), content: mdcContent, type: 'rule', activation: 'manual', scope: 'project', ruleName, priority: 'low', }) } return { files: adaptedFiles, summary: { always: rulesByActivation.always.length, autoAttached: rulesByActivation.autoAttached.length, agentRequested: rulesByActivation.agentRequested.length, manual: rulesByActivation.manual.length, totalFiles: adaptedFiles.length, }, } } /** * Generate Cursor MDC format with proper frontmatter (FIXED IMPLEMENTATION) * @param {Object} rule - Rule object * @param {Object} projectContext - Project context * @returns {string} MDC formatted content */ generateCursorMDC(rule, _projectContext) { const activationType = this.getCursorActivationType(rule) const globs = rule.frontmatter?.globs || [] const description = rule.frontmatter?.description || '' const cleanContent = this.stripFrontmatter(rule.content) return `--- type: ${activationType} ${globs.length > 0 ? `globs: ${JSON.stringify(globs)}` : ''} ${activationType === 'agent-requested' ? `description: "${description}"` : ''} alwaysApply: ${rule.frontmatter?.alwaysApply} --- ${cleanContent}` } /** * Get Cursor activation type (FIXED IMPLEMENTATION) * @param {Object} rule - Rule object * @returns {string} Activation type */ getCursorActivationType(rule) { if (rule.frontmatter?.alwaysApply) { return 'always' } if (rule.frontmatter?.globs && rule.frontmatter?.globs.length > 0) { return 'auto-attached' } if (rule.frontmatter?.description) { return 'agent-requested' } return 'manual' } /** * Get Cursor file name (FIXED IMPLEMENTATION) * @param {Object} rule - Rule object * @returns {string} File name */ getCursorFileName(rule) { const category = rule.frontmatter?.category || 'general' const framework = rule.frontmatter?.framework || '' const description = rule.frontmatter?.description || '' let baseName = framework || category if (description) { baseName = description .toLowerCase() .replace(/[^a-z0-9\s]/g, '') .replace(/\s+/g, '-') } return `${baseName.toLowerCase().replace(/[^a-z0-9-]/g, '')}.md` } /** * Group Cursor rules by activation type * @param {Array} rules - Rules to group * @returns {Object} Rules grouped by activation type */ groupCursorRulesByActivation(rules) { const groups = { always: [], autoAttached: [], agentRequested: [], manual: [], } for (const rule of rules) { const activationType = this.getCursorActivationType(rule) const groupKey = activationType === 'auto-attached' ? 'autoAttached' : activationType === 'agent-requested' ? 'agentRequested' : activationType if (groups[groupKey]) { groups[groupKey].push(rule) } else { groups.manual.push(rule) } } return groups } /** * Get Cursor rule name for manual activation * @param {Object} rule - Rule object * @returns {string} Rule name for @ruleName usage */ getCursorRuleName(rule) { const description = rule.frontmatter?.description || '' const category = rule.frontmatter?.category || 'rule' if (description) { return description .toLowerCase() .replace(/[^a-z0-9\s]/g, '') .replace(/\s+/g, '') .substring(0, 20) } return category.toLowerCase() } /** * Adapt rules for Windsurf's memories system with proper deployment * @param {Array} rules - Standardized rules * @param {Object} projectContext - Project context * @returns {Object} Windsurf-native memory files */ async adaptForWindsurf(rules, projectContext) { const adaptedFiles = [] const rulesDir = path.join(this.projectPath, '.windsurf', 'rules') const globalDir = path.join(os.homedir(), '.codeium', 'windsurf', 'memories') const projectRuleFile = path.join(this.projectPath, '.windsurfrules.md') // 1. Global rules (organization-wide standards) - Deploy to ~/.codeium/windsurf/memories/ const globalRules = rules.filter( (rule) => rule.frontmatter?.category === 'core' || rule.frontmatter?.alwaysApply === true ) if (globalRules.length > 0) { let globalContent = this.generateWindsurfGlobalMemory(globalRules, projectContext) // Enforce 6K character limit for global rules if (globalContent.length > 6000) { if (this.verbose) { console.warn(chalk.yellow('Global Windsurf rules exceed 6K limit, truncating...')) } globalContent = `${globalContent.substring(0, 5900)}\n\n*Truncated due to character limit*` } adaptedFiles.push({ path: path.join(globalDir, 'global_rules.md'), content: globalContent, type: 'memory', scope: 'global', characterCount: globalContent.length, activationType: 'always-on', }) } // 2. Workspace rules (project-specific) - Deploy to .windsurf/rules/ const workspaceRules = rules.filter( (rule) => rule.frontmatter?.category !== 'core' && rule.frontmatter?.alwaysApply !== true ) for (const rule of workspaceRules) { let windsurfContent = this.generateWindsurfRule(rule, projectContext) // Ensure character limit compliance (6K per file) if (windsurfContent.length > 6000) { if (this.verbose) { console.warn( chalk.yellow(`Rule ${rule.frontmatter?.description} exceeds 6K limit, truncating...`) ) } windsurfContent = `${windsurfContent.substring(0, 5900)}\n\n*Truncated due to character limit*` } const fileName = this.getWindsurfFileName(rule) const activationType = this.getWindsurfActivationType(rule) adaptedFiles.push({ path: path.join(rulesDir, fileName), content: windsurfContent, type: 'rule', scope: 'workspace', characterCount: windsurfContent.length, activationType, }) } // 3. Project rule file (.windsurfrules.md) - Alternative single-file approach if (workspaceRules.length > 0) { const consolidatedContent = this.generateConsolidatedWindsurfRules( workspaceRules, projectContext ) // Check if consolidated approach is better (fewer files, under limits) if (consolidatedContent.length <= 6000 && workspaceRules.length > 3) { adaptedFiles.push({ path: projectRuleFile, content: consolidatedContent, type: 'project-rules', scope: 'project', characterCount: consolidatedContent.length, activationType: 'model-decision', }) } } // Ensure total character limit compliance (12K across all workspace rules) const totalChars = adaptedFiles .filter((f) => f.scope === 'workspace') .reduce((total, file) => total + file.characterCount, 0) if (totalChars > 12000) { if (this.verbose) { console.warn( chalk.yellow(`⚠️ Windsurf workspace rules exceed 12K total limit (${totalChars} chars)`) ) } // Truncate files proportionally to stay under limit const reductionRatio = 11000 / totalChars // Leave some buffer for (const file of adaptedFiles.filter((f) => f.scope === 'workspace')) { const targetLength = Math.floor(file.characterCount * reductionRatio) if (file.content.length > targetLength) { file.content = `${file.content.substring(0, targetLength - 50)}\n\n*Truncated for total limit*` file.characterCount = file.content.length } } } return { files: adaptedFiles, summary: { globalRules: adaptedFiles.filter((f) => f.scope === 'global').length, workspaceRules: adaptedFiles.filter((f) => f.scope === 'workspace').length, totalCharacters: adaptedFiles.reduce((total, file) => total + file.characterCount, 0), characterLimit: 12000, }, } } /** * Adapt rules for GitHub Copilot's guidelines system (FIXED APPROACH) * Generate setup instructions instead of files as per report findings * @param {Array} rules - Standardized rules * @param {Object} projectContext - Project context * @returns {Object} GitHub Copilot setup instructions */ async adaptForGitHubCopilot(rules, _projectContext) { // Don't generate files - generate setup instructions const prioritizedRules = this.prioritizeRulesForCopilot(rules) const selectedRules = prioritizedRules.slice(0, 6) // GitHub Copilot Enterprise limit const guidelines = selectedRules.map((rule, index) => ({ number: index + 1, name: rule.frontmatter?.description || `Rule ${index + 1}`, description: this.truncateToCharLimit(this.stripFrontmatter(rule.content), 600), // 600 char limit filePatterns: rule.frontmatter?.globs || ['**/*'], })) const instructionsContent = `# GitHub Copilot Setup Instructions ## Configure in Repository Settings ⚠️ **Enterprise Feature**: Requires GitHub Copilot Enterprise plan ## Configuration Steps 1. Go to Settings → Code & automation → Copilot → Code review 2. Add these coding guidelines: ${guidelines .map( (guideline) => ` ### Guideline ${guideline.number}: ${guideline.name} **Description:** ${guideline.description} **File patterns:** ${guideline.filePatterns.join(', ')} ` ) .join('')} ## Important Notes - Guidelines only apply to code reviews, not code completion - Each description limited to 600 characters - File patterns use fnmatch syntax (\`*\` wildcard) - Manual setup required in repository settings - Maximum 6 guidelines per repository ## Alternative Options For file-based AI rule management, consider: - **Claude Code**: Memory files and slash commands - **Cursor**: .cursor/rules/ directory with MDC format - **Windsurf**: .windsurf/rules/ directory with XML grouping --- *Generated by VDK CLI - Setup Instructions Only*` return { files: [ { path: path.join(this.projectPath, 'GITHUB_COPILOT_SETUP.md'), content: instructionsContent, type: 'instructions', scope: 'repository', }, ], guidelines, summary: { totalGuidelines: guidelines.length, maxGuidelines: 6, requiresManualSetup: true, enterpriseOnly: true, approach: 'instructions-only', }, } } // === Claude Code Memory Generators === /** * Generate Claude Code global memory for cross-project preferences * @param {Array} globalRules - Global core rules * @returns {string} Global memory content */ generateClaudeGlobalMemory(_globalRules) { return `# Claude Code User Memory ## Coding Preferences ### Code Style - Use consistent indentation (2-space for JS/TS/JSON/YAML, 4-space for Python/Go) - Always use semicolons in JavaScript/TypeScript - Prefer const/let over var - Use descriptive variable names, avoid abbreviations - Add JSDoc comments for complex functions ### Project Structure - Follow conventional directory structures (src/, lib/, components/, etc.) - Keep components small and focused (single responsibility) - Separate business logic from UI components - Use barrel exports (index.js/ts) for clean imports ### Testing - Write tests for all business logic - Use descriptive test names that explain behavior - Group related tests with describe blocks - Mock external dependencies in unit tests ## Development Environment ### Tools & Setup - Primary IDE: Claude Code - Package manager: pnpm (primary), npm (fallback) - Use TypeScript for new JavaScript projects - Use ESLint + Prettier for code formatting ### Git Workflow - Use conventional commit messages (feat:, fix:, docs:, etc.) - Create feature branches from main/master - Squash commits before merging to main - Always create pull/merge requests for code review ## Workflow Preferences ### Development Workflow - Start with failing tests (TDD when appropriate) - Run tests before committing - Use feature flags for incomplete features - Implement monitoring and observability --- *Global Claude Code preferences - Applied across all projects*` } generateClaudeMainMemory(_coreRules, projectContext) { const projectName = path.basename(this.projectPath) const techStack = projectContext.techStack || {} const content = `# ${projectName} - Claude Code Memory ## Project Overview This project uses VDK CLI for AI assistant integration and follows specific patterns and conventions. ### Key Information - **VDK CLI Integration**: Active - **Primary Languages**: ${techStack.primaryLanguages?.join(', ') || 'Not detected'} - **Frameworks**: ${techStack.frameworks?.join(', ') || 'Not detected'} - **Architecture**: ${projectContext.patterns?.architecturalPatterns?.join(', ') || 'Standard'} ## Memory Hierarchy @CLAUDE-patterns.md @CLAUDE-personal.md ## Important Conventions - All AI rules are stored in \`.ai/rules/\` directory - Rules follow unified YAML frontmatter format - Project follows VDK CLI naming conventions - Memory persistence is enabled for context continuity ## VDK CLI Commands - \`/project:analyze\` - Analyze project structure and patterns - \`/project:refresh\` - Update VDK rules based on project changes - \`/project:validate\` - Validate rule consistency --- *Team-shared project memory - Last updated: ${new Date().toISOString()}*` return content } /** * Generate Claude Code personal preferences import file * @returns {string} Personal preferences import content */ generateClaudePersonalPrefsImport() { return `# Personal Preferences Import ## Import Personal Settings @~/.claude/CLAUDE.md ## Project-Specific Overrides - Personal preferences are imported automatically - Project-specific settings take precedence - Use this file to override global settings for this project --- *Personal preferences import - Loads from ~/.claude/CLAUDE.md*` } generateClaudeTechMemory(techRules, _projectContext) { const projectName = path.basename(this.projectPath) let content = `# ${projectName} - Technology Patterns ## Technology-Specific Guidelines This file contains technology and framework-specific patterns for this project. ` // Group rules by framework/technology const rulesByTech = this.groupRulesByTechnology(techRules) for (const [tech, rules] of Object.entries(rulesByTech)) { content += `## ${tech}\n\n` for (const rule of rules) { const cleanContent = this.stripFrontmatter(rule.content) content += this.extractKeySections(cleanContent, [ 'patterns', 'best practices', 'conventions', ]) content += '\n' } content += '\n' } content += `--- *Generated by VDK CLI - Technology patterns for ${projectName}*` return content } generateClaudeIntegrationMemory(projectContext) { const projectName = path.basename(this.projectPath) const techStack = projectContext.techStack || {} return `# ${projectName} - Integration Context ## Technology Stack Integration ### Detected Stack - **Languages**: ${techStack.primaryLanguages?.join(', ') || 'Not detected'} - **Frameworks**: ${techStack.frameworks?.join(', ') || 'Not detected'} - **Libraries**: ${techStack.libraries?.join(', ') || 'Not detected'} - **Build Tools**: ${techStack.buildTools?.join(', ') || 'Not detected'} ### Integration Patterns - VDK CLI manages AI assistant rules across multiple platforms - Claude Code provides memory persistence and slash commands - Project rules are automatically synchronized with team preferences - Memory hierarchy: Project → Local → User preferences ### Available Commands - \`/project:analyze\` - Analyze project structure and patterns - \`/project:refresh\` - Update VDK rules based on project changes - \`/project:validate\` - Validate rule consistency --- *Integration context maintained by VDK CLI*` } generateClaudeActiveContext(_projectContext) { return `# Active Development Context ## Current Session - **Project**: ${path.basename(this.projectPath)} - **Last Analysis**: ${new Date().toISOString()} - **VDK Version**: Active ## Quick Reference - Project memory files are active - Technology patterns loaded - Integration context available ## Session Notes *This file can be used for temporary notes and context that shouldn't be shared with the team* --- *Local memory file - not version controlled*` } generateClaudeSlashCommand(rule, _projectContext) { const commandName = this.getCommandName(rule.frontmatter?.description) const cleanContent = this.stripFrontmatter(rule.content) return `# ${commandName} ${cleanContent} Arguments: $ARGUMENTS --- *Simple contextual aid with $ARGUMENTS placeholder support*` } // === Cursor MDC Generators === (Duplicates removed - using methods from line 358) // === Windsurf Memory Generators === generateWindsurfGlobalMemory(globalRules, _projectContext) { let content = `# Global Development Standards - VDK Enhanced ## Organization Standards ` for (const rule of globalRules) { const cleanContent = this.stripFrontmatter(rule.content) const xmlSection = this.convertToWindsurfXML(rule, cleanContent) content += `${xmlSection}\n\n` } content += '*Organization-wide standards - Applied across all Windsurf workspaces*' return content } generateWindsurfRule(rule, _projectContext) { const cleanContent = this.stripFrontmatter(rule.content) const _title = this.extractTitle(cleanContent) || rule.frontmatter?.description || 'Rule' const category = rule.frontmatter?.category || 'general' const xmlTag = this.getWindsurfXMLTag(category) return `<${xmlTag}> ${cleanContent} </${xmlTag}>` } convertToWindsurfXML(rule, content) { const category = rule.frontmatter?.category || 'general' const xmlTag = this.getWindsurfXMLTag(category) const keyContent = this.extractKeySections(content, [ 'principles', 'guidelines', 'patterns', 'best practices', ]) return `<${xmlTag}> ${this.formatForWindsurf(keyContent)} </${xmlTag}>` } getWindsurfXMLTag(category) { const tagMap = { core: 'development-standards', language: 'language-standards', technology: 'technology-guidelines', framework: 'technology-guidelines', testing: 'testing-patterns', task: 'task-workflow', assistant: 'ai-assistance', } return tagMap[category] || 'rule' } formatForWindsurf(content) { // Format content for Windsurf XML sections with proper indentation return content .split('\n') .map((line) => (line.trim() ? `- ${line.trim()}` : '')) .filter((line) => line) .join('\n') } getWindsurfFileName(rule) { const framework = rule.frontmatter?.framework || rule.frontmatter?.category || 'general' return `${framework.toLowerCase().replace(/[^a-z0-9]/g, '-')}.md` } /** * Determine Windsurf activation type based on rule characteristics * @param {Object} rule - Rule object * @returns {string} Activation type */ getWindsurfActivationType(rule) { // Manual - Via @mention in Cascade if (rule.frontmatter?.category === 'task') { return 'manual' } // Always On - Automatically applied if (rule.frontmatter?.alwaysApply === true) { return 'always-on' } // Glob - Based on file pattern matching if (rule.frontmatter?.globs && rule.frontmatter?.globs.length > 0) { return 'glob' } // Model Decision - Based on natural language description if (rule.frontmatter?.description) { return 'model-decision' } return 'model-decision' // Default } /** * Generate consolidated Windsurf rules for single-file deployment * @param {Array} rules - Workspace rules * @param {Object} projectContext - Project context * @returns {string} Consolidated content */ generateConsolidatedWindsurfRules(rules, _projectContext) { const projectName = path.basename(this.projectPath) let content = `# ${projectName} - Windsurf Project Rules ## Development Standards ` // Group rules by category for better organization const rulesByCategory = this.groupRulesByCategory(rules) for (const [category, categoryRules] of Object.entries(rulesByCategory)) { const xmlTag = this.getWindsurfXMLTag(category) content += `<${xmlTag}>\n` for (const rule of categoryRules) { const cleanContent = this.stripFrontmatter(rule.content) const keyPoints = this.extractKeyPoints(cleanContent) content += `${keyPoints.map((point) => `- ${point}`).join('\n')}\n` } content += `</${xmlTag}>\n\n` } content += '*Consolidated project rules - Model decision activation*' return content } /** * Group rules by category for organized display * @param {Array} rules - Rules to group * @returns {Object} Rules grouped by category */ groupRulesByCategory(rules) { const groups = {} for (const rule of rules) { const category = rule.frontmatter?.category || 'general' if (!groups[category]) { groups[category] = [] } groups[category].push(rule) } return groups } /** * Truncate content to character limit (HELPER for GitHub Copilot) * @param {string} content - Content to truncate * @param {number} limit - Character limit * @returns {string} Truncated content */ truncateToCharLimit(content, limit) { if (content.length <= limit) { return content } // Try to truncate at a sentence boundary const truncated = content.substring(0, limit - 3) const lastSentence = truncated.lastIndexOf('.') if (lastSentence > limit * 0.7) { // If we can keep at least 70% and end at sentence return truncated.substring(0, lastSentence + 1) } return `${truncated}...` } // === GitHub Copilot Generators === prioritizeRulesForCopilot(rules) { // Prioritize rules based on their effectiveness for code review return rules .filter((rule) => rule.frontmatter?.category !== 'assistant') // Skip assistant-specific rules .sort((a, b) => { const aPriority = this.getCopilotPriority(a) const bPriority = this.getCopilotPriority(b) return bPriority - aPriority }) } getCopilotPriority(rule) { // Assign priority scores for GitHub Copilot effectiveness const category = rule.frontmatter?.category || 'general' const hasGlobs = rule.frontmatter?.globs && rule.frontmatter?.globs.length > 0 let score = 0 if (category === 'core') { score += 10 } if (category === 'language' || category === 'technology') { score += 8 } if (category === 'stack') { score += 6 } if (category === 'task') { score += 4 } if (hasGlobs) { score += 3 } if (rule.frontmatter?.alwaysApply === true) { score += 2 } return score } generateCopilotGuideline(rule, _projectContext) { const title = this.extractTitle(this.stripFrontmatter(rule.content)) || rule.frontmatter?.description const description = this.generateCopilotDescription(rule) const paths = rule.frontmatter?.globs || [] return { title: title.substring(0, 100), // Reasonable title length description: description.substring(0, 600), // GitHub Copilot limit paths, } } generateCopilotDescription(rule) { const cleanContent = this.stripFrontmatter(rule.content) const keyPoints = this.extractKeyPoints(cleanContent) // Create concise description focused on what Copilot should look for let description = rule.frontmatter?.description if (keyPoints.length > 0) { description += `. Key points: ${keyPoints.slice(0, 3).join('. ')}` } return description } generateCopilotDocumentation(guidelines, _projectContext) { const projectName = path.basename(this.projectPath) let content = `# GitHub Copilot Guidelines for ${projectName} ## Overview This directory contains GitHub Copilot Enterprise coding guidelines generated by VDK CLI. ## Active Guidelines ` guidelines.forEach((guideline, index) => { content += `### ${index + 1}. ${guideline.title} **Description**: ${guideline.description} ` if (guideline.paths.length > 0) { content += `**File Patterns**: ${guideline.paths.map((p) => `\`${p}\``).join(', ')} ` } }) content += `## Setup Instructions 1. Navigate to your repository on GitHub 2. Go to Settings → Code & automation → Copilot → Code review 3. Configure the guidelines listed above 4. Each guideline should be added with its title and description ## VDK Integration - Guidelines updated automatically with \`vdk sync\` - Project patterns detected and incorporated - Maximum 6 guidelines per repository (GitHub Copilot limit) --- *Generated by VDK CLI*` return content } // === Utility Methods === stripFrontmatter(content) { if (content.startsWith('---')) { const parts = content.split('---') return parts.slice(2).join('---').trim() } return content } extractTitle(content) { const lines = content.split('\n') const titleLine = lines.find((line) => line.startsWith('# ')) return titleLine ? titleLine.replace('# ', '').trim() : 'Untitled' } extractKeySections(content, sectionTypes) { const lines = content.split('\n') let result = '' let inTargetSection = false for (const line of lines) { if (line.startsWith('## ')) { const sectionTitle = line.toLowerCase() inTargetSection = sectionTypes.some((type) => sectionTitle.includes(type)) } if (inTargetSection && !line.startsWith('#')) { result += `${line}\n` } } return result.trim() } extractKeyPoints(content) { const lines = content.split('\n') return lines .filter((line) => line.trim().startsWith('-') || line.trim().startsWith('*')) .map((line) => line.replace(/^[-*]\s*/, '').trim()) .filter((line) => line.length > 10) // Filter out very short points } groupRulesByTechnology(rules) { const groups = {} for (const rule of rules) { const tech = rule.frontmatter?.framework || rule.frontmatter?.category || 'General' if (!groups[tech]) { groups[tech] = [] } groups[tech].push(rule) } return groups } getCommandName(description) { return description .replace(/[^a-zA-Z0-9\s]/g, '') .split(' ') .slice(0, 3) .join(' ') .trim() } }