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.
This commit is contained in:
Alex Verkhovsky 2026-03-21 00:02:56 -06:00
parent 02879f7b6b
commit 0a0d9400bb
3 changed files with 5 additions and 659 deletions

View File

@ -98,17 +98,6 @@ async function createSkillCollisionFixture() {
].join('\n'), ].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( await fs.writeFile(
path.join(configDir, 'skill-manifest.csv'), 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', '---\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'); const wfSkillDir29 = path.join(tempFixture29, 'core', 'workflows', 'wf-skill');
await fs.ensureDir(wfSkillDir29); await fs.ensureDir(wfSkillDir29);
await fs.writeFile( await fs.writeFile(
@ -1593,18 +1582,10 @@ async function runTests() {
'Skill path includes relative path from module root', '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 // Skill in tasks/ dir should be in skills
const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill'); const taskSkillEntry29 = generator29.skills.find((s) => s.canonicalId === 'task-skill');
assert(taskSkillEntry29 !== undefined, 'Skill in tasks/ dir appears in skills[]'); 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 // Native agent entrypoint should be installed as a verbatim skill and also
// remain visible to the agent manifest pipeline. // remain visible to the agent manifest pipeline.
const nativeAgentEntry29 = generator29.skills.find((s) => s.canonicalId === 'bmad-tea'); 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'); 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'); assert(nativeAgentManifest29 !== undefined, 'Native type:agent SKILL.md dir appears in agents[] for agent metadata');
// Regular workflow should be in workflows, NOT in skills // Regular type:workflow should NOT appear in skills[]
const regularWf29 = generator29.workflows.find((w) => w.name === 'Regular Workflow');
assert(regularWf29 !== undefined, 'Regular type:workflow appears in workflows[]');
const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf'); const regularInSkills29 = generator29.skills.find((s) => s.canonicalId === 'regular-wf');
assert(regularInSkills29 === undefined, 'Regular type:workflow does NOT appear in skills[]'); 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'); const wfSkill29 = generator29.skills.find((s) => s.canonicalId === 'wf-skill');
assert(wfSkill29 !== undefined, 'Skill in workflows/ dir appears in skills[]'); 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 // Test scanInstalledModules recognizes skill-only modules
const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod'); const skillOnlyModDir29 = path.join(tempFixture29, 'skill-only-mod');

View File

@ -1124,11 +1124,9 @@ class Installer {
// Pre-register manifest files // Pre-register manifest files
const cfgDir = path.join(bmadDir, '_config'); const cfgDir = path.join(bmadDir, '_config');
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml')); 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, '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 // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv
message('Generating manifests...'); message('Generating manifests...');
const manifestGen = new ManifestGenerator(); const manifestGen = new ManifestGenerator();

View File

@ -16,15 +16,12 @@ const {
const packageJson = require('../../../../../package.json'); const packageJson = require('../../../../../package.json');
/** /**
* Generates manifest files for installed workflows, agents, and tasks * Generates manifest files for installed skills and agents
*/ */
class ManifestGenerator { class ManifestGenerator {
constructor() { constructor() {
this.workflows = [];
this.skills = []; this.skills = [];
this.agents = []; this.agents = [];
this.tasks = [];
this.tools = [];
this.modules = []; this.modules = [];
this.files = []; this.files = [];
this.selectedIdes = []; this.selectedIdes = [];
@ -85,10 +82,6 @@ class ManifestGenerator {
this.modules = allModules; this.modules = allModules;
this.updatedModules = allModules; // Include ALL modules (including custom) for scanning 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.bmadDir = bmadDir;
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad') this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
this.allInstalledFiles = installedFiles; this.allInstalledFiles = installedFiles;
@ -111,35 +104,20 @@ class ManifestGenerator {
// Collect skills first (populates skillClaimedDirs before legacy collectors run) // Collect skills first (populates skillClaimedDirs before legacy collectors run)
await this.collectSkills(); await this.collectSkills();
// Collect workflow data
await this.collectWorkflows(selectedModules);
// Collect agent data - use updatedModules which includes all installed modules // Collect agent data - use updatedModules which includes all installed modules
await this.collectAgents(this.updatedModules); 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 // Write manifest files and collect their paths
const manifestFiles = [ const manifestFiles = [
await this.writeMainManifest(cfgDir), await this.writeMainManifest(cfgDir),
await this.writeWorkflowManifest(cfgDir),
await this.writeSkillManifest(cfgDir), await this.writeSkillManifest(cfgDir),
await this.writeAgentManifest(cfgDir), await this.writeAgentManifest(cfgDir),
await this.writeTaskManifest(cfgDir),
await this.writeToolManifest(cfgDir),
await this.writeFilesManifest(cfgDir), await this.writeFilesManifest(cfgDir),
]; ];
return { return {
skills: this.skills.length, skills: this.skills.length,
workflows: this.workflows.length,
agents: this.agents.length, agents: this.agents.length,
tasks: this.tasks.length,
tools: this.tools.length,
files: this.files.length, files: this.files.length,
manifestFiles: manifestFiles, 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 * Collect all agents from core and selected modules
* Scans the INSTALLED bmad directory, not the source * Scans the INSTALLED bmad directory, not the source
@ -589,212 +420,6 @@ class ManifestGenerator {
return agents; 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>([^<]+)<\/objective>/);
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneFalseMatch = content.match(/<task[^>]+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>([^<]+)<\/objective>/);
description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneFalseMatch = content.match(/<tool[^>]+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 * Write main manifest as YAML with installation info only
* Fetches fresh version info for all modules * Fetches fresh version info for all modules
@ -880,131 +505,6 @@ class ManifestGenerator {
return manifestPath; 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<string>} 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<string>} rowValues - Values from old row
* @param {Array<string>} oldColumns - Old column names
* @param {Array<string>} 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 * Write skill manifest CSV
* @returns {string} Path to the manifest file * @returns {string} Path to the manifest file
@ -1105,134 +605,6 @@ class ManifestGenerator {
return csvPath; 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 * Write files manifest CSV
*/ */