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:
parent
b120dd9327
commit
4596fad5d6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue