@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
JavaScript
/**
* 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;