Compare commits

..

No commits in common. "a567170501b0aa56dedf2c934a5619a6e6071286" and "eade619d173d70dca36ec558ad3244f9467d0b2a" have entirely different histories.

6 changed files with 713 additions and 631 deletions

View File

@ -10,19 +10,19 @@ class ConfigCollector {
this.collectedConfig = {};
this.existingConfig = null;
this.currentProjectDir = null;
this._officialModulesInstance = null;
this._moduleManagerInstance = null;
}
/**
* Get or create a cached OfficialModules instance (lazy initialization)
* @returns {Object} OfficialModules instance
* Get or create a cached ModuleManager instance (lazy initialization)
* @returns {Object} ModuleManager instance
*/
_getOfficialModules() {
if (!this._officialModulesInstance) {
const { OfficialModules } = require('../modules/official-modules');
this._officialModulesInstance = new OfficialModules();
_getModuleManager() {
if (!this._moduleManagerInstance) {
const { ModuleManager } = require('../modules/manager');
this._moduleManagerInstance = new ModuleManager();
}
return this._officialModulesInstance;
return this._moduleManagerInstance;
}
/**
@ -153,7 +153,7 @@ class ConfigCollector {
const results = [];
for (const moduleName of modules) {
// Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search
// Resolve module.yaml path - custom paths first, then standard location, then ModuleManager search
let moduleConfigPath = null;
const customPath = this.customModulePaths?.get(moduleName);
if (customPath) {
@ -163,7 +163,7 @@ class ConfigCollector {
if (await fs.pathExists(standardPath)) {
moduleConfigPath = standardPath;
} else {
const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true });
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
@ -364,7 +364,7 @@ class ConfigCollector {
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(moduleConfigPath))) {
const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true });
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
@ -378,7 +378,7 @@ class ConfigCollector {
configPath = moduleConfigPath;
} else {
// Check if this is a custom module with custom.yaml
const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true });
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
@ -674,7 +674,7 @@ class ConfigCollector {
// If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(moduleConfigPath))) {
const moduleSourcePath = await this._getOfficialModules().findModuleSource(moduleName, { silent: true });
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +0,0 @@
const path = require('node:path');
const { CustomHandler } = require('../custom-handler');
class CustomModules {
constructor() {
this.paths = new Map();
}
has(moduleCode) {
return this.paths.has(moduleCode);
}
get(moduleCode) {
return this.paths.get(moduleCode);
}
set(moduleId, sourcePath) {
this.paths.set(moduleId, sourcePath);
}
/**
* Discover custom module source paths from all available sources.
* @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance
* @returns {Map<string, string>} Map of module ID to source path
*/
async discoverPaths(config, paths) {
this.paths = new Map();
if (config._quickUpdate) {
if (config._customModuleSources) {
for (const [moduleId, customInfo] of config._customModuleSources) {
this.paths.set(moduleId, customInfo.sourcePath);
}
}
return this.paths;
}
// From manifest (regular updates)
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
for (const customModule of config._existingInstall.customModules) {
let absoluteSourcePath = customModule.sourcePath;
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
absoluteSourcePath = path.join(paths.bmadDir, absoluteSourcePath);
} else if (!absoluteSourcePath && customModule.relativePath) {
absoluteSourcePath = path.resolve(paths.projectRoot, customModule.relativePath);
} else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
absoluteSourcePath = path.resolve(absoluteSourcePath);
}
if (absoluteSourcePath) {
this.paths.set(customModule.id, absoluteSourcePath);
}
}
}
// From UI: selectedFiles
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (customInfo && customInfo.id) {
this.paths.set(customInfo.id, customInfo.path);
}
}
}
// From UI: sources
if (config.customContent && config.customContent.sources) {
for (const source of config.customContent.sources) {
this.paths.set(source.id, source.path);
}
}
// From UI: cachedModules
if (config.customContent && config.customContent.cachedModules) {
const selectedCachedIds = config.customContent.selectedCachedModules || [];
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
for (const cachedModule of config.customContent.cachedModules) {
if (cachedModule.id && cachedModule.cachePath && (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))) {
this.paths.set(cachedModule.id, cachedModule.cachePath);
}
}
}
return this.paths;
}
}
module.exports = { CustomModules };

View File

@ -1,7 +1,5 @@
const fs = require('fs-extra');
const os = require('node:os');
const path = require('node:path');
const { execSync } = require('node:child_process');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
@ -133,191 +131,6 @@ class ExternalModuleManager {
const module = await this.getModuleByCode(code);
return module ? module.moduleDefinition : null;
}
/**
* Get the cache directory for external modules
* @returns {string} Path to the external modules cache directory
*/
getExternalCacheDir() {
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules');
return cacheDir;
}
/**
* Clone an external module repository to cache
* @param {string} moduleCode - Code of the external module
* @param {Object} options - Clone options
* @param {boolean} options.silent - Suppress spinner output
* @returns {string} Path to the cloned repository
*/
async cloneExternalModule(moduleCode, options = {}) {
const moduleInfo = await this.getModuleByCode(moduleCode);
if (!moduleInfo) {
throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
}
const cacheDir = this.getExternalCacheDir();
const moduleCacheDir = path.join(cacheDir, moduleCode);
const silent = options.silent || false;
// Create cache directory if it doesn't exist
await fs.ensureDir(cacheDir);
// Helper to create a spinner or a no-op when silent
const createSpinner = async () => {
if (silent) {
return {
start() {},
stop() {},
error() {},
message() {},
cancel() {},
clear() {},
get isSpinning() {
return false;
},
get isCancelled() {
return false;
},
};
}
return await prompts.spinner();
};
// Track if we need to install dependencies
let needsDependencyInstall = false;
let wasNewClone = false;
// Check if already cloned
if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
// Fetch and reset to remote - works better with shallow clones than pull
execSync('git fetch origin --depth 1', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git reset --hard origin/HEAD', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code
if (currentRef !== newRef) {
needsDependencyInstall = true;
}
} catch {
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone
await fs.remove(moduleCacheDir);
wasNewClone = true;
}
} else {
wasNewClone = true;
}
// Clone if not exists or was removed
if (wasNewClone) {
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
} catch (error) {
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
}
}
// Install dependencies if package.json exists
const packageJsonPath = path.join(moduleCacheDir, 'package.json');
const nodeModulesPath = path.join(moduleCacheDir, 'node_modules');
if (await fs.pathExists(packageJsonPath)) {
// Install if node_modules doesn't exist, or if package.json is newer (dependencies changed)
const nodeModulesMissing = !(await fs.pathExists(nodeModulesPath));
// Force install if we updated or cloned new
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
}
} else {
// Check if package.json is newer than node_modules
let packageJsonNewer = false;
try {
const packageStats = await fs.stat(packageJsonPath);
const nodeModulesStats = await fs.stat(nodeModulesPath);
packageJsonNewer = packageStats.mtime > nodeModulesStats.mtime;
} catch {
// If stat fails, assume we need to install
packageJsonNewer = true;
}
if (packageJsonNewer) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
}
}
}
}
return moduleCacheDir;
}
/**
* Find the source path for an external module
* @param {string} moduleCode - Code of the external module
* @param {Object} options - Options passed to cloneExternalModule
* @returns {string|null} Path to the module source or null if not found
*/
async findExternalModuleSource(moduleCode, options = {}) {
const moduleInfo = await this.getModuleByCode(moduleCode);
if (!moduleInfo) {
return null;
}
// Clone the external module repo
const cloneDir = await this.cloneExternalModule(moduleCode, options);
// The module-definition specifies the path to module.yaml relative to repo root
// We need to return the directory containing module.yaml
const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'src/module.yaml'
const moduleDir = path.dirname(path.join(cloneDir, moduleDefinitionPath));
return moduleDir;
}
}
module.exports = { ExternalModuleManager };

View File

@ -4,10 +4,51 @@ const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { ExternalModuleManager } = require('./external-manager');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
class OfficialModules {
/**
* Manages the installation, updating, and removal of BMAD modules.
* Handles module discovery, dependency resolution, and configuration processing.
*
* @class ModuleManager
* @requires fs-extra
* @requires yaml
* @requires prompts
*
* @example
* const manager = new ModuleManager();
* const modules = await manager.listAvailable();
* await manager.install('core-module', '/path/to/bmad');
*/
class ModuleManager {
constructor(options = {}) {
this.externalModuleManager = new ExternalModuleManager();
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
this.customModulePaths = new Map(); // Initialize custom module paths
this.externalModuleManager = new ExternalModuleManager(); // For external official modules
}
/**
* Set the bmad folder name for placeholder replacement
* @param {string} bmadFolderName - The bmad folder name
*/
setBmadFolderName(bmadFolderName) {
this.bmadFolderName = bmadFolderName;
}
/**
* Set the core configuration for access during module installation
* @param {Object} coreConfig - Core configuration object
*/
setCoreConfig(coreConfig) {
this.coreConfig = coreConfig;
}
/**
* Set custom module paths for priority lookup
* @param {Map<string, string>} customModulePaths - Map of module ID to source path
*/
setCustomModulePaths(customModulePaths) {
this.customModulePaths = customModulePaths;
}
/**
@ -16,7 +57,7 @@ class OfficialModules {
* @param {string} targetPath - Target file path
* @param {boolean} overwrite - Whether to overwrite existing files (default: true)
*/
async copyFile(sourcePath, targetPath, overwrite = true) {
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, overwrite = true) {
await fs.copy(sourcePath, targetPath, { overwrite });
}
@ -26,7 +67,7 @@ class OfficialModules {
* @param {string} targetDir - Target directory path
* @param {boolean} overwrite - Whether to overwrite existing files (default: true)
*/
async copyDirectory(sourceDir, targetDir, overwrite = true) {
async copyDirectoryWithPlaceholderReplacement(sourceDir, targetDir, overwrite = true) {
await fs.ensureDir(targetDir);
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
@ -35,15 +76,16 @@ class OfficialModules {
const targetPath = path.join(targetDir, entry.name);
if (entry.isDirectory()) {
await this.copyDirectory(sourcePath, targetPath, overwrite);
await this.copyDirectoryWithPlaceholderReplacement(sourcePath, targetPath, overwrite);
} else {
await this.copyFile(sourcePath, targetPath, overwrite);
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, overwrite);
}
}
}
/**
* List all available built-in modules (core and bmm).
* List all available modules (excluding core which is always installed)
* bmm is the only built-in module, directly under src/bmm-skills
* All other modules come from external-official-modules.yaml
* @returns {Object} Object with modules array and customModules array
*/
@ -51,15 +93,6 @@ class OfficialModules {
const modules = [];
const customModules = [];
// Add built-in core module (directly under src/core-skills)
const corePath = getSourcePath('core-skills');
if (await fs.pathExists(corePath)) {
const coreInfo = await this.getModuleInfo(corePath, 'core', 'src/core-skills');
if (coreInfo) {
modules.push(coreInfo);
}
}
// Add built-in bmm module (directly under src/bmm-skills)
const bmmPath = getSourcePath('bmm-skills');
if (await fs.pathExists(bmmPath)) {
@ -69,6 +102,25 @@ class OfficialModules {
}
}
// Check for cached custom modules in _config/custom/
if (this.bmadDir) {
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
if (await fs.pathExists(customCacheDir)) {
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
for (const entry of cacheEntries) {
if (entry.isDirectory()) {
const cachePath = path.join(customCacheDir, entry.name);
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
moduleInfo.isCustom = true;
moduleInfo.fromCache = true;
customModules.push(moduleInfo);
}
}
}
}
}
return { modules, customModules };
}
@ -142,12 +194,9 @@ class OfficialModules {
async findModuleSource(moduleCode, options = {}) {
const projectRoot = getProjectRoot();
// Check for core module (directly under src/core-skills)
if (moduleCode === 'core') {
const corePath = getSourcePath('core-skills');
if (await fs.pathExists(corePath)) {
return corePath;
}
// First check custom module paths if they exist
if (this.customModulePaths && this.customModulePaths.has(moduleCode)) {
return this.customModulePaths.get(moduleCode);
}
// Check for built-in bmm module (directly under src/bmm-skills)
@ -159,7 +208,7 @@ class OfficialModules {
}
// Check external official modules
const externalSource = await this.externalModuleManager.findExternalModuleSource(moduleCode, options);
const externalSource = await this.findExternalModuleSource(moduleCode, options);
if (externalSource) {
return externalSource;
}
@ -167,6 +216,199 @@ class OfficialModules {
return null;
}
/**
* Check if a module is an external official module
* @param {string} moduleCode - Code of the module to check
* @returns {boolean} True if the module is external
*/
async isExternalModule(moduleCode) {
return await this.externalModuleManager.hasModule(moduleCode);
}
/**
* Get the cache directory for external modules
* @returns {string} Path to the external modules cache directory
*/
getExternalCacheDir() {
const os = require('node:os');
const cacheDir = path.join(os.homedir(), '.bmad', 'cache', 'external-modules');
return cacheDir;
}
/**
* Clone an external module repository to cache
* @param {string} moduleCode - Code of the external module
* @returns {string} Path to the cloned repository
*/
async cloneExternalModule(moduleCode, options = {}) {
const { execSync } = require('node:child_process');
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
if (!moduleInfo) {
throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
}
const cacheDir = this.getExternalCacheDir();
const moduleCacheDir = path.join(cacheDir, moduleCode);
const silent = options.silent || false;
// Create cache directory if it doesn't exist
await fs.ensureDir(cacheDir);
// Helper to create a spinner or a no-op when silent
const createSpinner = async () => {
if (silent) {
return {
start() {},
stop() {},
error() {},
message() {},
cancel() {},
clear() {},
get isSpinning() {
return false;
},
get isCancelled() {
return false;
},
};
}
return await prompts.spinner();
};
// Track if we need to install dependencies
let needsDependencyInstall = false;
let wasNewClone = false;
// Check if already cloned
if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
// Fetch and reset to remote - works better with shallow clones than pull
execSync('git fetch origin --depth 1', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git reset --hard origin/HEAD', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code
if (currentRef !== newRef) {
needsDependencyInstall = true;
}
} catch {
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone
await fs.remove(moduleCacheDir);
wasNewClone = true;
}
} else {
wasNewClone = true;
}
// Clone if not exists or was removed
if (wasNewClone) {
const fetchSpinner = await createSpinner();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
fetchSpinner.stop(`Fetched ${moduleInfo.name}`);
} catch (error) {
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
}
}
// Install dependencies if package.json exists
const packageJsonPath = path.join(moduleCacheDir, 'package.json');
const nodeModulesPath = path.join(moduleCacheDir, 'node_modules');
if (await fs.pathExists(packageJsonPath)) {
// Install if node_modules doesn't exist, or if package.json is newer (dependencies changed)
const nodeModulesMissing = !(await fs.pathExists(nodeModulesPath));
// Force install if we updated or cloned new
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
}
} else {
// Check if package.json is newer than node_modules
let packageJsonNewer = false;
try {
const packageStats = await fs.stat(packageJsonPath);
const nodeModulesStats = await fs.stat(nodeModulesPath);
packageJsonNewer = packageStats.mtime > nodeModulesStats.mtime;
} catch {
// If stat fails, assume we need to install
packageJsonNewer = true;
}
if (packageJsonNewer) {
const installSpinner = await createSpinner();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000, // 2 minute timeout
});
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` ${error.message}`);
}
}
}
}
return moduleCacheDir;
}
/**
* Find the source path for an external module
* @param {string} moduleCode - Code of the external module
* @returns {string|null} Path to the module source or null if not found
*/
async findExternalModuleSource(moduleCode, options = {}) {
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
if (!moduleInfo) {
return null;
}
// Clone the external module repo
const cloneDir = await this.cloneExternalModule(moduleCode, options);
// The module-definition specifies the path to module.yaml relative to repo root
// We need to return the directory containing module.yaml
const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'src/module.yaml'
const moduleDir = path.dirname(path.join(cloneDir, moduleDefinitionPath));
return moduleDir;
}
/**
* Install a module
* @param {string} moduleName - Code of the module to install (from module.yaml)
@ -178,7 +420,7 @@ class OfficialModules {
* @param {Object} options.logger - Logger instance for output
*/
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
const sourcePath = options.sourcePath || (await this.findModuleSource(moduleName, { silent: options.silent }));
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
const targetPath = path.join(bmadDir, moduleName);
// Check if source module exists
@ -399,7 +641,7 @@ class OfficialModules {
}
// Copy the file with placeholder replacement
await this.copyFile(sourceFile, targetFile);
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
// Track the file if callback provided
if (fileTrackingCallback) {
@ -654,7 +896,7 @@ class OfficialModules {
}
// Copy file with placeholder replacement
await this.copyFile(sourceFile, targetFile);
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
}
}
@ -683,4 +925,4 @@ class OfficialModules {
}
}
module.exports = { OfficialModules };
module.exports = { ModuleManager };

View File

@ -423,10 +423,8 @@ class UI {
selectedModules.push(...customModuleResult.selectedCustomModules);
}
// Ensure core is in the modules list
if (!selectedModules.includes('core')) {
selectedModules.unshift('core');
}
// Filter out core - it's always installed via installCore flag
selectedModules = selectedModules.filter((m) => m !== 'core');
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -436,6 +434,7 @@ class UI {
return {
actionType: 'update',
directory: confirmedDirectory,
installCore: true,
modules: selectedModules,
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
@ -544,16 +543,14 @@ class UI {
selectedModules.push(...customContentConfig.selectedModuleIds);
}
// Ensure core is in the modules list
if (!selectedModules.includes('core')) {
selectedModules.unshift('core');
}
selectedModules = selectedModules.filter((m) => m !== 'core');
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
return {
actionType: 'install',
directory: confirmedDirectory,
installCore: true,
modules: selectedModules,
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
@ -938,9 +935,9 @@ class UI {
}
// Add official modules
const { OfficialModules } = require('../installers/lib/modules/official-modules');
const officialModules = new OfficialModules();
const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const { modules: availableModules, customModules: customModulesFromCache } = await moduleManager.listAvailable();
// First, add all items to appropriate sections
const allCustomModules = [];
@ -995,9 +992,9 @@ class UI {
* @returns {Array} Selected module codes (excluding core)
*/
async selectAllModules(installedModuleIds = new Set()) {
const { OfficialModules } = require('../installers/lib/modules/official-modules');
const officialModulesSource = new OfficialModules();
const { modules: localModules } = await officialModulesSource.listAvailable();
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const { modules: localModules } = await moduleManager.listAvailable();
// Get external modules
const externalManager = new ExternalModuleManager();
@ -1072,7 +1069,7 @@ class UI {
maxItems: allOptions.length,
});
const result = selected ? [...selected] : [];
const result = selected ? selected.filter((m) => m !== 'core') : [];
// Display selected modules as bulleted list
if (result.length > 0) {
@ -1092,9 +1089,9 @@ class UI {
* @returns {Array} Default module codes
*/
async getDefaultModules(installedModuleIds = new Set()) {
const { OfficialModules } = require('../installers/lib/modules/official-modules');
const officialModules = new OfficialModules();
const { modules: localModules } = await officialModules.listAvailable();
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const { modules: localModules } = await moduleManager.listAvailable();
const defaultModules = [];