install quick updates
This commit is contained in:
parent
0067fb4880
commit
8d81edf847
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue