mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-01-30 04:32:02 +00:00
Fix windsurf installer to use UnifiedInstaller with flat files
- Replace manual artifact collection with UnifiedInstaller class - Remove nested folder structure (.windsurf/workflows/bmad/[module]/[type]/) - Now installs flat files to .windsurf/workflows/ (e.g., bmad-bmm-agent-pm.md) - Use NamingStyle.FLAT_DASH and TemplateType.WINDSURF - Add customTemplateFn for Windsurf-specific auto_execution_mode frontmatter - Simplify cleanup() and update installCustomAgentLauncher() for flat structure
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const chalk = require('chalk');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { UnifiedInstaller, NamingStyle, TemplateType } = require('./shared/unified-installer');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
/**
|
||||
* Windsurf IDE setup handler
|
||||
*
|
||||
* Uses UnifiedInstaller for consistent artifact collection and writing.
|
||||
* Windsurf-specific configuration:
|
||||
* - Flat file naming (FLAT_DASH): bmad-bmm-agent-pm.md
|
||||
* - Windsurf frontmatter with auto_execution_mode
|
||||
*/
|
||||
class WindsurfSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('windsurf', 'Windsurf', true); // preferred IDE
|
||||
this.configDir = '.windsurf';
|
||||
this.workflowsDir = 'workflows';
|
||||
this.unifiedInstaller = new UnifiedInstaller(this.bmadFolderName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,93 +29,34 @@ class WindsurfSetup extends BaseIdeSetup {
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||
|
||||
// Create .windsurf/workflows/bmad directory structure
|
||||
// Create .windsurf/workflows directory
|
||||
const windsurfDir = path.join(projectDir, this.configDir);
|
||||
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
|
||||
const bmadWorkflowsDir = path.join(workflowsDir, 'bmad');
|
||||
|
||||
await this.ensureDir(bmadWorkflowsDir);
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
// Clean up any existing BMAD workflows before reinstalling
|
||||
await this.cleanup(projectDir);
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
// Use UnifiedInstaller with Windsurf-specific configuration
|
||||
const counts = await this.unifiedInstaller.install(projectDir, bmadDir, {
|
||||
targetDir: workflowsDir,
|
||||
namingStyle: NamingStyle.FLAT_DASH,
|
||||
templateType: TemplateType.WINDSURF,
|
||||
customTemplateFn: this.windsurfTemplate.bind(this),
|
||||
}, options.selectedModules || []);
|
||||
|
||||
// Convert artifacts to agent format for module organization
|
||||
const agents = agentArtifacts.map((a) => ({ module: a.module, name: a.name }));
|
||||
|
||||
// Get tasks, tools, and workflows (standalone only)
|
||||
const tasks = await this.getTasks(bmadDir, true);
|
||||
const tools = await this.getTools(bmadDir, true);
|
||||
const workflows = await this.getWorkflows(bmadDir, true);
|
||||
|
||||
// Create directories for each module under bmad/
|
||||
const modules = new Set();
|
||||
for (const item of [...agents, ...tasks, ...tools, ...workflows]) modules.add(item.module);
|
||||
|
||||
for (const module of modules) {
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'agents'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tasks'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'tools'));
|
||||
await this.ensureDir(path.join(bmadWorkflowsDir, module, 'workflows'));
|
||||
}
|
||||
|
||||
// Process agent launchers as workflows with organized structure
|
||||
let agentCount = 0;
|
||||
for (const artifact of agentArtifacts) {
|
||||
const processedContent = this.createWorkflowContent({ module: artifact.module, name: artifact.name }, artifact.content);
|
||||
|
||||
// Organized path: bmad/module/agents/agent-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, artifact.module, 'agents', `${artifact.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
agentCount++;
|
||||
}
|
||||
|
||||
// Process tasks as workflows with organized structure
|
||||
let taskCount = 0;
|
||||
for (const task of tasks) {
|
||||
const content = await this.readFile(task.path);
|
||||
const processedContent = this.createTaskWorkflowContent(task, content);
|
||||
|
||||
// Organized path: bmad/module/tasks/task-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, task.module, 'tasks', `${task.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
taskCount++;
|
||||
}
|
||||
|
||||
// Process tools as workflows with organized structure
|
||||
let toolCount = 0;
|
||||
for (const tool of tools) {
|
||||
const content = await this.readFile(tool.path);
|
||||
const processedContent = this.createToolWorkflowContent(tool, content);
|
||||
|
||||
// Organized path: bmad/module/tools/tool-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, tool.module, 'tools', `${tool.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
toolCount++;
|
||||
}
|
||||
|
||||
// Process workflows with organized structure
|
||||
let workflowCount = 0;
|
||||
for (const workflow of workflows) {
|
||||
const content = await this.readFile(workflow.path);
|
||||
const processedContent = this.createWorkflowWorkflowContent(workflow, content);
|
||||
|
||||
// Organized path: bmad/module/workflows/workflow-name.md
|
||||
const targetPath = path.join(bmadWorkflowsDir, workflow.module, 'workflows', `${workflow.name}.md`);
|
||||
await this.writeFile(targetPath, processedContent);
|
||||
workflowCount++;
|
||||
}
|
||||
// Post-process tasks and tools to add Windsurf auto_execution_mode
|
||||
// UnifiedInstaller handles agents/workflows correctly, but tasks/tools
|
||||
// need special handling for proper Windsurf frontmatter
|
||||
await this.addWindsurfTaskToolFrontmatter(workflowsDir);
|
||||
|
||||
console.log(chalk.green(`✓ ${this.name} configured:`));
|
||||
console.log(chalk.dim(` - ${agentCount} agents installed`));
|
||||
console.log(chalk.dim(` - ${taskCount} tasks installed`));
|
||||
console.log(chalk.dim(` - ${toolCount} tools installed`));
|
||||
console.log(chalk.dim(` - ${workflowCount} workflows installed`));
|
||||
console.log(chalk.dim(` - Organized in modules: ${[...modules].join(', ')}`));
|
||||
console.log(chalk.dim(` - ${counts.agents} agents installed`));
|
||||
console.log(chalk.dim(` - ${counts.tasks} tasks installed`));
|
||||
console.log(chalk.dim(` - ${counts.tools} tools installed`));
|
||||
console.log(chalk.dim(` - ${counts.workflows} workflows installed`));
|
||||
console.log(chalk.dim(` - Total: ${counts.total} items`));
|
||||
console.log(chalk.dim(` - Workflows directory: ${path.relative(projectDir, workflowsDir)}`));
|
||||
|
||||
// Provide additional configuration hints
|
||||
@@ -122,88 +70,120 @@ class WindsurfSetup extends BaseIdeSetup {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
agents: agentCount,
|
||||
tasks: taskCount,
|
||||
tools: toolCount,
|
||||
workflows: workflowCount,
|
||||
...counts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for an agent
|
||||
* Windsurf-specific template function
|
||||
* Adds proper Windsurf frontmatter with auto_execution_mode
|
||||
*/
|
||||
createWorkflowContent(agent, content) {
|
||||
// Strip existing frontmatter from launcher
|
||||
windsurfTemplate(artifact, content, templateType) {
|
||||
// Strip existing frontmatter
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: ${agent.name}
|
||||
auto_execution_mode: 3
|
||||
// Determine auto_execution_mode based on type
|
||||
let autoExecMode = '1'; // default for workflows
|
||||
let description = artifact.name || artifact.displayName || 'workflow';
|
||||
|
||||
if (artifact.type === 'agent') {
|
||||
autoExecMode = '3';
|
||||
description = artifact.name || 'agent';
|
||||
} else if (artifact.type === 'workflow') {
|
||||
autoExecMode = '1';
|
||||
description = artifact.name || 'workflow';
|
||||
}
|
||||
|
||||
return `---
|
||||
description: ${description}
|
||||
auto_execution_mode: ${autoExecMode}
|
||||
---
|
||||
|
||||
${contentWithoutFrontmatter}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Windsurf auto_execution_mode to task and tool files
|
||||
* These are generated by TaskToolCommandGenerator with basic YAML
|
||||
* but need the Windsurf-specific auto_execution_mode field
|
||||
*/
|
||||
async addWindsurfTaskToolFrontmatter(workflowsDir) {
|
||||
if (!(await fs.pathExists(workflowsDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.name.startsWith('bmad-') || !entry.name.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(workflowsDir, entry.name);
|
||||
let content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Check if this is a task or tool file
|
||||
// They have pattern: bmad-module-task-name.md or bmad-module-tool-name.md
|
||||
const parts = entry.name.replace('bmad-', '').replace('.md', '').split('-');
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const type = parts[parts.length - 2]; // second to last part should be 'task' or 'tool'
|
||||
|
||||
if (type === 'task' || type === 'tool') {
|
||||
// Check if auto_execution_mode is already present
|
||||
if (content.includes('auto_execution_mode')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract existing description if present
|
||||
const descMatch = content.match(/description: '(.+?)'/);
|
||||
const description = descMatch ? descMatch[1] : entry.name.replace('.md', '');
|
||||
|
||||
// Strip existing frontmatter and add Windsurf-specific frontmatter
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
|
||||
|
||||
content = `---
|
||||
description: '${description}'
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${contentWithoutFrontmatter}`;
|
||||
|
||||
return workflowContent;
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
console.log(chalk.dim(` Updated ${updatedCount} task/tool files with Windsurf frontmatter`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a task
|
||||
*/
|
||||
createTaskWorkflowContent(task, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: task-${task.name}
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a tool
|
||||
*/
|
||||
createToolWorkflowContent(tool, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: tool-${tool.name}
|
||||
auto_execution_mode: 2
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow content for a workflow
|
||||
*/
|
||||
createWorkflowWorkflowContent(workflow, content) {
|
||||
// Create simple Windsurf frontmatter matching original format
|
||||
let workflowContent = `---
|
||||
description: ${workflow.name}
|
||||
auto_execution_mode: 1
|
||||
---
|
||||
|
||||
${content}`;
|
||||
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Windsurf configuration - surgically remove only BMAD files
|
||||
* Cleanup Windsurf configuration - remove only BMAD files
|
||||
*/
|
||||
async cleanup(projectDir) {
|
||||
const fs = require('fs-extra');
|
||||
const bmadPath = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad');
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (await fs.pathExists(bmadPath)) {
|
||||
// Remove the entire bmad folder - this is our territory
|
||||
await fs.remove(bmadPath);
|
||||
console.log(chalk.dim(` Cleaned up existing BMAD workflows`));
|
||||
if (await fs.pathExists(workflowsDir)) {
|
||||
// Remove all bmad* files from workflows directory
|
||||
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
||||
let removedCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('bmad')) {
|
||||
const entryPath = path.join(workflowsDir, entry.name);
|
||||
await fs.remove(entryPath);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(chalk.dim(` Cleaned up ${removedCount} existing BMAD workflow files`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,14 +196,13 @@ ${content}`;
|
||||
* @returns {Object|null} Info about created command
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const fs = require('fs-extra');
|
||||
const customAgentsDir = path.join(projectDir, this.configDir, this.workflowsDir, 'bmad', 'custom', 'agents');
|
||||
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
|
||||
|
||||
if (!(await this.exists(path.join(projectDir, this.configDir)))) {
|
||||
return null; // IDE not configured for this project
|
||||
}
|
||||
|
||||
await this.ensureDir(customAgentsDir);
|
||||
await this.ensureDir(workflowsDir);
|
||||
|
||||
const launcherContent = `You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
@@ -237,7 +216,7 @@ ${content}`;
|
||||
</agent-activation>
|
||||
`;
|
||||
|
||||
// Windsurf uses workflow format with frontmatter
|
||||
// Windsurf uses workflow format with frontmatter - flat naming
|
||||
const workflowContent = `---
|
||||
description: ${metadata.title || agentName}
|
||||
auto_execution_mode: 3
|
||||
@@ -245,12 +224,14 @@ auto_execution_mode: 3
|
||||
|
||||
${launcherContent}`;
|
||||
|
||||
const launcherPath = path.join(customAgentsDir, `${agentName}.md`);
|
||||
// Use flat naming: bmad-custom-agent-agentname.md
|
||||
const flatName = `bmad-custom-agent-${agentName}.md`;
|
||||
const launcherPath = path.join(workflowsDir, flatName);
|
||||
await fs.writeFile(launcherPath, workflowContent);
|
||||
|
||||
return {
|
||||
path: launcherPath,
|
||||
command: `bmad/custom/agents/${agentName}`,
|
||||
command: flatName.replace('.md', ''),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user