From 401e27f37a397f57f37fb52635e78c1b22d5e1d9 Mon Sep 17 00:00:00 2001 From: Razvan Bugoi <46108577+RazvanBugoi@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:45:12 +0000 Subject: [PATCH] Improve installer path resolution --- test/test-installation-components.js | 21 +++++---- tools/cli/lib/agent/installer.js | 70 +++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 464ca613..9bf251e1 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -15,6 +15,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); +const { resolvePath } = require('../tools/cli/lib/agent/installer'); // ANSI colors const colors = { @@ -138,18 +139,20 @@ async function runTests() { console.log(`${colors.yellow}Test Suite 3: Path Variable Resolution${colors.reset}\n`); try { - const builder = new YamlXmlBuilder(); + const context = { + projectRoot: '/home/project', + bmadFolder: 'bmad-custom', + installed_path: '/home/project/bmad-custom/workflows/demo', + config_source: '/home/project/bmad-custom/config.yaml', + }; - // Test path resolution logic (if exposed) - // This would test {project-root}, {installed_path}, {config_source} resolution - - const testPath = '{project-root}/bmad/bmm/config.yaml'; - const expectedPattern = /\/bmad\/bmm\/config\.yaml$/; + const testPath = '{project-root}/{bmad_folder}/workflows/{installed_path}/config?{config_source}'; + const resolved = resolvePath(testPath, context); assert( - true, // Placeholder - would test actual resolution - 'Path variable resolution pattern matches expected format', - 'Note: This test validates path resolution logic exists', + resolved === '/home/project/bmad-custom/workflows//home/project/bmad-custom/workflows/demo/config?/home/project/bmad-custom/config.yaml', + 'Path variable resolution replaces all documented tokens', + `Resolved: ${resolved}`, ); } catch (error) { assert(false, 'Path resolution works', error.message); diff --git a/tools/cli/lib/agent/installer.js b/tools/cli/lib/agent/installer.js index d79abd23..25ed9af8 100644 --- a/tools/cli/lib/agent/installer.js +++ b/tools/cli/lib/agent/installer.js @@ -36,13 +36,46 @@ function findBmadConfig(startPath = process.cwd()) { } /** - * Resolve path variables like {project-root} and {bmad-folder} + * Resolve path variables like {project-root}, {bmad-folder}, {installed_path} + * and {config_source} using provided context data * @param {string} pathStr - Path with variables - * @param {Object} context - Contains projectRoot, bmadFolder + * @param {Object} context - Contains projectRoot, bmadFolder, installed_path, config_source * @returns {string} Resolved path */ -function resolvePath(pathStr, context) { - return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder); +function resolvePath(pathStr, context = {}) { + if (!pathStr || typeof pathStr !== 'string') return pathStr; + + const normalizedContext = { + projectRoot: context.projectRoot, + bmadFolder: context.bmadFolder, + installed_path: context.installed_path || context.installedPath, + config_source: context.config_source || context.configSource, + }; + + const replacements = { + '{project-root}': normalizedContext.projectRoot, + '{project_root}': normalizedContext.projectRoot, + '{bmad-folder}': normalizedContext.bmadFolder, + '{bmad_folder}': normalizedContext.bmadFolder, + '{installed_path}': normalizedContext.installed_path, + '{config_source}': normalizedContext.config_source, + }; + + // Also map any additional context keys directly to {key} tokens + for (const [key, value] of Object.entries(context)) { + if (value === undefined || value === null) continue; + + const snakeKey = key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`); + const dashKey = snakeKey.replaceAll('_', '-'); + + replacements[`{${snakeKey}}`] ??= value; + replacements[`{${dashKey}}`] ??= value; + } + + return Object.entries(replacements).reduce((result, [token, value]) => { + if (value === undefined || value === null) return result; + return result.replaceAll(token, value); + }, pathStr); } /** @@ -281,7 +314,13 @@ function installAgent(agentInfo, answers, targetPath, options = {}) { } // Find and copy sidecar folder - const sidecarFiles = copyAgentSidecarFiles(agentInfo.path, agentSidecarDir, agentInfo.yamlFile); + const installContext = { + projectRoot: options.projectRoot || process.cwd(), + bmadFolder: options.bmadFolder || '.bmad', + ...(options.installContext || {}), + }; + + const sidecarFiles = copyAgentSidecarFiles(agentInfo.path, agentSidecarDir, agentInfo.yamlFile, installContext); result.sidecarCopied = true; result.sidecarFiles = sidecarFiles; result.sidecarDir = agentSidecarDir; @@ -335,10 +374,12 @@ function copySidecarFiles(sourceDir, targetDir, excludeYaml) { * @param {string} excludeYaml - The .agent.yaml file to exclude * @returns {Array} List of copied files */ -function copyAgentSidecarFiles(sourceDir, targetSidecarDir, excludeYaml) { +function copyAgentSidecarFiles(sourceDir, targetSidecarDir, excludeYaml, pathContext = {}) { const copied = []; const preserved = []; + const textExtensions = ['.md', '.mdx', '.yaml', '.yml', '.json', '.txt', '.xml', '.csv']; + // Find folders with "sidecar" in the name const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); @@ -368,7 +409,22 @@ function copyAgentSidecarFiles(sourceDir, targetSidecarDir, excludeYaml) { // File exists - preserve it preserved.push(destPath); } else { - // File doesn't exist - copy it + // File doesn't exist - copy it with placeholder resolution when applicable + const ext = path.extname(srcPath).toLowerCase(); + const shouldResolve = Object.keys(pathContext).length > 0 && textExtensions.includes(ext); + + if (shouldResolve) { + try { + const content = fs.readFileSync(srcPath, 'utf8'); + const resolved = resolvePath(content, pathContext); + fs.writeFileSync(destPath, resolved, 'utf8'); + copied.push(destPath); + continue; + } catch { + // Fall back to raw copy below if resolution fails + } + } + fs.copyFileSync(srcPath, destPath); copied.push(destPath); }