From a72b790f3be6c77355511ace2d63e6bec4d751f1 Mon Sep 17 00:00:00 2001 From: Houston Zhang <161981770+Djanghao@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:24:09 -0400 Subject: [PATCH] feat: add qwen-code ide support to bmad installer. (#392) Co-authored-by: Djanghao --- tools/installer/bin/bmad.js | 3 +- tools/installer/config/install.config.yaml | 14 ++- tools/installer/lib/ide-setup.js | 102 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/tools/installer/bin/bmad.js b/tools/installer/bin/bmad.js index 3a14fd95..ff623239 100755 --- a/tools/installer/bin/bmad.js +++ b/tools/installer/bin/bmad.js @@ -41,7 +41,7 @@ program .option('-f, --full', 'Install complete BMad Method') .option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)') .option('-d, --directory ', 'Installation directory') - .option('-i, --ide ', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, github-copilot, other)') + .option('-i, --ide ', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, other)') .option('-e, --expansion-packs ', 'Install specific expansion packs (can specify multiple)') .action(async (options) => { try { @@ -314,6 +314,7 @@ async function promptInstallation() { { name: 'Kilo Code', value: 'kilo' }, { name: 'Cline', value: 'cline' }, { name: 'Gemini CLI', value: 'gemini' }, + { name: 'Qwen Code', value: 'qwen-code' }, { name: 'Github Copilot', value: 'github-copilot' } ] } diff --git a/tools/installer/config/install.config.yaml b/tools/installer/config/install.config.yaml index c74387ae..96e86aea 100644 --- a/tools/installer/config/install.config.yaml +++ b/tools/installer/config/install.config.yaml @@ -98,4 +98,16 @@ ide-configurations: # To use BMAD agents in Kilo Code: # 1. Open the mode selector in VSCode # 2. Select a bmad-{agent} mode (e.g. "bmad-dev") - # 3. The AI adopts that agent's persona and capabilities \ No newline at end of file + # 3. The AI adopts that agent's persona and capabilities + + qwen-code: + name: Qwen Code + rule-dir: .qwen/bmad-method/ + format: single-file + command-suffix: .md + instructions: | + # To use BMad agents with Qwen Code: + # 1. The installer creates a .qwen/bmad-method/ directory in your project. + # 2. It concatenates all agent files into a single QWEN.md file. + # 3. Simply mention the agent in your prompt (e.g., "As *dev, ..."). + # 4. The Qwen Code CLI will automatically have the context for that agent. \ No newline at end of file diff --git a/tools/installer/lib/ide-setup.js b/tools/installer/lib/ide-setup.js index f7f0bbfd..4768a931 100644 --- a/tools/installer/lib/ide-setup.js +++ b/tools/installer/lib/ide-setup.js @@ -59,6 +59,8 @@ class IdeSetup extends BaseIdeSetup { return this.setupGeminiCli(installDir, selectedAgent); case "github-copilot": return this.setupGitHubCopilot(installDir, selectedAgent, spinner, preConfiguredSettings); + case "qwen-code": + return this.setupQwenCode(installDir, selectedAgent); default: console.log(chalk.yellow(`\nIDE ${ide} not yet supported`)); return false; @@ -977,6 +979,106 @@ class IdeSetup extends BaseIdeSetup { 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).replace(/\\/g, '/'); + 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);