refactor: replace module installer scripts with declarative directories config

Removes the security-risky _module-installer pattern (code execution at
install time) in favor of a declarative `directories` key in module.yaml.
The main installer now handles directory creation centrally based on this
config, eliminating per-module installer.js scripts and their CJS/ESM issues.

Changes:
- Delete src/bmm/_module-installer/installer.js
- Delete src/core/_module-installer/installer.js
- Add `directories` key to src/bmm/module.yaml
- Rename runModuleInstaller() -> createModuleDirectories()
- Remove _module-installer from ESLint overrides
- Remove _module-installer from file-ref validator skip dirs
This commit is contained in:
Brian Madison 2026-02-08 19:21:48 -06:00
parent 90ea3cbed7
commit c563cef0c2
10 changed files with 107 additions and 233 deletions

View File

@ -114,17 +114,6 @@ export default [
}, },
}, },
// Module installer scripts use CommonJS for compatibility
{
files: ['**/_module-installer/**/*.js'],
rules: {
// Allow CommonJS patterns for installer scripts
'unicorn/prefer-module': 'off',
'n/no-missing-require': 'off',
'n/no-unpublished-require': 'off',
},
},
// ESLint config file should not be checked for publish-related Node rules // ESLint config file should not be checked for publish-related Node rules
{ {
files: ['eslint.config.mjs'], files: ['eslint.config.mjs'],

View File

@ -1,48 +0,0 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
// Directories to create from config
const DIRECTORIES = ['output_folder', 'planning_artifacts', 'implementation_artifacts'];
/**
* BMM Module Installer
* Creates output directories configured in module config
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, logger } = options;
try {
logger.log(chalk.blue('🚀 Installing BMM Module...'));
// Create configured directories
for (const configKey of DIRECTORIES) {
const configValue = config[configKey];
if (!configValue) continue;
const dirPath = configValue.replace('{project-root}/', '');
const fullPath = path.join(projectRoot, dirPath);
if (!(await fs.pathExists(fullPath))) {
const dirName = configKey.replace('_', ' ');
logger.log(chalk.yellow(`Creating ${dirName} directory: ${dirPath}`));
await fs.ensureDir(fullPath);
}
}
logger.log(chalk.green('✓ BMM Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing BMM module: ${error.message}`));
return false;
}
}
module.exports = { install };

View File

@ -42,3 +42,9 @@ project_knowledge: # Artifacts from research, document-project output, other lon
prompt: "Where should long-term project knowledge be stored? (docs, research, references)" prompt: "Where should long-term project knowledge be stored? (docs, research, references)"
default: "docs" default: "docs"
result: "{project-root}/{value}" result: "{project-root}/{value}"
# Directories to create during installation (declarative, no code execution)
directories:
- "{planning_artifacts}"
- "{implementation_artifacts}"
- "{project_knowledge}"

View File

@ -1,60 +0,0 @@
const chalk = require('chalk');
/**
* Core Module Installer
* Standard module installer function that executes after IDE installations
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, installedIDEs, logger } = options;
try {
logger.log(chalk.blue('🏗️ Installing Core Module...'));
// Core agent configs are created by the main installer's createAgentConfigs method
// No need to create them here - they'll be handled along with all other agents
// Handle IDE-specific configurations if needed
if (installedIDEs && installedIDEs.length > 0) {
logger.log(chalk.cyan(`Configuring Core for IDEs: ${installedIDEs.join(', ')}`));
// Add any IDE-specific Core configurations here
for (const ide of installedIDEs) {
await configureForIDE(ide, projectRoot, config, logger);
}
}
logger.log(chalk.green('✓ Core Module installation complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error installing Core module: ${error.message}`));
return false;
}
}
/**
* Configure Core module for specific IDE
* @private
*/
async function configureForIDE(ide) {
// Add IDE-specific configurations here
switch (ide) {
case 'claude-code': {
// Claude Code specific Core configurations
break;
}
// Add more IDEs as needed
default: {
// No specific configuration needed
break;
}
}
}
module.exports = { install };

View File

@ -42,13 +42,12 @@ modules:
type: bmad-org type: bmad-org
npmPackage: bmad-method-test-architecture-enterprise npmPackage: bmad-method-test-architecture-enterprise
# TODO: Enable once fixes applied: # whiteport-design-system:
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
# whiteport-design-system: # module-definition: src/module.yaml
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion # code: wds
# module-definition: src/module.yaml # name: "Whiteport UX Design System"
# code: WDS # description: "UX design framework with Figma integration"
# name: "Whiteport UX Design System" # defaultSelected: false
# description: "UX design framework with Figma integration" # type: community
# defaultSelected: false # npmPackage: bmad-method-wds-expansion
# type: community

View File

@ -188,20 +188,18 @@ class ConfigCollector {
this.allAnswers = {}; this.allAnswers = {};
} }
// Load module's install config schema // Load module's config schema from module.yaml
// First, try the standard src/modules location // First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project // If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) { if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source // Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager'); const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager(); const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName); const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
@ -211,8 +209,6 @@ class ConfigCollector {
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else { } else {
// Check if this is a custom module with custom.yaml // Check if this is a custom module with custom.yaml
const { ModuleManager } = require('../modules/manager'); const { ModuleManager } = require('../modules/manager');
@ -221,9 +217,8 @@ class ConfigCollector {
if (moduleSourcePath) { if (moduleSourcePath) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml'); const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(moduleSourcePath, '_module-installer', 'custom.yaml');
if ((await fs.pathExists(rootCustomConfigPath)) || (await fs.pathExists(moduleInstallerCustomPath))) { if (await fs.pathExists(rootCustomConfigPath)) {
isCustomModule = true; isCustomModule = true;
// For custom modules, we don't have an install-config schema, so just use existing values // For custom modules, we don't have an install-config schema, so just use existing values
// The custom.yaml values will be loaded and merged during installation // The custom.yaml values will be loaded and merged during installation
@ -500,28 +495,24 @@ class ConfigCollector {
} }
// Load module's config // Load module's config
// First, check if we have a custom module path for this module // First, check if we have a custom module path for this module
let installerConfigPath = null;
let moduleConfigPath = null; let moduleConfigPath = null;
if (this.customModulePaths && this.customModulePaths.has(moduleName)) { if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName); const customPath = this.customModulePaths.get(moduleName);
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(customPath, 'module.yaml'); moduleConfigPath = path.join(customPath, 'module.yaml');
} else { } else {
// Try the standard src/modules location // Try the standard src/modules location
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml'); moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
} }
// If not found in src/modules or custom paths, search the project // If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) { if (!(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source // Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager'); const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager(); const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName); const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) { if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml'); moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
} }
} }
@ -529,8 +520,6 @@ class ConfigCollector {
let configPath = null; let configPath = null;
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else { } else {
// No config for this module // No config for this module
return; return;

View File

@ -1070,11 +1070,11 @@ class Installer {
warn: (msg) => console.warn(msg), // Always show warnings warn: (msg) => console.warn(msg), // Always show warnings
}; };
// Run core module installer if core was installed // Create directories for core module if core was installed
if (config.installCore || resolution.byModule.core) { if (config.installCore || resolution.byModule.core) {
spinner.message('Running core module installer...'); spinner.message('Creating core module directories...');
await this.moduleManager.runModuleInstaller('core', bmadDir, { await this.moduleManager.createModuleDirectories('core', bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {}, moduleConfig: moduleConfigs.core || {},
coreConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {},
@ -1083,13 +1083,13 @@ class Installer {
}); });
} }
// Run installers for user-selected modules // Create directories for user-selected modules
if (config.modules && config.modules.length > 0) { if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) { for (const moduleName of config.modules) {
spinner.message(`Running ${moduleName} module installer...`); spinner.message(`Creating ${moduleName} module directories...`);
// Pass installed IDEs and module config to module installer // Pass installed IDEs and module config to directory creator
await this.moduleManager.runModuleInstaller(moduleName, bmadDir, { await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {}, moduleConfig: moduleConfigs[moduleName] || {},
coreConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {},
@ -1904,8 +1904,8 @@ class Installer {
continue; continue;
} }
// Skip _module-installer directory - it's only needed at install time // Skip module.yaml at root - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') { if (file === 'module.yaml') {
continue; continue;
} }
@ -1958,10 +1958,6 @@ class Installer {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir); const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles); files.push(...subFiles);
} else { } else {

View File

@ -55,7 +55,7 @@ class CustomHandler {
// Found a custom.yaml file // Found a custom.yaml file
customPaths.push(fullPath); customPaths.push(fullPath);
} else if ( } else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory) entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
// Skip if it's in src/modules (those are standard modules) // Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules')) !fullPath.includes(path.join('src', 'modules'))
) { ) {

View File

@ -236,17 +236,11 @@ class ModuleManager {
async getModuleInfo(modulePath, defaultName, sourceDescription) { async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (module.yaml OR custom.yaml) // Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml'); const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml'); const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null; let configPath = null;
if (await fs.pathExists(moduleConfigPath)) { if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath; configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
} else if (await fs.pathExists(rootCustomConfigPath)) { } else if (await fs.pathExists(rootCustomConfigPath)) {
configPath = rootCustomConfigPath; configPath = rootCustomConfigPath;
} }
@ -268,7 +262,7 @@ class ModuleManager {
description: 'BMAD Module', description: 'BMAD Module',
version: '5.0.0', version: '5.0.0',
source: sourceDescription, source: sourceDescription,
isCustom: configPath === customConfigPath || configPath === rootCustomConfigPath || isCustomSource, isCustom: configPath === rootCustomConfigPath || isCustomSource,
}; };
// Read module config for metadata // Read module config for metadata
@ -541,7 +535,6 @@ class ModuleManager {
// Check if this is a custom module and read its custom.yaml values // Check if this is a custom module and read its custom.yaml values
let customConfig = null; let customConfig = null;
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml'); const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
const moduleInstallerCustomPath = path.join(sourcePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) { if (await fs.pathExists(rootCustomConfigPath)) {
try { try {
@ -550,13 +543,6 @@ class ModuleManager {
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
} }
} else if (await fs.pathExists(moduleInstallerCustomPath)) {
try {
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
customConfig = yaml.parse(customContent);
} catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
}
} }
// If this is a custom module, merge its values into the module config // If this is a custom module, merge its values into the module config
@ -585,9 +571,9 @@ class ModuleManager {
// Process agent files to inject activation block // Process agent files to inject activation block
await this.processAgentFiles(targetPath, moduleName); await this.processAgentFiles(targetPath, moduleName);
// Call module-specific installer if it exists (unless explicitly skipped) // Create directories declared in module.yaml (unless explicitly skipped)
if (!options.skipModuleInstaller) { if (!options.skipModuleInstaller) {
await this.runModuleInstaller(moduleName, bmadDir, options); await this.createModuleDirectories(moduleName, bmadDir, options);
} }
// Capture version info for manifest // Capture version info for manifest
@ -743,8 +729,8 @@ class ModuleManager {
continue; continue;
} }
// Skip _module-installer directory - it's only needed at install time // Skip module.yaml at root - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') { if (file === 'module.yaml') {
continue; continue;
} }
@ -1259,80 +1245,101 @@ class ModuleManager {
} }
/** /**
* Run module-specific installer if it exists * Create directories declared in module.yaml's `directories` key
* This replaces the security-risky module installer pattern with declarative config
* @param {string} moduleName - Name of the module * @param {string} moduleName - Name of the module
* @param {string} bmadDir - Target bmad directory * @param {string} bmadDir - Target bmad directory
* @param {Object} options - Installation options * @param {Object} options - Installation options
* @param {Object} options.moduleConfig - Module configuration from config collector
* @param {Object} options.coreConfig - Core configuration
*/ */
async runModuleInstaller(moduleName, bmadDir, options = {}) { async createModuleDirectories(moduleName, bmadDir, options = {}) {
const moduleConfig = options.moduleConfig || {};
const projectRoot = path.dirname(bmadDir);
// Special handling for core module - it's in src/core not src/modules // Special handling for core module - it's in src/core not src/modules
let sourcePath; let sourcePath;
if (moduleName === 'core') { if (moduleName === 'core') {
sourcePath = getSourcePath('core'); sourcePath = getSourcePath('core');
} else { } else {
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); sourcePath = await this.findModuleSource(moduleName, { silent: true });
if (!sourcePath) { if (!sourcePath) {
// No source found, skip module installer return; // No source found, skip
return;
} }
} }
const installerDir = path.join(sourcePath, '_module-installer'); // Read module.yaml to find the `directories` key
// Prefer .cjs (always CommonJS) then fall back to .js const moduleYamlPath = path.join(sourcePath, 'module.yaml');
const cjsPath = path.join(installerDir, 'installer.cjs'); if (!(await fs.pathExists(moduleYamlPath))) {
const jsPath = path.join(installerDir, 'installer.js'); return; // No module.yaml, skip
const hasCjs = await fs.pathExists(cjsPath);
const installerPath = hasCjs ? cjsPath : jsPath;
// Check if module has a custom installer
if (!hasCjs && !(await fs.pathExists(jsPath))) {
return; // No custom installer
} }
let moduleYaml;
try { try {
// .cjs files are always CommonJS and safe to require(). const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
// .js files may be ESM (when the package sets "type":"module"), moduleYaml = yaml.parse(yamlContent);
// so use dynamic import() which handles both CJS and ESM. } catch {
let moduleInstaller; return; // Invalid YAML, skip
if (hasCjs) { }
moduleInstaller = require(installerPath);
} else { if (!moduleYaml || !moduleYaml.directories) {
const { pathToFileURL } = require('node:url'); return; // No directories declared, skip
const imported = await import(pathToFileURL(installerPath).href); }
// CJS module.exports lands on .default; ESM default can be object, function, or class
moduleInstaller = imported.default == null ? imported : imported.default; // Get color utility for styled output
const color = await prompts.getColor();
const directories = moduleYaml.directories;
const wdsFolders = moduleYaml.wds_folders || [];
for (const dirRef of directories) {
// Parse variable reference like "{design_artifacts}"
const varMatch = dirRef.match(/^\{([^}]+)\}$/);
if (!varMatch) {
// Not a variable reference, skip
continue;
} }
if (typeof moduleInstaller.install === 'function') { const configKey = varMatch[1];
// Get project root (parent of bmad directory) const dirValue = moduleConfig[configKey];
const projectRoot = path.dirname(bmadDir); if (!dirValue || typeof dirValue !== 'string') {
continue; // No value or not a string, skip
}
// Prepare logger (use console if not provided) // Strip {project-root}/ prefix if present
const logger = options.logger || { let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
log: console.log,
error: console.error,
warn: console.warn,
};
// Call the module installer // Handle remaining {project-root} anywhere in the path
const result = await moduleInstaller.install({ dirPath = dirPath.replaceAll('{project-root}', '');
projectRoot,
config: options.moduleConfig || {},
coreConfig: options.coreConfig || {},
installedIDEs: options.installedIDEs || [],
logger,
});
if (!result) { // Resolve to absolute path
await prompts.log.warn(`Module installer for ${moduleName} returned false`); const fullPath = path.join(projectRoot, dirPath);
// Validate path is within project root (prevent directory traversal)
const normalizedPath = path.normalize(fullPath);
const normalizedRoot = path.normalize(projectRoot);
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
await prompts.log.warn(color.yellow(`Warning: ${configKey} path escapes project root, skipping: ${dirPath}`));
continue;
}
// Create directory if it doesn't exist
if (!(await fs.pathExists(fullPath))) {
const dirName = configKey.replaceAll('_', ' ');
await prompts.log.message(color.yellow(`Creating ${dirName} directory: ${dirPath}`));
await fs.ensureDir(fullPath);
}
// Create WDS subfolders if this is the design_artifacts directory
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
await prompts.log.message(color.cyan('Creating WDS folder structure...'));
for (const subfolder of wdsFolders) {
const subPath = path.join(fullPath, subfolder);
if (!(await fs.pathExists(subPath))) {
await fs.ensureDir(subPath);
await prompts.log.message(color.dim(`${subfolder}/`));
}
} }
} }
} catch {
// Post-install scripts are optional; module files are already installed.
// TODO: Eliminate post-install scripts entirely by adding a `directories` key
// to module.yaml that declares which config keys are paths to auto-create.
// The main installer can then handle directory creation centrally, removing
// the need for per-module installer.js scripts and their CJS/ESM issues.
} }
} }
@ -1402,10 +1409,6 @@ class ModuleManager {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Skip _module-installer directories
if (entry.name === '_module-installer') {
continue;
}
const subFiles = await this.getFileList(fullPath, baseDir); const subFiles = await this.getFileList(fullPath, baseDir);
files.push(...subFiles); files.push(...subFiles);
} else { } else {

View File

@ -42,7 +42,7 @@ const STRICT = process.argv.includes('--strict');
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']); const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']);
// Skip directories // Skip directories
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']); const SKIP_DIRS = new Set(['node_modules', '.git']);
// Pattern: {project-root}/_bmad/ references // Pattern: {project-root}/_bmad/ references
const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g; const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g;