Files
BMAD-METHOD/tools/cli/installers/lib/ide/windsurf.js
Brian Madison 5aef6379b9 Fix github-copilot installer to use UnifiedInstaller for prompts
- Add .github/prompts directory alongside .github/agents
- Use UnifiedInstaller with TemplateType.COPILOT for prompts/workflows/tasks/tools
- Fix typo: bmd-custom- -> bmad- prefix for agents
- Update cleanup to handle both directories
- Format fixes for auggie.js and windsurf.js
2026-01-25 03:56:40 -06:00

245 lines
8.4 KiB
JavaScript

const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
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);
}
/**
* Setup Windsurf IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
// Create .windsurf/workflows directory
const windsurfDir = path.join(projectDir, this.configDir);
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
await this.ensureDir(workflowsDir);
// Clean up any existing BMAD workflows before reinstalling
await this.cleanup(projectDir);
// 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 || [],
);
// 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(` - ${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
if (options.showHints !== false) {
console.log(chalk.dim('\n Windsurf workflow settings:'));
console.log(chalk.dim(' - auto_execution_mode: 3 (recommended for agents)'));
console.log(chalk.dim(' - auto_execution_mode: 2 (recommended for tasks/tools)'));
console.log(chalk.dim(' - auto_execution_mode: 1 (recommended for workflows)'));
console.log(chalk.dim(' - Workflows can be triggered via the Windsurf menu'));
}
return {
success: true,
...counts,
};
}
/**
* Windsurf-specific template function
* Adds proper Windsurf frontmatter with auto_execution_mode
*/
windsurfTemplate(artifact, content, templateType) {
// Strip existing frontmatter
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
const contentWithoutFrontmatter = content.replace(frontmatterRegex, '');
// 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.at(-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}`;
await fs.writeFile(filePath, content, 'utf8');
updatedCount++;
}
}
if (updatedCount > 0) {
console.log(chalk.dim(` Updated ${updatedCount} task/tool files with Windsurf frontmatter`));
}
}
/**
* Cleanup Windsurf configuration - remove only BMAD files
*/
async cleanup(projectDir) {
const workflowsDir = path.join(projectDir, this.configDir, this.workflowsDir);
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`));
}
}
}
/**
* Install a custom agent launcher for Windsurf
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created command
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
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(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.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @${agentPath}
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. FOLLOW every step in the <activation> section precisely
4. DISPLAY the welcome/greeting as instructed
5. PRESENT the numbered menu
6. WAIT for user input before proceeding
</agent-activation>
`;
// Windsurf uses workflow format with frontmatter - flat naming
const workflowContent = `---
description: ${metadata.title || agentName}
auto_execution_mode: 3
---
${launcherContent}`;
// 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: flatName.replace('.md', ''),
};
}
}
module.exports = { WindsurfSetup };