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 <noreply@anthropic.com>
This commit is contained in:
Mårten Angner 2026-02-24 19:53:39 +01:00
parent b120dd9327
commit 4596fad5d6
11 changed files with 1094 additions and 6 deletions

View File

@ -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

View File

@ -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 `<!-- WDS Agent Launcher -->
<!-- This file references the compiled agent. Do not edit directly. -->
<!-- Source: ${this.wdsFolderName}/agents/${agentName}.md -->
@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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 += '<!-- Generated by WDS installer - Do not edit manually -->\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 };

View File

@ -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<string>} 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 };

View File

@ -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 };

View File

@ -232,7 +232,7 @@ function buildActivationBlock(agent, wdsFolder) {
<step n="1">Load persona from this current agent file (already in context)</step>
<step n="2">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
</step>

View File

@ -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();

View File

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