feat(skills): add type:skill manifest for verbatim skill directory copying
Introduce `type: skill` in bmad-skill-manifest.yaml to signal the installer to copy entire skill directories verbatim into IDE skill directories, replacing the launcher-based approach. Changes: - skill-manifest.js: fix single-entry detection for type-only manifests, add getArtifactType export - manifest-generator.js: collect type:skill entries separately, write skill-manifest.csv, derive canonicalId from directory name - _config-driven.js: add installVerbatimSkills with YAML-safe SKILL.md generation, stale file cleanup, and warning on parse failures - Rename quick-dev-new-preview to bmad-quick-dev-new-preview so directory name is the canonical ID - Update workflow.md installed_path to reference IDE skill base directory Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8e5898e862
commit
6da9e55ce5
|
|
@ -0,0 +1 @@
|
|||
type: skill
|
||||
|
|
@ -81,7 +81,7 @@ YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `
|
|||
|
||||
### 2. Paths
|
||||
|
||||
- `installed_path` = `{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview`
|
||||
- `installed_path` = the directory containing this workflow.md file (the skill's base directory)
|
||||
- `templateFile` = `{installed_path}/tech-spec-template.md`
|
||||
- `wipFile` = `{implementation_artifacts}/tech-spec-wip.md`
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-quick-dev-new-preview
|
||||
type: workflow
|
||||
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
||||
|
|
@ -5,7 +5,11 @@ const crypto = require('node:crypto');
|
|||
const csv = require('csv-parse/sync');
|
||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
|
||||
const {
|
||||
loadSkillManifest: loadSkillManifestShared,
|
||||
getCanonicalId: getCanonicalIdShared,
|
||||
getArtifactType: getArtifactTypeShared,
|
||||
} = require('../ide/shared/skill-manifest');
|
||||
|
||||
// Load package.json for version info
|
||||
const packageJson = require('../../../../../package.json');
|
||||
|
|
@ -16,6 +20,7 @@ const packageJson = require('../../../../../package.json');
|
|||
class ManifestGenerator {
|
||||
constructor() {
|
||||
this.workflows = [];
|
||||
this.skills = [];
|
||||
this.agents = [];
|
||||
this.tasks = [];
|
||||
this.tools = [];
|
||||
|
|
@ -34,6 +39,11 @@ class ManifestGenerator {
|
|||
return getCanonicalIdShared(manifest, filename);
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
getArtifactType(manifest, filename) {
|
||||
return getArtifactTypeShared(manifest, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text for CSV output by normalizing whitespace.
|
||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||
|
|
@ -105,6 +115,7 @@ class ManifestGenerator {
|
|||
const manifestFiles = [
|
||||
await this.writeMainManifest(cfgDir),
|
||||
await this.writeWorkflowManifest(cfgDir),
|
||||
await this.writeSkillManifest(cfgDir),
|
||||
await this.writeAgentManifest(cfgDir),
|
||||
await this.writeTaskManifest(cfgDir),
|
||||
await this.writeToolManifest(cfgDir),
|
||||
|
|
@ -228,6 +239,24 @@ class ManifestGenerator {
|
|||
? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}`
|
||||
: `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`;
|
||||
|
||||
// Check if this is a type:skill entry — collect separately, skip workflow CSV
|
||||
const artifactType = this.getArtifactType(skillManifest, entry.name);
|
||||
if (artifactType === 'skill') {
|
||||
const canonicalId = path.basename(dir);
|
||||
this.skills.push({
|
||||
name: workflow.name,
|
||||
description: this.cleanForCSV(workflow.description),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
canonicalId,
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
console.log(`[DEBUG] ✓ Added skill (skipped workflow CSV): ${workflow.name} as ${canonicalId}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Workflows with standalone: false are filtered out above
|
||||
workflows.push({
|
||||
name: workflow.name,
|
||||
|
|
@ -793,6 +822,31 @@ class ManifestGenerator {
|
|||
return csvPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write skill manifest CSV
|
||||
* @returns {string} Path to the manifest file
|
||||
*/
|
||||
async writeSkillManifest(cfgDir) {
|
||||
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
|
||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
|
||||
let csvContent = 'canonicalId,name,description,module,path\n';
|
||||
|
||||
for (const skill of this.skills) {
|
||||
const row = [
|
||||
escapeCsv(skill.canonicalId),
|
||||
escapeCsv(skill.name),
|
||||
escapeCsv(skill.description),
|
||||
escapeCsv(skill.module),
|
||||
escapeCsv(skill.path),
|
||||
].join(',');
|
||||
csvContent += row + '\n';
|
||||
}
|
||||
|
||||
await fs.writeFile(csvPath, csvContent);
|
||||
return csvPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write agent manifest CSV
|
||||
* @returns {string} Path to the manifest file
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const prompts = require('../../../lib/prompts');
|
|||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const csv = require('csv-parse/sync');
|
||||
|
||||
/**
|
||||
* Config-driven IDE setup handler
|
||||
|
|
@ -119,14 +120,14 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
// Skip targets with explicitly empty artifact_types array
|
||||
// This prevents creating empty directories when no artifacts will be written
|
||||
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
|
||||
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
|
||||
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } };
|
||||
}
|
||||
|
||||
const targetPath = path.join(projectDir, target_dir);
|
||||
await this.ensureDir(targetPath);
|
||||
|
||||
const selectedModules = options.selectedModules || [];
|
||||
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
||||
|
||||
// Install agents
|
||||
if (!artifact_types || artifact_types.includes('agents')) {
|
||||
|
|
@ -151,6 +152,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
results.tools = taskToolResult.tools || 0;
|
||||
}
|
||||
|
||||
// Install verbatim skills (type: skill)
|
||||
if (config.skill_format) {
|
||||
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
||||
}
|
||||
|
||||
await this.printSummary(results, target_dir, options);
|
||||
return { success: true, results };
|
||||
}
|
||||
|
|
@ -164,7 +170,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 };
|
||||
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
||||
|
||||
for (const target of targets) {
|
||||
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||
|
|
@ -173,6 +179,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
allResults.workflows += result.results.workflows || 0;
|
||||
allResults.tasks += result.results.tasks || 0;
|
||||
allResults.tools += result.results.tools || 0;
|
||||
allResults.skills += result.results.skills || 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -622,6 +629,82 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
return baseName.replace(/\.md$/, extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} targetPath - Target skills directory
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<number>} Count of skills installed
|
||||
*/
|
||||
async installVerbatimSkills(projectDir, bmadDir, targetPath, config) {
|
||||
const bmadFolderName = path.basename(bmadDir);
|
||||
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||
|
||||
if (!(await fs.pathExists(csvPath))) return 0;
|
||||
|
||||
const csvContent = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
for (const record of records) {
|
||||
const canonicalId = record.canonicalId;
|
||||
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"
|
||||
// Strip bmadFolderName prefix and join with bmadDir, then get dirname
|
||||
const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), '');
|
||||
const sourceFile = path.join(bmadDir, relativePath);
|
||||
const sourceDir = path.dirname(sourceFile);
|
||||
|
||||
if (!(await fs.pathExists(sourceDir))) continue;
|
||||
|
||||
// Clean target before copy to prevent stale files
|
||||
const skillDir = path.join(targetPath, canonicalId);
|
||||
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
|
||||
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'bmad-skill-manifest.yaml') continue;
|
||||
const srcPath = path.join(sourceDir, entry.name);
|
||||
const destPath = path.join(skillDir, entry.name);
|
||||
await fs.copy(srcPath, destPath);
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print installation summary
|
||||
* @param {Object} results - Installation results
|
||||
|
|
@ -634,6 +717,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
if (results.workflows > 0) parts.push(`${results.workflows} workflows`);
|
||||
if (results.tasks > 0) parts.push(`${results.tasks} tasks`);
|
||||
if (results.tools > 0) parts.push(`${results.tools} tools`);
|
||||
if (results.skills > 0) parts.push(`${results.skills} skills`);
|
||||
await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ async function loadSkillManifest(dirPath) {
|
|||
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 };
|
||||
if (parsed.canonicalId || parsed.type) return { __single: parsed };
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
|
||||
|
|
@ -45,4 +45,25 @@ function getCanonicalId(manifest, filename) {
|
|||
return '';
|
||||
}
|
||||
|
||||
module.exports = { loadSkillManifest, getCanonicalId };
|
||||
/**
|
||||
* Get the artifact type for a specific file from a loaded skill manifest.
|
||||
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
||||
* @param {string} filename - Source filename to look up
|
||||
* @returns {string|null} type or null
|
||||
*/
|
||||
function getArtifactType(manifest, filename) {
|
||||
if (!manifest) return null;
|
||||
// Single-entry manifest applies to all files in the directory
|
||||
if (manifest.__single) return manifest.__single.type || null;
|
||||
// Multi-entry: look up by filename directly
|
||||
if (manifest[filename]) return manifest[filename].type || null;
|
||||
// Fallback: try alternate extensions for compiled files
|
||||
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
||||
const agentKey = `${baseName}.agent.yaml`;
|
||||
if (manifest[agentKey]) return manifest[agentKey].type || null;
|
||||
const xmlKey = `${baseName}.xml`;
|
||||
if (manifest[xmlKey]) return manifest[xmlKey].type || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };
|
||||
|
|
|
|||
Loading…
Reference in New Issue