refactor(skills): add SKILL.md entrypoint to skill directories
Align skill source format with Open Skills standard: each skill directory now contains a SKILL.md with name/description frontmatter where name must match the directory name exactly. The installer copies skill directories verbatim instead of generating SKILL.md. - Add SKILL.md to both tracer bullet skill directories - Strip name/description from workflow.md frontmatter (SKILL.md owns it) - Installer reads metadata from SKILL.md, validates name matches dirname - Install path in manifest CSV now points to SKILL.md - Copy filter excludes OS/editor artifacts (.DS_Store, backups, dotfiles) - Debug-guard validation messages, keep name-mismatch as hard error - Add typeof guard for malformed YAML frontmatter - Add negative test cases for parseSkillMd validation (Suite 30)
This commit is contained in:
parent
ee25fcca6f
commit
34abaf2f4a
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
name: bmad-quick-dev-new-preview
|
||||
description: 'Unified quick flow - clarify intent, plan, implement, review, present.'
|
||||
---
|
||||
|
||||
Follow the instructions in [workflow.md](workflow.md).
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
---
|
||||
name: quick-dev-new-preview
|
||||
description: 'Unified quick flow - clarify intent, plan, implement, review, present.'
|
||||
main_config: '{project-root}/_bmad/bmm/config.yaml'
|
||||
|
||||
# Related workflows
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
name: bmad-review-adversarial-general
|
||||
description: 'Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something'
|
||||
---
|
||||
|
||||
Follow the instructions in [workflow.md](workflow.md).
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
---
|
||||
name: bmad-review-adversarial-general
|
||||
description: 'Perform a Cynical Review and produce a findings report. Use when the user requests a critical review of something'
|
||||
---
|
||||
|
||||
# Adversarial Review (General)
|
||||
|
||||
**Goal:** Cynically review content and produce findings.
|
||||
|
|
|
|||
|
|
@ -1607,9 +1607,10 @@ async function runTests() {
|
|||
await fs.ensureDir(skillDir29);
|
||||
await fs.writeFile(path.join(skillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||
await fs.writeFile(
|
||||
path.join(skillDir29, 'workflow.md'),
|
||||
'---\nname: My Custom Skill\ndescription: A skill at an unusual path\n---\n\nSkill body content\n',
|
||||
path.join(skillDir29, 'SKILL.md'),
|
||||
'---\nname: my-skill\ndescription: A skill at an unusual path\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
|
||||
);
|
||||
await fs.writeFile(path.join(skillDir29, 'workflow.md'), '# My Custom Skill\n\nSkill body content\n');
|
||||
|
||||
// --- Regular workflow dir: core/workflows/regular-wf/ (type: workflow) ---
|
||||
const wfDir29 = path.join(tempFixture29, 'core', 'workflows', 'regular-wf');
|
||||
|
|
@ -1625,18 +1626,20 @@ async function runTests() {
|
|||
await fs.ensureDir(wfSkillDir29);
|
||||
await fs.writeFile(path.join(wfSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||
await fs.writeFile(
|
||||
path.join(wfSkillDir29, 'workflow.md'),
|
||||
'---\nname: Workflow Skill\ndescription: A skill inside workflows dir\n---\n\nSkill in workflows\n',
|
||||
path.join(wfSkillDir29, 'SKILL.md'),
|
||||
'---\nname: wf-skill\ndescription: A skill inside workflows dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
|
||||
);
|
||||
await fs.writeFile(path.join(wfSkillDir29, 'workflow.md'), '# Workflow Skill\n\nSkill in workflows\n');
|
||||
|
||||
// --- Skill inside tasks/ dir: core/tasks/task-skill/ ---
|
||||
const taskSkillDir29 = path.join(tempFixture29, 'core', 'tasks', 'task-skill');
|
||||
await fs.ensureDir(taskSkillDir29);
|
||||
await fs.writeFile(path.join(taskSkillDir29, 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||
await fs.writeFile(
|
||||
path.join(taskSkillDir29, 'workflow.md'),
|
||||
'---\nname: Task Skill\ndescription: A skill inside tasks dir\n---\n\nSkill in tasks\n',
|
||||
path.join(taskSkillDir29, 'SKILL.md'),
|
||||
'---\nname: task-skill\ndescription: A skill inside tasks dir\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
|
||||
);
|
||||
await fs.writeFile(path.join(taskSkillDir29, 'workflow.md'), '# Task Skill\n\nSkill in tasks\n');
|
||||
|
||||
// Minimal agent so core module is detected
|
||||
await fs.ensureDir(path.join(tempFixture29, 'core', 'agents'));
|
||||
|
|
@ -1649,14 +1652,14 @@ async function runTests() {
|
|||
// Skill at unusual path should be in skills
|
||||
const skillEntry29 = generator29.skills.find((s) => s.canonicalId === 'my-skill');
|
||||
assert(skillEntry29 !== undefined, 'Skill at unusual path appears in skills[]');
|
||||
assert(skillEntry29 && skillEntry29.name === 'My Custom Skill', 'Skill has correct name from frontmatter');
|
||||
assert(skillEntry29 && skillEntry29.name === 'my-skill', 'Skill has correct name from frontmatter');
|
||||
assert(
|
||||
skillEntry29 && skillEntry29.path.includes('custom-area/my-skill/workflow.md'),
|
||||
skillEntry29 && skillEntry29.path.includes('custom-area/my-skill/SKILL.md'),
|
||||
'Skill path includes relative path from module root',
|
||||
);
|
||||
|
||||
// Skill should NOT be in workflows
|
||||
const inWorkflows29 = generator29.workflows.find((w) => w.name === 'My Custom Skill');
|
||||
const inWorkflows29 = generator29.workflows.find((w) => w.name === 'my-skill');
|
||||
assert(inWorkflows29 === undefined, 'Skill at unusual path does NOT appear in workflows[]');
|
||||
|
||||
// Skill in tasks/ dir should be in skills
|
||||
|
|
@ -1664,7 +1667,7 @@ async function runTests() {
|
|||
assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]');
|
||||
|
||||
// Skill in tasks/ should NOT appear in tasks[]
|
||||
const inTasks29 = generator29.tasks.find((t) => t.name === 'Task Skill');
|
||||
const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill');
|
||||
assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]');
|
||||
|
||||
// Regular workflow should be in workflows, NOT in skills
|
||||
|
|
@ -1677,7 +1680,7 @@ async function runTests() {
|
|||
// Skill inside workflows/ should be in skills[], NOT in workflows[] (exercises findWorkflows skip at lines 311/322)
|
||||
const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill');
|
||||
assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]');
|
||||
const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'Workflow Skill');
|
||||
const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'wf-skill');
|
||||
assert(wfSkillInWorkflows29 === undefined, 'Skill in workflows/ dir does NOT appear in workflows[]');
|
||||
|
||||
// Test scanInstalledModules recognizes skill-only modules
|
||||
|
|
@ -1685,9 +1688,10 @@ async function runTests() {
|
|||
await fs.ensureDir(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill'));
|
||||
await fs.writeFile(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'bmad-skill-manifest.yaml'), 'type: skill\n');
|
||||
await fs.writeFile(
|
||||
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'),
|
||||
'---\nname: Nested Skill\ndescription: desc\n---\nbody\n',
|
||||
path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'SKILL.md'),
|
||||
'---\nname: my-skill\ndescription: desc\n---\n\nFollow the instructions in [workflow.md](workflow.md).\n',
|
||||
);
|
||||
await fs.writeFile(path.join(skillOnlyModDir29, 'deep', 'nested', 'my-skill', 'workflow.md'), '# Nested Skill\n\nbody\n');
|
||||
|
||||
const scannedModules29 = await generator29.scanInstalledModules(tempFixture29);
|
||||
assert(scannedModules29.includes('skill-only-mod'), 'scanInstalledModules recognizes skill-only module');
|
||||
|
|
@ -1699,6 +1703,74 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Suite 30: parseSkillMd validation (negative cases)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 30: parseSkillMd Validation${colors.reset}\n`);
|
||||
|
||||
let tempFixture30;
|
||||
try {
|
||||
tempFixture30 = path.join(os.tmpdir(), `bmad-test-30-${Date.now()}`);
|
||||
await fs.ensureDir(tempFixture30);
|
||||
|
||||
const generator30 = new ManifestGenerator();
|
||||
generator30.bmadFolderName = '_bmad';
|
||||
|
||||
// Case 1: Missing SKILL.md entirely
|
||||
const noSkillDir = path.join(tempFixture30, 'no-skill-md');
|
||||
await fs.ensureDir(noSkillDir);
|
||||
const result1 = await generator30.parseSkillMd(path.join(noSkillDir, 'SKILL.md'), noSkillDir, 'no-skill-md');
|
||||
assert(result1 === null, 'parseSkillMd returns null when SKILL.md is missing');
|
||||
|
||||
// Case 2: SKILL.md with no frontmatter
|
||||
const noFmDir = path.join(tempFixture30, 'no-frontmatter');
|
||||
await fs.ensureDir(noFmDir);
|
||||
await fs.writeFile(path.join(noFmDir, 'SKILL.md'), '# Just a heading\n\nNo frontmatter here.\n');
|
||||
const result2 = await generator30.parseSkillMd(path.join(noFmDir, 'SKILL.md'), noFmDir, 'no-frontmatter');
|
||||
assert(result2 === null, 'parseSkillMd returns null when SKILL.md has no frontmatter');
|
||||
|
||||
// Case 3: SKILL.md missing description
|
||||
const noDescDir = path.join(tempFixture30, 'no-desc');
|
||||
await fs.ensureDir(noDescDir);
|
||||
await fs.writeFile(path.join(noDescDir, 'SKILL.md'), '---\nname: no-desc\n---\n\nBody.\n');
|
||||
const result3 = await generator30.parseSkillMd(path.join(noDescDir, 'SKILL.md'), noDescDir, 'no-desc');
|
||||
assert(result3 === null, 'parseSkillMd returns null when description is missing');
|
||||
|
||||
// Case 4: SKILL.md missing name
|
||||
const noNameDir = path.join(tempFixture30, 'no-name');
|
||||
await fs.ensureDir(noNameDir);
|
||||
await fs.writeFile(path.join(noNameDir, 'SKILL.md'), '---\ndescription: has desc but no name\n---\n\nBody.\n');
|
||||
const result4 = await generator30.parseSkillMd(path.join(noNameDir, 'SKILL.md'), noNameDir, 'no-name');
|
||||
assert(result4 === null, 'parseSkillMd returns null when name is missing');
|
||||
|
||||
// Case 5: Name mismatch
|
||||
const mismatchDir = path.join(tempFixture30, 'actual-dir-name');
|
||||
await fs.ensureDir(mismatchDir);
|
||||
await fs.writeFile(path.join(mismatchDir, 'SKILL.md'), '---\nname: wrong-name\ndescription: A skill\n---\n\nBody.\n');
|
||||
const result5 = await generator30.parseSkillMd(path.join(mismatchDir, 'SKILL.md'), mismatchDir, 'actual-dir-name');
|
||||
assert(result5 === null, 'parseSkillMd returns null when name does not match directory name');
|
||||
|
||||
// Case 6: Valid SKILL.md (positive control)
|
||||
const validDir = path.join(tempFixture30, 'valid-skill');
|
||||
await fs.ensureDir(validDir);
|
||||
await fs.writeFile(path.join(validDir, 'SKILL.md'), '---\nname: valid-skill\ndescription: A valid skill\n---\n\nBody.\n');
|
||||
const result6 = await generator30.parseSkillMd(path.join(validDir, 'SKILL.md'), validDir, 'valid-skill');
|
||||
assert(result6 !== null && result6.name === 'valid-skill', 'parseSkillMd returns metadata for valid SKILL.md');
|
||||
|
||||
// Case 7: Malformed YAML (non-object)
|
||||
const malformedDir = path.join(tempFixture30, 'malformed');
|
||||
await fs.ensureDir(malformedDir);
|
||||
await fs.writeFile(path.join(malformedDir, 'SKILL.md'), '---\njust a string\n---\n\nBody.\n');
|
||||
const result7 = await generator30.parseSkillMd(path.join(malformedDir, 'SKILL.md'), malformedDir, 'malformed');
|
||||
assert(result7 === null, 'parseSkillMd returns null for non-object YAML frontmatter');
|
||||
} catch (error) {
|
||||
assert(false, 'parseSkillMd validation test succeeds', error.message);
|
||||
} finally {
|
||||
if (tempFixture30) await fs.remove(tempFixture30).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class ManifestGenerator {
|
|||
/**
|
||||
* Recursively walk a module directory tree, collecting skill directories.
|
||||
* A skill directory is one that contains both a bmad-skill-manifest.yaml with
|
||||
* type: skill AND a workflow.md file.
|
||||
* type: skill AND a SKILL.md file with name/description frontmatter.
|
||||
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
||||
*/
|
||||
async collectSkills() {
|
||||
|
|
@ -169,75 +169,62 @@ class ManifestGenerator {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check this directory for skill manifest + workflow file
|
||||
// Check this directory for skill manifest
|
||||
const manifest = await this.loadSkillManifest(dir);
|
||||
|
||||
const workflowFile = 'workflow.md';
|
||||
const workflowPath = path.join(dir, workflowFile);
|
||||
if (await fs.pathExists(workflowPath)) {
|
||||
const artifactType = this.getArtifactType(manifest, workflowFile);
|
||||
if (artifactType === 'skill') {
|
||||
// Read and parse the workflow file
|
||||
try {
|
||||
const rawContent = await fs.readFile(workflowPath, 'utf8');
|
||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
// Determine if this directory is a skill (type: skill in manifest)
|
||||
const skillFile = 'SKILL.md';
|
||||
const artifactType = this.getArtifactType(manifest, skillFile);
|
||||
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (frontmatterMatch) {
|
||||
const workflow = yaml.parse(frontmatterMatch[1]);
|
||||
if (artifactType === 'skill') {
|
||||
const skillMdPath = path.join(dir, 'SKILL.md');
|
||||
const dirName = path.basename(dir);
|
||||
|
||||
if (!workflow || !workflow.name || !workflow.description) {
|
||||
if (debug) console.log(`[DEBUG] collectSkills: skipped (missing name/description): ${workflowPath}`);
|
||||
} else {
|
||||
// Build path relative from module root
|
||||
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
|
||||
const installPath = relativePath
|
||||
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${workflowFile}`
|
||||
: `${this.bmadFolderName}/${moduleName}/${workflowFile}`;
|
||||
// Validate and parse SKILL.md
|
||||
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
|
||||
|
||||
// Skills derive canonicalId from directory name — never from manifest
|
||||
if (manifest && manifest.__single && manifest.__single.canonicalId) {
|
||||
console.warn(
|
||||
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
|
||||
);
|
||||
}
|
||||
const canonicalId = path.basename(dir);
|
||||
if (skillMeta) {
|
||||
// Build path relative from module root (points to SKILL.md — the permanent entrypoint)
|
||||
const relativePath = path.relative(modulePath, dir).split(path.sep).join('/');
|
||||
const installPath = relativePath
|
||||
? `${this.bmadFolderName}/${moduleName}/${relativePath}/${skillFile}`
|
||||
: `${this.bmadFolderName}/${moduleName}/${skillFile}`;
|
||||
|
||||
this.skills.push({
|
||||
name: workflow.name,
|
||||
description: this.cleanForCSV(workflow.description),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
canonicalId,
|
||||
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
|
||||
});
|
||||
// Skills derive canonicalId from directory name — never from manifest
|
||||
if (manifest && manifest.__single && manifest.__single.canonicalId) {
|
||||
console.warn(
|
||||
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
|
||||
);
|
||||
}
|
||||
const canonicalId = dirName;
|
||||
|
||||
// Add to files list
|
||||
this.files.push({
|
||||
type: 'skill',
|
||||
name: workflow.name,
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
});
|
||||
this.skills.push({
|
||||
name: skillMeta.name,
|
||||
description: this.cleanForCSV(skillMeta.description),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
canonicalId,
|
||||
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
|
||||
});
|
||||
|
||||
this.skillClaimedDirs.add(dir);
|
||||
// Add to files list
|
||||
this.files.push({
|
||||
type: 'skill',
|
||||
name: skillMeta.name,
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`);
|
||||
this.skillClaimedDirs.add(dir);
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] collectSkills: claimed skill "${skillMeta.name}" as ${canonicalId} at ${dir}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if manifest says type:skill but no workflow file found
|
||||
// Warn if manifest says type:skill but directory was not claimed
|
||||
if (manifest && !this.skillClaimedDirs.has(dir)) {
|
||||
// Check if any entry in the manifest is type:skill
|
||||
let hasSkillType = false;
|
||||
if (manifest.__single) {
|
||||
hasSkillType = manifest.__single.type === 'skill';
|
||||
|
|
@ -250,12 +237,7 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
if (hasSkillType && debug) {
|
||||
const hasWorkflow = entries.some((e) => e.name === workflowFile);
|
||||
if (hasWorkflow) {
|
||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`);
|
||||
} else {
|
||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md: ${dir}`);
|
||||
}
|
||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but failed validation: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,6 +257,50 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate SKILL.md for a skill directory.
|
||||
* Returns parsed frontmatter object with name/description, or null if invalid.
|
||||
* @param {string} skillMdPath - Absolute path to SKILL.md
|
||||
* @param {string} dir - Skill directory path (for error messages)
|
||||
* @param {string} dirName - Expected name (must match frontmatter name)
|
||||
* @param {boolean} debug - Whether to emit debug-level messages
|
||||
* @returns {Promise<Object|null>} Parsed frontmatter or null
|
||||
*/
|
||||
async parseSkillMd(skillMdPath, dir, dirName, debug = false) {
|
||||
if (!(await fs.pathExists(skillMdPath))) {
|
||||
if (debug) console.log(`[DEBUG] parseSkillMd: "${dir}" is missing SKILL.md — skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const rawContent = await fs.readFile(skillMdPath, 'utf8');
|
||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (frontmatterMatch) {
|
||||
const skillMeta = yaml.parse(frontmatterMatch[1]);
|
||||
|
||||
if (!skillMeta || typeof skillMeta !== 'object' || !skillMeta.name || !skillMeta.description) {
|
||||
if (debug) console.log(`[DEBUG] parseSkillMd: SKILL.md in "${dir}" is missing name or description — skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (skillMeta.name !== dirName) {
|
||||
console.error(`Error: SKILL.md name "${skillMeta.name}" does not match directory name "${dirName}" — skipping`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return skillMeta;
|
||||
}
|
||||
|
||||
if (debug) console.log(`[DEBUG] parseSkillMd: SKILL.md in "${dir}" has no frontmatter — skipping`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (debug) console.log(`[DEBUG] parseSkillMd: failed to parse SKILL.md in "${dir}": ${error.message} — skipping`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all workflows from core and selected modules
|
||||
* Scans the INSTALLED bmad directory, not the source
|
||||
|
|
|
|||
|
|
@ -627,7 +627,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
|
||||
/**
|
||||
* Install verbatim skill directories (type: skill entries from skill-manifest.csv).
|
||||
* Copies the entire source directory into the IDE skill directory, auto-generating SKILL.md.
|
||||
* Copies the entire source directory as-is into the IDE skill directory.
|
||||
* The source SKILL.md is used directly — no frontmatter transformation or file generation.
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} targetPath - Target skills directory
|
||||
|
|
@ -653,7 +654,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
if (!canonicalId) continue;
|
||||
|
||||
// Derive source directory from path column
|
||||
// path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md"
|
||||
// path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md"
|
||||
// Strip bmadFolderName prefix and join with bmadDir, then get dirname
|
||||
const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), '');
|
||||
const sourceFile = path.join(bmadDir, relativePath);
|
||||
|
|
@ -666,30 +667,14 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
await fs.remove(skillDir);
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
// Parse workflow.md frontmatter for description
|
||||
let description = `${canonicalId} skill`;
|
||||
try {
|
||||
const workflowContent = await fs.readFile(sourceFile, 'utf8');
|
||||
const fmMatch = workflowContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (fmMatch) {
|
||||
const frontmatter = yaml.parse(fmMatch[1]);
|
||||
if (frontmatter?.description) {
|
||||
description = frontmatter.description;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Failed to parse frontmatter from ${sourceFile}: ${error.message}`);
|
||||
}
|
||||
|
||||
// Generate SKILL.md with YAML-safe frontmatter
|
||||
const frontmatterYaml = yaml.stringify({ name: canonicalId, description: String(description) }, { lineWidth: 0 }).trimEnd();
|
||||
const skillMd = `---\n${frontmatterYaml}\n---\n\nIT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL workflow.md, READ its entire contents and follow its directions exactly!\n`;
|
||||
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
|
||||
|
||||
// Copy all files except bmad-skill-manifest.yaml
|
||||
// Copy all skill files, filtering OS/editor artifacts
|
||||
const skipPatterns = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
|
||||
const skipSuffixes = ['~', '.swp', '.swo', '.bak'];
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'bmad-skill-manifest.yaml') continue;
|
||||
if (skipPatterns.has(entry.name)) continue;
|
||||
if (entry.name.startsWith('.') && entry.name !== '.gitkeep') continue;
|
||||
if (skipSuffixes.some((s) => entry.name.endsWith(s))) continue;
|
||||
const srcPath = path.join(sourceDir, entry.name);
|
||||
const destPath = path.join(skillDir, entry.name);
|
||||
await fs.copy(srcPath, destPath);
|
||||
|
|
|
|||
Loading…
Reference in New Issue