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:
parent
90ea3cbed7
commit
c563cef0c2
|
|
@ -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'],
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -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:
|
# whiteport-design-system:
|
||||||
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
||||||
# module-definition: src/module.yaml
|
# module-definition: src/module.yaml
|
||||||
# code: WDS
|
# code: wds
|
||||||
# name: "Whiteport UX Design System"
|
# name: "Whiteport UX Design System"
|
||||||
# description: "UX design framework with Figma integration"
|
# description: "UX design framework with Figma integration"
|
||||||
# defaultSelected: false
|
# defaultSelected: false
|
||||||
# type: community
|
# type: community
|
||||||
|
# npmPackage: bmad-method-wds-expansion
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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'))
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
let moduleInstaller;
|
|
||||||
if (hasCjs) {
|
|
||||||
moduleInstaller = require(installerPath);
|
|
||||||
} else {
|
|
||||||
const { pathToFileURL } = require('node:url');
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof moduleInstaller.install === 'function') {
|
|
||||||
// Get project root (parent of bmad directory)
|
|
||||||
const projectRoot = path.dirname(bmadDir);
|
|
||||||
|
|
||||||
// Prepare logger (use console if not provided)
|
|
||||||
const logger = options.logger || {
|
|
||||||
log: console.log,
|
|
||||||
error: console.error,
|
|
||||||
warn: console.warn,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the module installer
|
|
||||||
const result = await moduleInstaller.install({
|
|
||||||
projectRoot,
|
|
||||||
config: options.moduleConfig || {},
|
|
||||||
coreConfig: options.coreConfig || {},
|
|
||||||
installedIDEs: options.installedIDEs || [],
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
await prompts.log.warn(`Module installer for ${moduleName} returned false`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Post-install scripts are optional; module files are already installed.
|
return; // Invalid YAML, skip
|
||||||
// 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
|
if (!moduleYaml || !moduleYaml.directories) {
|
||||||
// the need for per-module installer.js scripts and their CJS/ESM issues.
|
return; // No directories declared, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configKey = varMatch[1];
|
||||||
|
const dirValue = moduleConfig[configKey];
|
||||||
|
if (!dirValue || typeof dirValue !== 'string') {
|
||||||
|
continue; // No value or not a string, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip {project-root}/ prefix if present
|
||||||
|
let dirPath = dirValue.replace(/^\{project-root\}\/?/, '');
|
||||||
|
|
||||||
|
// Handle remaining {project-root} anywhere in the path
|
||||||
|
dirPath = dirPath.replaceAll('{project-root}', '');
|
||||||
|
|
||||||
|
// Resolve to absolute path
|
||||||
|
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}/`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue