From 93a1e1dc46c85be869a53f50dcaedf8f851b7cee Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 21 Mar 2026 00:12:40 -0600 Subject: [PATCH] refactor(installer): remove dead task/tool/workflow manifest code (#2083) * refactor(installer): discover skills by SKILL.md instead of manifest YAML Switch skill discovery gate from requiring bmad-skill-manifest.yaml with type: skill to detecting any directory with a valid SKILL.md (frontmatter name + description, name matches directory name). Delete 34 stub manifests that carried no data beyond type: skill. Agent manifests (9) are retained for persona metadata consumed by agent-manifest.csv. * refactor(installer): remove dead task/tool/workflow manifest code The remove-skill-manifest-yaml branch deleted the scanners that discover tasks, tools, and workflows but left behind the code that writes their manifest CSVs. Remove collectTasks/Tools/Workflows, writeTaskManifest/ToolManifest/WorkflowManifest, their helpers, and the now-unreachable getPreservedCsvRows/upgradeRowToSchema methods. Update installer pre-registration and test assertions accordingly. --- test/test-installation-components.js | 30 +- tools/cli/installers/lib/core/installer.js | 4 +- .../installers/lib/core/manifest-generator.js | 630 +----------------- 3 files changed, 5 insertions(+), 659 deletions(-) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 44455fbb7..c5b04a1ee 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -98,17 +98,6 @@ async function createSkillCollisionFixture() { ].join('\n'), ); - await fs.writeFile( - path.join(configDir, 'workflow-manifest.csv'), - [ - 'name,description,module,path,canonicalId', - '"help","Workflow help","core","_bmad/core/workflows/help/workflow.md","bmad-help"', - '', - ].join('\n'), - ); - - await fs.writeFile(path.join(configDir, 'task-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n'); - await fs.writeFile(path.join(configDir, 'tool-manifest.csv'), 'name,displayName,description,module,path,standalone,canonicalId\n'); await fs.writeFile( path.join(configDir, 'skill-manifest.csv'), [ @@ -1549,7 +1538,7 @@ async function runTests() { '---\nname: Regular Workflow\ndescription: A regular workflow not a skill\n---\n\nWorkflow body\n', ); - // --- Skill inside workflows/ dir: core/workflows/wf-skill/ (exercises findWorkflows skip logic) --- + // --- Skill inside workflows/ dir: core/workflows/wf-skill/ --- const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill'); await fs.ensureDir(wfSkillDir29); await fs.writeFile( @@ -1593,18 +1582,10 @@ async function runTests() { 'Skill path includes relative path from module root', ); - // Skill should NOT be in workflows - const inWorkflows29 = generator29.workflows.find((w) => w.name === 'my-skill'); - assert(inWorkflows29 === undefined, 'Skill at unusual path does NOT appear in workflows[]'); - // Skill in tasks/ dir should be in skills const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill'); assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]'); - // Skill in tasks/ should NOT appear in tasks[] - const inTasks29 = generator29.tasks.find((t) => t.name === 'task-skill'); - assert(inTasks29 === undefined, 'Skill in tasks/ dir does NOT appear in tasks[]'); - // Native agent entrypoint should be installed as a verbatim skill and also // remain visible to the agent manifest pipeline. const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea'); @@ -1616,18 +1597,13 @@ async function runTests() { const nativeAgentManifest29 = generator29.agents.find((a) => a.name === 'bmad-tea'); assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata'); - // Regular workflow should be in workflows, NOT in skills - const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow'); - assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]'); - + // Regular type:workflow should NOT appear in skills[] const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf'); assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]'); - // Skill inside workflows/ should be in skills[], NOT in workflows[] (exercises findWorkflows skip at lines 311/322) + // Skill inside workflows/ should be in skills[] const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill'); assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]'); - const wfSkillInWorkflows29 = generator29.workflows.find((w) => w.name === 'wf-skill'); - assert(wfSkillInWorkflows29 === undefined, 'Skill in workflows/ dir does NOT appear in workflows[]'); // Test scanInstalledModules recognizes skill-only modules const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod'); diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index dd3902657..217da91ec 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1124,11 +1124,9 @@ class Installer { // Pre-register manifest files const cfgDir = path.join(bmadDir, '_config'); this.installedFiles.add(path.join(cfgDir, 'manifest.yaml')); - this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv')); this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv')); - this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv')); - // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes + // Generate CSV manifests for agents, skills AND ALL FILES with hashes // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv message('Generating manifests...'); const manifestGen = new ManifestGenerator(); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 69b6b509f..0dd0b24e4 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -16,15 +16,12 @@ const { const packageJson = require('../../../../../package.json'); /** - * Generates manifest files for installed workflows, agents, and tasks + * Generates manifest files for installed skills and agents */ class ManifestGenerator { constructor() { - this.workflows = []; this.skills = []; this.agents = []; - this.tasks = []; - this.tools = []; this.modules = []; this.files = []; this.selectedIdes = []; @@ -85,10 +82,6 @@ class ManifestGenerator { this.modules = allModules; this.updatedModules = allModules; // Include ALL modules (including custom) for scanning - // For CSV manifests, we need to include ALL modules that are installed - // preservedModules controls which modules stay as-is in the CSV (don't get rescanned) - // But all modules should be included in the final manifest - this.preservedModules = allModules; // Include ALL modules (including custom) this.bmadDir = bmadDir; this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad') this.allInstalledFiles = installedFiles; @@ -111,35 +104,20 @@ class ManifestGenerator { // Collect skills first (populates skillClaimedDirs before legacy collectors run) await this.collectSkills(); - // Collect workflow data - await this.collectWorkflows(selectedModules); - // Collect agent data - use updatedModules which includes all installed modules await this.collectAgents(this.updatedModules); - // Collect task data - await this.collectTasks(this.updatedModules); - - // Collect tool data - await this.collectTools(this.updatedModules); - // Write manifest files and collect their paths const manifestFiles = [ await this.writeMainManifest(cfgDir), - await this.writeWorkflowManifest(cfgDir), await this.writeSkillManifest(cfgDir), await this.writeAgentManifest(cfgDir), - await this.writeTaskManifest(cfgDir), - await this.writeToolManifest(cfgDir), await this.writeFilesManifest(cfgDir), ]; return { skills: this.skills.length, - workflows: this.workflows.length, agents: this.agents.length, - tasks: this.tasks.length, - tools: this.tools.length, files: this.files.length, manifestFiles: manifestFiles, }; @@ -289,153 +267,6 @@ class ManifestGenerator { } } - /** - * Collect all workflows from core and selected modules - * Scans the INSTALLED bmad directory, not the source - */ - async collectWorkflows(selectedModules) { - this.workflows = []; - - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const modulePath = path.join(this.bmadDir, moduleName); - - if (await fs.pathExists(modulePath)) { - const moduleWorkflows = await this.getWorkflowsFromPath(modulePath, moduleName); - this.workflows.push(...moduleWorkflows); - - // Also scan tasks/ for type:skill entries (skills can live anywhere) - const tasksSkills = await this.getWorkflowsFromPath(modulePath, moduleName, 'tasks'); - this.workflows.push(...tasksSkills); - } - } - } - - /** - * Recursively find and parse workflow.md files - */ - async getWorkflowsFromPath(basePath, moduleName, subDir = 'workflows') { - const workflows = []; - const workflowsPath = path.join(basePath, subDir); - const debug = process.env.BMAD_DEBUG_MANIFEST === 'true'; - - if (debug) { - console.log(`[DEBUG] Scanning workflows in: ${workflowsPath}`); - } - - if (!(await fs.pathExists(workflowsPath))) { - if (debug) { - console.log(`[DEBUG] Workflows path does not exist: ${workflowsPath}`); - } - return workflows; - } - - // Recursively find workflow.md files - const findWorkflows = async (dir, relativePath = '') => { - // Skip directories already claimed as skills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(dir)) return; - - const entries = await fs.readdir(dir, { withFileTypes: true }); - // Load skill manifest for this directory (if present) - const skillManifest = await this.loadSkillManifest(dir); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - // Skip directories claimed by collectSkills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(fullPath)) continue; - // Recurse into subdirectories - const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - await findWorkflows(fullPath, newRelativePath); - } else if (entry.name === 'workflow.md' || (entry.name.startsWith('workflow-') && entry.name.endsWith('.md'))) { - // Parse workflow file (both YAML and MD formats) - if (debug) { - console.log(`[DEBUG] Found workflow file: ${fullPath}`); - } - try { - // Read and normalize line endings (fix Windows CRLF issues) - const rawContent = await fs.readFile(fullPath, 'utf8'); - const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); - - // Parse MD workflow with YAML frontmatter - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (!frontmatterMatch) { - if (debug) { - console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); - } - continue; // Skip MD files without frontmatter - } - const workflow = yaml.parse(frontmatterMatch[1]); - - if (debug) { - console.log(`[DEBUG] Parsed: name="${workflow.name}", description=${workflow.description ? 'OK' : 'MISSING'}`); - } - - // Skip template workflows (those with placeholder values) - if (workflow.name && workflow.name.includes('{') && workflow.name.includes('}')) { - if (debug) { - console.log(`[DEBUG] Skipped (template placeholder): ${workflow.name}`); - } - continue; - } - - // Skip workflows marked as non-standalone (reference/example workflows) - if (workflow.standalone === false) { - if (debug) { - console.log(`[DEBUG] Skipped (standalone=false): ${workflow.name}`); - } - continue; - } - - if (workflow.name && workflow.description) { - // Build relative path for installation - const installPath = - moduleName === 'core' - ? `${this.bmadFolderName}/core/${subDir}/${relativePath}/${entry.name}` - : `${this.bmadFolderName}/${moduleName}/${subDir}/${relativePath}/${entry.name}`; - - // Workflows with standalone: false are filtered out above - workflows.push({ - name: workflow.name, - description: this.cleanForCSV(workflow.description), - module: moduleName, - path: installPath, - canonicalId: this.getCanonicalId(skillManifest, entry.name), - }); - - // Add to files list - this.files.push({ - type: 'workflow', - name: workflow.name, - module: moduleName, - path: installPath, - }); - - if (debug) { - console.log(`[DEBUG] ✓ Added workflow: ${workflow.name} (${moduleName})`); - } - } else { - if (debug) { - console.log(`[DEBUG] Skipped (missing name or description): ${fullPath}`); - } - } - } catch (error) { - await prompts.log.warn(`Failed to parse workflow at ${fullPath}: ${error.message}`); - } - } - } - }; - - await findWorkflows(workflowsPath); - - if (debug) { - console.log(`[DEBUG] Total workflows found in ${moduleName}: ${workflows.length}`); - } - - return workflows; - } - /** * Collect all agents from core and selected modules * Scans the INSTALLED bmad directory, not the source @@ -589,212 +420,6 @@ class ManifestGenerator { return agents; } - /** - * Collect all tasks from core and selected modules - * Scans the INSTALLED bmad directory, not the source - */ - async collectTasks(selectedModules) { - this.tasks = []; - - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const tasksPath = path.join(this.bmadDir, moduleName, 'tasks'); - - if (await fs.pathExists(tasksPath)) { - const moduleTasks = await this.getTasksFromDir(tasksPath, moduleName); - this.tasks.push(...moduleTasks); - } - } - } - - /** - * Get tasks from a directory - */ - async getTasksFromDir(dirPath, moduleName) { - // Skip directories claimed by collectSkills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return []; - const tasks = []; - const files = await fs.readdir(dirPath); - // Load skill manifest for this directory (if present) - const skillManifest = await this.loadSkillManifest(dirPath); - - for (const file of files) { - // Check for both .xml and .md files - if (file.endsWith('.xml') || file.endsWith('.md')) { - const filePath = path.join(dirPath, file); - 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 displayName = name; - let description = ''; - let standalone = false; - - if (file.endsWith('.md')) { - // Parse YAML frontmatter for .md tasks - const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (frontmatterMatch) { - try { - const frontmatter = yaml.parse(frontmatterMatch[1]); - name = frontmatter.name || name; - displayName = frontmatter.displayName || frontmatter.name || name; - description = this.cleanForCSV(frontmatter.description || ''); - // Tasks are standalone by default unless explicitly false (internal=true is already filtered above) - standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; - } catch { - // If YAML parsing fails, use defaults - standalone = true; // Default to standalone - } - } else { - standalone = true; // No frontmatter means standalone - } - } else { - // For .xml tasks, extract from tag attributes - const nameMatch = content.match(/name="([^"]+)"/); - displayName = nameMatch ? nameMatch[1] : name; - - const descMatch = content.match(/description="([^"]+)"/); - const objMatch = content.match(/([^<]+)<\/objective>/); - description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - - const standaloneFalseMatch = content.match(/]+standalone="false"/); - standalone = !standaloneFalseMatch; - } - - // Build relative path for installation - const installPath = - moduleName === 'core' ? `${this.bmadFolderName}/core/tasks/${file}` : `${this.bmadFolderName}/${moduleName}/tasks/${file}`; - - tasks.push({ - name: name, - displayName: displayName, - description: description, - module: moduleName, - path: installPath, - standalone: standalone, - canonicalId: this.getCanonicalId(skillManifest, file), - }); - - // Add to files list - this.files.push({ - type: 'task', - name: name, - module: moduleName, - path: installPath, - }); - } - } - - return tasks; - } - - /** - * Collect all tools from core and selected modules - * Scans the INSTALLED bmad directory, not the source - */ - async collectTools(selectedModules) { - this.tools = []; - - // Use updatedModules which already includes deduplicated 'core' + selectedModules - for (const moduleName of this.updatedModules) { - const toolsPath = path.join(this.bmadDir, moduleName, 'tools'); - - if (await fs.pathExists(toolsPath)) { - const moduleTools = await this.getToolsFromDir(toolsPath, moduleName); - this.tools.push(...moduleTools); - } - } - } - - /** - * Get tools from a directory - */ - async getToolsFromDir(dirPath, moduleName) { - // Skip directories claimed by collectSkills - if (this.skillClaimedDirs && this.skillClaimedDirs.has(dirPath)) return []; - const tools = []; - const files = await fs.readdir(dirPath); - // Load skill manifest for this directory (if present) - const skillManifest = await this.loadSkillManifest(dirPath); - - for (const file of files) { - // Check for both .xml and .md files - if (file.endsWith('.xml') || file.endsWith('.md')) { - const filePath = path.join(dirPath, file); - 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 displayName = name; - let description = ''; - let standalone = false; - - if (file.endsWith('.md')) { - // Parse YAML frontmatter for .md tools - const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (frontmatterMatch) { - try { - const frontmatter = yaml.parse(frontmatterMatch[1]); - name = frontmatter.name || name; - displayName = frontmatter.displayName || frontmatter.name || name; - description = this.cleanForCSV(frontmatter.description || ''); - // Tools are standalone by default unless explicitly false (internal=true is already filtered above) - standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; - } catch { - // If YAML parsing fails, use defaults - standalone = true; // Default to standalone - } - } else { - standalone = true; // No frontmatter means standalone - } - } else { - // For .xml tools, extract from tag attributes - const nameMatch = content.match(/name="([^"]+)"/); - displayName = nameMatch ? nameMatch[1] : name; - - const descMatch = content.match(/description="([^"]+)"/); - const objMatch = content.match(/([^<]+)<\/objective>/); - description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - - const standaloneFalseMatch = content.match(/]+standalone="false"/); - standalone = !standaloneFalseMatch; - } - - // Build relative path for installation - const installPath = - moduleName === 'core' ? `${this.bmadFolderName}/core/tools/${file}` : `${this.bmadFolderName}/${moduleName}/tools/${file}`; - - tools.push({ - name: name, - displayName: displayName, - description: description, - module: moduleName, - path: installPath, - standalone: standalone, - canonicalId: this.getCanonicalId(skillManifest, file), - }); - - // Add to files list - this.files.push({ - type: 'tool', - name: name, - module: moduleName, - path: installPath, - }); - } - } - - return tools; - } - /** * Write main manifest as YAML with installation info only * Fetches fresh version info for all modules @@ -880,131 +505,6 @@ class ManifestGenerator { return manifestPath; } - /** - * Read existing CSV and preserve rows for modules NOT being updated - * @param {string} csvPath - Path to existing CSV file - * @param {number} moduleColumnIndex - Which column contains the module name (0-indexed) - * @param {Array} expectedColumns - Expected column names in order - * @param {Object} defaultValues - Default values for missing columns - * @returns {Array} Preserved CSV rows (without header), upgraded to match expected columns - */ - async getPreservedCsvRows(csvPath, moduleColumnIndex, expectedColumns, defaultValues = {}) { - if (!(await fs.pathExists(csvPath)) || this.preservedModules.length === 0) { - return []; - } - - try { - const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.trim().split('\n'); - - if (lines.length < 2) { - return []; // No data rows - } - - // Parse header to understand old schema - const header = lines[0]; - const headerColumns = header.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - const oldColumns = headerColumns.map((c) => c.replaceAll(/^"|"$/g, '')); - - // Skip header row for data - const dataRows = lines.slice(1); - const preservedRows = []; - - for (const row of dataRows) { - // Simple CSV parsing (handles quoted values) - const columns = row.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - const cleanColumns = columns.map((c) => c.replaceAll(/^"|"$/g, '')); - - const moduleValue = cleanColumns[moduleColumnIndex]; - - // Keep this row if it belongs to a preserved module - if (this.preservedModules.includes(moduleValue)) { - // Upgrade row to match expected schema - const upgradedRow = this.upgradeRowToSchema(cleanColumns, oldColumns, expectedColumns, defaultValues); - preservedRows.push(upgradedRow); - } - } - - return preservedRows; - } catch (error) { - await prompts.log.warn(`Failed to read existing CSV ${csvPath}: ${error.message}`); - return []; - } - } - - /** - * Upgrade a CSV row from old schema to new schema - * @param {Array} rowValues - Values from old row - * @param {Array} oldColumns - Old column names - * @param {Array} newColumns - New column names - * @param {Object} defaultValues - Default values for missing columns - * @returns {string} Upgraded CSV row - */ - upgradeRowToSchema(rowValues, oldColumns, newColumns, defaultValues) { - const upgradedValues = []; - - for (const newCol of newColumns) { - const oldIndex = oldColumns.indexOf(newCol); - - if (oldIndex !== -1 && oldIndex < rowValues.length) { - // Column exists in old schema, use its value - upgradedValues.push(rowValues[oldIndex]); - } else if (defaultValues[newCol] === undefined) { - // Column missing, no default provided - upgradedValues.push(''); - } else { - // Column missing, use default value - upgradedValues.push(defaultValues[newCol]); - } - } - - // Properly quote values and join - return upgradedValues.map((v) => `"${v}"`).join(','); - } - - /** - * Write workflow manifest CSV - * @returns {string} Path to the manifest file - */ - async writeWorkflowManifest(cfgDir) { - const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); - const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - - // Create CSV header - standalone column removed, canonicalId added as optional column - let csv = 'name,description,module,path,canonicalId\n'; - - // Build workflows map from discovered workflows only - // Old entries are NOT preserved - the manifest reflects what actually exists on disk - const allWorkflows = new Map(); - - // Only add workflows that were actually discovered in this scan - for (const workflow of this.workflows) { - const key = `${workflow.module}:${workflow.name}`; - allWorkflows.set(key, { - name: workflow.name, - description: workflow.description, - module: workflow.module, - path: workflow.path, - canonicalId: workflow.canonicalId || '', - }); - } - - // Write all workflows - for (const [, value] of allWorkflows) { - const row = [ - escapeCsv(value.name), - escapeCsv(value.description), - escapeCsv(value.module), - escapeCsv(value.path), - escapeCsv(value.canonicalId), - ].join(','); - csv += row + '\n'; - } - - await fs.writeFile(csvPath, csv); - return csvPath; - } - /** * Write skill manifest CSV * @returns {string} Path to the manifest file @@ -1105,134 +605,6 @@ class ManifestGenerator { return csvPath; } - /** - * Write task manifest CSV - * @returns {string} Path to the manifest file - */ - 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); - } - } - - // Create CSV header with standalone and canonicalId columns - let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; - - // Combine existing and new tasks - const allTasks = new Map(); - - // Add existing entries - for (const [key, value] of existingEntries) { - allTasks.set(key, value); - } - - // 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, - canonicalId: task.canonicalId || '', - }); - } - - // 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), - escapeCsv(record.canonicalId), - ].join(','); - csvContent += row + '\n'; - } - - await fs.writeFile(csvPath, csvContent); - return csvPath; - } - - /** - * Write tool manifest CSV - * @returns {string} Path to the manifest file - */ - 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); - } - } - - // Create CSV header with standalone and canonicalId columns - let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; - - // Combine existing and new tools - const allTools = new Map(); - - // Add existing entries - for (const [key, value] of existingEntries) { - allTools.set(key, value); - } - - // 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, - canonicalId: tool.canonicalId || '', - }); - } - - // 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), - escapeCsv(record.canonicalId), - ].join(','); - csvContent += row + '\n'; - } - - await fs.writeFile(csvPath, csvContent); - return csvPath; - } - /** * Write files manifest CSV */