Compare commits

..

1 Commits

Author SHA1 Message Date
Davor Racic 2edf03c63d
Merge 5d470b2de3 into 323cd75efd 2026-02-02 11:16:31 +01:00
3 changed files with 82 additions and 116 deletions

View File

@ -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
@ -215,7 +201,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,
}); });
@ -333,15 +319,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,
}); });
@ -408,7 +403,7 @@ class ManifestGenerator {
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 || '';
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -421,7 +416,7 @@ 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 standaloneMatch = content.match(/<task[^>]+standalone="true"/); const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -434,7 +429,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,
@ -502,7 +497,7 @@ class ManifestGenerator {
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 || '';
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -515,7 +510,7 @@ 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 standaloneMatch = content.match(/<tool[^>]+standalone="true"/); const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -528,7 +523,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,
@ -788,23 +783,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();
@ -817,38 +819,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;
} }
@ -858,23 +840,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();
@ -887,30 +876,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;
} }
@ -920,23 +894,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();
@ -949,30 +930,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;
} }

View File

@ -297,7 +297,7 @@ class CustomHandler {
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']); const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
for (const agentFile of agentFiles) { for (const agentFile of agentFiles) {
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);

View File

@ -871,7 +871,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);