refactor(installer): remove legacy workflow, task, and agent IDE generators
All platforms now use skill_format exclusively. The old WorkflowCommandGenerator, TaskToolCommandGenerator, and AgentCommandGenerator code paths in _config-driven.js were no-ops — collectSkills claims every directory before the legacy collectors run, making their manifests empty. Removed: - workflow-command-generator.js (deleted) - task-tool-command-generator.js (deleted) - writeAgentArtifacts, writeWorkflowArtifacts, writeTaskToolArtifacts - AgentCommandGenerator import from _config-driven.js - Legacy artifact_types/agents/workflows/tasks result fields Simplified installToTarget, installToMultipleTargets, printSummary, and IDE manager detail builder to skills-only. Updated test fixture to use SKILL.md format instead of old agent format.
This commit is contained in:
parent
182550407c
commit
c23b2db27a
|
|
@ -49,34 +49,37 @@ function assert(condition, testName, errorMessage = '') {
|
|||
}
|
||||
|
||||
async function createTestBmadFixture() {
|
||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
||||
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
||||
const fixtureDir = path.join(fixtureRoot, '_bmad');
|
||||
await fs.ensureDir(fixtureDir);
|
||||
|
||||
// Minimal workflow manifest (generators check for this)
|
||||
// Skill manifest CSV — the sole source of truth for IDE skill installation
|
||||
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
||||
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
|
||||
await fs.writeFile(
|
||||
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
|
||||
[
|
||||
'canonicalId,name,description,module,path,install_to_bmad',
|
||||
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
|
||||
const minimalAgent = [
|
||||
// Minimal SKILL.md for the skill entry
|
||||
const skillDir = path.join(fixtureDir, 'core', 'bmad-master');
|
||||
await fs.ensureDir(skillDir);
|
||||
await fs.writeFile(
|
||||
path.join(skillDir, 'SKILL.md'),
|
||||
[
|
||||
'---',
|
||||
'name: "test agent"',
|
||||
'description: "Minimal test agent fixture"',
|
||||
'name: bmad-master',
|
||||
'description: Minimal test agent fixture',
|
||||
'---',
|
||||
'',
|
||||
'<!-- agent-activation -->',
|
||||
'You are a test agent.',
|
||||
'',
|
||||
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
|
||||
'<persona>Test persona</persona>',
|
||||
'</agent>',
|
||||
].join('\n');
|
||||
|
||||
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
|
||||
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
|
||||
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
|
||||
|
||||
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
|
||||
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
||||
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
|
||||
].join('\n'),
|
||||
);
|
||||
await fs.writeFile(path.join(skillDir, 'bmad-skill-manifest.yaml'), 'SKILL.md:\n type: skill\n');
|
||||
|
||||
return fixtureDir;
|
||||
}
|
||||
|
|
@ -1837,18 +1840,12 @@ async function runTests() {
|
|||
});
|
||||
|
||||
assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names');
|
||||
assert(result.detail === '2 agents', 'Installer detail reports agents separately from skills');
|
||||
assert(result.handlerResult.results.skillDirectories === 2, 'Result exposes unique skill directory count');
|
||||
assert(result.handlerResult.results.agents === 2, 'Result retains generated agent write count');
|
||||
assert(result.handlerResult.results.workflows === 1, 'Result retains generated workflow count');
|
||||
assert(result.detail === '1 skills', 'Installer detail reports skill count');
|
||||
assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count');
|
||||
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
||||
assert(
|
||||
await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-agent-bmad-master', 'SKILL.md')),
|
||||
'Agent skill directory is created',
|
||||
);
|
||||
assert(
|
||||
await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-help', 'SKILL.md')),
|
||||
'Overlapping skill directory is created once',
|
||||
'Skill directory is created from skill-manifest',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Skill-format unique count test succeeds', error.message);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ const fs = require('fs-extra');
|
|||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
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');
|
||||
|
||||
/**
|
||||
|
|
@ -115,53 +112,20 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async installToTarget(projectDir, bmadDir, config, options) {
|
||||
const { target_dir, template_type, artifact_types } = config;
|
||||
const { target_dir } = config;
|
||||
|
||||
// Skip targets with explicitly empty artifact_types and no verbatim skills
|
||||
// This prevents creating empty directories when no artifacts will be written
|
||||
const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0;
|
||||
if (skipStandardArtifacts && !config.skill_format) {
|
||||
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } };
|
||||
if (!config.skill_format) {
|
||||
return { success: true, results: { 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, skills: 0 };
|
||||
this.skillWriteTracker = config.skill_format ? new Set() : null;
|
||||
this.skillWriteTracker = new Set();
|
||||
const results = { skills: 0 };
|
||||
|
||||
// Install standard artifacts (agents, workflows, tasks, tools)
|
||||
if (!skipStandardArtifacts) {
|
||||
// Install agents
|
||||
if (!artifact_types || artifact_types.includes('agents')) {
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config);
|
||||
}
|
||||
|
||||
// Install workflows
|
||||
if (!artifact_types || artifact_types.includes('workflows')) {
|
||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
||||
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
||||
}
|
||||
|
||||
// Install tasks and tools using template system (supports TOML for Gemini, MD for others)
|
||||
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
||||
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
|
||||
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
|
||||
results.tasks = taskToolResult.tasks || 0;
|
||||
results.tools = taskToolResult.tools || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Install verbatim skills (type: skill)
|
||||
if (config.skill_format) {
|
||||
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
||||
results.skillDirectories = this.skillWriteTracker ? this.skillWriteTracker.size : 0;
|
||||
}
|
||||
results.skillDirectories = this.skillWriteTracker.size;
|
||||
|
||||
await this.printSummary(results, target_dir, options);
|
||||
this.skillWriteTracker = null;
|
||||
|
|
@ -177,15 +141,11 @@ 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, skills: 0 };
|
||||
const allResults = { skills: 0 };
|
||||
|
||||
for (const target of targets) {
|
||||
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||
if (result.success) {
|
||||
allResults.agents += result.results.agents || 0;
|
||||
allResults.workflows += result.results.workflows || 0;
|
||||
allResults.tasks += result.results.tasks || 0;
|
||||
allResults.tools += result.results.tools || 0;
|
||||
allResults.skills += result.results.skills || 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -193,118 +153,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
return { success: true, results: allResults };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write agent artifacts to target directory
|
||||
* @param {string} targetPath - Target directory path
|
||||
* @param {Array} artifacts - Agent artifacts
|
||||
* @param {string} templateType - Template type to use
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<number>} Count of artifacts written
|
||||
*/
|
||||
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
// Try to load platform-specific template, fall back to default-agent
|
||||
const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||
let count = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write workflow artifacts to target directory
|
||||
* @param {string} targetPath - Target directory path
|
||||
* @param {Array} artifacts - Workflow artifacts
|
||||
* @param {string} templateType - Template type to use
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<number>} Count of artifacts written
|
||||
*/
|
||||
async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
let count = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'workflow-command') {
|
||||
const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`;
|
||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, 'default-workflow');
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write task/tool artifacts to target directory using templates
|
||||
* @param {string} targetPath - Target directory path
|
||||
* @param {Array} artifacts - Task/tool artifacts
|
||||
* @param {string} templateType - Template type to use
|
||||
* @param {Object} config - Installation configuration
|
||||
* @returns {Promise<Object>} Counts of tasks and tools written
|
||||
*/
|
||||
async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
let taskCount = 0;
|
||||
let toolCount = 0;
|
||||
|
||||
// Pre-load templates to avoid repeated file I/O in the loop
|
||||
const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task');
|
||||
const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool');
|
||||
|
||||
const { artifact_types } = config;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type !== 'task' && artifact.type !== 'tool') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if the specific artifact type is not requested in config
|
||||
if (artifact_types) {
|
||||
if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue;
|
||||
if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue;
|
||||
}
|
||||
|
||||
// Use pre-loaded template based on artifact type
|
||||
const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate;
|
||||
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, artifact.type, extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
|
||||
if (artifact.type === 'task') {
|
||||
taskCount++;
|
||||
} else {
|
||||
toolCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return { tasks: taskCount, tools: toolCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load template based on type and configuration
|
||||
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||
|
|
@ -711,13 +559,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
*/
|
||||
async printSummary(results, targetDir, options = {}) {
|
||||
if (options.silent) return;
|
||||
const parts = [];
|
||||
const totalDirs =
|
||||
results.skillDirectories || (results.workflows || 0) + (results.tasks || 0) + (results.tools || 0) + (results.skills || 0);
|
||||
const skillCount = totalDirs - (results.agents || 0);
|
||||
if (skillCount > 0) parts.push(`${skillCount} skills`);
|
||||
if (results.agents > 0) parts.push(`${results.agents} agents`);
|
||||
await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`);
|
||||
const count = results.skillDirectories || results.skills || 0;
|
||||
if (count > 0) {
|
||||
await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -159,14 +159,9 @@ class IdeManager {
|
|||
// Build detail string from handler-returned data
|
||||
let detail = '';
|
||||
if (handlerResult && handlerResult.results) {
|
||||
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
|
||||
const r = handlerResult.results;
|
||||
const parts = [];
|
||||
const totalDirs = r.skillDirectories || (r.workflows || 0) + (r.tasks || 0) + (r.tools || 0) + (r.skills || 0);
|
||||
const skillCount = totalDirs - (r.agents || 0);
|
||||
if (skillCount > 0) parts.push(`${skillCount} skills`);
|
||||
if (r.agents > 0) parts.push(`${r.agents} agents`);
|
||||
detail = parts.join(', ');
|
||||
const count = r.skillDirectories || r.skills || 0;
|
||||
if (count > 0) detail = `${count} skills`;
|
||||
}
|
||||
// Propagate handler's success status (default true for backward compat)
|
||||
const success = handlerResult?.success !== false;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD
|
|||
|
||||
/**
|
||||
* Generates launcher command files for each agent
|
||||
* Similar to WorkflowCommandGenerator but for agents
|
||||
*/
|
||||
class AgentCommandGenerator {
|
||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||
|
|
|
|||
|
|
@ -1,368 +0,0 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* Generates command files for standalone tasks and tools
|
||||
*/
|
||||
class TaskToolCommandGenerator {
|
||||
/**
|
||||
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad')
|
||||
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
|
||||
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
|
||||
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
|
||||
* used for template placeholder rendering ({{bmadFolderName}}).
|
||||
*/
|
||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect task and tool artifacts for IDE installation
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @returns {Promise<Object>} Artifacts array with metadata
|
||||
*/
|
||||
async collectTaskToolArtifacts(bmadDir) {
|
||||
const tasks = await this.loadTaskManifest(bmadDir);
|
||||
const tools = await this.loadToolManifest(bmadDir);
|
||||
|
||||
// All tasks/tools in manifest are standalone (internal=true items are filtered during manifest generation)
|
||||
const artifacts = [];
|
||||
const bmadPrefix = `${BMAD_FOLDER_NAME}/`;
|
||||
|
||||
// Collect task artifacts
|
||||
for (const task of tasks || []) {
|
||||
let taskPath = (task.path || '').replaceAll('\\', '/');
|
||||
// Convert absolute paths to relative paths
|
||||
if (path.isAbsolute(taskPath)) {
|
||||
taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/');
|
||||
}
|
||||
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
||||
if (taskPath.startsWith(bmadPrefix)) {
|
||||
taskPath = taskPath.slice(bmadPrefix.length);
|
||||
}
|
||||
|
||||
const taskExt = path.extname(taskPath) || '.md';
|
||||
artifacts.push({
|
||||
type: 'task',
|
||||
name: task.name,
|
||||
displayName: task.displayName || task.name,
|
||||
description: task.description || `Execute ${task.displayName || task.name}`,
|
||||
module: task.module,
|
||||
canonicalId: task.canonicalId || '',
|
||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
|
||||
path: taskPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Collect tool artifacts
|
||||
for (const tool of tools || []) {
|
||||
let toolPath = (tool.path || '').replaceAll('\\', '/');
|
||||
// Convert absolute paths to relative paths
|
||||
if (path.isAbsolute(toolPath)) {
|
||||
toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/');
|
||||
}
|
||||
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
||||
if (toolPath.startsWith(bmadPrefix)) {
|
||||
toolPath = toolPath.slice(bmadPrefix.length);
|
||||
}
|
||||
|
||||
const toolExt = path.extname(toolPath) || '.md';
|
||||
artifacts.push({
|
||||
type: 'tool',
|
||||
name: tool.name,
|
||||
displayName: tool.displayName || tool.name,
|
||||
description: tool.description || `Execute ${tool.displayName || tool.name}`,
|
||||
module: tool.module,
|
||||
canonicalId: tool.canonicalId || '',
|
||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
|
||||
path: toolPath,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
counts: {
|
||||
tasks: (tasks || []).length,
|
||||
tools: (tools || []).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task and tool commands from manifest CSVs
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad)
|
||||
*/
|
||||
async generateTaskToolCommands(projectDir, bmadDir, baseCommandsDir = null) {
|
||||
const tasks = await this.loadTaskManifest(bmadDir);
|
||||
const tools = await this.loadToolManifest(bmadDir);
|
||||
|
||||
// Base commands directory - use provided or default to Claude Code structure
|
||||
const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad');
|
||||
|
||||
let generatedCount = 0;
|
||||
|
||||
// Generate command files for tasks
|
||||
for (const task of tasks || []) {
|
||||
const moduleTasksDir = path.join(commandsDir, task.module, 'tasks');
|
||||
await fs.ensureDir(moduleTasksDir);
|
||||
|
||||
const commandContent = this.generateCommandContent(task, 'task');
|
||||
const commandPath = path.join(moduleTasksDir, `${task.name}.md`);
|
||||
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
// Generate command files for tools
|
||||
for (const tool of tools || []) {
|
||||
const moduleToolsDir = path.join(commandsDir, tool.module, 'tools');
|
||||
await fs.ensureDir(moduleToolsDir);
|
||||
|
||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
||||
const commandPath = path.join(moduleToolsDir, `${tool.name}.md`);
|
||||
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
generated: generatedCount,
|
||||
tasks: (tasks || []).length,
|
||||
tools: (tools || []).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate command content for a task or tool
|
||||
*/
|
||||
generateCommandContent(item, type) {
|
||||
const description = item.description || `Execute ${item.displayName || item.name}`;
|
||||
|
||||
// Convert path to use {project-root} placeholder
|
||||
// Handle undefined/missing path by constructing from module and name
|
||||
let itemPath = item.path;
|
||||
if (!itemPath || typeof itemPath !== 'string') {
|
||||
// Fallback: construct path from module and name if path is missing
|
||||
const typePlural = type === 'task' ? 'tasks' : 'tools';
|
||||
itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`;
|
||||
} else {
|
||||
// Normalize path separators to forward slashes
|
||||
itemPath = itemPath.replaceAll('\\', '/');
|
||||
|
||||
// Extract relative path from absolute paths (Windows or Unix)
|
||||
// Look for _bmad/ or bmad/ in the path and extract everything after it
|
||||
// Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/...
|
||||
// Use [/\\] to handle both Unix forward slashes and Windows backslashes,
|
||||
// and also paths without a leading separator (e.g., C:/_bmad/...)
|
||||
const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/);
|
||||
if (bmadMatch) {
|
||||
// Found /_bmad/ or /bmad/ - use relative path after it
|
||||
itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`;
|
||||
} else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) {
|
||||
// Relative path starting with _bmad/
|
||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`;
|
||||
} else if (itemPath.startsWith('bmad/')) {
|
||||
// Relative path starting with bmad/
|
||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`;
|
||||
} else if (!itemPath.startsWith('{project-root}')) {
|
||||
// For other relative paths, prefix with project root and bmad folder
|
||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `---
|
||||
description: '${description.replaceAll("'", "''")}'
|
||||
---
|
||||
|
||||
# ${item.displayName || item.name}
|
||||
|
||||
Read the entire ${type} file at: ${itemPath}
|
||||
|
||||
Follow all instructions in the ${type} file exactly as written.
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load task manifest CSV
|
||||
*/
|
||||
async loadTaskManifest(bmadDir) {
|
||||
const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv');
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
return csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load tool manifest CSV
|
||||
*/
|
||||
async loadToolManifest(bmadDir) {
|
||||
const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv');
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
return csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task and tool commands using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_help.md
|
||||
*
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @returns {Object} Generation results
|
||||
*/
|
||||
async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
|
||||
const tasks = await this.loadTaskManifest(bmadDir);
|
||||
const tools = await this.loadToolManifest(bmadDir);
|
||||
|
||||
let generatedCount = 0;
|
||||
|
||||
// Generate command files for tasks
|
||||
for (const task of tasks || []) {
|
||||
const commandContent = this.generateCommandContent(task, 'task');
|
||||
// Use underscore format: bmad_bmm_name.md
|
||||
const flatName = toColonName(task.module, 'tasks', task.name);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
// Generate command files for tools
|
||||
for (const tool of tools || []) {
|
||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
||||
// Use underscore format: bmad_bmm_name.md
|
||||
const flatName = toColonName(tool.module, 'tools', tool.name);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
generated: generatedCount,
|
||||
tasks: (tasks || []).length,
|
||||
tools: (tools || []).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate task and tool commands using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_help.md
|
||||
*
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @returns {Object} Generation results
|
||||
*/
|
||||
async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
|
||||
const tasks = await this.loadTaskManifest(bmadDir);
|
||||
const tools = await this.loadToolManifest(bmadDir);
|
||||
|
||||
let generatedCount = 0;
|
||||
|
||||
// Generate command files for tasks
|
||||
for (const task of tasks || []) {
|
||||
const commandContent = this.generateCommandContent(task, 'task');
|
||||
// Use dash format: bmad-bmm-name.md
|
||||
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
// Generate command files for tools
|
||||
for (const tool of tools || []) {
|
||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
||||
// Use dash format: bmad-bmm-name.md
|
||||
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
generatedCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
generated: generatedCount,
|
||||
tasks: (tasks || []).length,
|
||||
tools: (tools || []).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write task/tool artifacts using underscore format (Windows-compatible)
|
||||
* Creates flat files like: bmad_bmm_help.md
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
||||
* @returns {number} Count of commands written
|
||||
*/
|
||||
async writeColonArtifacts(baseCommandsDir, artifacts) {
|
||||
let writtenCount = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
||||
// Use underscore format: bmad_module_name.md
|
||||
const flatName = toColonPath(artifact.relativePath);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
writtenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return writtenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write task/tool artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-help.md
|
||||
*
|
||||
* Note: Tasks/tools do NOT have bmad-agent- prefix - only agents do.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
||||
* @returns {number} Count of commands written
|
||||
*/
|
||||
async writeDashArtifacts(baseCommandsDir, artifacts) {
|
||||
let writtenCount = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
||||
// Use dash format: bmad-module-name.md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const commandPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(commandPath));
|
||||
await fs.writeFile(commandPath, commandContent);
|
||||
writtenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return writtenCount;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TaskToolCommandGenerator };
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { BMAD_FOLDER_NAME } = require('./path-utils');
|
||||
|
||||
/**
|
||||
* Generates command files for each workflow in the manifest
|
||||
*/
|
||||
class WorkflowCommandGenerator {
|
||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||
this.bmadFolderName = bmadFolderName;
|
||||
}
|
||||
|
||||
async collectWorkflowArtifacts(bmadDir) {
|
||||
const workflows = await this.loadWorkflowManifest(bmadDir);
|
||||
|
||||
if (!workflows) {
|
||||
return { artifacts: [], counts: { commands: 0, launchers: 0 } };
|
||||
}
|
||||
|
||||
// ALL workflows now generate commands - no standalone filtering
|
||||
const allWorkflows = workflows;
|
||||
|
||||
const artifacts = [];
|
||||
|
||||
for (const workflow of allWorkflows) {
|
||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
|
||||
let workflowRelPath = workflow.path || '';
|
||||
// Normalize path separators for cross-platform compatibility
|
||||
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
||||
// Remove _bmad/ prefix if present to get relative path from project root
|
||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||
if (workflowRelPath.includes('_bmad/')) {
|
||||
const parts = workflowRelPath.split(/_bmad\//);
|
||||
if (parts.length > 1) {
|
||||
workflowRelPath = parts.slice(1).join('/');
|
||||
}
|
||||
} else if (workflowRelPath.includes('/src/')) {
|
||||
// Normalize source paths (e.g. .../src/bmm/...) to relative module path (e.g. bmm/...)
|
||||
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
|
||||
if (match) {
|
||||
workflowRelPath = `${match[1]}/${match[2]}`;
|
||||
}
|
||||
}
|
||||
artifacts.push({
|
||||
type: 'workflow-command',
|
||||
name: workflow.name,
|
||||
description: workflow.description || `${workflow.name} workflow`,
|
||||
module: workflow.module,
|
||||
canonicalId: workflow.canonicalId || '',
|
||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
||||
sourcePath: workflow.path,
|
||||
});
|
||||
}
|
||||
|
||||
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
|
||||
for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) {
|
||||
artifacts.push({
|
||||
type: 'workflow-launcher',
|
||||
module,
|
||||
relativePath: path.join(module, 'workflows', 'README.md'),
|
||||
content: launcherContent,
|
||||
sourcePath: null,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
counts: {
|
||||
commands: allWorkflows.length,
|
||||
launchers: Object.keys(groupedWorkflows).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow launcher files for each module
|
||||
*/
|
||||
async createModuleWorkflowLaunchers(baseCommandsDir, workflowsByModule) {
|
||||
for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) {
|
||||
const content = this.buildLauncherContent(module, moduleWorkflows);
|
||||
const moduleWorkflowsDir = path.join(baseCommandsDir, module, 'workflows');
|
||||
await fs.ensureDir(moduleWorkflowsDir);
|
||||
const launcherPath = path.join(moduleWorkflowsDir, 'README.md');
|
||||
await fs.writeFile(launcherPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
groupWorkflowsByModule(workflows) {
|
||||
const workflowsByModule = {};
|
||||
|
||||
for (const workflow of workflows) {
|
||||
if (!workflowsByModule[workflow.module]) {
|
||||
workflowsByModule[workflow.module] = [];
|
||||
}
|
||||
|
||||
workflowsByModule[workflow.module].push({
|
||||
...workflow,
|
||||
displayPath: this.transformWorkflowPath(workflow.path),
|
||||
});
|
||||
}
|
||||
|
||||
return workflowsByModule;
|
||||
}
|
||||
|
||||
buildModuleWorkflowLaunchers(groupedWorkflows) {
|
||||
const launchers = {};
|
||||
|
||||
for (const [module, moduleWorkflows] of Object.entries(groupedWorkflows)) {
|
||||
launchers[module] = this.buildLauncherContent(module, moduleWorkflows);
|
||||
}
|
||||
|
||||
return launchers;
|
||||
}
|
||||
|
||||
buildLauncherContent(module, moduleWorkflows) {
|
||||
let content = `# ${module.toUpperCase()} Workflows
|
||||
|
||||
## Available Workflows in ${module}
|
||||
|
||||
`;
|
||||
|
||||
for (const workflow of moduleWorkflows) {
|
||||
content += `**${workflow.name}**\n`;
|
||||
content += `- Path: \`${workflow.displayPath}\`\n`;
|
||||
content += `- ${workflow.description}\n\n`;
|
||||
}
|
||||
|
||||
content += `
|
||||
## Execution
|
||||
|
||||
When running any workflow:
|
||||
1. LOAD the workflow.md file at the path shown above
|
||||
2. READ its entire contents and follow its directions exactly
|
||||
3. Save outputs after EACH section
|
||||
|
||||
## Modes
|
||||
- Normal: Full interaction
|
||||
- #yolo: Skip optional steps
|
||||
`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
transformWorkflowPath(workflowPath) {
|
||||
let transformed = workflowPath;
|
||||
|
||||
if (workflowPath.includes('/src/bmm-skills/')) {
|
||||
const match = workflowPath.match(/\/src\/bmm-skills\/(.+)/);
|
||||
if (match) {
|
||||
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
|
||||
}
|
||||
} else if (workflowPath.includes('/src/core-skills/')) {
|
||||
const match = workflowPath.match(/\/src\/core-skills\/(.+)/);
|
||||
if (match) {
|
||||
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
async loadWorkflowManifest(bmadDir) {
|
||||
const manifestPath = path.join(bmadDir, '_config', 'workflow-manifest.csv');
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||
return csv.parse(csvContent, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { WorkflowCommandGenerator };
|
||||
Loading…
Reference in New Issue