From 4596fad5d60ea2f245cc96c47115173a9f7e0335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Angner?= Date: Tue, 24 Feb 2026 19:53:39 +0100 Subject: [PATCH] Add IDE handler system with slash command support Implement BMad-style IDE handler architecture for WDS: - Base class (_base-ide.js) with template method pattern - Dynamic handler discovery via IdeManager - Priority handlers: Windsurf, Cursor, Claude Code, Cline, GitHub Copilot - Installer integration after agent compilation step - Saga activation now branches on starting_point config (pitch vs brief) - Remove parenthetical hint from learning material prompt Co-Authored-By: Claude Opus 4.6 --- src/agents/saga-analyst.agent.yaml | 9 +- tools/cli/installers/lib/ide/_base-ide.js | 266 ++++++++++++++++++ tools/cli/installers/lib/ide/claude-code.js | 114 ++++++++ tools/cli/installers/lib/ide/cline.js | 118 ++++++++ tools/cli/installers/lib/ide/cursor.js | 116 ++++++++ .../cli/installers/lib/ide/github-copilot.js | 106 +++++++ tools/cli/installers/lib/ide/manager.js | 222 +++++++++++++++ tools/cli/installers/lib/ide/windsurf.js | 108 +++++++ tools/cli/lib/compiler.js | 2 +- tools/cli/lib/installer.js | 37 +++ tools/cli/lib/ui.js | 2 +- 11 files changed, 1094 insertions(+), 6 deletions(-) create mode 100644 tools/cli/installers/lib/ide/_base-ide.js create mode 100644 tools/cli/installers/lib/ide/claude-code.js create mode 100644 tools/cli/installers/lib/ide/cline.js create mode 100644 tools/cli/installers/lib/ide/cursor.js create mode 100644 tools/cli/installers/lib/ide/github-copilot.js create mode 100644 tools/cli/installers/lib/ide/manager.js create mode 100644 tools/cli/installers/lib/ide/windsurf.js diff --git a/src/agents/saga-analyst.agent.yaml b/src/agents/saga-analyst.agent.yaml index 8613403c6..cb56c69a1 100644 --- a/src/agents/saga-analyst.agent.yaml +++ b/src/agents/saga-analyst.agent.yaml @@ -36,12 +36,13 @@ agent: prompts: - id: activation content: | - Hi {{user_name}}, I'm Saga, your strategic analyst! 👋 + Hi {user_name}, I'm Saga, your strategic analyst! 👋 - I'll help you create a Product Brief and Trigger Map for {{project_name}}. + I'll help you create a Product Brief and Trigger Map for {project_name}. - Let's start with the Product Brief. Tell me in your own words: - **What are you building?** + Check {starting_point} from config: + - If "pitch": Say "Before we dive into formal documentation, let's talk about your idea! Tell me in your own words — **what's the big idea? What problem are you solving and for whom?**" Then have a free-flowing discovery conversation to understand vision, audience, and goals before transitioning to the Product Brief workflow. + - If "brief": Say "Let's start with the Product Brief. Tell me in your own words: **What are you building?**" Then proceed directly with the [PB] Product Brief workflow. menu: - trigger: AS or fuzzy match on alignment-signoff diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js new file mode 100644 index 000000000..3f762fc31 --- /dev/null +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -0,0 +1,266 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const yaml = require('yaml'); + +/** + * Base class for IDE-specific setup + * All IDE handlers should extend this class + */ +class BaseIdeSetup { + constructor(name, displayName = null, preferred = false) { + this.name = name; + this.displayName = displayName || name; // Human-readable name for UI + this.preferred = preferred; // Whether this IDE should be shown in preferred list + this.configDir = null; // Override in subclasses (e.g., '.windsurf/workflows/wds') + this.configFile = null; // Override in subclasses when detection is file-based + this.detectionPaths = []; // Additional paths that indicate the IDE is configured + this.wdsFolderName = '_wds'; // Default, can be overridden + } + + /** + * Set the WDS folder name for placeholder replacement + * @param {string} wdsFolderName - The WDS folder name + */ + setWdsFolderName(wdsFolderName) { + this.wdsFolderName = wdsFolderName; + } + + /** + * Main setup method - must be implemented by subclasses + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + throw new Error(`setup() must be implemented by ${this.name} handler`); + } + + /** + * Cleanup IDE configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + // Default implementation - can be overridden + if (this.configDir) { + const configPath = path.join(projectDir, this.configDir); + if (await fs.pathExists(configPath)) { + await fs.remove(configPath); + console.log(chalk.dim(`Removed ${this.name} WDS configuration`)); + } + } + } + + /** + * Detect whether this IDE already has configuration in the project + * Subclasses can override for custom logic + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + const pathsToCheck = []; + + if (this.configDir) { + pathsToCheck.push(path.join(projectDir, this.configDir)); + } + + if (this.configFile) { + pathsToCheck.push(path.join(projectDir, this.configFile)); + } + + if (Array.isArray(this.detectionPaths)) { + for (const candidate of this.detectionPaths) { + if (!candidate) continue; + const resolved = path.isAbsolute(candidate) ? candidate : path.join(projectDir, candidate); + pathsToCheck.push(resolved); + } + } + + for (const candidate of pathsToCheck) { + if (await fs.pathExists(candidate)) { + return true; + } + } + + return false; + } + + /** + * Get list of agents from WDS installation + * @param {string} wdsDir - WDS installation directory + * @returns {Array} List of agent files with metadata + */ + async getAgents(wdsDir) { + const agents = []; + const agentsPath = path.join(wdsDir, 'agents'); + + if (!(await fs.pathExists(agentsPath))) { + return agents; + } + + const files = await fs.readdir(agentsPath); + + for (const file of files) { + if (!file.endsWith('.md')) continue; + + const filePath = path.join(agentsPath, file); + const agentName = file.replace('.md', ''); + + // Extract metadata from agent file + const metadata = await this.extractAgentMetadata(filePath); + + // Create slug from agent name (e.g., 'saga-analyst' -> 'saga') + const slug = metadata.slug || agentName.split('-')[0]; + + agents.push({ + name: agentName, + slug: slug, + path: filePath, + relativePath: path.relative(wdsDir, filePath), + filename: file, + metadata: metadata, + }); + } + + return agents; + } + + /** + * Extract agent metadata from compiled agent markdown file + * Reads YAML frontmatter or fallback to defaults + * @param {string} filePath - Path to agent markdown file + * @returns {Object} Agent metadata + */ + async extractAgentMetadata(filePath) { + try { + const content = await fs.readFile(filePath, 'utf8'); + + // Try to extract YAML frontmatter + const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (frontmatterMatch) { + const frontmatter = yaml.parse(frontmatterMatch[1]); + + return { + name: frontmatter.name || path.basename(filePath, '.md'), + description: frontmatter.description || frontmatter.role || '', + icon: frontmatter.icon || '📋', + slug: frontmatter.id ? path.basename(frontmatter.id, '.md').split('-')[0] : null, + }; + } + + // Fallback: extract from filename + const agentName = path.basename(filePath, '.md'); + return { + name: this.formatTitle(agentName), + description: agentName.includes('saga') ? 'Strategic Analyst' : + agentName.includes('freya') ? 'UX Designer' : + agentName.includes('idunn') ? 'Product Manager' : '', + icon: '📋', + slug: agentName.split('-')[0], + }; + } catch (error) { + // Fallback metadata on error + const agentName = path.basename(filePath, '.md'); + return { + name: this.formatTitle(agentName), + description: '', + icon: '📋', + slug: agentName.split('-')[0], + }; + } + } + + /** + * Format agent launcher content that references the compiled agent + * @param {string} agentName - Agent name (e.g., 'saga-analyst') + * @param {string} agentPath - Relative path to agent file (e.g., 'agents/saga-analyst.md') + * @returns {string} Launcher content + */ + formatAgentLauncher(agentName, agentPath) { + const relativePath = path.relative(process.cwd(), agentPath) + .replace(/\\/g, '/'); // Convert Windows paths to forward slashes + + return ` + + + +@include(${this.wdsFolderName}/agents/${agentName}.md) +`; + } + + /** + * Process content with IDE-specific frontmatter + * Subclasses must override this to add IDE-specific headers + * @param {string} content - Launcher content + * @param {Object} metadata - Agent metadata + * @returns {string} Processed content with IDE-specific frontmatter + */ + processContent(content, metadata = {}) { + // Default implementation - subclasses should override + return content; + } + + /** + * Ensure directory exists + * @param {string} dirPath - Directory path + */ + async ensureDir(dirPath) { + await fs.ensureDir(dirPath); + } + + /** + * Write file with content (replaces _wds placeholder) + * @param {string} filePath - File path + * @param {string} content - File content + */ + async writeFile(filePath, content) { + // Replace _wds placeholder if present + if (typeof content === 'string' && content.includes('_wds')) { + content = content.replaceAll('_wds', this.wdsFolderName); + } + + await this.ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, content, 'utf8'); + } + + /** + * Check if path exists + * @param {string} pathToCheck - Path to check + * @returns {boolean} True if path exists + */ + async exists(pathToCheck) { + return await fs.pathExists(pathToCheck); + } + + /** + * Alias for exists method + * @param {string} pathToCheck - Path to check + * @returns {boolean} True if path exists + */ + async pathExists(pathToCheck) { + return await fs.pathExists(pathToCheck); + } + + /** + * Read file content + * @param {string} filePath - File path + * @returns {string} File content + */ + async readFile(filePath) { + return await fs.readFile(filePath, 'utf8'); + } + + /** + * Format name as title + * @param {string} name - Name to format (e.g., 'saga-analyst') + * @returns {string} Formatted title (e.g., 'Saga Analyst') + */ + formatTitle(name) { + return name + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } +} + +module.exports = { BaseIdeSetup }; diff --git a/tools/cli/installers/lib/ide/claude-code.js b/tools/cli/installers/lib/ide/claude-code.js new file mode 100644 index 000000000..43641ef5f --- /dev/null +++ b/tools/cli/installers/lib/ide/claude-code.js @@ -0,0 +1,114 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); + +/** + * Claude Code IDE setup handler for WDS + */ +class ClaudeCodeSetup extends BaseIdeSetup { + constructor() { + super('claude-code', 'Claude Code', true); // preferred IDE + this.configDir = '.claude/skills/wds'; + } + + /** + * Setup Claude Code IDE configuration + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + // Create .claude/skills/wds directory + const targetDir = path.join(projectDir, this.configDir); + await this.ensureDir(targetDir); + + // Get all WDS agents + const agents = await this.getAgents(wdsDir); + + if (agents.length === 0) { + throw new Error('No agents found in WDS installation'); + } + + // Create launcher file for each agent + let agentCount = 0; + for (const agent of agents) { + // Create launcher content that references the compiled agent + const launcher = this.formatAgentLauncher(agent.name, agent.path); + + // Add Claude Code-specific YAML frontmatter + const content = this.processContent(launcher, agent.metadata); + + // Write launcher file + const filePath = path.join(targetDir, `${agent.slug}.md`); + await this.writeFile(filePath, content); + agentCount++; + } + + if (options.logger) { + options.logger.log(chalk.dim(` - ${agentCount} agent(s) configured for Claude Code`)); + } + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Process content with Claude Code-specific YAML frontmatter + * @param {string} content - Launcher content + * @param {Object} metadata - Agent metadata + * @returns {string} Processed content with Claude Code YAML frontmatter + */ + processContent(content, metadata = {}) { + // Strip any existing frontmatter from launcher + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + + const name = metadata.name || 'WDS Agent'; + const description = metadata.description || 'Agent'; + + // Create Claude Code YAML metadata header + const yamlHeader = `--- +name: ${name} +description: ${description} +--- + +`; + + return yamlHeader + contentWithoutFrontmatter; + } + + /** + * Cleanup Claude Code WDS configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const wdsPath = path.join(projectDir, this.configDir); + + if (await this.exists(wdsPath)) { + await this.remove(wdsPath); + console.log(chalk.dim(`Removed Claude Code WDS configuration`)); + } + } + + /** + * Detect if Claude Code is configured in project + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + return await this.exists(path.join(projectDir, '.claude')); + } + + /** + * Remove directory helper + * @param {string} dirPath - Directory to remove + */ + async remove(dirPath) { + const fs = require('fs-extra'); + await fs.remove(dirPath); + } +} + +module.exports = { ClaudeCodeSetup }; diff --git a/tools/cli/installers/lib/ide/cline.js b/tools/cli/installers/lib/ide/cline.js new file mode 100644 index 000000000..af23fd25b --- /dev/null +++ b/tools/cli/installers/lib/ide/cline.js @@ -0,0 +1,118 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); + +/** + * Cline IDE setup handler for WDS + */ +class ClineSetup extends BaseIdeSetup { + constructor() { + super('cline', 'Cline', false); + this.configDir = '.cline'; + } + + /** + * Setup Cline IDE configuration + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + // Create .cline directory + const targetDir = path.join(projectDir, this.configDir); + await this.ensureDir(targetDir); + + // Get all WDS agents + const agents = await this.getAgents(wdsDir); + + if (agents.length === 0) { + throw new Error('No agents found in WDS installation'); + } + + // Create launcher file for each agent + let agentCount = 0; + for (const agent of agents) { + // Create launcher content that references the compiled agent + const launcher = this.formatAgentLauncher(agent.name, agent.path); + + // Add Cline-specific formatting (flat markdown, no frontmatter) + const content = this.processContent(launcher, agent.metadata); + + // Write launcher file + const filePath = path.join(targetDir, `${agent.slug}.md`); + await this.writeFile(filePath, content); + agentCount++; + } + + if (options.logger) { + options.logger.log(chalk.dim(` - ${agentCount} agent(s) configured for Cline`)); + } + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Process content with Cline-specific formatting + * Cline uses flat markdown with no frontmatter + * @param {string} content - Launcher content + * @param {Object} metadata - Agent metadata + * @returns {string} Processed content without frontmatter + */ + processContent(content, metadata = {}) { + // Strip any existing frontmatter from launcher + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + + // Add title header for Cline + const title = metadata.name ? + `${metadata.name} - ${metadata.description}` : + metadata.description || 'WDS Agent'; + + return `# ${title} + +${contentWithoutFrontmatter}`; + } + + /** + * Cleanup Cline WDS configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const wdsPath = path.join(projectDir, this.configDir); + + if (await this.exists(wdsPath)) { + // Only remove WDS agent files, not entire .cline directory + const agents = ['saga.md', 'freya.md', 'idunn.md']; + for (const agentFile of agents) { + const filePath = path.join(wdsPath, agentFile); + if (await this.exists(filePath)) { + await this.removeFile(filePath); + } + } + console.log(chalk.dim(`Removed Cline WDS configuration`)); + } + } + + /** + * Detect if Cline is configured in project + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + return await this.exists(path.join(projectDir, '.cline')); + } + + /** + * Remove file helper + * @param {string} filePath - File to remove + */ + async removeFile(filePath) { + const fs = require('fs-extra'); + await fs.remove(filePath); + } +} + +module.exports = { ClineSetup }; diff --git a/tools/cli/installers/lib/ide/cursor.js b/tools/cli/installers/lib/ide/cursor.js new file mode 100644 index 000000000..2475e8b8a --- /dev/null +++ b/tools/cli/installers/lib/ide/cursor.js @@ -0,0 +1,116 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); + +/** + * Cursor IDE setup handler for WDS + */ +class CursorSetup extends BaseIdeSetup { + constructor() { + super('cursor', 'Cursor', true); // preferred IDE + this.configDir = '.cursor/rules/wds'; + } + + /** + * Setup Cursor IDE configuration + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + // Create .cursor/rules/wds directory + const targetDir = path.join(projectDir, this.configDir); + await this.ensureDir(targetDir); + + // Get all WDS agents + const agents = await this.getAgents(wdsDir); + + if (agents.length === 0) { + throw new Error('No agents found in WDS installation'); + } + + // Create launcher file for each agent + let agentCount = 0; + for (const agent of agents) { + // Create launcher content that references the compiled agent + const launcher = this.formatAgentLauncher(agent.name, agent.path); + + // Add Cursor-specific MDC frontmatter + const content = this.processContent(launcher, agent.metadata); + + // Write launcher file with .mdc extension + const filePath = path.join(targetDir, `${agent.slug}.mdc`); + await this.writeFile(filePath, content); + agentCount++; + } + + if (options.logger) { + options.logger.log(chalk.dim(` - ${agentCount} agent(s) configured for Cursor`)); + } + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Process content with Cursor-specific MDC frontmatter + * @param {string} content - Launcher content + * @param {Object} metadata - Agent metadata + * @returns {string} Processed content with Cursor MDC frontmatter + */ + processContent(content, metadata = {}) { + // Strip any existing frontmatter from launcher + const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; + const contentWithoutFrontmatter = content.replace(frontmatterRegex, ''); + + const description = metadata.name ? + `WDS Agent: ${metadata.name} - ${metadata.description}` : + `WDS Agent: ${metadata.description || 'Agent'}`; + + // Create Cursor MDC metadata header + const mdcHeader = `--- +description: ${description} +globs: +alwaysApply: false +--- + +`; + + return mdcHeader + contentWithoutFrontmatter; + } + + /** + * Cleanup Cursor WDS configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const wdsPath = path.join(projectDir, this.configDir); + + if (await this.exists(wdsPath)) { + await this.remove(wdsPath); + console.log(chalk.dim(`Removed Cursor WDS configuration`)); + } + } + + /** + * Detect if Cursor is configured in project + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + return await this.exists(path.join(projectDir, '.cursor')); + } + + /** + * Remove directory helper + * @param {string} dirPath - Directory to remove + */ + async remove(dirPath) { + const fs = require('fs-extra'); + await fs.remove(dirPath); + } +} + +module.exports = { CursorSetup }; diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js new file mode 100644 index 000000000..d9e4ff0fd --- /dev/null +++ b/tools/cli/installers/lib/ide/github-copilot.js @@ -0,0 +1,106 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); + +/** + * GitHub Copilot IDE setup handler for WDS + * Note: GitHub Copilot doesn't support includes/references, so we must copy full agent content + */ +class GitHubCopilotSetup extends BaseIdeSetup { + constructor() { + super('github-copilot', 'GitHub Copilot', false); + this.configFile = '.github/copilot-instructions.md'; + } + + /** + * Setup GitHub Copilot IDE configuration + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + // Ensure .github directory exists + const githubDir = path.join(projectDir, '.github'); + await this.ensureDir(githubDir); + + // Get all WDS agents + const agents = await this.getAgents(wdsDir); + + if (agents.length === 0) { + throw new Error('No agents found in WDS installation'); + } + + // Build content to append + let content = '\n\n## WDS Agents\n\n'; + content += '\n\n'; + + // For each agent, read the full compiled agent content + let agentCount = 0; + for (const agent of agents) { + // Read the full agent content + const agentContent = await this.readFile(agent.path); + + // Add section header + content += `### ${agent.metadata.name} - ${agent.metadata.description}\n\n`; + + // Add full agent content + content += agentContent + '\n\n'; + content += '---\n\n'; + + agentCount++; + } + + // Append to copilot-instructions.md + const filePath = path.join(projectDir, this.configFile); + + if (await this.exists(filePath)) { + // File exists, append to it + const fs = require('fs-extra'); + await fs.appendFile(filePath, content); + } else { + // File doesn't exist, create it + await this.writeFile(filePath, content); + } + + if (options.logger) { + options.logger.log(chalk.dim(` - ${agentCount} agent(s) configured for GitHub Copilot`)); + } + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Cleanup GitHub Copilot WDS configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const filePath = path.join(projectDir, this.configFile); + + if (await this.exists(filePath)) { + // Read file, remove WDS Agents section + const fs = require('fs-extra'); + const content = await fs.readFile(filePath, 'utf8'); + + // Remove WDS Agents section (everything from "## WDS Agents" to the end or next ## section) + const wdsAgentsRegex = /\n\n## WDS Agents\n\n[\s\S]*?(?=\n\n##\s|$)/; + const cleanedContent = content.replace(wdsAgentsRegex, ''); + + await fs.writeFile(filePath, cleanedContent); + console.log(chalk.dim(`Removed GitHub Copilot WDS configuration`)); + } + } + + /** + * Detect if GitHub Copilot is configured in project + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + return await this.exists(path.join(projectDir, '.github')); + } +} + +module.exports = { GitHubCopilotSetup }; diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js new file mode 100644 index 000000000..2ef2c2c9f --- /dev/null +++ b/tools/cli/installers/lib/ide/manager.js @@ -0,0 +1,222 @@ +const fs = require('fs-extra'); +const path = require('node:path'); +const chalk = require('chalk'); + +/** + * IDE Manager - handles IDE-specific setup for WDS + * Dynamically discovers and loads IDE handlers + */ +class IdeManager { + constructor() { + this.handlers = new Map(); + this.loadHandlers(); + this.wdsFolderName = '_wds'; // Default, can be overridden + } + + /** + * Set the WDS folder name for all IDE handlers + * @param {string} wdsFolderName - The WDS folder name + */ + setWdsFolderName(wdsFolderName) { + this.wdsFolderName = wdsFolderName; + // Update all loaded handlers + for (const handler of this.handlers.values()) { + if (typeof handler.setWdsFolderName === 'function') { + handler.setWdsFolderName(wdsFolderName); + } + } + } + + /** + * Dynamically load all IDE handlers from directory + */ + loadHandlers() { + const ideDir = __dirname; + + try { + // Get all JS files in the IDE directory + const files = fs.readdirSync(ideDir).filter((file) => { + // Skip base class, manager, and utility files (starting with _) + return ( + file.endsWith('.js') && + !file.startsWith('_') && + file !== 'manager.js' + ); + }); + + // Sort alphabetically for consistent ordering + files.sort(); + + for (const file of files) { + const moduleName = path.basename(file, '.js'); + + try { + const modulePath = path.join(ideDir, file); + const HandlerModule = require(modulePath); + + // Get the first exported class (handles various export styles) + const HandlerClass = HandlerModule.default || HandlerModule[Object.keys(HandlerModule)[0]]; + + if (HandlerClass) { + const instance = new HandlerClass(); + // Use the name property from the instance (set in constructor) + // Only add if the instance has a valid name + if (instance.name && typeof instance.name === 'string') { + this.handlers.set(instance.name, instance); + } else { + console.log(chalk.yellow(` Warning: ${moduleName} handler missing valid 'name' property`)); + } + } + } catch (error) { + console.log(chalk.yellow(` Warning: Could not load ${moduleName}: ${error.message}`)); + } + } + } catch (error) { + console.error(chalk.red('Failed to load IDE handlers:'), error.message); + } + } + + /** + * Get all available IDEs with their metadata + * @returns {Array} Array of IDE information objects + */ + getAvailableIdes() { + const ides = []; + + for (const [key, handler] of this.handlers) { + // Skip handlers without valid names + const name = handler.displayName || handler.name || key; + + // Filter out invalid entries + if (!key || !name || typeof key !== 'string' || typeof name !== 'string') { + continue; + } + + ides.push({ + value: key, + name: name, + preferred: handler.preferred || false, + }); + } + + // Sort: preferred first, then alphabetical + ides.sort((a, b) => { + if (a.preferred && !b.preferred) return -1; + if (!a.preferred && b.preferred) return 1; + return a.name.localeCompare(b.name); + }); + + return ides; + } + + /** + * Setup IDE integrations for selected IDEs + * Main method called by installer + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Array} selectedIdes - List of IDE names to setup + * @param {Object} options - Setup options (logger, etc.) + * @returns {Object} Results object with success/failure counts + */ + async setup(projectDir, wdsDir, selectedIdes, options = {}) { + const results = { + success: [], + failed: [], + skipped: [], + }; + + const logger = options.logger || console; + + // Set WDS folder name if provided + if (options.wdsFolderName) { + this.setWdsFolderName(options.wdsFolderName); + } + + for (const ideName of selectedIdes) { + const handler = this.handlers.get(ideName.toLowerCase()); + + if (!handler) { + logger.warn(chalk.yellow(` ⚠ IDE '${ideName}' is not yet supported`)); + results.skipped.push({ ide: ideName, reason: 'unsupported' }); + continue; + } + + try { + logger.log(chalk.dim(` Setting up ${handler.displayName || ideName}...`)); + await handler.setup(projectDir, wdsDir, options); + results.success.push(ideName); + logger.log(chalk.green(` ✓ ${handler.displayName || ideName} configured`)); + } catch (error) { + logger.warn(chalk.yellow(` ⚠ Failed to setup ${ideName}: ${error.message}`)); + results.failed.push({ ide: ideName, error: error.message }); + } + } + + // Log summary + if (results.success.length > 0) { + logger.log(chalk.dim(` Configured ${results.success.length} IDE(s)`)); + } + + if (results.failed.length > 0) { + logger.warn(chalk.yellow(` ${results.failed.length} IDE(s) failed to configure`)); + } + + return results; + } + + /** + * Cleanup IDE configurations + * @param {string} projectDir - Project directory + * @returns {Array} Results for each IDE + */ + async cleanup(projectDir) { + const results = []; + + for (const [name, handler] of this.handlers) { + try { + await handler.cleanup(projectDir); + results.push({ ide: name, success: true }); + } catch (error) { + results.push({ ide: name, success: false, error: error.message }); + } + } + + return results; + } + + /** + * Get list of supported IDEs + * @returns {Array} List of supported IDE names + */ + getSupportedIdes() { + return [...this.handlers.keys()]; + } + + /** + * Check if an IDE is supported + * @param {string} ideName - Name of the IDE + * @returns {boolean} True if IDE is supported + */ + isSupported(ideName) { + return this.handlers.has(ideName.toLowerCase()); + } + + /** + * Detect installed IDEs in project + * @param {string} projectDir - Project directory + * @returns {Array} List of detected IDEs + */ + async detectInstalledIdes(projectDir) { + const detected = []; + + for (const [name, handler] of this.handlers) { + if (typeof handler.detect === 'function' && (await handler.detect(projectDir))) { + detected.push(name); + } + } + + return detected; + } +} + +module.exports = { IdeManager }; diff --git a/tools/cli/installers/lib/ide/windsurf.js b/tools/cli/installers/lib/ide/windsurf.js new file mode 100644 index 000000000..94d8f1300 --- /dev/null +++ b/tools/cli/installers/lib/ide/windsurf.js @@ -0,0 +1,108 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); + +/** + * Windsurf IDE setup handler for WDS + */ +class WindsurfSetup extends BaseIdeSetup { + constructor() { + super('windsurf', 'Windsurf', true); // preferred IDE + this.configDir = '.windsurf/workflows/wds'; + } + + /** + * Setup Windsurf IDE configuration + * @param {string} projectDir - Project directory + * @param {string} wdsDir - WDS installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, wdsDir, options = {}) { + // Create .windsurf/workflows/wds directory + const targetDir = path.join(projectDir, this.configDir); + await this.ensureDir(targetDir); + + // Get all WDS agents + const agents = await this.getAgents(wdsDir); + + if (agents.length === 0) { + throw new Error('No agents found in WDS installation'); + } + + // Create launcher file for each agent + let agentCount = 0; + for (const agent of agents) { + // Create launcher content that references the compiled agent + const launcher = this.formatAgentLauncher(agent.name, agent.path); + + // Add Windsurf-specific frontmatter + const content = this.processContent(launcher, agent.metadata); + + // Write launcher file + const filePath = path.join(targetDir, `${agent.slug}.md`); + await this.writeFile(filePath, content); + agentCount++; + } + + if (options.logger) { + options.logger.log(chalk.dim(` - ${agentCount} agent(s) configured for Windsurf`)); + } + + return { + success: true, + agents: agentCount, + }; + } + + /** + * Process content with Windsurf-specific frontmatter + * @param {string} content - Launcher content + * @param {Object} metadata - Agent metadata + * @returns {string} Processed content with Windsurf YAML frontmatter + */ + processContent(content, metadata = {}) { + const description = metadata.name ? + `${metadata.name} - ${metadata.description}` : + metadata.description || 'WDS Agent'; + + return `--- +description: ${description} +auto_execution_mode: 3 +--- + +${content}`; + } + + /** + * Cleanup Windsurf WDS configuration + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const wdsPath = path.join(projectDir, this.configDir); + + if (await this.exists(wdsPath)) { + await this.remove(wdsPath); + console.log(chalk.dim(`Removed Windsurf WDS configuration`)); + } + } + + /** + * Detect if Windsurf is configured in project + * @param {string} projectDir - Project directory + * @returns {boolean} + */ + async detect(projectDir) { + return await this.exists(path.join(projectDir, '.windsurf')); + } + + /** + * Remove directory helper + * @param {string} dirPath - Directory to remove + */ + async remove(dirPath) { + const fs = require('fs-extra'); + await fs.remove(dirPath); + } +} + +module.exports = { WindsurfSetup }; diff --git a/tools/cli/lib/compiler.js b/tools/cli/lib/compiler.js index 45e96ed7c..b6659f127 100644 --- a/tools/cli/lib/compiler.js +++ b/tools/cli/lib/compiler.js @@ -232,7 +232,7 @@ function buildActivationBlock(agent, wdsFolder) { Load persona from this current agent file (already in context) IMMEDIATE ACTION REQUIRED - BEFORE ANY OUTPUT: - Load and read {project-root}/${wdsFolder}/config.yaml NOW - - Store ALL fields as session variables: {user_name}, {communication_language}, {output_folder} + - Store ALL fields as session variables: {user_name}, {communication_language}, {output_folder}, {starting_point}, {project_name} - VERIFY: If config not loaded, STOP and report error to user - DO NOT PROCEED to step 3 until config is successfully loaded and variables stored diff --git a/tools/cli/lib/installer.js b/tools/cli/lib/installer.js index 327285a19..27b03c3bd 100644 --- a/tools/cli/lib/installer.js +++ b/tools/cli/lib/installer.js @@ -100,6 +100,43 @@ class Installer { throw error; } + // Step 3.5: Setup IDE integrations + if (config.ides && config.ides.length > 0) { + const ideSpinner = ora('Setting up IDE integrations...').start(); + try { + const { IdeManager } = require('../installers/lib/ide/manager'); + const ideManager = new IdeManager(); + + const results = await ideManager.setup( + projectDir, + wdsDir, + config.ides, + { + logger: { + log: (msg) => {}, // Suppress detailed logs during spinner + warn: (msg) => console.log(msg), + }, + wdsFolderName: wdsFolder, + } + ); + + const successCount = results.success.length; + const failedCount = results.failed.length; + const skippedCount = results.skipped.length; + + if (successCount > 0) { + ideSpinner.succeed(`IDE integrations configured (${successCount} IDE${successCount > 1 ? 's' : ''})`); + } else if (failedCount > 0 || skippedCount > 0) { + ideSpinner.warn(`IDE setup completed with ${failedCount} failed, ${skippedCount} skipped`); + } else { + ideSpinner.succeed('IDE integrations configured'); + } + } catch (error) { + ideSpinner.warn(`IDE setup encountered issues: ${error.message}`); + console.log(chalk.dim(' You can still use WDS by manually activating agents')); + } + } + // Step 4: Create work products folder structure const rootFolder = root_folder || 'design-process'; const docsSpinner = ora(`Creating project folders in ${rootFolder}/...`).start(); diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index cb270fc2a..89291e030 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -94,7 +94,7 @@ class UI { { type: 'confirm', name: 'install_learning', - message: 'Install learning & reference material? (You can remove it later)', + message: 'Install learning & reference material?', default: true, }, ]);