refactor(installer): normalize config gate and flatten core into module list
Split install() input into config (clean official fields) and customConfig (full originalConfig copy for custom module concerns). Core is no longer special-cased — it's just another module in config.modules that goes through OfficialModules.install(). Deletes installCore(), copyCoreFiles(), copyFile(), getFileList() dead code. Updates ui.js and quickUpdate() callers to keep core in the modules list instead of setting installCore: true.
This commit is contained in:
parent
aa406419e7
commit
68f723d427
|
|
@ -38,14 +38,30 @@ class Installer {
|
||||||
* Main installation method
|
* Main installation method
|
||||||
* @param {Object} config - Installation configuration
|
* @param {Object} config - Installation configuration
|
||||||
* @param {string} config.directory - Target directory
|
* @param {string} config.directory - Target directory
|
||||||
* @param {boolean} config.installCore - Whether to install core
|
* @param {string[]} config.modules - Modules to install (including 'core')
|
||||||
* @param {string[]} config.modules - Modules to install
|
|
||||||
* @param {string[]} config.ides - IDEs to configure
|
* @param {string[]} config.ides - IDEs to configure
|
||||||
* @param {boolean} config.skipIde - Skip IDE configuration
|
|
||||||
*/
|
*/
|
||||||
async install(originalConfig) {
|
async install(originalConfig) {
|
||||||
// Clone config to avoid mutating the caller's object
|
// Build a normalized config with explicit fields — no opaque spread
|
||||||
const config = { ...originalConfig };
|
// Clean config for the official module install path
|
||||||
|
const modules = [...(originalConfig.modules || [])];
|
||||||
|
if (originalConfig.installCore && !modules.includes('core')) {
|
||||||
|
modules.unshift('core');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
directory: originalConfig.directory,
|
||||||
|
modules,
|
||||||
|
ides: originalConfig.skipIde ? [] : [...(originalConfig.ides || [])],
|
||||||
|
skipPrompts: originalConfig.skipPrompts || false,
|
||||||
|
verbose: originalConfig.verbose || false,
|
||||||
|
force: originalConfig.force || false,
|
||||||
|
actionType: originalConfig.actionType,
|
||||||
|
coreConfig: originalConfig.coreConfig || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything — custom modules, quick-update state, the whole mess
|
||||||
|
const customConfig = { ...originalConfig };
|
||||||
|
|
||||||
// if core config isn't collected, we haven't run the UI -> display logo/version
|
// if core config isn't collected, we haven't run the UI -> display logo/version
|
||||||
const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
|
const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
|
||||||
|
|
@ -56,7 +72,7 @@ class Installer {
|
||||||
const paths = await InstallPaths.create(config);
|
const paths = await InstallPaths.create(config);
|
||||||
|
|
||||||
// Collect configurations for official modules
|
// Collect configurations for official modules
|
||||||
const moduleConfigs = await this._collectConfigs(config, paths);
|
const moduleConfigs = await this._collectConfigs(customConfig, paths);
|
||||||
|
|
||||||
await this.customModules.discoverPaths(config, paths);
|
await this.customModules.discoverPaths(config, paths);
|
||||||
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
||||||
|
|
@ -71,7 +87,7 @@ class Installer {
|
||||||
spinner.message('Checking for existing installation...');
|
spinner.message('Checking for existing installation...');
|
||||||
const existingInstall = await this.detector.detect(paths.bmadDir);
|
const existingInstall = await this.detector.detect(paths.bmadDir);
|
||||||
|
|
||||||
if (existingInstall.installed && !config.force && !config._quickUpdate) {
|
if (existingInstall.installed && !config.force && !customConfig._quickUpdate) {
|
||||||
spinner.stop('Existing installation detected');
|
spinner.stop('Existing installation detected');
|
||||||
|
|
||||||
// Check if user already decided what to do (from early menu in ui.js)
|
// Check if user already decided what to do (from early menu in ui.js)
|
||||||
|
|
@ -93,8 +109,8 @@ class Installer {
|
||||||
|
|
||||||
if (action === 'update') {
|
if (action === 'update') {
|
||||||
// Store that we're updating for later processing
|
// Store that we're updating for later processing
|
||||||
config._isUpdate = true;
|
customConfig._isUpdate = true;
|
||||||
config._existingInstall = existingInstall;
|
customConfig._existingInstall = existingInstall;
|
||||||
|
|
||||||
// Detect modules that were previously installed but are NOT in the new selection (to be removed)
|
// Detect modules that were previously installed but are NOT in the new selection (to be removed)
|
||||||
const previouslyInstalledModules = new Set(existingInstall.modules.map((m) => m.id));
|
const previouslyInstalledModules = new Set(existingInstall.modules.map((m) => m.id));
|
||||||
|
|
@ -162,8 +178,8 @@ class Installer {
|
||||||
const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
|
const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
|
||||||
const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
|
const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
|
||||||
|
|
||||||
config._customFiles = customFiles;
|
customConfig._customFiles = customFiles;
|
||||||
config._modifiedFiles = modifiedFiles;
|
customConfig._modifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
// Preserve existing core configuration during updates
|
// Preserve existing core configuration during updates
|
||||||
// Read the current core config.yaml to maintain user's settings
|
// Read the current core config.yaml to maintain user's settings
|
||||||
|
|
@ -174,10 +190,9 @@ class Installer {
|
||||||
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
|
||||||
const existingCoreConfig = yaml.parse(coreConfigContent);
|
const existingCoreConfig = yaml.parse(coreConfigContent);
|
||||||
|
|
||||||
// Store in config.coreConfig so it's preserved through the installation
|
// Preserve through the installation
|
||||||
config.coreConfig = existingCoreConfig;
|
config.coreConfig = existingCoreConfig;
|
||||||
|
customConfig.coreConfig = existingCoreConfig;
|
||||||
// Also store in configCollector for use during config collection
|
|
||||||
this.configCollector.collectedConfig.core = existingCoreConfig;
|
this.configCollector.collectedConfig.core = existingCoreConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
|
await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`);
|
||||||
|
|
@ -234,7 +249,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
||||||
|
|
||||||
config._tempBackupDir = tempBackupDir;
|
customConfig._tempBackupDir = tempBackupDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For modified files, back them up to temp directory (will be restored as .bak files after install)
|
// For modified files, back them up to temp directory (will be restored as .bak files after install)
|
||||||
|
|
@ -251,21 +266,21 @@ class Installer {
|
||||||
}
|
}
|
||||||
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
||||||
|
|
||||||
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
customConfig._tempModifiedBackupDir = tempModifiedBackupDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (existingInstall.installed && config._quickUpdate) {
|
} else if (existingInstall.installed && customConfig._quickUpdate) {
|
||||||
// Quick update mode - automatically treat as update without prompting
|
// Quick update mode - automatically treat as update without prompting
|
||||||
spinner.message('Preparing quick update...');
|
spinner.message('Preparing quick update...');
|
||||||
config._isUpdate = true;
|
customConfig._isUpdate = true;
|
||||||
config._existingInstall = existingInstall;
|
customConfig._existingInstall = existingInstall;
|
||||||
|
|
||||||
// Detect custom and modified files BEFORE updating
|
// Detect custom and modified files BEFORE updating
|
||||||
const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
|
const existingFilesManifest = await this.readFilesManifest(paths.bmadDir);
|
||||||
const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
|
const { customFiles, modifiedFiles } = await this.detectCustomFiles(paths.bmadDir, existingFilesManifest);
|
||||||
|
|
||||||
config._customFiles = customFiles;
|
customConfig._customFiles = customFiles;
|
||||||
config._modifiedFiles = modifiedFiles;
|
customConfig._modifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
// Also check cache directory for custom modules (like quick update does)
|
// Also check cache directory for custom modules (like quick update does)
|
||||||
const cacheDir = paths.customCacheDir;
|
const cacheDir = paths.customCacheDir;
|
||||||
|
|
@ -314,7 +329,7 @@ class Installer {
|
||||||
await fs.copy(customFile, backupPath);
|
await fs.copy(customFile, backupPath);
|
||||||
}
|
}
|
||||||
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
spinner.stop(`Backed up ${customFiles.length} custom files`);
|
||||||
config._tempBackupDir = tempBackupDir;
|
customConfig._tempBackupDir = tempBackupDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back up modified files
|
// Back up modified files
|
||||||
|
|
@ -330,7 +345,7 @@ class Installer {
|
||||||
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
||||||
}
|
}
|
||||||
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
spinner.stop(`Backed up ${modifiedFiles.length} modified files`);
|
||||||
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
customConfig._tempModifiedBackupDir = tempModifiedBackupDir;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,10 +353,10 @@ class Installer {
|
||||||
// Skip for quick update since we already have the IDE list
|
// Skip for quick update since we already have the IDE list
|
||||||
spinner.stop('Pre-checks complete');
|
spinner.stop('Pre-checks complete');
|
||||||
let toolSelection;
|
let toolSelection;
|
||||||
if (config._quickUpdate) {
|
if (customConfig._quickUpdate) {
|
||||||
// Quick update already has IDEs configured, use saved configurations
|
// Quick update already has IDEs configured, use saved configurations
|
||||||
const preConfiguredIdes = {};
|
const preConfiguredIdes = {};
|
||||||
const savedIdeConfigs = config._savedIdeConfigs || {};
|
const savedIdeConfigs = customConfig._savedIdeConfigs || {};
|
||||||
|
|
||||||
for (const ide of config.ides || []) {
|
for (const ide of config.ides || []) {
|
||||||
// Use saved config if available, otherwise mark as already configured (legacy)
|
// Use saved config if available, otherwise mark as already configured (legacy)
|
||||||
|
|
@ -364,8 +379,8 @@ class Installer {
|
||||||
toolSelection = await this.collectToolConfigurations(
|
toolSelection = await this.collectToolConfigurations(
|
||||||
paths.projectRoot,
|
paths.projectRoot,
|
||||||
config.modules,
|
config.modules,
|
||||||
config._isFullReinstall || false,
|
customConfig._isFullReinstall || false,
|
||||||
config._previouslyConfiguredIdes || [],
|
customConfig._previouslyConfiguredIdes || [],
|
||||||
preSelectedIdes,
|
preSelectedIdes,
|
||||||
config.skipPrompts || false,
|
config.skipPrompts || false,
|
||||||
);
|
);
|
||||||
|
|
@ -397,8 +412,8 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
|
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
|
||||||
if (config._isUpdate && config._existingInstall) {
|
if (customConfig._isUpdate && customConfig._existingInstall) {
|
||||||
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);
|
const previouslyInstalledIdes = new Set(customConfig._existingInstall.ides || []);
|
||||||
const newlySelectedIdes = new Set(config.ides || []);
|
const newlySelectedIdes = new Set(config.ides || []);
|
||||||
|
|
||||||
const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide));
|
const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide));
|
||||||
|
|
@ -491,15 +506,15 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom content is already handled in UI before module selection
|
// Custom content is already handled in UI before module selection
|
||||||
const finalCustomContent = config.customContent;
|
const finalCustomContent = customConfig.customContent;
|
||||||
|
|
||||||
// Build custom module ID set first (needed to filter official list)
|
// Build custom module ID set first (needed to filter official list)
|
||||||
const customModuleIds = new Set();
|
const customModuleIds = new Set();
|
||||||
for (const id of this.customModules.paths.keys()) {
|
for (const id of this.customModules.paths.keys()) {
|
||||||
customModuleIds.add(id);
|
customModuleIds.add(id);
|
||||||
}
|
}
|
||||||
if (config._customModuleSources) {
|
if (customConfig._customModuleSources) {
|
||||||
for (const [moduleId, customInfo] of config._customModuleSources) {
|
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
|
||||||
if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
|
if (!customModuleIds.has(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
|
||||||
customModuleIds.add(moduleId);
|
customModuleIds.add(moduleId);
|
||||||
}
|
}
|
||||||
|
|
@ -520,7 +535,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Official modules: from config.modules, excluding core (handled separately) and custom modules
|
// Official modules: from config.modules, excluding core (handled separately) and custom modules
|
||||||
const officialModules = (config.modules || []).filter((m) => !(config.installCore && m === 'core') && !customModuleIds.has(m));
|
const officialModules = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
||||||
|
|
||||||
// Combined list for manifest generation and IDE setup
|
// Combined list for manifest generation and IDE setup
|
||||||
const allModules = [...officialModules];
|
const allModules = [...officialModules];
|
||||||
|
|
@ -536,7 +551,7 @@ class Installer {
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
|
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
const isQuickUpdate = config._quickUpdate || false;
|
const isQuickUpdate = customConfig._quickUpdate || false;
|
||||||
|
|
||||||
// Collect directory creation results for output after tasks() completes
|
// Collect directory creation results for output after tasks() completes
|
||||||
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||||
|
|
@ -544,20 +559,7 @@ class Installer {
|
||||||
// Build task list conditionally
|
// Build task list conditionally
|
||||||
const installTasks = [];
|
const installTasks = [];
|
||||||
|
|
||||||
// Core installation task
|
// Module installation task (core is just another module in the list)
|
||||||
if (config.installCore) {
|
|
||||||
installTasks.push({
|
|
||||||
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
|
|
||||||
task: async (message) => {
|
|
||||||
await this.installCore(paths.bmadDir);
|
|
||||||
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
|
|
||||||
await this.generateModuleConfigs(paths.bmadDir, { core: config.coreConfig || {} });
|
|
||||||
return isQuickUpdate ? 'Core updated' : 'Core installed';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module installation task
|
|
||||||
if (allModules.length > 0) {
|
if (allModules.length > 0) {
|
||||||
installTasks.push({
|
installTasks.push({
|
||||||
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
||||||
|
|
@ -569,7 +571,7 @@ class Installer {
|
||||||
installedModuleNames,
|
installedModuleNames,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this._installCustomModules(config, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, {
|
await this._installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, {
|
||||||
message,
|
message,
|
||||||
installedModuleNames,
|
installedModuleNames,
|
||||||
});
|
});
|
||||||
|
|
@ -590,24 +592,7 @@ class Installer {
|
||||||
warn: async (msg) => await prompts.log.warn(msg),
|
warn: async (msg) => await prompts.log.warn(msg),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Core module directories
|
// Module directories (core is in config.modules like any other module)
|
||||||
if (config.installCore) {
|
|
||||||
const result = await this.officialModules.createModuleDirectories('core', paths.bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs.core || {},
|
|
||||||
existingModuleConfig: this.configCollector.existingConfig?.core || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
dirResults.createdDirs.push(...result.createdDirs);
|
|
||||||
dirResults.movedDirs.push(...(result.movedDirs || []));
|
|
||||||
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User-selected module directories
|
|
||||||
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) {
|
||||||
message(`Setting up ${moduleName}...`);
|
message(`Setting up ${moduleName}...`);
|
||||||
|
|
@ -649,17 +634,17 @@ class Installer {
|
||||||
message('Generating manifests...');
|
message('Generating manifests...');
|
||||||
const manifestGen = new ManifestGenerator();
|
const manifestGen = new ManifestGenerator();
|
||||||
|
|
||||||
const allModulesForManifest = config._quickUpdate
|
const allModulesForManifest = customConfig._quickUpdate
|
||||||
? config._existingModules || allModules || []
|
? customConfig._existingModules || allModules || []
|
||||||
: config._preserveModules
|
: customConfig._preserveModules
|
||||||
? [...allModules, ...config._preserveModules]
|
? [...allModules, ...customConfig._preserveModules]
|
||||||
: allModules || [];
|
: allModules || [];
|
||||||
|
|
||||||
let modulesForCsvPreserve;
|
let modulesForCsvPreserve;
|
||||||
if (config._quickUpdate) {
|
if (customConfig._quickUpdate) {
|
||||||
modulesForCsvPreserve = config._existingModules || allModules || [];
|
modulesForCsvPreserve = customConfig._existingModules || allModules || [];
|
||||||
} else {
|
} else {
|
||||||
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
modulesForCsvPreserve = customConfig._preserveModules ? [...allModules, ...customConfig._preserveModules] : allModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestStats = await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
|
const manifestStats = await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||||
|
|
@ -769,8 +754,9 @@ class Installer {
|
||||||
|
|
||||||
// File restoration task (only for updates)
|
// File restoration task (only for updates)
|
||||||
if (
|
if (
|
||||||
config._isUpdate &&
|
customConfig._isUpdate &&
|
||||||
((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
|
((customConfig._customFiles && customConfig._customFiles.length > 0) ||
|
||||||
|
(customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0))
|
||||||
) {
|
) {
|
||||||
postIdeTasks.push({
|
postIdeTasks.push({
|
||||||
title: 'Finalizing installation',
|
title: 'Finalizing installation',
|
||||||
|
|
@ -778,12 +764,12 @@ class Installer {
|
||||||
let customFiles = [];
|
let customFiles = [];
|
||||||
let modifiedFiles = [];
|
let modifiedFiles = [];
|
||||||
|
|
||||||
if (config._customFiles && config._customFiles.length > 0) {
|
if (customConfig._customFiles && customConfig._customFiles.length > 0) {
|
||||||
message(`Restoring ${config._customFiles.length} custom files...`);
|
message(`Restoring ${customConfig._customFiles.length} custom files...`);
|
||||||
|
|
||||||
for (const originalPath of config._customFiles) {
|
for (const originalPath of customConfig._customFiles) {
|
||||||
const relativePath = path.relative(paths.bmadDir, originalPath);
|
const relativePath = path.relative(paths.bmadDir, originalPath);
|
||||||
const backupPath = path.join(config._tempBackupDir, relativePath);
|
const backupPath = path.join(customConfig._tempBackupDir, relativePath);
|
||||||
|
|
||||||
if (await fs.pathExists(backupPath)) {
|
if (await fs.pathExists(backupPath)) {
|
||||||
await fs.ensureDir(path.dirname(originalPath));
|
await fs.ensureDir(path.dirname(originalPath));
|
||||||
|
|
@ -791,22 +777,22 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) {
|
||||||
await fs.remove(config._tempBackupDir);
|
await fs.remove(customConfig._tempBackupDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
customFiles = config._customFiles;
|
customFiles = customConfig._customFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
if (customConfig._modifiedFiles && customConfig._modifiedFiles.length > 0) {
|
||||||
modifiedFiles = config._modifiedFiles;
|
modifiedFiles = customConfig._modifiedFiles;
|
||||||
|
|
||||||
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) {
|
||||||
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
message(`Restoring ${modifiedFiles.length} modified files as .bak...`);
|
||||||
|
|
||||||
for (const modifiedFile of modifiedFiles) {
|
for (const modifiedFile of modifiedFiles) {
|
||||||
const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
|
const relativePath = path.relative(paths.bmadDir, modifiedFile.path);
|
||||||
const tempBackupPath = path.join(config._tempModifiedBackupDir, relativePath);
|
const tempBackupPath = path.join(customConfig._tempModifiedBackupDir, relativePath);
|
||||||
const bakPath = modifiedFile.path + '.bak';
|
const bakPath = modifiedFile.path + '.bak';
|
||||||
|
|
||||||
if (await fs.pathExists(tempBackupPath)) {
|
if (await fs.pathExists(tempBackupPath)) {
|
||||||
|
|
@ -815,13 +801,13 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.remove(config._tempModifiedBackupDir);
|
await fs.remove(customConfig._tempModifiedBackupDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store for summary access
|
// Store for summary access
|
||||||
config._restoredCustomFiles = customFiles;
|
customConfig._restoredCustomFiles = customFiles;
|
||||||
config._restoredModifiedFiles = modifiedFiles;
|
customConfig._restoredModifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
return 'Installation finalized';
|
return 'Installation finalized';
|
||||||
},
|
},
|
||||||
|
|
@ -831,8 +817,8 @@ class Installer {
|
||||||
await prompts.tasks(postIdeTasks);
|
await prompts.tasks(postIdeTasks);
|
||||||
|
|
||||||
// Retrieve restored file info for summary
|
// Retrieve restored file info for summary
|
||||||
const customFiles = config._restoredCustomFiles || [];
|
const customFiles = customConfig._restoredCustomFiles || [];
|
||||||
const modifiedFiles = config._restoredModifiedFiles || [];
|
const modifiedFiles = customConfig._restoredModifiedFiles || [];
|
||||||
|
|
||||||
// Render consolidated summary
|
// Render consolidated summary
|
||||||
await this.renderInstallSummary(results, {
|
await this.renderInstallSummary(results, {
|
||||||
|
|
@ -863,11 +849,11 @@ class Installer {
|
||||||
|
|
||||||
// Clean up any temp backup directories that were created before the failure
|
// Clean up any temp backup directories that were created before the failure
|
||||||
try {
|
try {
|
||||||
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
if (customConfig._tempBackupDir && (await fs.pathExists(customConfig._tempBackupDir))) {
|
||||||
await fs.remove(config._tempBackupDir);
|
await fs.remove(customConfig._tempBackupDir);
|
||||||
}
|
}
|
||||||
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
if (customConfig._tempModifiedBackupDir && (await fs.pathExists(customConfig._tempModifiedBackupDir))) {
|
||||||
await fs.remove(config._tempModifiedBackupDir);
|
await fs.remove(customConfig._tempModifiedBackupDir);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort cleanup — don't mask the original error
|
// Best-effort cleanup — don't mask the original error
|
||||||
|
|
@ -881,27 +867,27 @@ class Installer {
|
||||||
* Collect configurations for official modules (core + selected).
|
* Collect configurations for official modules (core + selected).
|
||||||
* Custom module configs are handled separately in CustomModules.discoverPaths.
|
* Custom module configs are handled separately in CustomModules.discoverPaths.
|
||||||
*/
|
*/
|
||||||
async _collectConfigs(config, paths) {
|
async _collectConfigs(customConfig, paths) {
|
||||||
// Seed core config if pre-collected from interactive UI
|
// Seed core config if pre-collected from interactive UI
|
||||||
if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
|
if (customConfig.coreConfig && Object.keys(customConfig.coreConfig).length > 0) {
|
||||||
this.configCollector.collectedConfig.core = config.coreConfig;
|
this.configCollector.collectedConfig.core = customConfig.coreConfig;
|
||||||
this.configCollector.allAnswers = {};
|
this.configCollector.allAnswers = {};
|
||||||
for (const [key, value] of Object.entries(config.coreConfig)) {
|
for (const [key, value] of Object.entries(customConfig.coreConfig)) {
|
||||||
this.configCollector.allAnswers[`core_${key}`] = value;
|
this.configCollector.allAnswers[`core_${key}`] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick update already collected everything
|
// Quick update already collected everything
|
||||||
if (config._quickUpdate) {
|
if (customConfig._quickUpdate) {
|
||||||
return this.configCollector.collectedConfig;
|
return this.configCollector.collectedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Official modules: core + selected (excluding core if already collected)
|
// Modules to collect configs for — skip core if its config was pre-collected from UI
|
||||||
const officialModules = (config.modules || []).filter((m) => m !== 'core');
|
const hasCoreConfig = customConfig.coreConfig && Object.keys(customConfig.coreConfig).length > 0;
|
||||||
const toCollect = config.coreConfig && Object.keys(config.coreConfig).length > 0 ? officialModules : ['core', ...officialModules];
|
const toCollect = hasCoreConfig ? (customConfig.modules || []).filter((m) => m !== 'core') : [...(customConfig.modules || [])];
|
||||||
|
|
||||||
return await this.configCollector.collectAllConfigurations(toCollect, paths.projectRoot, {
|
return await this.configCollector.collectAllConfigurations(toCollect, paths.projectRoot, {
|
||||||
skipPrompts: config.skipPrompts,
|
skipPrompts: customConfig.skipPrompts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -924,24 +910,20 @@ class Installer {
|
||||||
|
|
||||||
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||||
|
|
||||||
if (moduleName === 'core') {
|
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
|
||||||
await this.installCore(paths.bmadDir);
|
await this.officialModules.install(
|
||||||
} else {
|
moduleName,
|
||||||
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
|
paths.bmadDir,
|
||||||
await this.officialModules.install(
|
(filePath) => {
|
||||||
moduleName,
|
this.installedFiles.add(filePath);
|
||||||
paths.bmadDir,
|
},
|
||||||
(filePath) => {
|
{
|
||||||
this.installedFiles.add(filePath);
|
skipModuleInstaller: true,
|
||||||
},
|
moduleConfig: moduleConfig,
|
||||||
{
|
installer: this,
|
||||||
skipModuleInstaller: true,
|
silent: true,
|
||||||
moduleConfig: moduleConfig,
|
},
|
||||||
installer: this,
|
);
|
||||||
silent: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||||
}
|
}
|
||||||
|
|
@ -957,7 +939,7 @@ class Installer {
|
||||||
* @param {boolean} isQuickUpdate - Whether this is a quick update
|
* @param {boolean} isQuickUpdate - Whether this is a quick update
|
||||||
* @param {Object} ctx - Shared context: { message, installedModuleNames }
|
* @param {Object} ctx - Shared context: { message, installedModuleNames }
|
||||||
*/
|
*/
|
||||||
async _installCustomModules(config, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, ctx) {
|
async _installCustomModules(customConfig, paths, moduleConfigs, finalCustomContent, addResult, isQuickUpdate, ctx) {
|
||||||
const { message, installedModuleNames } = ctx;
|
const { message, installedModuleNames } = ctx;
|
||||||
|
|
||||||
// Collect all custom module IDs with their info from all sources
|
// Collect all custom module IDs with their info from all sources
|
||||||
|
|
@ -973,8 +955,8 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second: custom module sources from manifest (for quick update)
|
// Second: custom module sources from manifest (for quick update)
|
||||||
if (config._customModuleSources) {
|
if (customConfig._customModuleSources) {
|
||||||
for (const [moduleId, customInfo] of config._customModuleSources) {
|
for (const [moduleId, customInfo] of customConfig._customModuleSources) {
|
||||||
if (!customModules.has(moduleId)) {
|
if (!customModules.has(moduleId)) {
|
||||||
const info = { ...customInfo };
|
const info = { ...customInfo };
|
||||||
if (info.sourcePath && !info.path) {
|
if (info.sourcePath && !info.path) {
|
||||||
|
|
@ -1030,7 +1012,7 @@ class Installer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await this.generateModuleConfigs(paths.bmadDir, {
|
await this.generateModuleConfigs(paths.bmadDir, {
|
||||||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
[moduleName]: { ...customConfig.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
});
|
});
|
||||||
|
|
||||||
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||||
|
|
@ -1837,12 +1819,9 @@ class Installer {
|
||||||
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
||||||
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
||||||
|
|
||||||
// Core module is special - never include it in update flow
|
|
||||||
const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
|
|
||||||
|
|
||||||
// Only update modules that are BOTH installed AND available (we have source for)
|
// Only update modules that are BOTH installed AND available (we have source for)
|
||||||
const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
|
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
|
||||||
const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
|
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
|
||||||
|
|
||||||
// Add custom modules that were kept without sources to the skipped modules
|
// Add custom modules that were kept without sources to the skipped modules
|
||||||
// This ensures their agents are preserved in the manifest
|
// This ensures their agents are preserved in the manifest
|
||||||
|
|
@ -1892,10 +1871,8 @@ class Installer {
|
||||||
// Build the config object for the installer
|
// Build the config object for the installer
|
||||||
const installConfig = {
|
const installConfig = {
|
||||||
directory: projectDir,
|
directory: projectDir,
|
||||||
installCore: true,
|
modules: modulesToUpdate, // Only update modules we have source for (includes core)
|
||||||
modules: modulesToUpdate, // Only update modules we have source for
|
|
||||||
ides: configuredIdes,
|
ides: configuredIdes,
|
||||||
skipIde: configuredIdes.length === 0,
|
|
||||||
coreConfig: this.configCollector.collectedConfig.core,
|
coreConfig: this.configCollector.collectedConfig.core,
|
||||||
actionType: 'install', // Use regular install flow
|
actionType: 'install', // Use regular install flow
|
||||||
_quickUpdate: true, // Flag to skip certain prompts
|
_quickUpdate: true, // Flag to skip certain prompts
|
||||||
|
|
@ -1917,9 +1894,9 @@ class Installer {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
moduleCount: modulesToUpdate.length + 1, // +1 for core
|
moduleCount: modulesToUpdate.length,
|
||||||
hadNewFields: promptedForNewFields,
|
hadNewFields: promptedForNewFields,
|
||||||
modules: ['core', ...modulesToUpdate],
|
modules: modulesToUpdate,
|
||||||
skippedModules: skippedModules,
|
skippedModules: skippedModules,
|
||||||
ides: configuredIdes,
|
ides: configuredIdes,
|
||||||
};
|
};
|
||||||
|
|
@ -2062,14 +2039,15 @@ class Installer {
|
||||||
* Private: Update core
|
* Private: Update core
|
||||||
*/
|
*/
|
||||||
async updateCore(bmadDir, force = false) {
|
async updateCore(bmadDir, force = false) {
|
||||||
const sourcePath = getModulePath('core');
|
|
||||||
const targetPath = path.join(bmadDir, 'core');
|
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await fs.remove(targetPath);
|
await this.officialModules.install('core', bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
||||||
await this.installCore(bmadDir);
|
skipModuleInstaller: true,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Selective update - preserve user modifications
|
// Selective update - preserve user modifications
|
||||||
|
const sourcePath = getModulePath('core');
|
||||||
|
const targetPath = path.join(bmadDir, 'core');
|
||||||
await this.fileOps.syncDirectory(sourcePath, targetPath);
|
await this.fileOps.syncDirectory(sourcePath, targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2554,83 +2532,6 @@ class Installer {
|
||||||
return '_bmad-output';
|
return '_bmad-output';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Private: Install core
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
*/
|
|
||||||
async installCore(bmadDir) {
|
|
||||||
const sourcePath = getModulePath('core');
|
|
||||||
const targetPath = path.join(bmadDir, 'core');
|
|
||||||
|
|
||||||
// Copy core files
|
|
||||||
await this.copyCoreFiles(sourcePath, targetPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy core files (similar to copyModuleWithFiltering but for core)
|
|
||||||
* @param {string} sourcePath - Source path
|
|
||||||
* @param {string} targetPath - Target path
|
|
||||||
*/
|
|
||||||
async copyCoreFiles(sourcePath, targetPath) {
|
|
||||||
// Get all files in source
|
|
||||||
const files = await this.getFileList(sourcePath);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// Skip sub-modules directory - these are IDE-specific and handled separately
|
|
||||||
if (file.startsWith('sub-modules/')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip module.yaml at root - it's only needed at install time
|
|
||||||
if (file === 'module.yaml') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip config.yaml templates - we'll generate clean ones with actual values
|
|
||||||
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceFile = path.join(sourcePath, file);
|
|
||||||
const targetFile = path.join(targetPath, file);
|
|
||||||
|
|
||||||
// Copy the file with placeholder replacement
|
|
||||||
await fs.ensureDir(path.dirname(targetFile));
|
|
||||||
await this.copyFile(sourceFile, targetFile);
|
|
||||||
|
|
||||||
// Track the installed file
|
|
||||||
this.installedFiles.add(targetFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyFile(sourcePath, targetPath) {
|
|
||||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of all files in a directory recursively
|
|
||||||
* @param {string} dir - Directory path
|
|
||||||
* @param {string} baseDir - Base directory for relative paths
|
|
||||||
* @returns {Array} List of 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()) {
|
|
||||||
const subFiles = await this.getFileList(fullPath, baseDir);
|
|
||||||
files.push(...subFiles);
|
|
||||||
} else {
|
|
||||||
files.push(path.relative(baseDir, fullPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a CSV line, handling quoted fields
|
* Parse a CSV line, handling quoted fields
|
||||||
* @param {string} line - CSV line to parse
|
* @param {string} line - CSV line to parse
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,7 @@ class OfficialModules {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all available modules (excluding core which is always installed)
|
* List all available built-in modules (core and bmm).
|
||||||
* bmm is the only built-in module, directly under src/bmm-skills
|
|
||||||
* All other modules come from external-official-modules.yaml
|
* All other modules come from external-official-modules.yaml
|
||||||
* @returns {Object} Object with modules array and customModules array
|
* @returns {Object} Object with modules array and customModules array
|
||||||
*/
|
*/
|
||||||
|
|
@ -52,6 +51,15 @@ class OfficialModules {
|
||||||
const modules = [];
|
const modules = [];
|
||||||
const customModules = [];
|
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)
|
// Add built-in bmm module (directly under src/bmm-skills)
|
||||||
const bmmPath = getSourcePath('bmm-skills');
|
const bmmPath = getSourcePath('bmm-skills');
|
||||||
if (await fs.pathExists(bmmPath)) {
|
if (await fs.pathExists(bmmPath)) {
|
||||||
|
|
|
||||||
|
|
@ -423,8 +423,10 @@ class UI {
|
||||||
selectedModules.push(...customModuleResult.selectedCustomModules);
|
selectedModules.push(...customModuleResult.selectedCustomModules);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out core - it's always installed via installCore flag
|
// Ensure core is in the modules list
|
||||||
selectedModules = selectedModules.filter((m) => m !== 'core');
|
if (!selectedModules.includes('core')) {
|
||||||
|
selectedModules.unshift('core');
|
||||||
|
}
|
||||||
|
|
||||||
// Get tool selection
|
// Get tool selection
|
||||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
||||||
|
|
@ -434,7 +436,6 @@ class UI {
|
||||||
return {
|
return {
|
||||||
actionType: 'update',
|
actionType: 'update',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
installCore: true,
|
|
||||||
modules: selectedModules,
|
modules: selectedModules,
|
||||||
ides: toolSelection.ides,
|
ides: toolSelection.ides,
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
|
|
@ -543,14 +544,16 @@ class UI {
|
||||||
selectedModules.push(...customContentConfig.selectedModuleIds);
|
selectedModules.push(...customContentConfig.selectedModuleIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedModules = selectedModules.filter((m) => m !== 'core');
|
// Ensure core is in the modules list
|
||||||
|
if (!selectedModules.includes('core')) {
|
||||||
|
selectedModules.unshift('core');
|
||||||
|
}
|
||||||
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
|
||||||
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
|
const coreConfig = await this.collectCoreConfig(confirmedDirectory, options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actionType: 'install',
|
actionType: 'install',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
installCore: true,
|
|
||||||
modules: selectedModules,
|
modules: selectedModules,
|
||||||
ides: toolSelection.ides,
|
ides: toolSelection.ides,
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
|
|
@ -1069,7 +1072,7 @@ class UI {
|
||||||
maxItems: allOptions.length,
|
maxItems: allOptions.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = selected ? selected.filter((m) => m !== 'core') : [];
|
const result = selected ? [...selected] : [];
|
||||||
|
|
||||||
// Display selected modules as bulleted list
|
// Display selected modules as bulleted list
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue