installer functional

This commit is contained in:
Brian Madison
2025-06-12 22:38:24 -05:00
parent bf09224e05
commit 8916211ba9
39 changed files with 1769 additions and 419 deletions

View File

@@ -0,0 +1,62 @@
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
class ConfigLoader {
constructor() {
this.configPath = path.join(__dirname, '..', 'config', 'install.config.yml');
this.config = null;
}
async load() {
if (this.config) return this.config;
try {
const configContent = await fs.readFile(this.configPath, 'utf8');
this.config = yaml.load(configContent);
return this.config;
} catch (error) {
throw new Error(`Failed to load configuration: ${error.message}`);
}
}
async getInstallationOptions() {
const config = await this.load();
return config['installation-options'] || {};
}
async getAvailableAgents() {
const config = await this.load();
return config['available-agents'] || [];
}
async getAgentDependencies(agentId) {
const config = await this.load();
const dependencies = config['agent-dependencies'] || {};
// Always include core files
const coreFiles = dependencies['core-files'] || [];
// Add agent-specific dependencies
const agentDeps = dependencies[agentId] || [];
return [...coreFiles, ...agentDeps];
}
async getIdeConfiguration(ide) {
const config = await this.load();
const ideConfigs = config['ide-configurations'] || {};
return ideConfigs[ide] || null;
}
getBmadCorePath() {
// Get the path to bmad-core relative to the installer (now under tools)
return path.join(__dirname, '..', '..', '..', 'bmad-core');
}
getAgentPath(agentId) {
return path.join(this.getBmadCorePath(), 'agents', `${agentId}.md`);
}
}
module.exports = new ConfigLoader();

View File

@@ -0,0 +1,154 @@
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';
}
async copyFile(source, destination) {
try {
await fs.ensureDir(path.dirname(destination));
await fs.copy(source, destination);
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy ${source}:`), error.message);
return false;
}
}
async copyDirectory(source, destination) {
try {
await fs.ensureDir(destination);
await fs.copy(source, destination);
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy directory ${source}:`), error.message);
return false;
}
}
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);
} catch (error) {
return null;
}
}
async createManifest(installDir, config, files) {
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
const manifest = {
version: require('../package.json').version,
installed_at: new Date().toISOString(),
install_type: config.installType,
agent: config.agent || null,
ide_setup: config.ide || null,
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
});
}
// 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);
try {
const content = await fs.readFile(manifestPath, 'utf8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
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';
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;
}
async ensureDirectory(dirPath) {
await fs.ensureDir(dirPath);
}
async pathExists(filePath) {
return fs.pathExists(filePath);
}
async readFile(filePath) {
return fs.readFile(filePath, 'utf8');
}
async writeFile(filePath, content) {
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content);
}
async removeDirectory(dirPath) {
await fs.remove(dirPath);
}
}
module.exports = new FileManager();

View File

@@ -0,0 +1,189 @@
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':
return this.setupCursor(installDir, selectedAgent);
case 'claude-code':
return this.setupClaudeCode(installDir, selectedAgent);
case 'windsurf':
return this.setupWindsurf(installDir, selectedAgent);
default:
console.log(chalk.yellow(`\nIDE ${ide} not yet supported`));
return false;
}
}
async setupCursor(installDir, selectedAgent) {
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`);
}
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---\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';
// 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 += '\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`;
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);
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`);
}
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}`));
return true;
}
async setupWindsurf(installDir, selectedAgent) {
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`);
}
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';
// 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 += '\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`;
await fileManager.writeFile(mdPath, mdContent);
console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
}
}
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');
}
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'
};
return agentTitles[agentId] || agentId;
}
}
module.exports = new IdeSetup();

View File

@@ -0,0 +1,269 @@
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();
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'));
return;
}
}
let files = [];
if (config.installType === 'full') {
// Full installation - copy entire bmad-core folder as a subdirectory
spinner.text = 'Copying complete bmad-core folder...';
const sourceDir = configLoader.getBmadCorePath();
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') {
// 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`);
await fileManager.copyFile(agentPath, destAgentPath);
files.push(`agents/${config.agent}.md`);
// Copy dependencies
const dependencies = await configLoader.getAgentDependencies(config.agent);
const sourceBase = configLoader.getBmadCorePath();
for (const dep of dependencies) {
spinner.text = `Copying dependency: ${dep}`;
if (dep.includes('*')) {
// Handle glob patterns
const copiedFiles = await fileManager.copyGlobPattern(
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/', ''));
if (await fileManager.copyFile(sourcePath, destPath)) {
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...';
await fileManager.createManifest(installDir, config, files);
spinner.succeed('Installation complete!');
// Show success message
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(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);
}
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');
throw error;
}
}
async update(options) {
const spinner = ora('Checking for updates...').start();
try {
// Find existing installation
const installDir = await this.findInstallation();
if (!installDir) {
spinner.fail('No BMAD installation found');
return;
}
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
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);
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}`));
if (!options.dryRun) {
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}`));
return;
}
// Perform update
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)}`));
}
}
// Re-run installation with same config
const config = {
installType: manifest.install_type,
agent: manifest.agent,
directory: installDir,
ide: manifest.ide_setup
};
await this.install(config);
} catch (error) {
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.cyan(` ${agent.id.padEnd(20)}`), agent.description);
});
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'));
return;
}
const manifest = await fileManager.readManifest(installDir);
if (!manifest) {
console.log(chalk.red('Invalid installation - manifest not found'));
return;
}
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(` 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);
if (modifiedFiles.length > 0) {
console.log(chalk.yellow(` Modified Files: ${modifiedFiles.length}`));
}
console.log('');
}
async getAvailableAgents() {
return configLoader.getAvailableAgents();
}
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');
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 (await fileManager.pathExists(manifestPath)) {
return process.cwd();
}
}
return null;
}
}
module.exports = new Installer();