install update

This commit is contained in:
Brian Madison
2025-06-14 15:06:41 -05:00
parent 2902221069
commit 5a7ded34e9
137 changed files with 65732 additions and 422 deletions

View File

@@ -1,13 +1,13 @@
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');
const glob = require('glob');
const chalk = require('chalk');
const fs = require("fs-extra");
const path = require("path");
const crypto = require("crypto");
const glob = require("glob");
const chalk = require("chalk");
class FileManager {
constructor() {
this.manifestDir = '.bmad';
this.manifestFile = 'install-manifest.yml';
this.manifestDir = ".bmad-core";
this.manifestFile = "install-manifest.yml";
}
async copyFile(source, destination) {
@@ -27,7 +27,10 @@ class FileManager {
await fs.copy(source, destination);
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy directory ${source}:`), error.message);
console.error(
chalk.red(`Failed to copy directory ${source}:`),
error.message
);
return false;
}
}
@@ -35,64 +38,76 @@ class FileManager {
async copyGlobPattern(pattern, sourceDir, destDir) {
const files = glob.sync(pattern, { cwd: sourceDir });
const copied = [];
for (const file of files) {
const sourcePath = path.join(sourceDir, file);
const destPath = path.join(destDir, file);
if (await this.copyFile(sourcePath, destPath)) {
copied.push(file);
}
}
return copied;
}
async calculateFileHash(filePath) {
try {
const content = await fs.readFile(filePath);
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
return crypto
.createHash("sha256")
.update(content)
.digest("hex")
.slice(0, 16);
} catch (error) {
return null;
}
}
async createManifest(installDir, config, files) {
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
const manifestPath = path.join(
installDir,
this.manifestDir,
this.manifestFile
);
const manifest = {
version: require('../package.json').version,
version: require("../package.json").version,
installed_at: new Date().toISOString(),
install_type: config.installType,
agent: config.agent || null,
ide_setup: config.ide || null,
files: []
files: [],
};
// Add file information
for (const file of files) {
const filePath = path.join(installDir, file);
const hash = await this.calculateFileHash(filePath);
manifest.files.push({
path: file,
hash: hash,
modified: false
modified: false,
});
}
// Write manifest
await fs.ensureDir(path.dirname(manifestPath));
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
return manifest;
}
async readManifest(installDir) {
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
const manifestPath = path.join(
installDir,
this.manifestDir,
this.manifestFile
);
try {
const content = await fs.readFile(manifestPath, 'utf8');
const content = await fs.readFile(manifestPath, "utf8");
return JSON.parse(content);
} catch (error) {
return null;
@@ -101,30 +116,30 @@ class FileManager {
async checkModifiedFiles(installDir, manifest) {
const modified = [];
for (const file of manifest.files) {
const filePath = path.join(installDir, file.path);
const currentHash = await this.calculateFileHash(filePath);
if (currentHash && currentHash !== file.hash) {
modified.push(file.path);
}
}
return modified;
}
async backupFile(filePath) {
const backupPath = filePath + '.bak';
const backupPath = filePath + ".bak";
let counter = 1;
let finalBackupPath = backupPath;
// Find a unique backup filename
while (await fs.pathExists(finalBackupPath)) {
finalBackupPath = `${filePath}.bak${counter}`;
counter++;
}
await fs.copy(filePath, finalBackupPath);
return finalBackupPath;
}
@@ -138,7 +153,7 @@ class FileManager {
}
async readFile(filePath) {
return fs.readFile(filePath, 'utf8');
return fs.readFile(filePath, "utf8");
}
async writeFile(filePath, content) {
@@ -151,4 +166,4 @@ class FileManager {
}
}
module.exports = new FileManager();
module.exports = new FileManager();

View File

@@ -1,24 +1,26 @@
const path = require('path');
const fileManager = require('./file-manager');
const configLoader = require('./config-loader');
const chalk = require('chalk');
const path = require("path");
const fileManager = require("./file-manager");
const configLoader = require("./config-loader");
const chalk = require("chalk");
class IdeSetup {
async setup(ide, installDir, selectedAgent = null) {
const ideConfig = await configLoader.getIdeConfiguration(ide);
if (!ideConfig) {
console.log(chalk.yellow(`\nNo configuration available for ${ide}`));
return false;
}
switch (ide) {
case 'cursor':
case "cursor":
return this.setupCursor(installDir, selectedAgent);
case 'claude-code':
case "claude-code":
return this.setupClaudeCode(installDir, selectedAgent);
case 'windsurf':
case "windsurf":
return this.setupWindsurf(installDir, selectedAgent);
case "roo":
return this.setupRoo(installDir, selectedAgent);
default:
console.log(chalk.yellow(`\nIDE ${ide} not yet supported`));
return false;
@@ -26,164 +28,344 @@ class IdeSetup {
}
async setupCursor(installDir, selectedAgent) {
const cursorRulesDir = path.join(installDir, '.cursor', 'rules');
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
const cursorRulesDir = path.join(installDir, ".cursor", "rules");
const agents = selectedAgent
? [selectedAgent]
: await this.getAllAgentIds(installDir);
await fileManager.ensureDirectory(cursorRulesDir);
for (const agentId of agents) {
// Check if .bmad-core is a subdirectory (full install) or if agents are in root (single agent install)
let agentPath = path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`);
if (!await fileManager.pathExists(agentPath)) {
agentPath = path.join(installDir, 'agents', `${agentId}.md`);
let agentPath = path.join(
installDir,
".bmad-core",
"agents",
`${agentId}.md`
);
if (!(await fileManager.pathExists(agentPath))) {
agentPath = path.join(installDir, "agents", `${agentId}.md`);
}
if (await fileManager.pathExists(agentPath)) {
const agentContent = await fileManager.readFile(agentPath);
const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`);
// Create MDC content with proper format
let mdcContent = '---\n';
mdcContent += 'description: \n';
mdcContent += 'globs: []\n';
mdcContent += 'alwaysApply: false\n';
mdcContent += '---\n\n';
let mdcContent = "---\n";
mdcContent += "description: \n";
mdcContent += "globs: []\n";
mdcContent += "alwaysApply: false\n";
mdcContent += "---\n\n";
mdcContent += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${this.getAgentTitle(agentId)} agent persona.\n\n`;
mdcContent += '## Agent Activation\n\n';
mdcContent += 'CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n';
mdcContent += '```yml\n';
mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${this.getAgentTitle(
agentId
)} agent persona.\n\n`;
mdcContent += "## Agent Activation\n\n";
mdcContent +=
"CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
mdcContent += "```yml\n";
// Extract just the YAML content from the agent file
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
if (yamlMatch) {
mdcContent += yamlMatch[1].trim();
} else {
// If no YAML found, include the whole content minus the header
mdcContent += agentContent.replace(/^#.*$/m, '').trim();
mdcContent += agentContent.replace(/^#.*$/m, "").trim();
}
mdcContent += '\n```\n\n';
mdcContent += '## File Reference\n\n';
mdcContent += "\n```\n\n";
mdcContent += "## File Reference\n\n";
mdcContent += `The complete agent definition is available in [.bmad-core/agents/${agentId}.md](mdc:.bmad-core/agents/${agentId}.md).\n\n`;
mdcContent += '## Usage\n\n';
mdcContent += `When the user types \`@${agentId}\`, activate this ${this.getAgentTitle(agentId)} persona and follow all instructions defined in the YML configuration above.\n`;
mdcContent += "## Usage\n\n";
mdcContent += `When the user types \`@${agentId}\`, activate this ${this.getAgentTitle(
agentId
)} persona and follow all instructions defined in the YML configuration above.\n`;
await fileManager.writeFile(mdcPath, mdcContent);
console.log(chalk.green(`✓ Created rule: ${agentId}.mdc`));
}
}
console.log(chalk.green(`\n✓ Created Cursor rules in ${cursorRulesDir}`));
return true;
}
async setupClaudeCode(installDir, selectedAgent) {
const commandsDir = path.join(installDir, '.claude', 'commands');
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
const commandsDir = path.join(installDir, ".claude", "commands");
const agents = selectedAgent
? [selectedAgent]
: await this.getAllAgentIds(installDir);
await fileManager.ensureDirectory(commandsDir);
for (const agentId of agents) {
// Check if .bmad-core is a subdirectory (full install) or if agents are in root (single agent install)
let agentPath = path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`);
if (!await fileManager.pathExists(agentPath)) {
agentPath = path.join(installDir, 'agents', `${agentId}.md`);
let agentPath = path.join(
installDir,
".bmad-core",
"agents",
`${agentId}.md`
);
if (!(await fileManager.pathExists(agentPath))) {
agentPath = path.join(installDir, "agents", `${agentId}.md`);
}
const commandPath = path.join(commandsDir, `${agentId}.md`);
if (await fileManager.pathExists(agentPath)) {
// Create command file with agent content
const agentContent = await fileManager.readFile(agentPath);
// Add command header
let commandContent = `# /${agentId} Command\n\n`;
commandContent += `When this command is used, adopt the following agent persona:\n\n`;
commandContent += agentContent;
await fileManager.writeFile(commandPath, commandContent);
console.log(chalk.green(`✓ Created command: /${agentId}`));
}
}
console.log(chalk.green(`\n✓ Created Claude Code commands in ${commandsDir}`));
console.log(
chalk.green(`\n✓ Created Claude Code commands in ${commandsDir}`)
);
return true;
}
async setupWindsurf(installDir, selectedAgent) {
const windsurfRulesDir = path.join(installDir, '.windsurf', 'rules');
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
const windsurfRulesDir = path.join(installDir, ".windsurf", "rules");
const agents = selectedAgent
? [selectedAgent]
: await this.getAllAgentIds(installDir);
await fileManager.ensureDirectory(windsurfRulesDir);
for (const agentId of agents) {
// Check if .bmad-core is a subdirectory (full install) or if agents are in root (single agent install)
let agentPath = path.join(installDir, '.bmad-core', 'agents', `${agentId}.md`);
if (!await fileManager.pathExists(agentPath)) {
agentPath = path.join(installDir, 'agents', `${agentId}.md`);
let agentPath = path.join(
installDir,
".bmad-core",
"agents",
`${agentId}.md`
);
if (!(await fileManager.pathExists(agentPath))) {
agentPath = path.join(installDir, "agents", `${agentId}.md`);
}
if (await fileManager.pathExists(agentPath)) {
const agentContent = await fileManager.readFile(agentPath);
const mdPath = path.join(windsurfRulesDir, `${agentId}.md`);
// Create MD content (similar to Cursor but without frontmatter)
let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${this.getAgentTitle(agentId)} agent persona.\n\n`;
mdContent += '## Agent Activation\n\n';
mdContent += 'CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n';
mdContent += '```yml\n';
mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${this.getAgentTitle(
agentId
)} agent persona.\n\n`;
mdContent += "## Agent Activation\n\n";
mdContent +=
"CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
mdContent += "```yml\n";
// Extract just the YAML content from the agent file
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
if (yamlMatch) {
mdContent += yamlMatch[1].trim();
} else {
// If no YAML found, include the whole content minus the header
mdContent += agentContent.replace(/^#.*$/m, '').trim();
mdContent += agentContent.replace(/^#.*$/m, "").trim();
}
mdContent += '\n```\n\n';
mdContent += '## File Reference\n\n';
mdContent += "\n```\n\n";
mdContent += "## File Reference\n\n";
mdContent += `The complete agent definition is available in [.bmad-core/agents/${agentId}.md](.bmad-core/agents/${agentId}.md).\n\n`;
mdContent += '## Usage\n\n';
mdContent += `When the user types \`@${agentId}\`, activate this ${this.getAgentTitle(agentId)} persona and follow all instructions defined in the YML configuration above.\n`;
mdContent += "## Usage\n\n";
mdContent += `When the user types \`@${agentId}\`, activate this ${this.getAgentTitle(
agentId
)} persona and follow all instructions defined in the YML configuration above.\n`;
await fileManager.writeFile(mdPath, mdContent);
console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
}
}
console.log(chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`));
console.log(
chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`)
);
return true;
}
async getAllAgentIds(installDir) {
// Check if .bmad-core is a subdirectory (full install) or if agents are in root (single agent install)
let agentsDir = path.join(installDir, '.bmad-core', 'agents');
if (!await fileManager.pathExists(agentsDir)) {
agentsDir = path.join(installDir, 'agents');
let agentsDir = path.join(installDir, ".bmad-core", "agents");
if (!(await fileManager.pathExists(agentsDir))) {
agentsDir = path.join(installDir, "agents");
}
const glob = require('glob');
const agentFiles = glob.sync('*.md', { cwd: agentsDir });
return agentFiles.map(file => path.basename(file, '.md'));
const glob = require("glob");
const agentFiles = glob.sync("*.md", { cwd: agentsDir });
return agentFiles.map((file) => path.basename(file, ".md"));
}
getAgentTitle(agentId) {
const agentTitles = {
'analyst': 'Business Analyst',
'architect': 'Solution Architect',
'bmad-master': 'BMAD Master',
'bmad-orchestrator': 'BMAD Orchestrator',
'dev': 'Developer',
'pm': 'Product Manager',
'po': 'Product Owner',
'qa': 'QA Specialist',
'sm': 'Scrum Master',
'ux-expert': 'UX Expert'
analyst: "Business Analyst",
architect: "Solution Architect",
"bmad-master": "BMAD Master",
"bmad-orchestrator": "BMAD Orchestrator",
dev: "Developer",
pm: "Product Manager",
po: "Product Owner",
qa: "QA Specialist",
sm: "Scrum Master",
"ux-expert": "UX Expert",
};
return agentTitles[agentId] || agentId;
}
async setupRoo(installDir, selectedAgent) {
const agents = selectedAgent
? [selectedAgent]
: await this.getAllAgentIds(installDir);
// Create .roo directory first
const rooDir = path.join(installDir, ".roo");
await fileManager.ensureDirectory(rooDir);
// Check for existing .roomodes file inside .roo directory
const roomodesPath = path.join(rooDir, ".roomodes");
let existingModes = [];
let existingContent = "";
if (await fileManager.pathExists(roomodesPath)) {
existingContent = await fileManager.readFile(roomodesPath);
// Parse existing modes to avoid duplicates
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
for (const match of modeMatches) {
existingModes.push(match[1]);
}
console.log(
chalk.yellow(
`Found existing .roomodes file with ${existingModes.length} modes`
)
);
}
// Create new modes content
let newModesContent = "";
for (const agentId of agents) {
// Skip if already exists
if (existingModes.includes(`bmad-${agentId}`)) {
console.log(
chalk.dim(`Skipping ${agentId} - already exists in .roomodes`)
);
continue;
}
// Read agent file to extract all information
let agentPath = path.join(
installDir,
".bmad-core",
"agents",
`${agentId}.md`
);
if (!(await fileManager.pathExists(agentPath))) {
agentPath = path.join(installDir, "agents", `${agentId}.md`);
}
if (await fileManager.pathExists(agentPath)) {
const agentContent = await fileManager.readFile(agentPath);
// Extract YAML content
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
if (yamlMatch) {
const yaml = yamlMatch[1];
// Extract agent info from YAML
const titleMatch = yaml.match(/title:\s*(.+)/);
const iconMatch = yaml.match(/icon:\s*(.+)/);
const whenToUseMatch = yaml.match(/whenToUse:\s*"(.+)"/);
const title = titleMatch ? titleMatch[1].trim() : agentId;
const icon = iconMatch ? iconMatch[1].trim() : "🤖";
const whenToUse = whenToUseMatch
? whenToUseMatch[1].trim()
: `Use for ${title} tasks`;
// Build mode entry
newModesContent += ` - slug: bmad-${agentId}\n`;
newModesContent += ` name: "${icon} ${title}"\n`;
newModesContent += ` roleDefinition: "You are the ${title} from BMAD-METHOD."\n`;
newModesContent += ` whenToUse: "${whenToUse}"\n`;
newModesContent += ` customInstructions: "CRITICAL: Read the full YML from .bmad-core/agents/${agentId}.md, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode."\n`;
newModesContent += ` groups:\n`;
newModesContent += ` - read\n`;
newModesContent += ` - edit # Full access - adjust permissions as needed\n`;
newModesContent += "\n";
console.log(
chalk.green(`✓ Added mode: bmad-${agentId} (${icon} ${title})`)
);
}
}
}
// Build final roomodes content
let roomodesContent = "";
if (existingContent) {
// If there's existing content, append new modes to it
roomodesContent = existingContent.trim() + "\n" + newModesContent;
} else {
// Create new .roomodes file with proper YAML structure
roomodesContent = "modes:\n" + newModesContent;
}
// Write .roomodes file
await fileManager.writeFile(roomodesPath, roomodesContent);
console.log(chalk.green("✓ Created .roo/.roomodes file"));
// Create README in .roo directory
const rooReadme = `# Roo Code Custom Modes for BMAD-METHOD
This directory contains custom mode configurations for Roo Code to enable BMAD agent personalities.
## Setup
The \`.roomodes\` file in the project root defines all BMAD agents as custom modes. Modes are automatically available in Roo Code when you open this project.
## Available Modes
${agents.map((id) => `- **bmad-${id}** - ${this.getAgentTitle(id)}`).join("\n")}
## Usage
In Roo Code:
1. Open the mode selector (usually in the status bar)
2. Select any BMAD agent mode
3. The AI will adopt that agent's personality and expertise
## Adding Custom Instructions
To add mode-specific instructions:
1. Create files in \`.roo/rules-{mode-slug}/\`
2. Add markdown files with additional context
3. The AI will automatically include these when the mode is active
`;
const readmePath = path.join(rooDir, "README.md");
await fileManager.writeFile(readmePath, rooReadme);
console.log(chalk.green("✓ Created .roo/README.md"));
console.log(chalk.green(`\n✓ Roo Code setup complete!`));
console.log(
chalk.dim(
"Custom modes will be available when you open this project in Roo Code"
)
);
return true;
}
}
module.exports = new IdeSetup();
module.exports = new IdeSetup();

View File

@@ -1,238 +1,282 @@
const chalk = require('chalk');
const ora = require('ora');
const path = require('path');
const configLoader = require('./config-loader');
const fileManager = require('./file-manager');
const ideSetup = require('./ide-setup');
const chalk = require("chalk");
const ora = require("ora");
const path = require("path");
const configLoader = require("./config-loader");
const fileManager = require("./file-manager");
const ideSetup = require("./ide-setup");
class Installer {
async install(config) {
const spinner = ora('Installing BMAD Method...').start();
const spinner = ora("Installing BMAD Method...").start();
try {
// Resolve installation directory
const installDir = path.resolve(config.directory);
// Check if directory already exists
if (await fileManager.pathExists(installDir)) {
const manifest = await fileManager.readManifest(installDir);
if (manifest) {
spinner.fail('BMAD is already installed in this directory');
console.log(chalk.yellow('\nUse "bmad update" to update the existing installation'));
spinner.fail("BMAD is already installed in this directory");
console.log(
chalk.yellow(
'\nUse "bmad update" to update the existing installation'
)
);
return;
}
}
let files = [];
if (config.installType === 'full') {
if (config.installType === "full") {
// Full installation - copy entire .bmad-core folder as a subdirectory
spinner.text = 'Copying complete .bmad-core folder...';
spinner.text = "Copying complete .bmad-core folder...";
const sourceDir = configLoader.getBmadCorePath();
const bmadCoreDestDir = path.join(installDir, '.bmad-core');
const bmadCoreDestDir = path.join(installDir, ".bmad-core");
await fileManager.copyDirectory(sourceDir, bmadCoreDestDir);
// Get list of all files for manifest
const glob = require('glob');
files = glob.sync('**/*', {
cwd: bmadCoreDestDir,
nodir: true,
ignore: ['**/.git/**', '**/node_modules/**']
}).map(file => path.join('.bmad-core', file));
} else if (config.installType === 'single-agent') {
const glob = require("glob");
files = glob
.sync("**/*", {
cwd: bmadCoreDestDir,
nodir: true,
ignore: ["**/.git/**", "**/node_modules/**"],
})
.map((file) => path.join(".bmad-core", file));
} else if (config.installType === "single-agent") {
// Single agent installation
spinner.text = `Installing ${config.agent} agent...`;
// Copy agent file
const agentPath = configLoader.getAgentPath(config.agent);
const destAgentPath = path.join(installDir, 'agents', `${config.agent}.md`);
const destAgentPath = path.join(
installDir,
"agents",
`${config.agent}.md`
);
await fileManager.copyFile(agentPath, destAgentPath);
files.push(`agents/${config.agent}.md`);
// Copy dependencies
const dependencies = await configLoader.getAgentDependencies(config.agent);
const dependencies = await configLoader.getAgentDependencies(
config.agent
);
const sourceBase = configLoader.getBmadCorePath();
for (const dep of dependencies) {
spinner.text = `Copying dependency: ${dep}`;
if (dep.includes('*')) {
if (dep.includes("*")) {
// Handle glob patterns
const copiedFiles = await fileManager.copyGlobPattern(
dep.replace('.bmad-core/', ''),
dep.replace(".bmad-core/", ""),
sourceBase,
installDir
);
files.push(...copiedFiles);
} else {
// Handle single files
const sourcePath = path.join(sourceBase, dep.replace('.bmad-core/', ''));
const destPath = path.join(installDir, dep.replace('.bmad-core/', ''));
const sourcePath = path.join(
sourceBase,
dep.replace(".bmad-core/", "")
);
const destPath = path.join(
installDir,
dep.replace(".bmad-core/", "")
);
if (await fileManager.copyFile(sourcePath, destPath)) {
files.push(dep.replace('.bmad-core/', ''));
files.push(dep.replace(".bmad-core/", ""));
}
}
}
}
// Set up IDE integration if requested
if (config.ide) {
spinner.text = `Setting up ${config.ide} integration...`;
// For full installations, IDE rules should be in the root install dir, not .bmad-core
await ideSetup.setup(config.ide, installDir, config.agent);
}
// Create manifest
spinner.text = 'Creating installation manifest...';
spinner.text = "Creating installation manifest...";
await fileManager.createManifest(installDir, config, files);
spinner.succeed('Installation complete!');
spinner.succeed("Installation complete!");
// Show success message
console.log(chalk.green('\n✓ BMAD Method installed successfully!\n'));
console.log(chalk.green("\n✓ BMAD Method installed successfully!\n"));
if (config.ide) {
const ideConfig = await configLoader.getIdeConfiguration(config.ide);
if (ideConfig && ideConfig.instructions) {
console.log(chalk.bold('To use BMAD agents in ' + ideConfig.name + ':'));
console.log(
chalk.bold("To use BMAD agents in " + ideConfig.name + ":")
);
console.log(ideConfig.instructions);
}
} else {
console.log(chalk.yellow('No IDE configuration was set up.'));
console.log('You can manually configure your IDE using the agent files in:', installDir);
console.log(chalk.yellow("No IDE configuration was set up."));
console.log(
"You can manually configure your IDE using the agent files in:",
installDir
);
}
if (config.installType === 'single-agent') {
console.log(chalk.dim('\nNeed other agents? Run: npx bmad-method install --agent=<name>'));
console.log(chalk.dim('Need everything? Run: npx bmad-method install --full'));
if (config.installType === "single-agent") {
console.log(
chalk.dim(
"\nNeed other agents? Run: npx bmad-method install --agent=<name>"
)
);
console.log(
chalk.dim("Need everything? Run: npx bmad-method install --full")
);
}
} catch (error) {
spinner.fail('Installation failed');
spinner.fail("Installation failed");
throw error;
}
}
async update(options) {
const spinner = ora('Checking for updates...').start();
const spinner = ora("Checking for updates...").start();
try {
// Find existing installation
const installDir = await this.findInstallation();
if (!installDir) {
spinner.fail('No BMAD installation found');
spinner.fail("No BMAD installation found");
return;
}
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
spinner.fail('Invalid installation - manifest not found');
spinner.fail("Invalid installation - manifest not found");
return;
}
// Check for modified files
spinner.text = 'Checking for modified files...';
const modifiedFiles = await fileManager.checkModifiedFiles(installDir, manifest);
spinner.text = "Checking for modified files...";
const modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
if (modifiedFiles.length > 0 && !options.force) {
spinner.warn('Found modified files');
console.log(chalk.yellow('\nThe following files have been modified:'));
modifiedFiles.forEach(file => console.log(` - ${file}`));
spinner.warn("Found modified files");
console.log(chalk.yellow("\nThe following files have been modified:"));
modifiedFiles.forEach((file) => console.log(` - ${file}`));
if (!options.dryRun) {
console.log(chalk.yellow('\nUse --force to overwrite modified files'));
console.log(chalk.yellow('or manually backup your changes first'));
console.log(
chalk.yellow("\nUse --force to overwrite modified files")
);
console.log(chalk.yellow("or manually backup your changes first"));
}
return;
}
if (options.dryRun) {
spinner.info('Dry run - no changes will be made');
console.log('\nFiles that would be updated:');
manifest.files.forEach(file => console.log(` - ${file.path}`));
spinner.info("Dry run - no changes will be made");
console.log("\nFiles that would be updated:");
manifest.files.forEach((file) => console.log(` - ${file.path}`));
return;
}
// Perform update
spinner.text = 'Updating files...';
spinner.text = "Updating files...";
// Backup modified files if forcing
if (modifiedFiles.length > 0 && options.force) {
for (const file of modifiedFiles) {
const filePath = path.join(installDir, file);
const backupPath = await fileManager.backupFile(filePath);
console.log(chalk.dim(` Backed up: ${file}${path.basename(backupPath)}`));
console.log(
chalk.dim(` Backed up: ${file}${path.basename(backupPath)}`)
);
}
}
// Re-run installation with same config
const config = {
installType: manifest.install_type,
agent: manifest.agent,
directory: installDir,
ide: manifest.ide_setup
ide: manifest.ide_setup,
};
await this.install(config);
} catch (error) {
spinner.fail('Update failed');
spinner.fail("Update failed");
throw error;
}
}
async listAgents() {
const agents = await configLoader.getAvailableAgents();
console.log(chalk.bold('\nAvailable BMAD Agents:\n'));
agents.forEach(agent => {
console.log(chalk.bold("\nAvailable BMAD Agents:\n"));
agents.forEach((agent) => {
console.log(chalk.cyan(` ${agent.id.padEnd(20)}`), agent.description);
});
console.log(chalk.dim('\nInstall with: npx bmad-method install --agent=<id>\n'));
console.log(
chalk.dim("\nInstall with: npx bmad-method install --agent=<id>\n")
);
}
async showStatus() {
const installDir = await this.findInstallation();
if (!installDir) {
console.log(chalk.yellow('No BMAD installation found in current directory tree'));
console.log(
chalk.yellow("No BMAD installation found in current directory tree")
);
return;
}
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
console.log(chalk.red('Invalid installation - manifest not found'));
console.log(chalk.red("Invalid installation - manifest not found"));
return;
}
console.log(chalk.bold('\nBMAD Installation Status:\n'));
console.log(chalk.bold("\nBMAD Installation Status:\n"));
console.log(` Directory: ${installDir}`);
console.log(` Version: ${manifest.version}`);
console.log(` Installed: ${new Date(manifest.installed_at).toLocaleDateString()}`);
console.log(
` Installed: ${new Date(
manifest.installed_at
).toLocaleDateString()}`
);
console.log(` Type: ${manifest.install_type}`);
if (manifest.agent) {
console.log(` Agent: ${manifest.agent}`);
}
if (manifest.ide_setup) {
console.log(` IDE Setup: ${manifest.ide_setup}`);
}
console.log(` Total Files: ${manifest.files.length}`);
// Check for modifications
const modifiedFiles = await fileManager.checkModifiedFiles(installDir, manifest);
const modifiedFiles = await fileManager.checkModifiedFiles(
installDir,
manifest
);
if (modifiedFiles.length > 0) {
console.log(chalk.yellow(` Modified Files: ${modifiedFiles.length}`));
}
console.log('');
console.log("");
}
async getAvailableAgents() {
@@ -242,28 +286,28 @@ class Installer {
async findInstallation() {
// Look for .bmad-core in current directory or parent directories
let currentDir = process.cwd();
while (currentDir !== path.dirname(currentDir)) {
const bmadDir = path.join(currentDir, '.bmad-core');
const manifestPath = path.join(bmadDir, '.bmad', 'install-manifest.yml');
const bmadDir = path.join(currentDir, ".bmad-core");
const manifestPath = path.join(bmadDir, "install-manifest.yml");
if (await fileManager.pathExists(manifestPath)) {
return bmadDir;
}
currentDir = path.dirname(currentDir);
}
// Also check if we're inside a .bmad-core directory
if (path.basename(process.cwd()) === '.bmad-core') {
const manifestPath = path.join(process.cwd(), '.bmad', 'install-manifest.yml');
if (path.basename(process.cwd()) === ".bmad-core") {
const manifestPath = path.join(process.cwd(), "install-manifest.yml");
if (await fileManager.pathExists(manifestPath)) {
return process.cwd();
}
}
return null;
}
}
module.exports = new Installer();
module.exports = new Installer();