From 41f9cc19134521f944f4b1d320fa7a661859ec6d Mon Sep 17 00:00:00 2001 From: Philip Louw <35452556+igknot@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:17:02 +0200 Subject: [PATCH] feat: add kiro-cli installer with BMad Core compliance (#993) - Implement KiroCliSetup class extending BaseIdeSetup - Generate 21 agents from YAML sources with JSON configs and markdown prompts - Add runtime resource loading and numbered menu formatting - Include BMad Core validation for required agent fields - Fix agent naming conventions to prevent double prefixes - Add .kiro/ directory to gitignore Follows BMad Method standards for IDE installer integration. Co-authored-by: Brian --- .gitignore | 3 +- tools/cli/installers/lib/ide/kiro-cli.js | 302 +++++++++++++++++++++++ 2 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 tools/cli/installers/lib/ide/kiro-cli.js diff --git a/.gitignore b/.gitignore index cef2ce1a..01026ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ z*/ .codex .github/chatmodes .agent -.agentvibes/ \ No newline at end of file +.agentvibes/ +.kiro/ \ No newline at end of file diff --git a/tools/cli/installers/lib/ide/kiro-cli.js b/tools/cli/installers/lib/ide/kiro-cli.js new file mode 100644 index 00000000..5ea9bc5f --- /dev/null +++ b/tools/cli/installers/lib/ide/kiro-cli.js @@ -0,0 +1,302 @@ +const path = require('node:path'); +const { BaseIdeSetup } = require('./_base-ide'); +const chalk = require('chalk'); +const fs = require('fs-extra'); +const yaml = require('js-yaml'); + +/** + * Kiro CLI setup handler for BMad Method + */ +class KiroCliSetup extends BaseIdeSetup { + constructor() { + super('kiro-cli', 'Kiro CLI', false); + this.configDir = '.kiro'; + this.agentsDir = 'agents'; + } + + /** + * Cleanup old BMAD installation before reinstalling + * @param {string} projectDir - Project directory + */ + async cleanup(projectDir) { + const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir); + + if (await fs.pathExists(bmadAgentsDir)) { + // Remove existing BMad agents + const files = await fs.readdir(bmadAgentsDir); + for (const file of files) { + if (file.startsWith('bmad-') || file.includes('bmad')) { + await fs.remove(path.join(bmadAgentsDir, file)); + } + } + console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`)); + } + } + + /** + * Setup Kiro CLI configuration with BMad agents + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, bmadDir, options = {}) { + console.log(chalk.cyan(`Setting up ${this.name}...`)); + + await this.cleanup(projectDir); + + const kiroDir = path.join(projectDir, this.configDir); + const agentsDir = path.join(kiroDir, this.agentsDir); + + await this.ensureDir(agentsDir); + + // Create BMad agents from source YAML files + await this.createBmadAgentsFromSource(agentsDir, projectDir); + + console.log(chalk.green(`✓ ${this.name} configured with BMad agents`)); + } + + /** + * Create BMad agent definitions from source YAML files + * @param {string} agentsDir - Agents directory + * @param {string} projectDir - Project directory + */ + async createBmadAgentsFromSource(agentsDir, projectDir) { + const sourceDir = path.join(__dirname, '../../../../../src/modules'); + + // Find all agent YAML files + const agentFiles = await this.findAgentFiles(sourceDir); + + for (const agentFile of agentFiles) { + try { + await this.processAgentFile(agentFile, agentsDir, projectDir); + } catch (error) { + console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`)); + } + } + } + + /** + * Find all agent YAML files in modules and core + * @param {string} sourceDir - Source modules directory + * @returns {Array} Array of agent file paths + */ + async findAgentFiles(sourceDir) { + const agentFiles = []; + + // Check core agents + const coreAgentsDir = path.join(__dirname, '../../../../../src/core/agents'); + if (await fs.pathExists(coreAgentsDir)) { + const files = await fs.readdir(coreAgentsDir); + + for (const file of files) { + if (file.endsWith('.agent.yaml')) { + agentFiles.push(path.join(coreAgentsDir, file)); + } + } + } + + // Check module agents + if (!(await fs.pathExists(sourceDir))) { + return agentFiles; + } + + const modules = await fs.readdir(sourceDir); + + for (const module of modules) { + const moduleAgentsDir = path.join(sourceDir, module, 'agents'); + + if (await fs.pathExists(moduleAgentsDir)) { + const files = await fs.readdir(moduleAgentsDir); + + for (const file of files) { + if (file.endsWith('.agent.yaml')) { + agentFiles.push(path.join(moduleAgentsDir, file)); + } + } + } + } + + return agentFiles; + } + + /** + * Validate BMad Core compliance + * @param {Object} agentData - Agent YAML data + * @returns {boolean} True if compliant + */ + validateBmadCompliance(agentData) { + const requiredFields = ['agent.metadata.id', 'agent.persona.role', 'agent.persona.principles']; + + for (const field of requiredFields) { + const keys = field.split('.'); + let current = agentData; + + for (const key of keys) { + if (!current || !current[key]) { + return false; + } + current = current[key]; + } + } + + return true; + } + + /** + * Process individual agent YAML file + * @param {string} agentFile - Path to agent YAML file + * @param {string} agentsDir - Target agents directory + * @param {string} projectDir - Project directory + */ + async processAgentFile(agentFile, agentsDir, projectDir) { + const yamlContent = await fs.readFile(agentFile, 'utf8'); + const agentData = yaml.load(yamlContent); + + if (!this.validateBmadCompliance(agentData)) { + return; + } + + // Extract agent name from ID path (e.g., "{bmad_folder}/bmm/agents/analyst.md" -> "analyst") + const idPath = agentData.agent.metadata.id; + const basename = path.basename(idPath, '.md'); + const agentName = basename.startsWith('bmad-') ? basename : `bmad-${basename}`; + + // Create JSON definition + await this.createAgentDefinitionFromYaml(agentsDir, agentName, agentData); + + // Create prompt file + await this.createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir); + } + + /** + * Sanitize agent name for file naming + * @param {string} name - Agent name + * @returns {string} Sanitized name + */ + sanitizeAgentName(name) { + return name + .toLowerCase() + .replaceAll(/\s+/g, '-') + .replaceAll(/[^a-z0-9-]/g, ''); + } + + /** + * Create agent JSON definition from YAML data + * @param {string} agentsDir - Agents directory + * @param {string} agentName - Agent name (role-based) + * @param {Object} agentData - Agent YAML data + */ + async createAgentDefinitionFromYaml(agentsDir, agentName, agentData) { + const personName = agentData.agent.metadata.name; + const role = agentData.agent.persona.role; + + const agentConfig = { + name: agentName, + description: `${personName} - ${role}`, + prompt: `file://./${agentName}-prompt.md`, + tools: ['*'], + mcpServers: {}, + useLegacyMcpJson: true, + resources: [], + }; + + const agentPath = path.join(agentsDir, `${agentName}.json`); + await fs.writeJson(agentPath, agentConfig, { spaces: 2 }); + } + + /** + * Create agent prompt from YAML data + * @param {string} agentsDir - Agents directory + * @param {string} agentName - Agent name (role-based) + * @param {Object} agentData - Agent YAML data + * @param {string} projectDir - Project directory + */ + async createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir) { + const promptPath = path.join(agentsDir, `${agentName}-prompt.md`); + + // Generate prompt from YAML data + const prompt = this.generatePromptFromYaml(agentData); + await fs.writeFile(promptPath, prompt); + } + + /** + * Generate prompt content from YAML data + * @param {Object} agentData - Agent YAML data + * @returns {string} Generated prompt + */ + generatePromptFromYaml(agentData) { + const agent = agentData.agent; + const name = agent.metadata.name; + const icon = agent.metadata.icon || '🤖'; + const role = agent.persona.role; + const identity = agent.persona.identity; + const style = agent.persona.communication_style; + const principles = agent.persona.principles; + + let prompt = `# ${name} ${icon}\n\n`; + prompt += `## Role\n${role}\n\n`; + + if (identity) { + prompt += `## Identity\n${identity}\n\n`; + } + + if (style) { + prompt += `## Communication Style\n${style}\n\n`; + } + + if (principles) { + prompt += `## Principles\n`; + if (typeof principles === 'string') { + // Handle multi-line string principles + prompt += principles + '\n\n'; + } else if (Array.isArray(principles)) { + // Handle array principles + for (const principle of principles) { + prompt += `- ${principle}\n`; + } + prompt += '\n'; + } + } + + // Add menu items if available + if (agent.menu && agent.menu.length > 0) { + prompt += `## Available Workflows\n`; + for (let i = 0; i < agent.menu.length; i++) { + const item = agent.menu[i]; + prompt += `${i + 1}. **${item.trigger}**: ${item.description}\n`; + } + prompt += '\n'; + } + + prompt += `## Instructions\nYou are ${name}, part of the BMad Method. Follow your role and principles while assisting users with their development needs.\n`; + + return prompt; + } + + /** + * Check if Kiro CLI is available + * @returns {Promise} True if available + */ + async isAvailable() { + try { + const { execSync } = require('node:child_process'); + execSync('kiro-cli --version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } + } + + /** + * Get installation instructions + * @returns {string} Installation instructions + */ + getInstallInstructions() { + return `Install Kiro CLI: + curl -fsSL https://github.com/aws/kiro-cli/releases/latest/download/install.sh | bash + + Or visit: https://github.com/aws/kiro-cli`; + } +} + +module.exports = { KiroCliSetup };