diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js index 92596db3..6463311e 100644 --- a/tools/cli/installers/lib/ide/windsurf.js +++ b/tools/cli/installers/lib/ide/windsurf.js @@ -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}`; `; - // 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', ''), }; } }