Merge pull request #2 from RazvanBugoi/codex/extend-resolvepath-to-substitute-tokens

Improve installer path resolution
This commit is contained in:
Razvan Bugoi 2025-12-07 13:47:06 +00:00 committed by GitHub
commit e0bb3a2b5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 75 additions and 16 deletions

View File

@ -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);

View File

@ -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);
}