diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index de72e5ab4..59c303243 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -27,7 +27,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { super(platformCode, platformConfig.name, platformConfig.preferred); this.platformConfig = platformConfig; this.installerConfig = platformConfig.installer || null; - this._manifestCache = new Map(); // Set configDir from target_dir so base-class detect() works if (this.installerConfig?.target_dir) { @@ -117,7 +116,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { */ async installToTarget(projectDir, bmadDir, config, options) { const { target_dir, template_type, artifact_types } = config; - this._manifestCache = new Map(); // Skip targets with explicitly empty artifact_types array // This prevents creating empty directories when no artifacts will be written @@ -506,13 +504,13 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const sourceRef = this.resolveArtifactSourceRef(artifact, bmadDir); const duplicatePrototypeIds = await this.getPrototypeSkillIdsForArtifact(artifact, bmadDir, sourceRef); for (const prototypeId of duplicatePrototypeIds) { - if (!this.isSafeSkillFolderName(prototypeId)) continue; if (prototypeId === skillName) continue; + if (typeof prototypeId !== 'string' || !prototypeId.trim()) continue; const prototypeDir = path.join(targetPath, prototypeId); await this.ensureDir(prototypeDir); const sourceSkillContent = await this.readPrototypeSourceSkillContent(sourceRef, prototypeId); - const prototypeContent = sourceSkillContent ?? this.transformToSkillFormat(content, prototypeId); - await this.writeFile(path.join(prototypeDir, 'SKILL.md'), prototypeContent); + if (!sourceSkillContent) continue; + await this.writeFile(path.join(prototypeDir, 'SKILL.md'), sourceSkillContent); } } @@ -526,11 +524,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const sourceRef = sourceRefOverride ?? this.resolveArtifactSourceRef(artifact, bmadDir); if (!sourceRef) return []; - let manifest = this._manifestCache.get(sourceRef.dirPath); - if (manifest === undefined) { - manifest = await loadSkillManifest(sourceRef.dirPath); - this._manifestCache.set(sourceRef.dirPath, manifest); - } + const manifest = await loadSkillManifest(sourceRef.dirPath); return getPrototypeIds(manifest, sourceRef.filename); } @@ -542,40 +536,12 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @returns {Promise} Source SKILL.md content or null */ async readPrototypeSourceSkillContent(sourceRef, prototypeId) { - if (!sourceRef || !this.isSafeSkillFolderName(prototypeId)) return null; - - const resolvedSourceDir = path.resolve(sourceRef.dirPath); - const sourceSkillPath = path.resolve(resolvedSourceDir, prototypeId, 'SKILL.md'); - const relativeToSourceDir = path.relative(resolvedSourceDir, sourceSkillPath); - - if ( - relativeToSourceDir === '..' || - relativeToSourceDir.startsWith(`..${path.sep}`) || - path.isAbsolute(relativeToSourceDir) - ) { - return null; - } - + if (!sourceRef || typeof prototypeId !== 'string' || !prototypeId.trim()) return null; + const sourceSkillPath = path.join(sourceRef.dirPath, prototypeId, 'SKILL.md'); if (!(await fs.pathExists(sourceSkillPath))) return null; return fs.readFile(sourceSkillPath, 'utf8'); } - /** - * Validate skill folder names used for source lookup. - * @param {string} skillId - Candidate skill ID - * @returns {boolean} True if safe to use as a folder name segment - */ - isSafeSkillFolderName(skillId) { - return ( - typeof skillId === 'string' && - skillId.length > 0 && - skillId !== '.' && - skillId !== '..' && - !skillId.includes('/') && - !skillId.includes('\\') - ); - } - /** * Resolve the artifact source directory + filename within installed bmad tree. * @param {Object} artifact - Artifact metadata @@ -615,15 +581,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const filename = path.basename(normalized); if (!filename || filename === '.' || filename === '..') return null; - const resolvedBmadDir = path.resolve(bmadDir); const relativeDir = path.dirname(normalized); - const dirPath = relativeDir === '.' ? resolvedBmadDir : path.resolve(resolvedBmadDir, relativeDir); - const pathFromBmadRoot = path.relative(resolvedBmadDir, dirPath); - - if (pathFromBmadRoot === '..' || pathFromBmadRoot.startsWith(`..${path.sep}`) || path.isAbsolute(pathFromBmadRoot)) { - return null; - } - + const dirPath = relativeDir === '.' ? bmadDir : path.join(bmadDir, relativeDir); return { dirPath, filename }; } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index ebec2b6cb..45efd2ec1 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -266,7 +266,7 @@ function parseUnderscoreName(filename) { /** * Resolve the skill name for an artifact. - * Prefers canonicalId from a skill manifest sidecar when available, + * Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available, * falling back to the path-derived name from toDashPath(). * * @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId) diff --git a/tools/cli/installers/lib/ide/shared/skill-manifest.js b/tools/cli/installers/lib/ide/shared/skill-manifest.js index dce4aaca6..b47266b83 100644 --- a/tools/cli/installers/lib/ide/shared/skill-manifest.js +++ b/tools/cli/installers/lib/ide/shared/skill-manifest.js @@ -2,32 +2,26 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); -const SKILL_MANIFEST_FILENAMES = ['skill-manifest.yaml', 'bmad-skill-manifest.yaml', 'manifest.yaml']; - /** * Load skill manifest from a directory. * Single-entry manifests (canonicalId at top level) apply to all files in the directory. * Multi-entry manifests are keyed by source filename. - * @param {string} dirPath - Directory to check for supported manifest filenames + * @param {string} dirPath - Directory to check for bmad-skill-manifest.yaml * @returns {Object|null} Parsed manifest or null */ async function loadSkillManifest(dirPath) { - for (const manifestFilename of SKILL_MANIFEST_FILENAMES) { - const manifestPath = path.join(dirPath, manifestFilename); - try { - if (!(await fs.pathExists(manifestPath))) continue; - const content = await fs.readFile(manifestPath, 'utf8'); - const parsed = yaml.parse(content); - if (!parsed || typeof parsed !== 'object') return null; - if (parsed.canonicalId) return { __single: parsed }; - return parsed; - } catch (error) { - console.warn(`Warning: Failed to parse ${manifestFilename} in ${dirPath}: ${error.message}`); - return null; - } + const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml'); + try { + if (!(await fs.pathExists(manifestPath))) return null; + const content = await fs.readFile(manifestPath, 'utf8'); + const parsed = yaml.parse(content); + if (!parsed || typeof parsed !== 'object') return null; + if (parsed.canonicalId) return { __single: parsed }; + return parsed; + } catch (error) { + console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`); + return null; } - - return null; } /**