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. 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 section precisely 4. DISPLAY the welcome/greeting as instructed 5. PRESENT the numbered menu 6. WAIT for user input before proceeding `; // 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 };