Files
BMAD-METHOD/tools/cli/lib/replace-project-root.js
2025-09-28 23:17:07 -05:00

240 lines
8.7 KiB
JavaScript

/**
* Utility function to replace {project-root} placeholders with actual installation target
* Used during BMAD installation to set correct paths in agent and task files
*/
const fs = require('node:fs');
const path = require('node:path');
/**
* Replace {project-root} and {output_folder}/ placeholders in a single file
* @param {string} filePath - Path to the file to process
* @param {string} projectRoot - The actual project root path to substitute (must include trailing slash)
* @param {string} docOut - The document output path (with leading slash)
* @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
* @returns {boolean} - True if replacements were made, false otherwise
*/
function replacePlaceholdersInFile(filePath, projectRoot, docOut = '/docs', removeCompletely = false) {
try {
let content = fs.readFileSync(filePath, 'utf8');
const originalContent = content;
if (removeCompletely) {
// Remove placeholders entirely (for bundling)
content = content.replaceAll('{project-root}', '');
content = content.replaceAll('{output_folder}/', '');
} else {
// Handle the combined pattern first to avoid double slashes
if (projectRoot && docOut) {
// Replace {project-root}{output_folder}/ combinations first
// Remove leading slash from docOut since projectRoot has trailing slash
// Add trailing slash to docOut
const docOutNoLeadingSlash = docOut.replace(/^\//, '');
const docOutWithTrailingSlash = docOutNoLeadingSlash.endsWith('/') ? docOutNoLeadingSlash : docOutNoLeadingSlash + '/';
content = content.replaceAll('{project-root}{output_folder}/', projectRoot + docOutWithTrailingSlash);
}
// Then replace remaining individual placeholders
if (projectRoot) {
content = content.replaceAll('{project-root}', projectRoot);
}
if (docOut) {
// For standalone {output_folder}/, keep the leading slash and add trailing slash
const docOutWithTrailingSlash = docOut.endsWith('/') ? docOut : docOut + '/';
content = content.replaceAll('{output_folder}/', docOutWithTrailingSlash);
}
}
if (content !== originalContent) {
fs.writeFileSync(filePath, content, 'utf8');
return true;
}
return false;
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message);
return false;
}
}
/**
* Legacy function name for backward compatibility
*/
function replaceProjectRootInFile(filePath, projectRoot, removeCompletely = false) {
return replacePlaceholdersInFile(filePath, projectRoot, '/docs', removeCompletely);
}
/**
* Recursively replace {project-root} and {output_folder}/ in all files in a directory
* @param {string} dirPath - Directory to process
* @param {string} projectRoot - The actual project root path to substitute (or null to remove)
* @param {string} docOut - The document output path (with leading slash)
* @param {Array<string>} extensions - File extensions to process (default: ['.md', '.xml', '.yaml'])
* @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
* @param {boolean} verbose - If true, show detailed output for each file
* @returns {Object} - Stats object with counts of files processed and modified
*/
function replacePlaceholdersInDirectory(
dirPath,
projectRoot,
docOut = '/docs',
extensions = ['.md', '.xml', '.yaml'],
removeCompletely = false,
verbose = false,
) {
const stats = {
processed: 0,
modified: 0,
errors: 0,
};
function processDirectory(currentPath) {
try {
const items = fs.readdirSync(currentPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(currentPath, item.name);
if (item.isDirectory()) {
// Skip node_modules and .git directories
if (item.name !== 'node_modules' && item.name !== '.git') {
processDirectory(fullPath);
}
} else if (item.isFile()) {
// Check if file has one of the target extensions
const ext = path.extname(item.name).toLowerCase();
if (extensions.includes(ext)) {
stats.processed++;
if (replacePlaceholdersInFile(fullPath, projectRoot, docOut, removeCompletely)) {
stats.modified++;
if (verbose) {
console.log(`✓ Updated: ${fullPath}`);
}
}
}
}
}
} catch (error) {
console.error(`Error processing directory ${currentPath}:`, error.message);
stats.errors++;
}
}
processDirectory(dirPath);
return stats;
}
/**
* Legacy function for backward compatibility
*/
function replaceProjectRootInDirectory(dirPath, projectRoot, extensions = ['.md', '.xml'], removeCompletely = false) {
return replacePlaceholdersInDirectory(dirPath, projectRoot, '/docs', extensions, removeCompletely);
}
/**
* Replace placeholders in a list of specific files
* @param {Array<string>} filePaths - Array of file paths to process
* @param {string} projectRoot - The actual project root path to substitute (or null to remove)
* @param {string} docOut - The document output path (with leading slash)
* @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing
* @returns {Object} - Stats object with counts of files processed and modified
*/
function replacePlaceholdersInFiles(filePaths, projectRoot, docOut = '/docs', removeCompletely = false) {
const stats = {
processed: 0,
modified: 0,
errors: 0,
};
for (const filePath of filePaths) {
stats.processed++;
try {
if (replacePlaceholdersInFile(filePath, projectRoot, docOut, removeCompletely)) {
stats.modified++;
console.log(`✓ Updated: ${filePath}`);
}
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message);
stats.errors++;
}
}
return stats;
}
/**
* Legacy function for backward compatibility
*/
function replaceProjectRootInFiles(filePaths, projectRoot, removeCompletely = false) {
return replacePlaceholdersInFiles(filePaths, projectRoot, '/docs', removeCompletely);
}
/**
* Main installation helper - replaces {project-root} and {output_folder}/ during BMAD installation
* @param {string} installPath - Path where BMAD is being installed
* @param {string} targetProjectRoot - The project root to set in the files (slash will be added)
* @param {string} docsOutputPath - The documentation output path (relative to project root)
* @param {boolean} verbose - If true, show detailed output
* @returns {Object} - Installation stats
*/
function processInstallation(installPath, targetProjectRoot, docsOutputPath = 'docs', verbose = false) {
// Ensure project root has trailing slash since usage is like {project-root}/bmad
const projectRootWithSlash = targetProjectRoot.endsWith('/') ? targetProjectRoot : targetProjectRoot + '/';
// Ensure docs path has leading slash (for internal use) but will add trailing slash during replacement
const normalizedDocsPath = docsOutputPath.replaceAll(/^\/+|\/+$/g, '');
const docOutPath = normalizedDocsPath ? `/${normalizedDocsPath}` : '/docs';
if (verbose) {
console.log(`\nReplacing {project-root} with: ${projectRootWithSlash}`);
console.log(`Replacing {output_folder}/ with: ${docOutPath}/`);
console.log(`Processing files in: ${installPath}\n`);
}
const stats = replacePlaceholdersInDirectory(installPath, projectRootWithSlash, docOutPath, ['.md', '.xml', '.yaml'], false, verbose);
if (verbose) {
console.log('\n--- Installation Processing Complete ---');
}
console.log(`Files processed: ${stats.processed}`);
console.log(`Files modified: ${stats.modified}`);
if (stats.errors > 0) {
console.log(`Errors encountered: ${stats.errors}`);
}
return stats;
}
/**
* Bundle helper - removes {project-root}/ references for web bundling
* @param {string} bundlePath - Path where files are being bundled
* @returns {Object} - Bundle stats
*/
function processBundleRemoval(bundlePath) {
console.log(`\nRemoving {project-root}/ references for bundling`);
console.log(`Processing files in: ${bundlePath}\n`);
const stats = replaceProjectRootInDirectory(bundlePath, null, ['.md', '.xml'], true);
console.log('\n--- Bundle Processing Complete ---');
console.log(`Files processed: ${stats.processed}`);
console.log(`Files modified: ${stats.modified}`);
if (stats.errors > 0) {
console.log(`Errors encountered: ${stats.errors}`);
}
return stats;
}
module.exports = {
replacePlaceholdersInFile,
replacePlaceholdersInDirectory,
replacePlaceholdersInFiles,
replaceProjectRootInFile,
replaceProjectRootInDirectory,
replaceProjectRootInFiles,
processInstallation,
processBundleRemoval,
};