refactor: remove YAML workflow code paths from CLI installer pipeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ceacf7dc1
commit
73d19baf7e
|
|
@ -148,7 +148,7 @@ class ManifestGenerator {
|
||||||
/**
|
/**
|
||||||
* Recursively walk a module directory tree, collecting skill directories.
|
* Recursively walk a module directory tree, collecting skill directories.
|
||||||
* A skill directory is one that contains both a bmad-skill-manifest.yaml with
|
* A skill directory is one that contains both a bmad-skill-manifest.yaml with
|
||||||
* type: skill AND a workflow.md (or workflow.yaml) file.
|
* type: skill AND a workflow.md file.
|
||||||
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
|
||||||
*/
|
*/
|
||||||
async collectSkills() {
|
async collectSkills() {
|
||||||
|
|
@ -172,76 +172,66 @@ class ManifestGenerator {
|
||||||
// Check this directory for skill manifest + workflow file
|
// Check this directory for skill manifest + workflow file
|
||||||
const manifest = await this.loadSkillManifest(dir);
|
const manifest = await this.loadSkillManifest(dir);
|
||||||
|
|
||||||
// Try both workflow.md and workflow.yaml
|
const workflowFile = 'workflow.md';
|
||||||
const workflowFilenames = ['workflow.md', 'workflow.yaml'];
|
const workflowPath = path.join(dir, workflowFile);
|
||||||
for (const workflowFile of workflowFilenames) {
|
if (await fs.pathExists(workflowPath)) {
|
||||||
const workflowPath = path.join(dir, workflowFile);
|
|
||||||
if (!(await fs.pathExists(workflowPath))) continue;
|
|
||||||
|
|
||||||
const artifactType = this.getArtifactType(manifest, workflowFile);
|
const artifactType = this.getArtifactType(manifest, workflowFile);
|
||||||
if (artifactType !== 'skill') continue;
|
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');
|
||||||
|
|
||||||
// Read and parse the workflow file
|
|
||||||
try {
|
|
||||||
const rawContent = await fs.readFile(workflowPath, 'utf8');
|
|
||||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
||||||
|
|
||||||
let workflow;
|
|
||||||
if (workflowFile === 'workflow.yaml') {
|
|
||||||
workflow = yaml.parse(content);
|
|
||||||
} else {
|
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (!frontmatterMatch) {
|
if (!frontmatterMatch) {
|
||||||
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
|
if (debug) console.log(`[DEBUG] collectSkills: skipped (no frontmatter): ${workflowPath}`);
|
||||||
continue;
|
} else {
|
||||||
|
const workflow = yaml.parse(frontmatterMatch[1]);
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
this.skills.push({
|
||||||
|
name: workflow.name,
|
||||||
|
description: this.cleanForCSV(workflow.description),
|
||||||
|
module: moduleName,
|
||||||
|
path: installPath,
|
||||||
|
canonicalId,
|
||||||
|
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to files list
|
||||||
|
this.files.push({
|
||||||
|
type: 'skill',
|
||||||
|
name: workflow.name,
|
||||||
|
module: moduleName,
|
||||||
|
path: installPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.skillClaimedDirs.add(dir);
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
workflow = yaml.parse(frontmatterMatch[1]);
|
} catch (error) {
|
||||||
|
if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workflow || !workflow.name || !workflow.description) {
|
|
||||||
if (debug) console.log(`[DEBUG] collectSkills: skipped (missing name/description): ${workflowPath}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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}`;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
this.skills.push({
|
|
||||||
name: workflow.name,
|
|
||||||
description: this.cleanForCSV(workflow.description),
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
canonicalId,
|
|
||||||
install_to_bmad: this.getInstallToBmad(manifest, workflowFile),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to files list
|
|
||||||
this.files.push({
|
|
||||||
type: 'skill',
|
|
||||||
name: workflow.name,
|
|
||||||
module: moduleName,
|
|
||||||
path: installPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.skillClaimedDirs.add(dir);
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] collectSkills: claimed skill "${workflow.name}" as ${canonicalId} at ${dir}`);
|
|
||||||
}
|
|
||||||
break; // Successfully claimed — skip remaining workflow filenames
|
|
||||||
} catch (error) {
|
|
||||||
if (debug) console.log(`[DEBUG] collectSkills: failed to parse ${workflowPath}: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,11 +250,11 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasSkillType && debug) {
|
if (hasSkillType && debug) {
|
||||||
const hasWorkflow = workflowFilenames.some((f) => entries.some((e) => e.name === f));
|
const hasWorkflow = entries.some((e) => e.name === workflowFile);
|
||||||
if (hasWorkflow) {
|
if (hasWorkflow) {
|
||||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`);
|
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but workflow file failed to parse: ${dir}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md/workflow.yaml: ${dir}`);
|
console.log(`[DEBUG] collectSkills: dir has type:skill manifest but no workflow.md: ${dir}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -308,7 +298,7 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find and parse workflow.yaml and workflow.md files
|
* Recursively find and parse workflow.md files
|
||||||
*/
|
*/
|
||||||
async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') {
|
async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') {
|
||||||
const workflows = [];
|
const workflows = [];
|
||||||
|
|
@ -326,7 +316,7 @@ class ManifestGenerator {
|
||||||
return workflows;
|
return workflows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively find workflow.yaml files
|
// Recursively find workflow.md files
|
||||||
const findWorkflows = async (dir, relativePath = '') => {
|
const findWorkflows = async (dir, relativePath = '') => {
|
||||||
// Skip directories already claimed as skills
|
// Skip directories already claimed as skills
|
||||||
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return;
|
if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return;
|
||||||
|
|
@ -345,7 +335,6 @@ class ManifestGenerator {
|
||||||
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||||
await findWorkflows(fullPath, newRelativePath);
|
await findWorkflows(fullPath, newRelativePath);
|
||||||
} else if (
|
} else if (
|
||||||
entry.name === 'workflow.yaml' ||
|
|
||||||
entry.name === 'workflow.md' ||
|
entry.name === 'workflow.md' ||
|
||||||
(entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))
|
(entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))
|
||||||
) {
|
) {
|
||||||
|
|
@ -358,21 +347,15 @@ class ManifestGenerator {
|
||||||
const rawContent = await fs.readFile(fullPath, 'utf8');
|
const rawContent = await fs.readFile(fullPath, 'utf8');
|
||||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||||
|
|
||||||
let workflow;
|
// Parse MD workflow with YAML frontmatter
|
||||||
if (entry.name === 'workflow.yaml') {
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
// Parse YAML workflow
|
if (!frontmatterMatch) {
|
||||||
workflow = yaml.parse(content);
|
if (debug) {
|
||||||
} else {
|
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
||||||
// Parse MD workflow with YAML frontmatter
|
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
||||||
if (!frontmatterMatch) {
|
|
||||||
if (debug) {
|
|
||||||
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
|
|
||||||
}
|
|
||||||
continue; // Skip MD files without frontmatter
|
|
||||||
}
|
}
|
||||||
workflow = yaml.parse(frontmatterMatch[1]);
|
continue; // Skip MD files without frontmatter
|
||||||
}
|
}
|
||||||
|
const workflow = yaml.parse(frontmatterMatch[1]);
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`);
|
console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`);
|
||||||
|
|
@ -1343,7 +1326,7 @@ class ManifestGenerator {
|
||||||
// Check for manifest in this directory
|
// Check for manifest in this directory
|
||||||
const manifest = await this.loadSkillManifest(dir);
|
const manifest = await this.loadSkillManifest(dir);
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
const type = this.getArtifactType(manifest, 'workflow.md') || this.getArtifactType(manifest, 'workflow.yaml');
|
const type = this.getArtifactType(manifest, 'workflow.md');
|
||||||
if (type === 'skill') return true;
|
if (type === 'skill') return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ class BaseIdeSetup {
|
||||||
// Get core workflows
|
// Get core workflows
|
||||||
const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows');
|
const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows');
|
||||||
if (await fs.pathExists(coreWorkflowsPath)) {
|
if (await fs.pathExists(coreWorkflowsPath)) {
|
||||||
const coreWorkflows = await this.findWorkflowYamlFiles(coreWorkflowsPath);
|
const coreWorkflows = await this.findWorkflowFiles(coreWorkflowsPath);
|
||||||
workflows.push(
|
workflows.push(
|
||||||
...coreWorkflows.map((w) => ({
|
...coreWorkflows.map((w) => ({
|
||||||
...w,
|
...w,
|
||||||
|
|
@ -304,7 +304,7 @@ class BaseIdeSetup {
|
||||||
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
|
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_config' && entry.name !== 'agents') {
|
||||||
const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows');
|
const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows');
|
||||||
if (await fs.pathExists(moduleWorkflowsPath)) {
|
if (await fs.pathExists(moduleWorkflowsPath)) {
|
||||||
const moduleWorkflows = await this.findWorkflowYamlFiles(moduleWorkflowsPath);
|
const moduleWorkflows = await this.findWorkflowFiles(moduleWorkflowsPath);
|
||||||
workflows.push(
|
workflows.push(
|
||||||
...moduleWorkflows.map((w) => ({
|
...moduleWorkflows.map((w) => ({
|
||||||
...w,
|
...w,
|
||||||
|
|
@ -324,11 +324,11 @@ class BaseIdeSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find workflow.yaml files
|
* Recursively find workflow.md files
|
||||||
* @param {string} dir - Directory to search
|
* @param {string} dir - Directory to search
|
||||||
* @returns {Array} List of workflow file info objects
|
* @returns {Array} List of workflow file info objects
|
||||||
*/
|
*/
|
||||||
async findWorkflowYamlFiles(dir) {
|
async findWorkflowFiles(dir) {
|
||||||
const workflows = [];
|
const workflows = [];
|
||||||
|
|
||||||
if (!(await fs.pathExists(dir))) {
|
if (!(await fs.pathExists(dir))) {
|
||||||
|
|
@ -342,14 +342,17 @@ class BaseIdeSetup {
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
// Recursively search subdirectories
|
// Recursively search subdirectories
|
||||||
const subWorkflows = await this.findWorkflowYamlFiles(fullPath);
|
const subWorkflows = await this.findWorkflowFiles(fullPath);
|
||||||
workflows.push(...subWorkflows);
|
workflows.push(...subWorkflows);
|
||||||
} else if (entry.isFile() && entry.name === 'workflow.yaml') {
|
} else if (entry.isFile() && entry.name === 'workflow.md') {
|
||||||
// Read workflow.yaml to get name and standalone property
|
// Read workflow.md frontmatter to get name and standalone property
|
||||||
try {
|
try {
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const content = await fs.readFile(fullPath, 'utf8');
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
const workflowData = yaml.parse(content);
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
|
if (!frontmatterMatch) continue;
|
||||||
|
|
||||||
|
const workflowData = yaml.parse(frontmatterMatch[1]);
|
||||||
|
|
||||||
if (workflowData && workflowData.name) {
|
if (workflowData && workflowData.name) {
|
||||||
// Workflows are standalone by default unless explicitly false
|
// Workflows are standalone by default unless explicitly false
|
||||||
|
|
|
||||||
|
|
@ -232,16 +232,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
if (artifact.type === 'workflow-command') {
|
if (artifact.type === 'workflow-command') {
|
||||||
// Use different template based on workflow type (YAML vs MD)
|
const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`;
|
||||||
// Default to 'default' template type, but allow override via config
|
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, 'default-workflow');
|
||||||
const workflowTemplateType = artifact.isYamlWorkflow
|
|
||||||
? config.yaml_workflow_template || `${templateType}-workflow-yaml`
|
|
||||||
: config.md_workflow_template || `${templateType}-workflow`;
|
|
||||||
|
|
||||||
// Fall back to default templates if specific ones don't exist
|
|
||||||
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
|
|
||||||
// workflowTemplateType already contains full name (e.g., 'gemini-workflow-yaml'), so pass empty artifactType
|
|
||||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
const filename = this.generateFilename(artifact, 'workflow', extension);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ function toDashPath(relativePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip common file extensions to avoid double extensions in generated filenames
|
// Strip common file extensions to avoid double extensions in generated filenames
|
||||||
// e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow'
|
// e.g., 'create-story.xml' → 'create-story', 'workflow.md' → 'workflow'
|
||||||
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
||||||
const parts = withoutExt.split(/[/\\]/);
|
const parts = withoutExt.split(/[/\\]/);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD
|
||||||
*/
|
*/
|
||||||
class WorkflowCommandGenerator {
|
class WorkflowCommandGenerator {
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||||
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
|
|
||||||
this.bmadFolderName = bmadFolderName;
|
this.bmadFolderName = bmadFolderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +66,7 @@ class WorkflowCommandGenerator {
|
||||||
|
|
||||||
for (const workflow of allWorkflows) {
|
for (const workflow of allWorkflows) {
|
||||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml)
|
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
|
||||||
let workflowRelPath = workflow.path || '';
|
let workflowRelPath = workflow.path || '';
|
||||||
// Normalize path separators for cross-platform compatibility
|
// Normalize path separators for cross-platform compatibility
|
||||||
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
||||||
|
|
@ -85,11 +84,8 @@ class WorkflowCommandGenerator {
|
||||||
workflowRelPath = `${match[1]}/${match[2]}`;
|
workflowRelPath = `${match[1]}/${match[2]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Determine if this is a YAML workflow (use normalized path which is guaranteed to be a string)
|
|
||||||
const isYamlWorkflow = workflowRelPath.endsWith('.yaml') || workflowRelPath.endsWith('.yml');
|
|
||||||
artifacts.push({
|
artifacts.push({
|
||||||
type: 'workflow-command',
|
type: 'workflow-command',
|
||||||
isYamlWorkflow: isYamlWorkflow, // For template selection
|
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
description: workflow.description || `${workflow.name} workflow`,
|
description: workflow.description || `${workflow.name} workflow`,
|
||||||
module: workflow.module,
|
module: workflow.module,
|
||||||
|
|
@ -125,17 +121,14 @@ class WorkflowCommandGenerator {
|
||||||
* Generate command content for a workflow
|
* Generate command content for a workflow
|
||||||
*/
|
*/
|
||||||
async generateCommandContent(workflow, bmadDir) {
|
async generateCommandContent(workflow, bmadDir) {
|
||||||
// Determine template based on workflow file type
|
const templatePath = path.join(__dirname, '../templates/workflow-commander.md');
|
||||||
const isMarkdownWorkflow = workflow.path.endsWith('workflow.md');
|
|
||||||
const templateName = isMarkdownWorkflow ? 'workflow-commander.md' : 'workflow-command-template.md';
|
|
||||||
const templatePath = path.join(path.dirname(this.templatePath), templateName);
|
|
||||||
|
|
||||||
// Load the appropriate template
|
// Load the template
|
||||||
const template = await fs.readFile(templatePath, 'utf8');
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
|
||||||
// Convert source path to installed path
|
// Convert source path to installed path
|
||||||
// From: /Users/.../src/bmm/workflows/.../workflow.yaml
|
// From: /Users/.../src/bmm/workflows/.../workflow.md
|
||||||
// To: {project-root}/_bmad/bmm/workflows/.../workflow.yaml
|
// To: {project-root}/_bmad/bmm/workflows/.../workflow.md
|
||||||
let workflowPath = workflow.path;
|
let workflowPath = workflow.path;
|
||||||
|
|
||||||
// Extract the relative path from source
|
// Extract the relative path from source
|
||||||
|
|
@ -218,10 +211,9 @@ class WorkflowCommandGenerator {
|
||||||
## Execution
|
## Execution
|
||||||
|
|
||||||
When running any workflow:
|
When running any workflow:
|
||||||
1. LOAD {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
|
1. LOAD the workflow.md file at the path shown above
|
||||||
2. Pass the workflow path as 'workflow-config' parameter
|
2. READ its entire contents and follow its directions exactly
|
||||||
3. Follow workflow.xml instructions EXACTLY
|
3. Save outputs after EACH section
|
||||||
4. Save outputs after EACH section
|
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
- Normal: Full interaction
|
- Normal: Full interaction
|
||||||
|
|
|
||||||
|
|
@ -762,14 +762,8 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a workflow.yaml file
|
// Copy the file with placeholder replacement
|
||||||
if (file.endsWith('workflow.yaml')) {
|
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
||||||
await fs.ensureDir(path.dirname(targetFile));
|
|
||||||
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
|
|
||||||
} else {
|
|
||||||
// Copy the file with placeholder replacement
|
|
||||||
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track the file if callback provided
|
// Track the file if callback provided
|
||||||
if (fileTrackingCallback) {
|
if (fileTrackingCallback) {
|
||||||
|
|
@ -778,91 +772,6 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy workflow.yaml file with web_bundle section stripped
|
|
||||||
* Preserves comments, formatting, and line breaks
|
|
||||||
* @param {string} sourceFile - Source workflow.yaml file path
|
|
||||||
* @param {string} targetFile - Target workflow.yaml file path
|
|
||||||
*/
|
|
||||||
async copyWorkflowYamlStripped(sourceFile, targetFile) {
|
|
||||||
// Read the source YAML file
|
|
||||||
let yamlContent = await fs.readFile(sourceFile, 'utf8');
|
|
||||||
|
|
||||||
// IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML
|
|
||||||
// Otherwise parsing will fail on the placeholder
|
|
||||||
yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First check if web_bundle exists by parsing
|
|
||||||
const workflowConfig = yaml.parse(yamlContent);
|
|
||||||
|
|
||||||
if (workflowConfig.web_bundle === undefined) {
|
|
||||||
// No web_bundle section, just write (placeholders already replaced above)
|
|
||||||
await fs.writeFile(targetFile, yamlContent, 'utf8');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the line that starts web_bundle
|
|
||||||
const lines = yamlContent.split('\n');
|
|
||||||
let startIdx = -1;
|
|
||||||
let endIdx = -1;
|
|
||||||
let baseIndent = 0;
|
|
||||||
|
|
||||||
// Find the start of web_bundle section
|
|
||||||
for (const [i, line] of lines.entries()) {
|
|
||||||
const match = line.match(/^(\s*)web_bundle:/);
|
|
||||||
if (match) {
|
|
||||||
startIdx = i;
|
|
||||||
baseIndent = match[1].length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startIdx === -1) {
|
|
||||||
// web_bundle not found in text (shouldn't happen), copy as-is
|
|
||||||
await fs.writeFile(targetFile, yamlContent, 'utf8');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the end of web_bundle section
|
|
||||||
// It ends when we find a line with same or less indentation that's not empty/comment
|
|
||||||
endIdx = startIdx;
|
|
||||||
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if (line.trim() === '' || line.trim().startsWith('#')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check indentation
|
|
||||||
const indent = line.match(/^(\s*)/)[1].length;
|
|
||||||
if (indent <= baseIndent) {
|
|
||||||
// Found next section at same or lower indentation
|
|
||||||
endIdx = i - 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find an end, it goes to end of file
|
|
||||||
if (endIdx === startIdx) {
|
|
||||||
endIdx = lines.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the web_bundle section (including the line before if it's just a blank line)
|
|
||||||
const newLines = [...lines.slice(0, startIdx), ...lines.slice(endIdx + 1)];
|
|
||||||
|
|
||||||
// Clean up any double blank lines that might result
|
|
||||||
const strippedYaml = newLines.join('\n').replaceAll(/\n\n\n+/g, '\n\n');
|
|
||||||
|
|
||||||
// Placeholders already replaced at the beginning of this function
|
|
||||||
await fs.writeFile(targetFile, strippedYaml, 'utf8');
|
|
||||||
} catch {
|
|
||||||
// If anything fails, just copy the file as-is
|
|
||||||
await prompts.log.warn(` Could not process ${path.basename(sourceFile)}, copying as-is`);
|
|
||||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile .agent.yaml files to .md format in modules
|
* Compile .agent.yaml files to .md format in modules
|
||||||
|
|
@ -1169,9 +1078,7 @@ class ModuleManager {
|
||||||
const installWorkflowPath = item['workflow-install']; // Where to copy TO
|
const installWorkflowPath = item['workflow-install']; // Where to copy TO
|
||||||
|
|
||||||
// Parse SOURCE workflow path
|
// Parse SOURCE workflow path
|
||||||
// Handle both _bmad placeholder and hardcoded 'bmad'
|
// Example: {project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.md
|
||||||
// Example: {project-root}/_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
|
|
||||||
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml
|
|
||||||
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
|
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/);
|
||||||
if (!sourceMatch) {
|
if (!sourceMatch) {
|
||||||
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`);
|
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`);
|
||||||
|
|
@ -1181,8 +1088,7 @@ class ModuleManager {
|
||||||
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
|
const [, sourceModule, sourceWorkflowSubPath] = sourceMatch;
|
||||||
|
|
||||||
// Parse INSTALL workflow path
|
// Parse INSTALL workflow path
|
||||||
// Handle_bmad
|
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.md
|
||||||
// Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml
|
|
||||||
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
|
const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/);
|
||||||
if (!installMatch) {
|
if (!installMatch) {
|
||||||
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
|
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`);
|
||||||
|
|
@ -1192,9 +1098,9 @@ class ModuleManager {
|
||||||
const installWorkflowSubPath = installMatch[2];
|
const installWorkflowSubPath = installMatch[2];
|
||||||
|
|
||||||
const sourceModulePath = getModulePath(sourceModule);
|
const sourceModulePath = getModulePath(sourceModule);
|
||||||
const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, ''));
|
const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.md$/, ''));
|
||||||
|
|
||||||
const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.yaml$/, ''));
|
const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.md$/, ''));
|
||||||
|
|
||||||
// Check if source workflow exists
|
// Check if source workflow exists
|
||||||
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
|
if (!(await fs.pathExists(actualSourceWorkflowPath))) {
|
||||||
|
|
@ -1204,18 +1110,12 @@ class ModuleManager {
|
||||||
|
|
||||||
// Copy the entire workflow folder
|
// Copy the entire workflow folder
|
||||||
await prompts.log.message(
|
await prompts.log.message(
|
||||||
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`,
|
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.md$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.md$/, '')}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
|
await fs.ensureDir(path.dirname(actualDestWorkflowPath));
|
||||||
// Copy the workflow directory recursively with placeholder replacement
|
// Copy the workflow directory recursively with placeholder replacement
|
||||||
await this.copyDirectoryWithPlaceholderReplacement(actualSourceWorkflowPath, actualDestWorkflowPath);
|
await this.copyDirectoryWithPlaceholderReplacement(actualSourceWorkflowPath, actualDestWorkflowPath);
|
||||||
|
|
||||||
// Update the workflow.yaml config_source reference
|
|
||||||
const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml');
|
|
||||||
if (await fs.pathExists(workflowYamlPath)) {
|
|
||||||
await this.updateWorkflowConfigSource(workflowYamlPath, moduleName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1224,27 +1124,6 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update workflow.yaml config_source to point to new module
|
|
||||||
* @param {string} workflowYamlPath - Path to workflow.yaml file
|
|
||||||
* @param {string} newModuleName - New module name to reference
|
|
||||||
*/
|
|
||||||
async updateWorkflowConfigSource(workflowYamlPath, newModuleName) {
|
|
||||||
let yamlContent = await fs.readFile(workflowYamlPath, 'utf8');
|
|
||||||
|
|
||||||
// Replace config_source: "{project-root}/_bmad/OLD_MODULE/config.yaml"
|
|
||||||
// with config_source: "{project-root}/_bmad/NEW_MODULE/config.yaml"
|
|
||||||
// Note: At this point _bmad has already been replaced with actual folder name
|
|
||||||
const configSourcePattern = /config_source:\s*["']?\{project-root\}\/[^/]+\/[^/]+\/config\.yaml["']?/g;
|
|
||||||
const newConfigSource = `config_source: "{project-root}/${this.bmadFolderName}/${newModuleName}/config.yaml"`;
|
|
||||||
|
|
||||||
const updatedYaml = yamlContent.replaceAll(configSourcePattern, newConfigSource);
|
|
||||||
|
|
||||||
if (updatedYaml !== yamlContent) {
|
|
||||||
await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8');
|
|
||||||
await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create directories declared in module.yaml's `directories` key
|
* Create directories declared in module.yaml's `directories` key
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,7 @@ class AgentAnalyzer {
|
||||||
if (Array.isArray(execArray)) {
|
if (Array.isArray(execArray)) {
|
||||||
for (const exec of execArray) {
|
for (const exec of execArray) {
|
||||||
if (exec.route) {
|
if (exec.route) {
|
||||||
// Check if route is a workflow or exec
|
profile.usedAttributes.add('exec');
|
||||||
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
|
|
||||||
profile.usedAttributes.add('workflow');
|
|
||||||
} else {
|
|
||||||
profile.usedAttributes.add('exec');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (exec.workflow) profile.usedAttributes.add('workflow');
|
if (exec.workflow) profile.usedAttributes.add('workflow');
|
||||||
if (exec.action) profile.usedAttributes.add('action');
|
if (exec.action) profile.usedAttributes.add('action');
|
||||||
|
|
|
||||||
|
|
@ -229,12 +229,7 @@ function processExecArray(execArray) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exec.route) {
|
if (exec.route) {
|
||||||
// Determine if it's a workflow or exec based on file extension or context
|
result.route = exec.route;
|
||||||
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
|
|
||||||
result.workflow = exec.route;
|
|
||||||
} else {
|
|
||||||
result.route = exec.route;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exec.data !== null && exec.data !== undefined) {
|
if (exec.data !== null && exec.data !== undefined) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue