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:
Brian Madison
2026-01-24 10:55:05 -06:00
parent c0adbc4e76
commit c5d0fb55ba

View File

@@ -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', ''),
};
}
}