From 60475ac6f8ce049dbbe9e50eb35a91483184b691 Mon Sep 17 00:00:00 2001 From: Tiki <778563781@qq.com> Date: Mon, 20 Oct 2025 21:34:42 +0800 Subject: [PATCH] feat(tools/cli): Refactor Qwen IDE configuration logic to support modular command structure (#762) - Unify BMad directory name from 'BMad' to lowercase 'bmad' - Use shared utility functions [getAgentsFromBmad] and [getTasksFromBmad] to fetch agents and tasks - Create independent subdirectory structures (agents, tasks) for each module - Update file writing paths to store TOML files by module classification - Remove legacy QWEN.md merged documentation generation logic - Add TOML metadata header support (not available in previous versions) - Clean up old version configuration directories (including uppercase BMad and bmad-method) --- tools/cli/installers/lib/ide/qwen.js | 250 +++++++++------------------ 1 file changed, 82 insertions(+), 168 deletions(-) diff --git a/tools/cli/installers/lib/ide/qwen.js b/tools/cli/installers/lib/ide/qwen.js index 3daa7906..f8a3b0d0 100644 --- a/tools/cli/installers/lib/ide/qwen.js +++ b/tools/cli/installers/lib/ide/qwen.js @@ -1,6 +1,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); +const { getAgentsFromBmad, getTasksFromBmad } = require('./shared/bmad-artifacts'); /** * Qwen Code setup handler @@ -11,7 +12,7 @@ class QwenSetup extends BaseIdeSetup { super('qwen', 'Qwen Code'); this.configDir = '.qwen'; this.commandsDir = 'commands'; - this.bmadDir = 'BMad'; + this.bmadDir = 'bmad'; } /** @@ -27,11 +28,8 @@ class QwenSetup extends BaseIdeSetup { const qwenDir = path.join(projectDir, this.configDir); const commandsDir = path.join(qwenDir, this.commandsDir); const bmadCommandsDir = path.join(commandsDir, this.bmadDir); - const agentsDir = path.join(bmadCommandsDir, 'agents'); - const tasksDir = path.join(bmadCommandsDir, 'tasks'); - await this.ensureDir(agentsDir); - await this.ensureDir(tasksDir); + await this.ensureDir(bmadCommandsDir); // Update existing settings.json if present await this.updateSettings(qwenDir); @@ -40,68 +38,55 @@ class QwenSetup extends BaseIdeSetup { await this.cleanupOldConfig(qwenDir); // Get agents and tasks - const agents = await this.getAgents(bmadDir); - const tasks = await this.getTasks(bmadDir); + const agents = await getAgentsFromBmad(bmadDir, options.selectedModules || []); + const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); + + // Create directories for each module (including standalone) + const modules = new Set(); + for (const item of [...agents, ...tasks]) modules.add(item.module); + + for (const module of modules) { + await this.ensureDir(path.join(bmadCommandsDir, module)); + await this.ensureDir(path.join(bmadCommandsDir, module, 'agents')); + await this.ensureDir(path.join(bmadCommandsDir, module, 'tasks')); + } // Create TOML files for each agent let agentCount = 0; for (const agent of agents) { - const content = await this.readFile(agent.path); - const tomlContent = this.createAgentToml(agent, content, projectDir); - const tomlPath = path.join(agentsDir, `${agent.name}.toml`); - await this.writeFile(tomlPath, tomlContent); + const content = await this.readAndProcess(agent.path, { + module: agent.module, + name: agent.name, + }); + + const targetPath = path.join(bmadCommandsDir, agent.module, 'agents', `${agent.name}.toml`); + + await this.writeFile(targetPath, content); + agentCount++; - console.log(chalk.green(` ✓ Added agent: /BMad:agents:${agent.name}`)); + console.log(chalk.green(` ✓ Added agent: /bmad:${agent.module}:agents:${agent.name}`)); } // Create TOML files for each task let taskCount = 0; for (const task of tasks) { - const content = await this.readFile(task.path); - const tomlContent = this.createTaskToml(task, content, projectDir); - const tomlPath = path.join(tasksDir, `${task.name}.toml`); - await this.writeFile(tomlPath, tomlContent); + const content = await this.readAndProcess(task.path, { + module: task.module, + name: task.name, + }); + + const targetPath = path.join(bmadCommandsDir, task.module, 'agents', `${agent.name}.toml`); + + await this.writeFile(targetPath, content); + taskCount++; - console.log(chalk.green(` ✓ Added task: /BMad:tasks:${task.name}`)); + console.log(chalk.green(` ✓ Added task: /bmad:${task.module}:tasks:${task.name}`)); } - // Create concatenated QWEN.md for reference - let concatenatedContent = `# BMAD Method - Qwen Code Configuration - -This file contains all BMAD agents and tasks configured for use with Qwen Code. - -## Agents -Agents can be activated using: \`/BMad:agents:\` - -## Tasks -Tasks can be executed using: \`/BMad:tasks:\` - ---- - -`; - - for (const agent of agents) { - const content = await this.readFile(agent.path); - const agentSection = this.createAgentSection(agent, content, projectDir); - concatenatedContent += agentSection; - concatenatedContent += '\n\n---\n\n'; - } - - for (const task of tasks) { - const content = await this.readFile(task.path); - const taskSection = this.createTaskSection(task, content, projectDir); - concatenatedContent += taskSection; - concatenatedContent += '\n\n---\n\n'; - } - - const qwenMdPath = path.join(bmadCommandsDir, 'QWEN.md'); - await this.writeFile(qwenMdPath, concatenatedContent); - console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${agentCount} agents configured`)); console.log(chalk.dim(` - ${taskCount} tasks configured`)); - console.log(chalk.dim(` - Agents activated with: /BMad:agents:`)); - console.log(chalk.dim(` - Tasks activated with: /BMad:tasks:`)); + console.log(chalk.dim(` - Commands directory: ${path.relative(projectDir, bmadCommandsDir)}`)); return { success: true, @@ -152,6 +137,7 @@ Tasks can be executed using: \`/BMad:tasks:\` const fs = require('fs-extra'); const agentsDir = path.join(qwenDir, 'agents'); const bmadMethodDir = path.join(qwenDir, 'bmad-method'); + const bmadDir = path.join(qwenDir, 'bmadDir'); if (await fs.pathExists(agentsDir)) { await fs.remove(agentsDir); @@ -162,135 +148,57 @@ Tasks can be executed using: \`/BMad:tasks:\` await fs.remove(bmadMethodDir); console.log(chalk.green(' ✓ Removed old bmad-method directory')); } + + if (await fs.pathExists(bmadDir)) { + await fs.remove(bmadDir); + console.log(chalk.green(' ✓ Removed old BMad directory')); + } } /** - * Create TOML file for agent + * Read and process file content */ - createAgentToml(agent, content, projectDir) { - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/); - const yamlContent = yamlMatch ? yamlMatch[1] : content; - const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/'); - - return `# ${title} Agent -name = "${agent.name}" -description = """ -${title} agent from BMAD ${agent.module.toUpperCase()} module. - -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: - -\`\`\`yaml -${yamlContent} -\`\`\` - -File: ${relativePath} -"""`; + async readAndProcess(filePath, metadata) { + const fs = require('fs-extra'); + const content = await fs.readFile(filePath, 'utf8'); + return this.processContent(content, metadata); } /** - * Create TOML file for task + * Override processContent to add TOML metadata header for Qwen + * @param {string} content - File content + * @param {Object} metadata - File metadata + * @returns {string} Processed content with Qwen template */ - createTaskToml(task, content, projectDir) { - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(task.name); - const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/); - const yamlContent = yamlMatch ? yamlMatch[1] : content; - const relativePath = path.relative(projectDir, task.path).replaceAll('\\', '/'); + processContent(content, metadata = {}) { + // First apply base processing (includes activation injection for agents) + let prompt = super.processContent(content, metadata); - return `# ${title} Task -name = "${task.name}" -description = """ -${title} task from BMAD ${task.module.toUpperCase()} module. + // Determine the type and description based on content + const isAgent = content.includes('([^<]+)<\/name>/); + const taskName = nameMatch ? nameMatch[1] : metadata.name; + description = `BMAD ${metadata.module.toUpperCase()} Task: ${taskName}`; + } else { + description = `BMAD ${metadata.module.toUpperCase()}: ${metadata.name}`; + } -File: ${relativePath} -"""`; - } - - /** - * Create agent section for concatenated file - */ - createAgentSection(agent, content, projectDir) { - // Extract metadata - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(agent.name); - - // Extract YAML content - const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/); - const yamlContent = yamlMatch ? yamlMatch[1] : content; - - // Get relative path - const relativePath = path.relative(projectDir, agent.path).replaceAll('\\', '/'); - - let section = `# ${agent.name.toUpperCase()} Agent Rule - -This rule is triggered when the user types \`/BMad:agents:${agent.name}\` and activates the ${title} agent persona. - -## Agent Activation - -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: - -\`\`\`yaml -${yamlContent} -\`\`\` - -## File Reference - -The complete agent definition is available in [${relativePath}](${relativePath}). - -## Usage - -When the user types \`/BMad:agents:${agent.name}\`, activate this ${title} persona and follow all instructions defined in the YAML configuration above. - -## Module - -Part of the BMAD ${agent.module.toUpperCase()} module.`; - - return section; - } - - /** - * Create task section for concatenated file - */ - createTaskSection(task, content, projectDir) { - const titleMatch = content.match(/title="([^"]+)"/); - const title = titleMatch ? titleMatch[1] : this.formatTitle(task.name); - const yamlMatch = content.match(/```ya?ml\r?\n([\s\S]*?)```/); - const yamlContent = yamlMatch ? yamlMatch[1] : content; - const relativePath = path.relative(projectDir, task.path).replaceAll('\\', '/'); - - let section = `# ${task.name.toUpperCase()} Task - -This task is triggered when the user types \`/BMad:tasks:${task.name}\` and executes the ${title} task. - -## Task Execution - -Execute this task by following the instructions in the YAML configuration: - -\`\`\`yaml -${yamlContent} -\`\`\` - -## File Reference - -The complete task definition is available in [${relativePath}](${relativePath}). - -## Usage - -When the user types \`/BMad:tasks:${task.name}\`, execute this ${title} task and follow all instructions defined in the YAML configuration above. - -## Module - -Part of the BMAD ${task.module.toUpperCase()} module.`; - - return section; + return `description = "${description}" +prompt = """ +${prompt} +""" +`; } /** @@ -310,6 +218,7 @@ Part of the BMAD ${task.module.toUpperCase()} module.`; const fs = require('fs-extra'); const bmadCommandsDir = path.join(projectDir, this.configDir, this.commandsDir, this.bmadDir); const oldBmadMethodDir = path.join(projectDir, this.configDir, 'bmad-method'); + const oldBMadDir = path.join(projectDir, this.configDir, 'BMad'); if (await fs.pathExists(bmadCommandsDir)) { await fs.remove(bmadCommandsDir); @@ -320,6 +229,11 @@ Part of the BMAD ${task.module.toUpperCase()} module.`; await fs.remove(oldBmadMethodDir); console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); } + + if (await fs.pathExists(oldBMadDir)) { + await fs.remove(oldBMadDir); + console.log(chalk.dim(`Removed old BMAD configuration from Qwen Code`)); + } } }