fix: installer fixes and non-interactive mode improvements (#1612)
* fix: remove duplicate 'recompilation complete' message in compile-agents output
The spinner.stop() in compileAgents() already displays this message,
so the additional log.success() call produced a redundant line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove deselected IDE configurations during installer update
When updating an existing installation, IDEs that were previously
configured but unchecked in the new selection are now detected and
cleaned up after user confirmation, mirroring the existing module
removal flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove unreachable return statements after process.exit calls
Remove 4 dead `return;` statements that immediately follow `process.exit(0)`
calls in install.js. Since process.exit() terminates the process, the
subsequent return statements are unreachable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: batch directory creation log messages for cleaner installer output
Collect all created directory names during module directory setup and
emit them as a single log message instead of one per directory. This
eliminates the excessive blank-line spacing that @clack/prompts adds
between individual log.message() calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: consolidate directory creation output across all modules
Move directory creation logging from ModuleManager.createModuleDirectories
into the installer caller. The method now returns created directory info
instead of logging directly, allowing the installer to batch all module
directories into a single log message under one spinner. Also adds spacing
before the final "Installation complete" status line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: propagate skipPrompts flag to IDE collectConfiguration calls
The --yes flag (skipPrompts) was not being passed through to IDE handler
collectConfiguration() calls, causing the Codex installer to hang on its
interactive prompt in non-interactive mode (CI/CD, --yes flag).
- Add skipPrompts parameter to collectToolConfigurations() and forward it
to handler.collectConfiguration() options
- Add early return in CodexSetup.collectConfiguration() when skipPrompts
is true, defaulting to global install location
- Guard IDE removal confirmation prompt with skipPrompts check, defaulting
to preserve existing configs (consistent with prompt default: false)
Fixes #1610
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings from PR #1612
- Restore saved IDE configurations when removal is cancelled or skipped
in non-interactive mode, preventing silent config downgrade (e.g.,
Codex 'project' install reverting to 'global')
- Short-circuit IDE removal block when skipPrompts is true to eliminate
unnecessary warning/cancelled log output in --yes mode
- Add null guard on config.ides before pushing re-added IDEs
- Return empty result object from createModuleDirectories early exits
instead of undefined for a uniform return contract
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: guard remaining unguarded prompts with skipPrompts for non-interactive mode
Add skipPrompts guards to 4 remaining unguarded interactive prompts in
installer.js that caused hangs in non-interactive mode (--yes flag):
- Module removal confirmation: preserves modules by default
- Update action selection: defaults to 'update'
- Custom module missing sources: keeps all modules
- Custom module delete confirmation: unreachable via early return
Additional robustness fixes:
- Defensive type-check before .trim() on prompt result (symbol guard)
- Console.log suppression scoped per-IDE instead of global try/finally
- process.exit flush via setImmediate for legacy v4 exit path
- JSDoc updated for new skipPrompts parameter
Follows established pattern from commit f967fdde (IDE skipPrompts guards).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate installer console.warn/error calls to prompts.log API
Replace all user-facing console.warn(), console.error(), and console.log()
calls with their prompts.log.* equivalents for consistent @clack/prompts
styled output across the installer codebase.
- Migrate 13 console.warn/error calls across 5 files to prompts.log.*
- Convert moduleLogger callbacks to async prompts.log.* in installer.js
- Replace blank-line console.log() with prompts.log.message('')
- Add prompts import to 5 files that lacked it
- Remove redundant "Warning:" prefixes (prompts.log.warn adds styling)
- Remove redundant local prompts require in installer.js L454
- Add missing await on logger.log call in manager.js
- Debug-gated console.log calls in manifest-generator.js left as-is
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace installer spinner with tasks() progress and consolidate summary
Replace the serial spinner during non-interactive install phases with
@clack/prompts tasks() component for clearer progress visibility. The
install flow now uses two tasks() blocks (pre-IDE and post-IDE) with
the IDE setup retaining its own spinner since it may prompt.
- Refactor install phases into tasks() callbacks with message() updates
- Merge next-steps content into the "BMAD is ready to use!" summary note
- Fix spinner.stop() tense: "Reviewing..." → past tense ("reviewed")
- Move directory creation output after tasks() to avoid breaking progress
- Remove dead showInstallSummary() from ui.js
- Harden error handling: try/finally on IDE spinner, safe catch block
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: consolidate install messages into single start banner
Combine start and end marketing messages into one banner shown before
installation begins. Remove the post-install end message and its
displayEndMessage() calls — the install summary note now serves as
the final output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: apply prettier formatting to install command files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename "Module installers" label to "Module directories" in summary
The old script-based module installer pattern was replaced with
declarative directory creation, but the task title and summary label
were never updated to reflect that change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add batch module configuration gateway with express/customize modes
Replace N individual "Accept Defaults?" confirm prompts with a single
select gateway ("Express Setup" vs "Customize"). When customizing, a
multiselect shows only modules with configurable options. All others
silently receive defaults via spinner progress.
- Add scanModuleSchemas() to pre-scan module metadata and display names
- Add select/multiselect gateway in collectAllConfigurations()
- Replace per-module confirm with modulesToCustomize Set check
- Suppress UI output during silent default config via _silentConfig flag
- Reorder installer tasks: module dirs before config generation
- Add resolution null guards for edge-case safety
- Cache ModuleManager instance via _getModuleManager() for reuse
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move module directories instead of creating new ones on path change
When users modify a module's directory path during installer update/modify,
the old directory is now moved to the new location instead of creating an
empty directory while leaving the old one (with its documents) behind.
Includes: cross-device fs.move, error handling with fallback, path
normalization, parent/child path guard, and warning when both dirs exist.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add --yes flag guards to all unguarded prompts in update/modify path
Guard 5 interactive prompts in ui.js that caused the installer to hang
when --yes flag was used with existing installations. Add skipPrompts
field to 3 return objects that were missing it, ensuring installer.js
downstream guards work correctly for all update paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
36c21dbada
commit
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,16 +884,18 @@ 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 (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
if (!this._silentConfig) {
|
||||||
await prompts.log.step(moduleDisplayName);
|
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||||
if (moduleConfig.subheader) {
|
await prompts.log.step(moduleDisplayName);
|
||||||
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
if (moduleConfig.subheader) {
|
||||||
|
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
||||||
|
} else {
|
||||||
|
await prompts.log.message(` \u2713 No custom configuration required`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await prompts.log.message(` \u2713 No custom configuration required`);
|
// Module has config but just no questions to ask
|
||||||
|
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Module has config but just no questions to ask
|
|
||||||
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@ const yaml = require('yaml');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
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');
|
||||||
|
|
@ -241,7 +242,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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -693,7 +694,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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1072,7 +1073,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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -857,7 +857,7 @@ class ModuleManager {
|
||||||
await fs.writeFile(targetFile, strippedYaml, 'utf8');
|
await fs.writeFile(targetFile, strippedYaml, 'utf8');
|
||||||
} catch {
|
} catch {
|
||||||
// If anything fails, just copy the file as-is
|
// If anything fails, just copy the file as-is
|
||||||
await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`);
|
await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
|
||||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1012,7 +1012,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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1247,15 +1247,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;
|
||||||
|
|
@ -1264,14 +1269,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;
|
||||||
|
|
@ -1279,17 +1284,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}"
|
||||||
|
|
@ -1318,29 +1324,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];
|
||||||
const dirName = configKey.replaceAll('_', ' ');
|
let oldFullPath = null;
|
||||||
await prompts.log.message(color.yellow(`Creating ${dirName} directory: ${dirPath}`));
|
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('_', ' ');
|
||||||
|
|
||||||
|
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