/** * 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} 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} 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, };