fix: support CRLF line endings and add task/tool templates for all IDEs

This commit is contained in:
Davor Racić 2026-01-31 21:29:54 +01:00 committed by Davor Racic
parent 542a7429ec
commit 91b2d84ff8
8 changed files with 190 additions and 11 deletions

View File

@ -146,7 +146,7 @@ class DependencyResolver {
const content = await fs.readFile(file.path, 'utf8'); const content = await fs.readFile(file.path, 'utf8');
// Parse YAML frontmatter for explicit dependencies // Parse YAML frontmatter for explicit dependencies
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
// Pre-process to handle backticks in YAML values // Pre-process to handle backticks in YAML values

View File

@ -161,7 +161,7 @@ class ManifestGenerator {
workflow = yaml.parse(content); workflow = yaml.parse(content);
} else { } else {
// Parse MD workflow with YAML frontmatter // Parse MD workflow with YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!frontmatterMatch) { if (!frontmatterMatch) {
if (debug) { if (debug) {
console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`);
@ -392,7 +392,7 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tasks // Parse YAML frontmatter for .md tasks
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
@ -481,7 +481,7 @@ class ManifestGenerator {
if (file.endsWith('.md')) { if (file.endsWith('.md')) {
// Parse YAML frontmatter for .md tools // Parse YAML frontmatter for .md tools
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) { if (frontmatterMatch) {
try { try {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);

View File

@ -86,10 +86,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
} }
// Install tasks and tools // Install tasks and tools using template system (supports TOML for Gemini, MD for others)
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
const taskToolGen = new TaskToolCommandGenerator(); const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
results.tasks = taskToolResult.tasks || 0; results.tasks = taskToolResult.tasks || 0;
results.tools = taskToolResult.tools || 0; results.tools = taskToolResult.tools || 0;
} }
@ -180,6 +181,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
return count; 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<Object>} 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 * Load template based on type and configuration
* @param {string} templateType - Template type (claude, windsurf, etc.) * @param {string} templateType - Template type (claude, windsurf, etc.)
@ -314,10 +362,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
renderTemplate(template, artifact) { renderTemplate(template, artifact) {
// Use the appropriate path property based on artifact type // Use the appropriate path property based on artifact type
let pathToUse = artifact.relativePath || ''; let pathToUse = artifact.relativePath || '';
if (artifact.type === 'agent-launcher') { switch (artifact.type) {
pathToUse = artifact.agentPath || artifact.relativePath || ''; case 'agent-launcher': {
} else if (artifact.type === 'workflow-command') { pathToUse = artifact.agentPath || artifact.relativePath || '';
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'workflow-command': {
pathToUse = artifact.workflowPath || artifact.relativePath || '';
break;
}
case 'task':
case 'tool': {
pathToUse = artifact.path || artifact.relativePath || '';
break;
}
// No default
} }
let rendered = template let rendered = template

View File

@ -8,6 +8,81 @@ const { toColonName, toColonPath, toDashPath } = require('./path-utils');
* Generates command files for standalone tasks and tools * Generates command files for standalone tasks and tools
*/ */
class TaskToolCommandGenerator { class TaskToolCommandGenerator {
/**
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: 'bmad')
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
* used for template placeholder rendering ({{bmadFolderName}}).
*/
constructor(bmadFolderName = 'bmad') {
this.bmadFolderName = bmadFolderName;
}
/**
* Collect task and tool artifacts for IDE installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Promise<Object>} Artifacts array with metadata
*/
async collectTaskToolArtifacts(bmadDir) {
const tasks = await this.loadTaskManifest(bmadDir);
const tools = await this.loadToolManifest(bmadDir);
// Filter to only standalone items
const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : [];
const artifacts = [];
// Collect task artifacts
for (const task of standaloneTasks) {
let taskPath = (task.path || '').replaceAll('\\', '/');
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (taskPath.startsWith('_bmad/')) {
taskPath = taskPath.slice(6); // Remove '_bmad/'
}
artifacts.push({
type: 'task',
name: task.name,
displayName: task.displayName || task.name,
description: task.description || `Execute ${task.displayName || task.name}`,
module: task.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${task.module}/tasks/${task.name}.md`,
path: taskPath,
});
}
// Collect tool artifacts
for (const tool of standaloneTools) {
let toolPath = (tool.path || '').replaceAll('\\', '/');
// Remove _bmad/ prefix if present to get relative path within bmad folder
if (toolPath.startsWith('_bmad/')) {
toolPath = toolPath.slice(6); // Remove '_bmad/'
}
artifacts.push({
type: 'tool',
name: tool.name,
displayName: tool.displayName || tool.name,
description: tool.description || `Execute ${tool.displayName || tool.name}`,
module: tool.module,
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${tool.module}/tools/${tool.name}.md`,
path: toolPath,
});
}
return {
artifacts,
counts: {
tasks: standaloneTasks.length,
tools: standaloneTools.length,
},
};
}
/** /**
* Generate task and tool commands from manifest CSVs * Generate task and tool commands from manifest CSVs
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the task file exactly as written.

View File

@ -0,0 +1,10 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the tool file exactly as written.

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} task from the BMad Method."
prompt = """
Execute the BMAD '{{name}}' task.
TASK INSTRUCTIONS:
1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TASK FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""

View File

@ -0,0 +1,11 @@
description = "Executes the {{name}} tool from the BMad Method."
prompt = """
Execute the BMAD '{{name}}' tool.
TOOL INSTRUCTIONS:
1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}}
2. READ its entire contents
3. FOLLOW every instruction precisely as specified
TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}}
"""