Merge b032b5a590 into 36c21dbada
This commit is contained in:
commit
150b7a5183
|
|
@ -39,7 +39,6 @@ module.exports = {
|
|||
if (config.actionType === 'cancel') {
|
||||
await prompts.log.warn('Installation cancelled.');
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle quick update separately
|
||||
|
|
@ -47,23 +46,14 @@ module.exports = {
|
|||
const result = await installer.quickUpdate(config);
|
||||
await prompts.log.success('Quick update complete!');
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle compile agents separately
|
||||
if (config.actionType === 'compile-agents') {
|
||||
const result = await installer.compileAgents(config);
|
||||
await prompts.log.success('Agent recompilation complete!');
|
||||
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`);
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular install/update flow
|
||||
|
|
@ -72,16 +62,10 @@ module.exports = {
|
|||
// Check if installation was cancelled
|
||||
if (result && result.cancelled) {
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if installation succeeded
|
||||
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);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,28 +14,10 @@ startMessage: |
|
|||
but anticipate no massive breaking changes
|
||||
- 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.
|
||||
- No gated Discord. No paywalls.
|
||||
- No gated Discord. No paywalls. No gated content.
|
||||
- We believe in empowering everyone, not just those who can pay.
|
||||
- Knowledge should be shared, not sold.
|
||||
|
||||
🙏 SUPPORT BMAD DEVELOPMENT:
|
||||
- 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
|
||||
- For speaking inquiries or interviews, reach out to BMad on Discord!
|
||||
|
||||
📚 RESOURCES:
|
||||
- Docs: http://docs.bmad-method.org/ (bookmark it!)
|
||||
- Changelog: https://github.com/bmad-code-org/BMAD-METHOD/CHANGELOG.md
|
||||
|
||||
⭐⭐⭐ HELP US GROW:
|
||||
⭐ HELP US GROW:
|
||||
- Star us on GitHub: https://github.com/bmad-code-org/BMAD-METHOD/
|
||||
- Subscribe on YouTube: https://www.youtube.com/@BMadCode
|
||||
- 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.existingConfig = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Array} modules - List of modules to configure (including 'core')
|
||||
|
|
@ -141,6 +218,7 @@ class ConfigCollector {
|
|||
// Store custom module paths for use in collectModuleConfig
|
||||
this.customModulePaths = options.customModulePaths || new Map();
|
||||
this.skipPrompts = options.skipPrompts || false;
|
||||
this.modulesToCustomize = undefined;
|
||||
await this.loadExistingConfig(projectDir);
|
||||
|
||||
// Check if core was already collected (e.g., in early collection phase)
|
||||
|
|
@ -154,10 +232,95 @@ class ConfigCollector {
|
|||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
this.collectedConfig._meta = {
|
||||
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 (!(await fs.pathExists(moduleConfigPath))) {
|
||||
// Use the module manager to find the module source
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
|
|
@ -211,9 +371,7 @@ class ConfigCollector {
|
|||
configPath = moduleConfigPath;
|
||||
} else {
|
||||
// Check if this is a custom module with custom.yaml
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
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 (!(await fs.pathExists(moduleConfigPath))) {
|
||||
// Use the module manager to find the module source
|
||||
const { ModuleManager } = require('../modules/manager');
|
||||
const moduleManager = new ModuleManager();
|
||||
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
|
||||
const moduleSourcePath = await this._getModuleManager().findModuleSource(moduleName, { silent: true });
|
||||
|
||||
if (moduleSourcePath) {
|
||||
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
|
||||
|
|
@ -579,12 +734,12 @@ class ConfigCollector {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
let customize = true;
|
||||
if (!this._silentConfig) await prompts.log.step(`Configuring ${moduleDisplayName}`);
|
||||
let useDefaults = true;
|
||||
if (moduleName === 'core') {
|
||||
// Core module: no confirm prompt, continues directly
|
||||
} else {
|
||||
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
|
||||
useDefaults = false; // Core: always show all questions
|
||||
} else if (this.modulesToCustomize === undefined) {
|
||||
// Fallback: original per-module confirm (backward compat for direct calls)
|
||||
const customizeAnswer = await prompts.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
|
|
@ -593,10 +748,13 @@ class ConfigCollector {
|
|||
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
|
||||
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 hasNoConfig = actualConfigKeys.length === 0;
|
||||
|
||||
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
if (moduleConfig.subheader) {
|
||||
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
||||
if (!this._silentConfig) {
|
||||
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
|
||||
await prompts.log.step(moduleDisplayName);
|
||||
if (moduleConfig.subheader) {
|
||||
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`);
|
||||
} else {
|
||||
await prompts.log.message(` \u2713 No custom configuration required`);
|
||||
}
|
||||
} 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 path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
class CustomModuleCache {
|
||||
constructor(bmadDir) {
|
||||
|
|
@ -195,7 +196,7 @@ class CustomModuleCache {
|
|||
// Verify cache integrity
|
||||
const currentCacheHash = await this.calculateHash(cacheDir);
|
||||
if (currentCacheHash !== cached.cacheHash) {
|
||||
console.warn(`Warning: Cache integrity check failed for ${moduleId}`);
|
||||
await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Manages IDE configuration persistence
|
||||
|
|
@ -93,7 +94,7 @@ class IdeConfigManager {
|
|||
const config = yaml.parse(content);
|
||||
return config;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +124,7 @@ class IdeConfigManager {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,6 +4,7 @@ const yaml = require('yaml');
|
|||
const crypto = require('node:crypto');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
// Load package.json for version info
|
||||
const packageJson = require('../../../../../package.json');
|
||||
|
|
@ -241,7 +242,7 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
} 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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1072,7 +1073,7 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
|||
const fs = require('fs-extra');
|
||||
const crypto = require('node:crypto');
|
||||
const { getProjectRoot } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
class Manifest {
|
||||
/**
|
||||
|
|
@ -100,7 +101,7 @@ class Manifest {
|
|||
ides: manifestData.ides || [],
|
||||
};
|
||||
} 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');
|
||||
return yaml.parse(content);
|
||||
} 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) {
|
||||
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.)
|
||||
|
|
@ -774,7 +775,7 @@ class Manifest {
|
|||
configs[moduleName] = yaml.parse(content);
|
||||
}
|
||||
} 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);
|
||||
version = pkg.version;
|
||||
} 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,
|
||||
};
|
||||
} 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
|
||||
*/
|
||||
async collectConfiguration(options = {}) {
|
||||
// Non-interactive mode: use default (global)
|
||||
if (options.skipPrompts) {
|
||||
return { installLocation: 'global' };
|
||||
}
|
||||
|
||||
let confirmed = false;
|
||||
let installLocation = 'global';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Manages external official modules defined in external-official-modules.yaml
|
||||
|
|
@ -29,7 +30,7 @@ class ExternalModuleManager {
|
|||
this.cachedModules = config;
|
||||
return config;
|
||||
} 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: {} };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ class ModuleManager {
|
|||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||
} catch (error) {
|
||||
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 {
|
||||
// Check if package.json is newer than node_modules
|
||||
|
|
@ -478,7 +478,7 @@ class ModuleManager {
|
|||
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`);
|
||||
} catch (error) {
|
||||
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');
|
||||
customConfig = yaml.parse(customContent);
|
||||
} 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) {
|
||||
options.moduleConfig = { ...options.moduleConfig, ...customConfig };
|
||||
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');
|
||||
} catch {
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -1012,7 +1012,7 @@ class ModuleManager {
|
|||
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`);
|
||||
}
|
||||
} 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
|
||||
* 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} bmadDir - Target bmad directory
|
||||
* @param {Object} options - Installation options
|
||||
* @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
|
||||
* @returns {Promise<{createdDirs: string[], movedDirs: string[], createdWdsFolders: string[]}>} Created directories info
|
||||
*/
|
||||
async createModuleDirectories(moduleName, bmadDir, options = {}) {
|
||||
const moduleConfig = options.moduleConfig || {};
|
||||
const existingModuleConfig = options.existingModuleConfig || {};
|
||||
const projectRoot = path.dirname(bmadDir);
|
||||
const emptyResult = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||
|
||||
// Special handling for core module - it's in src/core not src/modules
|
||||
let sourcePath;
|
||||
|
|
@ -1264,14 +1269,14 @@ class ModuleManager {
|
|||
} else {
|
||||
sourcePath = await this.findModuleSource(moduleName, { silent: true });
|
||||
if (!sourcePath) {
|
||||
return; // No source found, skip
|
||||
return emptyResult; // No source found, skip
|
||||
}
|
||||
}
|
||||
|
||||
// Read module.yaml to find the `directories` key
|
||||
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
||||
return; // No module.yaml, skip
|
||||
return emptyResult; // No module.yaml, skip
|
||||
}
|
||||
|
||||
let moduleYaml;
|
||||
|
|
@ -1279,17 +1284,18 @@ class ModuleManager {
|
|||
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
|
||||
moduleYaml = yaml.parse(yamlContent);
|
||||
} catch {
|
||||
return; // Invalid YAML, skip
|
||||
return emptyResult; // Invalid YAML, skip
|
||||
}
|
||||
|
||||
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 wdsFolders = moduleYaml.wds_folders || [];
|
||||
const createdDirs = [];
|
||||
const movedDirs = [];
|
||||
const createdWdsFolders = [];
|
||||
|
||||
for (const dirRef of directories) {
|
||||
// Parse variable reference like "{design_artifacts}"
|
||||
|
|
@ -1318,29 +1324,96 @@ class ModuleManager {
|
|||
const normalizedPath = path.normalize(fullPath);
|
||||
const normalizedRoot = path.normalize(projectRoot);
|
||||
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;
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!(await fs.pathExists(fullPath))) {
|
||||
const dirName = configKey.replaceAll('_', ' ');
|
||||
await prompts.log.message(color.yellow(`Creating ${dirName} directory: ${dirPath}`));
|
||||
// Check if directory path changed from previous config (update/modify scenario)
|
||||
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('_', ' ');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Create WDS subfolders if this is the design_artifacts directory
|
||||
if (configKey === 'design_artifacts' && wdsFolders.length > 0) {
|
||||
await prompts.log.message(color.cyan('Creating WDS folder structure...'));
|
||||
for (const subfolder of wdsFolders) {
|
||||
const subPath = path.join(fullPath, subfolder);
|
||||
if (!(await fs.pathExists(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';
|
||||
|
||||
// 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 (!shouldProceed) {
|
||||
|
|
@ -227,6 +227,14 @@ class UI {
|
|||
}
|
||||
actionType = options.action;
|
||||
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 {
|
||||
actionType = await prompts.select({
|
||||
message: 'How would you like to proceed?',
|
||||
|
|
@ -242,6 +250,7 @@ class UI {
|
|||
actionType: 'quick-update',
|
||||
directory: confirmedDirectory,
|
||||
customContent: { hasCustomContent: false },
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -252,6 +261,7 @@ class UI {
|
|||
actionType: 'compile-agents',
|
||||
directory: confirmedDirectory,
|
||||
customContent: { hasCustomContent: false },
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +282,11 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
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 {
|
||||
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 {
|
||||
const changeCustomModules = await prompts.confirm({
|
||||
message: 'Modify custom modules, agents, or workflows?',
|
||||
|
|
@ -378,6 +409,7 @@ class UI {
|
|||
skipIde: toolSelection.skipIde,
|
||||
coreConfig: coreConfig,
|
||||
customContent: customModuleResult.customContentConfig,
|
||||
skipPrompts: options.yes || false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -529,6 +561,27 @@ class UI {
|
|||
if (configuredIdes.length > 0) {
|
||||
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
|
||||
const sortedTools = [
|
||||
...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
|
||||
* @returns {string} Confirmed directory path
|
||||
|
|
@ -1642,7 +1683,7 @@ class UI {
|
|||
* @param {string} bmadFolderName - Name of the BMAD folder
|
||||
* @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)) {
|
||||
return true; // Not legacy, proceed
|
||||
}
|
||||
|
|
@ -1668,6 +1709,11 @@ class UI {
|
|||
await prompts.log.warn('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({
|
||||
message: 'How would you like to proceed?',
|
||||
choices: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue