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);
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<string|null>} 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 };
}

View File

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

View File

@ -2,34 +2,28 @@ 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);
const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml');
try {
if (!(await fs.pathExists(manifestPath))) continue;
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 ${manifestFilename} in ${dirPath}: ${error.message}`);
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
return null;
}
}
return null;
}
/**
* Get the canonicalId for a specific file from a loaded skill manifest.
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)