const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const ora = require('ora'); const glob = require('glob'); const inquirer = require('inquirer'); const { promisify } = require('util'); const globAsync = promisify(glob); class V3ToV4Upgrader { constructor() { // Constructor remains empty } async upgrade(options = {}) { try { // Keep readline open throughout the process process.stdin.resume(); // 1. Welcome message console.log(chalk.bold('\nWelcome to BMAD-METHOD V3 to V4 Upgrade Tool\n')); console.log('This tool will help you upgrade your BMAD-METHOD V3 project to V4.\n'); console.log(chalk.cyan('What this tool does:')); console.log('- Creates a backup of your V3 files (.bmad-v3-backup/)'); console.log('- Installs the new V4 .bmad-core structure'); console.log('- Preserves your PRD, Architecture, and Stories in the new format\n'); console.log(chalk.yellow('What this tool does NOT do:')); console.log('- Modify your document content (use doc-migration-task after upgrade)'); console.log('- Touch any files outside bmad-agent/ and docs/\n'); // 2. Get project path const projectPath = await this.getProjectPath(options.projectPath); // 3. Validate V3 structure const validation = await this.validateV3Project(projectPath); if (!validation.isValid) { console.error(chalk.red('\nError: This doesn\'t appear to be a V3 project.')); console.error('Expected to find:'); console.error('- bmad-agent/ directory'); console.error('- docs/ directory\n'); console.error('Please check you\'re in the correct directory and try again.'); return; } // 4. Pre-flight check const analysis = await this.analyzeProject(projectPath); await this.showPreflightCheck(analysis, options); if (!options.dryRun) { const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: 'Continue with upgrade?', default: true }]); if (!confirm) { console.log('Upgrade cancelled.'); return; } } // 5. Create backup if (options.backup !== false && !options.dryRun) { await this.createBackup(projectPath); } // 6. Install V4 structure if (!options.dryRun) { await this.installV4Structure(projectPath); } // 7. Migrate documents if (!options.dryRun) { await this.migrateDocuments(projectPath, analysis); } // 8. Setup IDE if (!options.dryRun) { await this.setupIDE(projectPath); } // 9. Show completion report this.showCompletionReport(projectPath, analysis); process.exit(0); } catch (error) { console.error(chalk.red('\nUpgrade error:'), error.message); process.exit(1); } } async getProjectPath(providedPath) { if (providedPath) { return path.resolve(providedPath); } const { projectPath } = await inquirer.prompt([{ type: 'input', name: 'projectPath', message: 'Please enter the path to your V3 project:', default: process.cwd() }]); return path.resolve(projectPath); } async validateV3Project(projectPath) { const spinner = ora('Validating project structure...').start(); try { const bmadAgentPath = path.join(projectPath, 'bmad-agent'); const docsPath = path.join(projectPath, 'docs'); const hasBmadAgent = await this.pathExists(bmadAgentPath); const hasDocs = await this.pathExists(docsPath); if (hasBmadAgent) { spinner.text = '✓ Found bmad-agent/ directory'; console.log(chalk.green('\n✓ Found bmad-agent/ directory')); } if (hasDocs) { console.log(chalk.green('✓ Found docs/ directory')); } const isValid = hasBmadAgent && hasDocs; if (isValid) { spinner.succeed('This appears to be a valid V3 project'); } else { spinner.fail('Invalid V3 project structure'); } return { isValid, hasBmadAgent, hasDocs }; } catch (error) { spinner.fail('Validation failed'); throw error; } } async analyzeProject(projectPath) { const docsPath = path.join(projectPath, 'docs'); const bmadAgentPath = path.join(projectPath, 'bmad-agent'); // Find PRD const prdCandidates = ['prd.md', 'PRD.md', 'product-requirements.md']; let prdFile = null; for (const candidate of prdCandidates) { const candidatePath = path.join(docsPath, candidate); if (await this.pathExists(candidatePath)) { prdFile = candidate; break; } } // Find Architecture const archCandidates = ['architecture.md', 'Architecture.md', 'technical-architecture.md']; let archFile = null; for (const candidate of archCandidates) { const candidatePath = path.join(docsPath, candidate); if (await this.pathExists(candidatePath)) { archFile = candidate; break; } } // Find Front-end Architecture (V3 specific) const frontEndCandidates = ['front-end-architecture.md', 'frontend-architecture.md', 'ui-architecture.md']; let frontEndArchFile = null; for (const candidate of frontEndCandidates) { const candidatePath = path.join(docsPath, candidate); if (await this.pathExists(candidatePath)) { frontEndArchFile = candidate; break; } } // Find epic files const epicFiles = await globAsync('epic*.md', { cwd: docsPath }); // Find story files const storiesPath = path.join(docsPath, 'stories'); let storyFiles = []; if (await this.pathExists(storiesPath)) { storyFiles = await globAsync('*.md', { cwd: storiesPath }); } // Count custom files in bmad-agent const bmadAgentFiles = await globAsync('**/*.md', { cwd: bmadAgentPath, ignore: ['node_modules/**'] }); return { prdFile, archFile, frontEndArchFile, epicFiles, storyFiles, customFileCount: bmadAgentFiles.length }; } async showPreflightCheck(analysis, options) { console.log(chalk.bold('\nProject Analysis:')); console.log(`- PRD found: ${analysis.prdFile ? `docs/${analysis.prdFile}` : chalk.yellow('Not found')}`); console.log(`- Architecture found: ${analysis.archFile ? `docs/${analysis.archFile}` : chalk.yellow('Not found')}`); if (analysis.frontEndArchFile) { console.log(`- Front-end Architecture found: docs/${analysis.frontEndArchFile}`); } console.log(`- Epic files found: ${analysis.epicFiles.length} files (epic*.md)`); console.log(`- Stories found: ${analysis.storyFiles.length} files in docs/stories/`); console.log(`- Custom files in bmad-agent/: ${analysis.customFileCount}`); if (!options.dryRun) { console.log('\nThe following will be backed up to .bmad-v3-backup/:'); console.log('- bmad-agent/ (entire directory)'); console.log('- docs/ (entire directory)'); if (analysis.epicFiles.length > 0) { console.log(chalk.green('\nNote: Epic files found! They will be placed in docs/prd/ with an index.md file.')); console.log(chalk.green('Since epic files exist, you won\'t need to shard the PRD after upgrade.')); } } } async createBackup(projectPath) { const spinner = ora('Creating backup...').start(); try { const backupPath = path.join(projectPath, '.bmad-v3-backup'); // Check if backup already exists if (await this.pathExists(backupPath)) { spinner.fail('Backup directory already exists'); console.error(chalk.red('\nError: Backup directory .bmad-v3-backup/ already exists.')); console.error('\nThis might mean an upgrade was already attempted.'); console.error('Please remove or rename the existing backup and try again.'); throw new Error('Backup already exists'); } // Create backup directory await fs.mkdir(backupPath, { recursive: true }); spinner.text = '✓ Created .bmad-v3-backup/'; console.log(chalk.green('\n✓ Created .bmad-v3-backup/')); // Move bmad-agent const bmadAgentSrc = path.join(projectPath, 'bmad-agent'); const bmadAgentDest = path.join(backupPath, 'bmad-agent'); await fs.rename(bmadAgentSrc, bmadAgentDest); console.log(chalk.green('✓ Moved bmad-agent/ to backup')); // Move docs const docsSrc = path.join(projectPath, 'docs'); const docsDest = path.join(backupPath, 'docs'); await fs.rename(docsSrc, docsDest); console.log(chalk.green('✓ Moved docs/ to backup')); spinner.succeed('Backup created successfully'); } catch (error) { spinner.fail('Backup failed'); throw error; } } async installV4Structure(projectPath) { const spinner = ora('Installing V4 structure...').start(); try { // Get the source .bmad-core directory const sourcePath = path.join(__dirname, '..', '..', '.bmad-core'); const destPath = path.join(projectPath, '.bmad-core'); // Copy .bmad-core await this.copyDirectory(sourcePath, destPath); spinner.text = '✓ Copied fresh .bmad-core/ directory from V4'; console.log(chalk.green('\n✓ Copied fresh .bmad-core/ directory from V4')); // Create docs directory const docsPath = path.join(projectPath, 'docs'); await fs.mkdir(docsPath, { recursive: true }); console.log(chalk.green('✓ Created new docs/ directory')); console.log(chalk.yellow('\nNote: Your V3 bmad-agent content has been backed up and NOT migrated.')); console.log(chalk.yellow('The new V4 agents are completely different and look for different file structures.')); spinner.succeed('V4 structure installed successfully'); } catch (error) { spinner.fail('V4 installation failed'); throw error; } } async migrateDocuments(projectPath, analysis) { const spinner = ora('Migrating your project documents...').start(); try { const backupDocsPath = path.join(projectPath, '.bmad-v3-backup', 'docs'); const newDocsPath = path.join(projectPath, 'docs'); let copiedCount = 0; // Copy PRD if (analysis.prdFile) { const src = path.join(backupDocsPath, analysis.prdFile); const dest = path.join(newDocsPath, analysis.prdFile); await fs.copyFile(src, dest); console.log(chalk.green(`\n✓ Copied PRD to docs/${analysis.prdFile}`)); copiedCount++; } // Copy Architecture if (analysis.archFile) { const src = path.join(backupDocsPath, analysis.archFile); const dest = path.join(newDocsPath, analysis.archFile); await fs.copyFile(src, dest); console.log(chalk.green(`✓ Copied Architecture to docs/${analysis.archFile}`)); copiedCount++; } // Copy Front-end Architecture if exists if (analysis.frontEndArchFile) { const src = path.join(backupDocsPath, analysis.frontEndArchFile); const dest = path.join(newDocsPath, analysis.frontEndArchFile); await fs.copyFile(src, dest); console.log(chalk.green(`✓ Copied Front-end Architecture to docs/${analysis.frontEndArchFile}`)); console.log(chalk.yellow('Note: V4 uses a single full-stack-architecture.md - use doc-migration-task to merge')); copiedCount++; } // Copy stories if (analysis.storyFiles.length > 0) { const storiesDir = path.join(newDocsPath, 'stories'); await fs.mkdir(storiesDir, { recursive: true }); for (const storyFile of analysis.storyFiles) { const src = path.join(backupDocsPath, 'stories', storyFile); const dest = path.join(storiesDir, storyFile); await fs.copyFile(src, dest); } console.log(chalk.green(`✓ Copied ${analysis.storyFiles.length} story files to docs/stories/`)); copiedCount += analysis.storyFiles.length; } // Copy epic files to prd subfolder if (analysis.epicFiles.length > 0) { const prdDir = path.join(newDocsPath, 'prd'); await fs.mkdir(prdDir, { recursive: true }); for (const epicFile of analysis.epicFiles) { const src = path.join(backupDocsPath, epicFile); const dest = path.join(prdDir, epicFile); await fs.copyFile(src, dest); } console.log(chalk.green(`✓ Found and copied ${analysis.epicFiles.length} epic files to docs/prd/`)); // Create index.md for the prd folder await this.createPrdIndex(projectPath, analysis); console.log(chalk.green('✓ Created index.md in docs/prd/')); console.log(chalk.green('\nNote: Epic files detected! These are compatible with V4 and have been copied.')); console.log(chalk.green('You won\'t need to shard the PRD since epics already exist.')); copiedCount += analysis.epicFiles.length; } spinner.succeed(`Migrated ${copiedCount} documents successfully`); } catch (error) { spinner.fail('Document migration failed'); throw error; } } async setupIDE(projectPath) { const { ide } = await inquirer.prompt([{ type: 'list', name: 'ide', message: 'Which IDE are you using?', choices: [ { name: 'Cursor', value: 'cursor' }, { name: 'Claude Code', value: 'claude-code' }, { name: 'Windsurf', value: 'windsurf' }, { name: 'VS Code', value: 'skip' }, { name: 'Other/Skip', value: 'skip' } ] }]); const selectedIde = ide === 'skip' ? null : ide; if (selectedIde) { const ideSetup = require('../installer/lib/ide-setup'); const spinner = ora('Setting up IDE rules for all agents...').start(); try { await ideSetup.setup(selectedIde, projectPath); spinner.succeed('IDE setup complete!'); const ideMessages = { 'cursor': 'Rules created in .cursor/rules/', 'claude-code': 'Commands created in .claude/commands/', 'windsurf': 'Rules created in .windsurf/rules/' }; console.log(chalk.green(`- ${ideMessages[selectedIde]}`)); } catch (error) { spinner.fail('IDE setup failed'); console.error(chalk.yellow('IDE setup failed, but upgrade is complete.')); } } } showCompletionReport(projectPath, analysis) { console.log(chalk.bold.green('\n✓ Upgrade Complete!\n')); console.log(chalk.bold('Summary:')); console.log(`- V3 files backed up to: .bmad-v3-backup/`); console.log(`- V4 structure installed: .bmad-core/ (fresh from V4)`); const totalDocs = (analysis.prdFile ? 1 : 0) + (analysis.archFile ? 1 : 0) + (analysis.frontEndArchFile ? 1 : 0) + analysis.storyFiles.length; console.log(`- Documents migrated: ${totalDocs} files${analysis.epicFiles.length > 0 ? ` + ${analysis.epicFiles.length} epics` : ''}`); console.log(chalk.bold('\nImportant Changes:')); console.log('- The V4 agents (sm, dev, etc.) expect different file structures than V3'); console.log('- Your V3 bmad-agent content was NOT migrated (it\'s incompatible)'); if (analysis.epicFiles.length > 0) { console.log('- Epic files were found and copied - no PRD sharding needed!'); } if (analysis.frontEndArchFile) { console.log('- Front-end architecture found - V4 uses full-stack-architecture.md, migration needed'); } console.log(chalk.bold('\nNext Steps:')); console.log('1. Review your documents in the new docs/ folder'); console.log('2. Use @bmad-master agent to run the doc-migration-task to align your documents with V4 templates'); if (analysis.epicFiles.length === 0) { console.log('3. Use @bmad-master agent to shard the PRD to create epic files'); } console.log(chalk.dim('\nYour V3 backup is preserved in .bmad-v3-backup/ and can be restored if needed.')); } async pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } async copyDirectory(src, dest) { await fs.mkdir(dest, { recursive: true }); const entries = await fs.readdir(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { await this.copyDirectory(srcPath, destPath); } else { await fs.copyFile(srcPath, destPath); } } } async createPrdIndex(projectPath, analysis) { const prdIndexPath = path.join(projectPath, 'docs', 'prd', 'index.md'); const prdPath = path.join(projectPath, 'docs', analysis.prdFile || 'prd.md'); let indexContent = '# Product Requirements Document\n\n'; // Try to read the PRD to get the title and intro content if (analysis.prdFile && await this.pathExists(prdPath)) { try { const prdContent = await fs.readFile(prdPath, 'utf8'); const lines = prdContent.split('\n'); // Find the first heading const titleMatch = lines.find(line => line.startsWith('# ')); if (titleMatch) { indexContent = titleMatch + '\n\n'; } // Get any content before the first ## section let introContent = ''; let foundFirstSection = false; for (const line of lines) { if (line.startsWith('## ')) { foundFirstSection = true; break; } if (!line.startsWith('# ')) { introContent += line + '\n'; } } if (introContent.trim()) { indexContent += introContent.trim() + '\n\n'; } } catch (error) { // If we can't read the PRD, just use default content } } // Add sections list indexContent += '## Sections\n\n'; // Sort epic files for consistent ordering const sortedEpics = [...analysis.epicFiles].sort(); for (const epicFile of sortedEpics) { // Extract epic name from filename const epicName = epicFile .replace(/\.md$/, '') .replace(/^epic-?/i, '') .replace(/-/g, ' ') .replace(/^\d+\s*/, '') // Remove leading numbers .trim(); const displayName = epicName.charAt(0).toUpperCase() + epicName.slice(1); indexContent += `- [${displayName || epicFile.replace('.md', '')}](./${epicFile})\n`; } await fs.writeFile(prdIndexPath, indexContent); } } module.exports = V3ToV4Upgrader;