const fs = require("fs").promises; const path = require("path"); const { glob } = require("glob"); // Dynamic imports for ES modules let chalk, ora, inquirer; // Initialize ES modules async function initializeModules() { chalk = (await import("chalk")).default; ora = (await import("ora")).default; inquirer = (await import("inquirer")).default; } class V3ToV4Upgrader { constructor() { // Constructor remains empty } async upgrade(options = {}) { try { // Initialize ES modules await initializeModules(); // 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, options.ides); } // 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 UX/UI spec const uxSpecCandidates = [ "ux-ui-spec.md", "ux-ui-specification.md", "ui-spec.md", "ux-spec.md", ]; let uxSpecFile = null; for (const candidate of uxSpecCandidates) { const candidatePath = path.join(docsPath, candidate); if (await this.pathExists(candidatePath)) { uxSpecFile = candidate; break; } } // Find v0 prompt or UX prompt const uxPromptCandidates = [ "v0-prompt.md", "ux-prompt.md", "ui-prompt.md", "design-prompt.md", ]; let uxPromptFile = null; for (const candidate of uxPromptCandidates) { const candidatePath = path.join(docsPath, candidate); if (await this.pathExists(candidatePath)) { uxPromptFile = candidate; break; } } // Find epic files const epicFiles = await glob("epic*.md", { cwd: docsPath }); // Find story files const storiesPath = path.join(docsPath, "stories"); let storyFiles = []; if (await this.pathExists(storiesPath)) { storyFiles = await glob("*.md", { cwd: storiesPath }); } // Count custom files in bmad-agent const bmadAgentFiles = await glob("**/*.md", { cwd: bmadAgentPath, ignore: ["node_modules/**"], }); return { prdFile, archFile, frontEndArchFile, uxSpecFile, uxPromptFile, 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( `- UX/UI Spec found: ${ analysis.uxSpecFile ? `docs/${analysis.uxSpecFile}` : chalk.yellow("Not found") }` ); console.log( `- UX/Design Prompt found: ${ analysis.uxPromptFile ? `docs/${analysis.uxPromptFile}` : chalk.yellow("Not found") }` ); 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 (without dot prefix) 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")); // Create install manifest for future updates await this.createInstallManifest(projectPath); console.log(chalk.green("✓ Created install manifest")); 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 UX/UI Spec if exists if (analysis.uxSpecFile) { const src = path.join(backupDocsPath, analysis.uxSpecFile); const dest = path.join(newDocsPath, analysis.uxSpecFile); await fs.copyFile(src, dest); console.log( chalk.green(`✓ Copied UX/UI Spec to docs/${analysis.uxSpecFile}`) ); copiedCount++; } // Copy UX/Design Prompt if exists if (analysis.uxPromptFile) { const src = path.join(backupDocsPath, analysis.uxPromptFile); const dest = path.join(newDocsPath, analysis.uxPromptFile); await fs.copyFile(src, dest); console.log( chalk.green( `✓ Copied UX/Design Prompt to docs/${analysis.uxPromptFile}` ) ); 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, selectedIdes) { // Use the IDE selections passed from the installer if (!selectedIdes || selectedIdes.length === 0) { console.log(chalk.dim("No IDE setup requested - skipping")); return; } const ideSetup = require("../installer/lib/ide-setup"); const spinner = ora("Setting up IDE rules for all agents...").start(); try { const ideMessages = { cursor: "Rules created in .cursor/rules/bmad/", "claude-code": "Commands created in .claude/commands/BMad/", windsurf: "Rules created in .windsurf/rules/", trae: "Rules created in.trae/rules/", roo: "Custom modes created in .roomodes", cline: "Rules created in .clinerules/", }; // Setup each selected IDE for (const ide of selectedIdes) { spinner.text = `Setting up ${ide}...`; await ideSetup.setup(ide, projectPath); console.log(chalk.green(`\n✓ ${ideMessages[ide]}`)); } spinner.succeed(`IDE setup complete for ${selectedIdes.length} IDE(s)!`); } 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.uxSpecFile ? 1 : 0) + (analysis.uxPromptFile ? 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" ); } if (analysis.uxSpecFile || analysis.uxPromptFile) { console.log( "- UX/UI design files found and copied - ready for use with V4" ); } 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); } async createInstallManifest(projectPath) { const fileManager = require("../installer/lib/file-manager"); const { glob } = require("glob"); // Get all files in .bmad-core for the manifest const bmadCorePath = path.join(projectPath, ".bmad-core"); const files = await glob("**/*", { cwd: bmadCorePath, nodir: true, ignore: ["**/.git/**", "**/node_modules/**"], }); // Prepend .bmad-core/ to file paths for manifest const manifestFiles = files.map((file) => path.join(".bmad-core", file)); const config = { installType: "full", agent: null, ide: null, // Will be set if IDE setup is done later }; await fileManager.createManifest(projectPath, config, manifestFiles); } } module.exports = V3ToV4Upgrader;