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,210 lines (994 loc) 38.2 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 chalk from 'chalk'; import fs from 'fs/promises'; import os from 'os'; import path from 'path'; export class RuleAdapter { constructor(options = {}) { this.verbose = options.verbose || false; 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: Infinity, // No specific limit total: Infinity, }, claude: { perFile: 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 === 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: 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: 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: 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 || false} --- ${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: 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: 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(); } }