Compare commits
2 Commits
7c498a9fbe
...
02a43f96d7
| Author | SHA1 | Date |
|---|---|---|
|
|
02a43f96d7 | |
|
|
0bf8e0edfb |
|
|
@ -39,7 +39,6 @@ module.exports = {
|
||||||
if (config.actionType === 'cancel') {
|
if (config.actionType === 'cancel') {
|
||||||
await prompts.log.warn('Installation cancelled.');
|
await prompts.log.warn('Installation cancelled.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
|
|
@ -47,23 +46,14 @@ module.exports = {
|
||||||
const result = await installer.quickUpdate(config);
|
const result = await installer.quickUpdate(config);
|
||||||
await prompts.log.success('Quick update complete!');
|
await prompts.log.success('Quick update complete!');
|
||||||
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
|
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`);
|
||||||
|
|
||||||
// Display version-specific end message
|
|
||||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
|
||||||
const messageLoader = new MessageLoader();
|
|
||||||
await messageLoader.displayEndMessage();
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle compile agents separately
|
// Handle compile agents separately
|
||||||
if (config.actionType === 'compile-agents') {
|
if (config.actionType === 'compile-agents') {
|
||||||
const result = await installer.compileAgents(config);
|
const result = await installer.compileAgents(config);
|
||||||
await prompts.log.success('Agent recompilation complete!');
|
|
||||||
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
|
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular install/update flow
|
// Regular install/update flow
|
||||||
|
|
@ -72,16 +62,10 @@ module.exports = {
|
||||||
// Check if installation was cancelled
|
// Check if installation was cancelled
|
||||||
if (result && result.cancelled) {
|
if (result && result.cancelled) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if installation succeeded
|
// Check if installation succeeded
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
// Display version-specific end message from install-messages.yaml
|
|
||||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
|
||||||
const messageLoader = new MessageLoader();
|
|
||||||
await messageLoader.displayEndMessage();
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -14,28 +14,10 @@ startMessage: |
|
||||||
but anticipate no massive breaking changes
|
but anticipate no massive breaking changes
|
||||||
- Groundwork in place for customization and community modules
|
- Groundwork in place for customization and community modules
|
||||||
|
|
||||||
📚 New Docs Site: http://docs.bmad-method.org/
|
|
||||||
- High quality tutorials, guided walkthrough, and articles coming soon!
|
|
||||||
- Everything is free. No paywalls. No gated content.
|
|
||||||
- Knowledge should be shared, not sold.
|
|
||||||
|
|
||||||
💡 Love BMad? Please star us on GitHub & subscribe on YouTube!
|
|
||||||
- GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
|
||||||
- YouTube: https://www.youtube.com/@BMadCode
|
|
||||||
|
|
||||||
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
# Display at the END of installation (after all setup completes)
|
|
||||||
endMessage: |
|
|
||||||
════════════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
✨ BMAD V6 BETA IS INSTALLED! Thank you for being part of this journey!
|
|
||||||
|
|
||||||
🌟 BMad is 100% free and open source.
|
🌟 BMad is 100% free and open source.
|
||||||
- No gated Discord. No paywalls.
|
- No gated Discord. No paywalls. No gated content.
|
||||||
- We believe in empowering everyone, not just those who can pay.
|
- We believe in empowering everyone, not just those who can pay.
|
||||||
|
- Knowledge should be shared, not sold.
|
||||||
|
|
||||||
🙏 SUPPORT BMAD DEVELOPMENT:
|
🙏 SUPPORT BMAD DEVELOPMENT:
|
||||||
- During the Beta, please give us feedback and raise issues on GitHub!
|
- During the Beta, please give us feedback and raise issues on GitHub!
|
||||||
|
|
@ -47,13 +29,14 @@ endMessage: |
|
||||||
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
- Topics: AI-Native Transformation, Spec and Context Engineering, BMad Method
|
||||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
||||||
|
|
||||||
📚 RESOURCES:
|
⭐ HELP US GROW:
|
||||||
- Docs: http://docs.bmad-method.org/ (bookmark it!)
|
|
||||||
- Changelog: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
|
||||||
|
|
||||||
⭐⭐⭐ HELP US GROW:
|
|
||||||
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||||
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
||||||
- Every star & sub helps us reach more developers!
|
- Every star & sub helps us reach more developers!
|
||||||
|
|
||||||
════════════════════════════════════════════════════════════════════════════════
|
Latest updates: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
# No end message - install summary and next steps are rendered by the installer
|
||||||
|
endMessage: ""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
class CustomModuleCache {
|
class CustomModuleCache {
|
||||||
constructor(bmadDir) {
|
constructor(bmadDir) {
|
||||||
|
|
@ -195,7 +196,7 @@ class CustomModuleCache {
|
||||||
// Verify cache integrity
|
// Verify cache integrity
|
||||||
const currentCacheHash = await this.calculateHash(cacheDir);
|
const currentCacheHash = await this.calculateHash(cacheDir);
|
||||||
if (currentCacheHash !== cached.cacheHash) {
|
if (currentCacheHash !== cached.cacheHash) {
|
||||||
console.warn(`Warning: Cache integrity check failed for ${moduleId}`);
|
await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages IDE configuration persistence
|
* Manages IDE configuration persistence
|
||||||
|
|
@ -93,7 +94,7 @@ class IdeConfigManager {
|
||||||
const config = yaml.parse(content);
|
const config = yaml.parse(content);
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Failed to load IDE config for ${ideName}:`, error.message);
|
await prompts.log.warn(`Failed to load IDE config for ${ideName}: ${error.message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +124,7 @@ class IdeConfigManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Warning: Failed to load IDE configs:', error.message);
|
await prompts.log.warn(`Failed to load IDE configs: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,17 @@ class Installer {
|
||||||
* @param {boolean} isFullReinstall - Whether this is a full reinstall
|
* @param {boolean} isFullReinstall - Whether this is a full reinstall
|
||||||
* @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
|
* @param {Array} previousIdes - Previously configured IDEs (for reinstalls)
|
||||||
* @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
|
* @param {Array} preSelectedIdes - Pre-selected IDEs from early prompt (optional)
|
||||||
|
* @param {boolean} skipPrompts - Skip prompts and use defaults (for --yes flag)
|
||||||
* @returns {Object} Tool/IDE selection and configurations
|
* @returns {Object} Tool/IDE selection and configurations
|
||||||
*/
|
*/
|
||||||
async collectToolConfigurations(projectDir, selectedModules, isFullReinstall = false, previousIdes = [], preSelectedIdes = null) {
|
async collectToolConfigurations(
|
||||||
|
projectDir,
|
||||||
|
selectedModules,
|
||||||
|
isFullReinstall = false,
|
||||||
|
previousIdes = [],
|
||||||
|
preSelectedIdes = null,
|
||||||
|
skipPrompts = false,
|
||||||
|
) {
|
||||||
// Use pre-selected IDEs if provided, otherwise prompt
|
// Use pre-selected IDEs if provided, otherwise prompt
|
||||||
let toolConfig;
|
let toolConfig;
|
||||||
if (preSelectedIdes === null) {
|
if (preSelectedIdes === null) {
|
||||||
|
|
@ -182,6 +190,7 @@ class Installer {
|
||||||
selectedModules: selectedModules || [],
|
selectedModules: selectedModules || [],
|
||||||
projectDir,
|
projectDir,
|
||||||
bmadDir,
|
bmadDir,
|
||||||
|
skipPrompts,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Config-driven IDEs don't need configuration - mark as ready
|
// Config-driven IDEs don't need configuration - mark as ready
|
||||||
|
|
@ -406,6 +415,9 @@ class Installer {
|
||||||
let action = null;
|
let action = null;
|
||||||
if (config.actionType === 'update') {
|
if (config.actionType === 'update') {
|
||||||
action = 'update';
|
action = 'update';
|
||||||
|
} else if (config.skipPrompts) {
|
||||||
|
// Non-interactive mode: default to update
|
||||||
|
action = 'update';
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Ask the user (backwards compatibility for other code paths)
|
// Fallback: Ask the user (backwards compatibility for other code paths)
|
||||||
await prompts.log.warn('Existing BMAD installation detected');
|
await prompts.log.warn('Existing BMAD installation detected');
|
||||||
|
|
@ -431,9 +443,16 @@ class Installer {
|
||||||
|
|
||||||
// If there are modules to remove, ask for confirmation
|
// If there are modules to remove, ask for confirmation
|
||||||
if (modulesToRemove.length > 0) {
|
if (modulesToRemove.length > 0) {
|
||||||
const prompts = require('../../../lib/prompts');
|
if (config.skipPrompts) {
|
||||||
|
// Non-interactive mode: preserve modules (matches prompt default: false)
|
||||||
|
for (const moduleId of modulesToRemove) {
|
||||||
|
if (!config.modules) config.modules = [];
|
||||||
|
config.modules.push(moduleId);
|
||||||
|
}
|
||||||
|
spinner.start('Preparing update...');
|
||||||
|
} else {
|
||||||
if (spinner.isSpinning) {
|
if (spinner.isSpinning) {
|
||||||
spinner.stop('Reviewing module changes');
|
spinner.stop('Module changes reviewed');
|
||||||
}
|
}
|
||||||
|
|
||||||
await prompts.log.warn('Modules to be removed:');
|
await prompts.log.warn('Modules to be removed:');
|
||||||
|
|
@ -474,6 +493,7 @@ class Installer {
|
||||||
|
|
||||||
spinner.start('Preparing update...');
|
spinner.start('Preparing update...');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
||||||
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
||||||
|
|
@ -684,6 +704,7 @@ class Installer {
|
||||||
config._isFullReinstall || false,
|
config._isFullReinstall || false,
|
||||||
config._previouslyConfiguredIdes || [],
|
config._previouslyConfiguredIdes || [],
|
||||||
preSelectedIdes,
|
preSelectedIdes,
|
||||||
|
config.skipPrompts || false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,14 +713,80 @@ class Installer {
|
||||||
config.skipIde = toolSelection.skipIde;
|
config.skipIde = toolSelection.skipIde;
|
||||||
const ideConfigurations = toolSelection.configurations;
|
const ideConfigurations = toolSelection.configurations;
|
||||||
|
|
||||||
|
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
|
||||||
|
if (config._isUpdate && config._existingInstall) {
|
||||||
|
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);
|
||||||
|
const newlySelectedIdes = new Set(config.ides || []);
|
||||||
|
|
||||||
|
const idesToRemove = [...previouslyInstalledIdes].filter((ide) => !newlySelectedIdes.has(ide));
|
||||||
|
|
||||||
|
if (idesToRemove.length > 0) {
|
||||||
|
if (config.skipPrompts) {
|
||||||
|
// Non-interactive mode: silently preserve existing IDE configs
|
||||||
|
if (!config.ides) config.ides = [];
|
||||||
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||||
|
for (const ide of idesToRemove) {
|
||||||
|
config.ides.push(ide);
|
||||||
|
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
|
||||||
|
ideConfigurations[ide] = savedIdeConfigs[ide];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (spinner.isSpinning) {
|
||||||
|
spinner.stop('IDE changes reviewed');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prompts.log.warn('IDEs to be removed:');
|
||||||
|
for (const ide of idesToRemove) {
|
||||||
|
await prompts.log.error(` - ${ide}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmRemoval = await prompts.confirm({
|
||||||
|
message: `Remove BMAD configuration for ${idesToRemove.length} IDE(s)?`,
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmRemoval) {
|
||||||
|
await this.ideManager.ensureInitialized();
|
||||||
|
for (const ide of idesToRemove) {
|
||||||
|
try {
|
||||||
|
const handler = this.ideManager.handlers.get(ide);
|
||||||
|
if (handler) {
|
||||||
|
await handler.cleanup(projectDir);
|
||||||
|
}
|
||||||
|
await this.ideConfigManager.deleteIdeConfig(bmadDir, ide);
|
||||||
|
await prompts.log.message(` Removed: ${ide}`);
|
||||||
|
} catch (error) {
|
||||||
|
await prompts.log.warn(` Warning: Failed to remove ${ide}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prompts.log.success(` Removed ${idesToRemove.length} IDE(s)`);
|
||||||
|
} else {
|
||||||
|
await prompts.log.message(' IDE removal cancelled');
|
||||||
|
// Add IDEs back to selection and restore their saved configurations
|
||||||
|
if (!config.ides) config.ides = [];
|
||||||
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||||
|
for (const ide of idesToRemove) {
|
||||||
|
config.ides.push(ide);
|
||||||
|
if (savedIdeConfigs[ide] && !ideConfigurations[ide]) {
|
||||||
|
ideConfigurations[ide] = savedIdeConfigs[ide];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.start('Preparing installation...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Results collector for consolidated summary
|
// Results collector for consolidated summary
|
||||||
const results = [];
|
const results = [];
|
||||||
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
const addResult = (step, status, detail = '') => results.push({ step, status, detail });
|
||||||
|
|
||||||
if (spinner.isSpinning) {
|
if (spinner.isSpinning) {
|
||||||
spinner.message('Installing...');
|
spinner.message('Preparing installation...');
|
||||||
} else {
|
} else {
|
||||||
spinner.start('Installing...');
|
spinner.start('Preparing installation...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create bmad directory structure
|
// Create bmad directory structure
|
||||||
|
|
@ -728,20 +815,10 @@ class Installer {
|
||||||
|
|
||||||
const projectRoot = getProjectRoot();
|
const projectRoot = getProjectRoot();
|
||||||
|
|
||||||
// Step 1: Install core module first (if requested)
|
|
||||||
if (config.installCore) {
|
|
||||||
spinner.message('Installing BMAD core...');
|
|
||||||
await this.installCoreWithDependencies(bmadDir, { core: {} });
|
|
||||||
addResult('Core', 'ok', 'installed');
|
|
||||||
|
|
||||||
// Generate core config file
|
|
||||||
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom content is already handled in UI before module selection
|
// Custom content is already handled in UI before module selection
|
||||||
let finalCustomContent = config.customContent;
|
const finalCustomContent = config.customContent;
|
||||||
|
|
||||||
// Step 3: Prepare modules list including cached custom modules
|
// Prepare modules list including cached custom modules
|
||||||
let allModules = [...(config.modules || [])];
|
let allModules = [...(config.modules || [])];
|
||||||
|
|
||||||
// During quick update, we might have custom module sources from the manifest
|
// During quick update, we might have custom module sources from the manifest
|
||||||
|
|
@ -780,8 +857,6 @@ class Installer {
|
||||||
allModules = allModules.filter((m) => m !== 'core');
|
allModules = allModules.filter((m) => m !== 'core');
|
||||||
}
|
}
|
||||||
|
|
||||||
const modulesToInstall = allModules;
|
|
||||||
|
|
||||||
// For dependency resolution, we only need regular modules (not custom modules)
|
// For dependency resolution, we only need regular modules (not custom modules)
|
||||||
// Custom modules are already installed in _bmad and don't need dependency resolution from source
|
// Custom modules are already installed in _bmad and don't need dependency resolution from source
|
||||||
const regularModulesForResolution = allModules.filter((module) => {
|
const regularModulesForResolution = allModules.filter((module) => {
|
||||||
|
|
@ -796,70 +871,91 @@ class Installer {
|
||||||
return !isCustom;
|
return !isCustom;
|
||||||
});
|
});
|
||||||
|
|
||||||
// For dependency resolution, we need to pass the project root
|
// Stop spinner before tasks() takes over progress display
|
||||||
|
spinner.stop('Preparation complete');
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// FIRST TASKS BLOCK: Core installation through manifests (non-interactive)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
const isQuickUpdate = config._quickUpdate || false;
|
||||||
|
|
||||||
|
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
|
||||||
|
let taskResolution;
|
||||||
|
|
||||||
|
// Collect directory creation results for output after tasks() completes
|
||||||
|
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||||
|
|
||||||
|
// Build task list conditionally
|
||||||
|
const installTasks = [];
|
||||||
|
|
||||||
|
// Core installation task
|
||||||
|
if (config.installCore) {
|
||||||
|
installTasks.push({
|
||||||
|
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
|
||||||
|
task: async (message) => {
|
||||||
|
await this.installCoreWithDependencies(bmadDir, { core: {} });
|
||||||
|
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||||
|
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
|
||||||
|
return isQuickUpdate ? 'Core updated' : 'Core installed';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency resolution task
|
||||||
|
installTasks.push({
|
||||||
|
title: 'Resolving dependencies',
|
||||||
|
task: async (message) => {
|
||||||
// Create a temporary module manager that knows about custom content locations
|
// Create a temporary module manager that knows about custom content locations
|
||||||
const tempModuleManager = new ModuleManager({
|
const tempModuleManager = new ModuleManager({
|
||||||
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
bmadDir: bmadDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
spinner.message('Resolving dependencies...');
|
taskResolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
|
||||||
|
|
||||||
const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
|
|
||||||
verbose: config.verbose,
|
verbose: config.verbose,
|
||||||
moduleManager: tempModuleManager,
|
moduleManager: tempModuleManager,
|
||||||
});
|
});
|
||||||
|
return 'Dependencies resolved';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Install modules with their dependencies
|
// Module installation task
|
||||||
if (allModules && allModules.length > 0) {
|
if (allModules && allModules.length > 0) {
|
||||||
|
installTasks.push({
|
||||||
|
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
||||||
|
task: async (message) => {
|
||||||
|
const resolution = taskResolution;
|
||||||
const installedModuleNames = new Set();
|
const installedModuleNames = new Set();
|
||||||
|
|
||||||
for (const moduleName of allModules) {
|
for (const moduleName of allModules) {
|
||||||
// Skip if already installed
|
if (installedModuleNames.has(moduleName)) continue;
|
||||||
if (installedModuleNames.has(moduleName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
installedModuleNames.add(moduleName);
|
installedModuleNames.add(moduleName);
|
||||||
|
|
||||||
// Show appropriate message based on whether this is a quick update
|
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
||||||
const isQuickUpdate = config._quickUpdate || false;
|
|
||||||
spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
|
|
||||||
|
|
||||||
// Check if this is a custom module
|
// Check if this is a custom module
|
||||||
let isCustomModule = false;
|
let isCustomModule = false;
|
||||||
let customInfo = null;
|
let customInfo = null;
|
||||||
let useCache = false;
|
|
||||||
|
|
||||||
// First check if we have a cached version
|
// First check if we have a cached version
|
||||||
if (finalCustomContent && finalCustomContent.cachedModules) {
|
if (finalCustomContent && finalCustomContent.cachedModules) {
|
||||||
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
|
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
|
||||||
if (cachedModule) {
|
if (cachedModule) {
|
||||||
isCustomModule = true;
|
isCustomModule = true;
|
||||||
customInfo = {
|
customInfo = { id: moduleName, path: cachedModule.cachePath, config: {} };
|
||||||
id: moduleName,
|
|
||||||
path: cachedModule.cachePath,
|
|
||||||
config: {},
|
|
||||||
};
|
|
||||||
useCache = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check if we have custom module sources from the manifest (for quick update)
|
// Then check custom module sources from manifest (for quick update)
|
||||||
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
|
||||||
customInfo = config._customModuleSources.get(moduleName);
|
customInfo = config._customModuleSources.get(moduleName);
|
||||||
isCustomModule = true;
|
isCustomModule = true;
|
||||||
|
|
||||||
// Check if this is a cached module (source path starts with _config)
|
|
||||||
if (
|
if (
|
||||||
customInfo.sourcePath &&
|
customInfo.sourcePath &&
|
||||||
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom'))
|
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) &&
|
||||||
) {
|
!customInfo.path
|
||||||
useCache = true;
|
)
|
||||||
// Make sure we have the right path structure
|
|
||||||
if (!customInfo.path) {
|
|
||||||
customInfo.path = customInfo.sourcePath;
|
customInfo.path = customInfo.sourcePath;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally check regular custom content
|
// Finally check regular custom content
|
||||||
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
|
||||||
|
|
@ -875,16 +971,12 @@ class Installer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCustomModule && customInfo) {
|
if (isCustomModule && customInfo) {
|
||||||
// Custom modules are now installed via ModuleManager just like standard modules
|
|
||||||
// The custom module path should already be in customModulePaths from earlier setup
|
|
||||||
if (!customModulePaths.has(moduleName) && customInfo.path) {
|
if (!customModulePaths.has(moduleName) && customInfo.path) {
|
||||||
customModulePaths.set(moduleName, customInfo.path);
|
customModulePaths.set(moduleName, customInfo.path);
|
||||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
||||||
|
|
||||||
// Use ModuleManager to install the custom module
|
|
||||||
await this.moduleManager.install(
|
await this.moduleManager.install(
|
||||||
moduleName,
|
moduleName,
|
||||||
bmadDir,
|
bmadDir,
|
||||||
|
|
@ -894,19 +986,19 @@ class Installer {
|
||||||
{
|
{
|
||||||
isCustom: true,
|
isCustom: true,
|
||||||
moduleConfig: collectedModuleConfig,
|
moduleConfig: collectedModuleConfig,
|
||||||
isQuickUpdate: config._quickUpdate || false,
|
isQuickUpdate: isQuickUpdate,
|
||||||
installer: this,
|
installer: this,
|
||||||
silent: true,
|
silent: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create module config (include collected config from module.yaml prompts)
|
|
||||||
await this.generateModuleConfigs(bmadDir, {
|
await this.generateModuleConfigs(bmadDir, {
|
||||||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Regular module installation
|
if (!resolution || !resolution.byModule) {
|
||||||
// Special case for core module
|
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 {
|
||||||
|
|
@ -918,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 =
|
||||||
|
|
@ -928,107 +1023,185 @@ class Installer {
|
||||||
files.data.length +
|
files.data.length +
|
||||||
files.other.length;
|
files.other.length;
|
||||||
if (totalFiles > 0) {
|
if (totalFiles > 0) {
|
||||||
spinner.message(`Installing ${module} dependencies...`);
|
message(`Installing ${module} dependencies...`);
|
||||||
await this.installPartialModule(module, bmadDir, files);
|
await this.installPartialModule(module, bmadDir, files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// All content is now installed as modules - no separate custom content handling needed
|
// Module directory creation task
|
||||||
|
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 || {},
|
||||||
|
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) {
|
||||||
|
for (const moduleName of config.modules) {
|
||||||
|
message(`Setting up ${moduleName}...`);
|
||||||
|
const result = await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
||||||
|
installedIDEs: config.ides || [],
|
||||||
|
moduleConfig: moduleConfigs[moduleName] || {},
|
||||||
|
existingModuleConfig: this.configCollector.existingConfig?.[moduleName] || {},
|
||||||
|
coreConfig: moduleConfigs.core || {},
|
||||||
|
logger: moduleLogger,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
dirResults.createdDirs.push(...result.createdDirs);
|
||||||
|
dirResults.movedDirs.push(...(result.movedDirs || []));
|
||||||
|
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',
|
||||||
|
task: async (message) => {
|
||||||
// Generate clean config.yaml files for each installed module
|
// Generate clean config.yaml files for each installed module
|
||||||
spinner.message('Generating module configurations...');
|
|
||||||
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
await this.generateModuleConfigs(bmadDir, moduleConfigs);
|
||||||
addResult('Configurations', 'ok', 'generated');
|
addResult('Configurations', 'ok', 'generated');
|
||||||
|
|
||||||
// Create agent configuration files
|
// Pre-register manifest files
|
||||||
// Note: Legacy createAgentConfigs removed - using YAML customize system instead
|
|
||||||
// Customize templates are now created in processAgentFiles when building YAML agents
|
|
||||||
|
|
||||||
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
|
|
||||||
const cfgDir = path.join(bmadDir, '_config');
|
const cfgDir = path.join(bmadDir, '_config');
|
||||||
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
||||||
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
||||||
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
||||||
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
||||||
|
|
||||||
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
|
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes
|
||||||
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
// This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
|
||||||
spinner.message('Generating workflow and agent manifests...');
|
message('Generating manifests...');
|
||||||
const manifestGen = new ManifestGenerator();
|
const manifestGen = new ManifestGenerator();
|
||||||
|
|
||||||
// For quick update, we need ALL installed modules in the manifest
|
|
||||||
// Not just the ones being updated
|
|
||||||
const allModulesForManifest = config._quickUpdate
|
const allModulesForManifest = config._quickUpdate
|
||||||
? config._existingModules || allModules || []
|
? config._existingModules || allModules || []
|
||||||
: config._preserveModules
|
: config._preserveModules
|
||||||
? [...allModules, ...config._preserveModules]
|
? [...allModules, ...config._preserveModules]
|
||||||
: allModules || [];
|
: allModules || [];
|
||||||
|
|
||||||
// For regular installs (including when called from quick update), use what we have
|
|
||||||
let modulesForCsvPreserve;
|
let modulesForCsvPreserve;
|
||||||
if (config._quickUpdate) {
|
if (config._quickUpdate) {
|
||||||
// Quick update - use existing modules or fall back to modules being updated
|
|
||||||
modulesForCsvPreserve = config._existingModules || allModules || [];
|
modulesForCsvPreserve = config._existingModules || allModules || [];
|
||||||
} else {
|
} else {
|
||||||
// Regular install - use the modules we're installing plus any preserved ones
|
|
||||||
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||||
ides: config.ides || [],
|
ides: config.ides || [],
|
||||||
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
preservedModules: modulesForCsvPreserve,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom modules are now included in the main modules list - no separate tracking needed
|
|
||||||
|
|
||||||
addResult(
|
addResult(
|
||||||
'Manifests',
|
'Manifests',
|
||||||
'ok',
|
'ok',
|
||||||
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
|
`${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Merge all module-help.csv files into bmad-help.csv
|
// Merge help catalogs
|
||||||
// This must happen AFTER generateManifests because it depends on agent-manifest.csv
|
message('Generating help catalog...');
|
||||||
spinner.message('Generating workflow help catalog...');
|
|
||||||
await this.mergeModuleHelpCatalogs(bmadDir);
|
await this.mergeModuleHelpCatalogs(bmadDir);
|
||||||
addResult('Help catalog', 'ok');
|
addResult('Help catalog', 'ok');
|
||||||
|
|
||||||
// Configure IDEs and copy documentation
|
return 'Configurations generated';
|
||||||
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
},
|
||||||
// Ensure IDE manager is initialized (handlers may not be loaded in quick update flow)
|
};
|
||||||
await this.ideManager.ensureInitialized();
|
installTasks.push(configTask);
|
||||||
|
|
||||||
// Filter out any undefined/null values from the IDE list
|
// 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.movedDirs.length > 0) {
|
||||||
|
const lines = dirResults.movedDirs.map((d) => ` ${d}`).join('\n');
|
||||||
|
await prompts.log.message(color.cyan(`Moved directories:\n${lines}`));
|
||||||
|
}
|
||||||
|
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
|
||||||
|
const resolution = taskResolution;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// IDE SETUP: Keep as spinner since it may prompt for user input
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
if (!config.skipIde && config.ides && config.ides.length > 0) {
|
||||||
|
await this.ideManager.ensureInitialized();
|
||||||
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string');
|
||||||
|
|
||||||
if (validIdes.length === 0) {
|
if (validIdes.length === 0) {
|
||||||
addResult('IDE configuration', 'warn', 'no valid IDEs selected');
|
addResult('IDE configuration', 'warn', 'no valid IDEs selected');
|
||||||
} else {
|
} else {
|
||||||
// Check if any IDE might need prompting (no pre-collected config)
|
|
||||||
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]);
|
||||||
|
const ideSpinner = await prompts.spinner();
|
||||||
// Temporarily suppress console output if not verbose
|
ideSpinner.start('Configuring tools...');
|
||||||
const originalLog = console.log;
|
|
||||||
if (!config.verbose) {
|
|
||||||
console.log = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const ide of validIdes) {
|
for (const ide of validIdes) {
|
||||||
if (!needsPrompting || ideConfigurations[ide]) {
|
if (!needsPrompting || ideConfigurations[ide]) {
|
||||||
// All IDEs pre-configured, or this specific IDE has config: keep spinner running
|
ideSpinner.message(`Configuring ${ide}...`);
|
||||||
spinner.message(`Configuring ${ide}...`);
|
|
||||||
} else {
|
} else {
|
||||||
// This IDE needs prompting: stop spinner to allow user interaction
|
if (ideSpinner.isSpinning) {
|
||||||
if (spinner.isSpinning) {
|
ideSpinner.stop('Ready for IDE configuration');
|
||||||
spinner.stop('Ready for IDE configuration');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Silent when this IDE has pre-collected config (no prompts for THIS IDE)
|
// Suppress stray console output for pre-configured IDEs (no user interaction)
|
||||||
const ideHasConfig = Boolean(ideConfigurations[ide]);
|
const ideHasConfig = Boolean(ideConfigurations[ide]);
|
||||||
|
const originalLog = console.log;
|
||||||
|
if (!config.verbose && ideHasConfig) {
|
||||||
|
console.log = () => {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
|
const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||||
selectedModules: allModules || [],
|
selectedModules: allModules || [],
|
||||||
preCollectedConfig: ideConfigurations[ide] || null,
|
preCollectedConfig: ideConfigurations[ide] || null,
|
||||||
|
|
@ -1036,80 +1209,49 @@ class Installer {
|
||||||
silent: ideHasConfig,
|
silent: ideHasConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save IDE configuration for future updates
|
|
||||||
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) {
|
||||||
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect result for summary
|
|
||||||
if (setupResult.success) {
|
if (setupResult.success) {
|
||||||
addResult(ide, 'ok', setupResult.detail || '');
|
addResult(ide, 'ok', setupResult.detail || '');
|
||||||
} else {
|
} else {
|
||||||
addResult(ide, 'error', setupResult.error || 'failed');
|
addResult(ide, 'error', setupResult.error || 'failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart spinner if we stopped it for prompting
|
|
||||||
if (needsPrompting && !spinner.isSpinning) {
|
|
||||||
spinner.start('Configuring IDEs...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
console.log = originalLog;
|
console.log = originalLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsPrompting && !ideSpinner.isSpinning) {
|
||||||
|
ideSpinner.start('Configuring tools...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (ideSpinner.isSpinning) {
|
||||||
|
ideSpinner.stop('Tool configuration complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run module-specific installers after IDE setup
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
spinner.message('Running module-specific installers...');
|
// SECOND TASKS BLOCK: Post-IDE operations (non-interactive)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
const postIdeTasks = [];
|
||||||
|
|
||||||
// Create a conditional logger based on verbose mode
|
// File restoration task (only for updates)
|
||||||
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
|
if (
|
||||||
const moduleLogger = {
|
config._isUpdate &&
|
||||||
log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode
|
((config._customFiles && config._customFiles.length > 0) || (config._modifiedFiles && config._modifiedFiles.length > 0))
|
||||||
error: (msg) => console.error(msg), // Always show errors
|
) {
|
||||||
warn: (msg) => console.warn(msg), // Always show warnings
|
postIdeTasks.push({
|
||||||
};
|
title: 'Finalizing installation',
|
||||||
|
task: async (message) => {
|
||||||
// Create directories for core module if core was installed
|
|
||||||
if (config.installCore || resolution.byModule.core) {
|
|
||||||
spinner.message('Creating core module directories...');
|
|
||||||
|
|
||||||
await this.moduleManager.createModuleDirectories('core', bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs.core || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create directories for user-selected modules
|
|
||||||
if (config.modules && config.modules.length > 0) {
|
|
||||||
for (const moduleName of config.modules) {
|
|
||||||
spinner.message(`Creating ${moduleName} module directories...`);
|
|
||||||
|
|
||||||
// Pass installed IDEs and module config to directory creator
|
|
||||||
await this.moduleManager.createModuleDirectories(moduleName, bmadDir, {
|
|
||||||
installedIDEs: config.ides || [],
|
|
||||||
moduleConfig: moduleConfigs[moduleName] || {},
|
|
||||||
coreConfig: moduleConfigs.core || {},
|
|
||||||
logger: moduleLogger,
|
|
||||||
silent: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addResult('Module installers', 'ok');
|
|
||||||
|
|
||||||
// Note: Manifest files are already created by ManifestGenerator above
|
|
||||||
// No need to create legacy manifest.csv anymore
|
|
||||||
|
|
||||||
// If this was an update, restore custom files
|
|
||||||
let customFiles = [];
|
let customFiles = [];
|
||||||
let modifiedFiles = [];
|
let modifiedFiles = [];
|
||||||
if (config._isUpdate) {
|
|
||||||
if (config._customFiles && config._customFiles.length > 0) {
|
if (config._customFiles && config._customFiles.length > 0) {
|
||||||
spinner.message(`Restoring ${config._customFiles.length} custom files...`);
|
message(`Restoring ${config._customFiles.length} custom files...`);
|
||||||
|
|
||||||
for (const originalPath of config._customFiles) {
|
for (const originalPath of config._customFiles) {
|
||||||
const relativePath = path.relative(bmadDir, originalPath);
|
const relativePath = path.relative(bmadDir, originalPath);
|
||||||
|
|
@ -1121,7 +1263,6 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temp backup
|
|
||||||
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
|
||||||
await fs.remove(config._tempBackupDir);
|
await fs.remove(config._tempBackupDir);
|
||||||
}
|
}
|
||||||
|
|
@ -1132,9 +1273,8 @@ class Installer {
|
||||||
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
if (config._modifiedFiles && config._modifiedFiles.length > 0) {
|
||||||
modifiedFiles = config._modifiedFiles;
|
modifiedFiles = config._modifiedFiles;
|
||||||
|
|
||||||
// Restore modified files as .bak files
|
|
||||||
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
|
||||||
spinner.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(bmadDir, modifiedFile.path);
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
||||||
|
|
@ -1147,14 +1287,24 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temp backup
|
|
||||||
await fs.remove(config._tempModifiedBackupDir);
|
await fs.remove(config._tempModifiedBackupDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store for summary access
|
||||||
|
config._restoredCustomFiles = customFiles;
|
||||||
|
config._restoredModifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
|
return 'Installation finalized';
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the single installation spinner
|
await prompts.tasks(postIdeTasks);
|
||||||
spinner.stop('Installation complete');
|
|
||||||
|
// Retrieve restored file info for summary
|
||||||
|
const customFiles = config._restoredCustomFiles || [];
|
||||||
|
const modifiedFiles = config._restoredModifiedFiles || [];
|
||||||
|
|
||||||
// Render consolidated summary
|
// Render consolidated summary
|
||||||
await this.renderInstallSummary(results, {
|
await this.renderInstallSummary(results, {
|
||||||
|
|
@ -1173,7 +1323,15 @@ class Installer {
|
||||||
projectDir: projectDir,
|
projectDir: projectDir,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
if (spinner.isSpinning) {
|
||||||
spinner.error('Installation failed');
|
spinner.error('Installation failed');
|
||||||
|
} else {
|
||||||
|
await prompts.log.error('Installation failed');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ensure the original error is never swallowed by a logging failure
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1213,6 +1371,14 @@ class Installer {
|
||||||
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
|
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next steps
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
' Next steps:',
|
||||||
|
` Docs: ${color.dim('https://docs.bmad-method.org/')}`,
|
||||||
|
` Run ${color.cyan('/bmad-help')} in your IDE to get started`,
|
||||||
|
);
|
||||||
|
|
||||||
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
|
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1297,6 +1463,7 @@ class Installer {
|
||||||
projectRoot,
|
projectRoot,
|
||||||
'update',
|
'update',
|
||||||
existingInstall.modules.map((m) => m.id),
|
existingInstall.modules.map((m) => m.id),
|
||||||
|
config.skipPrompts || false,
|
||||||
);
|
);
|
||||||
|
|
||||||
spinner.start('Preparing update...');
|
spinner.start('Preparing update...');
|
||||||
|
|
@ -2160,6 +2327,7 @@ class Installer {
|
||||||
projectRoot,
|
projectRoot,
|
||||||
'update',
|
'update',
|
||||||
installedModules,
|
installedModules,
|
||||||
|
config.skipPrompts || false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
||||||
|
|
@ -2417,7 +2585,9 @@ class Installer {
|
||||||
|
|
||||||
if (proceed === 'exit') {
|
if (proceed === 'exit') {
|
||||||
await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.');
|
await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.');
|
||||||
process.exit(0);
|
// Allow event loop to flush pending I/O before exit
|
||||||
|
setImmediate(() => process.exit(0));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prompts.log.warn('Proceeding with installation despite legacy v4 folder');
|
await prompts.log.warn('Proceeding with installation despite legacy v4 folder');
|
||||||
|
|
@ -2601,9 +2771,10 @@ class Installer {
|
||||||
* @param {string} projectRoot - Project root directory
|
* @param {string} projectRoot - Project root directory
|
||||||
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
||||||
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
||||||
|
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
|
||||||
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
||||||
*/
|
*/
|
||||||
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
|
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
|
||||||
const validCustomModules = [];
|
const validCustomModules = [];
|
||||||
const keptModulesWithoutSources = []; // Track modules kept without sources
|
const keptModulesWithoutSources = []; // Track modules kept without sources
|
||||||
const customModulesWithMissingSources = [];
|
const customModulesWithMissingSources = [];
|
||||||
|
|
@ -2646,6 +2817,14 @@ class Installer {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-interactive mode: keep all modules with missing sources
|
||||||
|
if (skipPrompts) {
|
||||||
|
for (const missing of customModulesWithMissingSources) {
|
||||||
|
keptModulesWithoutSources.push(missing.id);
|
||||||
|
}
|
||||||
|
return { validCustomModules, keptModulesWithoutSources };
|
||||||
|
}
|
||||||
|
|
||||||
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
|
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
|
||||||
|
|
||||||
let keptCount = 0;
|
let keptCount = 0;
|
||||||
|
|
@ -2710,6 +2889,13 @@ class Installer {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Defensive: handleCancel should have exited, but guard against symbol propagation
|
||||||
|
if (typeof newSourcePath !== 'string') {
|
||||||
|
keptCount++;
|
||||||
|
keptModulesWithoutSources.push(missing.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the source in manifest
|
// Update the source in manifest
|
||||||
const resolvedPath = path.resolve(newSourcePath.trim());
|
const resolvedPath = path.resolve(newSourcePath.trim());
|
||||||
missing.info.sourcePath = resolvedPath;
|
missing.info.sourcePath = resolvedPath;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
const packageJson = require('../../../../../package.json');
|
const packageJson = require('../../../../../package.json');
|
||||||
|
|
@ -218,7 +219,7 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Failed to parse workflow at ${fullPath}: ${error.message}`);
|
await prompts.log.warn(`Failed to parse workflow at ${fullPath}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -689,7 +690,7 @@ class ManifestGenerator {
|
||||||
|
|
||||||
return preservedRows;
|
return preservedRows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Failed to read existing CSV ${csvPath}:`, error.message);
|
await prompts.log.warn(`Failed to read existing CSV ${csvPath}: ${error.message}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -915,7 +916,7 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Could not scan for installed modules: ${error.message}`);
|
await prompts.log.warn(`Could not scan for installed modules: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const { getProjectRoot } = require('../../../lib/project-root');
|
const { getProjectRoot } = require('../../../lib/project-root');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
class Manifest {
|
class Manifest {
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,7 +101,7 @@ class Manifest {
|
||||||
ides: manifestData.ides || [],
|
ides: manifestData.ides || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to read YAML manifest:', error.message);
|
await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +231,7 @@ class Manifest {
|
||||||
const content = await fs.readFile(yamlPath, 'utf8');
|
const content = await fs.readFile(yamlPath, 'utf8');
|
||||||
return yaml.parse(content);
|
return yaml.parse(content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to read YAML manifest:', error.message);
|
await prompts.log.error(`Failed to read YAML manifest: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,7 +473,7 @@ class Manifest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Warning: Could not parse ${filePath}:`, error.message);
|
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle other file types (CSV, JSON, YAML, etc.)
|
// Handle other file types (CSV, JSON, YAML, etc.)
|
||||||
|
|
@ -774,7 +775,7 @@ class Manifest {
|
||||||
configs[moduleName] = yaml.parse(content);
|
configs[moduleName] = yaml.parse(content);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Could not load config for module ${moduleName}:`, error.message);
|
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -876,7 +877,7 @@ class Manifest {
|
||||||
const pkg = require(packageJsonPath);
|
const pkg = require(packageJsonPath);
|
||||||
version = pkg.version;
|
version = pkg.version;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
await prompts.log.warn(`Failed to read package.json for ${moduleName}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -904,7 +905,7 @@ class Manifest {
|
||||||
repoUrl: moduleConfig.repoUrl || null,
|
repoUrl: moduleConfig.repoUrl || null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
|
await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
|
// Non-interactive mode: use default (global)
|
||||||
|
if (options.skipPrompts) {
|
||||||
|
return { installLocation: 'global' };
|
||||||
|
}
|
||||||
|
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
let installLocation = 'global';
|
let installLocation = 'global';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages external official modules defined in external-official-modules.yaml
|
* Manages external official modules defined in external-official-modules.yaml
|
||||||
|
|
@ -29,7 +30,7 @@ class ExternalModuleManager {
|
||||||
this.cachedModules = config;
|
this.cachedModules = config;
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to load external modules config: ${error.message}`);
|
await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
|
||||||
return { modules: {} };
|
return { modules: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,7 @@ class ModuleManager {
|
||||||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
||||||
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
|
if (!silent) await prompts.log.warn(` ${error.message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if package.json is newer than node_modules
|
// Check if package.json is newer than node_modules
|
||||||
|
|
@ -478,7 +478,7 @@ class ModuleManager {
|
||||||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`);
|
||||||
if (!silent) await prompts.log.warn(` Warning: ${error.message}`);
|
if (!silent) await prompts.log.warn(` ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +541,7 @@ class ModuleManager {
|
||||||
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
|
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
|
||||||
customConfig = yaml.parse(customContent);
|
customConfig = yaml.parse(customContent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,7 +549,7 @@ class ModuleManager {
|
||||||
if (customConfig) {
|
if (customConfig) {
|
||||||
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
||||||
if (options.logger) {
|
if (options.logger) {
|
||||||
options.logger.log(` Merged custom configuration for ${moduleName}`);
|
await options.logger.log(` Merged custom configuration for ${moduleName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,7 +970,7 @@ class ModuleManager {
|
||||||
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
|
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
|
||||||
}
|
}
|
||||||
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||||
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`);
|
await prompts.log.warn(` Agent marked as having sidecar but ${sidecarDirName} directory not found`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1208,15 +1208,20 @@ class ModuleManager {
|
||||||
/**
|
/**
|
||||||
* Create directories declared in module.yaml's `directories` key
|
* Create directories declared in module.yaml's `directories` key
|
||||||
* This replaces the security-risky module installer pattern with declarative config
|
* This replaces the security-risky module installer pattern with declarative config
|
||||||
|
* During updates, if a directory path changed, moves the old directory to the new path
|
||||||
* @param {string} moduleName - Name of the module
|
* @param {string} moduleName - Name of the module
|
||||||
* @param {string} bmadDir - Target bmad directory
|
* @param {string} bmadDir - Target bmad directory
|
||||||
* @param {Object} options - Installation options
|
* @param {Object} options - Installation options
|
||||||
* @param {Object} options.moduleConfig - Module configuration from config collector
|
* @param {Object} options.moduleConfig - Module configuration from config collector
|
||||||
|
* @param {Object} options.existingModuleConfig - Previous module config (for detecting path changes during updates)
|
||||||
* @param {Object} options.coreConfig - Core configuration
|
* @param {Object} options.coreConfig - Core configuration
|
||||||
|
* @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
|
||||||
*/
|
*/
|
||||||
async createModuleDirectories(moduleName, bmadDir, options = {}) {
|
async createModuleDirectories(moduleName, bmadDir, options = {}) {
|
||||||
const moduleConfig = options.moduleConfig || {};
|
const moduleConfig = options.moduleConfig || {};
|
||||||
|
const existingModuleConfig = options.existingModuleConfig || {};
|
||||||
const projectRoot = path.dirname(bmadDir);
|
const projectRoot = path.dirname(bmadDir);
|
||||||
|
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||||
|
|
||||||
// Special handling for core module - it's in src/core not src/modules
|
// Special handling for core module - it's in src/core not src/modules
|
||||||
let sourcePath;
|
let sourcePath;
|
||||||
|
|
@ -1225,14 +1230,14 @@ class ModuleManager {
|
||||||
} else {
|
} else {
|
||||||
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
||||||
if (!sourcePath) {
|
if (!sourcePath) {
|
||||||
return; // No source found, skip
|
return emptyResult; // No source found, skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read module.yaml to find the `directories` key
|
// Read module.yaml to find the `directories` key
|
||||||
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
||||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
if (!(await fs.pathExists(moduleYamlPath))) {
|
||||||
return; // No module.yaml, skip
|
return emptyResult; // No module.yaml, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
let moduleYaml;
|
let moduleYaml;
|
||||||
|
|
@ -1240,17 +1245,18 @@ class ModuleManager {
|
||||||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||||
moduleYaml = yaml.parse(yamlContent);
|
moduleYaml = yaml.parse(yamlContent);
|
||||||
} catch {
|
} catch {
|
||||||
return; // Invalid YAML, skip
|
return emptyResult; // Invalid YAML, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!moduleYaml || !moduleYaml.directories) {
|
if (!moduleYaml || !moduleYaml.directories) {
|
||||||
return; // No directories declared, skip
|
return emptyResult; // No directories declared, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get color utility for styled output
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
const directories = moduleYaml.directories;
|
const directories = moduleYaml.directories;
|
||||||
const wdsFolders = moduleYaml.wds_folders || [];
|
const wdsFolders = moduleYaml.wds_folders || [];
|
||||||
|
const createdDirs = [];
|
||||||
|
const movedDirs = [];
|
||||||
|
const createdWdsFolders = [];
|
||||||
|
|
||||||
for (const dirRef of directories) {
|
for (const dirRef of directories) {
|
||||||
// Parse variable reference like "{design_artifacts}"
|
// Parse variable reference like "{design_artifacts}"
|
||||||
|
|
@ -1279,29 +1285,96 @@ class ModuleManager {
|
||||||
const normalizedPath = path.normalize(fullPath);
|
const normalizedPath = path.normalize(fullPath);
|
||||||
const normalizedRoot = path.normalize(projectRoot);
|
const normalizedRoot = path.normalize(projectRoot);
|
||||||
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
|
if (!normalizedPath.startsWith(normalizedRoot + path.sep) && normalizedPath !== normalizedRoot) {
|
||||||
await prompts.log.warn(color.yellow(`Warning: ${configKey} path escapes project root, skipping: ${dirPath}`));
|
const color = await prompts.getColor();
|
||||||
|
await prompts.log.warn(color.yellow(`${configKey} path escapes project root, skipping: ${dirPath}`));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Check if directory path changed from previous config (update/modify scenario)
|
||||||
if (!(await fs.pathExists(fullPath))) {
|
const oldDirValue = existingModuleConfig[configKey];
|
||||||
|
let oldFullPath = null;
|
||||||
|
let oldDirPath = null;
|
||||||
|
if (oldDirValue && typeof oldDirValue === 'string') {
|
||||||
|
// F3: Normalize both values before comparing to avoid false negatives
|
||||||
|
// from trailing slashes, separator differences, or prefix format variations
|
||||||
|
let normalizedOld = oldDirValue.replace(/^\{project-root\}\/?/, '');
|
||||||
|
normalizedOld = path.normalize(normalizedOld.replaceAll('{project-root}', ''));
|
||||||
|
const normalizedNew = path.normalize(dirPath);
|
||||||
|
|
||||||
|
if (normalizedOld !== normalizedNew) {
|
||||||
|
oldDirPath = normalizedOld;
|
||||||
|
oldFullPath = path.join(projectRoot, oldDirPath);
|
||||||
|
const normalizedOldAbsolute = path.normalize(oldFullPath);
|
||||||
|
if (!normalizedOldAbsolute.startsWith(normalizedRoot + path.sep) && normalizedOldAbsolute !== normalizedRoot) {
|
||||||
|
oldFullPath = null; // Old path escapes project root, ignore it
|
||||||
|
}
|
||||||
|
|
||||||
|
// F13: Prevent parent/child move (e.g. docs/planning → docs/planning/v2)
|
||||||
|
if (oldFullPath) {
|
||||||
|
const normalizedNewAbsolute = path.normalize(fullPath);
|
||||||
|
if (
|
||||||
|
normalizedOldAbsolute.startsWith(normalizedNewAbsolute + path.sep) ||
|
||||||
|
normalizedNewAbsolute.startsWith(normalizedOldAbsolute + path.sep)
|
||||||
|
) {
|
||||||
|
const color = await prompts.getColor();
|
||||||
|
await prompts.log.warn(
|
||||||
|
color.yellow(
|
||||||
|
`${configKey}: cannot move between parent/child paths (${oldDirPath} / ${dirPath}), creating new directory instead`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
oldFullPath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dirName = configKey.replaceAll('_', ' ');
|
const dirName = configKey.replaceAll('_', ' ');
|
||||||
await prompts.log.message(color.yellow(`Creating ${dirName} directory: ${dirPath}`));
|
|
||||||
|
if (oldFullPath && (await fs.pathExists(oldFullPath)) && !(await fs.pathExists(fullPath))) {
|
||||||
|
// Path changed and old dir exists → move old to new location
|
||||||
|
// F1: Use fs.move() instead of fs.rename() for cross-device/volume support
|
||||||
|
// F2: Wrap in try/catch — fallback to creating new dir on failure
|
||||||
|
try {
|
||||||
|
await fs.ensureDir(path.dirname(fullPath));
|
||||||
|
await fs.move(oldFullPath, fullPath);
|
||||||
|
movedDirs.push(`${dirName}: ${oldDirPath} → ${dirPath}`);
|
||||||
|
} catch (moveError) {
|
||||||
|
const color = await prompts.getColor();
|
||||||
|
await prompts.log.warn(
|
||||||
|
color.yellow(
|
||||||
|
`Failed to move ${oldDirPath} → ${dirPath}: ${moveError.message}\n Creating new directory instead. Please move contents from the old directory manually.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await fs.ensureDir(fullPath);
|
||||||
|
createdDirs.push(`${dirName}: ${dirPath}`);
|
||||||
|
}
|
||||||
|
} else if (oldFullPath && (await fs.pathExists(oldFullPath)) && (await fs.pathExists(fullPath))) {
|
||||||
|
// F5: Both old and new directories exist — warn user about potential orphaned documents
|
||||||
|
const color = await prompts.getColor();
|
||||||
|
await prompts.log.warn(
|
||||||
|
color.yellow(
|
||||||
|
`${dirName}: path changed but both directories exist:\n Old: ${oldDirPath}\n New: ${dirPath}\n Old directory may contain orphaned documents — please review and merge manually.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!(await fs.pathExists(fullPath))) {
|
||||||
|
// New directory doesn't exist yet → create it
|
||||||
|
createdDirs.push(`${dirName}: ${dirPath}`);
|
||||||
await fs.ensureDir(fullPath);
|
await fs.ensureDir(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create WDS subfolders if this is the design_artifacts directory
|
// Create WDS subfolders if this is the design_artifacts directory
|
||||||
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
|
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
|
||||||
await prompts.log.message(color.cyan('Creating WDS folder structure...'));
|
|
||||||
for (const subfolder of wdsFolders) {
|
for (const subfolder of wdsFolders) {
|
||||||
const subPath = path.join(fullPath, subfolder);
|
const subPath = path.join(fullPath, subfolder);
|
||||||
if (!(await fs.pathExists(subPath))) {
|
if (!(await fs.pathExists(subPath))) {
|
||||||
await fs.ensureDir(subPath);
|
await fs.ensureDir(subPath);
|
||||||
await prompts.log.message(color.dim(` ✓ ${subfolder}/`));
|
createdWdsFolders.push(subfolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { createdDirs, movedDirs, createdWdsFolders };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ class UI {
|
||||||
const installedVersion = existingInstall.version || 'unknown';
|
const installedVersion = existingInstall.version || 'unknown';
|
||||||
|
|
||||||
// Check if version is pre beta
|
// Check if version is pre beta
|
||||||
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir));
|
const shouldProceed = await this.showLegacyVersionWarning(installedVersion, currentVersion, path.basename(bmadDir), options);
|
||||||
|
|
||||||
// If user chose to cancel, exit the installer
|
// If user chose to cancel, exit the installer
|
||||||
if (!shouldProceed) {
|
if (!shouldProceed) {
|
||||||
|
|
@ -227,6 +227,14 @@ class UI {
|
||||||
}
|
}
|
||||||
actionType = options.action;
|
actionType = options.action;
|
||||||
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
||||||
|
} else if (options.yes) {
|
||||||
|
// Default to quick-update if available, otherwise first available choice
|
||||||
|
if (choices.length === 0) {
|
||||||
|
throw new Error('No valid actions available for this installation');
|
||||||
|
}
|
||||||
|
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
|
||||||
|
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
|
||||||
|
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
|
||||||
} else {
|
} else {
|
||||||
actionType = await prompts.select({
|
actionType = await prompts.select({
|
||||||
message: 'How would you like to proceed?',
|
message: 'How would you like to proceed?',
|
||||||
|
|
@ -242,6 +250,7 @@ class UI {
|
||||||
actionType: 'quick-update',
|
actionType: 'quick-update',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
customContent: { hasCustomContent: false },
|
customContent: { hasCustomContent: false },
|
||||||
|
skipPrompts: options.yes || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,6 +261,7 @@ class UI {
|
||||||
actionType: 'compile-agents',
|
actionType: 'compile-agents',
|
||||||
directory: confirmedDirectory,
|
directory: confirmedDirectory,
|
||||||
customContent: { hasCustomContent: false },
|
customContent: { hasCustomContent: false },
|
||||||
|
skipPrompts: options.yes || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +282,11 @@ class UI {
|
||||||
.map((m) => m.trim())
|
.map((m) => m.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||||
|
} else if (options.yes) {
|
||||||
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||||
|
await prompts.log.info(
|
||||||
|
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||||
}
|
}
|
||||||
|
|
@ -330,6 +345,22 @@ class UI {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (options.yes) {
|
||||||
|
// Non-interactive mode: preserve existing custom modules (matches default: false)
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
customModuleResult.selectedCustomModules.push(entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prompts.log.info(
|
||||||
|
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const changeCustomModules = await prompts.confirm({
|
const changeCustomModules = await prompts.confirm({
|
||||||
message: 'Modify custom modules, agents, or workflows?',
|
message: 'Modify custom modules, agents, or workflows?',
|
||||||
|
|
@ -378,6 +409,7 @@ class UI {
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
coreConfig: coreConfig,
|
coreConfig: coreConfig,
|
||||||
customContent: customModuleResult.customContentConfig,
|
customContent: customModuleResult.customContentConfig,
|
||||||
|
skipPrompts: options.yes || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,6 +561,27 @@ class UI {
|
||||||
if (configuredIdes.length > 0) {
|
if (configuredIdes.length > 0) {
|
||||||
const allTools = [...preferredIdes, ...otherIdes];
|
const allTools = [...preferredIdes, ...otherIdes];
|
||||||
|
|
||||||
|
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
||||||
|
if (options.tools) {
|
||||||
|
if (options.tools.toLowerCase() === 'none') {
|
||||||
|
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||||
|
return { ides: [], skipIde: true };
|
||||||
|
}
|
||||||
|
const selectedIdes = options.tools
|
||||||
|
.split(',')
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||||
|
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||||
|
return { ides: selectedIdes, skipIde: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.yes) {
|
||||||
|
await prompts.log.info(`Non-interactive mode (--yes): keeping configured tools: ${configuredIdes.join(', ')}`);
|
||||||
|
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||||
|
return { ides: configuredIdes, skipIde: false };
|
||||||
|
}
|
||||||
|
|
||||||
// Sort: configured tools first, then preferred, then others
|
// Sort: configured tools first, then preferred, then others
|
||||||
const sortedTools = [
|
const sortedTools = [
|
||||||
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
|
...allTools.filter((ide) => configuredIdes.includes(ide.value)),
|
||||||
|
|
@ -691,18 +744,6 @@ class UI {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display installation summary
|
|
||||||
* @param {Object} result - Installation result
|
|
||||||
*/
|
|
||||||
async showInstallSummary(result) {
|
|
||||||
let summary = `Installed to: ${result.path}`;
|
|
||||||
if (result.modules && result.modules.length > 0) {
|
|
||||||
summary += `\nModules: ${result.modules.join(', ')}`;
|
|
||||||
}
|
|
||||||
await prompts.note(summary, 'BMAD is ready to use!');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get confirmed directory from user
|
* Get confirmed directory from user
|
||||||
* @returns {string} Confirmed directory path
|
* @returns {string} Confirmed directory path
|
||||||
|
|
@ -1642,7 +1683,7 @@ class UI {
|
||||||
* @param {string} bmadFolderName - Name of the BMAD folder
|
* @param {string} bmadFolderName - Name of the BMAD folder
|
||||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||||
*/
|
*/
|
||||||
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
async showLegacyVersionWarning(installedVersion, currentVersion, bmadFolderName, options = {}) {
|
||||||
if (!this.isLegacyVersion(installedVersion)) {
|
if (!this.isLegacyVersion(installedVersion)) {
|
||||||
return true; // Not legacy, proceed
|
return true; // Not legacy, proceed
|
||||||
}
|
}
|
||||||
|
|
@ -1668,6 +1709,11 @@ class UI {
|
||||||
await prompts.log.warn('VERSION WARNING');
|
await prompts.log.warn('VERSION WARNING');
|
||||||
await prompts.note(warningContent, 'Version Warning');
|
await prompts.note(warningContent, 'Version Warning');
|
||||||
|
|
||||||
|
if (options.yes) {
|
||||||
|
await prompts.log.warn('Non-interactive mode (--yes): auto-proceeding with legacy update');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const proceed = await prompts.select({
|
const proceed = await prompts.select({
|
||||||
message: 'How would you like to proceed?',
|
message: 'How would you like to proceed?',
|
||||||
choices: [
|
choices: [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue