fix(installer): port three main-branch patches and fix custom module routing
Port bug fixes that landed on main after this branch forked: 1. Remove dead .agent.yaml/.xml fallback logic from skill-manifest.js and simplify scanInstalledModules to hasAgents || hasSkills 2. Fix config paths (issue 55): add resolveConfigValue, cleanPromptValue, normalizeExistingValueForPrompt to prevent template duplication on re-prompt 3. Fix empty agent-manifest.csv: replace collectAgents/getAgentsFromDir with getAgentsFromDirRecursive that walks full module trees, fix 9 BMM manifests type: skill → type: agent, add dead-code TODO to bmad-artifacts.js 4. Fix custom module install crash: discoverPaths was receiving the clean Config (which strips customContent) instead of customConfig
This commit is contained in:
parent
9c6534259d
commit
7877513513
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-analyst
|
name: bmad-agent-analyst
|
||||||
displayName: Mary
|
displayName: Mary
|
||||||
title: Business Analyst
|
title: Business Analyst
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-tech-writer
|
name: bmad-agent-tech-writer
|
||||||
displayName: Paige
|
displayName: Paige
|
||||||
title: Technical Writer
|
title: Technical Writer
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-pm
|
name: bmad-agent-pm
|
||||||
displayName: John
|
displayName: John
|
||||||
title: Product Manager
|
title: Product Manager
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-ux-designer
|
name: bmad-agent-ux-designer
|
||||||
displayName: Sally
|
displayName: Sally
|
||||||
title: UX Designer
|
title: UX Designer
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-architect
|
name: bmad-agent-architect
|
||||||
displayName: Winston
|
displayName: Winston
|
||||||
title: Architect
|
title: Architect
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-dev
|
name: bmad-agent-dev
|
||||||
displayName: Amelia
|
displayName: Amelia
|
||||||
title: Developer Agent
|
title: Developer Agent
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-qa
|
name: bmad-agent-qa
|
||||||
displayName: Quinn
|
displayName: Quinn
|
||||||
title: QA Engineer
|
title: QA Engineer
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-quick-flow-solo-dev
|
name: bmad-agent-quick-flow-solo-dev
|
||||||
displayName: Barry
|
displayName: Barry
|
||||||
title: Quick Flow Solo Dev
|
title: Quick Flow Solo Dev
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
type: skill
|
type: agent
|
||||||
name: bmad-agent-sm
|
name: bmad-agent-sm
|
||||||
displayName: Bob
|
displayName: Bob
|
||||||
title: Scrum Master
|
title: Scrum Master
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class Installer {
|
||||||
const officialModules = await OfficialModules.build(config, paths);
|
const officialModules = await OfficialModules.build(config, paths);
|
||||||
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
||||||
|
|
||||||
await this.customModules.discoverPaths(config, paths);
|
await this.customModules.discoverPaths(customConfig, paths);
|
||||||
|
|
||||||
if (existingInstall.installed) {
|
if (existingInstall.installed) {
|
||||||
await this._removeDeselectedModules(existingInstall, config, paths);
|
await this._removeDeselectedModules(existingInstall, config, paths);
|
||||||
|
|
|
||||||
|
|
@ -268,64 +268,66 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect all agents from core and selected modules
|
* Collect all agents from selected modules by walking their directory trees.
|
||||||
* Scans the INSTALLED bmad directory, not the source
|
|
||||||
*/
|
*/
|
||||||
async collectAgents(selectedModules) {
|
async collectAgents(selectedModules) {
|
||||||
this.agents = [];
|
this.agents = [];
|
||||||
|
const debug = process.env.BMAD_DEBUG_MANIFEST === 'true';
|
||||||
|
|
||||||
// Use updatedModules which already includes deduplicated 'core' + selectedModules
|
// Walk each module's full directory tree looking for type:agent manifests
|
||||||
for (const moduleName of this.updatedModules) {
|
for (const moduleName of this.updatedModules) {
|
||||||
const agentsPath = path.join(this.bmadDir, moduleName, 'agents');
|
const modulePath = path.join(this.bmadDir, moduleName);
|
||||||
|
if (!(await fs.pathExists(modulePath))) continue;
|
||||||
|
|
||||||
if (await fs.pathExists(agentsPath)) {
|
const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug);
|
||||||
const moduleAgents = await this.getAgentsFromDir(agentsPath, moduleName);
|
|
||||||
this.agents.push(...moduleAgents);
|
this.agents.push(...moduleAgents);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get standalone agents from bmad/agents/ directory
|
// Get standalone agents from bmad/agents/ directory
|
||||||
const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
|
const standaloneAgentsDir = path.join(this.bmadDir, 'agents');
|
||||||
if (await fs.pathExists(standaloneAgentsDir)) {
|
if (await fs.pathExists(standaloneAgentsDir)) {
|
||||||
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
|
const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug);
|
||||||
|
|
||||||
for (const agentDir of agentDirs) {
|
|
||||||
if (!agentDir.isDirectory()) continue;
|
|
||||||
|
|
||||||
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
|
|
||||||
const standaloneAgents = await this.getAgentsFromDir(agentDirPath, 'standalone');
|
|
||||||
this.agents.push(...standaloneAgents);
|
this.agents.push(...standaloneAgents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get agents from a directory recursively
|
* Recursively walk a directory tree collecting agents.
|
||||||
* Only includes .md files with agent content
|
* Discovers agents via directory with bmad-skill-manifest.yaml containing type: agent
|
||||||
|
*
|
||||||
|
* @param {string} dirPath - Current directory being scanned
|
||||||
|
* @param {string} moduleName - Module this directory belongs to
|
||||||
|
* @param {string} relativePath - Path relative to the module root (for install path construction)
|
||||||
|
* @param {boolean} debug - Emit debug messages
|
||||||
*/
|
*/
|
||||||
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) {
|
||||||
// Skip directories claimed by collectSkills
|
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return [];
|
|
||||||
const agents = [];
|
const agents = [];
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
let entries;
|
||||||
// Load skill manifest for this directory (if present)
|
try {
|
||||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
// Check for type:agent manifest BEFORE checking skillClaimedDirs —
|
||||||
// Check for new-format agent: bmad-skill-manifest.yaml with type: agent
|
// agent dirs may be claimed by collectSkills for IDE installation,
|
||||||
// Note: type:agent dirs may also be claimed by collectSkills for IDE installation,
|
// but we still need them in agent-manifest.csv.
|
||||||
// but we still need to process them here for agent-manifest.csv
|
|
||||||
const dirManifest = await this.loadSkillManifest(fullPath);
|
const dirManifest = await this.loadSkillManifest(fullPath);
|
||||||
if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
|
if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') {
|
||||||
const m = dirManifest.__single;
|
const m = dirManifest.__single;
|
||||||
const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||||
const installPath =
|
const agentModule = m.module || moduleName;
|
||||||
moduleName === 'core'
|
const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`;
|
||||||
? `${this.bmadFolderName}/core/agents/${dirRelativePath}`
|
|
||||||
: `${this.bmadFolderName}/${moduleName}/agents/${dirRelativePath}`;
|
|
||||||
|
|
||||||
agents.push({
|
agents.push({
|
||||||
name: m.name || entry.name,
|
name: m.name || entry.name,
|
||||||
|
|
@ -337,7 +339,7 @@ class ManifestGenerator {
|
||||||
identity: m.identity ? this.cleanForCSV(m.identity) : '',
|
identity: m.identity ? this.cleanForCSV(m.identity) : '',
|
||||||
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
|
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
|
||||||
principles: m.principles ? this.cleanForCSV(m.principles) : '',
|
principles: m.principles ? this.cleanForCSV(m.principles) : '',
|
||||||
module: m.module || moduleName,
|
module: agentModule,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
canonicalId: m.canonicalId || '',
|
canonicalId: m.canonicalId || '',
|
||||||
});
|
});
|
||||||
|
|
@ -345,76 +347,24 @@ class ManifestGenerator {
|
||||||
this.files.push({
|
this.files.push({
|
||||||
type: 'agent',
|
type: 'agent',
|
||||||
name: m.name || entry.name,
|
name: m.name || entry.name,
|
||||||
module: moduleName,
|
module: agentModule,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip directories claimed by collectSkills (non-agent type skills)
|
// Skip directories claimed by collectSkills (non-agent type skills) —
|
||||||
|
// avoids recursing into skill trees that can't contain agents.
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue;
|
if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue;
|
||||||
|
|
||||||
// Recurse into subdirectories
|
// Recurse into subdirectories
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||||
const subDirAgents = await this.getAgentsFromDir(fullPath, moduleName, newRelativePath);
|
const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug);
|
||||||
agents.push(...subDirAgents);
|
agents.push(...subDirAgents);
|
||||||
} else if (entry.name.endsWith('.md') && entry.name.toLowerCase() !== 'readme.md') {
|
|
||||||
const content = await fs.readFile(fullPath, 'utf8');
|
|
||||||
|
|
||||||
// Skip files that don't contain <agent> tag (e.g., README files)
|
|
||||||
if (!content.includes('<agent')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip web-only agents
|
|
||||||
if (content.includes('localskip="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract agent metadata from the XML structure
|
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
|
||||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
|
||||||
const capabilitiesMatch = content.match(/capabilities="([^"]+)"/);
|
|
||||||
|
|
||||||
// Extract persona fields
|
|
||||||
const roleMatch = content.match(/<role>([^<]+)<\/role>/);
|
|
||||||
const identityMatch = content.match(/<identity>([\s\S]*?)<\/identity>/);
|
|
||||||
const styleMatch = content.match(/<communication_style>([\s\S]*?)<\/communication_style>/);
|
|
||||||
const principlesMatch = content.match(/<principles>([\s\S]*?)<\/principles>/);
|
|
||||||
|
|
||||||
// Build relative path for installation
|
|
||||||
const fileRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
||||||
const installPath =
|
|
||||||
moduleName === 'core'
|
|
||||||
? `${this.bmadFolderName}/core/agents/${fileRelativePath}`
|
|
||||||
: `${this.bmadFolderName}/${moduleName}/agents/${fileRelativePath}`;
|
|
||||||
|
|
||||||
const agentName = entry.name.replace('.md', '');
|
|
||||||
|
|
||||||
agents.push({
|
|
||||||
name: agentName,
|
|
||||||
displayName: nameMatch ? nameMatch[1] : agentName,
|
|
||||||
title: titleMatch ? titleMatch[1] : '',
|
|
||||||
icon: iconMatch ? iconMatch[1] : '',
|
|
||||||
capabilities: capabilitiesMatch ? this.cleanForCSV(capabilitiesMatch[1]) : '',
|
|
||||||
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
|
||||||
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
|
||||||
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
|
||||||
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to files list
|
|
||||||
this.files.push({
|
|
||||||
type: 'agent',
|
|
||||||
name: agentName,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return agents;
|
return agents;
|
||||||
|
|
@ -704,21 +654,12 @@ class ManifestGenerator {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this looks like a module (has agents, workflows, or tasks directory)
|
// Check if this looks like a module (has agents directory or skill manifests)
|
||||||
const modulePath = path.join(bmadDir, entry.name);
|
const modulePath = path.join(bmadDir, entry.name);
|
||||||
const hasAgents = await fs.pathExists(path.join(modulePath, 'agents'));
|
const hasAgents = await fs.pathExists(path.join(modulePath, 'agents'));
|
||||||
const hasWorkflows = await fs.pathExists(path.join(modulePath, 'workflows'));
|
const hasSkills = await this._hasSkillMdRecursive(modulePath);
|
||||||
const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks'));
|
|
||||||
const hasTools = await fs.pathExists(path.join(modulePath, 'tools'));
|
|
||||||
|
|
||||||
// Check for native-entrypoint-only modules: recursive scan for SKILL.md
|
if (hasAgents || hasSkills) {
|
||||||
let hasSkills = false;
|
|
||||||
if (!hasAgents && !hasWorkflows && !hasTasks && !hasTools) {
|
|
||||||
hasSkills = await this._hasSkillMdRecursive(modulePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it has any of these directories or skill manifests, it's likely a module
|
|
||||||
if (hasAgents || hasWorkflows || hasTasks || hasTools || hasSkills) {
|
|
||||||
modules.push(entry.name);
|
modules.push(entry.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,33 @@ const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
|
||||||
/**
|
/**
|
||||||
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
||||||
* Shared by installers that need Claude-style exports.
|
* Shared by installers that need Claude-style exports.
|
||||||
|
*
|
||||||
|
* TODO: Dead code cleanup — compiled XML agents are retired.
|
||||||
|
*
|
||||||
|
* All agents now use the SKILL.md directory format with bmad-skill-manifest.yaml
|
||||||
|
* (type: agent). The legacy pipeline below only discovers compiled .md files
|
||||||
|
* containing <agent> XML tags, which no longer exist. The following are dead:
|
||||||
|
*
|
||||||
|
* - getAgentsFromBmad() — scans {module}/agents/ for .md files with <agent> tags
|
||||||
|
* - getAgentsFromDir() — recursive helper for the above
|
||||||
|
* - AgentCommandGenerator — (agent-command-generator.js) generates launcher .md files
|
||||||
|
* that tell the LLM to load a compiled agent .md file
|
||||||
|
* - agent-command-template.md — (templates/) the launcher template with hardcoded
|
||||||
|
* {module}/agents/{{path}} reference
|
||||||
|
*
|
||||||
|
* Agent metadata for agent-manifest.csv is now handled entirely by
|
||||||
|
* ManifestGenerator.getAgentsFromDirRecursive() in manifest-generator.js,
|
||||||
|
* which walks the full module tree and finds type:agent directories.
|
||||||
|
*
|
||||||
|
* IDE installation of agents is handled by the native skill pipeline —
|
||||||
|
* each agent's SKILL.md directory is installed directly to the IDE's
|
||||||
|
* skills path, so no launcher intermediary is needed.
|
||||||
|
*
|
||||||
|
* Cleanup: remove getAgentsFromBmad, getAgentsFromDir, their exports,
|
||||||
|
* AgentCommandGenerator, agent-command-template.md, and all call sites
|
||||||
|
* in IDE installers that invoke collectAgentArtifacts / writeAgentLaunchers /
|
||||||
|
* writeColonArtifacts / writeDashArtifacts.
|
||||||
|
* getTasksFromBmad and getTasksFromDir may still be live — verify before removing.
|
||||||
*/
|
*/
|
||||||
async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
||||||
const agents = [];
|
const agents = [];
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ async function loadSkillManifest(dirPath) {
|
||||||
/**
|
/**
|
||||||
* Get the canonicalId for a specific file from a loaded skill manifest.
|
* Get the canonicalId for a specific file from a loaded skill manifest.
|
||||||
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
||||||
* @param {string} filename - Source filename to look up (e.g., 'pm.md', 'help.md', 'pm.agent.yaml')
|
* @param {string} filename - Source filename to look up (e.g., 'pm.md', 'help.md')
|
||||||
* @returns {string} canonicalId or empty string
|
* @returns {string} canonicalId or empty string
|
||||||
*/
|
*/
|
||||||
function getCanonicalId(manifest, filename) {
|
function getCanonicalId(manifest, filename) {
|
||||||
|
|
@ -36,12 +36,6 @@ function getCanonicalId(manifest, filename) {
|
||||||
if (manifest.__single) return manifest.__single.canonicalId || '';
|
if (manifest.__single) return manifest.__single.canonicalId || '';
|
||||||
// Multi-entry: look up by filename directly
|
// Multi-entry: look up by filename directly
|
||||||
if (manifest[filename]) return manifest[filename].canonicalId || '';
|
if (manifest[filename]) return manifest[filename].canonicalId || '';
|
||||||
// Fallback: try alternate extensions for compiled files
|
|
||||||
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
|
||||||
const agentKey = `${baseName}.agent.yaml`;
|
|
||||||
if (manifest[agentKey]) return manifest[agentKey].canonicalId || '';
|
|
||||||
const xmlKey = `${baseName}.xml`;
|
|
||||||
if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || '';
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,12 +51,6 @@ function getArtifactType(manifest, filename) {
|
||||||
if (manifest.__single) return manifest.__single.type || null;
|
if (manifest.__single) return manifest.__single.type || null;
|
||||||
// Multi-entry: look up by filename directly
|
// Multi-entry: look up by filename directly
|
||||||
if (manifest[filename]) return manifest[filename].type || null;
|
if (manifest[filename]) return manifest[filename].type || null;
|
||||||
// Fallback: try alternate extensions for compiled files
|
|
||||||
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
|
||||||
const agentKey = `${baseName}.agent.yaml`;
|
|
||||||
if (manifest[agentKey]) return manifest[agentKey].type || null;
|
|
||||||
const xmlKey = `${baseName}.xml`;
|
|
||||||
if (manifest[xmlKey]) return manifest[xmlKey].type || null;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,12 +66,6 @@ function getInstallToBmad(manifest, filename) {
|
||||||
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
|
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
|
||||||
// Multi-entry: look up by filename directly
|
// Multi-entry: look up by filename directly
|
||||||
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
|
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
|
||||||
// Fallback: try alternate extensions for compiled files
|
|
||||||
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
|
||||||
const agentKey = `${baseName}.agent.yaml`;
|
|
||||||
if (manifest[agentKey]) return manifest[agentKey].install_to_bmad !== false;
|
|
||||||
const xmlKey = `${baseName}.xml`;
|
|
||||||
if (manifest[xmlKey]) return manifest[xmlKey].install_to_bmad !== false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1624,8 +1624,49 @@ class OfficialModules {
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configValue = this.resolveConfigValue(configKey, currentModule, moduleConfig);
|
||||||
|
|
||||||
|
return configValue || match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean a stored path-like value for prompt display/input reuse.
|
||||||
|
* @param {*} value - Stored value
|
||||||
|
* @returns {*} Cleaned value
|
||||||
|
*/
|
||||||
|
cleanPromptValue(value) {
|
||||||
|
if (typeof value === 'string' && value.startsWith('{project-root}/')) {
|
||||||
|
return value.replace('{project-root}/', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a config key from answers, collected config, existing config, or schema defaults.
|
||||||
|
* @param {string} configKey - Config key to resolve
|
||||||
|
* @param {string} currentModule - Current module name
|
||||||
|
* @param {Object} moduleConfig - Current module config schema
|
||||||
|
* @returns {*} Resolved value
|
||||||
|
*/
|
||||||
|
resolveConfigValue(configKey, currentModule = null, moduleConfig = null) {
|
||||||
// Look for the config value in allAnswers (already answered questions)
|
// Look for the config value in allAnswers (already answered questions)
|
||||||
let configValue = this.allAnswers[configKey] || this.allAnswers[`core_${configKey}`];
|
let configValue = this.allAnswers?.[configKey] || this.allAnswers?.[`core_${configKey}`];
|
||||||
|
|
||||||
|
if (!configValue && this.allAnswers) {
|
||||||
|
for (const [answerKey, answerValue] of Object.entries(this.allAnswers)) {
|
||||||
|
if (answerKey.endsWith(`_${configKey}`)) {
|
||||||
|
configValue = answerValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the current module's persisted value when re-prompting an existing install
|
||||||
|
if (!configValue && currentModule && this._existingConfig?.[currentModule]?.[configKey] !== undefined) {
|
||||||
|
configValue = this._existingConfig[currentModule][configKey];
|
||||||
|
}
|
||||||
|
|
||||||
// Check in already collected config
|
// Check in already collected config
|
||||||
if (!configValue) {
|
if (!configValue) {
|
||||||
|
|
@ -1637,6 +1678,16 @@ class OfficialModules {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to other existing module config values
|
||||||
|
if (!configValue && this._existingConfig) {
|
||||||
|
for (const mod of Object.keys(this._existingConfig)) {
|
||||||
|
if (mod !== '_meta' && this._existingConfig[mod] && this._existingConfig[mod][configKey]) {
|
||||||
|
configValue = this._existingConfig[mod][configKey];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If still not found and we're in the same module, use the default from the config schema
|
// If still not found and we're in the same module, use the default from the config schema
|
||||||
if (!configValue && currentModule && moduleConfig && moduleConfig[configKey]) {
|
if (!configValue && currentModule && moduleConfig && moduleConfig[configKey]) {
|
||||||
const referencedItem = moduleConfig[configKey];
|
const referencedItem = moduleConfig[configKey];
|
||||||
|
|
@ -1645,8 +1696,49 @@ class OfficialModules {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return configValue || match;
|
return this.cleanPromptValue(configValue);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an existing stored value back into the prompt-facing value for templated fields.
|
||||||
|
* For example, "{test_artifacts}/{value}" + "_bmad-output/test-artifacts/test-design"
|
||||||
|
* becomes "test-design" so the template is not applied twice on modify.
|
||||||
|
* @param {*} existingValue - Stored config value
|
||||||
|
* @param {string} moduleName - Module name
|
||||||
|
* @param {Object} item - Config item definition
|
||||||
|
* @param {Object} moduleConfig - Current module config schema
|
||||||
|
* @returns {*} Prompt-facing default value
|
||||||
|
*/
|
||||||
|
normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig = null) {
|
||||||
|
const cleanedValue = this.cleanPromptValue(existingValue);
|
||||||
|
|
||||||
|
if (typeof cleanedValue !== 'string' || typeof item?.result !== 'string' || !item.result.includes('{value}')) {
|
||||||
|
return cleanedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [prefixTemplate = '', suffixTemplate = ''] = item.result.split('{value}');
|
||||||
|
const prefix = this.cleanPromptValue(this.replacePlaceholders(prefixTemplate, moduleName, moduleConfig));
|
||||||
|
const suffix = this.cleanPromptValue(this.replacePlaceholders(suffixTemplate, moduleName, moduleConfig));
|
||||||
|
|
||||||
|
if ((prefix && !cleanedValue.startsWith(prefix)) || (suffix && !cleanedValue.endsWith(suffix))) {
|
||||||
|
return cleanedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = prefix.length;
|
||||||
|
const endIndex = suffix ? cleanedValue.length - suffix.length : cleanedValue.length;
|
||||||
|
if (endIndex < startIndex) {
|
||||||
|
return cleanedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let promptValue = cleanedValue.slice(startIndex, endIndex);
|
||||||
|
if (promptValue.startsWith('/')) {
|
||||||
|
promptValue = promptValue.slice(1);
|
||||||
|
}
|
||||||
|
if (promptValue.endsWith('/')) {
|
||||||
|
promptValue = promptValue.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptValue || cleanedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1663,12 +1755,7 @@ class OfficialModules {
|
||||||
let existingValue = null;
|
let existingValue = null;
|
||||||
if (this._existingConfig && this._existingConfig[moduleName]) {
|
if (this._existingConfig && this._existingConfig[moduleName]) {
|
||||||
existingValue = this._existingConfig[moduleName][key];
|
existingValue = this._existingConfig[moduleName][key];
|
||||||
|
existingValue = this.normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig);
|
||||||
// Clean up existing value - remove {project-root}/ prefix if present
|
|
||||||
// This prevents duplication when the result template adds it back
|
|
||||||
if (typeof existingValue === 'string' && existingValue.startsWith('{project-root}/')) {
|
|
||||||
existingValue = existingValue.replace('{project-root}/', '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for user_name: default to system user
|
// Special handling for user_name: default to system user
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue