Compare commits
No commits in common. "37f38ffd48ce8bda27ca5c35599a05eaeca673bb" and "fc3c3d873c51c7a50eb6cbbf60b2e8ccde7feabd" have entirely different histories.
37f38ffd48
...
fc3c3d873c
|
|
@ -5,56 +5,3 @@
|
||||||
For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml.
|
For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml.
|
||||||
|
|
||||||
For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org.
|
For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org.
|
||||||
|
|
||||||
## Post-Install Notes
|
|
||||||
|
|
||||||
Modules can display setup guidance to users after configuration is collected during `npx bmad-method install`. Notes are defined in the module's own `module.yaml` — no changes to the installer are needed.
|
|
||||||
|
|
||||||
### Simple Format
|
|
||||||
|
|
||||||
Always displayed after the module is configured:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
post-install-notes: |
|
|
||||||
Remember to set the API_KEY environment variable.
|
|
||||||
See: https://example.com/setup
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conditional Format
|
|
||||||
|
|
||||||
Display different messages based on a config question's answer:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
post-install-notes:
|
|
||||||
config_key_name:
|
|
||||||
value1: |
|
|
||||||
Instructions for value1...
|
|
||||||
value2: |
|
|
||||||
Instructions for value2...
|
|
||||||
```
|
|
||||||
|
|
||||||
Values without an entry (e.g., `none`) display nothing. Multiple config keys can each have their own conditional notes.
|
|
||||||
|
|
||||||
### Example: TEA Module
|
|
||||||
|
|
||||||
The TEA module uses the conditional format keyed on `tea_browser_automation`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
post-install-notes:
|
|
||||||
tea_browser_automation:
|
|
||||||
cli: |
|
|
||||||
Playwright CLI Setup:
|
|
||||||
npm install -g @playwright/cli@latest
|
|
||||||
playwright-cli install --skills
|
|
||||||
mcp: |
|
|
||||||
Playwright MCP Setup (two servers):
|
|
||||||
1. playwright — npx @playwright/mcp@latest
|
|
||||||
2. playwright-test — npx playwright run-test-mcp-server
|
|
||||||
auto: |
|
|
||||||
Playwright CLI Setup:
|
|
||||||
...
|
|
||||||
Playwright MCP Setup (two servers):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
When a user selects `auto`, they see both CLI and MCP instructions. When they select `none`, nothing is shown.
|
|
||||||
|
|
|
||||||
|
|
@ -43,17 +43,12 @@ module.exports = {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
projectDir = path.resolve(customDir.trim());
|
projectDir = path.resolve(customDir);
|
||||||
} else {
|
} else {
|
||||||
projectDir = process.cwd();
|
projectDir = process.cwd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await fs.pathExists(projectDir))) {
|
|
||||||
await prompts.log.error(`Directory does not exist: ${projectDir}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { bmadDir } = await installer.findBmadDir(projectDir);
|
const { bmadDir } = await installer.findBmadDir(projectDir);
|
||||||
|
|
||||||
if (!(await fs.pathExists(bmadDir))) {
|
if (!(await fs.pathExists(bmadDir))) {
|
||||||
|
|
@ -95,26 +90,6 @@ module.exports = {
|
||||||
removeModules = selected.includes('modules');
|
removeModules = selected.includes('modules');
|
||||||
removeIdeConfigs = selected.includes('ide');
|
removeIdeConfigs = selected.includes('ide');
|
||||||
removeOutputFolder = selected.includes('output');
|
removeOutputFolder = selected.includes('output');
|
||||||
|
|
||||||
const red = (s) => `\u001B[31m${s}\u001B[0m`;
|
|
||||||
await prompts.note(
|
|
||||||
red('💀 This action is IRREVERSIBLE! Removed files cannot be recovered!') +
|
|
||||||
'\n' +
|
|
||||||
red('💀 IDE configurations and modules will need to be reinstalled.') +
|
|
||||||
'\n' +
|
|
||||||
red('💀 User artifacts are preserved unless explicitly selected.'),
|
|
||||||
'!! DESTRUCTIVE ACTION !!',
|
|
||||||
);
|
|
||||||
|
|
||||||
const confirmed = await prompts.confirm({
|
|
||||||
message: 'Proceed with uninstall?',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
await prompts.outro('Uninstall cancelled.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: IDE integrations
|
// Phase 1: IDE integrations
|
||||||
|
|
|
||||||
|
|
@ -550,8 +550,6 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.displayModulePostConfigNotes(moduleName, moduleConfig);
|
|
||||||
|
|
||||||
return newKeys.length > 0 || newStaticKeys.length > 0; // Return true if we had any new fields (interactive or static)
|
return newKeys.length > 0 || newStaticKeys.length > 0; // Return true if we had any new fields (interactive or static)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -925,8 +923,6 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.displayModulePostConfigNotes(moduleName, moduleConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1199,59 +1195,6 @@ class ConfigCollector {
|
||||||
return question;
|
return question;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display post-configuration notes for a module
|
|
||||||
* Shows prerequisite guidance based on collected config values
|
|
||||||
* Reads notes from the module's `post-install-notes` section in module.yaml
|
|
||||||
* Supports two formats:
|
|
||||||
* - Simple string: always displayed
|
|
||||||
* - Object keyed by config field name, with value-specific messages
|
|
||||||
* @param {string} moduleName - Module name
|
|
||||||
* @param {Object} moduleConfig - Parsed module.yaml content
|
|
||||||
*/
|
|
||||||
async displayModulePostConfigNotes(moduleName, moduleConfig) {
|
|
||||||
if (this._silentConfig) return;
|
|
||||||
if (!moduleConfig || !moduleConfig['post-install-notes']) return;
|
|
||||||
|
|
||||||
const notes = moduleConfig['post-install-notes'];
|
|
||||||
const color = await prompts.getColor();
|
|
||||||
|
|
||||||
// Format 1: Simple string - always display
|
|
||||||
if (typeof notes === 'string') {
|
|
||||||
await prompts.log.message('');
|
|
||||||
for (const line of notes.trim().split('\n')) {
|
|
||||||
await prompts.log.message(color.dim(line));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format 2: Conditional on config values
|
|
||||||
if (typeof notes === 'object') {
|
|
||||||
const config = this.collectedConfig[moduleName];
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
let hasOutput = false;
|
|
||||||
for (const [configKey, valueMessages] of Object.entries(notes)) {
|
|
||||||
const selectedValue = config[configKey];
|
|
||||||
if (!selectedValue || !valueMessages[selectedValue]) continue;
|
|
||||||
|
|
||||||
if (hasOutput) await prompts.log.message('');
|
|
||||||
hasOutput = true;
|
|
||||||
|
|
||||||
const message = valueMessages[selectedValue];
|
|
||||||
await prompts.log.message('');
|
|
||||||
for (const line of message.trim().split('\n')) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.endsWith(':') && !trimmedLine.startsWith(' ')) {
|
|
||||||
await prompts.log.info(color.bold(trimmedLine));
|
|
||||||
} else {
|
|
||||||
await prompts.log.message(color.dim(' ' + trimmedLine));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep merge two objects
|
* Deep merge two objects
|
||||||
* @param {Object} target - Target object
|
* @param {Object} target - Target object
|
||||||
|
|
|
||||||
|
|
@ -1552,18 +1552,30 @@ class Installer {
|
||||||
|
|
||||||
// 2. IDE CLEANUP (before _bmad/ deletion so configs are accessible)
|
// 2. IDE CLEANUP (before _bmad/ deletion so configs are accessible)
|
||||||
if (options.removeIdeConfigs !== false) {
|
if (options.removeIdeConfigs !== false) {
|
||||||
await this.uninstallIdeConfigs(projectDir, existingInstall, { silent: options.silent });
|
await this.ideManager.ensureInitialized();
|
||||||
|
const cleanupOptions = { isUninstall: true };
|
||||||
|
const ideList = existingInstall.ides || [];
|
||||||
|
if (ideList.length > 0) {
|
||||||
|
await this.ideManager.cleanupByList(projectDir, ideList, cleanupOptions);
|
||||||
|
} else {
|
||||||
|
await this.ideManager.cleanup(projectDir, cleanupOptions);
|
||||||
|
}
|
||||||
removed.ideConfigs = true;
|
removed.ideConfigs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. OUTPUT FOLDER (only if explicitly requested)
|
// 3. OUTPUT FOLDER (only if explicitly requested)
|
||||||
if (options.removeOutputFolder === true && outputFolder) {
|
if (options.removeOutputFolder === true && outputFolder) {
|
||||||
removed.outputFolder = await this.uninstallOutputFolder(projectDir, outputFolder);
|
const outputPath = path.join(projectDir, outputFolder);
|
||||||
|
if (await fs.pathExists(outputPath)) {
|
||||||
|
await fs.remove(outputPath);
|
||||||
|
removed.outputFolder = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. BMAD DIRECTORY (last, after everything that needs it)
|
// 4. BMAD DIRECTORY (last, after everything that needs it)
|
||||||
if (options.removeModules !== false) {
|
if (options.removeModules !== false) {
|
||||||
removed.modules = await this.uninstallModules(projectDir);
|
await fs.remove(bmadDir);
|
||||||
|
removed.modules = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, removed, version: existingInstall.version };
|
return { success: true, removed, version: existingInstall.version };
|
||||||
|
|
@ -1594,11 +1606,7 @@ class Installer {
|
||||||
*/
|
*/
|
||||||
async uninstallOutputFolder(projectDir, outputFolder) {
|
async uninstallOutputFolder(projectDir, outputFolder) {
|
||||||
if (!outputFolder) return false;
|
if (!outputFolder) return false;
|
||||||
const resolvedProject = path.resolve(projectDir);
|
const outputPath = path.join(projectDir, outputFolder);
|
||||||
const outputPath = path.resolve(resolvedProject, outputFolder);
|
|
||||||
if (!outputPath.startsWith(resolvedProject + path.sep)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (await fs.pathExists(outputPath)) {
|
if (await fs.pathExists(outputPath)) {
|
||||||
await fs.remove(outputPath);
|
await fs.remove(outputPath);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
if (!(await fs.pathExists(fullPath))) break;
|
if (!(await fs.pathExists(fullPath))) break;
|
||||||
const remaining = await fs.readdir(fullPath);
|
const remaining = await fs.readdir(fullPath);
|
||||||
if (remaining.length > 0) break;
|
if (remaining.length > 0) break;
|
||||||
await fs.rmdir(fullPath);
|
await fs.remove(fullPath);
|
||||||
} catch {
|
} catch {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -640,7 +640,7 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
// handles marker-based replacement in a single read-modify-write pass,
|
// handles marker-based replacement in a single read-modify-write pass,
|
||||||
// which correctly preserves user content outside the markers.
|
// which correctly preserves user content outside the markers.
|
||||||
if (options.isUninstall) {
|
if (options.isUninstall) {
|
||||||
await this.cleanupCopilotInstructions(projectDir, options);
|
await this.cleanupCopilotInstructions(projectDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -649,9 +649,8 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
* If file becomes empty after stripping, delete it.
|
* If file becomes empty after stripping, delete it.
|
||||||
* If a .bak backup exists and the main file was deleted, restore the backup.
|
* If a .bak backup exists and the main file was deleted, restore the backup.
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
* @param {Object} [options] - Options (e.g. { silent: true })
|
|
||||||
*/
|
*/
|
||||||
async cleanupCopilotInstructions(projectDir, options = {}) {
|
async cleanupCopilotInstructions(projectDir) {
|
||||||
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
||||||
const backupPath = `${instructionsPath}.bak`;
|
const backupPath = `${instructionsPath}.bak`;
|
||||||
|
|
||||||
|
|
@ -681,10 +680,8 @@ Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent ac
|
||||||
// If backup exists, restore it
|
// If backup exists, restore it
|
||||||
if (await fs.pathExists(backupPath)) {
|
if (await fs.pathExists(backupPath)) {
|
||||||
await fs.rename(backupPath, instructionsPath);
|
await fs.rename(backupPath, instructionsPath);
|
||||||
if (!options.silent) {
|
|
||||||
await prompts.log.message(' Restored copilot-instructions.md from backup');
|
await prompts.log.message(' Restored copilot-instructions.md from backup');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Write cleaned content back (preserve original whitespace)
|
// Write cleaned content back (preserve original whitespace)
|
||||||
await fs.writeFile(instructionsPath, cleaned, 'utf8');
|
await fs.writeFile(instructionsPath, cleaned, 'utf8');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue