From 2fc1abe3969c6b8f0a29d290aeb47dac21c4d9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davor=20Raci=C4=87?= Date: Mon, 2 Feb 2026 12:03:22 +0100 Subject: [PATCH] fix: use csv-parse library for proper CSV handling in manifest generation --- .../installers/lib/core/manifest-generator.js | 150 +++++++++++------- 1 file changed, 90 insertions(+), 60 deletions(-) diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index a0d4b7ec..5221b616 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -2,6 +2,7 @@ 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 @@ -783,30 +784,23 @@ 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 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); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with persona fields - let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; + let csvContent = '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(); @@ -819,18 +813,38 @@ class ManifestGenerator { // Add/update new agents for (const agent of this.agents) { const key = `${agent.module}:${agent.name}`; - allAgents.set( - key, - `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`, - ); + 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, + }); } // Write all agents - for (const [, value] of allAgents) { - csv += value + '\n'; + 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'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -840,30 +854,23 @@ 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 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); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -876,15 +883,30 @@ class ManifestGenerator { // Add/update new tasks for (const task of this.tasks) { const key = `${task.module}:${task.name}`; - allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`); + allTasks.set(key, { + name: task.name, + displayName: task.displayName, + description: task.description, + module: task.module, + path: task.path, + standalone: task.standalone, + }); } // Write all tasks - for (const [, value] of allTasks) { - csv += value + '\n'; + 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'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -894,30 +916,23 @@ 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 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); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tools const allTools = new Map(); @@ -930,15 +945,30 @@ class ManifestGenerator { // Add/update new tools for (const tool of this.tools) { const key = `${tool.module}:${tool.name}`; - allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`); + allTools.set(key, { + name: tool.name, + displayName: tool.displayName, + description: tool.description, + module: tool.module, + path: tool.path, + standalone: tool.standalone, + }); } // Write all tools - for (const [, value] of allTools) { - csv += value + '\n'; + 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'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; }