Drop YAML workflow support from CLI tooling
This commit is contained in:
parent
de63874520
commit
2224edaa84
|
|
@ -2,7 +2,6 @@ const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const csv = require('csv-parse/sync');
|
|
||||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
|
|
@ -22,19 +21,6 @@ class ManifestGenerator {
|
||||||
this.selectedIdes = [];
|
this.selectedIdes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean text for CSV output by normalizing whitespace and escaping quotes
|
|
||||||
* @param {string} text - Text to clean
|
|
||||||
* @returns {string} Cleaned text safe for CSV
|
|
||||||
*/
|
|
||||||
cleanForCSV(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
return text
|
|
||||||
.trim()
|
|
||||||
.replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space
|
|
||||||
.replaceAll('"', '""'); // Escape quotes for CSV
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate all manifests for the installation
|
* Generate all manifests for the installation
|
||||||
* @param {string} bmadDir - _bmad
|
* @param {string} bmadDir - _bmad
|
||||||
|
|
@ -159,12 +145,8 @@ class ManifestGenerator {
|
||||||
// Recurse into subdirectories
|
// Recurse into subdirectories
|
||||||
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.md') {
|
||||||
entry.name === 'workflow.yaml' ||
|
// Parse workflow file (MD with YAML frontmatter)
|
||||||
entry.name === 'workflow.md' ||
|
|
||||||
(entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))
|
|
||||||
) {
|
|
||||||
// Parse workflow file (both YAML and MD formats)
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log(`[DEBUG] Found workflow file: ${fullPath}`);
|
console.log(`[DEBUG] Found workflow file: ${fullPath}`);
|
||||||
}
|
}
|
||||||
|
|
@ -173,21 +155,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(/^---\r?\n([\s\S]*?)\r?\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'}`);
|
||||||
|
|
@ -219,7 +195,7 @@ class ManifestGenerator {
|
||||||
// Workflows with standalone: false are filtered out above
|
// Workflows with standalone: false are filtered out above
|
||||||
workflows.push({
|
workflows.push({
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
description: this.cleanForCSV(workflow.description),
|
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
});
|
});
|
||||||
|
|
@ -337,15 +313,24 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const agentName = entry.name.replace('.md', '');
|
const agentName = entry.name.replace('.md', '');
|
||||||
|
|
||||||
|
// Helper function to clean and escape CSV content
|
||||||
|
const cleanForCSV = (text) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text
|
||||||
|
.trim()
|
||||||
|
.replaceAll(/\s+/g, ' ') // Normalize whitespace
|
||||||
|
.replaceAll('"', '""'); // Escape quotes for CSV
|
||||||
|
};
|
||||||
|
|
||||||
agents.push({
|
agents.push({
|
||||||
name: agentName,
|
name: agentName,
|
||||||
displayName: nameMatch ? nameMatch[1] : agentName,
|
displayName: nameMatch ? nameMatch[1] : agentName,
|
||||||
title: titleMatch ? titleMatch[1] : '',
|
title: titleMatch ? titleMatch[1] : '',
|
||||||
icon: iconMatch ? iconMatch[1] : '',
|
icon: iconMatch ? iconMatch[1] : '',
|
||||||
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
role: roleMatch ? cleanForCSV(roleMatch[1]) : '',
|
||||||
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
identity: identityMatch ? cleanForCSV(identityMatch[1]) : '',
|
||||||
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '',
|
||||||
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '',
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
});
|
});
|
||||||
|
|
@ -394,11 +379,6 @@ class ManifestGenerator {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
|
||||||
// Skip internal/engine files (not user-facing tasks)
|
|
||||||
if (content.includes('internal="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
let name = file.replace(/\.(xml|md)$/, '');
|
||||||
let displayName = name;
|
let displayName = name;
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
@ -406,21 +386,17 @@ class ManifestGenerator {
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
if (file.endsWith('.md')) {
|
||||||
// Parse YAML frontmatter for .md tasks
|
// Parse YAML frontmatter for .md tasks
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (frontmatterMatch) {
|
if (frontmatterMatch) {
|
||||||
try {
|
try {
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
||||||
name = frontmatter.name || name;
|
name = frontmatter.name || name;
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
||||||
description = this.cleanForCSV(frontmatter.description || '');
|
description = frontmatter.description || '';
|
||||||
// Tasks are standalone by default unless explicitly false (internal=true is already filtered above)
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
||||||
standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false';
|
|
||||||
} catch {
|
} catch {
|
||||||
// If YAML parsing fails, use defaults
|
// If YAML parsing fails, use defaults
|
||||||
standalone = true; // Default to standalone
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
standalone = true; // No frontmatter means standalone
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For .xml tasks, extract from tag attributes
|
// For .xml tasks, extract from tag attributes
|
||||||
|
|
@ -429,10 +405,10 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
const descMatch = content.match(/description="([^"]+)"/);
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||||
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
||||||
|
|
||||||
const standaloneFalseMatch = content.match(/<task[^>]+standalone="false"/);
|
const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
|
||||||
standalone = !standaloneFalseMatch;
|
standalone = !!standaloneMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build relative path for installation
|
// Build relative path for installation
|
||||||
|
|
@ -442,7 +418,7 @@ class ManifestGenerator {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
name: name,
|
name: name,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
description: description,
|
description: description.replaceAll('"', '""'),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
|
@ -492,11 +468,6 @@ class ManifestGenerator {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const content = await fs.readFile(filePath, 'utf8');
|
const content = await fs.readFile(filePath, 'utf8');
|
||||||
|
|
||||||
// Skip internal tools (same as tasks)
|
|
||||||
if (content.includes('internal="true"')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = file.replace(/\.(xml|md)$/, '');
|
let name = file.replace(/\.(xml|md)$/, '');
|
||||||
let displayName = name;
|
let displayName = name;
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
@ -504,21 +475,17 @@ class ManifestGenerator {
|
||||||
|
|
||||||
if (file.endsWith('.md')) {
|
if (file.endsWith('.md')) {
|
||||||
// Parse YAML frontmatter for .md tools
|
// Parse YAML frontmatter for .md tools
|
||||||
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
if (frontmatterMatch) {
|
if (frontmatterMatch) {
|
||||||
try {
|
try {
|
||||||
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
||||||
name = frontmatter.name || name;
|
name = frontmatter.name || name;
|
||||||
displayName = frontmatter.displayName || frontmatter.name || name;
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
||||||
description = this.cleanForCSV(frontmatter.description || '');
|
description = frontmatter.description || '';
|
||||||
// Tools are standalone by default unless explicitly false (internal=true is already filtered above)
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
||||||
standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false';
|
|
||||||
} catch {
|
} catch {
|
||||||
// If YAML parsing fails, use defaults
|
// If YAML parsing fails, use defaults
|
||||||
standalone = true; // Default to standalone
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
standalone = true; // No frontmatter means standalone
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For .xml tools, extract from tag attributes
|
// For .xml tools, extract from tag attributes
|
||||||
|
|
@ -527,10 +494,10 @@ class ManifestGenerator {
|
||||||
|
|
||||||
const descMatch = content.match(/description="([^"]+)"/);
|
const descMatch = content.match(/description="([^"]+)"/);
|
||||||
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
|
||||||
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
|
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '';
|
||||||
|
|
||||||
const standaloneFalseMatch = content.match(/<tool[^>]+standalone="false"/);
|
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
|
||||||
standalone = !standaloneFalseMatch;
|
standalone = !!standaloneMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build relative path for installation
|
// Build relative path for installation
|
||||||
|
|
@ -540,7 +507,7 @@ class ManifestGenerator {
|
||||||
tools.push({
|
tools.push({
|
||||||
name: name,
|
name: name,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
description: description,
|
description: description.replaceAll('"', '""'),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
|
@ -733,15 +700,47 @@ class ManifestGenerator {
|
||||||
async writeWorkflowManifest(cfgDir) {
|
async writeWorkflowManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
const parseCsvLine = (line) => {
|
||||||
|
const columns = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || [];
|
||||||
|
return columns.map((c) => c.replaceAll(/^"|"$/g, ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read existing manifest to preserve entries
|
||||||
|
const existingEntries = new Map();
|
||||||
|
if (await fs.pathExists(csvPath)) {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
|
// Skip header
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line) {
|
||||||
|
const parts = parseCsvLine(line);
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
const [name, description, module, workflowPath] = parts;
|
||||||
|
existingEntries.set(`${module}:${name}`, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
module,
|
||||||
|
path: workflowPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create CSV header - standalone column removed, everything is canonicalized to 4 columns
|
// Create CSV header - standalone column removed, everything is canonicalized to 4 columns
|
||||||
let csv = 'name,description,module,path\n';
|
let csv = 'name,description,module,path\n';
|
||||||
|
|
||||||
// Build workflows map from discovered workflows only
|
// Combine existing and new workflows
|
||||||
// Old entries are NOT preserved - the manifest reflects what actually exists on disk
|
|
||||||
const allWorkflows = new Map();
|
const allWorkflows = new Map();
|
||||||
|
|
||||||
// Only add workflows that were actually discovered in this scan
|
// Add existing entries
|
||||||
|
for (const [key, value] of existingEntries) {
|
||||||
|
allWorkflows.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update new workflows
|
||||||
for (const workflow of this.workflows) {
|
for (const workflow of this.workflows) {
|
||||||
const key = `${workflow.module}:${workflow.name}`;
|
const key = `${workflow.module}:${workflow.name}`;
|
||||||
allWorkflows.set(key, {
|
allWorkflows.set(key, {
|
||||||
|
|
@ -768,23 +767,30 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeAgentManifest(cfgDir) {
|
async writeAgentManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const records = csv.parse(content, {
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
// Skip header
|
||||||
});
|
for (let i = 1; i < lines.length; i++) {
|
||||||
for (const record of records) {
|
const line = lines[i];
|
||||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 11) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[8];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with persona fields
|
// Create CSV header with persona fields
|
||||||
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
||||||
|
|
||||||
// Combine existing and new agents, preferring new data for duplicates
|
// Combine existing and new agents, preferring new data for duplicates
|
||||||
const allAgents = new Map();
|
const allAgents = new Map();
|
||||||
|
|
@ -797,38 +803,18 @@ class ManifestGenerator {
|
||||||
// Add/update new agents
|
// Add/update new agents
|
||||||
for (const agent of this.agents) {
|
for (const agent of this.agents) {
|
||||||
const key = `${agent.module}:${agent.name}`;
|
const key = `${agent.module}:${agent.name}`;
|
||||||
allAgents.set(key, {
|
allAgents.set(
|
||||||
name: agent.name,
|
key,
|
||||||
displayName: agent.displayName,
|
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
|
||||||
title: agent.title,
|
);
|
||||||
icon: agent.icon,
|
|
||||||
role: agent.role,
|
|
||||||
identity: agent.identity,
|
|
||||||
communicationStyle: agent.communicationStyle,
|
|
||||||
principles: agent.principles,
|
|
||||||
module: agent.module,
|
|
||||||
path: agent.path,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all agents
|
// Write all agents
|
||||||
for (const [, record] of allAgents) {
|
for (const [, value] of allAgents) {
|
||||||
const row = [
|
csv += value + '\n';
|
||||||
escapeCsv(record.name),
|
|
||||||
escapeCsv(record.displayName),
|
|
||||||
escapeCsv(record.title),
|
|
||||||
escapeCsv(record.icon),
|
|
||||||
escapeCsv(record.role),
|
|
||||||
escapeCsv(record.identity),
|
|
||||||
escapeCsv(record.communicationStyle),
|
|
||||||
escapeCsv(record.principles),
|
|
||||||
escapeCsv(record.module),
|
|
||||||
escapeCsv(record.path),
|
|
||||||
].join(',');
|
|
||||||
csvContent += row + '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csvContent);
|
await fs.writeFile(csvPath, csv);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -838,23 +824,30 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeTaskManifest(cfgDir) {
|
async writeTaskManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const records = csv.parse(content, {
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
// Skip header
|
||||||
});
|
for (let i = 1; i < lines.length; i++) {
|
||||||
for (const record of records) {
|
const line = lines[i];
|
||||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[3];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Combine existing and new tasks
|
// Combine existing and new tasks
|
||||||
const allTasks = new Map();
|
const allTasks = new Map();
|
||||||
|
|
@ -867,30 +860,15 @@ class ManifestGenerator {
|
||||||
// Add/update new tasks
|
// Add/update new tasks
|
||||||
for (const task of this.tasks) {
|
for (const task of this.tasks) {
|
||||||
const key = `${task.module}:${task.name}`;
|
const key = `${task.module}:${task.name}`;
|
||||||
allTasks.set(key, {
|
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
|
||||||
name: task.name,
|
|
||||||
displayName: task.displayName,
|
|
||||||
description: task.description,
|
|
||||||
module: task.module,
|
|
||||||
path: task.path,
|
|
||||||
standalone: task.standalone,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all tasks
|
// Write all tasks
|
||||||
for (const [, record] of allTasks) {
|
for (const [, value] of allTasks) {
|
||||||
const row = [
|
csv += value + '\n';
|
||||||
escapeCsv(record.name),
|
|
||||||
escapeCsv(record.displayName),
|
|
||||||
escapeCsv(record.description),
|
|
||||||
escapeCsv(record.module),
|
|
||||||
escapeCsv(record.path),
|
|
||||||
escapeCsv(record.standalone),
|
|
||||||
].join(',');
|
|
||||||
csvContent += row + '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csvContent);
|
await fs.writeFile(csvPath, csv);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,23 +878,30 @@ class ManifestGenerator {
|
||||||
*/
|
*/
|
||||||
async writeToolManifest(cfgDir) {
|
async writeToolManifest(cfgDir) {
|
||||||
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
||||||
|
|
||||||
// Read existing manifest to preserve entries
|
// Read existing manifest to preserve entries
|
||||||
const existingEntries = new Map();
|
const existingEntries = new Map();
|
||||||
if (await fs.pathExists(csvPath)) {
|
if (await fs.pathExists(csvPath)) {
|
||||||
const content = await fs.readFile(csvPath, 'utf8');
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
const records = csv.parse(content, {
|
const lines = content.split('\n').filter((line) => line.trim());
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
// Skip header
|
||||||
});
|
for (let i = 1; i < lines.length; i++) {
|
||||||
for (const record of records) {
|
const line = lines[i];
|
||||||
existingEntries.set(`${record.module}:${record.name}`, record);
|
if (line) {
|
||||||
|
// Parse CSV (simple parsing assuming no commas in quoted fields)
|
||||||
|
const parts = line.split('","');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
const name = parts[0].replace(/^"/, '');
|
||||||
|
const module = parts[3];
|
||||||
|
existingEntries.set(`${module}:${name}`, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone column
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
||||||
|
|
||||||
// Combine existing and new tools
|
// Combine existing and new tools
|
||||||
const allTools = new Map();
|
const allTools = new Map();
|
||||||
|
|
@ -929,30 +914,15 @@ class ManifestGenerator {
|
||||||
// Add/update new tools
|
// Add/update new tools
|
||||||
for (const tool of this.tools) {
|
for (const tool of this.tools) {
|
||||||
const key = `${tool.module}:${tool.name}`;
|
const key = `${tool.module}:${tool.name}`;
|
||||||
allTools.set(key, {
|
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
|
||||||
name: tool.name,
|
|
||||||
displayName: tool.displayName,
|
|
||||||
description: tool.description,
|
|
||||||
module: tool.module,
|
|
||||||
path: tool.path,
|
|
||||||
standalone: tool.standalone,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all tools
|
// Write all tools
|
||||||
for (const [, record] of allTools) {
|
for (const [, value] of allTools) {
|
||||||
const row = [
|
csv += value + '\n';
|
||||||
escapeCsv(record.name),
|
|
||||||
escapeCsv(record.displayName),
|
|
||||||
escapeCsv(record.description),
|
|
||||||
escapeCsv(record.module),
|
|
||||||
escapeCsv(record.path),
|
|
||||||
escapeCsv(record.standalone),
|
|
||||||
].join(',');
|
|
||||||
csvContent += row + '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(csvPath, csvContent);
|
await fs.writeFile(csvPath, csv);
|
||||||
return csvPath;
|
return csvPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -344,22 +344,18 @@ class BaseIdeSetup {
|
||||||
// Recursively search subdirectories
|
// Recursively search subdirectories
|
||||||
const subWorkflows = await this.findWorkflowFiles(fullPath);
|
const subWorkflows = await this.findWorkflowFiles(fullPath);
|
||||||
workflows.push(...subWorkflows);
|
workflows.push(...subWorkflows);
|
||||||
} else if (entry.isFile() && (entry.name === 'workflow.yaml' || entry.name === 'workflow.md')) {
|
} else if (entry.isFile() && entry.name === 'workflow.md') {
|
||||||
// Read workflow file to get name and standalone property
|
// Read workflow file 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');
|
||||||
let workflowData = null;
|
let workflowData = null;
|
||||||
|
|
||||||
if (entry.name === 'workflow.yaml') {
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
workflowData = yaml.parse(content);
|
if (!frontmatterMatch) {
|
||||||
} else {
|
continue;
|
||||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
||||||
if (!frontmatterMatch) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
workflowData = yaml.parse(frontmatterMatch[1]);
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
*/
|
*/
|
||||||
async installToTarget(projectDir, bmadDir, config, options) {
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
const { target_dir, template_type, artifact_types } = config;
|
const { target_dir, template_type, artifact_types } = config;
|
||||||
|
|
||||||
// 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 } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPath = path.join(projectDir, target_dir);
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
await this.ensureDir(targetPath);
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
|
|
@ -93,11 +86,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install tasks and tools using template system (supports TOML for Gemini, MD for others)
|
// Install tasks and tools
|
||||||
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
||||||
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
const taskToolGen = new TaskToolCommandGenerator();
|
||||||
const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
|
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath);
|
||||||
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
|
|
||||||
results.tasks = taskToolResult.tasks || 0;
|
results.tasks = taskToolResult.tasks || 0;
|
||||||
results.tools = taskToolResult.tools || 0;
|
results.tools = taskToolResult.tools || 0;
|
||||||
}
|
}
|
||||||
|
|
@ -140,12 +132,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
*/
|
*/
|
||||||
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||||
// Try to load platform-specific template, fall back to default-agent
|
// Try to load platform-specific template, fall back to default-agent
|
||||||
const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
const filename = this.generateFilename(artifact, 'agent');
|
||||||
const filePath = path.join(targetPath, filename);
|
const filePath = path.join(targetPath, filename);
|
||||||
await this.writeFile(filePath, content);
|
await this.writeFile(filePath, content);
|
||||||
count++;
|
count++;
|
||||||
|
|
@ -167,18 +159,14 @@ 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)
|
|
||||||
// Default to 'default' template type, but allow override via config
|
// Default to 'default' template type, but allow override via config
|
||||||
const workflowTemplateType = artifact.isYamlWorkflow
|
const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`;
|
||||||
? config.yaml_workflow_template || `${templateType}-workflow-yaml`
|
|
||||||
: config.md_workflow_template || `${templateType}-workflow`;
|
|
||||||
|
|
||||||
// Fall back to default templates if specific ones don't exist
|
// Fall back to default template if the requested one doesn't exist
|
||||||
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
|
const finalTemplateType = 'default-workflow';
|
||||||
// workflowTemplateType already contains full name (e.g., 'gemini-workflow-yaml'), so pass empty artifactType
|
const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType);
|
||||||
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');
|
||||||
const filePath = path.join(targetPath, filename);
|
const filePath = path.join(targetPath, filename);
|
||||||
await this.writeFile(filePath, content);
|
await this.writeFile(filePath, content);
|
||||||
count++;
|
count++;
|
||||||
|
|
@ -188,100 +176,40 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
return 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);
|
|
||||||
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
|
* Load template based on type and configuration
|
||||||
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||||
* @param {Object} config - Installation configuration
|
* @param {Object} config - Installation configuration
|
||||||
* @param {string} fallbackTemplateType - Fallback template type if requested template not found
|
* @param {string} fallbackTemplateType - Fallback template type if requested template not found
|
||||||
* @returns {Promise<{content: string, extension: string}>} Template content and extension
|
* @returns {Promise<string>} Template content
|
||||||
*/
|
*/
|
||||||
async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
|
async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
|
||||||
const { header_template, body_template } = config;
|
const { header_template, body_template } = config;
|
||||||
|
|
||||||
// Check for separate header/body templates
|
// Check for separate header/body templates
|
||||||
if (header_template || body_template) {
|
if (header_template || body_template) {
|
||||||
const content = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
||||||
// Allow config to override extension, default to .md
|
|
||||||
const ext = config.extension || '.md';
|
|
||||||
const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
|
|
||||||
return { content, extension: normalizedExt };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load combined template - try multiple extensions
|
// Load combined template
|
||||||
// If artifactType is empty, templateType already contains full name (e.g., 'gemini-workflow-yaml')
|
const templateName = `${templateType}-${artifactType}.md`;
|
||||||
const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
|
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
||||||
const templateDir = path.join(__dirname, 'templates', 'combined');
|
|
||||||
const extensions = ['.md', '.toml', '.yaml', '.yml'];
|
|
||||||
|
|
||||||
for (const ext of extensions) {
|
if (await fs.pathExists(templatePath)) {
|
||||||
const templatePath = path.join(templateDir, templateBaseName + ext);
|
return await fs.readFile(templatePath, 'utf8');
|
||||||
if (await fs.pathExists(templatePath)) {
|
|
||||||
const content = await fs.readFile(templatePath, 'utf8');
|
|
||||||
return { content, extension: ext };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to default template (if provided)
|
// Fall back to default template (if provided)
|
||||||
if (fallbackTemplateType) {
|
if (fallbackTemplateType) {
|
||||||
for (const ext of extensions) {
|
const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`);
|
||||||
const fallbackPath = path.join(templateDir, `${fallbackTemplateType}${ext}`);
|
if (await fs.pathExists(fallbackPath)) {
|
||||||
if (await fs.pathExists(fallbackPath)) {
|
return await fs.readFile(fallbackPath, 'utf8');
|
||||||
const content = await fs.readFile(fallbackPath, 'utf8');
|
|
||||||
return { content, extension: ext };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ultimate fallback - minimal template
|
// Ultimate fallback - minimal template
|
||||||
return { content: this.getDefaultTemplate(artifactType), extension: '.md' };
|
return this.getDefaultTemplate(artifactType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -338,7 +266,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
return `---
|
return `---
|
||||||
name: '{{name}}'
|
name: '{{name}}'
|
||||||
description: '{{description}}'
|
description: '{{description}}'
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
---
|
||||||
|
|
||||||
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
||||||
|
|
@ -353,7 +280,6 @@ You must fully embody this agent's persona and follow all activation instruction
|
||||||
return `---
|
return `---
|
||||||
name: '{{name}}'
|
name: '{{name}}'
|
||||||
description: '{{description}}'
|
description: '{{description}}'
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# {{name}}
|
# {{name}}
|
||||||
|
|
@ -371,24 +297,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
renderTemplate(template, artifact) {
|
renderTemplate(template, artifact) {
|
||||||
// Use the appropriate path property based on artifact type
|
// Use the appropriate path property based on artifact type
|
||||||
let pathToUse = artifact.relativePath || '';
|
let pathToUse = artifact.relativePath || '';
|
||||||
switch (artifact.type) {
|
if (artifact.type === 'agent-launcher') {
|
||||||
case 'agent-launcher': {
|
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
||||||
pathToUse = artifact.agentPath || artifact.relativePath || '';
|
} else if (artifact.type === 'workflow-command') {
|
||||||
|
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'workflow-command': {
|
|
||||||
pathToUse = artifact.workflowPath || artifact.relativePath || '';
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'task':
|
|
||||||
case 'tool': {
|
|
||||||
pathToUse = artifact.path || artifact.relativePath || '';
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rendered = template
|
let rendered = template
|
||||||
|
|
@ -411,27 +323,13 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
* Generate filename for artifact
|
* Generate filename for artifact
|
||||||
* @param {Object} artifact - Artifact data
|
* @param {Object} artifact - Artifact data
|
||||||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||||
* @param {string} extension - File extension to use (e.g., '.md', '.toml')
|
|
||||||
* @returns {string} Generated filename
|
* @returns {string} Generated filename
|
||||||
*/
|
*/
|
||||||
generateFilename(artifact, artifactType, extension = '.md') {
|
generateFilename(artifact, artifactType) {
|
||||||
const { toDashPath } = require('./shared/path-utils');
|
const { toDashPath } = require('./shared/path-utils');
|
||||||
|
// toDashPath already handles the .agent.md suffix for agents correctly
|
||||||
// Reuse central logic to ensure consistent naming conventions
|
// No need to add it again here
|
||||||
const standardName = toDashPath(artifact.relativePath);
|
return toDashPath(artifact.relativePath);
|
||||||
|
|
||||||
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
|
|
||||||
// This handles any extensions that might slip through toDashPath()
|
|
||||||
const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md');
|
|
||||||
|
|
||||||
// If using default markdown, preserve the bmad-agent- prefix for agents
|
|
||||||
if (extension === '.md') {
|
|
||||||
return baseName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other extensions (e.g., .toml), replace .md extension
|
|
||||||
// Note: agent prefix is preserved even with non-markdown extensions
|
|
||||||
return baseName.replace(/\.md$/, extension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = r
|
||||||
*/
|
*/
|
||||||
class WorkflowCommandGenerator {
|
class WorkflowCommandGenerator {
|
||||||
constructor(bmadFolderName = 'bmad') {
|
constructor(bmadFolderName = 'bmad') {
|
||||||
this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md');
|
this.templatePath = path.join(__dirname, '../templates/workflow-commander.md');
|
||||||
this.bmadFolderName = bmadFolderName;
|
this.bmadFolderName = bmadFolderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,11 +77,8 @@ class WorkflowCommandGenerator {
|
||||||
workflowRelPath = parts.slice(1).join('/');
|
workflowRelPath = parts.slice(1).join('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Determine if this is a YAML workflow
|
|
||||||
const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.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,
|
||||||
|
|
@ -117,9 +114,7 @@ class WorkflowCommandGenerator {
|
||||||
*/
|
*/
|
||||||
async generateCommandContent(workflow, bmadDir) {
|
async generateCommandContent(workflow, bmadDir) {
|
||||||
// Determine template based on workflow file type
|
// Determine template based on workflow file type
|
||||||
const isMarkdownWorkflow = workflow.path.endsWith('workflow.md');
|
const templatePath = path.join(path.dirname(this.templatePath), 'workflow-commander.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 appropriate template
|
||||||
const template = await fs.readFile(templatePath, 'utf8');
|
const template = await fs.readFile(templatePath, 'utf8');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const { XmlHandler } = require('../../../lib/xml-handler');
|
||||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
const { filterCustomizationData } = require('../../../lib/agent/compiler');
|
||||||
const { ExternalModuleManager } = require('./external-manager');
|
const { ExternalModuleManager } = require('./external-manager');
|
||||||
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the installation, updating, and removal of BMAD modules.
|
* Manages the installation, updating, and removal of BMAD modules.
|
||||||
|
|
@ -28,7 +27,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||||
class ModuleManager {
|
class ModuleManager {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.xmlHandler = new XmlHandler();
|
this.xmlHandler = new XmlHandler();
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden
|
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||||
this.customModulePaths = new Map(); // Initialize custom module paths
|
this.customModulePaths = new Map(); // Initialize custom module paths
|
||||||
this.externalModuleManager = new ExternalModuleManager(); // For external official modules
|
this.externalModuleManager = new ExternalModuleManager(); // For external official modules
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +416,7 @@ class ModuleManager {
|
||||||
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
|
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
|
||||||
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
|
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
|
||||||
try {
|
try {
|
||||||
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
|
execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', {
|
||||||
cwd: moduleCacheDir,
|
cwd: moduleCacheDir,
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
timeout: 120_000, // 2 minute timeout
|
timeout: 120_000, // 2 minute timeout
|
||||||
|
|
@ -442,7 +441,7 @@ class ModuleManager {
|
||||||
if (packageJsonNewer) {
|
if (packageJsonNewer) {
|
||||||
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
|
const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
|
||||||
try {
|
try {
|
||||||
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
|
execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', {
|
||||||
cwd: moduleCacheDir,
|
cwd: moduleCacheDir,
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
timeout: 120_000, // 2 minute timeout
|
timeout: 120_000, // 2 minute timeout
|
||||||
|
|
@ -740,8 +739,8 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a workflow file (YAML or MD)
|
// Check if this is a workflow file (MD)
|
||||||
if (file.endsWith('workflow.yaml') || file.endsWith('workflow.md')) {
|
if (file.endsWith('workflow.md')) {
|
||||||
await fs.ensureDir(path.dirname(targetFile));
|
await fs.ensureDir(path.dirname(targetFile));
|
||||||
await this.copyWorkflowFileStripped(sourceFile, targetFile);
|
await this.copyWorkflowFileStripped(sourceFile, targetFile);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -757,101 +756,19 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy workflow file with web_bundle section stripped (YAML or MD)
|
* Copy workflow file with web_bundle section stripped (MD)
|
||||||
* Preserves comments, formatting, and line breaks
|
* Preserves comments, formatting, and line breaks
|
||||||
* @param {string} sourceFile - Source workflow file path
|
* @param {string} sourceFile - Source workflow file path
|
||||||
* @param {string} targetFile - Target workflow file path
|
* @param {string} targetFile - Target workflow file path
|
||||||
*/
|
*/
|
||||||
async copyWorkflowFileStripped(sourceFile, targetFile) {
|
async copyWorkflowFileStripped(sourceFile, targetFile) {
|
||||||
if (sourceFile.endsWith('.md')) {
|
let mdContent = await fs.readFile(sourceFile, 'utf8');
|
||||||
let mdContent = await fs.readFile(sourceFile, 'utf8');
|
|
||||||
|
|
||||||
mdContent = mdContent.replaceAll('_bmad', '_bmad');
|
mdContent = mdContent.replaceAll('_bmad', '_bmad');
|
||||||
mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName);
|
mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName);
|
||||||
mdContent = this.stripWebBundleFromFrontmatter(mdContent);
|
mdContent = this.stripWebBundleFromFrontmatter(mdContent);
|
||||||
|
|
||||||
await fs.writeFile(targetFile, mdContent, 'utf8');
|
await fs.writeFile(targetFile, mdContent, 'utf8');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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', '_bmad');
|
|
||||||
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
|
|
||||||
console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`));
|
|
||||||
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stripWebBundleFromFrontmatter(content) {
|
stripWebBundleFromFrontmatter(content) {
|
||||||
|
|
@ -892,7 +809,7 @@ class ModuleManager {
|
||||||
for (const agentFile of agentFiles) {
|
for (const agentFile of agentFiles) {
|
||||||
if (!agentFile.endsWith('.agent.yaml')) continue;
|
if (!agentFile.endsWith('.agent.yaml')) continue;
|
||||||
|
|
||||||
const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/');
|
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
||||||
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
||||||
|
|
||||||
await fs.ensureDir(targetDir);
|
await fs.ensureDir(targetDir);
|
||||||
|
|
@ -1198,13 +1115,9 @@ class ModuleManager {
|
||||||
const installWorkflowSubPath = installMatch[2];
|
const installWorkflowSubPath = installMatch[2];
|
||||||
|
|
||||||
const sourceModulePath = getModulePath(sourceModule);
|
const sourceModulePath = getModulePath(sourceModule);
|
||||||
const actualSourceWorkflowPath = path.join(
|
const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.md$/, ''));
|
||||||
sourceModulePath,
|
|
||||||
'workflows',
|
|
||||||
sourceWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, ''),
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, ''));
|
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))) {
|
||||||
|
|
@ -1215,7 +1128,7 @@ class ModuleManager {
|
||||||
// Copy the entire workflow folder
|
// Copy the entire workflow folder
|
||||||
console.log(
|
console.log(
|
||||||
chalk.dim(
|
chalk.dim(
|
||||||
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, '')}`,
|
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.md$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.md$/, '')}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1225,12 +1138,8 @@ class ModuleManager {
|
||||||
|
|
||||||
// Update workflow config_source references
|
// Update workflow config_source references
|
||||||
const workflowMdPath = path.join(actualDestWorkflowPath, 'workflow.md');
|
const workflowMdPath = path.join(actualDestWorkflowPath, 'workflow.md');
|
||||||
const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml');
|
|
||||||
|
|
||||||
if (await fs.pathExists(workflowMdPath)) {
|
if (await fs.pathExists(workflowMdPath)) {
|
||||||
await this.updateWorkflowConfigSource(workflowMdPath, moduleName);
|
await this.updateWorkflowConfigSource(workflowMdPath, moduleName);
|
||||||
} else if (await fs.pathExists(workflowYamlPath)) {
|
|
||||||
await this.updateWorkflowConfigSource(workflowYamlPath, moduleName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue