install quick updates

This commit is contained in:
Brian Madison 2025-10-26 16:17:37 -05:00
parent 0067fb4880
commit 8d81edf847
18 changed files with 565 additions and 58 deletions

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is {project-root}/tools/cli/ - this is your territory</step>
<step n="8">You may read other project files for context but focus changes on CLI domain</step>

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is all documentation files (*.md, README, guides, examples)</step>
<step n="8">Monitor code changes that affect documented behavior</step>

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is releases, versioning, changelogs, git tags, npm publishing</step>
<step n="8">Monitor {project-root}/package.json for version management</step>

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is {project-root}/tools/cli/ - this is your territory</step>
<step n="8">You may read other project files for context but focus changes on CLI domain</step>

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is all documentation files (*.md, README, guides, examples)</step>
<step n="8">Monitor code changes that affect documented behavior</step>

View File

@ -12,8 +12,8 @@
- VERIFY: If config not loaded, STOP and report error to user
- DO NOT PROCEED to step 3 until config is successfully loaded and variables stored</step>
<step n="3">Remember: user's name is {user_name}</step>
<step n="4">Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/memories.md into permanent context</step>
<step n="4">Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives</step>
<step n="5">Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/memories.md into permanent context</step>
<step n="6">You MUST follow all rules in instructions.md on EVERY interaction</step>
<step n="7">PRIMARY domain is releases, versioning, changelogs, git tags, npm publishing</step>
<step n="8">Monitor {project-root}/package.json for version management</step>

View File

@ -32,8 +32,8 @@ agent:
critical_actions:
# CRITICAL: Load sidecar files FIRST for Expert agent
- Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/src/modules/bmd/agents/cli-chief-sidecar/memories.md into permanent context
- Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/bmd/agents/cli-chief-sidecar/memories.md into permanent context
- You MUST follow all rules in instructions.md on EVERY interaction
# Domain restriction for CLI focus
- PRIMARY domain is {project-root}/tools/cli/ - this is your territory

View File

@ -32,8 +32,8 @@ agent:
critical_actions:
# CRITICAL: Load sidecar files FIRST for Expert agent
- Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/src/modules/bmd/agents/doc-keeper-sidecar/memories.md into permanent context
- Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/bmd/agents/doc-keeper-sidecar/memories.md into permanent context
- You MUST follow all rules in instructions.md on EVERY interaction
# Domain restriction for documentation focus
- PRIMARY domain is all documentation files (*.md, README, guides, examples)

View File

@ -32,8 +32,8 @@ agent:
critical_actions:
# CRITICAL: Load sidecar files FIRST for Expert agent
- Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/src/modules/bmd/agents/release-chief-sidecar/memories.md into permanent context
- Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/instructions.md and follow ALL directives
- Load COMPLETE file {project-root}/bmd/agents/release-chief-sidecar/memories.md into permanent context
- You MUST follow all rules in instructions.md on EVERY interaction
# Domain restriction for release focus
- PRIMARY domain is releases, versioning, changelogs, git tags, npm publishing

View File

@ -89,7 +89,7 @@ my-custom-module/
### Example: install-config.yaml
**Reference**: `/Users/brianmadison/dev/BMAD-METHOD/src/modules/bmm/_module-installer/install-config.yaml`
**Reference**: `/src/modules/bmm/_module-installer/install-config.yaml`
```yaml
# Module metadata

View File

@ -1,5 +1,5 @@
# BMM Project Scale Levels - Source of Truth
# Reference: /src/modules/bmm/README.md lines 77-85
# Reference: /bmad/bmm/README.md lines 77-85
levels:
0:

View File

@ -23,6 +23,15 @@ module.exports = {
return;
}
// Handle quick update separately
if (config.actionType === 'quick-update') {
const result = await installer.quickUpdate(config);
console.log(chalk.green('\n✨ Quick update complete!'));
console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings`));
process.exit(0);
return;
}
// Regular install/update flow
const result = await installer.install(config);

View File

@ -26,22 +26,25 @@ class ConfigCollector {
return false;
}
// Try to load existing module configs
const modules = ['core', 'bmm', 'cis'];
// Dynamically discover all installed modules by scanning bmad directory
// A directory is a module ONLY if it contains a config.yaml file
let foundAny = false;
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const moduleName of modules) {
const moduleConfigPath = path.join(bmadDir, moduleName, 'config.yaml');
if (await fs.pathExists(moduleConfigPath)) {
try {
const content = await fs.readFile(moduleConfigPath, 'utf8');
const moduleConfig = yaml.load(content);
if (moduleConfig) {
this.existingConfig[moduleName] = moduleConfig;
foundAny = true;
for (const entry of entries) {
if (entry.isDirectory()) {
const moduleConfigPath = path.join(bmadDir, entry.name, 'config.yaml');
if (await fs.pathExists(moduleConfigPath)) {
try {
const content = await fs.readFile(moduleConfigPath, 'utf8');
const moduleConfig = yaml.load(content);
if (moduleConfig) {
this.existingConfig[entry.name] = moduleConfig;
foundAny = true;
}
} catch {
// Ignore parse errors for individual modules
}
} catch {
// Ignore parse errors for individual modules
}
}
}
@ -86,6 +89,203 @@ class ConfigCollector {
return this.collectedConfig;
}
/**
* Collect configuration for a single module (Quick Update mode - only new fields)
* @param {string} moduleName - Module name
* @param {string} projectDir - Target project directory
* @param {boolean} silentMode - If true, only prompt for new/missing fields
* @returns {boolean} True if new fields were prompted, false if all fields existed
*/
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
this.currentProjectDir = projectDir;
// Load existing config if not already loaded
if (!this.existingConfig) {
await this.loadExistingConfig(projectDir);
}
// Initialize allAnswers if not already initialized
if (!this.allAnswers) {
this.allAnswers = {};
}
// Load module's install config schema
const installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
const legacyConfigPath = path.join(getModulePath(moduleName), 'config.yaml');
let configPath = null;
if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(legacyConfigPath)) {
configPath = legacyConfigPath;
} else {
// No config schema for this module - use existing values
if (this.existingConfig && this.existingConfig[moduleName]) {
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
this.collectedConfig[moduleName] = { ...this.existingConfig[moduleName] };
}
return false;
}
const configContent = await fs.readFile(configPath, 'utf8');
const moduleConfig = yaml.load(configContent);
if (!moduleConfig) {
return false;
}
// Compare schema with existing config to find new/missing fields
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
const existingKeys = this.existingConfig && this.existingConfig[moduleName] ? Object.keys(this.existingConfig[moduleName]) : [];
const newKeys = configKeys.filter((key) => {
const item = moduleConfig[key];
// Check if it's a config item and doesn't exist in existing config
return item && typeof item === 'object' && item.prompt && !existingKeys.includes(key);
});
// If in silent mode and no new keys, use existing config and skip prompts
if (silentMode && newKeys.length === 0) {
if (this.existingConfig && this.existingConfig[moduleName]) {
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
this.collectedConfig[moduleName] = { ...this.existingConfig[moduleName] };
// Also populate allAnswers for cross-referencing
for (const [key, value] of Object.entries(this.existingConfig[moduleName])) {
this.allAnswers[`${moduleName}_${key}`] = value;
}
}
return false; // No new fields
}
// If we have new fields, show prompt section and collect only new fields
if (newKeys.length > 0) {
console.log(chalk.yellow(`\n📋 New configuration options available for ${moduleName}`));
if (moduleConfig.prompt) {
const prompts = Array.isArray(moduleConfig.prompt) ? moduleConfig.prompt : [moduleConfig.prompt];
CLIUtils.displayPromptSection(prompts);
}
const questions = [];
for (const key of newKeys) {
const item = moduleConfig[key];
const question = await this.buildQuestion(moduleName, key, item);
if (question) {
questions.push(question);
}
}
if (questions.length > 0) {
console.log(); // Line break before questions
const answers = await inquirer.prompt(questions);
// Store answers for cross-referencing
Object.assign(this.allAnswers, answers);
// Process answers and build result values
for (const key of Object.keys(answers)) {
const originalKey = key.replace(`${moduleName}_`, '');
const item = moduleConfig[originalKey];
const value = answers[key];
let result;
if (Array.isArray(value)) {
result = value;
} else if (item.result) {
result = this.processResultTemplate(item.result, value);
} else {
result = value;
}
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
this.collectedConfig[moduleName][originalKey] = result;
}
}
}
// Copy over existing values for fields that weren't prompted
if (this.existingConfig && this.existingConfig[moduleName]) {
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
for (const [key, value] of Object.entries(this.existingConfig[moduleName])) {
if (!this.collectedConfig[moduleName][key]) {
this.collectedConfig[moduleName][key] = value;
this.allAnswers[`${moduleName}_${key}`] = value;
}
}
}
return newKeys.length > 0; // Return true if we prompted for new fields
}
/**
* Process a result template with value substitution
* @param {*} resultTemplate - The result template
* @param {*} value - The value to substitute
* @returns {*} Processed result
*/
processResultTemplate(resultTemplate, value) {
let result = resultTemplate;
if (typeof result === 'string' && value !== undefined) {
if (typeof value === 'string') {
result = result.replace('{value}', value);
} else if (typeof value === 'boolean' || typeof value === 'number') {
if (result === '{value}') {
result = value;
} else {
result = result.replace('{value}', value);
}
} else {
result = value;
}
if (typeof result === 'string') {
result = result.replaceAll(/{([^}]+)}/g, (match, configKey) => {
if (configKey === 'project-root') {
return '{project-root}';
}
if (configKey === 'value') {
return match;
}
let configValue = this.allAnswers[configKey] || this.allAnswers[`${configKey}`];
if (!configValue) {
for (const [answerKey, answerValue] of Object.entries(this.allAnswers)) {
if (answerKey.endsWith(`_${configKey}`)) {
configValue = answerValue;
break;
}
}
}
if (!configValue) {
for (const mod of Object.keys(this.collectedConfig)) {
if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) {
configValue = this.collectedConfig[mod][configKey];
if (typeof configValue === 'string' && configValue.includes('{project-root}/')) {
configValue = configValue.replace('{project-root}/', '');
}
break;
}
}
}
return configValue || match;
});
}
}
return result;
}
/**
* Collect configuration for a single module
* @param {string} moduleName - Module name

View File

@ -55,14 +55,16 @@ class Detector {
}
// Check for modules
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg') {
const modulePath = path.join(bmadDir, entry.name);
// If manifest exists, use it as the source of truth for installed modules
// Otherwise fall back to directory scanning (legacy installations)
if (manifestData && manifestData.modules && manifestData.modules.length > 0) {
// Use manifest module list - these are officially installed modules
for (const moduleId of manifestData.modules) {
const modulePath = path.join(bmadDir, moduleId);
const moduleConfigPath = path.join(modulePath, 'config.yaml');
const moduleInfo = {
id: entry.name,
id: moduleId,
path: modulePath,
version: 'unknown',
};
@ -72,7 +74,7 @@ class Detector {
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const config = yaml.load(configContent);
moduleInfo.version = config.version || 'unknown';
moduleInfo.name = config.name || entry.name;
moduleInfo.name = config.name || moduleId;
moduleInfo.description = config.description;
} catch {
// Ignore config read errors
@ -81,6 +83,36 @@ class Detector {
result.modules.push(moduleInfo);
}
} else {
// Fallback: scan directory for modules (legacy installations without manifest)
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg') {
const modulePath = path.join(bmadDir, entry.name);
const moduleConfigPath = path.join(modulePath, 'config.yaml');
// Only treat it as a module if it has a config.yaml
if (await fs.pathExists(moduleConfigPath)) {
const moduleInfo = {
id: entry.name,
path: modulePath,
version: 'unknown',
};
try {
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const config = yaml.load(configContent);
moduleInfo.version = config.version || 'unknown';
moduleInfo.name = config.name || entry.name;
moduleInfo.description = config.description;
} catch {
// Ignore config read errors
}
result.modules.push(moduleInfo);
}
}
}
}
// Check for IDE configurations from manifest

View File

@ -162,8 +162,15 @@ class Installer {
}
}
// Collect configurations for modules (core was already collected in UI.promptInstall if interactive)
const moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory));
// Collect configurations for modules (skip if quick update already collected them)
let moduleConfigs;
if (config._quickUpdate) {
// Quick update already collected all configs, use them directly
moduleConfigs = this.configCollector.collectedConfig;
} else {
// Regular install - collect configurations (core was already collected in UI.promptInstall if interactive)
moduleConfigs = await this.configCollector.collectAllConfigurations(config.modules || [], path.resolve(config.directory));
}
// Tool selection will be collected after we determine if it's a reinstall/update/new install
@ -199,7 +206,7 @@ class Installer {
spinner.text = 'Checking for existing installation...';
const existingInstall = await this.detector.detect(bmadDir);
if (existingInstall.installed && !config.force) {
if (existingInstall.installed && !config.force && !config._quickUpdate) {
spinner.stop();
console.log(chalk.yellow('\n⚠ Existing BMAD installation detected'));
@ -300,18 +307,78 @@ class Installer {
console.log(chalk.dim('DEBUG: No modified files detected'));
}
}
} else if (existingInstall.installed && config._quickUpdate) {
// Quick update mode - automatically treat as update without prompting
spinner.text = 'Preparing quick update...';
config._isUpdate = true;
config._existingInstall = existingInstall;
// Detect custom and modified files BEFORE updating
const existingFilesManifest = await this.readFilesManifest(bmadDir);
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
config._customFiles = customFiles;
config._modifiedFiles = modifiedFiles;
// Back up custom files
if (customFiles.length > 0) {
const tempBackupDir = path.join(projectDir, '.bmad-custom-backup-temp');
await fs.ensureDir(tempBackupDir);
spinner.start(`Backing up ${customFiles.length} custom files...`);
for (const customFile of customFiles) {
const relativePath = path.relative(bmadDir, customFile);
const backupPath = path.join(tempBackupDir, relativePath);
await fs.ensureDir(path.dirname(backupPath));
await fs.copy(customFile, backupPath);
}
spinner.succeed(`Backed up ${customFiles.length} custom files`);
config._tempBackupDir = tempBackupDir;
}
// Back up modified files
if (modifiedFiles.length > 0) {
const tempModifiedBackupDir = path.join(projectDir, '.bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir);
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path);
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
await fs.ensureDir(path.dirname(tempBackupPath));
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
}
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
config._tempModifiedBackupDir = tempModifiedBackupDir;
}
}
// Now collect tool configurations after we know if it's a reinstall
// Skip for quick update since we already have the IDE list
spinner.stop();
const toolSelection = await this.collectToolConfigurations(
path.resolve(config.directory),
config.modules,
config._isFullReinstall || false,
config._previouslyConfiguredIdes || [],
);
let toolSelection;
if (config._quickUpdate) {
// Quick update already has IDEs configured, skip prompting
// Set a flag to indicate all IDEs are pre-configured
const preConfiguredIdes = {};
for (const ide of config.ides || []) {
preConfiguredIdes[ide] = { _alreadyConfigured: true };
}
toolSelection = {
ides: config.ides || [],
skipIde: !config.ides || config.ides.length === 0,
configurations: preConfiguredIdes,
};
} else {
toolSelection = await this.collectToolConfigurations(
path.resolve(config.directory),
config.modules,
config._isFullReinstall || false,
config._previouslyConfiguredIdes || [],
);
}
// Merge tool selection into config
// Merge tool selection into config (for both quick update and regular flow)
config.ides = toolSelection.ides;
config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations;
@ -385,8 +452,13 @@ class Installer {
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
spinner.start('Generating workflow and agent manifests...');
const manifestGen = new ManifestGenerator();
// Include preserved modules (from quick update) in the manifest
const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || [];
const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, {
ides: config.ides || [],
preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
});
spinner.succeed(
@ -1349,6 +1421,112 @@ class Installer {
}
}
/**
* Quick update method - preserves all settings and only prompts for new config fields
* @param {Object} config - Configuration with directory
* @returns {Object} Update result
*/
async quickUpdate(config) {
const ora = require('ora');
const spinner = ora('Starting quick update...').start();
try {
const projectDir = path.resolve(config.directory);
const bmadDir = path.join(projectDir, 'bmad');
// Check if bmad directory exists
if (!(await fs.pathExists(bmadDir))) {
spinner.fail('No BMAD installation found');
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
}
spinner.text = 'Detecting installed modules and configuration...';
// Detect existing installation
const existingInstall = await this.detector.detect(bmadDir);
const installedModules = existingInstall.modules.map((m) => m.id);
const configuredIdes = existingInstall.ides || [];
// Get available modules (what we have source for)
const availableModules = await this.moduleManager.listAvailable();
const availableModuleIds = new Set(availableModules.map((m) => m.id));
// Only update modules that are BOTH installed AND available (we have source for)
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
if (skippedModules.length > 0) {
console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`));
}
// Load existing configs and collect new fields (if any)
console.log(chalk.cyan('\n📋 Checking for new configuration options...'));
await this.configCollector.loadExistingConfig(projectDir);
let promptedForNewFields = false;
// Check core config for new fields
const corePrompted = await this.configCollector.collectModuleConfigQuick('core', projectDir, true);
if (corePrompted) {
promptedForNewFields = true;
}
// Check each module we're updating for new fields (NOT skipped modules)
for (const moduleName of modulesToUpdate) {
const modulePrompted = await this.configCollector.collectModuleConfigQuick(moduleName, projectDir, true);
if (modulePrompted) {
promptedForNewFields = true;
}
}
if (!promptedForNewFields) {
console.log(chalk.green('✓ All configuration is up to date, no new options to configure'));
}
// Add metadata
this.configCollector.collectedConfig._meta = {
version: require(path.join(getProjectRoot(), 'package.json')).version,
installDate: new Date().toISOString(),
lastModified: new Date().toISOString(),
};
// Now run the full installation with the collected configs
spinner.start('Updating BMAD installation...');
// Build the config object for the installer
const installConfig = {
directory: projectDir,
installCore: true,
modules: modulesToUpdate, // Only update modules we have source for
ides: configuredIdes,
skipIde: configuredIdes.length === 0,
coreConfig: this.configCollector.collectedConfig.core,
actionType: 'install', // Use regular install flow
_quickUpdate: true, // Flag to skip certain prompts
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
};
// Call the standard install method
const result = await this.install(installConfig);
spinner.succeed('Quick update complete!');
return {
success: true,
moduleCount: modulesToUpdate.length + 1, // +1 for core
hadNewFields: promptedForNewFields,
modules: ['core', ...modulesToUpdate],
skippedModules: skippedModules,
ides: configuredIdes,
};
} catch (error) {
spinner.fail('Quick update failed');
throw error;
}
}
/**
* Private: Prompt for update action
*/

View File

@ -28,8 +28,11 @@ class ManifestGenerator {
const cfgDir = path.join(bmadDir, '_cfg');
await fs.ensureDir(cfgDir);
// Store modules list
this.modules = ['core', ...selectedModules];
// Store modules list (all modules including preserved ones)
const preservedModules = options.preservedModules || [];
this.modules = ['core', ...selectedModules, ...preservedModules];
this.updatedModules = ['core', ...selectedModules]; // Only these get rescanned
this.preservedModules = preservedModules; // These stay as-is in CSVs
this.bmadDir = bmadDir;
this.allInstalledFiles = installedFiles;
@ -364,6 +367,45 @@ class ManifestGenerator {
return manifestPath;
}
/**
* Read existing CSV and preserve rows for modules NOT being updated
* @param {string} csvPath - Path to existing CSV file
* @param {number} moduleColumnIndex - Which column contains the module name (0-indexed)
* @returns {Array} Preserved CSV rows (without header)
*/
async getPreservedCsvRows(csvPath, moduleColumnIndex) {
if (!(await fs.pathExists(csvPath)) || this.preservedModules.length === 0) {
return [];
}
try {
const content = await fs.readFile(csvPath, 'utf8');
const lines = content.trim().split('\n');
// Skip header row
const dataRows = lines.slice(1);
const preservedRows = [];
for (const row of dataRows) {
// Simple CSV parsing (handles quoted values)
const columns = row.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
const cleanColumns = columns.map((c) => c.replaceAll(/^"|"$/g, ''));
const moduleValue = cleanColumns[moduleColumnIndex];
// Keep this row if it belongs to a preserved module
if (this.preservedModules.includes(moduleValue)) {
preservedRows.push(row);
}
}
return preservedRows;
} catch (error) {
console.warn(`Warning: Failed to read existing CSV ${csvPath}:`, error.message);
return [];
}
}
/**
* Write workflow manifest CSV
* @returns {string} Path to the manifest file
@ -371,14 +413,22 @@ class ManifestGenerator {
async writeWorkflowManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
// Get preserved rows from existing CSV (module is column 2, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 2);
// Create CSV header
let csv = 'name,description,module,path\n';
// Add rows
// Add new rows for updated modules
for (const workflow of this.workflows) {
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
}
// Add preserved rows for modules we didn't update
for (const row of preservedRows) {
csv += row + '\n';
}
await fs.writeFile(csvPath, csv);
return csvPath;
}
@ -390,14 +440,22 @@ class ManifestGenerator {
async writeAgentManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
// Get preserved rows from existing CSV (module is column 8, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 8);
// Create CSV header with persona fields
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
// Add rows
// Add new rows for updated modules
for (const agent of this.agents) {
csv += `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"\n`;
}
// Add preserved rows for modules we didn't update
for (const row of preservedRows) {
csv += row + '\n';
}
await fs.writeFile(csvPath, csv);
return csvPath;
}
@ -409,14 +467,22 @@ class ManifestGenerator {
async writeTaskManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'task-manifest.csv');
// Get preserved rows from existing CSV (module is column 3, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 3);
// Create CSV header
let csv = 'name,displayName,description,module,path\n';
// Add rows
// Add new rows for updated modules
for (const task of this.tasks) {
csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}"\n`;
}
// Add preserved rows for modules we didn't update
for (const row of preservedRows) {
csv += row + '\n';
}
await fs.writeFile(csvPath, csv);
return csvPath;
}
@ -444,6 +510,9 @@ class ManifestGenerator {
async writeFilesManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'files-manifest.csv');
// Get preserved rows from existing CSV (module is column 2, 0-indexed)
const preservedRows = await this.getPreservedCsvRows(csvPath, 2);
// Create CSV header with hash column
let csv = 'type,name,module,path,hash\n';
@ -490,11 +559,16 @@ class ManifestGenerator {
return a.name.localeCompare(b.name);
});
// Add rows
// Add rows for updated modules
for (const file of allFiles) {
csv += `"${file.type}","${file.name}","${file.module}","${file.path}","${file.hash}"\n`;
}
// Add preserved rows for modules we didn't update
for (const row of preservedRows) {
csv += row + '\n';
}
await fs.writeFile(csvPath, csv);
return csvPath;
}

View File

@ -128,8 +128,12 @@ class ClaudeCodeSetup extends BaseIdeSetup {
}
// Process Claude Code specific injections for installed modules
// Use pre-collected configuration if available
if (options.preCollectedConfig) {
// Use pre-collected configuration if available, or skip if already configured
if (options.preCollectedConfig && options.preCollectedConfig._alreadyConfigured) {
// IDE is already configured from previous installation, skip prompting
// Just process with default/existing configuration
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, {});
} else if (options.preCollectedConfig) {
await this.processModuleInjectionsWithConfig(projectDir, bmadDir, options, options.preCollectedConfig);
} else {
await this.processModuleInjections(projectDir, bmadDir, options);

View File

@ -35,12 +35,22 @@ class UI {
name: 'actionType',
message: 'What would you like to do?',
choices: [
{ name: 'Update BMAD Installation', value: 'install' },
{ name: 'Quick Update (Settings Preserved)', value: 'quick-update' },
{ name: 'Modify BMAD Installation (Confirm or change each setting)', value: 'install' },
{ name: 'Compile Agents (Quick rebuild of all agent .md files)', value: 'compile' },
],
default: 'quick-update',
},
]);
// Handle quick update separately
if (actionType === 'quick-update') {
return {
actionType: 'quick-update',
directory: confirmedDirectory,
};
}
// Handle agent compilation separately
if (actionType === 'compile') {
return {