|
|
|
|
@ -2,7 +2,6 @@ const path = require('node:path');
|
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
|
const yaml = require('yaml');
|
|
|
|
|
const crypto = require('node:crypto');
|
|
|
|
|
const csv = require('csv-parse/sync');
|
|
|
|
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
|
|
|
|
|
|
|
|
|
// Load package.json for version info
|
|
|
|
|
@ -22,19 +21,6 @@ class ManifestGenerator {
|
|
|
|
|
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
|
|
|
|
|
* @param {string} bmadDir - _bmad
|
|
|
|
|
@ -215,7 +201,7 @@ class ManifestGenerator {
|
|
|
|
|
// Workflows with standalone: false are filtered out above
|
|
|
|
|
workflows.push({
|
|
|
|
|
name: workflow.name,
|
|
|
|
|
description: this.cleanForCSV(workflow.description),
|
|
|
|
|
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV
|
|
|
|
|
module: moduleName,
|
|
|
|
|
path: installPath,
|
|
|
|
|
});
|
|
|
|
|
@ -333,15 +319,24 @@ class ManifestGenerator {
|
|
|
|
|
|
|
|
|
|
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({
|
|
|
|
|
name: agentName,
|
|
|
|
|
displayName: nameMatch ? nameMatch[1] : agentName,
|
|
|
|
|
title: titleMatch ? titleMatch[1] : '',
|
|
|
|
|
icon: iconMatch ? iconMatch[1] : '',
|
|
|
|
|
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
|
|
|
|
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
|
|
|
|
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
|
|
|
|
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
|
|
|
|
role: roleMatch ? cleanForCSV(roleMatch[1]) : '',
|
|
|
|
|
identity: identityMatch ? cleanForCSV(identityMatch[1]) : '',
|
|
|
|
|
communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '',
|
|
|
|
|
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '',
|
|
|
|
|
module: moduleName,
|
|
|
|
|
path: installPath,
|
|
|
|
|
});
|
|
|
|
|
@ -408,7 +403,7 @@ class ManifestGenerator {
|
|
|
|
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
|
|
|
name = frontmatter.name || name;
|
|
|
|
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
|
|
|
|
description = this.cleanForCSV(frontmatter.description || '');
|
|
|
|
|
description = frontmatter.description || '';
|
|
|
|
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
|
|
|
|
} catch {
|
|
|
|
|
// If YAML parsing fails, use defaults
|
|
|
|
|
@ -421,7 +416,7 @@ class ManifestGenerator {
|
|
|
|
|
|
|
|
|
|
const descMatch = content.match(/description="([^"]+)"/);
|
|
|
|
|
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 standaloneMatch = content.match(/<task[^>]+standalone="true"/);
|
|
|
|
|
standalone = !!standaloneMatch;
|
|
|
|
|
@ -434,7 +429,7 @@ class ManifestGenerator {
|
|
|
|
|
tasks.push({
|
|
|
|
|
name: name,
|
|
|
|
|
displayName: displayName,
|
|
|
|
|
description: description,
|
|
|
|
|
description: description.replaceAll('"', '""'),
|
|
|
|
|
module: moduleName,
|
|
|
|
|
path: installPath,
|
|
|
|
|
standalone: standalone,
|
|
|
|
|
@ -502,7 +497,7 @@ class ManifestGenerator {
|
|
|
|
|
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
|
|
|
name = frontmatter.name || name;
|
|
|
|
|
displayName = frontmatter.displayName || frontmatter.name || name;
|
|
|
|
|
description = this.cleanForCSV(frontmatter.description || '');
|
|
|
|
|
description = frontmatter.description || '';
|
|
|
|
|
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
|
|
|
|
|
} catch {
|
|
|
|
|
// If YAML parsing fails, use defaults
|
|
|
|
|
@ -515,7 +510,7 @@ class ManifestGenerator {
|
|
|
|
|
|
|
|
|
|
const descMatch = content.match(/description="([^"]+)"/);
|
|
|
|
|
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 standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
|
|
|
|
|
standalone = !!standaloneMatch;
|
|
|
|
|
@ -528,7 +523,7 @@ class ManifestGenerator {
|
|
|
|
|
tools.push({
|
|
|
|
|
name: name,
|
|
|
|
|
displayName: displayName,
|
|
|
|
|
description: description,
|
|
|
|
|
description: description.replaceAll('"', '""'),
|
|
|
|
|
module: moduleName,
|
|
|
|
|
path: installPath,
|
|
|
|
|
standalone: standalone,
|
|
|
|
|
@ -788,23 +783,30 @@ class ManifestGenerator {
|
|
|
|
|
*/
|
|
|
|
|
async writeAgentManifest(cfgDir) {
|
|
|
|
|
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
|
|
|
|
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
|
|
|
|
|
|
|
|
// Read existing manifest to preserve entries
|
|
|
|
|
const existingEntries = new Map();
|
|
|
|
|
if (await fs.pathExists(csvPath)) {
|
|
|
|
|
const content = await fs.readFile(csvPath, 'utf8');
|
|
|
|
|
const records = csv.parse(content, {
|
|
|
|
|
columns: true,
|
|
|
|
|
skip_empty_lines: true,
|
|
|
|
|
});
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
|
|
|
|
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) {
|
|
|
|
|
// 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
|
|
|
|
|
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
|
|
|
|
|
const allAgents = new Map();
|
|
|
|
|
@ -817,38 +819,18 @@ class ManifestGenerator {
|
|
|
|
|
// Add/update new agents
|
|
|
|
|
for (const agent of this.agents) {
|
|
|
|
|
const key = `${agent.module}:${agent.name}`;
|
|
|
|
|
allAgents.set(key, {
|
|
|
|
|
name: agent.name,
|
|
|
|
|
displayName: agent.displayName,
|
|
|
|
|
title: agent.title,
|
|
|
|
|
icon: agent.icon,
|
|
|
|
|
role: agent.role,
|
|
|
|
|
identity: agent.identity,
|
|
|
|
|
communicationStyle: agent.communicationStyle,
|
|
|
|
|
principles: agent.principles,
|
|
|
|
|
module: agent.module,
|
|
|
|
|
path: agent.path,
|
|
|
|
|
});
|
|
|
|
|
allAgents.set(
|
|
|
|
|
key,
|
|
|
|
|
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write all agents
|
|
|
|
|
for (const [, record] of allAgents) {
|
|
|
|
|
const row = [
|
|
|
|
|
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';
|
|
|
|
|
for (const [, value] of allAgents) {
|
|
|
|
|
csv += value + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(csvPath, csvContent);
|
|
|
|
|
await fs.writeFile(csvPath, csv);
|
|
|
|
|
return csvPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -858,23 +840,30 @@ class ManifestGenerator {
|
|
|
|
|
*/
|
|
|
|
|
async writeTaskManifest(cfgDir) {
|
|
|
|
|
const csvPath = path.join(cfgDir, 'task-manifest.csv');
|
|
|
|
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
|
|
|
|
|
|
|
|
// Read existing manifest to preserve entries
|
|
|
|
|
const existingEntries = new Map();
|
|
|
|
|
if (await fs.pathExists(csvPath)) {
|
|
|
|
|
const content = await fs.readFile(csvPath, 'utf8');
|
|
|
|
|
const records = csv.parse(content, {
|
|
|
|
|
columns: true,
|
|
|
|
|
skip_empty_lines: true,
|
|
|
|
|
});
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
|
|
|
|
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) {
|
|
|
|
|
// 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
|
|
|
|
|
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
|
|
|
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
|
|
|
|
|
|
|
|
|
// Combine existing and new tasks
|
|
|
|
|
const allTasks = new Map();
|
|
|
|
|
@ -887,30 +876,15 @@ class ManifestGenerator {
|
|
|
|
|
// Add/update new tasks
|
|
|
|
|
for (const task of this.tasks) {
|
|
|
|
|
const key = `${task.module}:${task.name}`;
|
|
|
|
|
allTasks.set(key, {
|
|
|
|
|
name: task.name,
|
|
|
|
|
displayName: task.displayName,
|
|
|
|
|
description: task.description,
|
|
|
|
|
module: task.module,
|
|
|
|
|
path: task.path,
|
|
|
|
|
standalone: task.standalone,
|
|
|
|
|
});
|
|
|
|
|
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write all tasks
|
|
|
|
|
for (const [, record] of allTasks) {
|
|
|
|
|
const row = [
|
|
|
|
|
escapeCsv(record.name),
|
|
|
|
|
escapeCsv(record.displayName),
|
|
|
|
|
escapeCsv(record.description),
|
|
|
|
|
escapeCsv(record.module),
|
|
|
|
|
escapeCsv(record.path),
|
|
|
|
|
escapeCsv(record.standalone),
|
|
|
|
|
].join(',');
|
|
|
|
|
csvContent += row + '\n';
|
|
|
|
|
for (const [, value] of allTasks) {
|
|
|
|
|
csv += value + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(csvPath, csvContent);
|
|
|
|
|
await fs.writeFile(csvPath, csv);
|
|
|
|
|
return csvPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -920,23 +894,30 @@ class ManifestGenerator {
|
|
|
|
|
*/
|
|
|
|
|
async writeToolManifest(cfgDir) {
|
|
|
|
|
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
|
|
|
|
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
|
|
|
|
|
|
|
|
|
// Read existing manifest to preserve entries
|
|
|
|
|
const existingEntries = new Map();
|
|
|
|
|
if (await fs.pathExists(csvPath)) {
|
|
|
|
|
const content = await fs.readFile(csvPath, 'utf8');
|
|
|
|
|
const records = csv.parse(content, {
|
|
|
|
|
columns: true,
|
|
|
|
|
skip_empty_lines: true,
|
|
|
|
|
});
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
existingEntries.set(`${record.module}:${record.name}`, record);
|
|
|
|
|
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) {
|
|
|
|
|
// 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
|
|
|
|
|
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
|
|
|
|
let csv = 'name,displayName,description,module,path,standalone\n';
|
|
|
|
|
|
|
|
|
|
// Combine existing and new tools
|
|
|
|
|
const allTools = new Map();
|
|
|
|
|
@ -949,30 +930,15 @@ class ManifestGenerator {
|
|
|
|
|
// Add/update new tools
|
|
|
|
|
for (const tool of this.tools) {
|
|
|
|
|
const key = `${tool.module}:${tool.name}`;
|
|
|
|
|
allTools.set(key, {
|
|
|
|
|
name: tool.name,
|
|
|
|
|
displayName: tool.displayName,
|
|
|
|
|
description: tool.description,
|
|
|
|
|
module: tool.module,
|
|
|
|
|
path: tool.path,
|
|
|
|
|
standalone: tool.standalone,
|
|
|
|
|
});
|
|
|
|
|
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write all tools
|
|
|
|
|
for (const [, record] of allTools) {
|
|
|
|
|
const row = [
|
|
|
|
|
escapeCsv(record.name),
|
|
|
|
|
escapeCsv(record.displayName),
|
|
|
|
|
escapeCsv(record.description),
|
|
|
|
|
escapeCsv(record.module),
|
|
|
|
|
escapeCsv(record.path),
|
|
|
|
|
escapeCsv(record.standalone),
|
|
|
|
|
].join(',');
|
|
|
|
|
csvContent += row + '\n';
|
|
|
|
|
for (const [, value] of allTools) {
|
|
|
|
|
csv += value + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fs.writeFile(csvPath, csvContent);
|
|
|
|
|
await fs.writeFile(csvPath, csv);
|
|
|
|
|
return csvPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|