customize installation folder for the bmad content
This commit is contained in:
parent
1728acfb0f
commit
fd2521ec69
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing found, check for any directory that contains core/config.yaml
|
||||
// This handles custom bmad_folder names
|
||||
// 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 configPath = path.join(projectDir, entry.name, 'core', 'config.yaml');
|
||||
if (await fs.pathExists(configPath)) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ class BaseIdeSetup {
|
|||
this.configFile = null; // Override in subclasses when detection is file-based
|
||||
this.detectionPaths = []; // Additional paths that indicate the IDE is configured
|
||||
this.xmlHandler = new XmlHandler();
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bmad folder name for placeholder replacement
|
||||
* @param {string} bmadFolderName - The bmad folder name
|
||||
*/
|
||||
setBmadFolderName(bmadFolderName) {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -489,24 +498,53 @@ class BaseIdeSetup {
|
|||
}
|
||||
|
||||
/**
|
||||
* Write file with content
|
||||
* Write file with content (replaces {bmad_folder} placeholder)
|
||||
* @param {string} filePath - File path
|
||||
* @param {string} content - File content
|
||||
*/
|
||||
async writeFile(filePath, content) {
|
||||
// Replace {bmad_folder} placeholder if present
|
||||
if (typeof content === 'string' && content.includes('{bmad_folder}')) {
|
||||
content = content.replaceAll('{bmad_folder}', this.bmadFolderName);
|
||||
}
|
||||
await this.ensureDir(path.dirname(filePath));
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy file from source to destination
|
||||
* Copy file from source to destination (replaces {bmad_folder} placeholder in text files)
|
||||
* @param {string} source - Source file path
|
||||
* @param {string} dest - Destination file path
|
||||
*/
|
||||
async copyFile(source, dest) {
|
||||
// List of text file extensions that should have placeholder replacement
|
||||
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
|
||||
const ext = path.extname(source).toLowerCase();
|
||||
|
||||
await this.ensureDir(path.dirname(dest));
|
||||
|
||||
// Check if this is a text file that might contain placeholders
|
||||
if (textExtensions.includes(ext)) {
|
||||
try {
|
||||
// Read the file content
|
||||
let content = await fs.readFile(source, 'utf8');
|
||||
|
||||
// Replace {bmad_folder} placeholder with actual folder name
|
||||
if (content.includes('{bmad_folder}')) {
|
||||
content = content.replaceAll('{bmad_folder}', this.bmadFolderName);
|
||||
}
|
||||
|
||||
// Write to dest with replaced content
|
||||
await fs.writeFile(dest, content, 'utf8');
|
||||
} catch {
|
||||
// If reading as text fails, fall back to regular copy
|
||||
await fs.copy(source, dest, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
// Binary file or other file type - just copy directly
|
||||
await fs.copy(source, dest, { overwrite: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path exists
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ class IdeManager {
|
|||
constructor() {
|
||||
this.handlers = new Map();
|
||||
this.loadHandlers();
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bmad folder name for all IDE handlers
|
||||
* @param {string} bmadFolderName - The bmad folder name
|
||||
*/
|
||||
setBmadFolderName(bmadFolderName) {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
// Update all loaded handlers
|
||||
for (const handler of this.handlers.values()) {
|
||||
if (typeof handler.setBmadFolderName === 'function') {
|
||||
handler.setBmadFolderName(bmadFolderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -26,6 +26,70 @@ class ModuleManager {
|
|||
// Path to source modules directory
|
||||
this.modulesSourcePath = getSourcePath('modules');
|
||||
this.xmlHandler = new XmlHandler();
|
||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bmad folder name for placeholder replacement
|
||||
* @param {string} bmadFolderName - The bmad folder name
|
||||
*/
|
||||
setBmadFolderName(bmadFolderName) {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file and replace {bmad_folder} placeholder with actual folder name
|
||||
* @param {string} sourcePath - Source file path
|
||||
* @param {string} targetPath - Target file path
|
||||
*/
|
||||
async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
|
||||
// List of text file extensions that should have placeholder replacement
|
||||
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
|
||||
const ext = path.extname(sourcePath).toLowerCase();
|
||||
|
||||
// Check if this is a text file that might contain placeholders
|
||||
if (textExtensions.includes(ext)) {
|
||||
try {
|
||||
// Read the file content
|
||||
let content = await fs.readFile(sourcePath, 'utf8');
|
||||
|
||||
// Replace {bmad_folder} placeholder with actual folder name
|
||||
if (content.includes('{bmad_folder}')) {
|
||||
content = content.replaceAll('{bmad_folder}', this.bmadFolderName);
|
||||
}
|
||||
|
||||
// Write to target with replaced content
|
||||
await fs.ensureDir(path.dirname(targetPath));
|
||||
await fs.writeFile(targetPath, content, 'utf8');
|
||||
} catch {
|
||||
// If reading as text fails (might be binary despite extension), fall back to regular copy
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
}
|
||||
} else {
|
||||
// Binary file or other file type - just copy directly
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a directory recursively with placeholder replacement
|
||||
* @param {string} sourceDir - Source directory path
|
||||
* @param {string} targetDir - Target directory path
|
||||
*/
|
||||
async copyDirectoryWithPlaceholderReplacement(sourceDir, targetDir) {
|
||||
await fs.ensureDir(targetDir);
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(sourceDir, entry.name);
|
||||
const targetPath = path.join(targetDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await this.copyDirectoryWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
} else {
|
||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -311,9 +375,8 @@ class ModuleManager {
|
|||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
||||
} else {
|
||||
// Copy the file normally
|
||||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
||||
// Copy the file with placeholder replacement
|
||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
||||
}
|
||||
|
||||
// Track the file if callback provided
|
||||
|
|
@ -333,12 +396,16 @@ class ModuleManager {
|
|||
// Read the source YAML file
|
||||
let yamlContent = await fs.readFile(sourceFile, 'utf8');
|
||||
|
||||
// IMPORTANT: Replace {bmad_folder} BEFORE parsing YAML
|
||||
// Otherwise parsing will fail on the placeholder
|
||||
yamlContent = yamlContent.replaceAll('{bmad_folder}', this.bmadFolderName);
|
||||
|
||||
try {
|
||||
// First check if web_bundle exists by parsing
|
||||
const workflowConfig = yaml.load(yamlContent);
|
||||
|
||||
if (workflowConfig.web_bundle === undefined) {
|
||||
// No web_bundle section, just copy as-is
|
||||
// No web_bundle section, just write (placeholders already replaced above)
|
||||
await fs.writeFile(targetFile, yamlContent, 'utf8');
|
||||
return;
|
||||
}
|
||||
|
|
@ -400,6 +467,7 @@ class ModuleManager {
|
|||
// Clean up any double blank lines that might result
|
||||
const strippedYaml = newLines.join('\n').replaceAll(/\n\n\n+/g, '\n\n');
|
||||
|
||||
// Placeholders already replaced at the beginning of this function
|
||||
await fs.writeFile(targetFile, strippedYaml, 'utf8');
|
||||
} catch {
|
||||
// If anything fails, just copy the file as-is
|
||||
|
|
@ -488,8 +556,10 @@ class ModuleManager {
|
|||
const installWorkflowPath = item['workflow-install']; // Where to copy TO
|
||||
|
||||
// Parse SOURCE workflow path
|
||||
// Example: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
|
||||
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
|
||||
// Handle both {bmad_folder} placeholder and hardcoded 'bmad'
|
||||
// Example: {project-root}/{bmad_folder}/bmm/workflows/4-implementation/create-story/workflow.yaml
|
||||
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
|
||||
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:\{bmad_folder\}|bmad)\/([^/]+)\/workflows\/(.+)/);
|
||||
if (!sourceMatch) {
|
||||
console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
|
||||
continue;
|
||||
|
|
@ -498,8 +568,9 @@ class ModuleManager {
|
|||
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
|
||||
|
||||
// Parse INSTALL workflow path
|
||||
// Example: {project-root}/bmad/bmgd/workflows/4-production/create-story/workflow.yaml
|
||||
const installMatch = installWorkflowPath.match(/\{project-root\}\/bmad\/([^/]+)\/workflows\/(.+)/);
|
||||
// Handle both {bmad_folder} placeholder and hardcoded 'bmad'
|
||||
// Example: {project-root}/{bmad_folder}/bmgd/workflows/4-production/create-story/workflow.yaml
|
||||
const installMatch = installWorkflowPath.match(/\{project-root\}\/(?:\{bmad_folder\}|bmad)\/([^/]+)\/workflows\/(.+)/);
|
||||
if (!installMatch) {
|
||||
console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
|
||||
continue;
|
||||
|
|
@ -527,7 +598,8 @@ class ModuleManager {
|
|||
);
|
||||
|
||||
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
|
||||
await fs.copy(actualSourceWorkflowPath, actualDestWorkflowPath, { overwrite: true });
|
||||
// Copy the workflow directory recursively with placeholder replacement
|
||||
await this.copyDirectoryWithPlaceholderReplacement(actualSourceWorkflowPath, actualDestWorkflowPath);
|
||||
|
||||
// Update the workflow.yaml config_source reference
|
||||
const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml');
|
||||
|
|
@ -550,16 +622,17 @@ class ModuleManager {
|
|||
async updateWorkflowConfigSource(workflowYamlPath, newModuleName) {
|
||||
let yamlContent = await fs.readFile(workflowYamlPath, 'utf8');
|
||||
|
||||
// Replace config_source: "{project-root}/bmad/OLD_MODULE/config.yaml"
|
||||
// with config_source: "{project-root}/bmad/NEW_MODULE/config.yaml"
|
||||
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/bmad\/[^/]+\/config\.yaml["']?/g;
|
||||
const newConfigSource = `config_source: "{project-root}/bmad/${newModuleName}/config.yaml"`;
|
||||
// Replace config_source: "{project-root}/{bmad_folder}/OLD_MODULE/config.yaml"
|
||||
// with config_source: "{project-root}/{bmad_folder}/NEW_MODULE/config.yaml"
|
||||
// Note: At this point {bmad_folder} has already been replaced with actual folder name
|
||||
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/[^/]+\/[^/]+\/config\.yaml["']?/g;
|
||||
const newConfigSource = `config_source: "{project-root}/${this.bmadFolderName}/${newModuleName}/config.yaml"`;
|
||||
|
||||
const updatedYaml = yamlContent.replaceAll(configSourcePattern, newConfigSource);
|
||||
|
||||
if (updatedYaml !== yamlContent) {
|
||||
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
|
||||
console.log(chalk.dim(` Updated config_source to: bmad/${newModuleName}/config.yaml`));
|
||||
console.log(chalk.dim(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -664,9 +737,8 @@ class ModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Copy file
|
||||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
||||
// Copy file with placeholder replacement
|
||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,21 @@ class UI {
|
|||
|
||||
const confirmedDirectory = await this.getConfirmedDirectory();
|
||||
|
||||
// Preflight: Check for legacy BMAD v4 footprints immediately after getting directory
|
||||
const { Detector } = require('../installers/lib/core/detector');
|
||||
const { Installer } = require('../installers/lib/core/installer');
|
||||
const detector = new Detector();
|
||||
const installer = new Installer();
|
||||
const legacyV4 = await detector.detectLegacyV4(confirmedDirectory);
|
||||
if (legacyV4.hasLegacyV4) {
|
||||
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
|
||||
}
|
||||
|
||||
// Check if there's an existing BMAD installation
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const bmadDir = path.join(confirmedDirectory, 'bmad');
|
||||
// Use findBmadDir to detect any custom folder names (V6+)
|
||||
const bmadDir = await installer.findBmadDir(confirmedDirectory);
|
||||
const hasExistingInstall = await fs.pathExists(bmadDir);
|
||||
|
||||
// Track action type (only set if there's an existing installation)
|
||||
|
|
|
|||
Loading…
Reference in New Issue