diff --git a/.claude/commands/bmad/bmd/agents/cli-chief.md b/.claude/commands/bmad/bmd/agents/cli-chief.md
index 27b206bb..e7361bf6 100644
--- a/.claude/commands/bmad/bmd/agents/cli-chief.md
+++ b/.claude/commands/bmad/bmd/agents/cli-chief.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is {project-root}/tools/cli/ - this is your territory
You may read other project files for context but focus changes on CLI domain
diff --git a/.claude/commands/bmad/bmd/agents/doc-keeper.md b/.claude/commands/bmad/bmd/agents/doc-keeper.md
index b7fc5373..ecd648c1 100644
--- a/.claude/commands/bmad/bmd/agents/doc-keeper.md
+++ b/.claude/commands/bmad/bmd/agents/doc-keeper.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is all documentation files (*.md, README, guides, examples)
Monitor code changes that affect documented behavior
diff --git a/.claude/commands/bmad/bmd/agents/release-chief.md b/.claude/commands/bmad/bmd/agents/release-chief.md
index 1c2aed72..00927e40 100644
--- a/.claude/commands/bmad/bmd/agents/release-chief.md
+++ b/.claude/commands/bmad/bmd/agents/release-chief.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is releases, versioning, changelogs, git tags, npm publishing
Monitor {project-root}/package.json for version management
diff --git a/bmad/bmd/agents/cli-chief.md b/bmad/bmd/agents/cli-chief.md
index 27b206bb..e7361bf6 100644
--- a/bmad/bmd/agents/cli-chief.md
+++ b/bmad/bmd/agents/cli-chief.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is {project-root}/tools/cli/ - this is your territory
You may read other project files for context but focus changes on CLI domain
diff --git a/bmad/bmd/agents/doc-keeper.md b/bmad/bmd/agents/doc-keeper.md
index b7fc5373..ecd648c1 100644
--- a/bmad/bmd/agents/doc-keeper.md
+++ b/bmad/bmd/agents/doc-keeper.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is all documentation files (*.md, README, guides, examples)
Monitor code changes that affect documented behavior
diff --git a/bmad/bmd/agents/release-chief.md b/bmad/bmd/agents/release-chief.md
index 1c2aed72..00927e40 100644
--- a/bmad/bmd/agents/release-chief.md
+++ b/bmad/bmd/agents/release-chief.md
@@ -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
Remember: user's name is {user_name}
- 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
PRIMARY domain is releases, versioning, changelogs, git tags, npm publishing
Monitor {project-root}/package.json for version management
diff --git a/bmd/agents/cli-chief.agent.yaml b/bmd/agents/cli-chief.agent.yaml
index 8dfd5edc..84f02746 100644
--- a/bmd/agents/cli-chief.agent.yaml
+++ b/bmd/agents/cli-chief.agent.yaml
@@ -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
diff --git a/bmd/agents/doc-keeper.agent.yaml b/bmd/agents/doc-keeper.agent.yaml
index cf48bce9..91b19605 100644
--- a/bmd/agents/doc-keeper.agent.yaml
+++ b/bmd/agents/doc-keeper.agent.yaml
@@ -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)
diff --git a/bmd/agents/release-chief.agent.yaml b/bmd/agents/release-chief.agent.yaml
index ac9b433f..d6b1fd44 100644
--- a/bmd/agents/release-chief.agent.yaml
+++ b/bmd/agents/release-chief.agent.yaml
@@ -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
diff --git a/bmd/bmad-custom-module-installer-plan.md b/bmd/bmad-custom-module-installer-plan.md
index 1d768cf4..6971e10d 100644
--- a/bmd/bmad-custom-module-installer-plan.md
+++ b/bmd/bmad-custom-module-installer-plan.md
@@ -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
diff --git a/src/modules/bmm/workflows/workflow-status/project-levels.yaml b/src/modules/bmm/workflows/workflow-status/project-levels.yaml
index fc38be03..75cf7fd6 100644
--- a/src/modules/bmm/workflows/workflow-status/project-levels.yaml
+++ b/src/modules/bmm/workflows/workflow-status/project-levels.yaml
@@ -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:
diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js
index 714b45ae..e968ca4f 100644
--- a/tools/cli/commands/install.js
+++ b/tools/cli/commands/install.js
@@ -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);
diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js
index 90b3a547..f55ee958 100644
--- a/tools/cli/installers/lib/core/config-collector.js
+++ b/tools/cli/installers/lib/core/config-collector.js
@@ -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
diff --git a/tools/cli/installers/lib/core/detector.js b/tools/cli/installers/lib/core/detector.js
index d3e090af..d8df39c5 100644
--- a/tools/cli/installers/lib/core/detector.js
+++ b/tools/cli/installers/lib/core/detector.js
@@ -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
diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js
index 6df7b66a..f52488dd 100644
--- a/tools/cli/installers/lib/core/installer.js
+++ b/tools/cli/installers/lib/core/installer.js
@@ -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
*/
diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js
index 2e1c759a..b3543e88 100644
--- a/tools/cli/installers/lib/core/manifest-generator.js
+++ b/tools/cli/installers/lib/core/manifest-generator.js
@@ -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;
}
diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js
index 83721553..98e03ee9 100644
--- a/tools/cli/installers/lib/ide/claude-code.js
+++ b/tools/cli/installers/lib/ide/claude-code.js
@@ -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);
diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js
index de576aa0..c48f8ded 100644
--- a/tools/cli/lib/ui.js
+++ b/tools/cli/lib/ui.js
@@ -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 {