Files
BMAD-METHOD/tools/upgraders/v3-to-v4-upgrader.js
manjaroblack 47b9d9f3e8 fix: spelling errors in documentation. (#297)
* fix: correct typos in documentation and agent files

Fix multiple instances of "assest" typo to "assets" in documentation
Correct "quetsions" typo to "questions" in repository structure sections
Add new words to cSpell dictionary in VS Code settings

* feat(trae): add support for trae ide integration

- Add trae guide documentation
- Update installer to support trae configuration
- Include trae in ide options and documentation references
- Fix typo in architect agent documentation

---------

Co-authored-by: Devin Stagner <devin@blackstag.family>
2025-07-05 21:08:26 -05:00

764 lines
23 KiB
JavaScript

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/",
"claude-code": "Commands created in .claude/commands/",
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;