feat(installer): align shard-doc prototype with source-skill install model
This commit is contained in:
parent
86556edfcc
commit
18277c0ba1
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
name: bmad-shard-doc-skill-prototype
|
||||||
|
description: Prototype native skill wrapper for shard-doc during transition.
|
||||||
|
---
|
||||||
|
|
||||||
|
# bmad-shard-doc-skill-prototype
|
||||||
|
|
||||||
|
Prototype marker: source-authored-skill
|
||||||
|
|
||||||
|
Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml
|
||||||
|
|
||||||
|
Follow all shard-doc task instructions exactly as written.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-shard-doc-skill-prototype
|
||||||
|
type: task
|
||||||
|
description: "Prototype native skill wrapper for shard-doc during installer transition"
|
||||||
|
|
@ -72,7 +72,7 @@ async function createTestBmadFixture() {
|
||||||
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
|
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
|
||||||
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
|
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
|
||||||
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
|
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
|
||||||
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
|
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
|
||||||
|
|
||||||
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
|
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
|
||||||
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
||||||
|
|
@ -91,7 +91,7 @@ async function createShardDocPrototypeFixture() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(fixtureDir, 'core', 'tasks', 'bmad-skill-manifest.yaml'),
|
path.join(fixtureDir, 'core', 'tasks', 'skill-manifest.yaml'),
|
||||||
[
|
[
|
||||||
'shard-doc.xml:',
|
'shard-doc.xml:',
|
||||||
' canonicalId: bmad-shard-doc',
|
' canonicalId: bmad-shard-doc',
|
||||||
|
|
@ -103,6 +103,26 @@ async function createShardDocPrototypeFixture() {
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype'));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype', 'SKILL.md'),
|
||||||
|
[
|
||||||
|
'---',
|
||||||
|
'name: bmad-shard-doc-skill-prototype',
|
||||||
|
'description: Source-authored prototype skill',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'# bmad-shard-doc-skill-prototype',
|
||||||
|
'',
|
||||||
|
'Prototype marker: source-authored-skill',
|
||||||
|
'',
|
||||||
|
'Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml',
|
||||||
|
'',
|
||||||
|
'Follow all shard-doc task instructions exactly as written.',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(fixtureDir, '_config', 'task-manifest.csv'),
|
path.join(fixtureDir, '_config', 'task-manifest.csv'),
|
||||||
[
|
[
|
||||||
|
|
@ -601,6 +621,7 @@ async function runTests() {
|
||||||
const codexPrototypeContent = await fs.readFile(codexPrototypeSkill, 'utf8');
|
const codexPrototypeContent = await fs.readFile(codexPrototypeSkill, 'utf8');
|
||||||
assert(codexCanonicalContent.includes('name: bmad-shard-doc'), 'Canonical shard-doc skill keeps canonical frontmatter name');
|
assert(codexCanonicalContent.includes('name: bmad-shard-doc'), 'Canonical shard-doc skill keeps canonical frontmatter name');
|
||||||
assert(codexPrototypeContent.includes('name: bmad-shard-doc-skill-prototype'), 'Prototype shard-doc skill uses prototype frontmatter name');
|
assert(codexPrototypeContent.includes('name: bmad-shard-doc-skill-prototype'), 'Prototype shard-doc skill uses prototype frontmatter name');
|
||||||
|
assert(codexPrototypeContent.includes('Prototype marker: source-authored-skill'), 'Prototype shard-doc skill is copied from source SKILL.md');
|
||||||
|
|
||||||
const geminiResult = await ideManager.setup('gemini', tempGeminiProjectDir, installedBmadDir, {
|
const geminiResult = await ideManager.setup('gemini', tempGeminiProjectDir, installedBmadDir, {
|
||||||
silent: true,
|
silent: true,
|
||||||
|
|
|
||||||
|
|
@ -503,12 +503,15 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
|
|
||||||
if (!bmadDir) return;
|
if (!bmadDir) return;
|
||||||
|
|
||||||
const duplicatePrototypeIds = await this.getPrototypeSkillIdsForArtifact(artifact, bmadDir);
|
const sourceRef = this.resolveArtifactSourceRef(artifact, bmadDir);
|
||||||
|
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;
|
||||||
const prototypeDir = path.join(targetPath, prototypeId);
|
const prototypeDir = path.join(targetPath, prototypeId);
|
||||||
await this.ensureDir(prototypeDir);
|
await this.ensureDir(prototypeDir);
|
||||||
const prototypeContent = this.transformToSkillFormat(content, prototypeId);
|
const sourceSkillContent = await this.readPrototypeSourceSkillContent(sourceRef, prototypeId);
|
||||||
|
const prototypeContent = sourceSkillContent ?? this.transformToSkillFormat(content, prototypeId);
|
||||||
await this.writeFile(path.join(prototypeDir, 'SKILL.md'), prototypeContent);
|
await this.writeFile(path.join(prototypeDir, 'SKILL.md'), prototypeContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -519,8 +522,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
* @param {string} bmadDir - Installed bmad directory
|
* @param {string} bmadDir - Installed bmad directory
|
||||||
* @returns {Promise<string[]>} Prototype skill IDs
|
* @returns {Promise<string[]>} Prototype skill IDs
|
||||||
*/
|
*/
|
||||||
async getPrototypeSkillIdsForArtifact(artifact, bmadDir) {
|
async getPrototypeSkillIdsForArtifact(artifact, bmadDir, sourceRefOverride = null) {
|
||||||
const sourceRef = this.resolveArtifactSourceRef(artifact, bmadDir);
|
const sourceRef = sourceRefOverride ?? this.resolveArtifactSourceRef(artifact, bmadDir);
|
||||||
if (!sourceRef) return [];
|
if (!sourceRef) return [];
|
||||||
|
|
||||||
let manifest = this._manifestCache.get(sourceRef.dirPath);
|
let manifest = this._manifestCache.get(sourceRef.dirPath);
|
||||||
|
|
@ -531,6 +534,48 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
return getPrototypeIds(manifest, sourceRef.filename);
|
return getPrototypeIds(manifest, sourceRef.filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read prototype SKILL.md content directly from source when present.
|
||||||
|
* This enables copy-as-is installation for native skill prototypes.
|
||||||
|
* @param {{dirPath: string, filename: string}|null} sourceRef - Resolved source reference
|
||||||
|
* @param {string} prototypeId - Prototype skill ID
|
||||||
|
* @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 (!(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.
|
* Resolve the artifact source directory + filename within installed bmad tree.
|
||||||
* @param {Object} artifact - Artifact metadata
|
* @param {Object} artifact - Artifact metadata
|
||||||
|
|
|
||||||
|
|
@ -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 bmad-skill-manifest.yaml sidecar when available,
|
* Prefers canonicalId from a skill manifest 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)
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,32 @@ 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 bmad-skill-manifest.yaml 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 bmad-skill-manifest.yaml
|
* @param {string} dirPath - Directory to check for supported manifest filenames
|
||||||
* @returns {Object|null} Parsed manifest or null
|
* @returns {Object|null} Parsed manifest or null
|
||||||
*/
|
*/
|
||||||
async function loadSkillManifest(dirPath) {
|
async function loadSkillManifest(dirPath) {
|
||||||
const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml');
|
for (const manifestFilename of SKILL_MANIFEST_FILENAMES) {
|
||||||
try {
|
const manifestPath = path.join(dirPath, manifestFilename);
|
||||||
if (!(await fs.pathExists(manifestPath))) return null;
|
try {
|
||||||
const content = await fs.readFile(manifestPath, 'utf8');
|
if (!(await fs.pathExists(manifestPath))) continue;
|
||||||
const parsed = yaml.parse(content);
|
const content = await fs.readFile(manifestPath, 'utf8');
|
||||||
if (!parsed || typeof parsed !== 'object') return null;
|
const parsed = yaml.parse(content);
|
||||||
if (parsed.canonicalId) return { __single: parsed };
|
if (!parsed || typeof parsed !== 'object') return null;
|
||||||
return parsed;
|
if (parsed.canonicalId) return { __single: parsed };
|
||||||
} catch (error) {
|
return parsed;
|
||||||
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
|
} catch (error) {
|
||||||
return null;
|
console.warn(`Warning: Failed to parse ${manifestFilename} in ${dirPath}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue