customize installation folder for the bmad content

This commit is contained in:
Brian Madison
2025-11-08 15:19:19 -06:00
parent 1728acfb0f
commit fd2521ec69
6 changed files with 242 additions and 58 deletions

View File

@@ -199,6 +199,8 @@ class Detector {
/**
* Detect legacy BMAD v4 footprints (case-sensitive path checks)
* V4 used .bmad-method as default folder name
* V6+ uses configurable folder names and ALWAYS has _cfg/manifest.yaml with installation.version
* @param {string} projectDir - Project directory to check
* @returns {{ hasLegacyV4: boolean, offenders: string[] }}
*/
@@ -223,18 +225,62 @@ class Detector {
return true;
};
// Helper: check if a directory is a V6+ installation
const isV6Installation = async (dirPath) => {
const manifestPath = path.join(dirPath, '_cfg', 'manifest.yaml');
if (!(await fs.pathExists(manifestPath))) {
return false;
}
try {
const yaml = require('js-yaml');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = yaml.load(manifestContent);
// V6+ manifest has installation.version
return manifest && manifest.installation && manifest.installation.version;
} catch {
return false;
}
};
const offenders = [];
// Find all directories starting with .bmad, bmad, or Bmad
// Strategy:
// 1. First scan for ANY V6+ installation (_cfg/manifest.yaml)
// 2. If V6+ found → don't flag anything (user is already on V6+)
// 3. If NO V6+ found → flag folders with "bmad" in name as potential V4 legacy
let hasV6Installation = false;
const potentialV4Folders = [];
try {
const entries = await fs.readdir(projectDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const name = entry.name;
// Match .bmad*, bmad* (lowercase), or Bmad* (capital B)
// BUT exclude 'bmad' exactly (that's the new v6 installation directory)
if ((name.startsWith('.bmad') || name.startsWith('bmad') || name.startsWith('Bmad')) && name !== 'bmad') {
offenders.push(path.join(projectDir, entry.name));
const fullPath = path.join(projectDir, entry.name);
// Check if directory is empty (skip empty leftover folders)
const dirContents = await fs.readdir(fullPath);
if (dirContents.length === 0) {
continue; // Skip empty folders
}
// Check if it's a V6+ installation by looking for _cfg/manifest.yaml
// This works for ANY folder name (not just bmad-prefixed)
const isV6 = await isV6Installation(fullPath);
if (isV6) {
// Found a V6+ installation - user is already on V6+
hasV6Installation = true;
// Don't break - continue scanning to be thorough
} else {
// Not V6+, check if folder name contains "bmad" (case insensitive)
const nameLower = name.toLowerCase();
if (nameLower.includes('bmad')) {
// Potential V4 legacy folder
potentialV4Folders.push(fullPath);
}
}
}
}
@@ -242,8 +288,15 @@ class Detector {
// Ignore errors reading directory
}
// Only flag V4 folders if NO V6+ installation was found
if (!hasV6Installation && potentialV4Folders.length > 0) {
offenders.push(...potentialV4Folders);
}
// Check inside various IDE command folders for legacy bmad folders
// List of IDE config folders that might have commands directories
// V4 used folders like 'bmad-method' or custom names in IDE commands
// V6+ uses 'bmad' in IDE commands (hardcoded in IDE handlers)
// Legacy V4 IDE command folders won't have a corresponding V6+ installation
const ideConfigFolders = ['.opencode', '.claude', '.crush', '.continue', '.cursor', '.windsurf', '.cline', '.roo-cline'];
for (const ideFolder of ideConfigFolders) {
@@ -255,7 +308,9 @@ class Detector {
for (const entry of commandEntries) {
if (entry.isDirectory()) {
const name = entry.name;
// Find bmad-related folders (excluding exact 'bmad' if it exists)
// V4 used 'bmad-method' or similar in IDE commands folders
// V6+ uses 'bmad' (hardcoded)
// So anything that's NOT 'bmad' but starts with bmad/Bmad is likely V4
if ((name.startsWith('bmad') || name.startsWith('Bmad') || name === 'BMad') && name !== 'bmad') {
offenders.push(path.join(commandsPath, entry.name));
}

View File

@@ -35,7 +35,7 @@ class Installer {
/**
* Find the bmad installation directory in a project
* Checks for custom bmad_folder names (.bmad, .bmad-custom, etc.) and falls back to 'bmad'
* V6+ installations can use ANY folder name but ALWAYS have _cfg/manifest.yaml
* @param {string} projectDir - Project directory
* @returns {Promise<string>} Path to bmad directory
*/
@@ -46,34 +46,25 @@ class Installer {
return path.join(projectDir, 'bmad');
}
// First, try to read from existing core config to get the bmad_folder value
const possibleDirs = ['.bmad', 'bmad']; // Common defaults
// Check if any of these exist
for (const dir of possibleDirs) {
const fullPath = path.join(projectDir, dir);
if (await fs.pathExists(fullPath)) {
// Try to read the config to confirm this is a bmad installation
const configPath = path.join(fullPath, 'core', 'config.yaml');
if (await fs.pathExists(configPath)) {
return fullPath;
// V6+ strategy: Look for ANY directory with _cfg/manifest.yaml
// This is the definitive marker of a V6+ installation
try {
const entries = await fs.readdir(projectDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const manifestPath = path.join(projectDir, entry.name, '_cfg', 'manifest.yaml');
if (await fs.pathExists(manifestPath)) {
// Found a V6+ installation
return path.join(projectDir, entry.name);
}
}
}
} catch {
// Ignore errors, fall through to default
}
// If nothing found, check for any directory that contains core/config.yaml
// This handles custom bmad_folder names
const entries = await fs.readdir(projectDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const configPath = path.join(projectDir, entry.name, 'core', 'config.yaml');
if (await fs.pathExists(configPath)) {
return path.join(projectDir, entry.name);
}
}
}
// Default fallback
// No V6+ installation found, return default
// This will be used for new installations
return path.join(projectDir, 'bmad');
}
@@ -249,12 +240,10 @@ class Installer {
// Display welcome message
CLIUtils.displaySection('BMAD™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
// Preflight: Handle legacy BMAD v4 footprints before any prompts/writes
// Note: Legacy V4 detection now happens earlier in UI.promptInstall()
// before any config collection, so we don't need to check again here
const projectDir = path.resolve(config.directory);
const legacyV4 = await this.detector.detectLegacyV4(projectDir);
if (legacyV4.hasLegacyV4) {
await this.handleLegacyV4Migration(projectDir, legacyV4);
}
// If core config was pre-collected (from interactive mode), use it
if (config.coreConfig) {
@@ -280,6 +269,10 @@ class Installer {
const bmadFolderName = moduleConfigs.core && moduleConfigs.core.bmad_folder ? moduleConfigs.core.bmad_folder : 'bmad';
this.bmadFolderName = bmadFolderName; // Store for use in other methods
// Set bmad folder name on module manager and IDE manager for placeholder replacement
this.moduleManager.setBmadFolderName(bmadFolderName);
this.ideManager.setBmadFolderName(bmadFolderName);
// Tool selection will be collected after we determine if it's a reinstall/update/new install
const spinner = ora('Preparing installation...').start();