refactor(installer): separate custom and official module install paths
Give CustomModules its own install() method with independent file-copy-with-filtering logic. Route _installCustomModules through CustomModules.install() instead of OfficialModules. Remove custom.yaml reading and sourcePath/isCustom options from OfficialModules.install(). Eliminate _buildModuleLists — compute official vs custom module IDs inline from discoverPaths results.
This commit is contained in:
parent
c31e334dd8
commit
b954d2ec81
|
|
@ -8,7 +8,6 @@ const { FileOps } = require('../../../lib/file-ops');
|
|||
const { Config } = require('./config');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { ManifestGenerator } = require('./manifest-generator');
|
||||
const { CustomHandler } = require('../custom-handler');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||
const { InstallPaths } = require('./install-paths');
|
||||
|
|
@ -41,7 +40,6 @@ class Installer {
|
|||
const config = Config.build(originalConfig);
|
||||
const paths = await InstallPaths.create(config);
|
||||
const officialModules = await OfficialModules.build(config, paths);
|
||||
|
||||
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
||||
|
||||
await this.customModules.discoverPaths(config, paths);
|
||||
|
|
@ -63,7 +61,11 @@ class Installer {
|
|||
|
||||
await this._cacheCustomModules(paths, addResult);
|
||||
|
||||
const { officialModuleIds, allModules } = await this._buildModuleLists(config, customConfig, paths);
|
||||
// Compute module lists: official = selected minus custom, all = both
|
||||
const customModuleIds = new Set(this.customModules.paths.keys());
|
||||
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
||||
const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
|
||||
|
||||
await this._installAndConfigure(config, customConfig, paths, officialModuleIds, allModules, addResult, officialModules);
|
||||
|
||||
await this._setupIdes(config, allModules, paths, addResult);
|
||||
|
|
@ -195,51 +197,6 @@ class Installer {
|
|||
addResult('Custom modules cached', 'ok');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the official and combined module lists from config and custom sources.
|
||||
* @returns {{ officialModuleIds: string[], allModules: string[] }}
|
||||
*/
|
||||
async _buildModuleLists(config, customConfig, paths) {
|
||||
const finalCustomContent = customConfig.customContent;
|
||||
|
||||
const customModuleIds = new Set();
|
||||
for (const id of this.customModules.paths.keys()) {
|
||||
customModuleIds.add(id);
|
||||
}
|
||||
if (customConfig._customModuleSources) {
|
||||
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
|
||||
if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
|
||||
customModuleIds.add(moduleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (finalCustomContent && finalCustomContent.cachedModules) {
|
||||
for (const cachedModule of finalCustomContent.cachedModules) {
|
||||
customModuleIds.add(cachedModule.id);
|
||||
}
|
||||
}
|
||||
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
||||
const customHandler = new CustomHandler();
|
||||
for (const customFile of finalCustomContent.selectedFiles) {
|
||||
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
|
||||
if (customInfo && customInfo.id) {
|
||||
customModuleIds.add(customInfo.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
||||
|
||||
const allModules = [...officialModuleIds];
|
||||
for (const id of customModuleIds) {
|
||||
if (!allModules.includes(id)) {
|
||||
allModules.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
return { officialModuleIds, allModules };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install modules, create directories, generate configs and manifests.
|
||||
*/
|
||||
|
|
@ -263,7 +220,7 @@ class Installer {
|
|||
installedModuleNames,
|
||||
});
|
||||
|
||||
await this._installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, officialModules, {
|
||||
await this._installCustomModules(config, paths, addResult, officialModules, {
|
||||
message,
|
||||
installedModuleNames,
|
||||
});
|
||||
|
|
@ -626,88 +583,27 @@ class Installer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Install custom modules from all custom module sources.
|
||||
* @param {Object} config - Installation configuration
|
||||
* @param {Object} paths - InstallPaths instance
|
||||
* @param {Object|undefined} finalCustomContent - Custom content from config
|
||||
* @param {Function} addResult - Callback to record installation results
|
||||
* @param {boolean} isQuickUpdate - Whether this is a quick update
|
||||
* @param {Object} ctx - Shared context: { message, installedModuleNames }
|
||||
* Install custom modules using CustomModules.install().
|
||||
* Source paths come from this.customModules.paths (populated by discoverPaths).
|
||||
*/
|
||||
async _installCustomModules(customConfig, paths, finalCustomContent, addResult, isQuickUpdate, officialModules, ctx) {
|
||||
async _installCustomModules(config, paths, addResult, officialModules, ctx) {
|
||||
const { message, installedModuleNames } = ctx;
|
||||
const isQuickUpdate = config.isQuickUpdate();
|
||||
|
||||
// Collect all custom module IDs with their info from all sources
|
||||
const customModules = new Map();
|
||||
|
||||
// First: cached modules from finalCustomContent
|
||||
if (finalCustomContent && finalCustomContent.cachedModules) {
|
||||
for (const cachedModule of finalCustomContent.cachedModules) {
|
||||
if (!customModules.has(cachedModule.id)) {
|
||||
customModules.set(cachedModule.id, { id: cachedModule.id, path: cachedModule.cachePath, config: {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second: custom module sources from manifest (for quick update)
|
||||
if (customConfig._customModuleSources) {
|
||||
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
|
||||
if (!customModules.has(moduleId)) {
|
||||
const info = { ...customInfo };
|
||||
if (info.sourcePath && !info.path) {
|
||||
info.path = path.isAbsolute(info.sourcePath) ? info.sourcePath : path.join(paths.bmadDir, info.sourcePath);
|
||||
}
|
||||
customModules.set(moduleId, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third: regular custom content from user input (non-cached)
|
||||
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
||||
const customHandler = new CustomHandler();
|
||||
for (const customFile of finalCustomContent.selectedFiles) {
|
||||
const info = await customHandler.getCustomInfo(customFile, paths.projectRoot);
|
||||
if (info && info.id && !customModules.has(info.id)) {
|
||||
customModules.set(info.id, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth: any remaining custom modules not yet covered
|
||||
for (const [moduleId, modulePath] of this.customModules.paths) {
|
||||
if (!customModules.has(moduleId)) {
|
||||
customModules.set(moduleId, { id: moduleId, path: modulePath, config: {} });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [moduleName, customInfo] of customModules) {
|
||||
for (const [moduleName, sourcePath] of this.customModules.paths) {
|
||||
if (installedModuleNames.has(moduleName)) continue;
|
||||
installedModuleNames.add(moduleName);
|
||||
|
||||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||
|
||||
if (!this.customModules.paths.has(moduleName) && customInfo.path) {
|
||||
this.customModules.paths.set(moduleName, customInfo.path);
|
||||
}
|
||||
|
||||
const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
|
||||
await officialModules.install(
|
||||
moduleName,
|
||||
paths.bmadDir,
|
||||
(filePath) => {
|
||||
this.installedFiles.add(filePath);
|
||||
},
|
||||
{
|
||||
isCustom: true,
|
||||
moduleConfig: collectedModuleConfig,
|
||||
isQuickUpdate: isQuickUpdate,
|
||||
installer: this,
|
||||
silent: true,
|
||||
sourcePath: customInfo.path,
|
||||
},
|
||||
);
|
||||
const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
||||
moduleConfig: collectedModuleConfig,
|
||||
});
|
||||
|
||||
// Generate runtime config.yaml with merged values
|
||||
await this.generateModuleConfigs(paths.bmadDir, {
|
||||
[moduleName]: { ...customConfig.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
||||
});
|
||||
|
||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { CustomHandler } = require('../custom-handler');
|
||||
const { Manifest } = require('../core/manifest');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
class CustomModules {
|
||||
constructor() {
|
||||
|
|
@ -18,6 +22,126 @@ class CustomModules {
|
|||
this.paths.set(moduleId, sourcePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom module from its source path.
|
||||
* @param {string} moduleName - Module identifier
|
||||
* @param {string} bmadDir - Target bmad directory
|
||||
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
||||
* @param {Object} options - Install options
|
||||
* @param {Object} options.moduleConfig - Pre-collected module configuration
|
||||
* @returns {Object} Install result
|
||||
*/
|
||||
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
|
||||
const sourcePath = this.paths.get(moduleName);
|
||||
if (!sourcePath) {
|
||||
throw new Error(`No source path for custom module '${moduleName}'`);
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(sourcePath))) {
|
||||
throw new Error(`Source for custom module '${moduleName}' not found at: ${sourcePath}`);
|
||||
}
|
||||
|
||||
const targetPath = path.join(bmadDir, moduleName);
|
||||
|
||||
// Read custom.yaml and merge into module config
|
||||
let moduleConfig = options.moduleConfig ? { ...options.moduleConfig } : {};
|
||||
const customConfigPath = path.join(sourcePath, 'custom.yaml');
|
||||
if (await fs.pathExists(customConfigPath)) {
|
||||
try {
|
||||
const content = await fs.readFile(customConfigPath, 'utf8');
|
||||
const customConfig = yaml.parse(content);
|
||||
if (customConfig) {
|
||||
moduleConfig = { ...moduleConfig, ...customConfig };
|
||||
}
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove existing installation
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
await fs.remove(targetPath);
|
||||
}
|
||||
|
||||
// Copy files with filtering
|
||||
await this._copyWithFiltering(sourcePath, targetPath, fileTrackingCallback);
|
||||
|
||||
// Add to manifest
|
||||
const manifest = new Manifest();
|
||||
const versionInfo = await manifest.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
||||
await manifest.addModule(bmadDir, moduleName, {
|
||||
version: versionInfo.version,
|
||||
source: versionInfo.source,
|
||||
npmPackage: versionInfo.npmPackage,
|
||||
repoUrl: versionInfo.repoUrl,
|
||||
});
|
||||
|
||||
return { success: true, module: moduleName, path: targetPath, moduleConfig };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy module files, filtering out install-time-only artifacts.
|
||||
* @param {string} sourcePath - Source module directory
|
||||
* @param {string} targetPath - Target module directory
|
||||
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
||||
*/
|
||||
async _copyWithFiltering(sourcePath, targetPath, fileTrackingCallback = null) {
|
||||
const files = await this._getFileList(sourcePath);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('sub-modules/')) continue;
|
||||
|
||||
const isInSidecar = path
|
||||
.dirname(file)
|
||||
.split('/')
|
||||
.some((dir) => dir.toLowerCase().endsWith('-sidecar'));
|
||||
if (isInSidecar) continue;
|
||||
|
||||
if (file === 'module.yaml') continue;
|
||||
if (file === 'config.yaml') continue;
|
||||
|
||||
const sourceFile = path.join(sourcePath, file);
|
||||
const targetFile = path.join(targetPath, file);
|
||||
|
||||
// Skip web-only agents
|
||||
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
||||
const content = await fs.readFile(sourceFile, 'utf8');
|
||||
if (/<agent[^>]*\slocalskip="true"[^>]*>/.test(content)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.ensureDir(path.dirname(targetFile));
|
||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
||||
|
||||
if (fileTrackingCallback) {
|
||||
fileTrackingCallback(targetFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively list all files in a directory.
|
||||
* @param {string} dir - Directory to scan
|
||||
* @param {string} baseDir - Base directory for relative paths
|
||||
* @returns {string[]} Relative file paths
|
||||
*/
|
||||
async _getFileList(dir, baseDir = dir) {
|
||||
const files = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await this._getFileList(fullPath, baseDir)));
|
||||
} else {
|
||||
files.push(path.relative(baseDir, fullPath));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover custom module source paths from all available sources.
|
||||
* @param {Object} config - Installation configuration
|
||||
|
|
|
|||
|
|
@ -231,52 +231,25 @@ 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
|
||||
if (!sourcePath) {
|
||||
// Provide a more user-friendly error message
|
||||
throw new Error(
|
||||
`Source for module '${moduleName}' is not available. It will be retained but cannot be updated without its source files.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this is a custom module and read its custom.yaml values
|
||||
let customConfig = null;
|
||||
const rootCustomConfigPath = path.join(sourcePath, 'custom.yaml');
|
||||
|
||||
if (await fs.pathExists(rootCustomConfigPath)) {
|
||||
try {
|
||||
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
|
||||
customConfig = yaml.parse(customContent);
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a custom module, merge its values into the module config
|
||||
if (customConfig) {
|
||||
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
||||
if (options.logger) {
|
||||
await options.logger.log(` Merged custom configuration for ${moduleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
await fs.remove(targetPath);
|
||||
}
|
||||
|
||||
// Copy module files with filtering
|
||||
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
|
||||
|
||||
// Create directories declared in module.yaml (unless explicitly skipped)
|
||||
if (!options.skipModuleInstaller) {
|
||||
await this.createModuleDirectories(moduleName, bmadDir, options);
|
||||
}
|
||||
|
||||
// Capture version info for manifest
|
||||
const { Manifest } = require('../core/manifest');
|
||||
const manifestObj = new Manifest();
|
||||
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
||||
|
|
@ -288,12 +261,7 @@ class OfficialModules {
|
|||
repoUrl: versionInfo.repoUrl,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
module: moduleName,
|
||||
path: targetPath,
|
||||
versionInfo,
|
||||
};
|
||||
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue