refactor(installer): trim shard-doc prototype path to lean behavior

This commit is contained in:
Dicky Moore 2026-03-07 20:26:34 +00:00
parent d5bb2398bd
commit b08495c6e4
3 changed files with 20 additions and 67 deletions

View File

@ -27,7 +27,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
super(platformCode, platformConfig.name, platformConfig.preferred); super(platformCode, platformConfig.name, platformConfig.preferred);
this.platformConfig = platformConfig; this.platformConfig = platformConfig;
this.installerConfig = platformConfig.installer || null; this.installerConfig = platformConfig.installer || null;
this._manifestCache = new Map();
// Set configDir from target_dir so base-class detect() works // Set configDir from target_dir so base-class detect() works
if (this.installerConfig?.target_dir) { if (this.installerConfig?.target_dir) {
@ -117,7 +116,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/ */
async installToTarget(projectDir, bmadDir, config, options) { async installToTarget(projectDir, bmadDir, config, options) {
const { target_dir, template_type, artifact_types } = config; const { target_dir, template_type, artifact_types } = config;
this._manifestCache = new Map();
// Skip targets with explicitly empty artifact_types array // Skip targets with explicitly empty artifact_types array
// This prevents creating empty directories when no artifacts will be written // 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 sourceRef = this.resolveArtifactSourceRef(artifact, bmadDir);
const duplicatePrototypeIds = await this.getPrototypeSkillIdsForArtifact(artifact, bmadDir, sourceRef); const duplicatePrototypeIds = await this.getPrototypeSkillIdsForArtifact(artifact, bmadDir, sourceRef);
for (const prototypeId of duplicatePrototypeIds) { for (const prototypeId of duplicatePrototypeIds) {
if (!this.isSafeSkillFolderName(prototypeId)) continue;
if (prototypeId === skillName) continue; if (prototypeId === skillName) continue;
if (typeof prototypeId !== 'string' || !prototypeId.trim()) continue;
const prototypeDir = path.join(targetPath, prototypeId); const prototypeDir = path.join(targetPath, prototypeId);
await this.ensureDir(prototypeDir); await this.ensureDir(prototypeDir);
const sourceSkillContent = await this.readPrototypeSourceSkillContent(sourceRef, prototypeId); const sourceSkillContent = await this.readPrototypeSourceSkillContent(sourceRef, prototypeId);
const prototypeContent = sourceSkillContent ?? this.transformToSkillFormat(content, prototypeId); if (!sourceSkillContent) continue;
await this.writeFile(path.join(prototypeDir, 'SKILL.md'), prototypeContent); 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); const sourceRef = sourceRefOverride ?? this.resolveArtifactSourceRef(artifact, bmadDir);
if (!sourceRef) return []; if (!sourceRef) return [];
let manifest = this._manifestCache.get(sourceRef.dirPath); const manifest = await loadSkillManifest(sourceRef.dirPath);
if (manifest === undefined) {
manifest = await loadSkillManifest(sourceRef.dirPath);
this._manifestCache.set(sourceRef.dirPath, manifest);
}
return getPrototypeIds(manifest, sourceRef.filename); return getPrototypeIds(manifest, sourceRef.filename);
} }
@ -542,40 +536,12 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @returns {Promise<string|null>} Source SKILL.md content or null * @returns {Promise<string|null>} Source SKILL.md content or null
*/ */
async readPrototypeSourceSkillContent(sourceRef, prototypeId) { async readPrototypeSourceSkillContent(sourceRef, prototypeId) {
if (!sourceRef || !this.isSafeSkillFolderName(prototypeId)) return null; if (!sourceRef || typeof prototypeId !== 'string' || !prototypeId.trim()) return null;
const sourceSkillPath = path.join(sourceRef.dirPath, prototypeId, 'SKILL.md');
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 (!(await fs.pathExists(sourceSkillPath))) return null; if (!(await fs.pathExists(sourceSkillPath))) return null;
return fs.readFile(sourceSkillPath, 'utf8'); 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. * Resolve the artifact source directory + filename within installed bmad tree.
* @param {Object} artifact - Artifact metadata * @param {Object} artifact - Artifact metadata
@ -615,15 +581,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
const filename = path.basename(normalized); const filename = path.basename(normalized);
if (!filename || filename === '.' || filename === '..') return null; if (!filename || filename === '.' || filename === '..') return null;
const resolvedBmadDir = path.resolve(bmadDir);
const relativeDir = path.dirname(normalized); const relativeDir = path.dirname(normalized);
const dirPath = relativeDir === '.' ? resolvedBmadDir : path.resolve(resolvedBmadDir, relativeDir); const dirPath = relativeDir === '.' ? bmadDir : path.join(bmadDir, relativeDir);
const pathFromBmadRoot = path.relative(resolvedBmadDir, dirPath);
if (pathFromBmadRoot === '..' || pathFromBmadRoot.startsWith(`..${path.sep}`) || path.isAbsolute(pathFromBmadRoot)) {
return null;
}
return { dirPath, filename }; return { dirPath, filename };
} }

View File

@ -266,7 +266,7 @@ function parseUnderscoreName(filename) {
/** /**
* Resolve the skill name for an artifact. * 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(). * falling back to the path-derived name from toDashPath().
* *
* @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId) * @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId)

View File

@ -2,32 +2,26 @@ const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const SKILL_MANIFEST_FILENAMES = ['skill-manifest.yaml', 'bmad-skill-manifest.yaml', 'manifest.yaml'];
/** /**
* Load skill manifest from a directory. * Load skill manifest from a directory.
* Single-entry manifests (canonicalId at top level) apply to all files in the directory. * Single-entry manifests (canonicalId at top level) apply to all files in the directory.
* Multi-entry manifests are keyed by source filename. * 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 * @returns {Object|null} Parsed manifest or null
*/ */
async function loadSkillManifest(dirPath) { async function loadSkillManifest(dirPath) {
for (const manifestFilename of SKILL_MANIFEST_FILENAMES) { const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml');
const manifestPath = path.join(dirPath, manifestFilename); try {
try { if (!(await fs.pathExists(manifestPath))) return null;
if (!(await fs.pathExists(manifestPath))) continue; const content = await fs.readFile(manifestPath, 'utf8');
const content = await fs.readFile(manifestPath, 'utf8'); const parsed = yaml.parse(content);
const parsed = yaml.parse(content); if (!parsed || typeof parsed !== 'object') return null;
if (!parsed || typeof parsed !== 'object') return null; if (parsed.canonicalId) return { __single: parsed };
if (parsed.canonicalId) return { __single: parsed }; return parsed;
return parsed; } catch (error) {
} catch (error) { console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
console.warn(`Warning: Failed to parse ${manifestFilename} in ${dirPath}: ${error.message}`); return null;
return null;
}
} }
return null;
} }
/** /**