diff --git a/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml b/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml index dd02073a6..9c88e320a 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml +++ b/src/bmm-skills/1-analysis/bmad-agent-analyst/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-analyst displayName: Mary title: Business Analyst diff --git a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml index 24af1bfc8..2aba65602 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml +++ b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-tech-writer displayName: Paige title: Technical Writer diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml index 85a2fde52..c38b5e1ed 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-pm displayName: John title: Product Manager diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml index bae324913..ca0983b4b 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-ux-designer displayName: Sally title: UX Designer diff --git a/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml b/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml index df54e57ed..ed1006ddd 100644 --- a/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml +++ b/src/bmm-skills/3-solutioning/bmad-agent-architect/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-architect displayName: Winston title: Architect diff --git a/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml index 2feeb538a..c6ca829c2 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-dev/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-dev displayName: Amelia title: Developer Agent diff --git a/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml index 5d561cd2b..ebf5e98bb 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-qa displayName: Quinn title: QA Engineer diff --git a/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml index 107435a3a..63013f345 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-quick-flow-solo-dev displayName: Barry title: Quick Flow Solo Dev diff --git a/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml b/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml index f1f46f84b..71fc35fa6 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +++ b/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml @@ -1,4 +1,4 @@ -type: skill +type: agent name: bmad-agent-sm displayName: Bob title: Scrum Master diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index f068e6857..e1d70608d 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -42,7 +42,7 @@ class Installer { const officialModules = await OfficialModules.build(config, paths); const existingInstall = await ExistingInstall.detect(paths.bmadDir); - await this.customModules.discoverPaths(config, paths); + await this.customModules.discoverPaths(customConfig, paths); if (existingInstall.installed) { await this._removeDeselectedModules(existingInstall, config, paths); diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 80dc2ddc4..65e0f4ed3 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -268,153 +268,103 @@ class ManifestGenerator { } /** - * Collect all agents from core and selected modules - * Scans the INSTALLED bmad directory, not the source + * Collect all agents from selected modules by walking their directory trees. */ async collectAgents(selectedModules) { 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) { - 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.getAgentsFromDir(agentsPath, moduleName); - this.agents.push(...moduleAgents); - } + const moduleAgents = await this.getAgentsFromDirRecursive(modulePath, moduleName, '', debug); + this.agents.push(...moduleAgents); } // Get standalone agents from bmad/agents/ directory const standaloneAgentsDir = path.join(this.bmadDir, 'agents'); if (await fs.pathExists(standaloneAgentsDir)) { - const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true }); + const standaloneAgents = await this.getAgentsFromDirRecursive(standaloneAgentsDir, 'standalone', '', debug); + this.agents.push(...standaloneAgents); + } - 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); - } + if (debug) { + console.log(`[DEBUG] collectAgents: total agents found: ${this.agents.length}`); } } /** - * Get agents from a directory recursively - * Only includes .md files with agent content + * Recursively walk a directory tree collecting agents. + * 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 = '') { - // Skip directories claimed by collectSkills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return []; + async getAgentsFromDirRecursive(dirPath, moduleName, relativePath = '', debug = false) { const agents = []; - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - // Load skill manifest for this directory (if present) - const skillManifest = await this.loadSkillManifest(dirPath); + let entries; + try { + entries = await fs.readdir(dirPath, { withFileTypes: true }); + } catch { + return agents; + } for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue; + const fullPath = path.join(dirPath, entry.name); - if (entry.isDirectory()) { - // Check for new-format agent: bmad-skill-manifest.yaml with type: agent - // Note: type:agent dirs may also be claimed by collectSkills for IDE installation, - // but we still need to process them here for agent-manifest.csv - const dirManifest = await this.loadSkillManifest(fullPath); - if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') { - const m = dirManifest.__single; - const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - const installPath = - moduleName === 'core' - ? `${this.bmadFolderName}/core/agents/${dirRelativePath}` - : `${this.bmadFolderName}/${moduleName}/agents/${dirRelativePath}`; - - agents.push({ - name: m.name || entry.name, - displayName: m.displayName || m.name || entry.name, - title: m.title || '', - icon: m.icon || '', - capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '', - role: m.role ? this.cleanForCSV(m.role) : '', - identity: m.identity ? this.cleanForCSV(m.identity) : '', - communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '', - principles: m.principles ? this.cleanForCSV(m.principles) : '', - module: m.module || moduleName, - path: installPath, - canonicalId: m.canonicalId || '', - }); - - this.files.push({ - type: 'agent', - name: m.name || entry.name, - module: moduleName, - path: installPath, - }); - continue; - } - - // Skip directories claimed by collectSkills (non-agent type skills) - if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue; - - // Recurse into subdirectories - const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - const subDirAgents = await this.getAgentsFromDir(fullPath, moduleName, newRelativePath); - 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 tag (e.g., README files) - if (!content.includes('([^<]+)<\/role>/); - const identityMatch = content.match(/([\s\S]*?)<\/identity>/); - const styleMatch = content.match(/([\s\S]*?)<\/communication_style>/); - const principlesMatch = content.match(/([\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', ''); + // Check for type:agent manifest BEFORE checking skillClaimedDirs — + // agent dirs may be claimed by collectSkills for IDE installation, + // but we still need them in agent-manifest.csv. + const dirManifest = await this.loadSkillManifest(fullPath); + if (dirManifest && dirManifest.__single && dirManifest.__single.type === 'agent') { + const m = dirManifest.__single; + const dirRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const agentModule = m.module || moduleName; + const installPath = `${this.bmadFolderName}/${agentModule}/${dirRelativePath}`; 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, + name: m.name || entry.name, + displayName: m.displayName || m.name || entry.name, + title: m.title || '', + icon: m.icon || '', + capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '', + role: m.role ? this.cleanForCSV(m.role) : '', + identity: m.identity ? this.cleanForCSV(m.identity) : '', + communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '', + principles: m.principles ? this.cleanForCSV(m.principles) : '', + module: agentModule, path: installPath, - canonicalId: this.getCanonicalId(skillManifest, entry.name), + canonicalId: m.canonicalId || '', }); - // Add to files list this.files.push({ type: 'agent', - name: agentName, - module: moduleName, + name: m.name || entry.name, + module: agentModule, path: installPath, }); + + if (debug) { + console.log(`[DEBUG] collectAgents: found type:agent "${m.name || entry.name}" at ${fullPath}`); + } + continue; } + + // 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; + + // Recurse into subdirectories + const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + const subDirAgents = await this.getAgentsFromDirRecursive(fullPath, moduleName, newRelativePath, debug); + agents.push(...subDirAgents); } return agents; @@ -704,21 +654,12 @@ class ManifestGenerator { 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 hasAgents = await fs.pathExists(path.join(modulePath, 'agents')); - const hasWorkflows = await fs.pathExists(path.join(modulePath, 'workflows')); - const hasTasks = await fs.pathExists(path.join(modulePath, 'tasks')); - const hasTools = await fs.pathExists(path.join(modulePath, 'tools')); + const hasSkills = await this._hasSkillMdRecursive(modulePath); - // Check for native-entrypoint-only modules: recursive scan for SKILL.md - 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) { + if (hasAgents || hasSkills) { modules.push(entry.name); } } diff --git a/tools/installer/ide/shared/bmad-artifacts.js b/tools/installer/ide/shared/bmad-artifacts.js index d3edf0cd2..ac0dbd190 100644 --- a/tools/installer/ide/shared/bmad-artifacts.js +++ b/tools/installer/ide/shared/bmad-artifacts.js @@ -5,6 +5,33 @@ const { loadSkillManifest, getCanonicalId } = require('./skill-manifest'); /** * Helpers for gathering BMAD agents/tasks from the installed tree. * 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 XML tags, which no longer exist. The following are dead: + * + * - getAgentsFromBmad() — scans {module}/agents/ for .md files with 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 = []) { const agents = []; diff --git a/tools/installer/ide/shared/skill-manifest.js b/tools/installer/ide/shared/skill-manifest.js index 22a7cceef..c5ae4aed8 100644 --- a/tools/installer/ide/shared/skill-manifest.js +++ b/tools/installer/ide/shared/skill-manifest.js @@ -27,7 +27,7 @@ async function loadSkillManifest(dirPath) { /** * Get the canonicalId for a specific file from a loaded skill manifest. * @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 */ function getCanonicalId(manifest, filename) { @@ -36,12 +36,6 @@ function getCanonicalId(manifest, filename) { if (manifest.__single) return manifest.__single.canonicalId || ''; // Multi-entry: look up by filename directly 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 ''; } @@ -57,12 +51,6 @@ function getArtifactType(manifest, filename) { if (manifest.__single) return manifest.__single.type || null; // Multi-entry: look up by filename directly 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; } @@ -78,12 +66,6 @@ function getInstallToBmad(manifest, filename) { if (manifest.__single) return manifest.__single.install_to_bmad !== false; // Multi-entry: look up by filename directly 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; } diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index b807f44f2..df8733fa5 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -1624,31 +1624,123 @@ class OfficialModules { return match; } - // Look for the config value in allAnswers (already answered questions) - let configValue = this.allAnswers[configKey] || this.allAnswers[`core_${configKey}`]; - - // Check in already collected config - if (!configValue) { - for (const mod of Object.keys(this.collectedConfig)) { - if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) { - configValue = this.collectedConfig[mod][configKey]; - break; - } - } - } - - // If still not found and we're in the same module, use the default from the config schema - if (!configValue && currentModule && moduleConfig && moduleConfig[configKey]) { - const referencedItem = moduleConfig[configKey]; - if (referencedItem && referencedItem.default !== undefined) { - configValue = referencedItem.default; - } - } + 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) + 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 + if (!configValue) { + for (const mod of Object.keys(this.collectedConfig)) { + if (mod !== '_meta' && this.collectedConfig[mod] && this.collectedConfig[mod][configKey]) { + configValue = this.collectedConfig[mod][configKey]; + break; + } + } + } + + // 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 (!configValue && currentModule && moduleConfig && moduleConfig[configKey]) { + const referencedItem = moduleConfig[configKey]; + if (referencedItem && referencedItem.default !== undefined) { + configValue = referencedItem.default; + } + } + + 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; + } + /** * Build a prompt question from a config item * @param {string} moduleName - Module name @@ -1663,12 +1755,7 @@ class OfficialModules { let existingValue = null; if (this._existingConfig && this._existingConfig[moduleName]) { existingValue = this._existingConfig[moduleName][key]; - - // 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}/', ''); - } + existingValue = this.normalizeExistingValueForPrompt(existingValue, moduleName, item, moduleConfig); } // Special handling for user_name: default to system user