2256 lines
70 KiB
JavaScript
2256 lines
70 KiB
JavaScript
const path = require("node:path");
|
|
const fs = require("fs-extra");
|
|
const yaml = require("js-yaml");
|
|
const chalk = require("chalk");
|
|
const inquirer = require("inquirer");
|
|
const fileManager = require("./file-manager");
|
|
const configLoader = require("./config-loader");
|
|
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|
|
const BaseIdeSetup = require("./ide-base-setup");
|
|
const resourceLocator = require("./resource-locator");
|
|
|
|
class IdeSetup extends BaseIdeSetup {
|
|
constructor() {
|
|
super();
|
|
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 {
|
|
console.warn("Failed to load IDE agent configuration, using defaults");
|
|
return {
|
|
"roo-permissions": {},
|
|
"cline-order": {},
|
|
};
|
|
}
|
|
}
|
|
|
|
async setup(
|
|
ide,
|
|
installDir,
|
|
selectedAgent = null,
|
|
spinner = null,
|
|
preConfiguredSettings = null,
|
|
) {
|
|
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 "iflow-cli": {
|
|
return this.setupIFlowCli(installDir, selectedAgent);
|
|
}
|
|
case "crush": {
|
|
return this.setupCrush(installDir, selectedAgent);
|
|
}
|
|
case "windsurf": {
|
|
return this.setupWindsurf(installDir, selectedAgent);
|
|
}
|
|
case "trae": {
|
|
return this.setupTrae(installDir, selectedAgent);
|
|
}
|
|
case "roo": {
|
|
return this.setupRoo(installDir, selectedAgent);
|
|
}
|
|
case "cline": {
|
|
return this.setupCline(installDir, selectedAgent);
|
|
}
|
|
case "kilo": {
|
|
return this.setupKilocode(installDir, selectedAgent);
|
|
}
|
|
case "gemini": {
|
|
return this.setupGeminiCli(installDir, selectedAgent);
|
|
}
|
|
case "opencode": {
|
|
return this.setupGeminiCli(installDir, selectedAgent);
|
|
}
|
|
case "github-copilot": {
|
|
return this.setupGitHubCopilot(
|
|
installDir,
|
|
selectedAgent,
|
|
spinner,
|
|
preConfiguredSettings,
|
|
);
|
|
}
|
|
case "qwen-code": {
|
|
return this.setupQwenCode(installDir, selectedAgent);
|
|
}
|
|
case "auggie-cli": {
|
|
return this.setupAuggieCLI(
|
|
installDir,
|
|
selectedAgent,
|
|
spinner,
|
|
preConfiguredSettings,
|
|
);
|
|
}
|
|
case "codex": {
|
|
return this.setupCodex(installDir, selectedAgent, {
|
|
webEnabled: false,
|
|
});
|
|
}
|
|
case "codex-web": {
|
|
return this.setupCodex(installDir, selectedAgent, { webEnabled: true });
|
|
}
|
|
default: {
|
|
console.log(chalk.yellow(`\nIDE ${ide} not yet supported`));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async setupCodex(installDir, selectedAgent, options) {
|
|
options = options ?? { webEnabled: false };
|
|
// Codex reads AGENTS.md at the project root as project memory (CLI & Web).
|
|
// Inject/update a BMAD section with guidance, directory, and details.
|
|
const filePath = path.join(installDir, "AGENTS.md");
|
|
const startMarker = "<!-- BEGIN: BMAD-AGENTS -->";
|
|
const endMarker = "<!-- END: BMAD-AGENTS -->";
|
|
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
const tasks = await this.getAllTaskIds(installDir);
|
|
|
|
// Build BMAD section content
|
|
let section = "";
|
|
section += `${startMarker}\n`;
|
|
section += `# BMAD-METHOD Agents and Tasks\n\n`;
|
|
section += `This section is auto-generated by BMAD-METHOD for Codex. Codex merges this AGENTS.md into context.\n\n`;
|
|
section += `## How To Use With Codex\n\n`;
|
|
section += `- Codex CLI: run \`codex\` in this project. Reference an agent naturally, e.g., "As dev, implement ...".\n`;
|
|
section += `- Codex Web: open this repo and reference roles the same way; Codex reads \`AGENTS.md\`.\n`;
|
|
section += `- Commit \`.bmad-core\` and this \`AGENTS.md\` file to your repo so Codex (Web/CLI) can read full agent definitions.\n`;
|
|
section += `- Refresh this section after agent updates: \`npx bmad-method install -f -i codex\`.\n\n`;
|
|
|
|
section += `### Helpful Commands\n\n`;
|
|
section += `- List agents: \`npx bmad-method list:agents\`\n`;
|
|
section += `- Reinstall BMAD core and regenerate AGENTS.md: \`npx bmad-method install -f -i codex\`\n`;
|
|
section += `- Validate configuration: \`npx bmad-method validate\`\n\n`;
|
|
|
|
// Agents directory table
|
|
section += `## Agents\n\n`;
|
|
section += `### Directory\n\n`;
|
|
section += `| Title | ID | When To Use |\n|---|---|---|\n`;
|
|
const agentSummaries = [];
|
|
for (const agentId of agents) {
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
if (!agentPath) continue;
|
|
const raw = await fileManager.readFile(agentPath);
|
|
const yamlMatch = raw.match(/```ya?ml\r?\n([\s\S]*?)```/);
|
|
const yamlBlock = yamlMatch ? yamlMatch[1].trim() : null;
|
|
const title = await this.getAgentTitle(agentId, installDir);
|
|
const whenToUse =
|
|
yamlBlock?.match(/whenToUse:\s*"?([^\n"]+)"?/i)?.[1]?.trim() || "";
|
|
agentSummaries.push({
|
|
agentId,
|
|
title,
|
|
whenToUse,
|
|
yamlBlock,
|
|
raw,
|
|
path: agentPath,
|
|
});
|
|
section += `| ${title} | ${agentId} | ${whenToUse || "—"} |\n`;
|
|
}
|
|
section += `\n`;
|
|
|
|
// Detailed agent sections
|
|
for (const {
|
|
agentId,
|
|
title,
|
|
whenToUse,
|
|
yamlBlock,
|
|
raw,
|
|
path: agentPath,
|
|
} of agentSummaries) {
|
|
const relativePath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
section += `### ${title} (id: ${agentId})\n`;
|
|
section += `Source: ${relativePath}\n\n`;
|
|
if (whenToUse) section += `- When to use: ${whenToUse}\n`;
|
|
section += `- How to activate: Mention "As ${agentId}, ..." or "Use ${title} to ..."\n\n`;
|
|
if (yamlBlock) {
|
|
section += "```yaml\n" + yamlBlock + "\n```\n\n";
|
|
} else {
|
|
section += "```md\n" + raw.trim() + "\n```\n\n";
|
|
}
|
|
}
|
|
|
|
// Tasks
|
|
if (tasks && tasks.length > 0) {
|
|
section += `## Tasks\n\n`;
|
|
section += `These are reusable task briefs you can reference directly in Codex.\n\n`;
|
|
for (const taskId of tasks) {
|
|
const taskPath = await this.findTaskPath(taskId, installDir);
|
|
if (!taskPath) continue;
|
|
const raw = await fileManager.readFile(taskPath);
|
|
const relativePath = path
|
|
.relative(installDir, taskPath)
|
|
.replaceAll("\\", "/");
|
|
section += `### Task: ${taskId}\n`;
|
|
section += `Source: ${relativePath}\n`;
|
|
section += `- How to use: "Use task ${taskId} with the appropriate agent" and paste relevant parts as needed.\n\n`;
|
|
section += "```md\n" + raw.trim() + "\n```\n\n";
|
|
}
|
|
}
|
|
|
|
section += `${endMarker}\n`;
|
|
|
|
// Write or update AGENTS.md
|
|
let finalContent = "";
|
|
if (await fileManager.pathExists(filePath)) {
|
|
const existing = await fileManager.readFile(filePath);
|
|
if (existing.includes(startMarker) && existing.includes(endMarker)) {
|
|
// Replace existing BMAD block
|
|
const pattern = String.raw`${startMarker}[\s\S]*?${endMarker}`;
|
|
const replaced = existing.replace(new RegExp(pattern, "m"), section);
|
|
finalContent = replaced;
|
|
} else {
|
|
// Append BMAD block to existing file
|
|
finalContent = existing.trimEnd() + `\n\n` + section;
|
|
}
|
|
} else {
|
|
// Create fresh AGENTS.md with a small header and BMAD block
|
|
finalContent += "# Project Agents\n\n";
|
|
finalContent +=
|
|
"This file provides guidance and memory for Codex CLI.\n\n";
|
|
finalContent += section;
|
|
}
|
|
|
|
await fileManager.writeFile(filePath, finalContent);
|
|
console.log(
|
|
chalk.green("✓ Created/updated AGENTS.md for Codex CLI integration"),
|
|
);
|
|
console.log(
|
|
chalk.dim(
|
|
"Codex reads AGENTS.md automatically. Run `codex` in this project to use BMAD agents.",
|
|
),
|
|
);
|
|
|
|
// Optionally add helpful npm scripts if a package.json exists
|
|
try {
|
|
const pkgPath = path.join(installDir, "package.json");
|
|
if (await fileManager.pathExists(pkgPath)) {
|
|
const pkgRaw = await fileManager.readFile(pkgPath);
|
|
const pkg = JSON.parse(pkgRaw);
|
|
pkg.scripts = pkg.scripts || {};
|
|
const updated = { ...pkg.scripts };
|
|
if (!updated["bmad:refresh"])
|
|
updated["bmad:refresh"] = "bmad-method install -f -i codex";
|
|
if (!updated["bmad:list"])
|
|
updated["bmad:list"] = "bmad-method list:agents";
|
|
if (!updated["bmad:validate"])
|
|
updated["bmad:validate"] = "bmad-method validate";
|
|
const changed = JSON.stringify(updated) !== JSON.stringify(pkg.scripts);
|
|
if (changed) {
|
|
const newPkg = { ...pkg, scripts: updated };
|
|
await fileManager.writeFile(
|
|
pkgPath,
|
|
JSON.stringify(newPkg, null, 2) + "\n",
|
|
);
|
|
console.log(
|
|
chalk.green(
|
|
"✓ Added npm scripts: bmad:refresh, bmad:list, bmad:validate",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
console.log(
|
|
chalk.yellow(
|
|
"! Skipped adding npm scripts (package.json not writable or invalid)",
|
|
),
|
|
);
|
|
}
|
|
|
|
// Adjust .gitignore behavior depending on Codex mode
|
|
try {
|
|
const gitignorePath = path.join(installDir, ".gitignore");
|
|
const ignoreLines = ["# BMAD (local only)", ".bmad-core/", ".bmad-*/"];
|
|
const exists = await fileManager.pathExists(gitignorePath);
|
|
if (options.webEnabled) {
|
|
if (exists) {
|
|
let gi = await fileManager.readFile(gitignorePath);
|
|
// Remove lines that ignore BMAD dot-folders
|
|
const updated = gi
|
|
.split(/\r?\n/)
|
|
.filter(
|
|
(l) =>
|
|
!/^\s*\.bmad-core\/?\s*$/.test(l) &&
|
|
!/^\s*\.bmad-\*\/?\s*$/.test(l),
|
|
)
|
|
.join("\n");
|
|
if (updated !== gi) {
|
|
await fileManager.writeFile(
|
|
gitignorePath,
|
|
updated.trimEnd() + "\n",
|
|
);
|
|
console.log(
|
|
chalk.green(
|
|
"✓ Updated .gitignore to include .bmad-core in commits",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Local-only: add ignores if missing
|
|
let base = exists ? await fileManager.readFile(gitignorePath) : "";
|
|
const haveCore = base.includes(".bmad-core/");
|
|
const haveStar = base.includes(".bmad-*/");
|
|
if (!haveCore || !haveStar) {
|
|
const sep = base.endsWith("\n") || base.length === 0 ? "" : "\n";
|
|
const add = [!haveCore || !haveStar ? ignoreLines.join("\n") : ""]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
const out = base + sep + add + "\n";
|
|
await fileManager.writeFile(gitignorePath, out);
|
|
console.log(
|
|
chalk.green(
|
|
"✓ Added .bmad-core/* to .gitignore for local-only Codex setup",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
console.log(chalk.yellow("! Could not update .gitignore (skipping)"));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupCursor(installDir, selectedAgent) {
|
|
const cursorRulesDir = path.join(installDir, ".cursor", "rules", "bmad");
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(cursorRulesDir);
|
|
|
|
for (const agentId of agents) {
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
|
|
if (agentPath) {
|
|
const mdcContent = await this.createAgentRuleContent(
|
|
agentId,
|
|
agentPath,
|
|
installDir,
|
|
"mdc",
|
|
);
|
|
const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`);
|
|
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 setupCrush(installDir, selectedAgent) {
|
|
// Setup bmad-core commands
|
|
const coreSlashPrefix = await this.getCoreSlashPrefix(installDir);
|
|
const coreAgents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getCoreAgentIds(installDir);
|
|
const coreTasks = await this.getCoreTaskIds(installDir);
|
|
await this.setupCrushForPackage(
|
|
installDir,
|
|
"core",
|
|
coreSlashPrefix,
|
|
coreAgents,
|
|
coreTasks,
|
|
".bmad-core",
|
|
);
|
|
|
|
// Setup expansion pack commands
|
|
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
|
|
for (const packInfo of expansionPacks) {
|
|
const packSlashPrefix = await this.getExpansionPackSlashPrefix(
|
|
packInfo.path,
|
|
);
|
|
const packAgents = await this.getExpansionPackAgents(packInfo.path);
|
|
const packTasks = await this.getExpansionPackTasks(packInfo.path);
|
|
|
|
if (packAgents.length > 0 || packTasks.length > 0) {
|
|
// Use the actual directory name where the expansion pack is installed
|
|
const rootPath = path.relative(installDir, packInfo.path);
|
|
await this.setupCrushForPackage(
|
|
installDir,
|
|
packInfo.name,
|
|
packSlashPrefix,
|
|
packAgents,
|
|
packTasks,
|
|
rootPath,
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupClaudeCode(installDir, selectedAgent) {
|
|
// Setup bmad-core commands
|
|
const coreSlashPrefix = await this.getCoreSlashPrefix(installDir);
|
|
const coreAgents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getCoreAgentIds(installDir);
|
|
const coreTasks = await this.getCoreTaskIds(installDir);
|
|
await this.setupClaudeCodeForPackage(
|
|
installDir,
|
|
"core",
|
|
coreSlashPrefix,
|
|
coreAgents,
|
|
coreTasks,
|
|
".bmad-core",
|
|
);
|
|
|
|
// Setup expansion pack commands
|
|
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
|
|
for (const packInfo of expansionPacks) {
|
|
const packSlashPrefix = await this.getExpansionPackSlashPrefix(
|
|
packInfo.path,
|
|
);
|
|
const packAgents = await this.getExpansionPackAgents(packInfo.path);
|
|
const packTasks = await this.getExpansionPackTasks(packInfo.path);
|
|
|
|
if (packAgents.length > 0 || packTasks.length > 0) {
|
|
// Use the actual directory name where the expansion pack is installed
|
|
const rootPath = path.relative(installDir, packInfo.path);
|
|
await this.setupClaudeCodeForPackage(
|
|
installDir,
|
|
packInfo.name,
|
|
packSlashPrefix,
|
|
packAgents,
|
|
packTasks,
|
|
rootPath,
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupClaudeCodeForPackage(
|
|
installDir,
|
|
packageName,
|
|
slashPrefix,
|
|
agentIds,
|
|
taskIds,
|
|
rootPath,
|
|
) {
|
|
const commandsBaseDir = path.join(
|
|
installDir,
|
|
".claude",
|
|
"commands",
|
|
slashPrefix,
|
|
);
|
|
const agentsDir = path.join(commandsBaseDir, "agents");
|
|
const tasksDir = path.join(commandsBaseDir, "tasks");
|
|
|
|
// Ensure directories exist
|
|
await fileManager.ensureDirectory(agentsDir);
|
|
await fileManager.ensureDirectory(tasksDir);
|
|
|
|
// Setup agents
|
|
for (const agentId of agentIds) {
|
|
// Find the agent file - for expansion packs, prefer the expansion pack version
|
|
let agentPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the agent in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"agents",
|
|
`${agentId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
agentPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(agentsDir, `${agentId}.md`);
|
|
|
|
if (agentPath) {
|
|
// Create command file with agent content
|
|
let agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
agentContent = agentContent.replaceAll("{root}", rootPath);
|
|
|
|
// 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 agent command: /${agentId}`));
|
|
}
|
|
}
|
|
|
|
// Setup tasks
|
|
for (const taskId of taskIds) {
|
|
// Find the task file - for expansion packs, prefer the expansion pack version
|
|
let taskPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the task in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"tasks",
|
|
`${taskId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
taskPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(tasksDir, `${taskId}.md`);
|
|
|
|
if (taskPath) {
|
|
// Create command file with task content
|
|
let taskContent = await fileManager.readFile(taskPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
taskContent = taskContent.replaceAll("{root}", rootPath);
|
|
|
|
// Add command header
|
|
let commandContent = `# /${taskId} Task\n\n`;
|
|
commandContent += `When this command is used, execute the following task:\n\n`;
|
|
commandContent += taskContent;
|
|
|
|
await fileManager.writeFile(commandPath, commandContent);
|
|
console.log(chalk.green(`✓ Created task command: /${taskId}`));
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(
|
|
`\n✓ Created Claude Code commands for ${packageName} in ${commandsBaseDir}`,
|
|
),
|
|
);
|
|
console.log(chalk.dim(` - Agents in: ${agentsDir}`));
|
|
console.log(chalk.dim(` - Tasks in: ${tasksDir}`));
|
|
}
|
|
|
|
async setupIFlowCli(installDir, selectedAgent) {
|
|
// Setup bmad-core commands
|
|
const coreSlashPrefix = await this.getCoreSlashPrefix(installDir);
|
|
const coreAgents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getCoreAgentIds(installDir);
|
|
const coreTasks = await this.getCoreTaskIds(installDir);
|
|
await this.setupIFlowCliForPackage(
|
|
installDir,
|
|
"core",
|
|
coreSlashPrefix,
|
|
coreAgents,
|
|
coreTasks,
|
|
".bmad-core",
|
|
);
|
|
|
|
// Setup expansion pack commands
|
|
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
|
|
for (const packInfo of expansionPacks) {
|
|
const packSlashPrefix = await this.getExpansionPackSlashPrefix(
|
|
packInfo.path,
|
|
);
|
|
const packAgents = await this.getExpansionPackAgents(packInfo.path);
|
|
const packTasks = await this.getExpansionPackTasks(packInfo.path);
|
|
|
|
if (packAgents.length > 0 || packTasks.length > 0) {
|
|
// Use the actual directory name where the expansion pack is installed
|
|
const rootPath = path.relative(installDir, packInfo.path);
|
|
await this.setupIFlowCliForPackage(
|
|
installDir,
|
|
packInfo.name,
|
|
packSlashPrefix,
|
|
packAgents,
|
|
packTasks,
|
|
rootPath,
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupIFlowCliForPackage(
|
|
installDir,
|
|
packageName,
|
|
slashPrefix,
|
|
agentIds,
|
|
taskIds,
|
|
rootPath,
|
|
) {
|
|
const commandsBaseDir = path.join(
|
|
installDir,
|
|
".iflow",
|
|
"commands",
|
|
slashPrefix,
|
|
);
|
|
const agentsDir = path.join(commandsBaseDir, "agents");
|
|
const tasksDir = path.join(commandsBaseDir, "tasks");
|
|
|
|
// Ensure directories exist
|
|
await fileManager.ensureDirectory(agentsDir);
|
|
await fileManager.ensureDirectory(tasksDir);
|
|
|
|
// Setup agents
|
|
for (const agentId of agentIds) {
|
|
// Find the agent file - for expansion packs, prefer the expansion pack version
|
|
let agentPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the agent in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"agents",
|
|
`${agentId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
agentPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(agentsDir, `${agentId}.md`);
|
|
|
|
if (agentPath) {
|
|
// Create command file with agent content
|
|
let agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
agentContent = agentContent.replaceAll("{root}", rootPath);
|
|
|
|
// 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 agent command: /${agentId}`));
|
|
}
|
|
}
|
|
|
|
// Setup tasks
|
|
for (const taskId of taskIds) {
|
|
// Find the task file - for expansion packs, prefer the expansion pack version
|
|
let taskPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the task in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"tasks",
|
|
`${taskId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
taskPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(tasksDir, `${taskId}.md`);
|
|
|
|
if (taskPath) {
|
|
// Create command file with task content
|
|
let taskContent = await fileManager.readFile(taskPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
taskContent = taskContent.replaceAll("{root}", rootPath);
|
|
|
|
// Add command header
|
|
let commandContent = `# /${taskId} Task\n\n`;
|
|
commandContent += `When this command is used, execute the following task:\n\n`;
|
|
commandContent += taskContent;
|
|
|
|
await fileManager.writeFile(commandPath, commandContent);
|
|
console.log(chalk.green(`✓ Created task command: /${taskId}`));
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(
|
|
`\n✓ Created iFlow CLI commands for ${packageName} in ${commandsBaseDir}`,
|
|
),
|
|
);
|
|
console.log(chalk.dim(` - Agents in: ${agentsDir}`));
|
|
console.log(chalk.dim(` - Tasks in: ${tasksDir}`));
|
|
}
|
|
|
|
async setupCrushForPackage(
|
|
installDir,
|
|
packageName,
|
|
slashPrefix,
|
|
agentIds,
|
|
taskIds,
|
|
rootPath,
|
|
) {
|
|
const commandsBaseDir = path.join(
|
|
installDir,
|
|
".crush",
|
|
"commands",
|
|
slashPrefix,
|
|
);
|
|
const agentsDir = path.join(commandsBaseDir, "agents");
|
|
const tasksDir = path.join(commandsBaseDir, "tasks");
|
|
|
|
// Ensure directories exist
|
|
await fileManager.ensureDirectory(agentsDir);
|
|
await fileManager.ensureDirectory(tasksDir);
|
|
|
|
// Setup agents
|
|
for (const agentId of agentIds) {
|
|
// Find the agent file - for expansion packs, prefer the expansion pack version
|
|
let agentPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the agent in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"agents",
|
|
`${agentId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
agentPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
agentPath = await this.findAgentPath(agentId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(agentsDir, `${agentId}.md`);
|
|
|
|
if (agentPath) {
|
|
// Create command file with agent content
|
|
let agentContent = await fileManager.readFile(agentPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
agentContent = agentContent.replaceAll("{root}", rootPath);
|
|
|
|
// 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 agent command: /${agentId}`));
|
|
}
|
|
}
|
|
|
|
// Setup tasks
|
|
for (const taskId of taskIds) {
|
|
// Find the task file - for expansion packs, prefer the expansion pack version
|
|
let taskPath;
|
|
if (packageName === "core") {
|
|
// For core, use the normal search
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
} else {
|
|
// For expansion packs, first try to find the task in the expansion pack directory
|
|
const expansionPackPath = path.join(
|
|
installDir,
|
|
rootPath,
|
|
"tasks",
|
|
`${taskId}.md`,
|
|
);
|
|
if (await fileManager.pathExists(expansionPackPath)) {
|
|
taskPath = expansionPackPath;
|
|
} else {
|
|
// Fall back to core if not found in expansion pack
|
|
taskPath = await this.findTaskPath(taskId, installDir);
|
|
}
|
|
}
|
|
|
|
const commandPath = path.join(tasksDir, `${taskId}.md`);
|
|
|
|
if (taskPath) {
|
|
// Create command file with task content
|
|
let taskContent = await fileManager.readFile(taskPath);
|
|
|
|
// Replace {root} placeholder with the appropriate root path for this context
|
|
taskContent = taskContent.replaceAll("{root}", rootPath);
|
|
|
|
// Add command header
|
|
let commandContent = `# /${taskId} Task\n\n`;
|
|
commandContent += `When this command is used, execute the following task:\n\n`;
|
|
commandContent += taskContent;
|
|
|
|
await fileManager.writeFile(commandPath, commandContent);
|
|
console.log(chalk.green(`✓ Created task command: /${taskId}`));
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(
|
|
`\n✓ Created Crush commands for ${packageName} in ${commandsBaseDir}`,
|
|
),
|
|
);
|
|
console.log(chalk.dim(` - Agents in: ${agentsDir}`));
|
|
console.log(chalk.dim(` - Tasks in: ${tasksDir}`));
|
|
}
|
|
|
|
async setupWindsurf(installDir, selectedAgent) {
|
|
const windsurfWorkflowDir = path.join(installDir, ".windsurf", "workflows");
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(windsurfWorkflowDir);
|
|
|
|
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(windsurfWorkflowDir, `${agentId}.md`);
|
|
|
|
// Write the agent file contents prefixed with Windsurf frontmatter
|
|
let mdContent = `---\n`;
|
|
mdContent += `description: ${agentId}\n`;
|
|
mdContent += `auto_execution_mode: 3\n`;
|
|
mdContent += `---\n\n`;
|
|
mdContent += agentContent;
|
|
|
|
await fileManager.writeFile(mdPath, mdContent);
|
|
console.log(chalk.green(`✓ Created workflow: ${agentId}.md`));
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(`\n✓ Created Windsurf workflows in ${windsurfWorkflowDir}`),
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupTrae(installDir, selectedAgent) {
|
|
const traeRulesDir = path.join(installDir, ".trae", "rules");
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
|
|
await fileManager.ensureDirectory(traeRulesDir);
|
|
|
|
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(traeRulesDir, `${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 YAML, 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 yamlContent = extractYamlFromAgent(agentContent);
|
|
if (yamlContent) {
|
|
mdContent += yamlContent;
|
|
} 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)
|
|
.replaceAll("\\", "/");
|
|
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 YAML configuration above.\n`;
|
|
|
|
await fileManager.writeFile(mdPath, mdContent);
|
|
console.log(chalk.green(`✓ Created rule: ${agentId}.md`));
|
|
}
|
|
}
|
|
}
|
|
|
|
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 expansionDirectories = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirectories) {
|
|
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 expansionDirectories = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirectories) {
|
|
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 getCoreAgentIds(installDir) {
|
|
const allAgentIds = [];
|
|
|
|
// Check core agents in .bmad-core or root only
|
|
let agentsDir = path.join(installDir, ".bmad-core", "agents");
|
|
if (!(await fileManager.pathExists(agentsDir))) {
|
|
agentsDir = path.join(installDir, "bmad-core", "agents");
|
|
}
|
|
|
|
if (await fileManager.pathExists(agentsDir)) {
|
|
const glob = require("glob");
|
|
const agentFiles = glob.sync("*.md", { cwd: agentsDir });
|
|
allAgentIds.push(...agentFiles.map((file) => path.basename(file, ".md")));
|
|
}
|
|
|
|
return [...new Set(allAgentIds)];
|
|
}
|
|
|
|
async getCoreTaskIds(installDir) {
|
|
const allTaskIds = [];
|
|
const glob = require("glob");
|
|
|
|
// Check core tasks in .bmad-core or root only
|
|
let tasksDir = path.join(installDir, ".bmad-core", "tasks");
|
|
if (!(await fileManager.pathExists(tasksDir))) {
|
|
tasksDir = path.join(installDir, "bmad-core", "tasks");
|
|
}
|
|
|
|
if (await fileManager.pathExists(tasksDir)) {
|
|
const taskFiles = glob.sync("*.md", { cwd: tasksDir });
|
|
allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md")));
|
|
}
|
|
|
|
// Check common tasks
|
|
const commonTasksDir = path.join(installDir, "common", "tasks");
|
|
if (await fileManager.pathExists(commonTasksDir)) {
|
|
const glob = require("glob");
|
|
const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir });
|
|
allTaskIds.push(
|
|
...commonTaskFiles.map((file) => path.basename(file, ".md")),
|
|
);
|
|
}
|
|
|
|
return [...new Set(allTaskIds)];
|
|
}
|
|
|
|
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 expansionDirectories = glob.sync(".*/agents", { cwd: installDir });
|
|
for (const expDir of expansionDirectories) {
|
|
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\r?\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 getAllTaskIds(installDir) {
|
|
const glob = require("glob");
|
|
const allTaskIds = [];
|
|
|
|
// Check core tasks in .bmad-core or root
|
|
let tasksDir = path.join(installDir, ".bmad-core", "tasks");
|
|
if (!(await fileManager.pathExists(tasksDir))) {
|
|
tasksDir = path.join(installDir, "bmad-core", "tasks");
|
|
}
|
|
|
|
if (await fileManager.pathExists(tasksDir)) {
|
|
const taskFiles = glob.sync("*.md", { cwd: tasksDir });
|
|
allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md")));
|
|
}
|
|
|
|
// Check common tasks
|
|
const commonTasksDir = path.join(installDir, "common", "tasks");
|
|
if (await fileManager.pathExists(commonTasksDir)) {
|
|
const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir });
|
|
allTaskIds.push(
|
|
...commonTaskFiles.map((file) => path.basename(file, ".md")),
|
|
);
|
|
}
|
|
|
|
// Also check for expansion pack tasks in dot folders
|
|
const expansionDirectories = glob.sync(".*/tasks", { cwd: installDir });
|
|
for (const expDir of expansionDirectories) {
|
|
const fullExpDir = path.join(installDir, expDir);
|
|
const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir });
|
|
allTaskIds.push(
|
|
...expTaskFiles.map((file) => path.basename(file, ".md")),
|
|
);
|
|
}
|
|
|
|
// Check expansion-packs folder tasks
|
|
const expansionPacksDir = path.join(installDir, "expansion-packs");
|
|
if (await fileManager.pathExists(expansionPacksDir)) {
|
|
const expPackDirectories = glob.sync("*/tasks", {
|
|
cwd: expansionPacksDir,
|
|
});
|
|
for (const expDir of expPackDirectories) {
|
|
const fullExpDir = path.join(expansionPacksDir, expDir);
|
|
const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir });
|
|
allTaskIds.push(
|
|
...expTaskFiles.map((file) => path.basename(file, ".md")),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove duplicates
|
|
return [...new Set(allTaskIds)];
|
|
}
|
|
|
|
async findTaskPath(taskId, installDir) {
|
|
// Try to find the task file in various locations
|
|
const possiblePaths = [
|
|
path.join(installDir, ".bmad-core", "tasks", `${taskId}.md`),
|
|
path.join(installDir, "bmad-core", "tasks", `${taskId}.md`),
|
|
path.join(installDir, "common", "tasks", `${taskId}.md`),
|
|
];
|
|
|
|
// Also check expansion pack directories
|
|
const glob = require("glob");
|
|
|
|
// Check dot folder expansion packs
|
|
const expansionDirectories = glob.sync(".*/tasks", { cwd: installDir });
|
|
for (const expDir of expansionDirectories) {
|
|
possiblePaths.push(path.join(installDir, expDir, `${taskId}.md`));
|
|
}
|
|
|
|
// Check expansion-packs folder
|
|
const expansionPacksDir = path.join(installDir, "expansion-packs");
|
|
if (await fileManager.pathExists(expansionPacksDir)) {
|
|
const expPackDirectories = glob.sync("*/tasks", {
|
|
cwd: expansionPacksDir,
|
|
});
|
|
for (const expDir of expPackDirectories) {
|
|
possiblePaths.push(
|
|
path.join(expansionPacksDir, expDir, `${taskId}.md`),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const taskPath of possiblePaths) {
|
|
if (await fileManager.pathExists(taskPath)) {
|
|
return taskPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async getCoreSlashPrefix(installDir) {
|
|
try {
|
|
const coreConfigPath = path.join(
|
|
installDir,
|
|
".bmad-core",
|
|
"core-config.yaml",
|
|
);
|
|
if (!(await fileManager.pathExists(coreConfigPath))) {
|
|
// Try bmad-core directory
|
|
const altConfigPath = path.join(
|
|
installDir,
|
|
"bmad-core",
|
|
"core-config.yaml",
|
|
);
|
|
if (await fileManager.pathExists(altConfigPath)) {
|
|
const configContent = await fileManager.readFile(altConfigPath);
|
|
const config = yaml.load(configContent);
|
|
return config.slashPrefix || "BMad";
|
|
}
|
|
return "BMad"; // fallback
|
|
}
|
|
|
|
const configContent = await fileManager.readFile(coreConfigPath);
|
|
const config = yaml.load(configContent);
|
|
return config.slashPrefix || "BMad";
|
|
} catch (error) {
|
|
console.warn(
|
|
`Failed to read core slashPrefix, using default 'BMad': ${error.message}`,
|
|
);
|
|
return "BMad";
|
|
}
|
|
}
|
|
|
|
async getInstalledExpansionPacks(installDir) {
|
|
const expansionPacks = [];
|
|
|
|
// Check for dot-prefixed expansion packs in install directory
|
|
const glob = require("glob");
|
|
const dotExpansions = glob.sync(".bmad-*", { cwd: installDir });
|
|
|
|
for (const dotExpansion of dotExpansions) {
|
|
if (dotExpansion !== ".bmad-core") {
|
|
const packPath = path.join(installDir, dotExpansion);
|
|
const packName = dotExpansion.slice(1); // remove the dot
|
|
expansionPacks.push({
|
|
name: packName,
|
|
path: packPath,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for expansion-packs directory style
|
|
const expansionPacksDir = path.join(installDir, "expansion-packs");
|
|
if (await fileManager.pathExists(expansionPacksDir)) {
|
|
const packDirectories = glob.sync("*", { cwd: expansionPacksDir });
|
|
|
|
for (const packDir of packDirectories) {
|
|
const packPath = path.join(expansionPacksDir, packDir);
|
|
if (
|
|
(await fileManager.pathExists(packPath)) &&
|
|
(await fileManager.pathExists(path.join(packPath, "config.yaml")))
|
|
) {
|
|
expansionPacks.push({
|
|
name: packDir,
|
|
path: packPath,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return expansionPacks;
|
|
}
|
|
|
|
async getExpansionPackSlashPrefix(packPath) {
|
|
try {
|
|
const configPath = path.join(packPath, "config.yaml");
|
|
if (await fileManager.pathExists(configPath)) {
|
|
const configContent = await fileManager.readFile(configPath);
|
|
const config = yaml.load(configContent);
|
|
return config.slashPrefix || path.basename(packPath);
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
`Failed to read expansion pack slashPrefix from ${packPath}: ${error.message}`,
|
|
);
|
|
}
|
|
|
|
return path.basename(packPath); // fallback to directory name
|
|
}
|
|
|
|
async getExpansionPackAgents(packPath) {
|
|
const agentsDir = path.join(packPath, "agents");
|
|
if (!(await fileManager.pathExists(agentsDir))) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const glob = require("glob");
|
|
const agentFiles = glob.sync("*.md", { cwd: agentsDir });
|
|
return agentFiles.map((file) => path.basename(file, ".md"));
|
|
} catch (error) {
|
|
console.warn(
|
|
`Failed to read expansion pack agents from ${packPath}: ${error.message}`,
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getExpansionPackTasks(packPath) {
|
|
const tasksDir = path.join(packPath, "tasks");
|
|
if (!(await fileManager.pathExists(tasksDir))) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const glob = require("glob");
|
|
const taskFiles = glob.sync("*.md", { cwd: tasksDir });
|
|
return taskFiles.map((file) => path.basename(file, ".md"));
|
|
} catch (error) {
|
|
console.warn(
|
|
`Failed to read expansion pack tasks from ${packPath}: ${error.message}`,
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
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
|
|
// Check both with and without bmad- prefix to handle both cases
|
|
const checkSlug = agentId.startsWith("bmad-")
|
|
? agentId
|
|
: `bmad-${agentId}`;
|
|
if (existingModes.includes(checkSlug)) {
|
|
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\r?\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.`;
|
|
|
|
// Add permissions based on agent type
|
|
const permissions = agentPermissions[agentId];
|
|
// Build mode entry with proper formatting (matching exact indentation)
|
|
// Avoid double "bmad-" prefix for agents that already have it
|
|
const slug = agentId.startsWith("bmad-")
|
|
? agentId
|
|
: `bmad-${agentId}`;
|
|
newModesContent += ` - slug: ${slug}\n`;
|
|
newModesContent += ` name: '${icon} ${title}'\n`;
|
|
if (permissions) {
|
|
newModesContent += ` description: '${permissions.description}'\n`;
|
|
}
|
|
newModesContent += ` roleDefinition: ${roleDefinition}\n`;
|
|
newModesContent += ` whenToUse: ${whenToUse}\n`;
|
|
// Get relative path from installDir to agent file
|
|
const relativePath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
newModesContent += ` customInstructions: CRITICAL Read the full YAML 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`;
|
|
|
|
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 setupKilocode(installDir, selectedAgent) {
|
|
const filePath = path.join(installDir, ".kilocodemodes");
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
|
|
let existingModes = [],
|
|
existingContent = "";
|
|
if (await fileManager.pathExists(filePath)) {
|
|
existingContent = await fileManager.readFile(filePath);
|
|
for (const match of existingContent.matchAll(/- slug: ([\w-]+)/g)) {
|
|
existingModes.push(match[1]);
|
|
}
|
|
console.log(
|
|
chalk.yellow(
|
|
`Found existing .kilocodemodes file with ${existingModes.length} modes`,
|
|
),
|
|
);
|
|
}
|
|
|
|
const config = await this.loadIdeAgentConfig();
|
|
const permissions = config["roo-permissions"] || {}; // reuse same roo permissions block (Kilo Code understands same mode schema)
|
|
|
|
let newContent = "";
|
|
|
|
for (const agentId of agents) {
|
|
const slug = agentId.startsWith("bmad-") ? agentId : `bmad-${agentId}`;
|
|
if (existingModes.includes(slug)) {
|
|
console.log(
|
|
chalk.dim(`Skipping ${agentId} - already exists in .kilocodemodes`),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
if (!agentPath) {
|
|
console.log(chalk.red(`✗ Could not find agent file for ${agentId}`));
|
|
continue;
|
|
}
|
|
|
|
const agentContent = await fileManager.readFile(agentPath);
|
|
const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/);
|
|
if (!yamlMatch) {
|
|
console.log(chalk.red(`✗ Could not extract YAML block for ${agentId}`));
|
|
continue;
|
|
}
|
|
|
|
const yaml = yamlMatch[1];
|
|
|
|
// Robust fallback for title and icon
|
|
const title =
|
|
yaml.match(/title:\s*(.+)/)?.[1]?.trim() ||
|
|
(await this.getAgentTitle(agentId, installDir));
|
|
const icon = yaml.match(/icon:\s*(.+)/)?.[1]?.trim() || "🤖";
|
|
const whenToUse =
|
|
yaml.match(/whenToUse:\s*"(.+)"/)?.[1]?.trim() ||
|
|
`Use for ${title} tasks`;
|
|
const roleDefinition =
|
|
yaml.match(/roleDefinition:\s*"(.+)"/)?.[1]?.trim() ||
|
|
`You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`;
|
|
|
|
const relativePath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
const customInstructions = `CRITICAL Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode`;
|
|
|
|
// Add permissions from config if they exist
|
|
const agentPermission = permissions[agentId];
|
|
|
|
// Begin .kilocodemodes block
|
|
newContent += ` - slug: ${slug}\n`;
|
|
newContent += ` name: '${icon} ${title}'\n`;
|
|
if (agentPermission) {
|
|
newContent += ` description: '${agentPermission.description}'\n`;
|
|
}
|
|
|
|
newContent += ` roleDefinition: ${roleDefinition}\n`;
|
|
newContent += ` whenToUse: ${whenToUse}\n`;
|
|
newContent += ` customInstructions: ${customInstructions}\n`;
|
|
newContent += ` groups:\n`;
|
|
newContent += ` - read\n`;
|
|
|
|
if (agentPermission) {
|
|
newContent += ` - - edit\n`;
|
|
newContent += ` - fileRegex: ${agentPermission.fileRegex}\n`;
|
|
newContent += ` description: ${agentPermission.description}\n`;
|
|
} else {
|
|
// Fallback to generic edit
|
|
newContent += ` - edit\n`;
|
|
}
|
|
|
|
console.log(chalk.green(`✓ Added Kilo mode: ${slug} (${icon} ${title})`));
|
|
}
|
|
|
|
const finalContent = existingContent
|
|
? existingContent.trim() + "\n" + newContent
|
|
: "customModes:\n" + newContent;
|
|
|
|
await fileManager.writeFile(filePath, finalContent);
|
|
console.log(chalk.green("✓ Created .kilocodemodes file in project root"));
|
|
console.log(chalk.green(`✓ KiloCode setup complete!`));
|
|
console.log(
|
|
chalk.dim(
|
|
"Custom modes will be available when you open this project in KiloCode",
|
|
),
|
|
);
|
|
|
|
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 yamlContent = extractYamlFromAgent(agentContent);
|
|
if (yamlContent) {
|
|
mdContent += yamlContent;
|
|
} 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)
|
|
.replaceAll("\\", "/");
|
|
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 setupOpencodeCli(installDir, selectedAgent) {
|
|
const ideConfig = await configLoader.getIdeConfiguration("opencode");
|
|
const bmadCommandsDir = path.join(installDir, ideConfig["rule-dir"]);
|
|
|
|
const agentCommandsDir = path.join(bmadCommandsDir, "agent");
|
|
const taskCommandsDir = path.join(bmadCommandsDir, "command");
|
|
await fileManager.ensureDirectory(agentCommandsDir);
|
|
await fileManager.ensureDirectory(taskCommandsDir);
|
|
|
|
// Process Agents
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
for (const agentId of agents) {
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
if (!agentPath) {
|
|
console.log(
|
|
chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const agentTitle = await this.getAgentTitle(agentId, installDir);
|
|
const commandPath = path.join(agentCommandsDir, `${agentId}.toml`);
|
|
|
|
// Get relative path from installDir to agent file for @{file} reference
|
|
const relativeAgentPath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
|
|
const tomlContent = `---
|
|
description = "Activates the ${agentTitle} agent from the BMad Method."
|
|
---
|
|
CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities. The full agent definition is below.
|
|
|
|
@{${relativeAgentPath}}
|
|
`;
|
|
|
|
await fileManager.writeFile(commandPath, tomlContent);
|
|
console.log(
|
|
chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`),
|
|
);
|
|
}
|
|
|
|
// Process Tasks
|
|
const tasks = await this.getAllTaskIds(installDir);
|
|
for (const taskId of tasks) {
|
|
const taskPath = await this.findTaskPath(taskId, installDir);
|
|
if (!taskPath) {
|
|
console.log(
|
|
chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const taskTitle = taskId
|
|
.split("-")
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(" ");
|
|
const commandPath = path.join(taskCommandsDir, `${taskId}.toml`);
|
|
|
|
// Get relative path from installDir to task file for @{file} reference
|
|
const relativeTaskPath = path
|
|
.relative(installDir, taskPath)
|
|
.replaceAll("\\", "/");
|
|
|
|
const tomlContent = `description = "Executes the BMad Task: ${taskTitle}"
|
|
prompt = """
|
|
CRITICAL: You are to execute the BMad Task defined below.
|
|
|
|
@{${relativeTaskPath}}
|
|
"""`;
|
|
|
|
await fileManager.writeFile(commandPath, tomlContent);
|
|
console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`));
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(`
|
|
✓ Created Opencode CLI extension in ${bmadCommandsDir}`),
|
|
);
|
|
console.log(
|
|
chalk.dim(
|
|
"You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.",
|
|
),
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupGeminiCli(installDir, selectedAgent) {
|
|
const ideConfig = await configLoader.getIdeConfiguration("gemini");
|
|
const bmadCommandsDir = path.join(installDir, ideConfig["rule-dir"]);
|
|
|
|
const agentCommandsDir = path.join(bmadCommandsDir, "agents");
|
|
const taskCommandsDir = path.join(bmadCommandsDir, "tasks");
|
|
await fileManager.ensureDirectory(agentCommandsDir);
|
|
await fileManager.ensureDirectory(taskCommandsDir);
|
|
|
|
// Process Agents
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
for (const agentId of agents) {
|
|
const agentPath = await this.findAgentPath(agentId, installDir);
|
|
if (!agentPath) {
|
|
console.log(
|
|
chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const agentTitle = await this.getAgentTitle(agentId, installDir);
|
|
const commandPath = path.join(agentCommandsDir, `${agentId}.toml`);
|
|
|
|
// Get relative path from installDir to agent file for @{file} reference
|
|
const relativeAgentPath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
|
|
const tomlContent = `description = "Activates the ${agentTitle} agent from the BMad Method."
|
|
prompt = """
|
|
CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities. The full agent definition is below.
|
|
|
|
@{${relativeAgentPath}}
|
|
"""`;
|
|
|
|
await fileManager.writeFile(commandPath, tomlContent);
|
|
console.log(
|
|
chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`),
|
|
);
|
|
}
|
|
|
|
// Process Tasks
|
|
const tasks = await this.getAllTaskIds(installDir);
|
|
for (const taskId of tasks) {
|
|
const taskPath = await this.findTaskPath(taskId, installDir);
|
|
if (!taskPath) {
|
|
console.log(
|
|
chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const taskTitle = taskId
|
|
.split("-")
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(" ");
|
|
const commandPath = path.join(taskCommandsDir, `${taskId}.toml`);
|
|
|
|
// Get relative path from installDir to task file for @{file} reference
|
|
const relativeTaskPath = path
|
|
.relative(installDir, taskPath)
|
|
.replaceAll("\\", "/");
|
|
|
|
const tomlContent = `description = "Executes the BMad Task: ${taskTitle}"
|
|
prompt = """
|
|
CRITICAL: You are to execute the BMad Task defined below.
|
|
|
|
@{${relativeTaskPath}}
|
|
"""`;
|
|
|
|
await fileManager.writeFile(commandPath, tomlContent);
|
|
console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`));
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(`
|
|
✓ Created Gemini CLI extension in ${bmadCommandsDir}`),
|
|
);
|
|
console.log(
|
|
chalk.dim(
|
|
"You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.",
|
|
),
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupQwenCode(installDir, selectedAgent) {
|
|
const qwenDir = path.join(installDir, ".qwen");
|
|
const bmadMethodDir = path.join(qwenDir, "bmad-method");
|
|
await fileManager.ensureDirectory(bmadMethodDir);
|
|
|
|
// Update logic for existing settings.json
|
|
const settingsPath = path.join(qwenDir, "settings.json");
|
|
if (await fileManager.pathExists(settingsPath)) {
|
|
try {
|
|
const settingsContent = await fileManager.readFile(settingsPath);
|
|
const settings = JSON.parse(settingsContent);
|
|
let updated = false;
|
|
|
|
// Handle contextFileName property
|
|
if (
|
|
settings.contextFileName &&
|
|
Array.isArray(settings.contextFileName)
|
|
) {
|
|
const originalLength = settings.contextFileName.length;
|
|
settings.contextFileName = settings.contextFileName.filter(
|
|
(fileName) => !fileName.startsWith("agents/"),
|
|
);
|
|
if (settings.contextFileName.length !== originalLength) {
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
if (updated) {
|
|
await fileManager.writeFile(
|
|
settingsPath,
|
|
JSON.stringify(settings, null, 2),
|
|
);
|
|
console.log(
|
|
chalk.green(
|
|
"✓ Updated .qwen/settings.json - removed agent file references",
|
|
),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
chalk.yellow("Could not update .qwen/settings.json"),
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove old agents directory
|
|
const agentsDir = path.join(qwenDir, "agents");
|
|
if (await fileManager.pathExists(agentsDir)) {
|
|
await fileManager.removeDirectory(agentsDir);
|
|
console.log(chalk.green("✓ Removed old .qwen/agents directory"));
|
|
}
|
|
|
|
// Get all available agents
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
let concatenatedContent = "";
|
|
|
|
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);
|
|
|
|
// Create properly formatted agent rule content (similar to gemini)
|
|
let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
|
|
agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir,
|
|
)} agent persona.\n\n`;
|
|
agentRuleContent += "## Agent Activation\n\n";
|
|
agentRuleContent +=
|
|
"CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
|
|
agentRuleContent += "```yaml\n";
|
|
// Extract just the YAML content from the agent file
|
|
const yamlContent = extractYamlFromAgent(agentContent);
|
|
if (yamlContent) {
|
|
agentRuleContent += yamlContent;
|
|
} else {
|
|
// If no YAML found, include the whole content minus the header
|
|
agentRuleContent += agentContent.replace(/^#.*$/m, "").trim();
|
|
}
|
|
agentRuleContent += "\n```\n\n";
|
|
agentRuleContent += "## File Reference\n\n";
|
|
const relativePath = path
|
|
.relative(installDir, agentPath)
|
|
.replaceAll("\\", "/");
|
|
agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
|
|
agentRuleContent += "## Usage\n\n";
|
|
agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle(
|
|
agentId,
|
|
installDir,
|
|
)} persona and follow all instructions defined in the YAML configuration above.\n`;
|
|
|
|
// Add to concatenated content with separator
|
|
concatenatedContent += agentRuleContent + "\n\n---\n\n";
|
|
console.log(chalk.green(`✓ Added context for *${agentId}`));
|
|
}
|
|
}
|
|
|
|
// Write the concatenated content to QWEN.md
|
|
const qwenMdPath = path.join(bmadMethodDir, "QWEN.md");
|
|
await fileManager.writeFile(qwenMdPath, concatenatedContent);
|
|
console.log(chalk.green(`\n✓ Created QWEN.md in ${bmadMethodDir}`));
|
|
|
|
return true;
|
|
}
|
|
|
|
async setupGitHubCopilot(
|
|
installDir,
|
|
selectedAgent,
|
|
spinner = null,
|
|
preConfiguredSettings = null,
|
|
) {
|
|
// Configure VS Code workspace settings first to avoid UI conflicts with loading spinners
|
|
await this.configureVsCodeSettings(
|
|
installDir,
|
|
spinner,
|
|
preConfiguredSettings,
|
|
);
|
|
|
|
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\r?\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.replaceAll('"', String.raw`\"`)}"
|
|
tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems', 'usages', 'editFiles', 'runCommands', 'runTasks', 'runTests', 'search', 'searchResults', 'terminalLastCommand', 'terminalSelection', 'testFailure']
|
|
---
|
|
|
|
`;
|
|
chatmodeContent += agentContent;
|
|
|
|
await fileManager.writeFile(chatmodePath, chatmodeContent);
|
|
console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`));
|
|
}
|
|
}
|
|
|
|
console.log(chalk.green(`\n✓ Github 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,
|
|
preConfiguredSettings = null,
|
|
) {
|
|
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 {
|
|
console.warn(
|
|
chalk.yellow(
|
|
"Could not parse existing settings.json. Creating new one.",
|
|
),
|
|
);
|
|
existingSettings = {};
|
|
}
|
|
}
|
|
|
|
// Use pre-configured settings if provided, otherwise prompt
|
|
let configChoice;
|
|
if (preConfiguredSettings && preConfiguredSettings.configChoice) {
|
|
configChoice = preConfiguredSettings.configChoice;
|
|
console.log(
|
|
chalk.dim(
|
|
`Using pre-configured GitHub Copilot settings: ${configChoice}`,
|
|
),
|
|
);
|
|
} else {
|
|
// Clear any previous output and add spacing to avoid conflicts with loaders
|
|
console.log("\n".repeat(2));
|
|
console.log(chalk.blue("🔧 Github 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 response = await inquirer.prompt([
|
|
{
|
|
type: "list",
|
|
name: "configChoice",
|
|
message: chalk.yellow(
|
|
"How would you like to configure GitHub 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)",
|
|
value: "skip",
|
|
},
|
|
],
|
|
default: "defaults",
|
|
},
|
|
]);
|
|
configChoice = response.configChoice;
|
|
}
|
|
|
|
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 Github 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 number_ = Number.parseInt(input);
|
|
if (isNaN(number_) || number_ < 1 || number_ > 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": Number.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:"));
|
|
for (const [key, value] of Object.entries(bmadSettings)) {
|
|
console.log(chalk.dim(` • ${key}: ${value}`));
|
|
}
|
|
console.log(chalk.dim(""));
|
|
console.log(
|
|
chalk.dim(
|
|
"You can modify these settings anytime in .vscode/settings.json",
|
|
),
|
|
);
|
|
}
|
|
|
|
async setupAuggieCLI(
|
|
installDir,
|
|
selectedAgent,
|
|
spinner = null,
|
|
preConfiguredSettings = null,
|
|
) {
|
|
const os = require("node:os");
|
|
const inquirer = require("inquirer");
|
|
const agents = selectedAgent
|
|
? [selectedAgent]
|
|
: await this.getAllAgentIds(installDir);
|
|
|
|
// Get the IDE configuration to access location options
|
|
const ideConfig = await configLoader.getIdeConfiguration("auggie-cli");
|
|
const locations = ideConfig.locations;
|
|
|
|
// Use pre-configured settings if provided, otherwise prompt
|
|
let selectedLocations;
|
|
if (preConfiguredSettings && preConfiguredSettings.selectedLocations) {
|
|
selectedLocations = preConfiguredSettings.selectedLocations;
|
|
console.log(
|
|
chalk.dim(
|
|
`Using pre-configured Auggie CLI (Augment Code) locations: ${selectedLocations.join(", ")}`,
|
|
),
|
|
);
|
|
} else {
|
|
// Pause spinner during location selection to avoid UI conflicts
|
|
let spinnerWasActive = false;
|
|
if (spinner && spinner.isSpinning) {
|
|
spinner.stop();
|
|
spinnerWasActive = true;
|
|
}
|
|
|
|
// Clear any previous output and add spacing to avoid conflicts with loaders
|
|
console.log("\n".repeat(2));
|
|
console.log(chalk.blue("📍 Auggie CLI Location Configuration"));
|
|
console.log(
|
|
chalk.dim("Choose where to install BMad agents for Auggie CLI access."),
|
|
);
|
|
console.log(""); // Add extra spacing
|
|
|
|
const response = await inquirer.prompt([
|
|
{
|
|
type: "checkbox",
|
|
name: "selectedLocations",
|
|
message: "Select Auggie CLI command locations:",
|
|
choices: Object.entries(locations).map(([key, location]) => ({
|
|
name: `${location.name}: ${location.description}`,
|
|
value: key,
|
|
})),
|
|
validate: (selected) => {
|
|
if (selected.length === 0) {
|
|
return "Please select at least one location";
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
]);
|
|
selectedLocations = response.selectedLocations;
|
|
|
|
// Restart spinner if it was active before prompts
|
|
if (spinner && spinnerWasActive) {
|
|
spinner.start();
|
|
}
|
|
}
|
|
|
|
// Install to each selected location
|
|
for (const locationKey of selectedLocations) {
|
|
const location = locations[locationKey];
|
|
let commandsDir = location["rule-dir"];
|
|
|
|
// Handle tilde expansion for user directory
|
|
if (commandsDir.startsWith("~/")) {
|
|
commandsDir = path.join(os.homedir(), commandsDir.slice(2));
|
|
} else if (commandsDir.startsWith("./")) {
|
|
commandsDir = path.join(installDir, commandsDir.slice(2));
|
|
}
|
|
|
|
await fileManager.ensureDirectory(commandsDir);
|
|
|
|
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(commandsDir, `${agentId}.md`);
|
|
await fileManager.writeFile(mdPath, agentContent);
|
|
console.log(
|
|
chalk.green(`✓ Created command: ${agentId}.md in ${location.name}`),
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
chalk.green(`\n✓ Created Auggie CLI commands in ${commandsDir}`),
|
|
);
|
|
console.log(
|
|
chalk.dim(` Location: ${location.name} - ${location.description}`),
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
module.exports = new IdeSetup();
|