UNPKG

@cloudkinetix/bmad-enhanced

Version:

Cloud-Kinetix enhanced fork of BMAD-METHOD - Breakthrough Method of Agile AI-driven Development with robust versioning and unified validation.

467 lines (412 loc) 13.6 kB
/** * UpstreamAnalyzer - Discovers what upstream BMAD installer actually created * * This class analyzes the installation directory after upstream runs to: * - Detect configured IDEs and their patterns * - Extract file formats and naming conventions * - Identify installed expansion packs * - Provide capability map for enhancement */ const fs = require('fs-extra'); const path = require('path'); class UpstreamAnalyzer { constructor(targetDir, configLoader = null) { this.targetDir = targetDir; this.configLoader = configLoader; this.analysisCache = null; } /** * Analyze the upstream installation and return capability map */ async analyzeInstallation() { if (this.analysisCache) { return this.analysisCache; } const analysis = { coreInstalled: await this.detectBmadCore(), configuredIDEs: await this.detectConfiguredIDEs(), installedExpansionPacks: await this.detectInstalledExpansionPacks(), upstreamCapabilities: await this.getUpstreamCapabilities() }; this.analysisCache = analysis; return analysis; } /** * Detect if BMAD core was installed */ async detectBmadCore() { const bmadCorePath = path.join(this.targetDir, '.bmad-core'); if (!await fs.pathExists(bmadCorePath)) { return { installed: false }; } const agentsPath = path.join(bmadCorePath, 'agents'); const agents = []; if (await fs.pathExists(agentsPath)) { const agentFiles = await fs.readdir(agentsPath); for (const file of agentFiles) { if (file.endsWith('.md')) { agents.push(file.replace('.md', '')); } } } return { installed: true, path: bmadCorePath, agents: agents, agentCount: agents.length }; } /** * Detect which IDEs were configured by upstream */ async detectConfiguredIDEs() { const configuredIDEs = []; const upstreamConfig = await this.getUpstreamCapabilities(); // Check each IDE from upstream config for (const [ideKey, ideConfig] of Object.entries(upstreamConfig.ideConfigurations || {})) { const detection = await this.detectIDEConfiguration(ideKey, ideConfig); if (detection.configured) { configuredIDEs.push({ id: ideKey, name: ideConfig.name, ...detection }); } } // Also detect any directories that look like IDE configs but aren't in upstream config const additionalIDEs = await this.detectUnknownIDEConfigs(); configuredIDEs.push(...additionalIDEs); return configuredIDEs; } /** * Detect specific IDE configuration patterns */ async detectIDEConfiguration(ideKey, ideConfig) { let targetPath, filePattern; // Handle special cases if (ideKey === 'roo') { // Roo uses a single .roomodes file targetPath = path.join(this.targetDir, '.roomodes'); const exists = await fs.pathExists(targetPath); return { configured: exists, type: 'single-file', path: exists ? targetPath : null, format: 'yaml', pattern: null }; } if (ideKey === 'github-copilot') { // GitHub Copilot uses .github/chatmodes and .vscode/settings.json const chatmodesPath = path.join(this.targetDir, '.github', 'chatmodes'); const vscodePath = path.join(this.targetDir, '.vscode', 'settings.json'); const chatmodesExists = await fs.pathExists(chatmodesPath); const vscodeExists = await fs.pathExists(vscodePath); return { configured: chatmodesExists || vscodeExists, type: 'multi-component', paths: { chatmodes: chatmodesExists ? chatmodesPath : null, vscode: vscodeExists ? vscodePath : null }, format: 'chatmode', pattern: '.chatmode.md' }; } if (ideKey === 'gemini') { // Gemini uses a single GEMINI.md file in .gemini/bmad-method/ directory const geminiFile = path.join(this.targetDir, '.gemini', 'bmad-method', 'GEMINI.md'); const exists = await fs.pathExists(geminiFile); return { configured: exists, type: 'single-file', path: exists ? geminiFile : null, format: '.md', pattern: null }; } // Standard IDE with rule-dir if (ideConfig['rule-dir']) { targetPath = path.join(this.targetDir, ideConfig['rule-dir']); const exists = await fs.pathExists(targetPath); if (!exists) { return { configured: false }; } // Analyze the structure and files const structure = await this.analyzeDirectoryStructure(targetPath); return { configured: true, type: ideConfig.format || 'multi-file', path: targetPath, format: ideConfig['command-suffix'] || '.md', pattern: structure.filePattern, structure: structure }; } return { configured: false }; } /** * Detect IDE configurations not in upstream config (future IDEs) */ async detectUnknownIDEConfigs() { const unknownIDEs = []; const knownPrefixes = ['.bmad-', '.ck-']; // Get the list of known IDE keys from upstream config const upstreamConfig = await this.getUpstreamCapabilities(); const knownIDEDirs = new Set(); // Build a set of known IDE directory names for (const [ideKey, ideConfig] of Object.entries(upstreamConfig.ideConfigurations || {})) { if (ideConfig['rule-dir']) { // Extract the base directory name (e.g., '.cursor' from '.cursor/rules/') const baseDir = ideConfig['rule-dir'].split('/')[0]; knownIDEDirs.add(baseDir); } if (ideConfig.file) { knownIDEDirs.add(ideConfig.file); } // Special cases if (ideKey === 'github-copilot') { knownIDEDirs.add('.github'); knownIDEDirs.add('.vscode'); } } try { const entries = await fs.readdir(this.targetDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name.startsWith('.')) { // Skip known non-IDE directories if (knownPrefixes.some(prefix => entry.name.startsWith(prefix))) { continue; } // Skip directories that are known IDEs if (knownIDEDirs.has(entry.name)) { continue; } // Check if this looks like an IDE configuration const structure = await this.analyzeDirectoryStructure(path.join(this.targetDir, entry.name)); if (structure.looksLikeIDE) { unknownIDEs.push({ id: entry.name.replace(/^\./, ''), name: `Unknown IDE (${entry.name})`, configured: true, type: 'unknown', path: path.join(this.targetDir, entry.name), structure: structure }); } } } } catch (error) { // Ignore errors in unknown IDE detection } return unknownIDEs; } /** * Analyze directory structure to understand patterns */ async analyzeDirectoryStructure(dirPath) { if (!await fs.pathExists(dirPath)) { return { exists: false }; } try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const files = entries.filter(e => e.isFile()).map(e => e.name); const dirs = entries.filter(e => e.isDirectory()).map(e => e.name); // Analyze file patterns const extensions = [...new Set(files.map(f => path.extname(f)).filter(Boolean))]; const hasMarkdown = files.some(f => f.endsWith('.md')); const hasMdc = files.some(f => f.endsWith('.mdc')); const hasNumberedFiles = files.some(f => /^\d+-.+/.test(f)); // Guess the file pattern let filePattern = '.md'; if (hasMdc) filePattern = '.mdc'; if (hasNumberedFiles) filePattern = 'numbered'; // Check if it looks like an IDE configuration const looksLikeIDE = (hasMarkdown || hasMdc) && files.length > 0; return { exists: true, files: files, directories: dirs, fileCount: files.length, dirCount: dirs.length, extensions: extensions, filePattern: filePattern, hasNumberedFiles: hasNumberedFiles, looksLikeIDE: looksLikeIDE }; } catch (error) { return { exists: false, error: error.message }; } } /** * Detect installed expansion packs */ async detectInstalledExpansionPacks() { const packs = []; try { const entries = await fs.readdir(this.targetDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && (entry.name.startsWith('.bmad-') || entry.name.startsWith('.ck-'))) { const packInfo = await this.analyzeExpansionPack(entry.name); if (packInfo) { packs.push(packInfo); } } } } catch (error) { // Ignore errors } return { total: packs.length, upstream: packs.filter(p => p.source === 'upstream'), ck: packs.filter(p => p.source === 'ck'), packs: packs }; } /** * Analyze a specific expansion pack */ async analyzeExpansionPack(packName) { const packPath = path.join(this.targetDir, packName); const agentsPath = path.join(packPath, 'agents'); let agents = []; if (await fs.pathExists(agentsPath)) { const agentFiles = await fs.readdir(agentsPath); agents = agentFiles.filter(f => f.endsWith('.md')).map(f => f.replace('.md', '')); } return { name: packName, cleanName: packName.replace(/^\./, ''), path: packPath, source: packName.startsWith('.ck-') ? 'ck' : 'upstream', agents: agents, agentCount: agents.length }; } /** * Get upstream capabilities from config */ async getUpstreamCapabilities() { if (!this.configLoader) { try { // Try to find the config loader relative to this file const ConfigLoader = require('../../tools/installer/lib/config-loader'); this.configLoader = new ConfigLoader(); } catch (error) { // If that fails, fall back to a default IDE configuration return this.getDefaultIDEConfigurations(); } } try { const config = await this.configLoader.load(); return { ideConfigurations: config['ide-configurations'] || {}, installationOptions: config['installation-options'] || {} }; } catch (error) { // If config loading fails, fall back to default return this.getDefaultIDEConfigurations(); } } /** * Get default IDE configurations for fallback */ getDefaultIDEConfigurations() { return { ideConfigurations: { 'cursor': { name: 'Cursor', 'rule-dir': '.cursor/rules/', format: 'multi-file', 'command-suffix': '.mdc' }, 'claude-code': { name: 'Claude Code', 'rule-dir': '.claude/commands/BMad/', format: 'multi-file', 'command-suffix': '.md' }, 'windsurf': { name: 'Windsurf', 'rule-dir': '.windsurf/rules/', format: 'multi-file', 'command-suffix': '.md' }, 'trae': { name: 'Trae', 'rule-dir': '.trae/rules/', format: 'multi-file', 'command-suffix': '.md' }, 'roo': { name: 'Roo Code', format: 'custom-modes', file: '.roomodes' }, 'cline': { name: 'Cline', 'rule-dir': '.clinerules/', format: 'multi-file', 'command-suffix': '.md' }, 'gemini': { name: 'Gemini CLI', 'rule-dir': '.gemini/bmad-method/', format: 'single-file', 'command-suffix': '.md' }, 'github-copilot': { name: 'Github Copilot', 'rule-dir': '.github/chatmodes/', format: 'multi-file', 'command-suffix': '.md' } }, installationOptions: {} }; } /** * Get enhancement opportunities for CK expansion packs */ async getEnhancementOpportunities(ckAgents) { const analysis = await this.analyzeInstallation(); const opportunities = []; for (const ide of analysis.configuredIDEs) { // Check if this IDE needs CK agents added const needsEnhancement = await this.ideNeedsEnhancement(ide, ckAgents); if (needsEnhancement) { opportunities.push({ ide: ide, agents: ckAgents, enhancementType: this.determineEnhancementType(ide) }); } } return opportunities; } /** * Check if IDE needs CK agent enhancement */ async ideNeedsEnhancement(ide, ckAgents) { // For now, assume all configured IDEs need CK agents // In the future, we could check if CK agents are already present return true; } /** * Determine the type of enhancement needed */ determineEnhancementType(ide) { if (ide.id === 'roo') return 'yaml-append'; if (ide.id === 'github-copilot') return 'chatmode-creation'; if (ide.id === 'cline' && ide.structure?.hasNumberedFiles) return 'numbered-files'; if (ide.structure?.filePattern === '.mdc') return 'mdc-files'; return 'standard-files'; } /** * Clear analysis cache (for testing) */ clearCache() { this.analysisCache = null; } } module.exports = UpstreamAnalyzer;