feat: add batch module configuration gateway with express/customize modes
Replace N individual "Accept Defaults?" confirm prompts with a single
select gateway ("Express Setup" vs "Customize"). When customizing, a
multiselect shows only modules with configurable options. All others
silently receive defaults via spinner progress.
- Add scanModuleSchemas() to pre-scan module metadata and display names
- Add select/multiselect gateway in collectAllConfigurations()
- Replace per-module confirm with modulesToCustomize Set check
- Suppress UI output during silent default config via _silentConfig flag
- Reorder installer tasks: module dirs before config generation
- Add resolution null guards for edge-case safety
- Cache ModuleManager instance via _getModuleManager() for reuse
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f73ea2cb18
commit
2f28ef1b80
|
|
@ -10,6 +10,19 @@ class ConfigCollector {
|
||||||
this.collectedConfig = {};
|
this.collectedConfig = {};
|
||||||
this.existingConfig = null;
|
this.existingConfig = null;
|
||||||
this.currentProjectDir = null;
|
this.currentProjectDir = null;
|
||||||
|
this._moduleManagerInstance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a cached ModuleManager instance (lazy initialization)
|
||||||
|
* @returns {Object} ModuleManager instance
|
||||||
|
*/
|
||||||
|
_getModuleManager() {
|
||||||
|
if (!this._moduleManagerInstance) {
|
||||||
|
const { ModuleManager } = require('../modules/manager');
|
||||||
|
this._moduleManagerInstance = new ModuleManager();
|
||||||
|
}
|
||||||
|
return this._moduleManagerInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -129,6 +142,70 @@ class ConfigCollector {
|
||||||
return foundAny;
|
return foundAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-scan module schemas to gather metadata for the configuration gateway prompt.
|
||||||
|
* Returns info about which modules have configurable options.
|
||||||
|
* @param {Array} modules - List of non-core module names
|
||||||
|
* @returns {Promise<Array>} Array of {moduleName, displayName, questionCount, hasFieldsWithoutDefaults}
|
||||||
|
*/
|
||||||
|
async scanModuleSchemas(modules) {
|
||||||
|
const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const moduleName of modules) {
|
||||||
|
// Resolve module.yaml path - custom paths first, then standard location, then ModuleManager search
|
||||||
|
let moduleConfigPath = null;
|
||||||
|
const customPath = this.customModulePaths?.get(moduleName);
|
||||||
|
if (customPath) {
|
||||||
|
moduleConfigPath = path.join(customPath, 'module.yaml');
|
||||||
|
} else {
|
||||||
|
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||||
|
if (await fs.pathExists(standardPath)) {
|
||||||
|
moduleConfigPath = standardPath;
|
||||||
|
} else {
|
||||||
|
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||||
|
if (moduleSourcePath) {
|
||||||
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!moduleConfigPath || !(await fs.pathExists(moduleConfigPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
||||||
|
const moduleConfig = yaml.parse(content);
|
||||||
|
if (!moduleConfig) continue;
|
||||||
|
|
||||||
|
const displayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
|
||||||
|
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
|
||||||
|
const questionKeys = configKeys.filter((key) => {
|
||||||
|
if (metadataFields.has(key)) return false;
|
||||||
|
const item = moduleConfig[key];
|
||||||
|
return item && typeof item === 'object' && item.prompt;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasFieldsWithoutDefaults = questionKeys.some((key) => {
|
||||||
|
const item = moduleConfig[key];
|
||||||
|
return item.default === undefined || item.default === null || item.default === '';
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
moduleName,
|
||||||
|
displayName,
|
||||||
|
questionCount: questionKeys.length,
|
||||||
|
hasFieldsWithoutDefaults,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(`Could not read schema for module "${moduleName}": ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration for all modules
|
* Collect configuration for all modules
|
||||||
* @param {Array} modules - List of modules to configure (including 'core')
|
* @param {Array} modules - List of modules to configure (including 'core')
|
||||||
|
|
@ -141,6 +218,7 @@ class ConfigCollector {
|
||||||
// Store custom module paths for use in collectModuleConfig
|
// Store custom module paths for use in collectModuleConfig
|
||||||
this.customModulePaths = options.customModulePaths || new Map();
|
this.customModulePaths = options.customModulePaths || new Map();
|
||||||
this.skipPrompts = options.skipPrompts || false;
|
this.skipPrompts = options.skipPrompts || false;
|
||||||
|
this.modulesToCustomize = undefined;
|
||||||
await this.loadExistingConfig(projectDir);
|
await this.loadExistingConfig(projectDir);
|
||||||
|
|
||||||
// Check if core was already collected (e.g., in early collection phase)
|
// Check if core was already collected (e.g., in early collection phase)
|
||||||
|
|
@ -154,10 +232,95 @@ class ConfigCollector {
|
||||||
this.allAnswers = {};
|
this.allAnswers = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const moduleName of allModules) {
|
// Split processing: core first, then gateway, then remaining modules
|
||||||
|
const coreModules = allModules.filter((m) => m === 'core');
|
||||||
|
const nonCoreModules = allModules.filter((m) => m !== 'core');
|
||||||
|
|
||||||
|
// Collect core config first (always fully prompted)
|
||||||
|
for (const moduleName of coreModules) {
|
||||||
await this.collectModuleConfig(moduleName, projectDir);
|
await this.collectModuleConfig(moduleName, projectDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show batch configuration gateway for non-core modules
|
||||||
|
// Scan all non-core module schemas for display names and config metadata
|
||||||
|
let scannedModules = [];
|
||||||
|
if (!this.skipPrompts && nonCoreModules.length > 0) {
|
||||||
|
scannedModules = await this.scanModuleSchemas(nonCoreModules);
|
||||||
|
const customizableModules = scannedModules.filter((m) => m.questionCount > 0);
|
||||||
|
|
||||||
|
if (customizableModules.length > 0) {
|
||||||
|
const configMode = await prompts.select({
|
||||||
|
message: 'Module configuration',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Express Setup', value: 'express', hint: 'accept all defaults (recommended)' },
|
||||||
|
{ name: 'Customize', value: 'customize', hint: 'choose modules to configure' },
|
||||||
|
],
|
||||||
|
default: 'express',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (configMode === 'customize') {
|
||||||
|
const choices = customizableModules.map((m) => ({
|
||||||
|
name: `${m.displayName} (${m.questionCount} option${m.questionCount === 1 ? '' : 's'})`,
|
||||||
|
value: m.moduleName,
|
||||||
|
hint: m.hasFieldsWithoutDefaults ? 'has fields without defaults' : undefined,
|
||||||
|
checked: m.hasFieldsWithoutDefaults,
|
||||||
|
}));
|
||||||
|
const selected = await prompts.multiselect({
|
||||||
|
message: 'Select modules to customize:',
|
||||||
|
choices,
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
this.modulesToCustomize = new Set(selected);
|
||||||
|
} else {
|
||||||
|
// Express mode: no modules to customize
|
||||||
|
this.modulesToCustomize = new Set();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// All non-core modules have zero config - no gateway needed
|
||||||
|
this.modulesToCustomize = new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect remaining non-core modules
|
||||||
|
if (this.modulesToCustomize === undefined) {
|
||||||
|
// No gateway was shown (skipPrompts, no non-core modules, or direct call) - process all normally
|
||||||
|
for (const moduleName of nonCoreModules) {
|
||||||
|
await this.collectModuleConfig(moduleName, projectDir);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Split into default modules (tasks progress) and customized modules (interactive)
|
||||||
|
const defaultModules = nonCoreModules.filter((m) => !this.modulesToCustomize.has(m));
|
||||||
|
const customizeModules = nonCoreModules.filter((m) => this.modulesToCustomize.has(m));
|
||||||
|
|
||||||
|
// Run default modules with a single spinner
|
||||||
|
if (defaultModules.length > 0) {
|
||||||
|
// Build display name map from all scanned modules for pre-call spinner messages
|
||||||
|
const displayNameMap = new Map();
|
||||||
|
for (const m of scannedModules) {
|
||||||
|
displayNameMap.set(m.moduleName, m.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configSpinner = await prompts.spinner();
|
||||||
|
configSpinner.start('Configuring modules...');
|
||||||
|
for (const moduleName of defaultModules) {
|
||||||
|
const displayName = displayNameMap.get(moduleName) || moduleName.toUpperCase();
|
||||||
|
configSpinner.message(`Configuring ${displayName}...`);
|
||||||
|
try {
|
||||||
|
this._silentConfig = true;
|
||||||
|
await this.collectModuleConfig(moduleName, projectDir);
|
||||||
|
} finally {
|
||||||
|
this._silentConfig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configSpinner.stop('Module configuration complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run customized modules individually (may show interactive prompts)
|
||||||
|
for (const moduleName of customizeModules) {
|
||||||
|
await this.collectModuleConfig(moduleName, projectDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add metadata
|
// Add metadata
|
||||||
this.collectedConfig._meta = {
|
this.collectedConfig._meta = {
|
||||||
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
version: require(path.join(getProjectRoot(), 'package.json')).version,
|
||||||
|
|
@ -194,10 +357,7 @@ class ConfigCollector {
|
||||||
|
|
||||||
// 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(moduleConfigPath))) {
|
if (!(await fs.pathExists(moduleConfigPath))) {
|
||||||
// Use the module manager to find the module source
|
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||||
const { ModuleManager } = require('../modules/manager');
|
|
||||||
const moduleManager = new ModuleManager();
|
|
||||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
|
||||||
|
|
||||||
if (moduleSourcePath) {
|
if (moduleSourcePath) {
|
||||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
|
|
@ -211,9 +371,7 @@ class ConfigCollector {
|
||||||
configPath = moduleConfigPath;
|
configPath = moduleConfigPath;
|
||||||
} 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 moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||||
const moduleManager = new ModuleManager();
|
|
||||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
|
||||||
|
|
||||||
if (moduleSourcePath) {
|
if (moduleSourcePath) {
|
||||||
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
|
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
|
||||||
|
|
@ -507,10 +665,7 @@ class ConfigCollector {
|
||||||
|
|
||||||
// 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(moduleConfigPath))) {
|
if (!(await fs.pathExists(moduleConfigPath))) {
|
||||||
// Use the module manager to find the module source
|
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||||
const { ModuleManager } = require('../modules/manager');
|
|
||||||
const moduleManager = new ModuleManager();
|
|
||||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
|
||||||
|
|
||||||
if (moduleSourcePath) {
|
if (moduleSourcePath) {
|
||||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||||
|
|
@ -579,12 +734,12 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await prompts.log.step(moduleDisplayName);
|
if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
|
||||||
let customize = true;
|
let useDefaults = true;
|
||||||
if (moduleName === 'core') {
|
if (moduleName === 'core') {
|
||||||
// Core module: no confirm prompt, continues directly
|
useDefaults = false; // Core: always show all questions
|
||||||
} else {
|
} else if (this.modulesToCustomize === undefined) {
|
||||||
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
|
// Fallback: original per-module confirm (backward compat for direct calls)
|
||||||
const customizeAnswer = await prompts.prompt([
|
const customizeAnswer = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
|
|
@ -593,10 +748,13 @@ class ConfigCollector {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
customize = customizeAnswer.customize;
|
useDefaults = customizeAnswer.customize;
|
||||||
|
} else {
|
||||||
|
// Batch mode: use defaults unless module was selected for customization
|
||||||
|
useDefaults = !this.modulesToCustomize.has(moduleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customize && moduleName !== 'core') {
|
if (useDefaults && moduleName !== 'core') {
|
||||||
// Accept defaults - only ask questions that have NO default value
|
// Accept defaults - only ask questions that have NO default value
|
||||||
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
|
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
|
||||||
|
|
||||||
|
|
@ -726,6 +884,7 @@ class ConfigCollector {
|
||||||
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
|
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
|
||||||
const hasNoConfig = actualConfigKeys.length === 0;
|
const hasNoConfig = actualConfigKeys.length === 0;
|
||||||
|
|
||||||
|
if (!this._silentConfig) {
|
||||||
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||||
await prompts.log.step(moduleDisplayName);
|
await prompts.log.step(moduleDisplayName);
|
||||||
if (moduleConfig.subheader) {
|
if (moduleConfig.subheader) {
|
||||||
|
|
@ -738,6 +897,7 @@ class ConfigCollector {
|
||||||
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we have no collected config for this module, but we have a module schema,
|
// If we have no collected config for this module, but we have a module schema,
|
||||||
// ensure we have at least an empty object
|
// ensure we have at least an empty object
|
||||||
|
|
|
||||||
|
|
@ -882,6 +882,9 @@ class Installer {
|
||||||
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
|
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
|
||||||
let taskResolution;
|
let taskResolution;
|
||||||
|
|
||||||
|
// Collect directory creation results for output after tasks() completes
|
||||||
|
const dirResults = { createdDirs: [], createdWdsFolders: [] };
|
||||||
|
|
||||||
// Build task list conditionally
|
// Build task list conditionally
|
||||||
const installTasks = [];
|
const installTasks = [];
|
||||||
|
|
||||||
|
|
@ -992,6 +995,10 @@ class Installer {
|
||||||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (!resolution || !resolution.byModule) {
|
||||||
|
addResult(`Module: ${moduleName}`, 'warn', 'skipped (no resolution data)');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (moduleName === 'core') {
|
if (moduleName === 'core') {
|
||||||
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1003,6 +1010,9 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install partial modules (only dependencies)
|
// Install partial modules (only dependencies)
|
||||||
|
if (!resolution || !resolution.byModule) {
|
||||||
|
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
||||||
|
}
|
||||||
for (const [module, files] of Object.entries(resolution.byModule)) {
|
for (const [module, files] of Object.entries(resolution.byModule)) {
|
||||||
if (!allModules.includes(module) && module !== 'core') {
|
if (!allModules.includes(module) && module !== 'core') {
|
||||||
const totalFiles =
|
const totalFiles =
|
||||||
|
|
@ -1024,8 +1034,62 @@ class Installer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration generation task
|
// Module directory creation task
|
||||||
installTasks.push({
|
installTasks.push({
|
||||||
|
title: 'Creating module directories',
|
||||||
|
task: async (message) => {
|
||||||
|
const resolution = taskResolution;
|
||||||
|
if (!resolution || !resolution.byModule) {
|
||||||
|
addResult('Module directories', 'warn', 'no resolution data');
|
||||||
|
return 'Module directories skipped (no resolution data)';
|
||||||
|
}
|
||||||
|
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
||||||
|
const moduleLogger = {
|
||||||
|
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
||||||
|
error: async (msg) => await prompts.log.error(msg),
|
||||||
|
warn: async (msg) => await prompts.log.warn(msg),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Core module directories
|
||||||
|
if (config.installCore || resolution.byModule.core) {
|
||||||
|
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
|
||||||
|
installedIDEs: config.ides || [],
|
||||||
|
moduleConfig: moduleConfigs.core || {},
|
||||||
|
coreConfig: moduleConfigs.core || {},
|
||||||
|
logger: moduleLogger,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
dirResults.createdDirs.push(...result.createdDirs);
|
||||||
|
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User-selected module directories
|
||||||
|
if (config.modules && config.modules.length > 0) {
|
||||||
|
for (const moduleName of config.modules) {
|
||||||
|
message(`Setting up ${moduleName}...`);
|
||||||
|
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
||||||
|
installedIDEs: config.ides || [],
|
||||||
|
moduleConfig: moduleConfigs[moduleName] || {},
|
||||||
|
coreConfig: moduleConfigs.core || {},
|
||||||
|
logger: moduleLogger,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
dirResults.createdDirs.push(...result.createdDirs);
|
||||||
|
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addResult('Module directories', 'ok');
|
||||||
|
return 'Module directories created';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration generation task (stored as named reference for deferred execution)
|
||||||
|
const configTask = {
|
||||||
title: 'Generating configurations',
|
title: 'Generating configurations',
|
||||||
task: async (message) => {
|
task: async (message) => {
|
||||||
// Generate clean config.yaml files for each installed module
|
// Generate clean config.yaml files for each installed module
|
||||||
|
|
@ -1075,9 +1139,26 @@ class Installer {
|
||||||
|
|
||||||
return 'Configurations generated';
|
return 'Configurations generated';
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
installTasks.push(configTask);
|
||||||
|
|
||||||
await prompts.tasks(installTasks);
|
// Run all tasks except config (which runs after directory output)
|
||||||
|
const mainTasks = installTasks.filter((t) => t !== configTask);
|
||||||
|
await prompts.tasks(mainTasks);
|
||||||
|
|
||||||
|
// Render directory creation output right after directory task
|
||||||
|
const color = await prompts.getColor();
|
||||||
|
if (dirResults.createdDirs.length > 0) {
|
||||||
|
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
|
||||||
|
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
|
||||||
|
}
|
||||||
|
if (dirResults.createdWdsFolders.length > 0) {
|
||||||
|
const lines = dirResults.createdWdsFolders.map((f) => color.dim(` \u2713 ${f}/`)).join('\n');
|
||||||
|
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now run configuration generation
|
||||||
|
await prompts.tasks([configTask]);
|
||||||
|
|
||||||
// Resolution is now available via closure-scoped taskResolution
|
// Resolution is now available via closure-scoped taskResolution
|
||||||
const resolution = taskResolution;
|
const resolution = taskResolution;
|
||||||
|
|
@ -1094,7 +1175,7 @@ class Installer {
|
||||||
} else {
|
} else {
|
||||||
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
||||||
const ideSpinner = await prompts.spinner();
|
const ideSpinner = await prompts.spinner();
|
||||||
ideSpinner.start('Configuring IDEs...');
|
ideSpinner.start('Configuring tools...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const ide of validIdes) {
|
for (const ide of validIdes) {
|
||||||
|
|
@ -1134,12 +1215,12 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsPrompting && !ideSpinner.isSpinning) {
|
if (needsPrompting && !ideSpinner.isSpinning) {
|
||||||
ideSpinner.start('Configuring IDEs...');
|
ideSpinner.start('Configuring tools...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (ideSpinner.isSpinning) {
|
if (ideSpinner.isSpinning) {
|
||||||
ideSpinner.stop('IDE configuration complete');
|
ideSpinner.stop('Tool configuration complete');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1150,58 +1231,6 @@ class Installer {
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
const postIdeTasks = [];
|
const postIdeTasks = [];
|
||||||
|
|
||||||
// Collect directory creation results for output after tasks() completes
|
|
||||||
const dirResults = { createdDirs: [], createdWdsFolders: [] };
|
|
||||||
|
|
||||||
// Module directory creation task
|
|
||||||
postIdeTasks.push({
|
|
||||||
title: 'Creating module directories',
|
|
||||||
task: async (message) => {
|
|
||||||
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
|
||||||
const moduleLogger = {
|
|
||||||
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
|
||||||
error: async (msg) => await prompts.log.error(msg),
|
|
||||||
warn: async (msg) => await prompts.log.warn(msg),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Core module directories
|
|
||||||
if (config.installCore || resolution.byModule.core) {
|
|
||||||
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs.core || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
dirResults.createdDirs.push(...result.createdDirs);
|
|
||||||
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User-selected module directories
|
|
||||||
if (config.modules && config.modules.length > 0) {
|
|
||||||
for (const moduleName of config.modules) {
|
|
||||||
message(`Setting up ${moduleName}...`);
|
|
||||||
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs[moduleName] || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
dirResults.createdDirs.push(...result.createdDirs);
|
|
||||||
dirResults.createdWdsFolders.push(...result.createdWdsFolders);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addResult('Module directories', 'ok');
|
|
||||||
return 'Module setup complete';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// File restoration task (only for updates)
|
// File restoration task (only for updates)
|
||||||
if (
|
if (
|
||||||
config._isUpdate &&
|
config._isUpdate &&
|
||||||
|
|
@ -1265,18 +1294,6 @@ class Installer {
|
||||||
|
|
||||||
await prompts.tasks(postIdeTasks);
|
await prompts.tasks(postIdeTasks);
|
||||||
|
|
||||||
// Render directory creation output after tasks() to avoid breaking progress display
|
|
||||||
if (dirResults.createdDirs.length > 0) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
const lines = dirResults.createdDirs.map((d) => ` ${d}`).join('\n');
|
|
||||||
await prompts.log.message(color.yellow(`Created directories:\n${lines}`));
|
|
||||||
}
|
|
||||||
if (dirResults.createdWdsFolders.length > 0) {
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
const lines = dirResults.createdWdsFolders.map((f) => color.dim(` ✓ ${f}/`)).join('\n');
|
|
||||||
await prompts.log.message(color.cyan(`Created WDS folder structure:\n${lines}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve restored file info for summary
|
// Retrieve restored file info for summary
|
||||||
const customFiles = config._restoredCustomFiles || [];
|
const customFiles = config._restoredCustomFiles || [];
|
||||||
const modifiedFiles = config._restoredModifiedFiles || [];
|
const modifiedFiles = config._restoredModifiedFiles || [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue