726 lines
28 KiB
JavaScript
726 lines
28 KiB
JavaScript
const path = require("path");
|
|
const fs = require("fs-extra");
|
|
const yaml = require("js-yaml");
|
|
const fileManager = require("./file-manager");
|
|
const configLoader = require("./config-loader");
|
|
|
|
// Dynamic import for ES module
|
|
let chalk;
|
|
let inquirer;
|
|
|
|
// Initialize ES modules
|
|
async function initializeModules() {
|
|
if (!chalk) {
|
|
chalk = (await import("chalk")).default;
|
|
}
|
|
if (!inquirer) {
|
|
inquirer = (await import("inquirer")).default;
|
|
}
|
|
}
|
|
|
|
class IdeSetup {
|
|
constructor() {
|
|
this.ideAgentConfig = null;
|
|
}
|
|
|
|
async loadIdeAgentConfig() {
|
|
if (this.ideAgentConfig) return this.ideAgentConfig;
|
|
|
|
try {
|
|
const configPath = path.join(__dirname, '..', 'config', 'ide-agent-config.yaml');
|
|
const configContent = await fs.readFile(configPath, 'utf8');
|
|
this.ideAgentConfig = yaml.load(configContent);
|
|
return this.ideAgentConfig;
|
|
} catch (error) {
|
|
console.warn('Failed to load IDE agent configuration, using defaults');
|
|
return {
|
|
'roo-permissions': {},
|
|
'cline-order': {}
|
|
};
|
|
}
|
|
}
|
|
|
|
async setup(ide, installDir, selectedAgent = null, spinner = null) {
|
|
await initializeModules();
|
|
const ideConfig = await configLoader.getIdeConfiguration(ide);
|
|
|
|
if (!ideConfig) {
|
|
console.log(chalk.yellow(`\nNo configuration available for ${ide}`));
|
|
return false;
|
|
}
|
|
|
|
switch (ide) {
|
|
case "cursor":
|
|
return this.setupCursor(installDir, selectedAgent);
|
|
case "claude-code":
|
|
return this.setupClaudeCode(installDir, selectedAgent);
|
|
case "windsurf":
|
|
return this.setupWindsurf(installDir, selectedAgent);
|
|
case "roo":
|
|
return this.setupRoo(installDir, selectedAgent);
|
|
case "cline":
|
|
return this.setupCline(installDir, selectedAgent);
|
|
case "gemini":
|
|
return this.setupGeminiCli(installDir, selectedAgent);
|
|
case "vs-code-copilot":
|
|
return this.setupVsCodeCopilot(installDir, selectedAgent, spinner);
|
|
default:
|
|
console.log(chalk.yellow(`\nIDE ${ide} not yet supported`));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async setupCursor(installDir, selectedAgent) {
|
|
const cursorRulesDir = path.join(installDir, ".cursor", "rules");
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(cursorRulesDir);
|
|
|
|
for (const agentId of agents) {
|
|
// Find the agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`);
|
|
|
|
// Create MDC content with proper format
|
|
let mdcContent = "---\n";
|
|
mdcContent += "description: \n";
|
|
mdcContent += "globs: []\n";
|
|
mdcContent += "alwaysApply: false\n";
|
|
mdcContent += "---\n\n";
|
|
mdcContent += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
|
|
mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir
|
|
)} agent persona.\n\n`;
|
|
mdcContent += "## Agent Activation\n\n";
|
|
mdcContent +=
|
|
"CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
|
|
mdcContent += "```yaml\n";
|
|
// Extract just the YAML content from the agent file
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
if (yamlMatch) {
|
|
mdcContent += yamlMatch[1].trim();
|
|
} else {
|
|
// If no YAML found, include the whole content minus the header
|
|
mdcContent += agentContent.replace(/^#.*$/m, "").trim();
|
|
}
|
|
mdcContent += "\n```\n\n";
|
|
mdcContent += "## File Reference\n\n";
|
|
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
|
mdcContent += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`;
|
|
mdcContent += "## Usage\n\n";
|
|
mdcContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir
|
|
)} persona and follow all instructions defined in the YML configuration above.\n`;
|
|
|
|
await fileManager.writeFile(mdcPath, mdcContent);
|
|
console.log(chalk.green(`✓ Created rule: ${agentId}.mdc`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Created Cursor rules in ${cursorRulesDir}`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupClaudeCode(installDir, selectedAgent) {
|
|
const commandsDir = path.join(installDir, ".claude", "commands");
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(commandsDir);
|
|
|
|
for (const agentId of agents) {
|
|
// Find the agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
const commandPath = path.join(commandsDir, `${agentId}.md`);
|
|
|
|
if (agentPath) {
|
|
// Create command file with agent content
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Add command header
|
|
let commandContent = `# /${agentId} Command\n\n`;
|
|
commandContent += `When this command is used, adopt the following agent persona:\n\n`;
|
|
commandContent += agentContent;
|
|
|
|
await fileManager.writeFile(commandPath, commandContent);
|
|
console.log(chalk.green(`✓ Created command: /${agentId}`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Created Claude Code commands in ${commandsDir}`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupWindsurf(installDir, selectedAgent) {
|
|
const windsurfRulesDir = path.join(installDir, ".windsurf", "rules");
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(windsurfRulesDir);
|
|
|
|
for (const agentId of agents) {
|
|
// Find the agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const mdPath = path.join(windsurfRulesDir, `${agentId}.md`);
|
|
|
|
// Create MD content (similar to Cursor but without frontmatter)
|
|
let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
|
|
mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir
|
|
)} agent persona.\n\n`;
|
|
mdContent += "## Agent Activation\n\n";
|
|
mdContent +=
|
|
"CRITICAL: Read the full YML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
|
|
mdContent += "```yaml\n";
|
|
// Extract just the YAML content from the agent file
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
if (yamlMatch) {
|
|
mdContent += yamlMatch[1].trim();
|
|
} else {
|
|
// If no YAML found, include the whole content minus the header
|
|
mdContent += agentContent.replace(/^#.*$/m, "").trim();
|
|
}
|
|
mdContent += "\n```\n\n";
|
|
mdContent += "## File Reference\n\n";
|
|
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
|
mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
|
|
mdContent += "## Usage\n\n";
|
|
mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir
|
|
)} persona and follow all instructions defined in the YML configuration above.\n`;
|
|
|
|
await fileManager.writeFile(mdPath, mdContent);
|
|
console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async findAgentPath(agentId, installDir) {
|
|
// Try to find the agent file in various locations
|
|
const possiblePaths = [
|
|
path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
|
|
path.join(installDir, "agents", `${agentId}.md`)
|
|
];
|
|
|
|
// Also check expansion pack directories
|
|
const glob = require("glob");
|
|
const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirs) {
|
|
possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`));
|
|
}
|
|
|
|
for (const agentPath of possiblePaths) {
|
|
if (await fileManager.pathExists(agentPath)) {
|
|
return agentPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async getAllAgentIds(installDir) {
|
|
const glob = require("glob");
|
|
const allAgentIds = [];
|
|
|
|
// Check core agents in .bmad-core or root
|
|
let agentsDir = path.join(installDir, ".bmad-core", "agents");
|
|
if (!(await fileManager.pathExists(agentsDir))) {
|
|
agentsDir = path.join(installDir, "agents");
|
|
}
|
|
|
|
if (await fileManager.pathExists(agentsDir)) {
|
|
const agentFiles = glob.sync("*.md", { cwd: agentsDir });
|
|
allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md")));
|
|
}
|
|
|
|
// Also check for expansion pack agents in dot folders
|
|
const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirs) {
|
|
const fullExpDir = path.join(installDir, expDir);
|
|
const expAgentFiles = glob.sync("*.md", { cwd: fullExpDir });
|
|
allAgentIds.push(...expAgentFiles.map((file) => path.basename(file, ".md")));
|
|
}
|
|
|
|
// Remove duplicates
|
|
return [...new Set(allAgentIds)];
|
|
}
|
|
|
|
async getAgentTitle(agentId, installDir) {
|
|
// Try to find the agent file in various locations
|
|
const possiblePaths = [
|
|
path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
|
|
path.join(installDir, "agents", `${agentId}.md`)
|
|
];
|
|
|
|
// Also check expansion pack directories
|
|
const glob = require("glob");
|
|
const expansionDirs = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirs) {
|
|
possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`));
|
|
}
|
|
|
|
for (const agentPath of possiblePaths) {
|
|
if (await fileManager.pathExists(agentPath)) {
|
|
try {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
|
|
if (yamlMatch) {
|
|
const yaml = yamlMatch[1];
|
|
const titleMatch = yaml.match(/title:\s*(.+)/);
|
|
if (titleMatch) {
|
|
return titleMatch[1].trim();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to read agent title for ${agentId}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to formatted agent ID
|
|
return agentId.split('-').map(word =>
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
).join(' ');
|
|
}
|
|
|
|
async setupRoo(installDir, selectedAgent) {
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
// Check for existing .roomodes file in project root
|
|
const roomodesPath = path.join(installDir, ".roomodes");
|
|
let existingModes = [];
|
|
let existingContent = "";
|
|
|
|
if (await fileManager.pathExists(roomodesPath)) {
|
|
existingContent = await fileManager.readFile(roomodesPath);
|
|
// Parse existing modes to avoid duplicates
|
|
const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g);
|
|
for (const match of modeMatches) {
|
|
existingModes.push(match[1]);
|
|
}
|
|
console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`));
|
|
}
|
|
|
|
// Create new modes content
|
|
let newModesContent = "";
|
|
|
|
// Load dynamic agent permissions from configuration
|
|
const config = await this.loadIdeAgentConfig();
|
|
const agentPermissions = config['roo-permissions'] || {};
|
|
|
|
for (const agentId of agents) {
|
|
// Skip if already exists
|
|
if (existingModes.includes(`bmad-${agentId}`)) {
|
|
console.log(chalk.dim(`Skipping ${agentId} - already exists in .roomodes`));
|
|
continue;
|
|
}
|
|
|
|
// Read agent file to extract all information
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Extract YAML content
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
if (yamlMatch) {
|
|
const yaml = yamlMatch[1];
|
|
|
|
// Extract agent info from YAML
|
|
const titleMatch = yaml.match(/title:\s*(.+)/);
|
|
const iconMatch = yaml.match(/icon:\s*(.+)/);
|
|
const whenToUseMatch = yaml.match(/whenToUse:\s*"(.+)"/);
|
|
const roleDefinitionMatch = yaml.match(/roleDefinition:\s*"(.+)"/);
|
|
|
|
const title = titleMatch ? titleMatch[1].trim() : await this.getAgentTitle(agentId, installDir);
|
|
const icon = iconMatch ? iconMatch[1].trim() : "🤖";
|
|
const whenToUse = whenToUseMatch ? whenToUseMatch[1].trim() : `Use for ${title} tasks`;
|
|
const roleDefinition = roleDefinitionMatch
|
|
? roleDefinitionMatch[1].trim()
|
|
: `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`;
|
|
|
|
// Build mode entry with proper formatting (matching exact indentation)
|
|
newModesContent += ` - slug: bmad-${agentId}\n`;
|
|
newModesContent += ` name: '${icon} ${title}'\n`;
|
|
newModesContent += ` roleDefinition: ${roleDefinition}\n`;
|
|
newModesContent += ` whenToUse: ${whenToUse}\n`;
|
|
// Get relative path from installDir to agent file
|
|
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
|
newModesContent += ` customInstructions: CRITICAL Read the full YML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
|
newModesContent += ` groups:\n`;
|
|
newModesContent += ` - read\n`;
|
|
|
|
// Add permissions based on agent type
|
|
const permissions = agentPermissions[agentId];
|
|
if (permissions) {
|
|
newModesContent += ` - - edit\n`;
|
|
newModesContent += ` - fileRegex: ${permissions.fileRegex}\n`;
|
|
newModesContent += ` description: ${permissions.description}\n`;
|
|
} else {
|
|
newModesContent += ` - edit\n`;
|
|
}
|
|
|
|
console.log(chalk.green(`✓ Added mode: bmad-${agentId} (${icon} ${title})`));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build final roomodes content
|
|
let roomodesContent = "";
|
|
if (existingContent) {
|
|
// If there's existing content, append new modes to it
|
|
roomodesContent = existingContent.trim() + "\n" + newModesContent;
|
|
} else {
|
|
// Create new .roomodes file with proper YAML structure
|
|
roomodesContent = "customModes:\n" + newModesContent;
|
|
}
|
|
|
|
// Write .roomodes file
|
|
await fileManager.writeFile(roomodesPath, roomodesContent);
|
|
console.log(chalk.green("✓ Created .roomodes file in project root"));
|
|
|
|
console.log(chalk.green(`\n✓ Roo Code setup complete!`));
|
|
console.log(chalk.dim("Custom modes will be available when you open this project in Roo Code"));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupCline(installDir, selectedAgent) {
|
|
const clineRulesDir = path.join(installDir, ".clinerules");
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(clineRulesDir);
|
|
|
|
// Load dynamic agent ordering from configuration
|
|
const config = await this.loadIdeAgentConfig();
|
|
const agentOrder = config['cline-order'] || {};
|
|
|
|
for (const agentId of agents) {
|
|
// Find the agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Get numeric prefix for ordering
|
|
const order = agentOrder[agentId] || 99;
|
|
const prefix = order.toString().padStart(2, '0');
|
|
const mdPath = path.join(clineRulesDir, `${prefix}-${agentId}.md`);
|
|
|
|
// Create MD content for Cline (focused on project standards and role)
|
|
let mdContent = `# ${await this.getAgentTitle(agentId, installDir)} Agent\n\n`;
|
|
mdContent += `This rule defines the ${await this.getAgentTitle(agentId, installDir)} persona and project standards.\n\n`;
|
|
mdContent += "## Role Definition\n\n";
|
|
mdContent +=
|
|
"When the user types `@" + agentId + "`, adopt this persona and follow these guidelines:\n\n";
|
|
mdContent += "```yaml\n";
|
|
// Extract just the YAML content from the agent file
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
if (yamlMatch) {
|
|
mdContent += yamlMatch[1].trim();
|
|
} else {
|
|
// If no YAML found, include the whole content minus the header
|
|
mdContent += agentContent.replace(/^#.*$/m, "").trim();
|
|
}
|
|
mdContent += "\n```\n\n";
|
|
mdContent += "## Project Standards\n\n";
|
|
mdContent += `- Always maintain consistency with project documentation in .bmad-core/\n`;
|
|
mdContent += `- Follow the agent's specific guidelines and constraints\n`;
|
|
mdContent += `- Update relevant project files when making changes\n`;
|
|
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
|
mdContent += `- Reference the complete agent definition in [${relativePath}](${relativePath})\n\n`;
|
|
mdContent += "## Usage\n\n";
|
|
mdContent += `Type \`@${agentId}\` to activate this ${await this.getAgentTitle(agentId, installDir)} persona.\n`;
|
|
|
|
await fileManager.writeFile(mdPath, mdContent);
|
|
console.log(chalk.green(`✓ Created rule: ${prefix}-${agentId}.md`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Created Cline rules in ${clineRulesDir}`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupGeminiCli(installDir, selectedAgent) {
|
|
await initializeModules();
|
|
const geminiDir = path.join(installDir, ".gemini");
|
|
const agentsContextDir = path.join(geminiDir, "agents");
|
|
await fileManager.ensureDirectory(agentsContextDir);
|
|
|
|
// Get all available agents
|
|
const agents = await this.getAllAgentIds(installDir);
|
|
const agentContextFiles = [];
|
|
|
|
for (const agentId of agents) {
|
|
// Find the source agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const contextFilePath = path.join(agentsContextDir, `${agentId}.md`);
|
|
|
|
// Copy the agent content directly into its own context file
|
|
await fileManager.writeFile(contextFilePath, agentContent);
|
|
|
|
// Store the relative path for settings.json
|
|
const relativePath = path.relative(geminiDir, contextFilePath);
|
|
agentContextFiles.push(relativePath.replace(/\\/g, '/')); // Ensure forward slashes for consistency
|
|
console.log(chalk.green(`✓ Created context file for @${agentId}`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Created individual agent context files in ${agentsContextDir}`));
|
|
|
|
// Add GEMINI.md to the context files array
|
|
agentContextFiles.push("GEMINI.md");
|
|
|
|
// Create or update settings.json
|
|
const settingsPath = path.join(geminiDir, "settings.json");
|
|
let settings = {};
|
|
|
|
if (await fileManager.pathExists(settingsPath)) {
|
|
try {
|
|
const existingSettings = await fileManager.readFile(settingsPath);
|
|
settings = JSON.parse(existingSettings);
|
|
console.log(chalk.yellow("Found existing .gemini/settings.json. Merging settings..."));
|
|
} catch (e) {
|
|
console.error(chalk.red("Error parsing existing settings.json. It will be overwritten."), e);
|
|
settings = {};
|
|
}
|
|
}
|
|
|
|
// Set contextFileName to our new array of files
|
|
settings.contextFileName = agentContextFiles;
|
|
|
|
await fileManager.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
console.log(chalk.green(`✓ Configured .gemini/settings.json to load all agent context files.`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupVsCodeCopilot(installDir, selectedAgent, spinner = null) {
|
|
await initializeModules();
|
|
|
|
// Configure VS Code workspace settings first to avoid UI conflicts with loading spinners
|
|
await this.configureVsCodeSettings(installDir, spinner);
|
|
|
|
const chatmodesDir = path.join(installDir, ".github", "chatmodes");
|
|
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(chatmodesDir);
|
|
|
|
for (const agentId of agents) {
|
|
// Find the agent file
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
const chatmodePath = path.join(chatmodesDir, `${agentId}.chatmode.md`);
|
|
|
|
if (agentPath) {
|
|
// Create chat mode file with agent content
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const agentTitle = await this.getAgentTitle(agentId, installDir);
|
|
|
|
// Extract whenToUse for the description
|
|
const yamlMatch = agentContent.match(/```ya?ml\n([\s\S]*?)```/);
|
|
let description = `Activates the ${agentTitle} agent persona.`;
|
|
if (yamlMatch) {
|
|
const whenToUseMatch = yamlMatch[1].match(/whenToUse:\s*"(.*?)"/);
|
|
if (whenToUseMatch && whenToUseMatch[1]) {
|
|
description = whenToUseMatch[1];
|
|
}
|
|
}
|
|
|
|
let chatmodeContent = `---
|
|
description: "${description.replace(/"/g, '\\"')}"
|
|
tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems', 'usages']
|
|
---
|
|
|
|
`;
|
|
chatmodeContent += agentContent;
|
|
|
|
await fileManager.writeFile(chatmodePath, chatmodeContent);
|
|
console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ VS Code Copilot setup complete!`));
|
|
console.log(chalk.dim(`You can now find the BMAD agents in the Chat view's mode selector.`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async configureVsCodeSettings(installDir, spinner) {
|
|
await initializeModules(); // Ensure inquirer is loaded
|
|
const vscodeDir = path.join(installDir, ".vscode");
|
|
const settingsPath = path.join(vscodeDir, "settings.json");
|
|
|
|
await fileManager.ensureDirectory(vscodeDir);
|
|
|
|
// Read existing settings if they exist
|
|
let existingSettings = {};
|
|
if (await fileManager.pathExists(settingsPath)) {
|
|
try {
|
|
const existingContent = await fileManager.readFile(settingsPath);
|
|
existingSettings = JSON.parse(existingContent);
|
|
console.log(chalk.yellow("Found existing .vscode/settings.json. Merging BMAD settings..."));
|
|
} catch (error) {
|
|
console.warn(chalk.yellow("Could not parse existing settings.json. Creating new one."));
|
|
existingSettings = {};
|
|
}
|
|
}
|
|
|
|
// Clear any previous output and add spacing to avoid conflicts with loaders
|
|
console.log('\n'.repeat(2));
|
|
console.log(chalk.blue("🔧 VS Code Copilot Agent Settings Configuration"));
|
|
console.log(chalk.dim("BMAD works best with specific VS Code settings for optimal agent experience."));
|
|
console.log(''); // Add extra spacing
|
|
|
|
const { configChoice } = await inquirer.prompt([
|
|
{
|
|
type: 'list',
|
|
name: 'configChoice',
|
|
message: 'How would you like to configure VS Code Copilot settings?',
|
|
choices: [
|
|
{
|
|
name: 'Use recommended defaults (fastest setup)',
|
|
value: 'defaults'
|
|
},
|
|
{
|
|
name: 'Configure each setting manually (customize to your preferences)',
|
|
value: 'manual'
|
|
},
|
|
{
|
|
name: 'Skip settings configuration (I\'ll configure manually later)\n',
|
|
value: 'skip'
|
|
}
|
|
],
|
|
default: 'defaults'
|
|
}
|
|
]);
|
|
|
|
let bmadSettings = {};
|
|
|
|
if (configChoice === 'skip') {
|
|
console.log(chalk.yellow("⚠️ Skipping VS Code settings configuration."));
|
|
console.log(chalk.dim("You can manually configure these settings in .vscode/settings.json:"));
|
|
console.log(chalk.dim(" • chat.agent.enabled: true"));
|
|
console.log(chalk.dim(" • chat.agent.maxRequests: 15"));
|
|
console.log(chalk.dim(" • github.copilot.chat.agent.runTasks: true"));
|
|
console.log(chalk.dim(" • chat.mcp.discovery.enabled: true"));
|
|
console.log(chalk.dim(" • github.copilot.chat.agent.autoFix: true"));
|
|
console.log(chalk.dim(" • chat.tools.autoApprove: false"));
|
|
return true;
|
|
}
|
|
|
|
if (configChoice === 'defaults') {
|
|
// Use recommended defaults
|
|
bmadSettings = {
|
|
"chat.agent.enabled": true,
|
|
"chat.agent.maxRequests": 15,
|
|
"github.copilot.chat.agent.runTasks": true,
|
|
"chat.mcp.discovery.enabled": true,
|
|
"github.copilot.chat.agent.autoFix": true,
|
|
"chat.tools.autoApprove": false
|
|
};
|
|
console.log(chalk.green("✓ Using recommended BMAD defaults for VS Code Copilot settings"));
|
|
} else {
|
|
// Manual configuration
|
|
console.log(chalk.blue("\n📋 Let's configure each setting for your preferences:"));
|
|
|
|
// Pause spinner during manual configuration prompts
|
|
let spinnerWasActive = false;
|
|
if (spinner && spinner.isSpinning) {
|
|
spinner.stop();
|
|
spinnerWasActive = true;
|
|
}
|
|
|
|
const manualSettings = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'maxRequests',
|
|
message: 'Maximum requests per agent session (recommended: 15)?',
|
|
default: '15',
|
|
validate: (input) => {
|
|
const num = parseInt(input);
|
|
if (isNaN(num) || num < 1 || num > 50) {
|
|
return 'Please enter a number between 1 and 50';
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
name: 'runTasks',
|
|
message: 'Allow agents to run workspace tasks (package.json scripts, etc.)?',
|
|
default: true
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
name: 'mcpDiscovery',
|
|
message: 'Enable MCP (Model Context Protocol) server discovery?',
|
|
default: true
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
name: 'autoFix',
|
|
message: 'Enable automatic error detection and fixing in generated code?',
|
|
default: true
|
|
},
|
|
{
|
|
type: 'confirm',
|
|
name: 'autoApprove',
|
|
message: 'Auto-approve ALL tools without confirmation? (⚠️ EXPERIMENTAL - less secure)',
|
|
default: false
|
|
}
|
|
]);
|
|
|
|
// Restart spinner if it was active before prompts
|
|
if (spinner && spinnerWasActive) {
|
|
spinner.start();
|
|
}
|
|
|
|
bmadSettings = {
|
|
"chat.agent.enabled": true, // Always enabled - required for BMAD agents
|
|
"chat.agent.maxRequests": parseInt(manualSettings.maxRequests),
|
|
"github.copilot.chat.agent.runTasks": manualSettings.runTasks,
|
|
"chat.mcp.discovery.enabled": manualSettings.mcpDiscovery,
|
|
"github.copilot.chat.agent.autoFix": manualSettings.autoFix,
|
|
"chat.tools.autoApprove": manualSettings.autoApprove
|
|
};
|
|
|
|
console.log(chalk.green("✓ Custom settings configured"));
|
|
}
|
|
|
|
// Merge settings (existing settings take precedence to avoid overriding user preferences)
|
|
const mergedSettings = { ...bmadSettings, ...existingSettings };
|
|
|
|
// Write the updated settings
|
|
await fileManager.writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
|
|
console.log(chalk.green("✓ VS Code workspace settings configured successfully"));
|
|
console.log(chalk.dim(" Settings written to .vscode/settings.json:"));
|
|
Object.entries(bmadSettings).forEach(([key, value]) => {
|
|
console.log(chalk.dim(` • ${key}: ${value}`));
|
|
});
|
|
console.log(chalk.dim(""));
|
|
console.log(chalk.dim("You can modify these settings anytime in .vscode/settings.json"));
|
|
}
|
|
}
|
|
|
|
module.exports = new IdeSetup();
|