From 2224edaa84bb72d0383ebfb262414a7a3f739b7b Mon Sep 17 00:00:00 2001 From: Dicky Moore Date: Thu, 5 Feb 2026 16:46:38 +0000 Subject: [PATCH] Drop YAML workflow support from CLI tooling --- .../installers/lib/core/manifest-generator.js | 294 ++++++++---------- tools/cli/installers/lib/ide/_base-ide.js | 14 +- .../cli/installers/lib/ide/_config-driven.js | 160 ++-------- .../ide/shared/workflow-command-generator.js | 9 +- tools/cli/installers/lib/modules/manager.js | 121 +------ 5 files changed, 183 insertions(+), 415 deletions(-) diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index eefc12b69..9883eac4c 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -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 @@ -159,12 +145,8 @@ class ManifestGenerator { // Recurse into subdirectories const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; await findWorkflows(fullPath, newRelativePath); - } else if ( - entry.name === 'workflow.yaml' || - entry.name === 'workflow.md' || - (entry.name.startsWith('workflow-') && entry.name.endsWith('.md')) - ) { - // Parse workflow file (both YAML and MD formats) + } else if (entry.name === 'workflow.md') { + // Parse workflow file (MD with YAML frontmatter) if (debug) { console.log(`[DEBUG] Found workflow file: ${fullPath}`); } @@ -173,21 +155,15 @@ class ManifestGenerator { const rawContent = await fs.readFile(fullPath, 'utf8'); const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); - let workflow; - if (entry.name === 'workflow.yaml') { - // Parse YAML workflow - workflow = yaml.parse(content); - } else { - // Parse MD workflow with YAML frontmatter - const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (!frontmatterMatch) { - if (debug) { - console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); - } - continue; // Skip MD files without frontmatter + // 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}`); } - workflow = yaml.parse(frontmatterMatch[1]); + 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'}`); @@ -219,7 +195,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, }); @@ -337,15 +313,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, }); @@ -394,11 +379,6 @@ class ManifestGenerator { 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 = ''; @@ -406,21 +386,17 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tasks - const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\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'; + description = frontmatter.description || ''; + standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } 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 @@ -429,10 +405,10 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); + description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; - const standaloneFalseMatch = content.match(/]+standalone="false"/); - standalone = !standaloneFalseMatch; + const standaloneMatch = content.match(/]+standalone="true"/); + standalone = !!standaloneMatch; } // Build relative path for installation @@ -442,7 +418,7 @@ class ManifestGenerator { tasks.push({ name: name, displayName: displayName, - description: description, + description: description.replaceAll('"', '""'), module: moduleName, path: installPath, standalone: standalone, @@ -492,11 +468,6 @@ class ManifestGenerator { 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 = ''; @@ -504,21 +475,17 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tools - const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\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'; + description = frontmatter.description || ''; + standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } 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 @@ -527,10 +494,10 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); + description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; - const standaloneFalseMatch = content.match(/]+standalone="false"/); - standalone = !standaloneFalseMatch; + const standaloneMatch = content.match(/]+standalone="true"/); + standalone = !!standaloneMatch; } // Build relative path for installation @@ -540,7 +507,7 @@ class ManifestGenerator { tools.push({ name: name, displayName: displayName, - description: description, + description: description.replaceAll('"', '""'), module: moduleName, path: installPath, standalone: standalone, @@ -733,15 +700,47 @@ class ManifestGenerator { async writeWorkflowManifest(cfgDir) { const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; + const parseCsvLine = (line) => { + const columns = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; + return columns.map((c) => c.replaceAll(/^"|"$/g, '')); + }; + + // 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) { + const parts = parseCsvLine(line); + if (parts.length >= 4) { + const [name, description, module, workflowPath] = parts; + existingEntries.set(`${module}:${name}`, { + name, + description, + module, + path: workflowPath, + }); + } + } + } + } // Create CSV header - standalone column removed, everything is canonicalized to 4 columns let csv = 'name,description,module,path\n'; - // Build workflows map from discovered workflows only - // Old entries are NOT preserved - the manifest reflects what actually exists on disk + // Combine existing and new workflows const allWorkflows = new Map(); - // Only add workflows that were actually discovered in this scan + // Add existing entries + for (const [key, value] of existingEntries) { + allWorkflows.set(key, value); + } + + // Add/update new workflows for (const workflow of this.workflows) { const key = `${workflow.module}:${workflow.name}`; allWorkflows.set(key, { @@ -768,23 +767,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(); @@ -797,38 +803,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; } @@ -838,23 +824,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(); @@ -867,30 +860,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; } @@ -900,23 +878,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(); @@ -929,30 +914,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; } diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index b82f3b6cf..3c25befee 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -344,22 +344,18 @@ class BaseIdeSetup { // Recursively search subdirectories const subWorkflows = await this.findWorkflowFiles(fullPath); workflows.push(...subWorkflows); - } else if (entry.isFile() && (entry.name === 'workflow.yaml' || entry.name === 'workflow.md')) { + } else if (entry.isFile() && entry.name === 'workflow.md') { // Read workflow file to get name and standalone property try { const yaml = require('yaml'); const content = await fs.readFile(fullPath, 'utf8'); let workflowData = null; - if (entry.name === 'workflow.yaml') { - workflowData = yaml.parse(content); - } else { - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (!frontmatterMatch) { - continue; - } - workflowData = yaml.parse(frontmatterMatch[1]); + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + continue; } + workflowData = yaml.parse(frontmatterMatch[1]); if (workflowData && workflowData.name) { // Workflows are standalone by default unless explicitly false diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 486889267..0ceff98a8 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -66,13 +66,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { */ async installToTarget(projectDir, bmadDir, config, options) { const { target_dir, template_type, artifact_types } = config; - - // Skip targets with explicitly empty artifact_types array - // This prevents creating empty directories when no artifacts will be written - if (Array.isArray(artifact_types) && artifact_types.length === 0) { - return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } }; - } - const targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); @@ -93,11 +86,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); } - // Install tasks and tools using template system (supports TOML for Gemini, MD for others) + // Install tasks and tools if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { - const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); - const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); - const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config); + const taskToolGen = new TaskToolCommandGenerator(); + const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); results.tasks = taskToolResult.tasks || 0; results.tools = taskToolResult.tools || 0; } @@ -140,12 +132,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { */ async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) { // Try to load platform-specific template, fall back to default-agent - const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent'); + const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent'); let count = 0; for (const artifact of artifacts) { const content = this.renderTemplate(template, artifact); - const filename = this.generateFilename(artifact, 'agent', extension); + const filename = this.generateFilename(artifact, 'agent'); const filePath = path.join(targetPath, filename); await this.writeFile(filePath, content); count++; @@ -167,18 +159,14 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { for (const artifact of artifacts) { if (artifact.type === 'workflow-command') { - // Use different template based on workflow type (YAML vs MD) // Default to 'default' template type, but allow override via config - const workflowTemplateType = artifact.isYamlWorkflow - ? config.yaml_workflow_template || `${templateType}-workflow-yaml` - : config.md_workflow_template || `${templateType}-workflow`; + const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`; - // Fall back to default templates if specific ones don't exist - const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow'; - // workflowTemplateType already contains full name (e.g., 'gemini-workflow-yaml'), so pass empty artifactType - const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType); + // Fall back to default template if the requested one doesn't exist + const finalTemplateType = 'default-workflow'; + const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType); const content = this.renderTemplate(template, artifact); - const filename = this.generateFilename(artifact, 'workflow', extension); + const filename = this.generateFilename(artifact, 'workflow'); const filePath = path.join(targetPath, filename); await this.writeFile(filePath, content); count++; @@ -188,100 +176,40 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return count; } - /** - * Write task/tool artifacts to target directory using templates - * @param {string} targetPath - Target directory path - * @param {Array} artifacts - Task/tool artifacts - * @param {string} templateType - Template type to use - * @param {Object} config - Installation configuration - * @returns {Promise} Counts of tasks and tools written - */ - async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) { - let taskCount = 0; - let toolCount = 0; - - // Pre-load templates to avoid repeated file I/O in the loop - const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task'); - const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool'); - - const { artifact_types } = config; - - for (const artifact of artifacts) { - if (artifact.type !== 'task' && artifact.type !== 'tool') { - continue; - } - - // Skip if the specific artifact type is not requested in config - if (artifact_types) { - if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue; - if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue; - } - - // Use pre-loaded template based on artifact type - const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate; - - const content = this.renderTemplate(template, artifact); - const filename = this.generateFilename(artifact, artifact.type, extension); - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); - - if (artifact.type === 'task') { - taskCount++; - } else { - toolCount++; - } - } - - return { tasks: taskCount, tools: toolCount }; - } - /** * Load template based on type and configuration * @param {string} templateType - Template type (claude, windsurf, etc.) * @param {string} artifactType - Artifact type (agent, workflow, task, tool) * @param {Object} config - Installation configuration * @param {string} fallbackTemplateType - Fallback template type if requested template not found - * @returns {Promise<{content: string, extension: string}>} Template content and extension + * @returns {Promise} Template content */ async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) { const { header_template, body_template } = config; // Check for separate header/body templates if (header_template || body_template) { - const content = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template); - // Allow config to override extension, default to .md - const ext = config.extension || '.md'; - const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`; - return { content, extension: normalizedExt }; + return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template); } - // Load combined template - try multiple extensions - // If artifactType is empty, templateType already contains full name (e.g., 'gemini-workflow-yaml') - const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType; - const templateDir = path.join(__dirname, 'templates', 'combined'); - const extensions = ['.md', '.toml', '.yaml', '.yml']; + // Load combined template + const templateName = `${templateType}-${artifactType}.md`; + const templatePath = path.join(__dirname, 'templates', 'combined', templateName); - for (const ext of extensions) { - const templatePath = path.join(templateDir, templateBaseName + ext); - if (await fs.pathExists(templatePath)) { - const content = await fs.readFile(templatePath, 'utf8'); - return { content, extension: ext }; - } + if (await fs.pathExists(templatePath)) { + return await fs.readFile(templatePath, 'utf8'); } // Fall back to default template (if provided) if (fallbackTemplateType) { - for (const ext of extensions) { - const fallbackPath = path.join(templateDir, `${fallbackTemplateType}${ext}`); - if (await fs.pathExists(fallbackPath)) { - const content = await fs.readFile(fallbackPath, 'utf8'); - return { content, extension: ext }; - } + const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`); + if (await fs.pathExists(fallbackPath)) { + return await fs.readFile(fallbackPath, 'utf8'); } } // Ultimate fallback - minimal template - return { content: this.getDefaultTemplate(artifactType), extension: '.md' }; + return this.getDefaultTemplate(artifactType); } /** @@ -338,7 +266,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return `--- name: '{{name}}' description: '{{description}}' -disable-model-invocation: true --- You must fully embody this agent's persona and follow all activation instructions exactly as specified. @@ -353,7 +280,6 @@ You must fully embody this agent's persona and follow all activation instruction return `--- name: '{{name}}' description: '{{description}}' -disable-model-invocation: true --- # {{name}} @@ -371,24 +297,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} renderTemplate(template, artifact) { // Use the appropriate path property based on artifact type let pathToUse = artifact.relativePath || ''; - switch (artifact.type) { - case 'agent-launcher': { - pathToUse = artifact.agentPath || artifact.relativePath || ''; - - break; - } - case 'workflow-command': { - pathToUse = artifact.workflowPath || artifact.relativePath || ''; - - break; - } - case 'task': - case 'tool': { - pathToUse = artifact.path || artifact.relativePath || ''; - - break; - } - // No default + if (artifact.type === 'agent-launcher') { + pathToUse = artifact.agentPath || artifact.relativePath || ''; + } else if (artifact.type === 'workflow-command') { + pathToUse = artifact.workflowPath || artifact.relativePath || ''; } let rendered = template @@ -411,27 +323,13 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * Generate filename for artifact * @param {Object} artifact - Artifact data * @param {string} artifactType - Artifact type (agent, workflow, task, tool) - * @param {string} extension - File extension to use (e.g., '.md', '.toml') * @returns {string} Generated filename */ - generateFilename(artifact, artifactType, extension = '.md') { + generateFilename(artifact, artifactType) { const { toDashPath } = require('./shared/path-utils'); - - // Reuse central logic to ensure consistent naming conventions - const standardName = toDashPath(artifact.relativePath); - - // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) - // This handles any extensions that might slip through toDashPath() - const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md'); - - // If using default markdown, preserve the bmad-agent- prefix for agents - if (extension === '.md') { - return baseName; - } - - // For other extensions (e.g., .toml), replace .md extension - // Note: agent prefix is preserved even with non-markdown extensions - return baseName.replace(/\.md$/, extension); + // toDashPath already handles the .agent.md suffix for agents correctly + // No need to add it again here + return toDashPath(artifact.relativePath); } /** diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index c01429397..d5055f491 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -9,7 +9,7 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = r */ class WorkflowCommandGenerator { constructor(bmadFolderName = 'bmad') { - this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); + this.templatePath = path.join(__dirname, '../templates/workflow-commander.md'); this.bmadFolderName = bmadFolderName; } @@ -77,11 +77,8 @@ class WorkflowCommandGenerator { workflowRelPath = parts.slice(1).join('/'); } } - // Determine if this is a YAML workflow - const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.endsWith('.yml'); artifacts.push({ type: 'workflow-command', - isYamlWorkflow: isYamlWorkflow, // For template selection name: workflow.name, description: workflow.description || `${workflow.name} workflow`, module: workflow.module, @@ -117,9 +114,7 @@ class WorkflowCommandGenerator { */ async generateCommandContent(workflow, bmadDir) { // Determine template based on workflow file type - const isMarkdownWorkflow = workflow.path.endsWith('workflow.md'); - const templateName = isMarkdownWorkflow ? 'workflow-commander.md' : 'workflow-command-template.md'; - const templatePath = path.join(path.dirname(this.templatePath), templateName); + const templatePath = path.join(path.dirname(this.templatePath), 'workflow-commander.md'); // Load the appropriate template const template = await fs.readFile(templatePath, 'utf8'); diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 8ee63047f..b91f59158 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -7,7 +7,6 @@ const { XmlHandler } = require('../../../lib/xml-handler'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { ExternalModuleManager } = require('./external-manager'); -const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); /** * Manages the installation, updating, and removal of BMAD modules. @@ -28,7 +27,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); class ModuleManager { constructor(options = {}) { this.xmlHandler = new XmlHandler(); - this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden + this.bmadFolderName = 'bmad'; // Default, can be overridden this.customModulePaths = new Map(); // Initialize custom module paths this.externalModuleManager = new ExternalModuleManager(); // For external official modules } @@ -417,7 +416,7 @@ class ModuleManager { if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); try { - execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { + execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', { cwd: moduleCacheDir, stdio: 'pipe', timeout: 120_000, // 2 minute timeout @@ -442,7 +441,7 @@ class ModuleManager { if (packageJsonNewer) { const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); try { - execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { + execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', { cwd: moduleCacheDir, stdio: 'pipe', timeout: 120_000, // 2 minute timeout @@ -740,8 +739,8 @@ class ModuleManager { } } - // Check if this is a workflow file (YAML or MD) - if (file.endsWith('workflow.yaml') || file.endsWith('workflow.md')) { + // Check if this is a workflow file (MD) + if (file.endsWith('workflow.md')) { await fs.ensureDir(path.dirname(targetFile)); await this.copyWorkflowFileStripped(sourceFile, targetFile); } else { @@ -757,101 +756,19 @@ class ModuleManager { } /** - * Copy workflow file with web_bundle section stripped (YAML or MD) + * Copy workflow file with web_bundle section stripped (MD) * Preserves comments, formatting, and line breaks * @param {string} sourceFile - Source workflow file path * @param {string} targetFile - Target workflow file path */ async copyWorkflowFileStripped(sourceFile, targetFile) { - if (sourceFile.endsWith('.md')) { - let mdContent = await fs.readFile(sourceFile, 'utf8'); + let mdContent = await fs.readFile(sourceFile, 'utf8'); - mdContent = mdContent.replaceAll('_bmad', '_bmad'); - mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName); - mdContent = this.stripWebBundleFromFrontmatter(mdContent); + mdContent = mdContent.replaceAll('_bmad', '_bmad'); + mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName); + mdContent = this.stripWebBundleFromFrontmatter(mdContent); - await fs.writeFile(targetFile, mdContent, 'utf8'); - return; - } - - // Read the source YAML file - let yamlContent = await fs.readFile(sourceFile, 'utf8'); - - // IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML - // Otherwise parsing will fail on the placeholder - yamlContent = yamlContent.replaceAll('_bmad', '_bmad'); - yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName); - - try { - // First check if web_bundle exists by parsing - const workflowConfig = yaml.parse(yamlContent); - - if (workflowConfig.web_bundle === undefined) { - // No web_bundle section, just write (placeholders already replaced above) - await fs.writeFile(targetFile, yamlContent, 'utf8'); - return; - } - - // Find the line that starts web_bundle - const lines = yamlContent.split('\n'); - let startIdx = -1; - let endIdx = -1; - let baseIndent = 0; - - // Find the start of web_bundle section - for (const [i, line] of lines.entries()) { - const match = line.match(/^(\s*)web_bundle:/); - if (match) { - startIdx = i; - baseIndent = match[1].length; - break; - } - } - - if (startIdx === -1) { - // web_bundle not found in text (shouldn't happen), copy as-is - await fs.writeFile(targetFile, yamlContent, 'utf8'); - return; - } - - // Find the end of web_bundle section - // It ends when we find a line with same or less indentation that's not empty/comment - endIdx = startIdx; - for (let i = startIdx + 1; i < lines.length; i++) { - const line = lines[i]; - - // Skip empty lines and comments - if (line.trim() === '' || line.trim().startsWith('#')) { - continue; - } - - // Check indentation - const indent = line.match(/^(\s*)/)[1].length; - if (indent <= baseIndent) { - // Found next section at same or lower indentation - endIdx = i - 1; - break; - } - } - - // If we didn't find an end, it goes to end of file - if (endIdx === startIdx) { - endIdx = lines.length - 1; - } - - // Remove the web_bundle section (including the line before if it's just a blank line) - const newLines = [...lines.slice(0, startIdx), ...lines.slice(endIdx + 1)]; - - // Clean up any double blank lines that might result - const strippedYaml = newLines.join('\n').replaceAll(/\n\n\n+/g, '\n\n'); - - // Placeholders already replaced at the beginning of this function - await fs.writeFile(targetFile, strippedYaml, 'utf8'); - } catch { - // If anything fails, just copy the file as-is - console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`)); - await fs.copy(sourceFile, targetFile, { overwrite: true }); - } + await fs.writeFile(targetFile, mdContent, 'utf8'); } stripWebBundleFromFrontmatter(content) { @@ -892,7 +809,7 @@ class ModuleManager { for (const agentFile of agentFiles) { 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)); await fs.ensureDir(targetDir); @@ -1198,13 +1115,9 @@ class ModuleManager { const installWorkflowSubPath = installMatch[2]; const sourceModulePath = getModulePath(sourceModule); - const actualSourceWorkflowPath = path.join( - sourceModulePath, - 'workflows', - sourceWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, ''), - ); + const actualSourceWorkflowPath = path.join(sourceModulePath, 'workflows', sourceWorkflowSubPath.replace(/\/workflow\.md$/, '')); - const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, '')); + const actualDestWorkflowPath = path.join(targetPath, 'workflows', installWorkflowSubPath.replace(/\/workflow\.md$/, '')); // Check if source workflow exists if (!(await fs.pathExists(actualSourceWorkflowPath))) { @@ -1215,7 +1128,7 @@ class ModuleManager { // Copy the entire workflow folder console.log( chalk.dim( - ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.(yaml|md)$/, '')}`, + ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.md$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.md$/, '')}`, ), ); @@ -1225,12 +1138,8 @@ class ModuleManager { // Update workflow config_source references const workflowMdPath = path.join(actualDestWorkflowPath, 'workflow.md'); - const workflowYamlPath = path.join(actualDestWorkflowPath, 'workflow.yaml'); - if (await fs.pathExists(workflowMdPath)) { await this.updateWorkflowConfigSource(workflowMdPath, moduleName); - } else if (await fs.pathExists(workflowYamlPath)) { - await this.updateWorkflowConfigSource(workflowYamlPath, moduleName); } } }