From 5b80649d3a0205c6251a17a077bba821e3bc270a Mon Sep 17 00:00:00 2001 From: Davor Racic Date: Wed, 4 Feb 2026 00:36:54 +0100 Subject: [PATCH 01/38] fix(installer): Multiple installer fixes (#1492) * fix: support CRLF line endings and add task/tool templates for all IDEs * fix: preserve file extensions in IDE task/tool paths and update BMAD branding * fix: double extension issue in wrapper filename generation * fix: correct path handling and variable reference in task/tool command generator * fix: change default BMAD folder name from 'bmad' to '_bmad' across all IDE components * refactor: centralize BMAD_FOLDER_NAME constant in path-utils * fix: Replace the rest of BMAD_FOLDER magic values * fix: add safety checks for setBmadFolderName method calls in IdeManager * fix: convert absolute paths to relative in task-tool-command-generator * fix: support .xml task files in bmad-artifacts task discovery * fix: skip internal tasks in manifest generation and IDE command discovery * fix: skip empty artifact_types targets and remove unused vscode_settings target * fix: skip internal tools in manifest generation and improve Windows path handling in command generator * fix: use csv-parse library for proper CSV handling in manifest generation * refactor: extract CSV text cleaning to reusable method in manifest generator * fix: normalize path separators to forward slashes in agent file copying for cross-platform compatibility --------- Co-authored-by: Alex Verkhovsky Co-authored-by: Brian --- src/core/tasks/workflow.xml | 2 +- .../lib/core/dependency-resolver.js | 2 +- tools/cli/installers/lib/core/installer.js | 4 +- .../installers/lib/core/manifest-generator.js | 210 +++++++++++------- tools/cli/installers/lib/custom/handler.js | 2 +- tools/cli/installers/lib/ide/_base-ide.js | 10 +- .../cli/installers/lib/ide/_config-driven.js | 88 +++++++- tools/cli/installers/lib/ide/codex.js | 8 +- tools/cli/installers/lib/ide/manager.js | 10 +- .../installers/lib/ide/platform-codes.yaml | 3 - .../lib/ide/shared/agent-command-generator.js | 4 +- .../lib/ide/shared/bmad-artifacts.js | 17 +- .../installers/lib/ide/shared/path-utils.js | 11 +- .../ide/shared/task-tool-command-generator.js | 122 +++++++++- .../ide/shared/workflow-command-generator.js | 4 +- .../ide/templates/combined/default-task.md | 10 + .../ide/templates/combined/default-tool.md | 10 + .../ide/templates/combined/gemini-task.toml | 11 + .../ide/templates/combined/gemini-tool.toml | 11 + tools/cli/installers/lib/modules/manager.js | 5 +- tools/cli/lib/agent/installer.js | 2 +- 21 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-task.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-tool.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/gemini-task.toml create mode 100644 tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml diff --git a/src/core/tasks/workflow.xml b/src/core/tasks/workflow.xml index fcf6f96be..8c55ec37f 100644 --- a/src/core/tasks/workflow.xml +++ b/src/core/tasks/workflow.xml @@ -1,4 +1,4 @@ - + Execute given workflow by loading its configuration, following instructions, and producing output diff --git a/tools/cli/installers/lib/core/dependency-resolver.js b/tools/cli/installers/lib/core/dependency-resolver.js index 317b07f8d..ee8a8a124 100644 --- a/tools/cli/installers/lib/core/dependency-resolver.js +++ b/tools/cli/installers/lib/core/dependency-resolver.js @@ -146,7 +146,7 @@ class DependencyResolver { const content = await fs.readFile(file.path, 'utf8'); // Parse YAML frontmatter for explicit dependencies - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { // Pre-process to handle backticks in YAML values diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index a14c3d192..cb1462700 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -17,9 +17,7 @@ const { ManifestGenerator } = require('./manifest-generator'); const { IdeConfigManager } = require('./ide-config-manager'); const { CustomHandler } = require('../custom/handler'); const prompts = require('../../../lib/prompts'); - -// BMAD installation folder name - this is constant and should never change -const BMAD_FOLDER_NAME = '_bmad'; +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); class Installer { constructor() { diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 100164d52..fcaee8ada 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -2,6 +2,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); const crypto = require('node:crypto'); +const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); // Load package.json for version info @@ -21,6 +22,19 @@ class ManifestGenerator { this.selectedIdes = []; } + /** + * Clean text for CSV output by normalizing whitespace and escaping quotes + * @param {string} text - Text to clean + * @returns {string} Cleaned text safe for CSV + */ + cleanForCSV(text) { + if (!text) return ''; + return text + .trim() + .replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space + .replaceAll('"', '""'); // Escape quotes for CSV + } + /** * Generate all manifests for the installation * @param {string} bmadDir - _bmad @@ -161,7 +175,7 @@ class ManifestGenerator { workflow = yaml.parse(content); } else { // Parse MD workflow with YAML frontmatter - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!frontmatterMatch) { if (debug) { console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); @@ -201,7 +215,7 @@ class ManifestGenerator { // Workflows with standalone: false are filtered out above workflows.push({ name: workflow.name, - description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV + description: this.cleanForCSV(workflow.description), module: moduleName, path: installPath, }); @@ -319,24 +333,15 @@ class ManifestGenerator { const agentName = entry.name.replace('.md', ''); - // Helper function to clean and escape CSV content - const cleanForCSV = (text) => { - if (!text) return ''; - return text - .trim() - .replaceAll(/\s+/g, ' ') // Normalize whitespace - .replaceAll('"', '""'); // Escape quotes for CSV - }; - agents.push({ name: agentName, displayName: nameMatch ? nameMatch[1] : agentName, title: titleMatch ? titleMatch[1] : '', icon: iconMatch ? iconMatch[1] : '', - role: roleMatch ? cleanForCSV(roleMatch[1]) : '', - identity: identityMatch ? cleanForCSV(identityMatch[1]) : '', - communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '', - principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', + role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '', + identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '', + communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '', + principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, }); @@ -385,6 +390,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -392,13 +402,13 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tasks - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; + description = this.cleanForCSV(frontmatter.description || ''); standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } catch { // If YAML parsing fails, use defaults @@ -411,7 +421,7 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); const standaloneMatch = content.match(/]+standalone="true"/); standalone = !!standaloneMatch; @@ -424,7 +434,7 @@ class ManifestGenerator { tasks.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -474,6 +484,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal tools (same as tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -481,13 +496,13 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tools - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; + description = this.cleanForCSV(frontmatter.description || ''); standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } catch { // If YAML parsing fails, use defaults @@ -500,7 +515,7 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); const standaloneMatch = content.match(/]+standalone="true"/); standalone = !!standaloneMatch; @@ -513,7 +528,7 @@ class ManifestGenerator { tools.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -773,30 +788,23 @@ class ManifestGenerator { */ async writeAgentManifest(cfgDir) { const csvPath = path.join(cfgDir, 'agent-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 11) { - const name = parts[0].replace(/^"/, ''); - const module = parts[8]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with persona fields - let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; + let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; // Combine existing and new agents, preferring new data for duplicates const allAgents = new Map(); @@ -809,18 +817,38 @@ class ManifestGenerator { // Add/update new agents for (const agent of this.agents) { const key = `${agent.module}:${agent.name}`; - allAgents.set( - key, - `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`, - ); + allAgents.set(key, { + name: agent.name, + displayName: agent.displayName, + title: agent.title, + icon: agent.icon, + role: agent.role, + identity: agent.identity, + communicationStyle: agent.communicationStyle, + principles: agent.principles, + module: agent.module, + path: agent.path, + }); } // Write all agents - for (const [, value] of allAgents) { - csv += value + '\n'; + for (const [, record] of allAgents) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.title), + escapeCsv(record.icon), + escapeCsv(record.role), + escapeCsv(record.identity), + escapeCsv(record.communicationStyle), + escapeCsv(record.principles), + escapeCsv(record.module), + escapeCsv(record.path), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -830,30 +858,23 @@ class ManifestGenerator { */ async writeTaskManifest(cfgDir) { const csvPath = path.join(cfgDir, 'task-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -866,15 +887,30 @@ class ManifestGenerator { // Add/update new tasks for (const task of this.tasks) { const key = `${task.module}:${task.name}`; - allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`); + allTasks.set(key, { + name: task.name, + displayName: task.displayName, + description: task.description, + module: task.module, + path: task.path, + standalone: task.standalone, + }); } // Write all tasks - for (const [, value] of allTasks) { - csv += value + '\n'; + for (const [, record] of allTasks) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -884,30 +920,23 @@ class ManifestGenerator { */ async writeToolManifest(cfgDir) { const csvPath = path.join(cfgDir, 'tool-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tools const allTools = new Map(); @@ -920,15 +949,30 @@ class ManifestGenerator { // Add/update new tools for (const tool of this.tools) { const key = `${tool.module}:${tool.name}`; - allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`); + allTools.set(key, { + name: tool.name, + displayName: tool.displayName, + description: tool.description, + module: tool.module, + path: tool.path, + standalone: tool.standalone, + }); } // Write all tools - for (const [, value] of allTools) { - csv += value + '\n'; + for (const [, record] of allTools) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } diff --git a/tools/cli/installers/lib/custom/handler.js b/tools/cli/installers/lib/custom/handler.js index c8aa52eee..8c730cf32 100644 --- a/tools/cli/installers/lib/custom/handler.js +++ b/tools/cli/installers/lib/custom/handler.js @@ -297,7 +297,7 @@ class CustomHandler { const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']); for (const agentFile of agentFiles) { - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index b16ee5184..4ae116772 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -3,6 +3,7 @@ const fs = require('fs-extra'); const chalk = require('chalk'); const { XmlHandler } = require('../../../lib/xml-handler'); const { getSourcePath } = require('../../../lib/project-root'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); /** * Base class for IDE-specific setup @@ -18,7 +19,7 @@ class BaseIdeSetup { this.configFile = null; // Override in subclasses when detection is file-based this.detectionPaths = []; // Additional paths that indicate the IDE is configured this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -57,7 +58,7 @@ class BaseIdeSetup { if (this.configDir) { const configPath = path.join(projectDir, this.configDir); if (await fs.pathExists(configPath)) { - const bmadRulesPath = path.join(configPath, 'bmad'); + const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); if (await fs.pathExists(bmadRulesPath)) { await fs.remove(bmadRulesPath); console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); @@ -445,6 +446,11 @@ class BaseIdeSetup { try { const content = await fs.readFile(fullPath, 'utf8'); + // Skip internal/engine files (not user-facing tasks/tools) + if (content.includes('internal="true"')) { + continue; + } + // Check for standalone="true" in XML files if (entry.name.endsWith('.xml')) { // Look for standalone="true" in the opening tag (task or tool) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 87be7300a..486889267 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -66,6 +66,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { */ async installToTarget(projectDir, bmadDir, config, options) { const { target_dir, template_type, artifact_types } = config; + + // Skip targets with explicitly empty artifact_types array + // This prevents creating empty directories when no artifacts will be written + if (Array.isArray(artifact_types) && artifact_types.length === 0) { + return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } }; + } + const targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); @@ -86,10 +93,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); } - // Install tasks and tools + // Install tasks and tools using template system (supports TOML for Gemini, MD for others) if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { - const taskToolGen = new TaskToolCommandGenerator(); - const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config); results.tasks = taskToolResult.tasks || 0; results.tools = taskToolResult.tools || 0; } @@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return count; } + /** + * Write task/tool artifacts to target directory using templates + * @param {string} targetPath - Target directory path + * @param {Array} artifacts - Task/tool artifacts + * @param {string} templateType - Template type to use + * @param {Object} config - Installation configuration + * @returns {Promise} Counts of tasks and tools written + */ + async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) { + let taskCount = 0; + let toolCount = 0; + + // Pre-load templates to avoid repeated file I/O in the loop + const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task'); + const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool'); + + const { artifact_types } = config; + + for (const artifact of artifacts) { + if (artifact.type !== 'task' && artifact.type !== 'tool') { + continue; + } + + // Skip if the specific artifact type is not requested in config + if (artifact_types) { + if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue; + if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue; + } + + // Use pre-loaded template based on artifact type + const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate; + + const content = this.renderTemplate(template, artifact); + const filename = this.generateFilename(artifact, artifact.type, extension); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + + if (artifact.type === 'task') { + taskCount++; + } else { + toolCount++; + } + } + + return { tasks: taskCount, tools: toolCount }; + } + /** * Load template based on type and configuration * @param {string} templateType - Template type (claude, windsurf, etc.) @@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} renderTemplate(template, artifact) { // Use the appropriate path property based on artifact type let pathToUse = artifact.relativePath || ''; - if (artifact.type === 'agent-launcher') { - pathToUse = artifact.agentPath || artifact.relativePath || ''; - } else if (artifact.type === 'workflow-command') { - pathToUse = artifact.workflowPath || artifact.relativePath || ''; + switch (artifact.type) { + case 'agent-launcher': { + pathToUse = artifact.agentPath || artifact.relativePath || ''; + + break; + } + case 'workflow-command': { + pathToUse = artifact.workflowPath || artifact.relativePath || ''; + + break; + } + case 'task': + case 'tool': { + pathToUse = artifact.path || artifact.relativePath || ''; + + break; + } + // No default } let rendered = template @@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} // Reuse central logic to ensure consistent naming conventions const standardName = toDashPath(artifact.relativePath); - // Clean up potential double extensions from source files (e.g. .yaml.md -> .md) - const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md'); + // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) + // This handles any extensions that might slip through toDashPath() + const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md'); // If using default markdown, preserve the bmad-agent- prefix for agents if (extension === '.md') { diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 5cd503e24..29f595f6c 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -104,7 +104,10 @@ class CodexSetup extends BaseIdeSetup { ); taskArtifacts.push({ type: 'task', + name: task.name, + displayName: task.name, module: task.module, + path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), content, @@ -116,7 +119,7 @@ class CodexSetup extends BaseIdeSetup { const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts); // Also write tasks using underscore format - const ttGen = new TaskToolCommandGenerator(); + const ttGen = new TaskToolCommandGenerator(this.bmadFolderName); const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts); const written = agentCount + workflowCount + tasksWritten; @@ -214,7 +217,10 @@ class CodexSetup extends BaseIdeSetup { artifacts.push({ type: 'task', + name: task.name, + displayName: task.name, module: task.module, + path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), content, diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 2b68dfad2..7d00588c0 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,6 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); const chalk = require('chalk'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); /** * IDE Manager - handles IDE-specific setup @@ -14,7 +15,7 @@ class IdeManager { constructor() { this.handlers = new Map(); this._initialized = false; - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -73,6 +74,9 @@ class IdeManager { if (HandlerClass) { const instance = new HandlerClass(); if (instance.name && typeof instance.name === 'string') { + if (typeof instance.setBmadFolderName === 'function') { + instance.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(instance.name, instance); } } @@ -100,7 +104,9 @@ class IdeManager { if (!platformInfo.installer) continue; const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); - handler.setBmadFolderName(this.bmadFolderName); + if (typeof handler.setBmadFolderName === 'function') { + handler.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(platformCode, handler); } } diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 6a9078a8a..2ca32aed5 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -94,9 +94,6 @@ platforms: - target_dir: .github/agents template_type: copilot_agents artifact_types: [agents] - - target_dir: .vscode - template_type: vscode_settings - artifact_types: [] iflow: name: "iFlow" diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index dec22a12a..caf60614f 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,14 +1,14 @@ const path = require('node:path'); const fs = require('fs-extra'); const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates launcher command files for each agent * Similar to WorkflowCommandGenerator but for agents */ class AgentCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/agent-command-template.md'); this.bmadFolderName = bmadFolderName; } diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index e88a64f5d..7bcfd6a79 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) { const files = await fs.readdir(dirPath); for (const file of files) { - if (!file.endsWith('.md')) { + // Include both .md and .xml task files + if (!file.endsWith('.md') && !file.endsWith('.xml')) { continue; } + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, 'utf8'); + + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + + // Remove extension to get task name + const ext = file.endsWith('.xml') ? '.xml' : '.md'; tasks.push({ - path: path.join(dirPath, file), - name: file.replace('.md', ''), + path: filePath, + name: file.replace(ext, ''), module: moduleName, }); } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index d6ad00f51..519669233 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -18,6 +18,9 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; +// BMAD installation folder name - centralized constant for all installers +const BMAD_FOLDER_NAME = '_bmad'; + /** * Convert hierarchical path to flat dash-separated name (NEW STANDARD) * Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md' @@ -59,7 +62,9 @@ function toDashPath(relativePath) { return 'bmad-unknown.md'; } - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions to avoid double extensions in generated filenames + // e.g., 'create-story.xml' → 'create-story', 'workflow.yaml' → 'workflow' + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) { * @deprecated Use toDashPath instead */ function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions (same as toDashPath for consistency) + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -289,4 +295,5 @@ module.exports = { TYPE_SEGMENTS, AGENT_SEGMENT, + BMAD_FOLDER_NAME, }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index a0c4bcf87..60eb54687 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -2,12 +2,98 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); -const { toColonName, toColonPath, toDashPath } = require('./path-utils'); +const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for standalone tasks and tools */ class TaskToolCommandGenerator { + /** + * @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad') + * Note: This parameter is accepted for API consistency with AgentCommandGenerator and + * WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores + * filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is + * used for template placeholder rendering ({{bmadFolderName}}). + */ + constructor(bmadFolderName = BMAD_FOLDER_NAME) { + this.bmadFolderName = bmadFolderName; + } + + /** + * Collect task and tool artifacts for IDE installation + * @param {string} bmadDir - BMAD installation directory + * @returns {Promise} Artifacts array with metadata + */ + async collectTaskToolArtifacts(bmadDir) { + const tasks = await this.loadTaskManifest(bmadDir); + const tools = await this.loadToolManifest(bmadDir); + + // Filter to only standalone items + const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + + const artifacts = []; + const bmadPrefix = `${BMAD_FOLDER_NAME}/`; + + // Collect task artifacts + for (const task of standaloneTasks) { + let taskPath = (task.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(taskPath)) { + taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (taskPath.startsWith(bmadPrefix)) { + taskPath = taskPath.slice(bmadPrefix.length); + } + + const taskExt = path.extname(taskPath) || '.md'; + artifacts.push({ + type: 'task', + name: task.name, + displayName: task.displayName || task.name, + description: task.description || `Execute ${task.displayName || task.name}`, + module: task.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${task.module}/tasks/${task.name}${taskExt}`, + path: taskPath, + }); + } + + // Collect tool artifacts + for (const tool of standaloneTools) { + let toolPath = (tool.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(toolPath)) { + toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (toolPath.startsWith(bmadPrefix)) { + toolPath = toolPath.slice(bmadPrefix.length); + } + + const toolExt = path.extname(toolPath) || '.md'; + artifacts.push({ + type: 'tool', + name: tool.name, + displayName: tool.displayName || tool.name, + description: tool.description || `Execute ${tool.displayName || tool.name}`, + module: tool.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, + path: toolPath, + }); + } + + return { + artifacts, + counts: { + tasks: standaloneTasks.length, + tools: standaloneTools.length, + }, + }; + } + /** * Generate task and tool commands from manifest CSVs * @param {string} projectDir - Project directory @@ -65,9 +151,35 @@ class TaskToolCommandGenerator { const description = item.description || `Execute ${item.displayName || item.name}`; // Convert path to use {project-root} placeholder + // Handle undefined/missing path by constructing from module and name let itemPath = item.path; - if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { - itemPath = `{project-root}/${itemPath}`; + if (!itemPath || typeof itemPath !== 'string') { + // Fallback: construct path from module and name if path is missing + const typePlural = type === 'task' ? 'tasks' : 'tools'; + itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`; + } else { + // Normalize path separators to forward slashes + itemPath = itemPath.replaceAll('\\', '/'); + + // Extract relative path from absolute paths (Windows or Unix) + // Look for _bmad/ or bmad/ in the path and extract everything after it + // Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/... + // Use [/\\] to handle both Unix forward slashes and Windows backslashes, + // and also paths without a leading separator (e.g., C:/_bmad/...) + const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/); + if (bmadMatch) { + // Found /_bmad/ or /bmad/ - use relative path after it + itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`; + } else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) { + // Relative path starting with _bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`; + } else if (itemPath.startsWith('bmad/')) { + // Relative path starting with bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`; + } else if (!itemPath.startsWith('{project-root}')) { + // For other relative paths, prefix with project root and bmad folder + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`; + } } return `--- @@ -187,7 +299,7 @@ Follow all instructions in the ${type} file exactly as written. // Generate command files for tasks for (const task of standaloneTasks) { const commandContent = this.generateCommandContent(task, 'task'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); @@ -198,7 +310,7 @@ Follow all instructions in the ${type} file exactly as written. // Generate command files for tools for (const tool of standaloneTools) { const commandContent = this.generateCommandContent(tool, 'tool'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index 6dab1a3f2..5a23fda2f 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -2,13 +2,13 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for each workflow in the manifest */ class WorkflowCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.bmadFolderName = bmadFolderName; } diff --git a/tools/cli/installers/lib/ide/templates/combined/default-task.md b/tools/cli/installers/lib/ide/templates/combined/default-task.md new file mode 100644 index 000000000..b865d6ffb --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-task.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the task file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/default-tool.md b/tools/cli/installers/lib/ide/templates/combined/default-tool.md new file mode 100644 index 000000000..11c6aac8d --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-tool.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the tool file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml new file mode 100644 index 000000000..7d15e2164 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} task from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' task. + +TASK INSTRUCTIONS: +1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TASK FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml new file mode 100644 index 000000000..fc78c6b72 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} tool from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' tool. + +TOOL INSTRUCTIONS: +1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 60c087b19..1f523fba3 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -7,6 +7,7 @@ const { XmlHandler } = require('../../../lib/xml-handler'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { ExternalModuleManager } = require('./external-manager'); +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); /** * Manages the installation, updating, and removal of BMAD modules. @@ -27,7 +28,7 @@ const { ExternalModuleManager } = require('./external-manager'); class ModuleManager { constructor(options = {}) { this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden this.customModulePaths = new Map(); // Initialize custom module paths this.externalModuleManager = new ExternalModuleManager(); // For external official modules } @@ -870,7 +871,7 @@ class ModuleManager { for (const agentFile of agentFiles) { if (!agentFile.endsWith('.agent.yaml')) continue; - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); diff --git a/tools/cli/lib/agent/installer.js b/tools/cli/lib/agent/installer.js index b55502edd..a76504530 100644 --- a/tools/cli/lib/agent/installer.js +++ b/tools/cli/lib/agent/installer.js @@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) { * @returns {string} Resolved path */ function resolvePath(pathStr, context) { - return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder); + return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder); } /** From 2d9ebcaf2f9e071ff3d155876c4c8dd78ac30a19 Mon Sep 17 00:00:00 2001 From: Davor Racic Date: Wed, 4 Feb 2026 00:39:05 +0100 Subject: [PATCH 02/38] feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt (#1514) * feat: Update @clack/prompts to v1.0.0 and Add autocompleteMultiselect prompt * fix(cli): flexible tool selection (skip recommended or additional) + fix spacing * feat(cli): improve tool selection UX with autocomplete and upgrade path * feat(cli): display selected tools after IDE selection with preferred markers * fix: formatting * fix: make selection message more clear * fix: formatting * fix: Remove redundant colon --------- Co-authored-by: Brian --- package-lock.json | 18 +- package.json | 4 +- .../installers/lib/core/config-collector.js | 6 +- tools/cli/installers/lib/core/installer.js | 3 - tools/cli/lib/prompts.js | 198 ++++++++++++- tools/cli/lib/ui.js | 273 +++++++++++++----- 6 files changed, 406 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe41085b3..d3888b281 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "6.0.0-Beta.5", "license": "MIT", "dependencies": { - "@clack/prompts": "^0.11.0", + "@clack/core": "^1.0.0", + "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", "boxen": "^5.1.2", "chalk": "^4.1.2", @@ -22,6 +23,7 @@ "ignore": "^7.0.5", "js-yaml": "^4.1.0", "ora": "^5.4.1", + "picocolors": "^1.1.1", "semver": "^7.6.3", "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", @@ -756,9 +758,9 @@ } }, "node_modules/@clack/core": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", - "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz", + "integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==", "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -766,12 +768,12 @@ } }, "node_modules/@clack/prompts": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", - "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz", + "integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==", "license": "MIT", "dependencies": { - "@clack/core": "0.5.0", + "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } diff --git a/package.json b/package.json index 8df5ea009..8798a6208 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ ] }, "dependencies": { - "@clack/prompts": "^0.11.0", + "@clack/core": "^1.0.0", + "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", "boxen": "^5.1.2", "chalk": "^4.1.2", @@ -82,6 +83,7 @@ "ignore": "^7.0.5", "js-yaml": "^4.1.0", "ora": "^5.4.1", + "picocolors": "^1.1.1", "semver": "^7.6.3", "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index f8b2042a5..f4eaf5e3d 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -586,7 +586,11 @@ class ConfigCollector { console.log(); console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); let customize = true; - if (moduleName !== 'core') { + if (moduleName === 'core') { + // Core module: no confirm prompt, so add spacing manually to match visual style + console.log(chalk.gray('│')); + } else { + // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) const customizeAnswer = await prompts.prompt([ { type: 'confirm', diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index cb1462700..edb15112c 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -695,9 +695,6 @@ class Installer { config.skipIde = toolSelection.skipIde; const ideConfigurations = toolSelection.configurations; - // Add spacing after prompts before installation progress - console.log(''); - if (spinner.isSpinning) { spinner.text = 'Continuing installation...'; } else { diff --git a/tools/cli/lib/prompts.js b/tools/cli/lib/prompts.js index 5d85e2b44..7ab2d21ef 100644 --- a/tools/cli/lib/prompts.js +++ b/tools/cli/lib/prompts.js @@ -8,6 +8,8 @@ */ let _clack = null; +let _clackCore = null; +let _picocolors = null; /** * Lazy-load @clack/prompts (ESM module) @@ -20,6 +22,28 @@ async function getClack() { return _clack; } +/** + * Lazy-load @clack/core (ESM module) + * @returns {Promise} The clack core module + */ +async function getClackCore() { + if (!_clackCore) { + _clackCore = await import('@clack/core'); + } + return _clackCore; +} + +/** + * Lazy-load picocolors + * @returns {Promise} The picocolors module + */ +async function getPicocolors() { + if (!_picocolors) { + _picocolors = (await import('picocolors')).default; + } + return _picocolors; +} + /** * Handle user cancellation gracefully * @param {any} value - The value to check @@ -191,6 +215,118 @@ async function groupMultiselect(options) { return result; } +/** + * Default filter function for autocomplete - case-insensitive label matching + * @param {string} search - Search string + * @param {Object} option - Option object with label + * @returns {boolean} Whether the option matches + */ +function defaultAutocompleteFilter(search, option) { + const label = option.label ?? String(option.value ?? ''); + return label.toLowerCase().includes(search.toLowerCase()); +} + +/** + * Autocomplete multi-select prompt with type-ahead filtering + * Custom implementation that always shows "Space/Tab:" in the hint + * @param {Object} options - Prompt options + * @param {string} options.message - The question to ask + * @param {Array} options.options - Array of choices [{label, value, hint?}] + * @param {string} [options.placeholder] - Placeholder text for search input + * @param {Array} [options.initialValues] - Array of initially selected values + * @param {boolean} [options.required=false] - Whether at least one must be selected + * @param {number} [options.maxItems=5] - Maximum visible items in scrollable list + * @param {Function} [options.filter] - Custom filter function (search, option) => boolean + * @returns {Promise} Array of selected values + */ +async function autocompleteMultiselect(options) { + const core = await getClackCore(); + const clack = await getClack(); + const color = await getPicocolors(); + + const filterFn = options.filter ?? defaultAutocompleteFilter; + + const prompt = new core.AutocompletePrompt({ + options: options.options, + multiple: true, + filter: filterFn, + validate: () => { + if (options.required && prompt.selectedValues.length === 0) { + return 'Please select at least one item'; + } + }, + initialValue: options.initialValues, + render() { + const barColor = this.state === 'error' ? color.yellow : color.cyan; + const bar = barColor(clack.S_BAR); + const barEnd = barColor(clack.S_BAR_END); + + const title = `${color.gray(clack.S_BAR)}\n${clack.symbol(this.state)} ${options.message}\n`; + + const userInput = this.userInput; + const placeholder = options.placeholder || 'Type to search...'; + const hasPlaceholder = userInput === '' && placeholder !== undefined; + + // Show placeholder or user input with cursor + const searchDisplay = + this.isNavigating || hasPlaceholder ? color.dim(hasPlaceholder ? placeholder : userInput) : this.userInputWithCursor; + + const allOptions = this.options; + const matchCount = + this.filteredOptions.length === allOptions.length + ? '' + : color.dim(` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`); + + // Render option with checkbox + const renderOption = (opt, isHighlighted) => { + const isSelected = this.selectedValues.includes(opt.value); + const label = opt.label ?? String(opt.value ?? ''); + const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : ''; + const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE); + return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`; + }; + + switch (this.state) { + case 'submit': { + return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`; + } + + case 'cancel': { + return `${title}${color.gray(clack.S_BAR)} ${color.strikethrough(color.dim(userInput))}`; + } + + default: { + // Always show "SPACE:" regardless of isNavigating state + const hints = [`${color.dim('↑/↓')} to navigate`, `${color.dim('TAB/SPACE:')} select`, `${color.dim('ENTER:')} confirm`]; + + const noMatchesLine = this.filteredOptions.length === 0 && userInput ? [`${bar} ${color.yellow('No matches found')}`] : []; + + const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : []; + + const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine]; + + const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`]; + + const optionLines = clack.limitOptions({ + cursor: this.cursor, + options: this.filteredOptions, + style: renderOption, + maxItems: options.maxItems || 5, + output: options.output, + rowPadding: headerLines.length + footerLines.length, + }); + + return [...headerLines, ...optionLines.map((line) => `${bar} ${line}`), ...footerLines].join('\n'); + } + } + }, + }); + + const result = await prompt.prompt(); + await handleCancel(result); + return result; +} + /** * Confirm prompt (replaces Inquirer 'confirm' type) * @param {Object} options - Prompt options @@ -211,7 +347,12 @@ async function confirm(options) { } /** - * Text input prompt (replaces Inquirer 'input' type) + * Text input prompt with Tab-to-fill-placeholder support (replaces Inquirer 'input' type) + * + * This custom implementation restores the Tab-to-fill-placeholder behavior that was + * intentionally removed in @clack/prompts v1.0.0 (placeholder became purely visual). + * Uses @clack/core's TextPrompt primitive with custom key handling. + * * @param {Object} options - Prompt options * @param {string} options.message - The question to ask * @param {string} [options.default] - Default value @@ -220,20 +361,64 @@ async function confirm(options) { * @returns {Promise} User's input */ async function text(options) { - const clack = await getClack(); + const core = await getClackCore(); + const color = await getPicocolors(); // Use default as placeholder if placeholder not explicitly provided // This shows the default value as grayed-out hint text const placeholder = options.placeholder === undefined ? options.default : options.placeholder; + const defaultValue = options.default; - const result = await clack.text({ - message: options.message, - defaultValue: options.default, - placeholder: typeof placeholder === 'string' ? placeholder : undefined, + const prompt = new core.TextPrompt({ + defaultValue, validate: options.validate, + render() { + const title = `${color.gray('◆')} ${options.message}`; + + // Show placeholder as dim text when input is empty + let valueDisplay; + if (this.state === 'error') { + valueDisplay = color.yellow(this.userInputWithCursor); + } else if (this.userInput) { + valueDisplay = this.userInputWithCursor; + } else if (placeholder) { + // Show placeholder with cursor indicator when empty + valueDisplay = `${color.inverse(color.hidden('_'))}${color.dim(placeholder)}`; + } else { + valueDisplay = color.inverse(color.hidden('_')); + } + + const bar = color.gray('│'); + + // Handle different states + if (this.state === 'submit') { + return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || defaultValue || '')}`; + } + + if (this.state === 'cancel') { + return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(this.userInput || ''))}`; + } + + if (this.state === 'error') { + return `${color.yellow('▲')} ${options.message}\n${bar} ${valueDisplay}\n${color.yellow('│')} ${color.yellow(this.error)}`; + } + + return `${title}\n${bar} ${valueDisplay}\n${bar}`; + }, }); + // Add Tab key handler to fill placeholder into input + prompt.on('key', (char) => { + if (char === '\t' && placeholder && !prompt.userInput) { + // Use _setUserInput with write=true to populate the readline and update internal state + prompt._setUserInput(placeholder, true); + } + }); + + const result = await prompt.prompt(); await handleCancel(result); + + // TextPrompt's finalize handler already applies defaultValue for empty input return result; } @@ -423,6 +608,7 @@ module.exports = { select, multiselect, groupMultiselect, + autocompleteMultiselect, confirm, text, password, diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index da5420cb2..89dc11c50 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -344,6 +344,9 @@ class UI { /** * Prompt for tool/IDE selection (called after module configuration) + * Uses a split prompt approach: + * 1. Recommended tools - standard multiselect for 3 preferred tools + * 2. Additional tools - autocompleteMultiselect with search capability * @param {string} projectDir - Project directory to check for existing IDEs * @returns {Object} Tool configuration */ @@ -366,95 +369,190 @@ class UI { const preferredIdes = ideManager.getPreferredIdes(); const otherIdes = ideManager.getOtherIdes(); - // Build grouped options object for groupMultiselect - const groupedOptions = {}; - const processedIdes = new Set(); - const initialValues = []; + // Determine which configured IDEs are in "preferred" vs "other" categories + const configuredPreferred = configuredIdes.filter((id) => preferredIdes.some((ide) => ide.value === id)); + const configuredOther = configuredIdes.filter((id) => otherIdes.some((ide) => ide.value === id)); - // First, add previously configured IDEs, marked with ✅ - if (configuredIdes.length > 0) { - const configuredGroup = []; - for (const ideValue of configuredIdes) { - // Skip empty or invalid IDE values - if (!ideValue || typeof ideValue !== 'string') { - continue; - } - - // Find the IDE in either preferred or other lists - const preferredIde = preferredIdes.find((ide) => ide.value === ideValue); - const otherIde = otherIdes.find((ide) => ide.value === ideValue); - const ide = preferredIde || otherIde; - - if (ide) { - configuredGroup.push({ - label: `${ide.name} ✅`, - value: ide.value, - }); - processedIdes.add(ide.value); - initialValues.push(ide.value); // Pre-select configured IDEs - } else { - // Warn about unrecognized IDE (but don't fail) - console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`)); - } - } - if (configuredGroup.length > 0) { - groupedOptions['Previously Configured'] = configuredGroup; - } + // Warn about previously configured tools that are no longer available + const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value)); + const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id)); + if (unknownTools.length > 0) { + console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`)); } - // Add preferred tools (excluding already processed) - const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value)); - if (remainingPreferred.length > 0) { - groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => { - processedIdes.add(ide.value); - return { - label: `${ide.name} ⭐`, - value: ide.value, - }; + // ───────────────────────────────────────────────────────────────────────────── + // UPGRADE PATH: If tools already configured, show all tools with configured at top + // ───────────────────────────────────────────────────────────────────────────── + if (configuredIdes.length > 0) { + const allTools = [...preferredIdes, ...otherIdes]; + + // Sort: configured tools first, then preferred, then others + const sortedTools = [ + ...allTools.filter((ide) => configuredIdes.includes(ide.value)), + ...allTools.filter((ide) => !configuredIdes.includes(ide.value)), + ]; + + const upgradeOptions = sortedTools.map((ide) => { + const isConfigured = configuredIdes.includes(ide.value); + const isPreferred = preferredIdes.some((p) => p.value === ide.value); + let label = ide.name; + if (isPreferred) label += ' ⭐'; + if (isConfigured) label += ' ✅'; + return { label, value: ide.value }; + }); + + // Sort initialValues to match display order + const sortedInitialValues = sortedTools.filter((ide) => configuredIdes.includes(ide.value)).map((ide) => ide.value); + + const upgradeSelected = await prompts.autocompleteMultiselect({ + message: 'Integrate with', + options: upgradeOptions, + initialValues: sortedInitialValues, + required: false, + maxItems: 8, + }); + + const selectedIdes = upgradeSelected || []; + + if (selectedIdes.length === 0) { + console.log(''); + const confirmNoTools = await prompts.confirm({ + message: 'No tools selected. Continue without installing any tools?', + default: false, + }); + + if (!confirmNoTools) { + return this.promptToolSelection(projectDir); + } + + return { ides: [], skipIde: true }; + } + + // Display selected tools + this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + + return { ides: selectedIdes, skipIde: false }; + } + + // ───────────────────────────────────────────────────────────────────────────── + // NEW INSTALL: Show recommended tools first with "Browse All" option + // ───────────────────────────────────────────────────────────────────────────── + const recommendedOptions = preferredIdes.map((ide) => { + const isConfigured = configuredPreferred.includes(ide.value); + return { + label: isConfigured ? `${ide.name} ⭐ ✅` : `${ide.name} ⭐`, + value: ide.value, + }; + }); + + // Add "browse all" option at the end if there are additional tools + if (otherIdes.length > 0) { + const totalTools = preferredIdes.length + otherIdes.length; + recommendedOptions.push({ + label: `→ Browse all supported tools (${totalTools} total)...`, + value: '__BROWSE_ALL__', }); } - // Add other tools (excluding already processed) - const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value)); - if (remainingOther.length > 0) { - groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({ - label: ide.name, - value: ide.value, - })); - } + // Pre-select previously configured preferred tools + const recommendedInitialValues = configuredPreferred.length > 0 ? configuredPreferred : undefined; - // Add standalone "None" option at the end - groupedOptions[' '] = [ - { - label: '⚠ None - I am not installing any tools', - value: '__NONE__', - }, - ]; - - let selectedIdes = []; - - selectedIdes = await prompts.groupMultiselect({ - message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, - options: groupedOptions, - initialValues: initialValues.length > 0 ? initialValues : undefined, - required: true, - selectableGroups: false, + const recommendedSelected = await prompts.multiselect({ + message: `Integrate with ${chalk.dim('(↑/↓ to navigate • SPACE: select • ENTER: confirm)')}:`, + options: recommendedOptions, + initialValues: recommendedInitialValues, + required: false, }); - // If user selected both "__NONE__" and other tools, honor the "None" choice - if (selectedIdes && selectedIdes.includes('__NONE__') && selectedIdes.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None - I am not installing any tools" was selected, so no tools will be configured.')); - console.log(); - selectedIdes = []; - } else if (selectedIdes && selectedIdes.includes('__NONE__')) { - // Only "__NONE__" was selected - selectedIdes = []; + const selectedRecommended = recommendedSelected || []; + + // ───────────────────────────────────────────────────────────────────────────── + // STEP 2: Handle "Browse All" selection - show additional tools if requested + // ───────────────────────────────────────────────────────────────────────────── + const wantsBrowseAll = selectedRecommended.includes('__BROWSE_ALL__'); + const filteredRecommended = selectedRecommended.filter((v) => v !== '__BROWSE_ALL__'); + + // Show additional tools if: + // 1. User explicitly chose "Browse All", OR + // 2. User has previously configured "other" tools, OR + // 3. User selected no recommended tools (allow them to pick from other tools) + const showAdditionalTools = wantsBrowseAll || configuredOther.length > 0 || filteredRecommended.length === 0; + + let selectedAdditionalOrAll = []; + + if (showAdditionalTools) { + // Show ALL tools if: + // - User explicitly chose "Browse All", OR + // - User selected nothing from recommended (so they can pick from everything) + // Otherwise, show only "other" tools as additional options + const showAllTools = wantsBrowseAll || filteredRecommended.length === 0; + const toolsToShow = showAllTools ? [...preferredIdes, ...otherIdes] : otherIdes; + + if (toolsToShow.length > 0) { + const allToolOptions = toolsToShow.map((ide) => { + const isConfigured = configuredIdes.includes(ide.value); + const isPreferred = preferredIdes.some((p) => p.value === ide.value); + let label = ide.name; + if (isPreferred) label += ' ⭐'; + if (isConfigured) label += ' ✅'; + return { + label, + value: ide.value, + }; + }); + + // Pre-select: previously configured tools + any recommended tools already selected + const initialValues = [...configuredIdes, ...filteredRecommended].filter((v, i, arr) => arr.indexOf(v) === i); // dedupe + + // Use "additional" only if user already selected some recommended tools + const isAdditional = !wantsBrowseAll && filteredRecommended.length > 0; + + console.log(''); + const selected = await prompts.autocompleteMultiselect({ + message: isAdditional ? 'Select additional tools:' : 'Select tools:', + options: allToolOptions, + initialValues: initialValues.length > 0 ? initialValues : undefined, + required: false, + maxItems: 8, + }); + + selectedAdditionalOrAll = selected || []; + } } + // Combine selections: + // - If "Browse All" was used, the second prompt contains ALL selections + // - Otherwise, combine recommended + additional + const allSelectedIdes = wantsBrowseAll ? selectedAdditionalOrAll : [...filteredRecommended, ...selectedAdditionalOrAll]; + + // ───────────────────────────────────────────────────────────────────────────── + // STEP 3: Confirm if no tools selected + // ───────────────────────────────────────────────────────────────────────────── + if (allSelectedIdes.length === 0) { + console.log(''); + const confirmNoTools = await prompts.confirm({ + message: 'No tools selected. Continue without installing any tools?', + default: false, + }); + + if (!confirmNoTools) { + // User wants to select tools - recurse + return this.promptToolSelection(projectDir); + } + + return { + ides: [], + skipIde: true, + }; + } + + // Display selected tools + const allTools = [...preferredIdes, ...otherIdes]; + this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools); + return { - ides: selectedIdes || [], - skipIde: !selectedIdes || selectedIdes.length === 0, + ides: allSelectedIdes, + skipIde: allSelectedIdes.length === 0, }; } @@ -1655,6 +1753,27 @@ class UI { console.log(''); } + + /** + * Display list of selected tools after IDE selection + * @param {Array} selectedIdes - Array of selected IDE values + * @param {Array} preferredIdes - Array of preferred IDE objects + * @param {Array} allTools - Array of all tool objects + */ + displaySelectedTools(selectedIdes, preferredIdes, allTools) { + if (selectedIdes.length === 0) return; + + const preferredValues = new Set(preferredIdes.map((ide) => ide.value)); + + console.log(''); + console.log(chalk.dim(' Selected tools:')); + for (const ideValue of selectedIdes) { + const tool = allTools.find((t) => t.value === ideValue); + const name = tool?.name || ideValue; + const marker = preferredValues.has(ideValue) ? ' ⭐' : ''; + console.log(chalk.dim(` • ${name}${marker}`)); + } + } } module.exports = { UI }; From df176d42064d06988bd16b86d33afa42fd6d3e58 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Tue, 3 Feb 2026 21:36:21 -0600 Subject: [PATCH 03/38] installer remove double tool questioning --- tools/cli/lib/ui.js | 95 +++++++-------------------------------------- 1 file changed, 14 insertions(+), 81 deletions(-) diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 89dc11c50..cc135fea3 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -435,95 +435,29 @@ class UI { } // ───────────────────────────────────────────────────────────────────────────── - // NEW INSTALL: Show recommended tools first with "Browse All" option + // NEW INSTALL: Show all tools with search // ───────────────────────────────────────────────────────────────────────────── - const recommendedOptions = preferredIdes.map((ide) => { - const isConfigured = configuredPreferred.includes(ide.value); + const allTools = [...preferredIdes, ...otherIdes]; + + const allToolOptions = allTools.map((ide) => { + const isPreferred = preferredIdes.some((p) => p.value === ide.value); + let label = ide.name; + if (isPreferred) label += ' ⭐'; return { - label: isConfigured ? `${ide.name} ⭐ ✅` : `${ide.name} ⭐`, + label, value: ide.value, }; }); - // Add "browse all" option at the end if there are additional tools - if (otherIdes.length > 0) { - const totalTools = preferredIdes.length + otherIdes.length; - recommendedOptions.push({ - label: `→ Browse all supported tools (${totalTools} total)...`, - value: '__BROWSE_ALL__', - }); - } - - // Pre-select previously configured preferred tools - const recommendedInitialValues = configuredPreferred.length > 0 ? configuredPreferred : undefined; - - const recommendedSelected = await prompts.multiselect({ - message: `Integrate with ${chalk.dim('(↑/↓ to navigate • SPACE: select • ENTER: confirm)')}:`, - options: recommendedOptions, - initialValues: recommendedInitialValues, + const selectedIdes = await prompts.autocompleteMultiselect({ + message: 'Select tools:', + options: allToolOptions, + initialValues: configuredIdes.length > 0 ? configuredIdes : undefined, required: false, + maxItems: 8, }); - const selectedRecommended = recommendedSelected || []; - - // ───────────────────────────────────────────────────────────────────────────── - // STEP 2: Handle "Browse All" selection - show additional tools if requested - // ───────────────────────────────────────────────────────────────────────────── - const wantsBrowseAll = selectedRecommended.includes('__BROWSE_ALL__'); - const filteredRecommended = selectedRecommended.filter((v) => v !== '__BROWSE_ALL__'); - - // Show additional tools if: - // 1. User explicitly chose "Browse All", OR - // 2. User has previously configured "other" tools, OR - // 3. User selected no recommended tools (allow them to pick from other tools) - const showAdditionalTools = wantsBrowseAll || configuredOther.length > 0 || filteredRecommended.length === 0; - - let selectedAdditionalOrAll = []; - - if (showAdditionalTools) { - // Show ALL tools if: - // - User explicitly chose "Browse All", OR - // - User selected nothing from recommended (so they can pick from everything) - // Otherwise, show only "other" tools as additional options - const showAllTools = wantsBrowseAll || filteredRecommended.length === 0; - const toolsToShow = showAllTools ? [...preferredIdes, ...otherIdes] : otherIdes; - - if (toolsToShow.length > 0) { - const allToolOptions = toolsToShow.map((ide) => { - const isConfigured = configuredIdes.includes(ide.value); - const isPreferred = preferredIdes.some((p) => p.value === ide.value); - let label = ide.name; - if (isPreferred) label += ' ⭐'; - if (isConfigured) label += ' ✅'; - return { - label, - value: ide.value, - }; - }); - - // Pre-select: previously configured tools + any recommended tools already selected - const initialValues = [...configuredIdes, ...filteredRecommended].filter((v, i, arr) => arr.indexOf(v) === i); // dedupe - - // Use "additional" only if user already selected some recommended tools - const isAdditional = !wantsBrowseAll && filteredRecommended.length > 0; - - console.log(''); - const selected = await prompts.autocompleteMultiselect({ - message: isAdditional ? 'Select additional tools:' : 'Select tools:', - options: allToolOptions, - initialValues: initialValues.length > 0 ? initialValues : undefined, - required: false, - maxItems: 8, - }); - - selectedAdditionalOrAll = selected || []; - } - } - - // Combine selections: - // - If "Browse All" was used, the second prompt contains ALL selections - // - Otherwise, combine recommended + additional - const allSelectedIdes = wantsBrowseAll ? selectedAdditionalOrAll : [...filteredRecommended, ...selectedAdditionalOrAll]; + const allSelectedIdes = selectedIdes || []; // ───────────────────────────────────────────────────────────────────────────── // STEP 3: Confirm if no tools selected @@ -547,7 +481,6 @@ class UI { } // Display selected tools - const allTools = [...preferredIdes, ...otherIdes]; this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools); return { From 006a1f707e40647a5fa448acdd3549a44561c677 Mon Sep 17 00:00:00 2001 From: murat Date: Wed, 4 Feb 2026 07:16:51 -0600 Subject: [PATCH 04/38] fix: docs llms order, bmgd draft, tea link --- docs/bmgd/game-types.md | 1 + docs/bmgd/index.md | 2 +- docs/bmgd/quick-flow-workflows.md | 1 + docs/reference/agents.md | 22 ++++++++++++++++++++ docs/reference/commands.md | 34 +++++++++++++++++++++++++++++++ docs/reference/testing.md | 21 +++++++++++++++++++ src/bmm/agents/qa.agent.yaml | 2 +- tools/build-docs.js | 22 +++++++++++++++++++- 8 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 docs/reference/agents.md create mode 100644 docs/reference/commands.md create mode 100644 docs/reference/testing.md diff --git a/docs/bmgd/game-types.md b/docs/bmgd/game-types.md index ed0a7164c..2ea407218 100644 --- a/docs/bmgd/game-types.md +++ b/docs/bmgd/game-types.md @@ -1,5 +1,6 @@ --- title: "Game Types Reference" +draft: true --- BMGD supports 24 game type templates. Each adds genre-specific sections to your GDD. diff --git a/docs/bmgd/index.md b/docs/bmgd/index.md index 08f2f6078..bd1565c12 100644 --- a/docs/bmgd/index.md +++ b/docs/bmgd/index.md @@ -1,6 +1,7 @@ --- title: "BMGD Quick Guide" description: Quick reference for BMad Game Dev Studio +draft: true --- ![BMGD Logo](bmgd-logo.png) @@ -110,4 +111,3 @@ Each template provides genre-specific GDD sections, mechanics patterns, testing - [Game Types Guide](game-types.md) - [Quick-Flow Guide](quick-flow-workflows.md) - diff --git a/docs/bmgd/quick-flow-workflows.md b/docs/bmgd/quick-flow-workflows.md index b86cc7674..69ac7506a 100644 --- a/docs/bmgd/quick-flow-workflows.md +++ b/docs/bmgd/quick-flow-workflows.md @@ -1,5 +1,6 @@ --- title: "Quick Flow Workflows" +draft: true --- How to create tech specs and execute implementations with Quick Flow. diff --git a/docs/reference/agents.md b/docs/reference/agents.md new file mode 100644 index 000000000..4771dae37 --- /dev/null +++ b/docs/reference/agents.md @@ -0,0 +1,22 @@ +--- +title: Agents +--- + +This page lists the default BMM (Agile suite) agents that install with BMAD Method, along with their menu triggers and primary workflows. + +Notes: +- Triggers are the short menu codes (e.g., `CP`) and fuzzy matches shown in each agent menu. +- Slash commands are generated separately. See `docs/reference/commands.md` for the slash command list and where they are defined. +- QA (Quinn) is the lightweight test automation agent in BMM. The full Test Architect (TEA) lives in its own module. + +| Agent | Triggers | Primary workflows | +| --------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- | +| Analyst (Mary) | `BP`, `RS`, `CB`, `DP` | Brainstorm Project, Research, Create Brief, Document Project | +| Product Manager (John) | `CP`, `VP`, `EP`, `CE`, `IR`, `CC` | Create/Validate/Edit PRD, Create Epics and Stories, Implementation Readiness, Correct Course | +| Architect (Winston) | `CA`, `IR` | Create Architecture, Implementation Readiness | +| Scrum Master (Bob) | `SP`, `CS`, `ER`, `CC` | Sprint Planning, Create Story, Epic Retrospective, Correct Course | +| Developer (Amelia) | `DS`, `CR` | Dev Story, Code Review | +| QA Engineer (Quinn) | `qa` | Automate (generate tests for existing features) | +| Quick Flow Solo Dev (Barry) | `QS`, `QD`, `CR` | Quick Spec, Quick Dev, Code Review | +| UX Designer (Sally) | `CU` | Create UX Design | +| Technical Writer (Paige) | `DP`, `WD`, `US`, `MG`, `VD`, `EC` | Document Project, Write Document, Update Standards, Mermaid Generate, Validate Doc, Explain Concept | diff --git a/docs/reference/commands.md b/docs/reference/commands.md new file mode 100644 index 000000000..39b97f637 --- /dev/null +++ b/docs/reference/commands.md @@ -0,0 +1,34 @@ +--- +title: Commands +description: How BMAD commands are generated and where to find them. +--- + +# Commands + +BMAD slash commands are generated by the installer for your IDE and **reflect the modules you have installed**. +That means the authoritative list lives **in your project**, not in a static docs page. + +## How to Discover Commands (Recommended) + +- Type `/bmad` in your IDE and use autocomplete to browse agents/workflows. +- Run `/bmad-help` to get guided next steps and context-aware recommendations. + +## Where Commands Are Generated + +The installer writes command files into your project (example paths for Claude Code): + +- `.claude/commands/bmad//agents/` +- `.claude/commands/bmad//workflows/` + +These folders are the **canonical, project-specific command list**. + +## Common Commands + +- `/bmad-help` - Interactive help and next-step guidance +- `/bmad::agents:` - Load an agent (e.g. `/bmad:bmm:agents:dev`) +- `/bmad::workflows:` - Run a workflow (e.g. `/bmad:bmm:workflows:create-prd`) + +## Why This Page Is Short + +BMAD is modular, so the exact commands vary by install. +Use your IDE's autocomplete or the generated command folders above to see *everything* available. diff --git a/docs/reference/testing.md b/docs/reference/testing.md new file mode 100644 index 000000000..562526fb2 --- /dev/null +++ b/docs/reference/testing.md @@ -0,0 +1,21 @@ +--- +title: Testing Options +--- + +# Testing Options + +BMad provides a built-in QA agent for quick test automation and a separate Test Architect (TEA) module for advanced testing. + +## Built-in QA (Quinn) + +Use the built-in QA agent for fast, straightforward test coverage: + +- Trigger: `QA` or `bmad-bmm-qa-automate` +- Best for: small projects, quick coverage, standard patterns + +## Test Architect (TEA) Module + +TEA is a standalone module with advanced testing workflows (test design, ATDD, automate, review, trace, NFR assessment). + +- Documentation: +- Install: `npx bmad-method@alpha install` and select the TEA module diff --git a/src/bmm/agents/qa.agent.yaml b/src/bmm/agents/qa.agent.yaml index bad945cd4..07ca4022f 100644 --- a/src/bmm/agents/qa.agent.yaml +++ b/src/bmm/agents/qa.agent.yaml @@ -27,7 +27,7 @@ agent: - Focus on realistic user scenarios menu: - - trigger: qa + - trigger: QA or fuzzy match on qa-automate workflow: "{project-root}/_bmad/bmm/workflows/qa/automate/workflow.yaml" description: "[QA] Automate - Generate tests for existing features (simplified)" diff --git a/tools/build-docs.js b/tools/build-docs.js index 38bb379ed..dfb2c0a8e 100644 --- a/tools/build-docs.js +++ b/tools/build-docs.js @@ -38,6 +38,7 @@ const LLM_EXCLUDE_PATTERNS = [ 'faq', 'reference/glossary/', 'explanation/game-dev/', + 'bmgd/', // Note: Files/dirs starting with _ (like _STYLE_GUIDE.md, _archive/) are excluded in shouldExcludeFromLlm() ]; @@ -194,7 +195,7 @@ function generateLlmsFullTxt(docsDir, outputDir) { console.log(' → Generating llms-full.txt...'); const date = new Date().toISOString().split('T')[0]; - const files = getAllMarkdownFiles(docsDir); + const files = getAllMarkdownFiles(docsDir).sort(compareLlmDocs); const output = [ '# BMAD Method Documentation (Full)', @@ -236,6 +237,25 @@ function generateLlmsFullTxt(docsDir, outputDir) { ); } +function compareLlmDocs(a, b) { + const aKey = getLlmSortKey(a); + const bKey = getLlmSortKey(b); + + if (aKey !== bKey) return aKey - bKey; + return a.localeCompare(b); +} + +function getLlmSortKey(filePath) { + if (filePath === 'index.md') return 0; + if (filePath === 'downloads.md') return 1; + if (filePath.startsWith(`tutorials${path.sep}`) || filePath.startsWith('tutorials/')) return 2; + if (filePath.startsWith(`how-to${path.sep}`) || filePath.startsWith('how-to/')) return 3; + if (filePath.startsWith(`explanation${path.sep}`) || filePath.startsWith('explanation/')) return 4; + if (filePath.startsWith(`reference${path.sep}`) || filePath.startsWith('reference/')) return 5; + if (filePath.startsWith(`bmgd${path.sep}`) || filePath.startsWith('bmgd/')) return 6; + return 7; +} + /** * Collects all Markdown (.md) files under a directory and returns their paths relative to a base directory. * @param {string} dir - Directory to search for Markdown files. From 210bcc6a99bae63d8c2c8dbaab3b4bf18487881b Mon Sep 17 00:00:00 2001 From: murat Date: Wed, 4 Feb 2026 07:25:12 -0600 Subject: [PATCH 05/38] fix: docs llms order, bmgd draft, tea link2 --- docs/reference/agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/agents.md b/docs/reference/agents.md index 4771dae37..00a2da9d5 100644 --- a/docs/reference/agents.md +++ b/docs/reference/agents.md @@ -16,7 +16,7 @@ Notes: | Architect (Winston) | `CA`, `IR` | Create Architecture, Implementation Readiness | | Scrum Master (Bob) | `SP`, `CS`, `ER`, `CC` | Sprint Planning, Create Story, Epic Retrospective, Correct Course | | Developer (Amelia) | `DS`, `CR` | Dev Story, Code Review | -| QA Engineer (Quinn) | `qa` | Automate (generate tests for existing features) | +| QA Engineer (Quinn) | `QA` | Automate (generate tests for existing features) | | Quick Flow Solo Dev (Barry) | `QS`, `QD`, `CR` | Quick Spec, Quick Dev, Code Review | | UX Designer (Sally) | `CU` | Create UX Design | | Technical Writer (Paige) | `DP`, `WD`, `US`, `MG`, `VD`, `EC` | Document Project, Write Document, Update Standards, Mermaid Generate, Validate Doc, Explain Concept | From f699a3683f2f4d492754dc098770e22e9a972452 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 15:43:48 -0600 Subject: [PATCH 06/38] excorcise the deamons part 1 - remove dead uneeed artifacts and items no longer being supported beyond beta of the BMM or core - but could return later as a module --- .vscode/settings.json | 3 +- src/bmm/agents/dev.agent.yaml | 1 - src/bmm/module-help.csv | 14 +- .../code-review/workflow.yaml | 1 - .../correct-course/workflow.yaml | 2 - .../create-story/workflow.yaml | 2 - .../4-implementation/dev-story/workflow.yaml | 2 - .../retrospective/workflow.yaml | 1 - .../sprint-planning/workflow.yaml | 2 - .../sprint-status/workflow.yaml | 3 - .../workflows/document-project/workflow.yaml | 6 - .../_shared/excalidraw-library.json | 90 ---- .../_shared/excalidraw-templates.yaml | 127 ----- .../create-dataflow/checklist.md | 39 -- .../create-dataflow/instructions.md | 130 ----- .../create-dataflow/workflow.yaml | 27 - .../create-diagram/checklist.md | 43 -- .../create-diagram/instructions.md | 141 ----- .../create-diagram/workflow.yaml | 27 - .../create-flowchart/checklist.md | 49 -- .../create-flowchart/instructions.md | 241 --------- .../create-flowchart/workflow.yaml | 27 - .../create-wireframe/checklist.md | 38 -- .../create-wireframe/instructions.md | 133 ----- .../create-wireframe/workflow.yaml | 27 - src/core/resources/excalidraw/README.md | 160 ------ .../excalidraw/excalidraw-helpers.md | 127 ----- .../resources/excalidraw/library-loader.md | 50 -- .../excalidraw/validate-json-instructions.md | 79 --- src/core/tasks/editorial-review-prose.xml | 26 +- src/core/tasks/editorial-review-structure.xml | 56 +- src/core/tasks/help.md | 52 +- src/core/tasks/index-docs.xml | 2 +- src/core/tasks/review-adversarial-general.xml | 4 +- src/core/tasks/shard-doc.xml | 3 +- src/core/tasks/workflow.xml | 2 +- .../advanced-elicitation/workflow.xml | 2 +- .../installers/lib/core/manifest-generator.js | 20 +- tools/cli/installers/lib/ide/_base-ide.js | 32 +- .../ide/shared/task-tool-command-generator.js | 49 +- tools/flattener/aggregate.js | 76 --- tools/flattener/binary.js | 80 --- tools/flattener/discovery.js | 71 --- tools/flattener/files.js | 35 -- tools/flattener/ignoreRules.js | 172 ------- tools/flattener/main.js | 483 ------------------ tools/flattener/projectRoot.js | 201 -------- tools/flattener/prompts.js | 44 -- tools/flattener/stats.helpers.js | 368 ------------- tools/flattener/stats.js | 75 --- tools/flattener/test-matrix.js | 409 --------------- tools/flattener/xml.js | 82 --- tools/schema/agent.js | 1 - 53 files changed, 139 insertions(+), 3798 deletions(-) delete mode 100644 src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json delete mode 100644 src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md delete mode 100644 src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml delete mode 100644 src/core/resources/excalidraw/README.md delete mode 100644 src/core/resources/excalidraw/excalidraw-helpers.md delete mode 100644 src/core/resources/excalidraw/library-loader.md delete mode 100644 src/core/resources/excalidraw/validate-json-instructions.md delete mode 100644 tools/flattener/aggregate.js delete mode 100644 tools/flattener/binary.js delete mode 100644 tools/flattener/discovery.js delete mode 100644 tools/flattener/files.js delete mode 100644 tools/flattener/ignoreRules.js delete mode 100644 tools/flattener/main.js delete mode 100644 tools/flattener/projectRoot.js delete mode 100644 tools/flattener/prompts.js delete mode 100644 tools/flattener/stats.helpers.js delete mode 100644 tools/flattener/stats.js delete mode 100644 tools/flattener/test-matrix.js delete mode 100644 tools/flattener/xml.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a85c1f33..f28c7f5d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -58,8 +58,7 @@ "tmpl", "Trae", "Unsharded", - "VNET", - "webskip" + "VNET" ], "json.schemas": [ { diff --git a/src/bmm/agents/dev.agent.yaml b/src/bmm/agents/dev.agent.yaml index 404a108c5..d88166eda 100644 --- a/src/bmm/agents/dev.agent.yaml +++ b/src/bmm/agents/dev.agent.yaml @@ -1,7 +1,6 @@ # Dev Implementation Agent Definition (v6) agent: - webskip: true metadata: id: "_bmad/bmm/agents/dev.md" name: Amelia diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index 45eeb6ab3..968f1b594 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -4,15 +4,11 @@ bmm,anytime,Generate Project Context,GPC,,_bmad/bmm/workflows/generate-project-c bmm,anytime,Quick Spec,QS,,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps brownfield additions to well established patterns utilities without extensive planning",planning_artifacts,"tech spec", bmm,anytime,Quick Dev,QD,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,, bmm,anytime,Correct Course,CC,,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal", -bmm,anytime,Create Dataflow,CDF,,_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml,bmad-bmm-create-excalidraw-dataflow,false,ux-designer,Create Mode,"Create data flow diagrams (DFD) in Excalidraw format - can be called standalone or during any workflow to add visual documentation",planning_artifacts,"dataflow diagram", -bmm,anytime,Create Diagram,CED,,_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml,bmad-bmm-create-excalidraw-diagram,false,ux-designer,Create Mode,"Create system architecture diagrams ERDs UML diagrams or general technical diagrams in Excalidraw format - use anytime or call from architecture workflow to add visual documentation",planning_artifacts,"diagram", -bmm,anytime,Create Flowchart,CFC,,_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml,bmad-bmm-create-excalidraw-flowchart,false,ux-designer,Create Mode,"Create a flowchart visualization in Excalidraw format for processes pipelines or logic flows - use anytime or during architecture to add process documentation",planning_artifacts,"flowchart", -bmm,anytime,Create Wireframe,CEW,,_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml,bmad-bmm-create-excalidraw-wireframe,false,ux-designer,Create Mode,"Create website or app wireframes in Excalidraw format - use anytime standalone or call from UX workflow to add UI mockups",planning_artifacts,"wireframe", -bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-write-document,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document", -bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-update-standards,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards", -bmm,anytime,Mermaid Generate,MG,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-mermaid-generate,false,tech-writer,,"Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.",planning_artifacts,"mermaid diagram", -bmm,anytime,Validate Document,VD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-validate-document,false,tech-writer,,"Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.",planning_artifacts,"validation report", -bmm,anytime,Explain Concept,EC,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,bmad-bmm-explain-concept,false,tech-writer,,"Create clear technical explanations with examples and diagrams for complex concepts. Breaks down into digestible sections using task-oriented approach.",project_knowledge,"explanation", +bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document", +bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards", +bmm,anytime,Mermaid Generate,MG,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.",planning_artifacts,"mermaid diagram", +bmm,anytime,Validate Document,VD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.",planning_artifacts,"validation report", +bmm,anytime,Explain Concept,EC,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Create clear technical explanations with examples and diagrams for complex concepts. Breaks down into digestible sections using task-oriented approach.",project_knowledge,"explanation", bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session", bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents", bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project_knowledge","research documents", diff --git a/src/bmm/workflows/4-implementation/code-review/workflow.yaml b/src/bmm/workflows/4-implementation/code-review/workflow.yaml index 9e66b9325..dd75c60d1 100644 --- a/src/bmm/workflows/4-implementation/code-review/workflow.yaml +++ b/src/bmm/workflows/4-implementation/code-review/workflow.yaml @@ -47,5 +47,4 @@ input_file_patterns: sharded_single: "{planning_artifacts}/*epic*/epic-{{epic_num}}.md" load_strategy: "SELECTIVE_LOAD" -standalone: true web_bundle: false diff --git a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml index 70813514a..c85402eca 100644 --- a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +++ b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml @@ -55,6 +55,4 @@ validation: "{installed_path}/checklist.md" checklist: "{installed_path}/checklist.md" default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md" -standalone: true - web_bundle: false diff --git a/src/bmm/workflows/4-implementation/create-story/workflow.yaml b/src/bmm/workflows/4-implementation/create-story/workflow.yaml index 258794c7c..57716af54 100644 --- a/src/bmm/workflows/4-implementation/create-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/create-story/workflow.yaml @@ -56,6 +56,4 @@ input_file_patterns: sharded: "{planning_artifacts}/*epic*/*.md" load_strategy: "SELECTIVE_LOAD" # Only load needed epic -standalone: true - web_bundle: false diff --git a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml index d5824ee17..0646d20f4 100644 --- a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml @@ -22,6 +22,4 @@ implementation_artifacts: "{config_source}:implementation_artifacts" sprint_status: "{implementation_artifacts}/sprint-status.yaml" project_context: "**/project-context.md" -standalone: true - web_bundle: false diff --git a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml index 80d934b2c..7cbf9f662 100644 --- a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +++ b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml @@ -54,5 +54,4 @@ sprint_status_file: "{implementation_artifacts}/sprint-status.yaml" story_directory: "{implementation_artifacts}" retrospectives_folder: "{implementation_artifacts}" -standalone: true web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml index 25ccf5f72..7f8686cd5 100644 --- a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml @@ -50,6 +50,4 @@ input_file_patterns: # Output configuration default_output_file: "{status_file}" -standalone: true - web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml index 6f10a9a67..fe8b5ff35 100644 --- a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml @@ -29,8 +29,5 @@ input_file_patterns: whole: "{implementation_artifacts}/sprint-status.yaml" load_strategy: "FULL_LOAD" -# Standalone so IDE commands get generated -standalone: true - # No web bundle needed web_bundle: false diff --git a/src/bmm/workflows/document-project/workflow.yaml b/src/bmm/workflows/document-project/workflow.yaml index 536257b3d..cd9a09de5 100644 --- a/src/bmm/workflows/document-project/workflow.yaml +++ b/src/bmm/workflows/document-project/workflow.yaml @@ -21,10 +21,4 @@ validation: "{installed_path}/checklist.md" # Required data files - CRITICAL for project type detection and documentation requirements documentation_requirements_csv: "{installed_path}/documentation-requirements.csv" -# Output configuration - Multiple files generated in output folder -# Primary output: {output_folder}/project-documentation/ -# Additional files generated by sub-workflows based on project structure - -standalone: true - web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json b/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json deleted file mode 100644 index d18f94af3..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "type": "excalidrawlib", - "version": 2, - "library": [ - { - "id": "start-end-circle", - "status": "published", - "elements": [ - { - "type": "ellipse", - "width": 120, - "height": 60, - "strokeColor": "#1976d2", - "backgroundColor": "#e3f2fd", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "process-rectangle", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 160, - "height": 80, - "strokeColor": "#1976d2", - "backgroundColor": "#e3f2fd", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0, - "roundness": { - "type": 3, - "value": 8 - } - } - ] - }, - { - "id": "decision-diamond", - "status": "published", - "elements": [ - { - "type": "diamond", - "width": 140, - "height": 100, - "strokeColor": "#f57c00", - "backgroundColor": "#fff3e0", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "data-store", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 140, - "height": 80, - "strokeColor": "#388e3c", - "backgroundColor": "#e8f5e9", - "fillStyle": "solid", - "strokeWidth": 2, - "roughness": 0 - } - ] - }, - { - "id": "external-entity", - "status": "published", - "elements": [ - { - "type": "rectangle", - "width": 120, - "height": 80, - "strokeColor": "#7b1fa2", - "backgroundColor": "#f3e5f5", - "fillStyle": "solid", - "strokeWidth": 3, - "roughness": 0 - } - ] - } - ] -} diff --git a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml b/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml deleted file mode 100644 index 6fab2a3d7..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +++ /dev/null @@ -1,127 +0,0 @@ -flowchart: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 100 - horizontal: 180 - elements: - start: - type: ellipse - width: 120 - height: 60 - label: "Start" - process: - type: rectangle - width: 160 - height: 80 - roundness: 8 - decision: - type: diamond - width: 140 - height: 100 - end: - type: ellipse - width: 120 - height: 60 - label: "End" - -diagram: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 120 - horizontal: 200 - elements: - component: - type: rectangle - width: 180 - height: 100 - roundness: 8 - database: - type: rectangle - width: 140 - height: 80 - service: - type: rectangle - width: 160 - height: 90 - roundness: 12 - external: - type: rectangle - width: 140 - height: 80 - -wireframe: - viewport: - x: 0 - y: 0 - zoom: 0.8 - grid: - size: 20 - spacing: - vertical: 40 - horizontal: 40 - elements: - container: - type: rectangle - width: 800 - height: 600 - strokeStyle: solid - strokeWidth: 2 - header: - type: rectangle - width: 800 - height: 80 - button: - type: rectangle - width: 120 - height: 40 - roundness: 4 - input: - type: rectangle - width: 300 - height: 40 - roundness: 4 - text: - type: text - fontSize: 16 - -dataflow: - viewport: - x: 0 - y: 0 - zoom: 1 - grid: - size: 20 - spacing: - vertical: 120 - horizontal: 200 - elements: - process: - type: ellipse - width: 140 - height: 80 - label: "Process" - datastore: - type: rectangle - width: 140 - height: 80 - label: "Data Store" - external: - type: rectangle - width: 120 - height: 80 - strokeWidth: 3 - label: "External Entity" - dataflow: - type: arrow - strokeWidth: 2 - label: "Data Flow" diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md deleted file mode 100644 index 3c9463d5d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +++ /dev/null @@ -1,39 +0,0 @@ -# Create Data Flow Diagram - Validation Checklist - -## DFD Notation - -- [ ] Processes shown as circles/ellipses -- [ ] Data stores shown as parallel lines or rectangles -- [ ] External entities shown as rectangles -- [ ] Data flows shown as labeled arrows -- [ ] Follows standard DFD notation - -## Structure - -- [ ] All processes numbered correctly -- [ ] All data flows labeled with data names -- [ ] All data stores named appropriately -- [ ] External entities clearly identified - -## Completeness - -- [ ] All inputs and outputs accounted for -- [ ] No orphaned processes (unconnected) -- [ ] Data conservation maintained -- [ ] Level appropriate (context/level 0/level 1) - -## Layout - -- [ ] Logical flow direction (left to right, top to bottom) -- [ ] No crossing data flows where avoidable -- [ ] Balanced layout -- [ ] Grid alignment maintained - -## Technical Quality - -- [ ] All elements properly grouped -- [ ] Arrows have proper bindings -- [ ] Text readable and properly sized -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md deleted file mode 100644 index 30d32ed33..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +++ /dev/null @@ -1,130 +0,0 @@ -# Create Data Flow Diagram - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates data flow diagrams (DFD) in Excalidraw format. - - - - - Review user's request and extract: DFD level, processes, data stores, external entities - Skip to Step 4 - - - - Ask: "What level of DFD do you need?" - Present options: - 1. Context Diagram (Level 0) - Single process showing system boundaries - 2. Level 1 DFD - Major processes and data flows - 3. Level 2 DFD - Detailed sub-processes - 4. Custom - Specify your requirements - - WAIT for selection - - - - Ask: "Describe the processes, data stores, and external entities in your system" - WAIT for user description - Summarize what will be included and confirm with user - - - - Check for existing theme.json, ask to use if exists - - Ask: "Choose a DFD color scheme:" - Present numbered options: - 1. Standard DFD - - Process: #e3f2fd (light blue) - - Data Store: #e8f5e9 (light green) - - External Entity: #f3e5f5 (light purple) - - Border: #1976d2 (blue) - - 2. Colorful DFD - - Process: #fff9c4 (light yellow) - - Data Store: #c5e1a5 (light lime) - - External Entity: #ffccbc (light coral) - - Border: #f57c00 (orange) - - 3. Minimal DFD - - Process: #f5f5f5 (light gray) - - Data Store: #eeeeee (gray) - - External Entity: #e0e0e0 (medium gray) - - Border: #616161 (dark gray) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - - - - - List all processes with numbers (1.0, 2.0, etc.) - List all data stores (D1, D2, etc.) - List all external entities - Map all data flows with labels - Show planned structure, confirm with user - - - - Load {{templates}} and extract `dataflow` section - Load {{library}} - Load theme.json - Load {{helpers}} - - - - Follow standard DFD notation from {{helpers}} - - Build Order: - 1. External entities (rectangles, bold border) - 2. Processes (circles/ellipses with numbers) - 3. Data stores (parallel lines or rectangles) - 4. Data flows (labeled arrows) - - - DFD Rules: - - Processes: Numbered (1.0, 2.0), verb phrases - - Data stores: Named (D1, D2), noun phrases - - External entities: Named, noun phrases - - Data flows: Labeled with data names, arrows show direction - - No direct flow between external entities - - No direct flow between data stores - - - Layout: - - External entities at edges - - Processes in center - - Data stores between processes - - Minimize crossing flows - - Left-to-right or top-to-bottom flow - - - - - Verify DFD rules compliance - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user - - - - Validate against {{validation}} - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml deleted file mode 100644 index 2f01e6b51..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-dataflow -description: "Create data flow diagrams (DFD) in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/dataflow-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md deleted file mode 100644 index 61d216aea..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +++ /dev/null @@ -1,43 +0,0 @@ -# Create Diagram - Validation Checklist - -## Element Structure - -- [ ] All components with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent component -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment appropriate for diagram type - -## Layout and Alignment - -- [ ] All elements snapped to 20px grid -- [ ] Component spacing consistent (40px/60px) -- [ ] Hierarchical alignment maintained -- [ ] No overlapping elements - -## Connections - -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected components -- [ ] Arrow routing avoids overlaps -- [ ] Relationship types clearly indicated - -## Notation and Standards - -- [ ] Follows specified notation standard (UML/ERD/etc) -- [ ] Symbols used correctly -- [ ] Cardinality/multiplicity shown where needed -- [ ] Labels and annotations clear - -## Theme and Styling - -- [ ] Theme colors applied consistently -- [ ] Component types visually distinguishable -- [ ] Text is readable -- [ ] Professional appearance - -## Output Quality - -- [ ] Element count under 80 -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md deleted file mode 100644 index 407a76bf7..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +++ /dev/null @@ -1,141 +0,0 @@ -# Create Diagram - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates system architecture diagrams, ERDs, UML diagrams, or general technical diagrams in Excalidraw format. - - - - - Review user's request and extract: diagram type, components/entities, relationships, notation preferences - Skip to Step 5 - Only ask about missing info in Steps 1-2 - - - - Ask: "What type of technical diagram do you need?" - Present options: - 1. System Architecture - 2. Entity-Relationship Diagram (ERD) - 3. UML Class Diagram - 4. UML Sequence Diagram - 5. UML Use Case Diagram - 6. Network Diagram - 7. Other - - WAIT for selection - - - - Ask: "Describe the components/entities and their relationships" - Ask: "What notation standard? (Standard/Simplified/Strict UML-ERD)" - WAIT for user input - Summarize what will be included and confirm with user - - - - Check if theme.json exists at output location - Ask to use it, load if yes, else proceed to Step 4 - Proceed to Step 4 - - - - Ask: "Choose a color scheme for your diagram:" - Present numbered options: - 1. Professional - - Component: #e3f2fd (light blue) - - Database: #e8f5e9 (light green) - - Service: #fff3e0 (light orange) - - Border: #1976d2 (blue) - - 2. Colorful - - Component: #e1bee7 (light purple) - - Database: #c5e1a5 (light lime) - - Service: #ffccbc (light coral) - - Border: #7b1fa2 (purple) - - 3. Minimal - - Component: #f5f5f5 (light gray) - - Database: #eeeeee (gray) - - Service: #e0e0e0 (medium gray) - - Border: #616161 (dark gray) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - Show preview and confirm - - - - List all components/entities - Map all relationships - Show planned layout - Ask: "Structure looks correct? (yes/no)" - Adjust and repeat - - - - Load {{templates}} and extract `diagram` section - Load {{library}} - Load theme.json and merge with template - Load {{helpers}} for guidelines - - - - Follow {{helpers}} for proper element creation - - For Each Component: - - Generate unique IDs (component-id, text-id, group-id) - - Create shape with groupIds - - Calculate text width - - Create text with containerId and matching groupIds - - Add boundElements - - - For Each Connection: - - Determine arrow type (straight/elbow) - - Create with startBinding and endBinding - - Update boundElements on both components - - - Build Order by Type: - - Architecture: Services → Databases → Connections → Labels - - ERD: Entities → Attributes → Relationships → Cardinality - - UML Class: Classes → Attributes → Methods → Relationships - - UML Sequence: Actors → Lifelines → Messages → Returns - - UML Use Case: Actors → Use Cases → Relationships - - - Alignment: - - Snap to 20px grid - - Space: 40px between components, 60px between sections - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm: "Diagram created at {{default_output_file}}. Open to view?" - - - - Validate against {{validation}} using {_bmad}/core/tasks/validate-workflow.xml - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml deleted file mode 100644 index f841a546f..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-diagram -description: "Create system architecture diagrams, ERDs, UML diagrams, or general technical diagrams in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-diagram" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/diagram-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md deleted file mode 100644 index 7da7fb78d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +++ /dev/null @@ -1,49 +0,0 @@ -# Create Flowchart - Validation Checklist - -## Element Structure - -- [ ] All shapes with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent shape -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment set (`textAlign` + `verticalAlign`) - -## Layout and Alignment - -- [ ] All elements snapped to 20px grid -- [ ] Consistent spacing between elements (60px minimum) -- [ ] Vertical alignment maintained for flow direction -- [ ] No overlapping elements - -## Connections - -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected shapes -- [ ] Arrow types appropriate (straight for forward, elbow for backward/upward) -- [ ] Gap set to 10 for all bindings - -## Theme and Styling - -- [ ] Theme colors applied consistently -- [ ] All shapes use theme primary fill color -- [ ] All borders use theme accent color -- [ ] Text color is readable (#1e1e1e) - -## Composition - -- [ ] Element count under 50 -- [ ] Library components referenced where possible -- [ ] No duplicate element definitions - -## Output Quality - -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location - -## Functional Requirements - -- [ ] Start point clearly marked -- [ ] End point clearly marked -- [ ] All process steps labeled -- [ ] Decision points use diamond shapes -- [ ] Flow direction is clear and logical diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md deleted file mode 100644 index 742679050..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +++ /dev/null @@ -1,241 +0,0 @@ -# Create Flowchart - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates a flowchart visualization in Excalidraw format for processes, pipelines, or logic flows. - - - - - Before asking any questions, analyze what the user has already told you - - Review the user's initial request and conversation history - Extract any mentioned: flowchart type, complexity, decision points, save location - - - Summarize your understanding - Skip directly to Step 4 (Plan Flowchart Layout) - - - - Note what you already know - Only ask about missing information in Step 1 - - - - Proceed with full elicitation in Step 1 - - - - - Ask Question 1: "What type of process flow do you need to visualize?" - Present numbered options: - 1. Business Process Flow - Document business workflows, approval processes, or operational procedures - 2. Algorithm/Logic Flow - Visualize code logic, decision trees, or computational processes - 3. User Journey Flow - Map user interactions, navigation paths, or experience flows - 4. Data Processing Pipeline - Show data transformation, ETL processes, or processing stages - 5. Other - Describe your specific flowchart needs - - WAIT for user selection (1-5) - - Ask Question 2: "How many main steps are in this flow?" - Present numbered options: - 1. Simple (3-5 steps) - Quick process with few decision points - 2. Medium (6-10 steps) - Standard workflow with some branching - 3. Complex (11-20 steps) - Detailed process with multiple decision points - 4. Very Complex (20+ steps) - Comprehensive workflow requiring careful layout - - WAIT for user selection (1-4) - Store selection in {{complexity}} - - Ask Question 3: "Does your flow include decision points (yes/no branches)?" - Present numbered options: - 1. No decisions - Linear flow from start to end - 2. Few decisions (1-2) - Simple branching with yes/no paths - 3. Multiple decisions (3-5) - Several conditional branches - 4. Complex decisions (6+) - Extensive branching logic - - WAIT for user selection (1-4) - Store selection in {{decision_points}} - - Ask Question 4: "Where should the flowchart be saved?" - Present numbered options: - 1. Default location - docs/flowcharts/[auto-generated-name].excalidraw - 2. Custom path - Specify your own file path - 3. Project root - Save in main project directory - 4. Specific folder - Choose from existing folders - - WAIT for user selection (1-4) - - Ask for specific path - WAIT for user input - - Store final path in {{default_output_file}} - - - - Check if theme.json exists at output location - - Ask: "Found existing theme. Use it? (yes/no)" - WAIT for user response - - Load and use existing theme - Skip to Step 4 - - - Proceed to Step 3 - - - - Proceed to Step 3 - - - - - Ask: "Let's create a theme for your flowchart. Choose a color scheme:" - Present numbered options: - 1. Professional Blue - - Primary Fill: #e3f2fd (light blue) - - Accent/Border: #1976d2 (blue) - - Decision: #fff3e0 (light orange) - - Text: #1e1e1e (dark gray) - - 2. Success Green - - Primary Fill: #e8f5e9 (light green) - - Accent/Border: #388e3c (green) - - Decision: #fff9c4 (light yellow) - - Text: #1e1e1e (dark gray) - - 3. Neutral Gray - - Primary Fill: #f5f5f5 (light gray) - - Accent/Border: #616161 (gray) - - Decision: #e0e0e0 (medium gray) - - Text: #1e1e1e (dark gray) - - 4. Warm Orange - - Primary Fill: #fff3e0 (light orange) - - Accent/Border: #f57c00 (orange) - - Decision: #ffe0b2 (peach) - - Text: #1e1e1e (dark gray) - - 5. Custom Colors - Define your own color palette - - WAIT for user selection (1-5) - Store selection in {{theme_choice}} - - - Ask: "Primary fill color (hex code)?" - WAIT for user input - Store in {{custom_colors.primary_fill}} - Ask: "Accent/border color (hex code)?" - WAIT for user input - Store in {{custom_colors.accent}} - Ask: "Decision color (hex code)?" - WAIT for user input - Store in {{custom_colors.decision}} - - - Create theme.json with selected colors - Show theme preview with all colors - Ask: "Theme looks good?" - Present numbered options: - 1. Yes, use this theme - Proceed with theme - 2. No, adjust colors - Modify color selections - 3. Start over - Choose different preset - - WAIT for selection (1-3) - - Repeat Step 3 - - - - - List all steps and decision points based on gathered requirements - Show user the planned structure - Ask: "Structure looks correct? (yes/no)" - WAIT for user response - - Adjust structure based on feedback - Repeat this step - - - - - Load {{templates}} file - Extract `flowchart` section from YAML - Load {{library}} file - Load theme.json and merge colors with template - Load {{helpers}} for element creation guidelines - - - - Follow guidelines from {{helpers}} for proper element creation - - Build ONE section at a time following these rules: - - For Each Shape with Label: - 1. Generate unique IDs (shape-id, text-id, group-id) - 2. Create shape with groupIds: [group-id] - 3. Calculate text width: (text.length × fontSize × 0.6) + 20, round to nearest 10 - 4. Create text element with: - - containerId: shape-id - - groupIds: [group-id] (SAME as shape) - - textAlign: "center" - - verticalAlign: "middle" - - width: calculated width - 5. Add boundElements to shape referencing text - - - For Each Arrow: - 1. Determine arrow type needed: - - Straight: For forward flow (left-to-right, top-to-bottom) - - Elbow: For upward flow, backward flow, or complex routing - 2. Create arrow with startBinding and endBinding - 3. Set startBinding.elementId to source shape ID - 4. Set endBinding.elementId to target shape ID - 5. Set gap: 10 for both bindings - 6. If elbow arrow, add intermediate points for direction changes - 7. Update boundElements on both connected shapes - - - Alignment: - - Snap all x, y to 20px grid - - Align shapes vertically (same x for vertical flow) - - Space elements: 60px between shapes - - - Build Order: - 1. Start point (circle) with label - 2. Each process step (rectangle) with label - 3. Each decision point (diamond) with label - 4. End point (circle) with label - 5. Connect all with bound arrows - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user: "Flowchart created at {{default_output_file}}. Open to view?" - - - - Validate against checklist at {{validation}} using {_bmad}/core/tasks/validate-workflow.xml - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml deleted file mode 100644 index 6079d6de2..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-flowchart -description: "Create a flowchart visualization in Excalidraw format for processes, pipelines, or logic flows" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/flowchart-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md deleted file mode 100644 index 3e2b26f41..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +++ /dev/null @@ -1,38 +0,0 @@ -# Create Wireframe - Validation Checklist - -## Layout Structure - -- [ ] Screen dimensions appropriate for device type -- [ ] Grid alignment (20px) maintained -- [ ] Consistent spacing between UI elements -- [ ] Proper hierarchy (header, content, footer) - -## UI Elements - -- [ ] All interactive elements clearly marked -- [ ] Buttons, inputs, and controls properly sized -- [ ] Text labels readable and appropriately sized -- [ ] Navigation elements clearly indicated - -## Fidelity - -- [ ] Matches requested fidelity level (low/medium/high) -- [ ] Appropriate level of detail -- [ ] Placeholder content used where needed -- [ ] No unnecessary decoration for low-fidelity - -## Annotations - -- [ ] Key interactions annotated -- [ ] Flow indicators present if multi-screen -- [ ] Important notes included -- [ ] Element purposes clear - -## Technical Quality - -- [ ] All elements properly grouped -- [ ] Text elements have containerId -- [ ] Snapped to grid -- [ ] No elements with `isDeleted: true` -- [ ] JSON is valid -- [ ] File saved to correct location diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md deleted file mode 100644 index dc9506b0d..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +++ /dev/null @@ -1,133 +0,0 @@ -# Create Wireframe - Workflow Instructions - -```xml -The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml -You MUST have already loaded and processed: {installed_path}/workflow.yaml -This workflow creates website or app wireframes in Excalidraw format. - - - - - Review user's request and extract: wireframe type, fidelity level, screen count, device type, save location - Skip to Step 5 - - - - Ask: "What type of wireframe do you need?" - Present options: - 1. Website (Desktop) - 2. Mobile App (iOS/Android) - 3. Web App (Responsive) - 4. Tablet App - 5. Multi-platform - - WAIT for selection - - - - Ask fidelity level (Low/Medium/High) - Ask screen count (Single/Few 2-3/Multiple 4-6/Many 7+) - Ask device dimensions or use standard - Ask save location - - - - Check for existing theme.json, ask to use if exists - - - - Ask: "Choose a wireframe style:" - Present numbered options: - 1. Classic Wireframe - - Background: #ffffff (white) - - Container: #f5f5f5 (light gray) - - Border: #9e9e9e (gray) - - Text: #424242 (dark gray) - - 2. High Contrast - - Background: #ffffff (white) - - Container: #eeeeee (light gray) - - Border: #212121 (black) - - Text: #000000 (black) - - 3. Blueprint Style - - Background: #1a237e (dark blue) - - Container: #3949ab (blue) - - Border: #7986cb (light blue) - - Text: #ffffff (white) - - 4. Custom - Define your own colors - - WAIT for selection - Create theme.json based on selection - Confirm with user - - - - List all screens and their purposes - Map navigation flow between screens - Identify key UI elements for each screen - Show planned structure, confirm with user - - - - Load {{templates}} and extract `wireframe` section - Load {{library}} - Load theme.json - Load {{helpers}} - - - - Follow {{helpers}} for proper element creation - - For Each Screen: - - Create container/frame - - Add header section - - Add content areas - - Add navigation elements - - Add interactive elements (buttons, inputs) - - Add labels and annotations - - - Build Order: - 1. Screen containers - 2. Layout sections (header, content, footer) - 3. Navigation elements - 4. Content blocks - 5. Interactive elements - 6. Labels and annotations - 7. Flow indicators (if multi-screen) - - - Fidelity Guidelines: - - Low: Basic shapes, minimal detail, placeholder text - - Medium: More defined elements, some styling, representative content - - High: Detailed elements, realistic sizing, actual content examples - - - - - Strip unused elements and elements with isDeleted: true - Save to {{default_output_file}} - - - - NEVER delete the file if validation fails - always fix syntax errors - Run: node -e "JSON.parse(require('fs').readFileSync('{{default_output_file}}', 'utf8')); console.log('✓ Valid JSON')" - - Read the error message carefully - it shows the syntax error and position - Open the file and navigate to the error location - Fix the syntax error (add missing comma, bracket, or quote as indicated) - Save the file - Re-run validation with the same command - Repeat until validation passes - - Once validation passes, confirm with user - - - - Validate against {{validation}} - - - -``` diff --git a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml b/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml deleted file mode 100644 index d89005a75..000000000 --- a/src/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: create-excalidraw-wireframe -description: "Create website or app wireframes in Excalidraw format" -author: "BMad" - -# Config values -config_source: "{project-root}/_bmad/bmm/config.yaml" -output_folder: "{config_source}:output_folder" - -# Workflow components -installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe" -shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared" -instructions: "{installed_path}/instructions.md" -validation: "{installed_path}/checklist.md" - -# Core Excalidraw resources (universal knowledge) -helpers: "{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md" -json_validation: "{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md" - -# Domain-specific resources (technical diagrams) -templates: "{shared_path}/excalidraw-templates.yaml" -library: "{shared_path}/excalidraw-library.json" - -# Output file (respects user's configured output_folder) -default_output_file: "{output_folder}/excalidraw-diagrams/wireframe-{timestamp}.excalidraw" - -standalone: true -web_bundle: false diff --git a/src/core/resources/excalidraw/README.md b/src/core/resources/excalidraw/README.md deleted file mode 100644 index c3840bea3..000000000 --- a/src/core/resources/excalidraw/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Core Excalidraw Resources - -Universal knowledge for creating Excalidraw diagrams. All agents that create Excalidraw files should reference these resources. - -## Purpose - -Provides the **HOW** (universal knowledge) while agents provide the **WHAT** (domain-specific application). - -**Core = "How to create Excalidraw elements"** - -- How to group shapes with text labels -- How to calculate text width -- How to create arrows with proper bindings -- How to validate JSON syntax -- Base structure and primitives - -**Agents = "What diagrams to create"** - -- Frame Expert (BMM): Technical flowcharts, architecture diagrams, wireframes -- Presentation Master (CIS): Pitch decks, creative visuals, Rube Goldberg machines -- Tech Writer (BMM): Documentation diagrams, concept explanations - -## Files in This Directory - -### excalidraw-helpers.md - -**Universal element creation patterns** - -- Text width calculation -- Element grouping rules (shapes + labels) -- Grid alignment -- Arrow creation (straight, elbow) -- Theme application -- Validation checklist -- Optimization rules - -**Agents reference this to:** - -- Create properly grouped shapes -- Calculate text dimensions -- Connect elements with arrows -- Ensure valid structure - -### validate-json-instructions.md - -**Universal JSON validation process** - -- How to validate Excalidraw JSON -- Common errors and fixes -- Workflow integration -- Error recovery - -**Agents reference this to:** - -- Validate files after creation -- Fix syntax errors -- Ensure files can be opened in Excalidraw - -### library-loader.md (Future) - -**How to load external .excalidrawlib files** - -- Programmatic library loading -- Community library integration -- Custom library management - -**Status:** To be developed when implementing external library support. - -## How Agents Use These Resources - -### Example: Frame Expert (Technical Diagrams) - -```yaml -# workflows/excalidraw-diagrams/create-flowchart/workflow.yaml -helpers: '{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md' -json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md' -``` - -**Domain-specific additions:** - -```yaml -# workflows/excalidraw-diagrams/_shared/flowchart-templates.yaml -flowchart: - start_node: - type: ellipse - width: 120 - height: 60 - process_box: - type: rectangle - width: 160 - height: 80 - decision_diamond: - type: diamond - width: 140 - height: 100 -``` - -### Example: Presentation Master (Creative Visuals) - -```yaml -# workflows/create-visual-metaphor/workflow.yaml -helpers: '{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md' -json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md' -``` - -**Domain-specific additions:** - -```yaml -# workflows/_shared/creative-templates.yaml -rube_goldberg: - whimsical_connector: - type: arrow - strokeStyle: dashed - roughness: 2 - playful_box: - type: rectangle - roundness: 12 -``` - -## What Doesn't Belong in Core - -**Domain-Specific Elements:** - -- Flowchart-specific templates (belongs in Frame Expert) -- Pitch deck layouts (belongs in Presentation Master) -- Documentation-specific styles (belongs in Tech Writer) - -**Agent Workflows:** - -- How to create a flowchart (Frame Expert workflow) -- How to create a pitch deck (Presentation Master workflow) -- Step-by-step diagram creation (agent-specific) - -**Theming:** - -- Currently in agent workflows -- **Future:** Will be refactored to core as user-configurable themes - -## Architecture Principle - -**Single Source of Truth:** - -- Core holds universal knowledge -- Agents reference core, don't duplicate -- Updates to core benefit all agents -- Agents specialize with domain knowledge - -**DRY (Don't Repeat Yourself):** - -- Element creation logic: ONCE in core -- Text width calculation: ONCE in core -- Validation process: ONCE in core -- Arrow binding patterns: ONCE in core - -## Future Enhancements - -1. **External Library Loader** - Load .excalidrawlib files from libraries.excalidraw.com -2. **Theme Management** - User-configurable color themes saved in core -3. **Component Library** - Shared reusable components across agents -4. **Layout Algorithms** - Auto-layout helpers for positioning elements diff --git a/src/core/resources/excalidraw/excalidraw-helpers.md b/src/core/resources/excalidraw/excalidraw-helpers.md deleted file mode 100644 index 362646800..000000000 --- a/src/core/resources/excalidraw/excalidraw-helpers.md +++ /dev/null @@ -1,127 +0,0 @@ -# Excalidraw Element Creation Guidelines - -## Text Width Calculation - -For text elements inside shapes (labels): - -``` -text_width = (text.length × fontSize × 0.6) + 20 -``` - -Round to nearest 10 for grid alignment. - -## Element Grouping Rules - -**CRITICAL:** When creating shapes with labels: - -1. Generate unique IDs: - - `shape-id` for the shape - - `text-id` for the text - - `group-id` for the group - -2. Shape element must have: - - `groupIds: [group-id]` - - `boundElements: [{type: "text", id: text-id}]` - -3. Text element must have: - - `containerId: shape-id` - - `groupIds: [group-id]` (SAME as shape) - - `textAlign: "center"` - - `verticalAlign: "middle"` - - `width: calculated_width` - -## Grid Alignment - -- Snap all `x`, `y` coordinates to 20px grid -- Formula: `Math.round(value / 20) * 20` -- Spacing between elements: 60px minimum - -## Arrow Creation - -### Straight Arrows - -Use for forward flow (left-to-right, top-to-bottom): - -```json -{ - "type": "arrow", - "startBinding": { - "elementId": "source-shape-id", - "focus": 0, - "gap": 10 - }, - "endBinding": { - "elementId": "target-shape-id", - "focus": 0, - "gap": 10 - }, - "points": [[0, 0], [distance_x, distance_y]] -} -``` - -### Elbow Arrows - -Use for upward flow, backward flow, or complex routing: - -```json -{ - "type": "arrow", - "startBinding": {...}, - "endBinding": {...}, - "points": [ - [0, 0], - [intermediate_x, 0], - [intermediate_x, intermediate_y], - [final_x, final_y] - ], - "elbowed": true -} -``` - -### Update Connected Shapes - -After creating arrow, update `boundElements` on both connected shapes: - -```json -{ - "id": "shape-id", - "boundElements": [ - { "type": "text", "id": "text-id" }, - { "type": "arrow", "id": "arrow-id" } - ] -} -``` - -## Theme Application - -Theme colors should be applied consistently: - -- **Shapes**: `backgroundColor` from theme primary fill -- **Borders**: `strokeColor` from theme accent -- **Text**: `strokeColor` = "#1e1e1e" (dark text) -- **Arrows**: `strokeColor` from theme accent - -## Validation Checklist - -Before saving, verify: - -- [ ] All shapes with labels have matching `groupIds` -- [ ] All text elements have `containerId` pointing to parent shape -- [ ] Text width calculated properly (no cutoff) -- [ ] Text alignment set (`textAlign` + `verticalAlign`) -- [ ] All elements snapped to 20px grid -- [ ] All arrows have `startBinding` and `endBinding` -- [ ] `boundElements` array updated on connected shapes -- [ ] Theme colors applied consistently -- [ ] No metadata or history in final output -- [ ] All IDs are unique - -## Optimization - -Remove from final output: - -- `appState` object -- `files` object (unless images used) -- All elements with `isDeleted: true` -- Unused library items -- Version history diff --git a/src/core/resources/excalidraw/library-loader.md b/src/core/resources/excalidraw/library-loader.md deleted file mode 100644 index 6fe5ea070..000000000 --- a/src/core/resources/excalidraw/library-loader.md +++ /dev/null @@ -1,50 +0,0 @@ -# External Library Loader - -**Status:** Placeholder for future implementation - -## Purpose - -Load external .excalidrawlib files from or custom sources. - -## Planned Capabilities - -- Load libraries by URL -- Load libraries from local files -- Merge multiple libraries -- Filter library components -- Cache loaded libraries - -## API Reference - -Will document how to use: - -- `importLibrary(url)` - Load library from URL -- `loadSceneOrLibraryFromBlob()` - Load from file -- `mergeLibraryItems()` - Combine libraries - -## Usage Example - -```yaml -# Future workflow.yaml structure -libraries: - - url: 'https://libraries.excalidraw.com/libraries/...' - filter: ['aws', 'cloud'] - - path: '{project-root}/_data/custom-library.excalidrawlib' -``` - -## Implementation Notes - -This will be developed when agents need to leverage the extensive library ecosystem available at . - -Hundreds of pre-built component libraries exist for: - -- AWS/Cloud icons -- UI/UX components -- Business diagrams -- Mind map shapes -- Floor plans -- And much more... - -## User Configuration - -Future: Users will be able to configure favorite libraries in their BMAD config for automatic loading. diff --git a/src/core/resources/excalidraw/validate-json-instructions.md b/src/core/resources/excalidraw/validate-json-instructions.md deleted file mode 100644 index 3abf3fc36..000000000 --- a/src/core/resources/excalidraw/validate-json-instructions.md +++ /dev/null @@ -1,79 +0,0 @@ -# JSON Validation Instructions - -## Purpose - -Validate Excalidraw JSON files after saving to catch syntax errors (missing commas, brackets, quotes). - -## How to Validate - -Use Node.js built-in JSON parsing to validate the file: - -```bash -node -e "JSON.parse(require('fs').readFileSync('FILE_PATH', 'utf8')); console.log('✓ Valid JSON')" -``` - -Replace `FILE_PATH` with the actual file path. - -## Exit Codes - -- Exit code 0 = Valid JSON -- Exit code 1 = Invalid JSON (syntax error) - -## Error Output - -If invalid, Node.js will output: - -- Error message with description -- Position in file where error occurred -- Line and column information (if available) - -## Common Errors and Fixes - -### Missing Comma - -``` -SyntaxError: Expected ',' or '}' after property value -``` - -**Fix:** Add comma after the property value - -### Missing Bracket/Brace - -``` -SyntaxError: Unexpected end of JSON input -``` - -**Fix:** Add missing closing bracket `]` or brace `}` - -### Extra Comma (Trailing) - -``` -SyntaxError: Unexpected token , -``` - -**Fix:** Remove the trailing comma before `]` or `}` - -### Missing Quote - -``` -SyntaxError: Unexpected token -``` - -**Fix:** Add missing quote around string value - -## Workflow Integration - -After saving an Excalidraw file, run validation: - -1. Save the file -2. Run: `node -e "JSON.parse(require('fs').readFileSync('{{save_location}}', 'utf8')); console.log('✓ Valid JSON')"` -3. If validation fails: - - Read the error message for line/position - - Open the file at that location - - Fix the syntax error - - Save and re-validate -4. Repeat until validation passes - -## Critical Rule - -**NEVER delete the file due to validation errors - always fix the syntax error at the reported location.** diff --git a/src/core/tasks/editorial-review-prose.xml b/src/core/tasks/editorial-review-prose.xml index 7ef28f904..deb53570e 100644 --- a/src/core/tasks/editorial-review-prose.xml +++ b/src/core/tasks/editorial-review-prose.xml @@ -1,7 +1,6 @@ + description="Clinical copy-editor that reviews text for communication issues"> Review text for communication issues that impede comprehension and output suggested fixes in a three-column table @@ -10,7 +9,7 @@ + is the final authority on tone, structure, and language choices." /> @@ -62,7 +61,8 @@ - Consult style_guide now and note its key requirements—these override default principles for this review + Consult style_guide now and note its key requirements—these override default principles for this + review Review all prose sections (skip code blocks, frontmatter, structural markup) Identify communication issues that impede comprehension For each issue, determine the minimal fix that achieves clarity @@ -77,16 +77,18 @@ Output: "No editorial issues identified" -| Original Text | Revised Text | Changes | -|---------------|--------------|---------| -| The exact original passage | The suggested revision | Brief explanation of what changed and why | + | Original Text | Revised Text | Changes | + |---------------|--------------|---------| + | The exact original passage | The suggested revision | Brief explanation of what changed and why | -| Original Text | Revised Text | Changes | -|---------------|--------------|---------| -| The system will processes data and it handles errors. | The system processes data and handles errors. | Fixed subject-verb agreement ("will processes" to "processes"); removed redundant "it" | -| Users can chose from options (lines 12, 45, 78) | Users can choose from options | Fixed spelling: "chose" to "choose" (appears in 3 locations) | + | Original Text | Revised Text | Changes | + |---------------|--------------|---------| + | The system will processes data and it handles errors. | The system processes data and handles errors. | Fixed subject-verb + agreement ("will processes" to "processes"); removed redundant "it" | + | Users can chose from options (lines 12, 45, 78) | Users can choose from options | Fixed spelling: "chose" to "choose" (appears in + 3 locations) | @@ -97,4 +99,4 @@ If no issues found after thorough review, output "No editorial issues identified" (this is valid completion, not an error) - + \ No newline at end of file diff --git a/src/core/tasks/editorial-review-structure.xml b/src/core/tasks/editorial-review-structure.xml index aac169ee1..426dc3c8c 100644 --- a/src/core/tasks/editorial-review-structure.xml +++ b/src/core/tasks/editorial-review-structure.xml @@ -4,29 +4,28 @@ + and simplification while preserving comprehension"> Review document structure and propose substantive changes to improve clarity and flow-run this BEFORE copy editing + desc="Document to review (markdown, plain text, or structured content)" /> + is the final authority on tone, structure, and language choices." /> + 'API reference', 'conceptual overview')" /> + 'decision makers')" /> + 'llm' optimizes for precision and density" /> + 'no limit')" /> MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER @@ -69,7 +68,7 @@ Cut emotional language, encouragement, and orientation sections IF concept is well-known from training (e.g., "conventional - commits", "REST APIs"): Reference the standard-don't re-teach it + commits", "REST APIs"): Reference the standard-don't re-teach it ELSE: Be explicit-don't assume the LLM will infer correctly Use consistent terminology-same word for same concept throughout @@ -132,7 +131,8 @@ Note reader_type and which principles apply (human-reader-principles or llm-reader-principles) - Consult style_guide now and note its key requirements—these override default principles for this analysis + Consult style_guide now and note its key requirements—these override default principles for this + analysis Map the document structure: list each major section with its word count Evaluate structure against the selected model's primary rules (e.g., 'Does recommendation come first?' for Pyramid) @@ -176,27 +176,27 @@ Output estimated total reduction if all recommendations accepted Output: "No substantive changes recommended-document structure is sound" -## Document Summary -- **Purpose:** [inferred or provided purpose] -- **Audience:** [inferred or provided audience] -- **Reader type:** [selected reader type] -- **Structure model:** [selected structure model] -- **Current length:** [X] words across [Y] sections + ## Document Summary + - **Purpose:** [inferred or provided purpose] + - **Audience:** [inferred or provided audience] + - **Reader type:** [selected reader type] + - **Structure model:** [selected structure model] + - **Current length:** [X] words across [Y] sections -## Recommendations + ## Recommendations -### 1. [CUT/MERGE/MOVE/CONDENSE/QUESTION/PRESERVE] - [Section or element name] -**Rationale:** [One sentence explanation] -**Impact:** ~[X] words -**Comprehension note:** [If applicable, note impact on reader understanding] + ### 1. [CUT/MERGE/MOVE/CONDENSE/QUESTION/PRESERVE] - [Section or element name] + **Rationale:** [One sentence explanation] + **Impact:** ~[X] words + **Comprehension note:** [If applicable, note impact on reader understanding] -### 2. ... + ### 2. ... -## Summary -- **Total recommendations:** [N] -- **Estimated reduction:** [X] words ([Y]% of original) -- **Meets length target:** [Yes/No/No target specified] -- **Comprehension trade-offs:** [Note any cuts that sacrifice reader engagement for brevity] + ## Summary + - **Total recommendations:** [N] + - **Estimated reduction:** [X] words ([Y]% of original) + - **Meets length target:** [Yes/No/No target specified] + - **Comprehension trade-offs:** [Note any cuts that sacrifice reader engagement for brevity] @@ -206,4 +206,4 @@ If no structural issues found, output "No substantive changes recommended" (this is valid completion, not an error) - + \ No newline at end of file diff --git a/src/core/tasks/help.md b/src/core/tasks/help.md index 3df95fd56..4e060ea1f 100644 --- a/src/core/tasks/help.md +++ b/src/core/tasks/help.md @@ -1,12 +1,11 @@ --- name: help description: Get unstuck by showing what workflow steps come next or answering questions about what to do -standalone: true --- # Task: BMAD Help -## KEY RULES +## ROUTING RULES - **Empty `phase` = anytime** — Universal tools work regardless of workflow state - **Numbered phases indicate sequence** — Phases like `1-discover` → `2-define` → `3-build` → `4-ship` flow in order (naming varies by module) @@ -15,6 +14,26 @@ standalone: true - **`required=true` blocks progress** — Required workflows must complete before proceeding to later phases - **Artifacts reveal completion** — Search resolved output paths for `outputs` patterns, fuzzy-match found files to workflow rows +## DISPLAY RULES + +### Command-Based Workflows +When `command` field has a value: +- Show the command prefixed with `/` (e.g., `/bmad-bmm-create-prd`) + +### Agent-Based Workflows +When `command` field is empty: +- User loads agent first via `/agent-command` +- Then invokes by referencing the `code` field or describing the `name` field +- Do NOT show a slash command — show the code value and agent load instruction instead + +Example presentation for empty command: +``` +Explain Concept (EC) +Load: /tech-writer, then ask to "EC about [topic]" +Agent: Tech Writer +Description: Create clear technical explanations with examples... +``` + ## MODULE DETECTION - **Empty `module` column** → universal tools (work across all modules) @@ -25,10 +44,10 @@ Detect the active module from conversation context, recent workflows, or user qu ## INPUT ANALYSIS Determine what was just completed: -- Did someone state they completed something? Proceed as if that was the input. -- Was a workflow just completed in this conversation? Proceed as if that was the input. -- Search resolved artifact locations for files; fuzzy-match to workflow `outputs` patterns. -- If an `index.md` exists, read it for additional context. +- Explicit completion stated by user +- Workflow completed in current conversation +- Artifacts found matching `outputs` patterns +- If `index.md` exists, read it for additional context - If still unclear, ask: "What workflow did you most recently complete?" ## EXECUTION @@ -37,26 +56,27 @@ Determine what was just completed: 2. **Resolve output locations** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. -3. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. +3. **Detect active module** — Use MODULE DETECTION above -4. **Detect active module** — Use MODULE DETECTION above to determine which module the user is working in. +4. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. -5. **Present recommendations** — Show next steps based on completed workflows, phase/sequence ordering (KEY RULES), and artifact detection. Format per the following - -## RECOMMENDED OUTPUT FORMAT +5. **Present recommendations** — Show next steps based on: + - Completed workflows detected + - Phase/sequence ordering (ROUTING RULES) + - Artifact presence **Optional items first** — List optional workflows until a required step is reached **Required items next** — List the next required workflow - For each item show: + + For each item, apply DISPLAY RULES above and include: - Workflow **name** - - **Command** (prefixed with `/`, e.g., `/bmad:example:build-prototype`) + - **Command** OR **Code + Agent load instruction** (per DISPLAY RULES) - **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)") - Brief **description** - ### Additional response output guidance to convey: +6. **Additional guidance to convey**: - Run each workflow in a **fresh context window** - - Load the agent using (`/` + `agent-command`), or run the workflow command directly - For **validation workflows**: recommend using a different high-quality LLM if available - For conversational requests: match the user's tone while presenting clearly -6. Return to the calling process after presenting recommendations. +7. Return to the calling process after presenting recommendations. diff --git a/src/core/tasks/index-docs.xml b/src/core/tasks/index-docs.xml index ff9a7de08..30e060921 100644 --- a/src/core/tasks/index-docs.xml +++ b/src/core/tasks/index-docs.xml @@ -1,5 +1,5 @@ + description="Generates or updates an index.md of all documents in the specified directory"> MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER DO NOT skip steps or change the sequence diff --git a/src/core/tasks/review-adversarial-general.xml b/src/core/tasks/review-adversarial-general.xml index 0ebe5cdfa..421719bb5 100644 --- a/src/core/tasks/review-adversarial-general.xml +++ b/src/core/tasks/review-adversarial-general.xml @@ -1,7 +1,7 @@ - + Cynically review content and produce findings @@ -45,4 +45,4 @@ HALT if content is empty or unreadable - + \ No newline at end of file diff --git a/src/core/tasks/shard-doc.xml b/src/core/tasks/shard-doc.xml index cd1dd6748..1dc8fe80e 100644 --- a/src/core/tasks/shard-doc.xml +++ b/src/core/tasks/shard-doc.xml @@ -1,6 +1,5 @@ + description="Splits large markdown documents into smaller, organized files based on level 2 (default) sections"> Split large markdown documents into smaller, organized files based on level 2 sections using @kayvan/markdown-tree-parser tool diff --git a/src/core/tasks/workflow.xml b/src/core/tasks/workflow.xml index 8c55ec37f..536c9d8e7 100644 --- a/src/core/tasks/workflow.xml +++ b/src/core/tasks/workflow.xml @@ -1,4 +1,4 @@ - + Execute given workflow by loading its configuration, following instructions, and producing output diff --git a/src/core/workflows/advanced-elicitation/workflow.xml b/src/core/workflows/advanced-elicitation/workflow.xml index 8a348d9ee..ea7395e41 100644 --- a/src/core/workflows/advanced-elicitation/workflow.xml +++ b/src/core/workflows/advanced-elicitation/workflow.xml @@ -1,4 +1,4 @@ - diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index fcaee8ada..1e6e3c30a 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -409,10 +409,14 @@ class ManifestGenerator { name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; description = this.cleanForCSV(frontmatter.description || ''); - standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; + // Tasks are standalone by default unless explicitly false (internal=true is already filtered above) + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { // If YAML parsing fails, use defaults + standalone = true; // Default to standalone } + } else { + standalone = true; // No frontmatter means standalone } } else { // For .xml tasks, extract from tag attributes @@ -423,8 +427,8 @@ class ManifestGenerator { const objMatch = content.match(/([^<]+)<\/objective>/); description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - const standaloneMatch = content.match(/]+standalone="true"/); - standalone = !!standaloneMatch; + const standaloneFalseMatch = content.match(/]+standalone="false"/); + standalone = !standaloneFalseMatch; } // Build relative path for installation @@ -503,10 +507,14 @@ class ManifestGenerator { name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; description = this.cleanForCSV(frontmatter.description || ''); - standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; + // Tools are standalone by default unless explicitly false (internal=true is already filtered above) + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { // If YAML parsing fails, use defaults + standalone = true; // Default to standalone } + } else { + standalone = true; // No frontmatter means standalone } } else { // For .xml tools, extract from tag attributes @@ -517,8 +525,8 @@ class ManifestGenerator { const objMatch = content.match(/([^<]+)<\/objective>/); description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); - const standaloneMatch = content.match(/]+standalone="true"/); - standalone = !!standaloneMatch; + const standaloneFalseMatch = content.match(/]+standalone="false"/); + standalone = !standaloneFalseMatch; } // Build relative path for installation diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index 4ae116772..dce8aee9f 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -352,13 +352,15 @@ class BaseIdeSetup { const workflowData = yaml.parse(content); if (workflowData && workflowData.name) { + // Workflows are standalone by default unless explicitly false + const standalone = workflowData.standalone !== false && workflowData.standalone !== 'false'; workflows.push({ name: workflowData.name, path: fullPath, relativePath: path.relative(dir, fullPath), filename: entry.name, description: workflowData.description || '', - standalone: workflowData.standalone === true, // Check standalone property + standalone: standalone, }); } } catch { @@ -442,36 +444,38 @@ class BaseIdeSetup { const matchedExt = extensions.find((e) => entry.name.endsWith(e)); if (matchedExt) { // Read file content to check for standalone attribute - let standalone = false; + // All non-internal files are considered standalone by default + let standalone = true; try { const content = await fs.readFile(fullPath, 'utf8'); - // Skip internal/engine files (not user-facing tasks/tools) + // Skip internal/engine files (not user-facing) if (content.includes('internal="true"')) { continue; } - // Check for standalone="true" in XML files + // Check for explicit standalone: false if (entry.name.endsWith('.xml')) { - // Look for standalone="true" in the opening tag (task or tool) - const standaloneMatch = content.match(/<(?:task|tool)[^>]+standalone="true"/); - standalone = !!standaloneMatch; + // For XML files, check for standalone="false" attribute + const tagMatch = content.match(/<(task|tool)[^>]*standalone="false"/); + standalone = !tagMatch; } else if (entry.name.endsWith('.md')) { - // Check for standalone: true in YAML frontmatter - const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); + // For MD files, parse YAML frontmatter + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { - const yaml = require('yaml'); try { + const yaml = require('yaml'); const frontmatter = yaml.parse(frontmatterMatch[1]); - standalone = frontmatter.standalone === true; + standalone = frontmatter.standalone !== false && frontmatter.standalone !== 'false'; } catch { - // Ignore YAML parse errors + // If YAML parsing fails, default to standalone } } + // No frontmatter means standalone (default) } } catch { - // If we can't read the file, assume not standalone - standalone = false; + // If we can't read the file, default to standalone + standalone = true; } files.push({ diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index 60eb54687..2455c75c9 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -28,15 +28,12 @@ class TaskToolCommandGenerator { const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - + // All tasks/tools in manifest are standalone (internal=true items are filtered during manifest generation) const artifacts = []; const bmadPrefix = `${BMAD_FOLDER_NAME}/`; // Collect task artifacts - for (const task of standaloneTasks) { + for (const task of tasks || []) { let taskPath = (task.path || '').replaceAll('\\', '/'); // Convert absolute paths to relative paths if (path.isAbsolute(taskPath)) { @@ -61,7 +58,7 @@ class TaskToolCommandGenerator { } // Collect tool artifacts - for (const tool of standaloneTools) { + for (const tool of tools || []) { let toolPath = (tool.path || '').replaceAll('\\', '/'); // Convert absolute paths to relative paths if (path.isAbsolute(toolPath)) { @@ -88,8 +85,8 @@ class TaskToolCommandGenerator { return { artifacts, counts: { - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }, }; } @@ -104,17 +101,13 @@ class TaskToolCommandGenerator { const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - // Base commands directory - use provided or default to Claude Code structure const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad'); let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const moduleTasksDir = path.join(commandsDir, task.module, 'tasks'); await fs.ensureDir(moduleTasksDir); @@ -126,7 +119,7 @@ class TaskToolCommandGenerator { } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const moduleToolsDir = path.join(commandsDir, tool.module, 'tools'); await fs.ensureDir(moduleToolsDir); @@ -139,8 +132,8 @@ class TaskToolCommandGenerator { return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } @@ -242,14 +235,10 @@ Follow all instructions in the ${type} file exactly as written. const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const commandContent = this.generateCommandContent(task, 'task'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(task.module, 'tasks', task.name); @@ -260,7 +249,7 @@ Follow all instructions in the ${type} file exactly as written. } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const commandContent = this.generateCommandContent(tool, 'tool'); // Use underscore format: bmad_bmm_name.md const flatName = toColonName(tool.module, 'tools', tool.name); @@ -272,8 +261,8 @@ Follow all instructions in the ${type} file exactly as written. return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } @@ -290,14 +279,10 @@ Follow all instructions in the ${type} file exactly as written. const tasks = await this.loadTaskManifest(bmadDir); const tools = await this.loadToolManifest(bmadDir); - // Filter to only standalone items - const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; - let generatedCount = 0; // Generate command files for tasks - for (const task of standaloneTasks) { + for (const task of tasks || []) { const commandContent = this.generateCommandContent(task, 'task'); // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); @@ -308,7 +293,7 @@ Follow all instructions in the ${type} file exactly as written. } // Generate command files for tools - for (const tool of standaloneTools) { + for (const tool of tools || []) { const commandContent = this.generateCommandContent(tool, 'tool'); // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); @@ -320,8 +305,8 @@ Follow all instructions in the ${type} file exactly as written. return { generated: generatedCount, - tasks: standaloneTasks.length, - tools: standaloneTools.length, + tasks: (tasks || []).length, + tools: (tools || []).length, }; } diff --git a/tools/flattener/aggregate.js b/tools/flattener/aggregate.js deleted file mode 100644 index 6a597a2fe..000000000 --- a/tools/flattener/aggregate.js +++ /dev/null @@ -1,76 +0,0 @@ -const fs = require('fs-extra'); -const path = require('node:path'); -const os = require('node:os'); -const { isBinaryFile } = require('./binary.js'); - -/** - * Aggregate file contents with bounded concurrency. - * Returns text files, binary files (with size), and errors. - * @param {string[]} files absolute file paths - * @param {string} rootDir - * @param {{ text?: string, warn?: (msg: string) => void } | null} spinner - */ -async function aggregateFileContents(files, rootDir, spinner = null) { - const results = { - textFiles: [], - binaryFiles: [], - errors: [], - totalFiles: files.length, - processedFiles: 0, - }; - - // Automatic concurrency selection based on CPU count and workload size. - // - Base on 2x logical CPUs, clamped to [2, 64] - // - For very small workloads, avoid excessive parallelism - const cpuCount = os.cpus && Array.isArray(os.cpus()) ? os.cpus().length : os.cpus?.length || 4; - let concurrency = Math.min(64, Math.max(2, (Number(cpuCount) || 4) * 2)); - if (files.length > 0 && files.length < concurrency) { - concurrency = Math.max(1, Math.min(concurrency, Math.ceil(files.length / 2))); - } - - async function processOne(filePath) { - try { - const relativePath = path.relative(rootDir, filePath); - if (spinner) { - spinner.text = `Processing: ${relativePath} (${results.processedFiles + 1}/${results.totalFiles})`; - } - - const binary = await isBinaryFile(filePath); - if (binary) { - const { size } = await fs.stat(filePath); - results.binaryFiles.push({ path: relativePath, absolutePath: filePath, size }); - } else { - const content = await fs.readFile(filePath, 'utf8'); - results.textFiles.push({ - path: relativePath, - absolutePath: filePath, - content, - size: content.length, - lines: content.split('\n').length, - }); - } - } catch (error) { - const relativePath = path.relative(rootDir, filePath); - const errorInfo = { path: relativePath, absolutePath: filePath, error: error.message }; - results.errors.push(errorInfo); - if (spinner) { - spinner.warn(`Warning: Could not read file ${relativePath}: ${error.message}`); - } else { - console.warn(`Warning: Could not read file ${relativePath}: ${error.message}`); - } - } finally { - results.processedFiles++; - } - } - - for (let index = 0; index < files.length; index += concurrency) { - const slice = files.slice(index, index + concurrency); - await Promise.all(slice.map(processOne)); - } - - return results; -} - -module.exports = { - aggregateFileContents, -}; diff --git a/tools/flattener/binary.js b/tools/flattener/binary.js deleted file mode 100644 index fcfb27c1a..000000000 --- a/tools/flattener/binary.js +++ /dev/null @@ -1,80 +0,0 @@ -const fsp = require('node:fs/promises'); -const path = require('node:path'); -const { Buffer } = require('node:buffer'); - -/** - * Efficiently determine if a file is binary without reading the whole file. - * - Fast path by extension for common binaries - * - Otherwise read a small prefix and check for NUL bytes - * @param {string} filePath - * @returns {Promise} - */ -async function isBinaryFile(filePath) { - try { - const stats = await fsp.stat(filePath); - if (stats.isDirectory()) { - throw new Error('EISDIR: illegal operation on a directory'); - } - - const binaryExtensions = new Set([ - '.jpg', - '.jpeg', - '.png', - '.gif', - '.bmp', - '.ico', - '.svg', - '.pdf', - '.doc', - '.docx', - '.xls', - '.xlsx', - '.ppt', - '.pptx', - '.zip', - '.tar', - '.gz', - '.rar', - '.7z', - '.exe', - '.dll', - '.so', - '.dylib', - '.mp3', - '.mp4', - '.avi', - '.mov', - '.wav', - '.ttf', - '.otf', - '.woff', - '.woff2', - '.bin', - '.dat', - '.db', - '.sqlite', - ]); - - const extension = path.extname(filePath).toLowerCase(); - if (binaryExtensions.has(extension)) return true; - if (stats.size === 0) return false; - - const sampleSize = Math.min(4096, stats.size); - const fd = await fsp.open(filePath, 'r'); - try { - const buffer = Buffer.allocUnsafe(sampleSize); - const { bytesRead } = await fd.read(buffer, 0, sampleSize, 0); - const slice = bytesRead === sampleSize ? buffer : buffer.subarray(0, bytesRead); - return slice.includes(0); - } finally { - await fd.close(); - } - } catch (error) { - console.warn(`Warning: Could not determine if file is binary: ${filePath} - ${error.message}`); - return false; - } -} - -module.exports = { - isBinaryFile, -}; diff --git a/tools/flattener/discovery.js b/tools/flattener/discovery.js deleted file mode 100644 index 7eaaa2d40..000000000 --- a/tools/flattener/discovery.js +++ /dev/null @@ -1,71 +0,0 @@ -const path = require('node:path'); -const { execFile } = require('node:child_process'); -const { promisify } = require('node:util'); -const { glob } = require('glob'); -const { loadIgnore } = require('./ignoreRules.js'); - -const pExecFile = promisify(execFile); - -async function isGitRepo(rootDir) { - try { - const { stdout } = await pExecFile('git', ['rev-parse', '--is-inside-work-tree'], { - cwd: rootDir, - }); - return ( - String(stdout || '') - .toString() - .trim() === 'true' - ); - } catch { - return false; - } -} - -async function gitListFiles(rootDir) { - try { - const { stdout } = await pExecFile('git', ['ls-files', '-co', '--exclude-standard'], { - cwd: rootDir, - }); - return String(stdout || '') - .split(/\r?\n/) - .map((s) => s.trim()) - .filter(Boolean); - } catch { - return []; - } -} - -/** - * Discover files under rootDir. - * - Prefer git ls-files when available for speed/correctness - * - Fallback to glob and apply unified ignore rules - * @param {string} rootDir - * @param {object} [options] - * @param {boolean} [options.preferGit=true] - * @returns {Promise} absolute file paths - */ -async function discoverFiles(rootDir, options = {}) { - const { preferGit = true } = options; - const { filter } = await loadIgnore(rootDir); - - // Try git first - if (preferGit && (await isGitRepo(rootDir))) { - const relFiles = await gitListFiles(rootDir); - const filteredRel = relFiles.filter((p) => filter(p)); - return filteredRel.map((p) => path.resolve(rootDir, p)); - } - - // Glob fallback - const globbed = await glob('**/*', { - cwd: rootDir, - nodir: true, - dot: true, - follow: false, - }); - const filteredRel = globbed.filter((p) => filter(p)); - return filteredRel.map((p) => path.resolve(rootDir, p)); -} - -module.exports = { - discoverFiles, -}; diff --git a/tools/flattener/files.js b/tools/flattener/files.js deleted file mode 100644 index e7236d7b0..000000000 --- a/tools/flattener/files.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require('node:path'); -const discovery = require('./discovery.js'); -const ignoreRules = require('./ignoreRules.js'); -const { isBinaryFile } = require('./binary.js'); -const { aggregateFileContents } = require('./aggregate.js'); - -// Backward-compatible signature; delegate to central loader -async function parseGitignore(gitignorePath) { - return await ignoreRules.parseGitignore(gitignorePath); -} - -async function discoverFiles(rootDir) { - try { - // Delegate to discovery module which respects .gitignore and defaults - return await discovery.discoverFiles(rootDir, { preferGit: true }); - } catch (error) { - console.error('Error discovering files:', error.message); - return []; - } -} - -async function filterFiles(files, rootDir) { - const { filter } = await ignoreRules.loadIgnore(rootDir); - const relativeFiles = files.map((f) => path.relative(rootDir, f)); - const filteredRelative = relativeFiles.filter((p) => filter(p)); - return filteredRelative.map((p) => path.resolve(rootDir, p)); -} - -module.exports = { - parseGitignore, - discoverFiles, - isBinaryFile, - aggregateFileContents, - filterFiles, -}; diff --git a/tools/flattener/ignoreRules.js b/tools/flattener/ignoreRules.js deleted file mode 100644 index b825edea7..000000000 --- a/tools/flattener/ignoreRules.js +++ /dev/null @@ -1,172 +0,0 @@ -const fs = require('fs-extra'); -const path = require('node:path'); -const ignore = require('ignore'); - -// Central default ignore patterns for discovery and filtering. -// These complement .gitignore and are applied regardless of VCS presence. -const DEFAULT_PATTERNS = [ - // Project/VCS - '**/_bmad/**', - '**/.git/**', - '**/.svn/**', - '**/.hg/**', - '**/.bzr/**', - // Package/build outputs - '**/node_modules/**', - '**/bower_components/**', - '**/vendor/**', - '**/packages/**', - '**/build/**', - '**/dist/**', - '**/out/**', - '**/target/**', - '**/bin/**', - '**/obj/**', - '**/release/**', - '**/debug/**', - // Environments - '**/.venv/**', - '**/venv/**', - '**/.virtualenv/**', - '**/virtualenv/**', - '**/env/**', - // Logs & coverage - '**/*.log', - '**/npm-debug.log*', - '**/yarn-debug.log*', - '**/yarn-error.log*', - '**/lerna-debug.log*', - '**/coverage/**', - '**/.nyc_output/**', - '**/.coverage/**', - '**/test-results/**', - // Caches & temp - '**/.cache/**', - '**/.tmp/**', - '**/.temp/**', - '**/tmp/**', - '**/temp/**', - '**/.sass-cache/**', - // IDE/editor - '**/.vscode/**', - '**/.idea/**', - '**/*.swp', - '**/*.swo', - '**/*~', - '**/.project', - '**/.classpath', - '**/.settings/**', - '**/*.sublime-project', - '**/*.sublime-workspace', - // Lockfiles - '**/package-lock.json', - '**/yarn.lock', - '**/pnpm-lock.yaml', - '**/composer.lock', - '**/Pipfile.lock', - // Python/Java/compiled artifacts - '**/*.pyc', - '**/*.pyo', - '**/*.pyd', - '**/__pycache__/**', - '**/*.class', - '**/*.jar', - '**/*.war', - '**/*.ear', - '**/*.o', - '**/*.so', - '**/*.dll', - '**/*.exe', - // System junk - '**/lib64/**', - '**/.venv/lib64/**', - '**/venv/lib64/**', - '**/_site/**', - '**/.jekyll-cache/**', - '**/.jekyll-metadata', - '**/.DS_Store', - '**/.DS_Store?', - '**/._*', - '**/.Spotlight-V100/**', - '**/.Trashes/**', - '**/ehthumbs.db', - '**/Thumbs.db', - '**/desktop.ini', - // XML outputs - '**/flattened-codebase.xml', - '**/repomix-output.xml', - // Images, media, fonts, archives, docs, dylibs - '**/*.jpg', - '**/*.jpeg', - '**/*.png', - '**/*.gif', - '**/*.bmp', - '**/*.ico', - '**/*.svg', - '**/*.pdf', - '**/*.doc', - '**/*.docx', - '**/*.xls', - '**/*.xlsx', - '**/*.ppt', - '**/*.pptx', - '**/*.zip', - '**/*.tar', - '**/*.gz', - '**/*.rar', - '**/*.7z', - '**/*.dylib', - '**/*.mp3', - '**/*.mp4', - '**/*.avi', - '**/*.mov', - '**/*.wav', - '**/*.ttf', - '**/*.otf', - '**/*.woff', - '**/*.woff2', - // Env files - '**/.env', - '**/.env.*', - '**/*.env', - // Misc - '**/junit.xml', -]; - -async function readIgnoreFile(filePath) { - try { - if (!(await fs.pathExists(filePath))) return []; - const content = await fs.readFile(filePath, 'utf8'); - return content - .split('\n') - .map((l) => l.trim()) - .filter((l) => l && !l.startsWith('#')); - } catch { - return []; - } -} - -// Backward compatible export matching previous signature -async function parseGitignore(gitignorePath) { - return readIgnoreFile(gitignorePath); -} - -async function loadIgnore(rootDir, extraPatterns = []) { - const ig = ignore(); - const gitignorePath = path.join(rootDir, '.gitignore'); - const patterns = [...(await readIgnoreFile(gitignorePath)), ...DEFAULT_PATTERNS, ...extraPatterns]; - // De-duplicate - const unique = [...new Set(patterns.map(String))]; - ig.add(unique); - - // Include-only filter: return true if path should be included - const filter = (relativePath) => !ig.ignores(relativePath.replaceAll('\\', '/')); - - return { ig, filter, patterns: unique }; -} - -module.exports = { - DEFAULT_PATTERNS, - parseGitignore, - loadIgnore, -}; diff --git a/tools/flattener/main.js b/tools/flattener/main.js deleted file mode 100644 index 72bb42f90..000000000 --- a/tools/flattener/main.js +++ /dev/null @@ -1,483 +0,0 @@ -const { Command } = require('commander'); -const fs = require('fs-extra'); -const path = require('node:path'); -const process = require('node:process'); - -// Modularized components -const { findProjectRoot } = require('./projectRoot.js'); -const { promptYesNo, promptPath } = require('./prompts.js'); -const { discoverFiles, filterFiles, aggregateFileContents } = require('./files.js'); -const { generateXMLOutput } = require('./xml.js'); -const { calculateStatistics } = require('./stats.js'); - -/** - * Recursively discover all files in a directory - * @param {string} rootDir - The root directory to scan - * @returns {Promise} Array of file paths - */ - -/** - * Parse .gitignore file and return ignore patterns - * @param {string} gitignorePath - Path to .gitignore file - * @returns {Promise} Array of ignore patterns - */ - -/** - * Check if a file is binary using file command and heuristics - * @param {string} filePath - Path to the file - * @returns {Promise} True if file is binary - */ - -/** - * Read and aggregate content from text files - * @param {string[]} files - Array of file paths - * @param {string} rootDir - The root directory - * @param {Object} spinner - Optional spinner instance for progress display - * @returns {Promise} Object containing file contents and metadata - */ - -/** - * Generate XML output with aggregated file contents using streaming - * @param {Object} aggregatedContent - The aggregated content object - * @param {string} outputPath - The output file path - * @returns {Promise} Promise that resolves when writing is complete - */ - -/** - * Calculate statistics for the processed files - * @param {Object} aggregatedContent - The aggregated content object - * @param {number} xmlFileSize - The size of the generated XML file in bytes - * @returns {Object} Statistics object - */ - -/** - * Filter files based on .gitignore patterns - * @param {string[]} files - Array of file paths - * @param {string} rootDir - The root directory - * @returns {Promise} Filtered array of file paths - */ - -/** - * Attempt to find the project root by walking up from startDir - * Looks for common project markers like .git, package.json, pyproject.toml, etc. - * @param {string} startDir - * @returns {Promise} project root directory or null if not found - */ - -const program = new Command(); - -program - .name('bmad-flatten') - .description('BMad-Method codebase flattener tool') - .version('1.0.0') - .option('-i, --input ', 'Input directory to flatten', process.cwd()) - .option('-o, --output ', 'Output file path', 'flattened-codebase.xml') - .action(async (options) => { - let inputDir = path.resolve(options.input); - let outputPath = path.resolve(options.output); - - // Detect if user explicitly provided -i/--input or -o/--output - const argv = process.argv.slice(2); - const userSpecifiedInput = argv.some((a) => a === '-i' || a === '--input' || a.startsWith('--input=')); - const userSpecifiedOutput = argv.some((a) => a === '-o' || a === '--output' || a.startsWith('--output=')); - const noPathArguments = !userSpecifiedInput && !userSpecifiedOutput; - - if (noPathArguments) { - const detectedRoot = await findProjectRoot(process.cwd()); - const suggestedOutput = detectedRoot ? path.join(detectedRoot, 'flattened-codebase.xml') : path.resolve('flattened-codebase.xml'); - - if (detectedRoot) { - const useDefaults = await promptYesNo( - `Detected project root at "${detectedRoot}". Use it as input and write output to "${suggestedOutput}"?`, - true, - ); - if (useDefaults) { - inputDir = detectedRoot; - outputPath = suggestedOutput; - } else { - inputDir = await promptPath('Enter input directory path', process.cwd()); - outputPath = await promptPath('Enter output file path', path.join(inputDir, 'flattened-codebase.xml')); - } - } else { - console.log('Could not auto-detect a project root.'); - inputDir = await promptPath('Enter input directory path', process.cwd()); - outputPath = await promptPath('Enter output file path', path.join(inputDir, 'flattened-codebase.xml')); - } - } - - // Ensure output directory exists - await fs.ensureDir(path.dirname(outputPath)); - - try { - // Verify input directory exists - if (!(await fs.pathExists(inputDir))) { - console.error(`❌ Error: Input directory does not exist: ${inputDir}`); - process.exit(1); - } - - // Import ora dynamically - const { default: ora } = await import('ora'); - - // Start file discovery with spinner - const discoverySpinner = ora('🔍 Discovering files...').start(); - const files = await discoverFiles(inputDir); - const filteredFiles = await filterFiles(files, inputDir); - discoverySpinner.succeed(`📁 Found ${filteredFiles.length} files to include`); - - // Process files with progress tracking - console.log('Reading file contents'); - const processingSpinner = ora('📄 Processing files...').start(); - const aggregatedContent = await aggregateFileContents(filteredFiles, inputDir, processingSpinner); - processingSpinner.succeed(`✅ Processed ${aggregatedContent.processedFiles}/${filteredFiles.length} files`); - if (aggregatedContent.errors.length > 0) { - console.log(`Errors: ${aggregatedContent.errors.length}`); - } - - // Generate XML output using streaming - const xmlSpinner = ora('🔧 Generating XML output...').start(); - await generateXMLOutput(aggregatedContent, outputPath); - xmlSpinner.succeed('📝 XML generation completed'); - - // Calculate and display statistics - const outputStats = await fs.stat(outputPath); - const stats = await calculateStatistics(aggregatedContent, outputStats.size, inputDir); - - // Display completion summary - console.log('\n📊 Completion Summary:'); - console.log(`✅ Successfully processed ${filteredFiles.length} files into ${path.basename(outputPath)}`); - console.log(`📁 Output file: ${outputPath}`); - console.log(`📏 Total source size: ${stats.totalSize}`); - console.log(`📄 Generated XML size: ${stats.xmlSize}`); - console.log(`📝 Total lines of code: ${stats.totalLines.toLocaleString()}`); - console.log(`🔢 Estimated tokens: ${stats.estimatedTokens}`); - console.log(`📊 File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors\n`); - - // Ask user if they want detailed stats + markdown report - const generateDetailed = await promptYesNo('Generate detailed stats (console + markdown) now?', true); - - if (generateDetailed) { - // Additional detailed stats - console.log('\n📈 Size Percentiles:'); - console.log( - ` Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round( - stats.medianFileSize, - ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`, - ); - - if (Array.isArray(stats.histogram) && stats.histogram.length > 0) { - console.log('\n🧮 Size Histogram:'); - for (const b of stats.histogram.slice(0, 2)) { - console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`); - } - if (stats.histogram.length > 2) { - console.log(` … and ${stats.histogram.length - 2} more buckets`); - } - } - - if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) { - const topExt = stats.byExtension.slice(0, 2); - console.log('\n📦 Top Extensions:'); - for (const e of topExt) { - const pct = stats.totalBytes ? (e.bytes / stats.totalBytes) * 100 : 0; - console.log(` ${e.ext}: ${e.count} files, ${e.bytes.toLocaleString()} bytes (${pct.toFixed(2)}%)`); - } - if (stats.byExtension.length > 2) { - console.log(` … and ${stats.byExtension.length - 2} more extensions`); - } - } - - if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) { - const topDir = stats.byDirectory.slice(0, 2); - console.log('\n📂 Top Directories:'); - for (const d of topDir) { - const pct = stats.totalBytes ? (d.bytes / stats.totalBytes) * 100 : 0; - console.log(` ${d.dir}: ${d.count} files, ${d.bytes.toLocaleString()} bytes (${pct.toFixed(2)}%)`); - } - if (stats.byDirectory.length > 2) { - console.log(` … and ${stats.byDirectory.length - 2} more directories`); - } - } - - if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) { - console.log('\n🌳 Depth Distribution:'); - const dd = stats.depthDistribution.slice(0, 2); - let line = ' ' + dd.map((d) => `${d.depth}:${d.count}`).join(' '); - if (stats.depthDistribution.length > 2) { - line += ` … +${stats.depthDistribution.length - 2} more`; - } - console.log(line); - } - - if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) { - console.log('\n🧵 Longest Paths:'); - for (const p of stats.longestPaths.slice(0, 2)) { - console.log(` ${p.path} (${p.length} chars, ${p.size.toLocaleString()} bytes)`); - } - if (stats.longestPaths.length > 2) { - console.log(` … and ${stats.longestPaths.length - 2} more paths`); - } - } - - if (stats.temporal) { - console.log('\n⏱️ Temporal:'); - if (stats.temporal.oldest) { - console.log(` Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`); - } - if (stats.temporal.newest) { - console.log(` Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`); - } - if (Array.isArray(stats.temporal.ageBuckets)) { - console.log(' Age buckets:'); - for (const b of stats.temporal.ageBuckets.slice(0, 2)) { - console.log(` ${b.label}: ${b.count} files, ${b.bytes.toLocaleString()} bytes`); - } - if (stats.temporal.ageBuckets.length > 2) { - console.log(` … and ${stats.temporal.ageBuckets.length - 2} more buckets`); - } - } - } - - if (stats.quality) { - console.log('\n✅ Quality Signals:'); - console.log(` Zero-byte files: ${stats.quality.zeroByteFiles}`); - console.log(` Empty text files: ${stats.quality.emptyTextFiles}`); - console.log(` Hidden files: ${stats.quality.hiddenFiles}`); - console.log(` Symlinks: ${stats.quality.symlinks}`); - console.log( - ` Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`, - ); - console.log(` Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`); - } - - if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) { - console.log('\n🧬 Duplicate Candidates:'); - for (const d of stats.duplicateCandidates.slice(0, 2)) { - console.log(` ${d.reason}: ${d.count} files @ ${d.size.toLocaleString()} bytes`); - } - if (stats.duplicateCandidates.length > 2) { - console.log(` … and ${stats.duplicateCandidates.length - 2} more groups`); - } - } - - if (typeof stats.compressibilityRatio === 'number') { - console.log(`\n🗜️ Compressibility ratio (sampled): ${(stats.compressibilityRatio * 100).toFixed(2)}%`); - } - - if (stats.git && stats.git.isRepo) { - console.log('\n🔧 Git:'); - console.log(` Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`); - console.log(` Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`); - if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) { - console.log(' LFS candidates (top 2):'); - for (const f of stats.git.lfsCandidates.slice(0, 2)) { - console.log(` ${f.path} (${f.size.toLocaleString()} bytes)`); - } - if (stats.git.lfsCandidates.length > 2) { - console.log(` … and ${stats.git.lfsCandidates.length - 2} more`); - } - } - } - - if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) { - console.log('\n📚 Largest Files (top 2):'); - for (const f of stats.largestFiles.slice(0, 2)) { - // Show LOC for text files when available; omit ext and mtime - let locStr = ''; - if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) { - const tf = aggregatedContent.textFiles.find((t) => t.path === f.path); - if (tf && typeof tf.lines === 'number') { - locStr = `, LOC: ${tf.lines.toLocaleString()}`; - } - } - console.log(` ${f.path} – ${f.sizeFormatted} (${f.percentOfTotal.toFixed(2)}%)${locStr}`); - } - if (stats.largestFiles.length > 2) { - console.log(` … and ${stats.largestFiles.length - 2} more files`); - } - } - - // Write a comprehensive markdown report next to the XML - { - const mdPath = outputPath.endsWith('.xml') ? outputPath.replace(/\.xml$/i, '.stats.md') : outputPath + '.stats.md'; - try { - const pct = (num, den) => (den ? (num / den) * 100 : 0); - const md = []; - md.push( - `# 🧾 Flatten Stats for ${path.basename(outputPath)}`, - '', - '## 📊 Summary', - `- Total source size: ${stats.totalSize}`, - `- Generated XML size: ${stats.xmlSize}`, - `- Total lines of code: ${stats.totalLines.toLocaleString()}`, - `- Estimated tokens: ${stats.estimatedTokens}`, - `- File breakdown: ${stats.textFiles} text, ${stats.binaryFiles} binary, ${stats.errorFiles} errors`, - '', - '## 📈 Size Percentiles', - `Avg: ${Math.round(stats.avgFileSize).toLocaleString()} B, Median: ${Math.round( - stats.medianFileSize, - ).toLocaleString()} B, p90: ${stats.p90.toLocaleString()} B, p95: ${stats.p95.toLocaleString()} B, p99: ${stats.p99.toLocaleString()} B`, - '', - ); - - // Histogram - if (Array.isArray(stats.histogram) && stats.histogram.length > 0) { - md.push('## 🧮 Size Histogram', '| Bucket | Files | Bytes |', '| --- | ---: | ---: |'); - for (const b of stats.histogram) { - md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`); - } - md.push(''); - } - - // Top Extensions - if (Array.isArray(stats.byExtension) && stats.byExtension.length > 0) { - md.push('## 📦 Top Extensions by Bytes (Top 20)', '| Ext | Files | Bytes | % of total |', '| --- | ---: | ---: | ---: |'); - for (const e of stats.byExtension.slice(0, 20)) { - const p = pct(e.bytes, stats.totalBytes); - md.push(`| ${e.ext} | ${e.count} | ${e.bytes.toLocaleString()} | ${p.toFixed(2)}% |`); - } - md.push(''); - } - - // Top Directories - if (Array.isArray(stats.byDirectory) && stats.byDirectory.length > 0) { - md.push( - '## 📂 Top Directories by Bytes (Top 20)', - '| Directory | Files | Bytes | % of total |', - '| --- | ---: | ---: | ---: |', - ); - for (const d of stats.byDirectory.slice(0, 20)) { - const p = pct(d.bytes, stats.totalBytes); - md.push(`| ${d.dir} | ${d.count} | ${d.bytes.toLocaleString()} | ${p.toFixed(2)}% |`); - } - md.push(''); - } - - // Depth distribution - if (Array.isArray(stats.depthDistribution) && stats.depthDistribution.length > 0) { - md.push('## 🌳 Depth Distribution', '| Depth | Count |', '| ---: | ---: |'); - for (const d of stats.depthDistribution) { - md.push(`| ${d.depth} | ${d.count} |`); - } - md.push(''); - } - - // Longest paths - if (Array.isArray(stats.longestPaths) && stats.longestPaths.length > 0) { - md.push('## 🧵 Longest Paths (Top 25)', '| Path | Length | Bytes |', '| --- | ---: | ---: |'); - for (const pth of stats.longestPaths) { - md.push(`| ${pth.path} | ${pth.length} | ${pth.size.toLocaleString()} |`); - } - md.push(''); - } - - // Temporal - if (stats.temporal) { - md.push('## ⏱️ Temporal'); - if (stats.temporal.oldest) { - md.push(`- Oldest: ${stats.temporal.oldest.path} (${stats.temporal.oldest.mtime})`); - } - if (stats.temporal.newest) { - md.push(`- Newest: ${stats.temporal.newest.path} (${stats.temporal.newest.mtime})`); - } - if (Array.isArray(stats.temporal.ageBuckets)) { - md.push('', '| Age | Files | Bytes |', '| --- | ---: | ---: |'); - for (const b of stats.temporal.ageBuckets) { - md.push(`| ${b.label} | ${b.count} | ${b.bytes.toLocaleString()} |`); - } - } - md.push(''); - } - - // Quality signals - if (stats.quality) { - md.push( - '## ✅ Quality Signals', - `- Zero-byte files: ${stats.quality.zeroByteFiles}`, - `- Empty text files: ${stats.quality.emptyTextFiles}`, - `- Hidden files: ${stats.quality.hiddenFiles}`, - `- Symlinks: ${stats.quality.symlinks}`, - `- Large files (>= ${(stats.quality.largeThreshold / (1024 * 1024)).toFixed(0)} MB): ${stats.quality.largeFilesCount}`, - `- Suspiciously large files (>= 100 MB): ${stats.quality.suspiciousLargeFilesCount}`, - '', - ); - } - - // Duplicates - if (Array.isArray(stats.duplicateCandidates) && stats.duplicateCandidates.length > 0) { - md.push('## 🧬 Duplicate Candidates', '| Reason | Files | Size (bytes) |', '| --- | ---: | ---: |'); - for (const d of stats.duplicateCandidates) { - md.push(`| ${d.reason} | ${d.count} | ${d.size.toLocaleString()} |`); - } - md.push('', '### 🧬 Duplicate Groups Details'); - let dupIndex = 1; - for (const d of stats.duplicateCandidates) { - md.push(`#### Group ${dupIndex}: ${d.count} files @ ${d.size.toLocaleString()} bytes (${d.reason})`); - if (Array.isArray(d.files) && d.files.length > 0) { - for (const fp of d.files) { - md.push(`- ${fp}`); - } - } else { - md.push('- (file list unavailable)'); - } - md.push(''); - dupIndex++; - } - md.push(''); - } - - // Compressibility - if (typeof stats.compressibilityRatio === 'number') { - md.push('## 🗜️ Compressibility', `Sampled compressibility ratio: ${(stats.compressibilityRatio * 100).toFixed(2)}%`, ''); - } - - // Git - if (stats.git && stats.git.isRepo) { - md.push( - '## 🔧 Git', - `- Tracked: ${stats.git.trackedCount} files, ${stats.git.trackedBytes.toLocaleString()} bytes`, - `- Untracked: ${stats.git.untrackedCount} files, ${stats.git.untrackedBytes.toLocaleString()} bytes`, - ); - if (Array.isArray(stats.git.lfsCandidates) && stats.git.lfsCandidates.length > 0) { - md.push('', '### 📦 LFS Candidates (Top 20)', '| Path | Bytes |', '| --- | ---: |'); - for (const f of stats.git.lfsCandidates.slice(0, 20)) { - md.push(`| ${f.path} | ${f.size.toLocaleString()} |`); - } - } - md.push(''); - } - - // Largest Files - if (Array.isArray(stats.largestFiles) && stats.largestFiles.length > 0) { - md.push('## 📚 Largest Files (Top 50)', '| Path | Size | % of total | LOC |', '| --- | ---: | ---: | ---: |'); - for (const f of stats.largestFiles) { - let loc = ''; - if (!f.isBinary && Array.isArray(aggregatedContent?.textFiles)) { - const tf = aggregatedContent.textFiles.find((t) => t.path === f.path); - if (tf && typeof tf.lines === 'number') { - loc = tf.lines.toLocaleString(); - } - } - md.push(`| ${f.path} | ${f.sizeFormatted} | ${f.percentOfTotal.toFixed(2)}% | ${loc} |`); - } - md.push(''); - } - - await fs.writeFile(mdPath, md.join('\n')); - console.log(`\n🧾 Detailed stats report written to: ${mdPath}`); - } catch (error) { - console.warn(`⚠️ Failed to write stats markdown: ${error.message}`); - } - } - } - } catch (error) { - console.error('❌ Critical error:', error.message); - console.error('An unexpected error occurred.'); - process.exit(1); - } - }); - -if (require.main === module) { - program.parse(); -} - -module.exports = program; diff --git a/tools/flattener/projectRoot.js b/tools/flattener/projectRoot.js deleted file mode 100644 index b2b9a7ae6..000000000 --- a/tools/flattener/projectRoot.js +++ /dev/null @@ -1,201 +0,0 @@ -const fs = require('fs-extra'); -const path = require('node:path'); - -// Deno/Node compatibility: explicitly import process -const process = require('node:process'); -const { execFile } = require('node:child_process'); -const { promisify } = require('node:util'); -const execFileAsync = promisify(execFile); - -// Simple memoization across calls (keyed by realpath of startDir) -const _cache = new Map(); - -async function _tryRun(cmd, args, cwd, timeoutMs = 500) { - try { - const { stdout } = await execFileAsync(cmd, args, { - cwd, - timeout: timeoutMs, - windowsHide: true, - maxBuffer: 1024 * 1024, - }); - const out = String(stdout || '').trim(); - return out || null; - } catch { - return null; - } -} - -async function _detectVcsTopLevel(startDir) { - // Run common VCS root queries in parallel; ignore failures - const gitP = _tryRun('git', ['rev-parse', '--show-toplevel'], startDir); - const hgP = _tryRun('hg', ['root'], startDir); - const svnP = (async () => { - const show = await _tryRun('svn', ['info', '--show-item', 'wc-root'], startDir); - if (show) return show; - const info = await _tryRun('svn', ['info'], startDir); - if (info) { - const line = info.split(/\r?\n/).find((l) => l.toLowerCase().startsWith('working copy root path:')); - if (line) return line.split(':').slice(1).join(':').trim(); - } - return null; - })(); - const [git, hg, svn] = await Promise.all([gitP, hgP, svnP]); - return git || hg || svn || null; -} - -/** - * Attempt to find the project root by walking up from startDir. - * Uses a robust, prioritized set of ecosystem markers (VCS > workspaces/monorepo > lock/build > language config). - * Also recognizes package.json with "workspaces" as a workspace root. - * You can augment markers via env PROJECT_ROOT_MARKERS as a comma-separated list of file/dir names. - * @param {string} startDir - * @returns {Promise} project root directory or null if not found - */ -async function findProjectRoot(startDir) { - try { - // Resolve symlinks for robustness (e.g., when invoked from a symlinked path) - let dir = path.resolve(startDir); - try { - dir = await fs.realpath(dir); - } catch { - // ignore if realpath fails; continue with resolved path - } - const startKey = dir; // preserve starting point for caching - if (_cache.has(startKey)) return _cache.get(startKey); - const fsRoot = path.parse(dir).root; - - // Helper to safely check for existence - const exists = (p) => fs.pathExists(p); - - // Build checks: an array of { makePath: (dir) => string, weight } - const checks = []; - - const add = (rel, weight) => { - const makePath = (d) => (Array.isArray(rel) ? path.join(d, ...rel) : path.join(d, rel)); - checks.push({ makePath, weight }); - }; - - // Highest priority: explicit sentinel markers - add('.project-root', 110); - add('.workspace-root', 110); - add('.repo-root', 110); - - // Highest priority: VCS roots - add('.git', 100); - add('.hg', 95); - add('.svn', 95); - - // Monorepo/workspace indicators - add('pnpm-workspace.yaml', 90); - add('lerna.json', 90); - add('turbo.json', 90); - add('nx.json', 90); - add('rush.json', 90); - add('go.work', 90); - add('WORKSPACE', 90); - add('WORKSPACE.bazel', 90); - add('MODULE.bazel', 90); - add('pants.toml', 90); - - // Lockfiles and package-manager/top-level locks - add('yarn.lock', 85); - add('pnpm-lock.yaml', 85); - add('package-lock.json', 85); - add('bun.lockb', 85); - add('Cargo.lock', 85); - add('composer.lock', 85); - add('poetry.lock', 85); - add('Pipfile.lock', 85); - add('Gemfile.lock', 85); - - // Build-system root indicators - add('settings.gradle', 80); - add('settings.gradle.kts', 80); - add('gradlew', 80); - add('pom.xml', 80); - add('build.sbt', 80); - add(['project', 'build.properties'], 80); - - // Language/project config markers - add('deno.json', 75); - add('deno.jsonc', 75); - add('pyproject.toml', 75); - add('Pipfile', 75); - add('requirements.txt', 75); - add('go.mod', 75); - add('Cargo.toml', 75); - add('composer.json', 75); - add('mix.exs', 75); - add('Gemfile', 75); - add('CMakeLists.txt', 75); - add('stack.yaml', 75); - add('cabal.project', 75); - add('rebar.config', 75); - add('pubspec.yaml', 75); - add('flake.nix', 75); - add('shell.nix', 75); - add('default.nix', 75); - add('.tool-versions', 75); - add('package.json', 74); // generic Node project (lower than lockfiles/workspaces) - - // Changesets - add(['.changeset', 'config.json'], 70); - add('.changeset', 70); - - // Custom markers via env (comma-separated names) - if (process.env.PROJECT_ROOT_MARKERS) { - for (const name of process.env.PROJECT_ROOT_MARKERS.split(',') - .map((s) => s.trim()) - .filter(Boolean)) { - add(name, 72); - } - } - - /** Check for package.json with "workspaces" */ - const hasWorkspacePackageJson = async (d) => { - const pkgPath = path.join(d, 'package.json'); - if (!(await exists(pkgPath))) return false; - try { - const raw = await fs.readFile(pkgPath, 'utf8'); - const pkg = JSON.parse(raw); - return Boolean(pkg && pkg.workspaces); - } catch { - return false; - } - }; - - let best = null; // { dir, weight } - - // Try to detect VCS toplevel once up-front; treat as authoritative slightly above .git marker - const vcsTop = await _detectVcsTopLevel(dir); - if (vcsTop) { - best = { dir: vcsTop, weight: 101 }; - } - - while (true) { - // Special check: package.json with "workspaces" - if ((await hasWorkspacePackageJson(dir)) && (!best || 90 >= best.weight)) best = { dir, weight: 90 }; - - // Evaluate all other checks in parallel - const results = await Promise.all(checks.map(async (c) => ({ c, ok: await exists(c.makePath(dir)) }))); - - for (const { c, ok } of results) { - if (!ok) continue; - if (!best || c.weight >= best.weight) { - best = { dir, weight: c.weight }; - } - } - - if (dir === fsRoot) break; - dir = path.dirname(dir); - } - - const out = best ? best.dir : null; - _cache.set(startKey, out); - return out; - } catch { - return null; - } -} - -module.exports = { findProjectRoot }; diff --git a/tools/flattener/prompts.js b/tools/flattener/prompts.js deleted file mode 100644 index 849256d88..000000000 --- a/tools/flattener/prompts.js +++ /dev/null @@ -1,44 +0,0 @@ -const os = require('node:os'); -const path = require('node:path'); -const readline = require('node:readline'); -const process = require('node:process'); - -function expandHome(p) { - if (!p) return p; - if (p.startsWith('~')) return path.join(os.homedir(), p.slice(1)); - return p; -} - -function createRl() { - return readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); -} - -function promptQuestion(question) { - return new Promise((resolve) => { - const rl = createRl(); - rl.question(question, (answer) => { - rl.close(); - resolve(answer); - }); - }); -} - -async function promptYesNo(question, defaultYes = true) { - const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] '; - const ans = (await promptQuestion(`${question}${suffix}`)).trim().toLowerCase(); - if (!ans) return defaultYes; - if (['y', 'yes'].includes(ans)) return true; - if (['n', 'no'].includes(ans)) return false; - return promptYesNo(question, defaultYes); -} - -async function promptPath(question, defaultValue) { - const prompt = `${question}${defaultValue ? ` (default: ${defaultValue})` : ''}: `; - const ans = (await promptQuestion(prompt)).trim(); - return expandHome(ans || defaultValue); -} - -module.exports = { promptYesNo, promptPath, promptQuestion, expandHome }; diff --git a/tools/flattener/stats.helpers.js b/tools/flattener/stats.helpers.js deleted file mode 100644 index 511bb0753..000000000 --- a/tools/flattener/stats.helpers.js +++ /dev/null @@ -1,368 +0,0 @@ -'use strict'; - -const fs = require('node:fs/promises'); -const path = require('node:path'); -const zlib = require('node:zlib'); -const { Buffer } = require('node:buffer'); -const crypto = require('node:crypto'); -const cp = require('node:child_process'); - -const KB = 1024; -const MB = 1024 * KB; - -const formatSize = (bytes) => { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; - return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; -}; - -const percentile = (sorted, p) => { - if (sorted.length === 0) return 0; - const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1)); - return sorted[idx]; -}; - -async function processWithLimit(items, fn, concurrency = 64) { - for (let i = 0; i < items.length; i += concurrency) { - await Promise.all(items.slice(i, i + concurrency).map(fn)); - } -} - -async function enrichAllFiles(textFiles, binaryFiles) { - /** @type {Array<{ path: string; absolutePath: string; size: number; lines?: number; isBinary: boolean; ext: string; dir: string; depth: number; hidden: boolean; mtimeMs: number; isSymlink: boolean; }>} */ - const allFiles = []; - - async function enrich(file, isBinary) { - const ext = (path.extname(file.path) || '').toLowerCase(); - const dir = path.dirname(file.path) || '.'; - const depth = file.path.split(path.sep).filter(Boolean).length; - const hidden = file.path.split(path.sep).some((seg) => seg.startsWith('.')); - let mtimeMs = 0; - let isSymlink = false; - try { - const lst = await fs.lstat(file.absolutePath); - mtimeMs = lst.mtimeMs; - isSymlink = lst.isSymbolicLink(); - } catch { - /* ignore lstat errors during enrichment */ - } - allFiles.push({ - path: file.path, - absolutePath: file.absolutePath, - size: file.size || 0, - lines: file.lines, - isBinary, - ext, - dir, - depth, - hidden, - mtimeMs, - isSymlink, - }); - } - - await processWithLimit(textFiles, (f) => enrich(f, false)); - await processWithLimit(binaryFiles, (f) => enrich(f, true)); - return allFiles; -} - -function buildHistogram(allFiles) { - const buckets = [ - [1 * KB, '0–1KB'], - [10 * KB, '1–10KB'], - [100 * KB, '10–100KB'], - [1 * MB, '100KB–1MB'], - [10 * MB, '1–10MB'], - [100 * MB, '10–100MB'], - [Infinity, '>=100MB'], - ]; - const histogram = buckets.map(([_, label]) => ({ label, count: 0, bytes: 0 })); - for (const f of allFiles) { - for (const [i, bucket] of buckets.entries()) { - if (f.size < bucket[0]) { - histogram[i].count++; - histogram[i].bytes += f.size; - break; - } - } - } - return histogram; -} - -function aggregateByExtension(allFiles) { - const byExtension = new Map(); - for (const f of allFiles) { - const key = f.ext || ''; - const v = byExtension.get(key) || { ext: key, count: 0, bytes: 0 }; - v.count++; - v.bytes += f.size; - byExtension.set(key, v); - } - return [...byExtension.values()].sort((a, b) => b.bytes - a.bytes); -} - -function aggregateByDirectory(allFiles) { - const byDirectory = new Map(); - function addDirBytes(dir, bytes) { - const v = byDirectory.get(dir) || { dir, count: 0, bytes: 0 }; - v.count++; - v.bytes += bytes; - byDirectory.set(dir, v); - } - for (const f of allFiles) { - const parts = f.dir === '.' ? [] : f.dir.split(path.sep); - let acc = ''; - for (let i = 0; i < parts.length; i++) { - acc = i === 0 ? parts[0] : acc + path.sep + parts[i]; - addDirBytes(acc, f.size); - } - if (parts.length === 0) addDirBytes('.', f.size); - } - return [...byDirectory.values()].sort((a, b) => b.bytes - a.bytes); -} - -function computeDepthAndLongest(allFiles) { - const depthDistribution = new Map(); - for (const f of allFiles) { - depthDistribution.set(f.depth, (depthDistribution.get(f.depth) || 0) + 1); - } - const longestPaths = [...allFiles] - .sort((a, b) => b.path.length - a.path.length) - .slice(0, 25) - .map((f) => ({ path: f.path, length: f.path.length, size: f.size })); - const depthDist = [...depthDistribution.entries()].sort((a, b) => a[0] - b[0]).map(([depth, count]) => ({ depth, count })); - return { depthDist, longestPaths }; -} - -function computeTemporal(allFiles, nowMs) { - let oldest = null, - newest = null; - const ageBuckets = [ - { label: '> 1 year', minDays: 365, maxDays: Infinity, count: 0, bytes: 0 }, - { label: '6–12 months', minDays: 180, maxDays: 365, count: 0, bytes: 0 }, - { label: '1–6 months', minDays: 30, maxDays: 180, count: 0, bytes: 0 }, - { label: '7–30 days', minDays: 7, maxDays: 30, count: 0, bytes: 0 }, - { label: '1–7 days', minDays: 1, maxDays: 7, count: 0, bytes: 0 }, - { label: '< 1 day', minDays: 0, maxDays: 1, count: 0, bytes: 0 }, - ]; - for (const f of allFiles) { - const ageDays = Math.max(0, (nowMs - (f.mtimeMs || nowMs)) / (24 * 60 * 60 * 1000)); - for (const b of ageBuckets) { - if (ageDays >= b.minDays && ageDays < b.maxDays) { - b.count++; - b.bytes += f.size; - break; - } - } - if (!oldest || f.mtimeMs < oldest.mtimeMs) oldest = f; - if (!newest || f.mtimeMs > newest.mtimeMs) newest = f; - } - return { - oldest: oldest ? { path: oldest.path, mtime: oldest.mtimeMs ? new Date(oldest.mtimeMs).toISOString() : null } : null, - newest: newest ? { path: newest.path, mtime: newest.mtimeMs ? new Date(newest.mtimeMs).toISOString() : null } : null, - ageBuckets, - }; -} - -function computeQuality(allFiles, textFiles) { - const zeroByteFiles = allFiles.filter((f) => f.size === 0).length; - const emptyTextFiles = textFiles.filter((f) => (f.size || 0) === 0 || (f.lines || 0) === 0).length; - const hiddenFiles = allFiles.filter((f) => f.hidden).length; - const symlinks = allFiles.filter((f) => f.isSymlink).length; - const largeThreshold = 50 * MB; - const suspiciousThreshold = 100 * MB; - const largeFilesCount = allFiles.filter((f) => f.size >= largeThreshold).length; - const suspiciousLargeFilesCount = allFiles.filter((f) => f.size >= suspiciousThreshold).length; - return { - zeroByteFiles, - emptyTextFiles, - hiddenFiles, - symlinks, - largeFilesCount, - suspiciousLargeFilesCount, - largeThreshold, - }; -} - -function computeDuplicates(allFiles, textFiles) { - const duplicatesBySize = new Map(); - for (const f of allFiles) { - const key = String(f.size); - const arr = duplicatesBySize.get(key) || []; - arr.push(f); - duplicatesBySize.set(key, arr); - } - const duplicateCandidates = []; - for (const [sizeKey, arr] of duplicatesBySize.entries()) { - if (arr.length < 2) continue; - const textGroup = arr.filter((f) => !f.isBinary); - const otherGroup = arr.filter((f) => f.isBinary); - const contentHashGroups = new Map(); - for (const tf of textGroup) { - try { - const src = textFiles.find((x) => x.absolutePath === tf.absolutePath); - const content = src ? src.content : ''; - const h = crypto.createHash('sha1').update(content).digest('hex'); - const g = contentHashGroups.get(h) || []; - g.push(tf); - contentHashGroups.set(h, g); - } catch { - /* ignore hashing errors for duplicate detection */ - } - } - for (const [_h, g] of contentHashGroups.entries()) { - if (g.length > 1) - duplicateCandidates.push({ - reason: 'same-size+text-hash', - size: Number(sizeKey), - count: g.length, - files: g.map((f) => f.path), - }); - } - if (otherGroup.length > 1) { - duplicateCandidates.push({ - reason: 'same-size', - size: Number(sizeKey), - count: otherGroup.length, - files: otherGroup.map((f) => f.path), - }); - } - } - return duplicateCandidates; -} - -function estimateCompressibility(textFiles) { - let compSampleBytes = 0; - let compCompressedBytes = 0; - for (const tf of textFiles) { - try { - const sampleLen = Math.min(256 * 1024, tf.size || 0); - if (sampleLen <= 0) continue; - const sample = tf.content.slice(0, sampleLen); - const gz = zlib.gzipSync(Buffer.from(sample, 'utf8')); - compSampleBytes += sampleLen; - compCompressedBytes += gz.length; - } catch { - /* ignore compression errors during sampling */ - } - } - return compSampleBytes > 0 ? compCompressedBytes / compSampleBytes : null; -} - -function computeGitInfo(allFiles, rootDir, largeThreshold) { - const info = { - isRepo: false, - trackedCount: 0, - trackedBytes: 0, - untrackedCount: 0, - untrackedBytes: 0, - lfsCandidates: [], - }; - try { - if (!rootDir) return info; - const top = cp - .execFileSync('git', ['rev-parse', '--show-toplevel'], { - cwd: rootDir, - stdio: ['ignore', 'pipe', 'ignore'], - }) - .toString() - .trim(); - if (!top) return info; - info.isRepo = true; - const out = cp.execFileSync('git', ['ls-files', '-z'], { - cwd: rootDir, - stdio: ['ignore', 'pipe', 'ignore'], - }); - const tracked = new Set(out.toString().split('\0').filter(Boolean)); - let trackedBytes = 0, - trackedCount = 0, - untrackedBytes = 0, - untrackedCount = 0; - const lfsCandidates = []; - for (const f of allFiles) { - const isTracked = tracked.has(f.path); - if (isTracked) { - trackedCount++; - trackedBytes += f.size; - if (f.size >= largeThreshold) lfsCandidates.push({ path: f.path, size: f.size }); - } else { - untrackedCount++; - untrackedBytes += f.size; - } - } - info.trackedCount = trackedCount; - info.trackedBytes = trackedBytes; - info.untrackedCount = untrackedCount; - info.untrackedBytes = untrackedBytes; - info.lfsCandidates = lfsCandidates.sort((a, b) => b.size - a.size).slice(0, 50); - } catch { - /* git not available or not a repo, ignore */ - } - return info; -} - -function computeLargestFiles(allFiles, totalBytes) { - const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100); - return [...allFiles] - .sort((a, b) => b.size - a.size) - .slice(0, 50) - .map((f) => ({ - path: f.path, - size: f.size, - sizeFormatted: formatSize(f.size), - percentOfTotal: toPct(f.size, totalBytes), - ext: f.ext || '', - isBinary: f.isBinary, - mtime: f.mtimeMs ? new Date(f.mtimeMs).toISOString() : null, - })); -} - -function mdTable(rows, headers) { - const header = `| ${headers.join(' | ')} |`; - const sep = `| ${headers.map(() => '---').join(' | ')} |`; - const body = rows.map((r) => `| ${r.join(' | ')} |`).join('\n'); - return `${header}\n${sep}\n${body}`; -} - -function buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, totalBytes) { - const toPct = (num, den) => (den === 0 ? 0 : (num / den) * 100); - const md = []; - md.push( - '\n### Top Largest Files (Top 50)\n', - mdTable( - largestFiles.map((f) => [f.path, f.sizeFormatted, `${f.percentOfTotal.toFixed(2)}%`, f.ext || '', f.isBinary ? 'binary' : 'text']), - ['Path', 'Size', '% of total', 'Ext', 'Type'], - ), - '\n\n### Top Extensions by Bytes (Top 20)\n', - ); - const topExtRows = byExtensionArr - .slice(0, 20) - .map((e) => [e.ext, String(e.count), formatSize(e.bytes), `${toPct(e.bytes, totalBytes).toFixed(2)}%`]); - md.push(mdTable(topExtRows, ['Ext', 'Count', 'Bytes', '% of total']), '\n\n### Top Directories by Bytes (Top 20)\n'); - const topDirRows = byDirectoryArr - .slice(0, 20) - .map((d) => [d.dir, String(d.count), formatSize(d.bytes), `${toPct(d.bytes, totalBytes).toFixed(2)}%`]); - md.push(mdTable(topDirRows, ['Directory', 'Files', 'Bytes', '% of total'])); - return md.join('\n'); -} - -module.exports = { - KB, - MB, - formatSize, - percentile, - processWithLimit, - enrichAllFiles, - buildHistogram, - aggregateByExtension, - aggregateByDirectory, - computeDepthAndLongest, - computeTemporal, - computeQuality, - computeDuplicates, - estimateCompressibility, - computeGitInfo, - computeLargestFiles, - buildMarkdownReport, -}; diff --git a/tools/flattener/stats.js b/tools/flattener/stats.js deleted file mode 100644 index b41d50e57..000000000 --- a/tools/flattener/stats.js +++ /dev/null @@ -1,75 +0,0 @@ -const H = require('./stats.helpers.js'); - -async function calculateStatistics(aggregatedContent, xmlFileSize, rootDir) { - const { textFiles, binaryFiles, errors } = aggregatedContent; - - const totalLines = textFiles.reduce((sum, f) => sum + (f.lines || 0), 0); - const estimatedTokens = Math.ceil(xmlFileSize / 4); - - // Build enriched file list - const allFiles = await H.enrichAllFiles(textFiles, binaryFiles); - const totalBytes = allFiles.reduce((s, f) => s + f.size, 0); - const sizes = allFiles.map((f) => f.size).sort((a, b) => a - b); - const avgSize = sizes.length > 0 ? totalBytes / sizes.length : 0; - const medianSize = sizes.length > 0 ? H.percentile(sizes, 50) : 0; - const p90 = H.percentile(sizes, 90); - const p95 = H.percentile(sizes, 95); - const p99 = H.percentile(sizes, 99); - - const histogram = H.buildHistogram(allFiles); - const byExtensionArr = H.aggregateByExtension(allFiles); - const byDirectoryArr = H.aggregateByDirectory(allFiles); - const { depthDist, longestPaths } = H.computeDepthAndLongest(allFiles); - const temporal = H.computeTemporal(allFiles, Date.now()); - const quality = H.computeQuality(allFiles, textFiles); - const duplicateCandidates = H.computeDuplicates(allFiles, textFiles); - const compressibilityRatio = H.estimateCompressibility(textFiles); - const git = H.computeGitInfo(allFiles, rootDir, quality.largeThreshold); - const largestFiles = H.computeLargestFiles(allFiles, totalBytes); - const markdownReport = H.buildMarkdownReport(largestFiles, byExtensionArr, byDirectoryArr, totalBytes); - - return { - // Back-compat summary - totalFiles: textFiles.length + binaryFiles.length, - textFiles: textFiles.length, - binaryFiles: binaryFiles.length, - errorFiles: errors.length, - totalSize: H.formatSize(totalBytes), - totalBytes, - xmlSize: H.formatSize(xmlFileSize), - totalLines, - estimatedTokens: estimatedTokens.toLocaleString(), - - // Distributions and percentiles - avgFileSize: avgSize, - medianFileSize: medianSize, - p90, - p95, - p99, - histogram, - - // Extensions and directories - byExtension: byExtensionArr, - byDirectory: byDirectoryArr, - depthDistribution: depthDist, - longestPaths, - - // Temporal - temporal, - - // Quality signals - quality, - - // Duplicates and compressibility - duplicateCandidates, - compressibilityRatio, - - // Git-aware - git, - - largestFiles, - markdownReport, - }; -} - -module.exports = { calculateStatistics }; diff --git a/tools/flattener/test-matrix.js b/tools/flattener/test-matrix.js deleted file mode 100644 index 0d9f6437e..000000000 --- a/tools/flattener/test-matrix.js +++ /dev/null @@ -1,409 +0,0 @@ -/* deno-lint-ignore-file */ -/* - Automatic test matrix for project root detection. - Creates temporary fixtures for various ecosystems and validates findProjectRoot(). - No external options or flags required. Safe to run multiple times. -*/ - -const os = require('node:os'); -const path = require('node:path'); -const fs = require('fs-extra'); -const { promisify } = require('node:util'); -const { execFile } = require('node:child_process'); -const process = require('node:process'); -const execFileAsync = promisify(execFile); - -const { findProjectRoot } = require('./projectRoot.js'); - -async function cmdAvailable(cmd) { - try { - await execFileAsync(cmd, ['--version'], { timeout: 500, windowsHide: true }); - return true; - } catch { - return false; - } - - async function testSvnMarker() { - const root = await mkTmpDir('svn'); - const nested = path.join(root, 'proj', 'code'); - await fs.ensureDir(nested); - await fs.ensureDir(path.join(root, '.svn')); - const found = await findProjectRoot(nested); - assertEqual(found, root, '.svn marker should be detected'); - return { name: 'svn-marker', ok: true }; - } - - async function testSymlinkStart() { - const root = await mkTmpDir('symlink-start'); - const nested = path.join(root, 'a', 'b'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, '.project-root'), '\n'); - const tmp = await mkTmpDir('symlink-tmp'); - const link = path.join(tmp, 'link-to-b'); - try { - await fs.symlink(nested, link); - } catch { - // symlink may not be permitted on some systems; skip - return { name: 'symlink-start', ok: true, skipped: true }; - } - const found = await findProjectRoot(link); - assertEqual(found, root, 'should resolve symlinked start to real root'); - return { name: 'symlink-start', ok: true }; - } - - async function testSubmoduleLikeInnerGitFile() { - const root = await mkTmpDir('submodule-like'); - const mid = path.join(root, 'mid'); - const leaf = path.join(mid, 'leaf'); - await fs.ensureDir(leaf); - // outer repo - await fs.ensureDir(path.join(root, '.git')); - // inner submodule-like .git file - await fs.writeFile(path.join(mid, '.git'), 'gitdir: ../.git/modules/mid\n'); - const found = await findProjectRoot(leaf); - assertEqual(found, root, 'outermost .git should win on tie weight'); - return { name: 'submodule-like-gitfile', ok: true }; - } -} - -async function mkTmpDir(name) { - const base = await fs.realpath(os.tmpdir()); - const dir = await fs.mkdtemp(path.join(base, `flattener-${name}-`)); - return dir; -} - -function assertEqual(actual, expected, msg) { - if (actual !== expected) { - throw new Error(`${msg}: expected="${expected}" actual="${actual}"`); - } -} - -async function testSentinel() { - const root = await mkTmpDir('sentinel'); - const nested = path.join(root, 'a', 'b', 'c'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, '.project-root'), '\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'sentinel .project-root should win'); - return { name: 'sentinel', ok: true }; -} - -async function testOtherSentinels() { - const root = await mkTmpDir('other-sentinels'); - const nested = path.join(root, 'x', 'y'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, '.workspace-root'), '\n'); - const found1 = await findProjectRoot(nested); - assertEqual(found1, root, 'sentinel .workspace-root should win'); - - await fs.remove(path.join(root, '.workspace-root')); - await fs.writeFile(path.join(root, '.repo-root'), '\n'); - const found2 = await findProjectRoot(nested); - assertEqual(found2, root, 'sentinel .repo-root should win'); - return { name: 'other-sentinels', ok: true }; -} - -async function testGitCliAndMarker() { - const hasGit = await cmdAvailable('git'); - if (!hasGit) return { name: 'git-cli', ok: true, skipped: true }; - - const root = await mkTmpDir('git'); - const nested = path.join(root, 'pkg', 'src'); - await fs.ensureDir(nested); - await execFileAsync('git', ['init'], { cwd: root, timeout: 2000 }); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'git toplevel should be detected'); - return { name: 'git-cli', ok: true }; -} - -async function testHgMarkerOrCli() { - // Prefer simple marker test to avoid requiring Mercurial install - const root = await mkTmpDir('hg'); - const nested = path.join(root, 'lib'); - await fs.ensureDir(nested); - await fs.ensureDir(path.join(root, '.hg')); - const found = await findProjectRoot(nested); - await assertEqual(found, root, '.hg marker should be detected'); - return { name: 'hg-marker', ok: true }; -} - -async function testWorkspacePnpm() { - const root = await mkTmpDir('pnpm-workspace'); - const pkgA = path.join(root, 'packages', 'a'); - await fs.ensureDir(pkgA); - await fs.writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n'); - const found = await findProjectRoot(pkgA); - await assertEqual(found, root, 'pnpm-workspace.yaml should be detected'); - return { name: 'pnpm-workspace', ok: true }; -} - -async function testPackageJsonWorkspaces() { - const root = await mkTmpDir('package-workspaces'); - const pkgA = path.join(root, 'packages', 'a'); - await fs.ensureDir(pkgA); - await fs.writeJson(path.join(root, 'package.json'), { private: true, workspaces: ['packages/*'] }, { spaces: 2 }); - const found = await findProjectRoot(pkgA); - await assertEqual(found, root, 'package.json workspaces should be detected'); - return { name: 'package.json-workspaces', ok: true }; -} - -async function testLockfiles() { - const root = await mkTmpDir('lockfiles'); - const nested = path.join(root, 'src'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'yarn.lock'), '\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'yarn.lock should be detected'); - return { name: 'lockfiles', ok: true }; -} - -async function testLanguageConfigs() { - const root = await mkTmpDir('lang-configs'); - const nested = path.join(root, 'x', 'y'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'pyproject.toml'), "[tool.poetry]\nname='tmp'\n"); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'pyproject.toml should be detected'); - return { name: 'language-configs', ok: true }; -} - -async function testPreferOuterOnTie() { - const root = await mkTmpDir('tie'); - const mid = path.join(root, 'mid'); - const leaf = path.join(mid, 'leaf'); - await fs.ensureDir(leaf); - // same weight marker at two levels - await fs.writeFile(path.join(root, 'requirements.txt'), '\n'); - await fs.writeFile(path.join(mid, 'requirements.txt'), '\n'); - const found = await findProjectRoot(leaf); - await assertEqual(found, root, 'outermost directory should win on equal weight'); - return { name: 'prefer-outermost-tie', ok: true }; -} - -// Additional coverage: Bazel, Nx/Turbo/Rush, Go workspaces, Deno, Java/Scala, PHP, Rust, Nix, Changesets, env markers, -// and priority interaction between package.json and lockfiles. - -async function testBazelWorkspace() { - const root = await mkTmpDir('bazel'); - const nested = path.join(root, 'apps', 'svc'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'WORKSPACE'), 'workspace(name="tmp")\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'Bazel WORKSPACE should be detected'); - return { name: 'bazel-workspace', ok: true }; -} - -async function testNx() { - const root = await mkTmpDir('nx'); - const nested = path.join(root, 'apps', 'web'); - await fs.ensureDir(nested); - await fs.writeJson(path.join(root, 'nx.json'), { npmScope: 'tmp' }, { spaces: 2 }); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'nx.json should be detected'); - return { name: 'nx', ok: true }; -} - -async function testTurbo() { - const root = await mkTmpDir('turbo'); - const nested = path.join(root, 'packages', 'x'); - await fs.ensureDir(nested); - await fs.writeJson(path.join(root, 'turbo.json'), { pipeline: {} }, { spaces: 2 }); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'turbo.json should be detected'); - return { name: 'turbo', ok: true }; -} - -async function testRush() { - const root = await mkTmpDir('rush'); - const nested = path.join(root, 'apps', 'a'); - await fs.ensureDir(nested); - await fs.writeJson(path.join(root, 'rush.json'), { projectFolderMinDepth: 1 }, { spaces: 2 }); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'rush.json should be detected'); - return { name: 'rush', ok: true }; -} - -async function testGoWorkAndMod() { - const root = await mkTmpDir('gowork'); - const mod = path.join(root, 'modA'); - const nested = path.join(mod, 'pkg'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'go.work'), 'go 1.22\nuse ./modA\n'); - await fs.writeFile(path.join(mod, 'go.mod'), 'module example.com/a\ngo 1.22\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'go.work should define the workspace root'); - return { name: 'go-work', ok: true }; -} - -async function testDenoJson() { - const root = await mkTmpDir('deno'); - const nested = path.join(root, 'src'); - await fs.ensureDir(nested); - await fs.writeJson(path.join(root, 'deno.json'), { tasks: {} }, { spaces: 2 }); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'deno.json should be detected'); - return { name: 'deno-json', ok: true }; -} - -async function testGradleSettings() { - const root = await mkTmpDir('gradle'); - const nested = path.join(root, 'app'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'settings.gradle'), "rootProject.name='tmp'\n"); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'settings.gradle should be detected'); - return { name: 'gradle-settings', ok: true }; -} - -async function testMavenPom() { - const root = await mkTmpDir('maven'); - const nested = path.join(root, 'module'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'pom.xml'), '\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'pom.xml should be detected'); - return { name: 'maven-pom', ok: true }; -} - -async function testSbtBuild() { - const root = await mkTmpDir('sbt'); - const nested = path.join(root, 'sub'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'build.sbt'), 'name := "tmp"\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'build.sbt should be detected'); - return { name: 'sbt-build', ok: true }; -} - -async function testComposer() { - const root = await mkTmpDir('composer'); - const nested = path.join(root, 'src'); - await fs.ensureDir(nested); - await fs.writeJson(path.join(root, 'composer.json'), { name: 'tmp/pkg' }, { spaces: 2 }); - await fs.writeFile(path.join(root, 'composer.lock'), '{}\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'composer.{json,lock} should be detected'); - return { name: 'composer', ok: true }; -} - -async function testCargo() { - const root = await mkTmpDir('cargo'); - const nested = path.join(root, 'src'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'Cargo.toml'), "[package]\nname='tmp'\nversion='0.0.0'\n"); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'Cargo.toml should be detected'); - return { name: 'cargo', ok: true }; -} - -async function testNixFlake() { - const root = await mkTmpDir('nix'); - const nested = path.join(root, 'work'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'flake.nix'), '{ }\n'); - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'flake.nix should be detected'); - return { name: 'nix-flake', ok: true }; -} - -async function testChangesetConfig() { - const root = await mkTmpDir('changeset'); - const nested = path.join(root, 'pkg'); - await fs.ensureDir(nested); - await fs.ensureDir(path.join(root, '.changeset')); - await fs.writeJson( - path.join(root, '.changeset', 'config.json'), - { $schema: 'https://unpkg.com/@changesets/config@2.3.1/schema.json' }, - { spaces: 2 }, - ); - const found = await findProjectRoot(nested); - await assertEqual(found, root, '.changeset/config.json should be detected'); - return { name: 'changesets', ok: true }; -} - -async function testEnvCustomMarker() { - const root = await mkTmpDir('env-marker'); - const nested = path.join(root, 'dir'); - await fs.ensureDir(nested); - await fs.writeFile(path.join(root, 'MY_ROOT'), '\n'); - const prev = process.env.PROJECT_ROOT_MARKERS; - process.env.PROJECT_ROOT_MARKERS = 'MY_ROOT'; - try { - const found = await findProjectRoot(nested); - await assertEqual(found, root, 'custom env marker should be honored'); - } finally { - if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS; - else process.env.PROJECT_ROOT_MARKERS = prev; - } - return { name: 'env-custom-marker', ok: true }; -} - -async function testPackageLowPriorityVsLock() { - const root = await mkTmpDir('pkg-vs-lock'); - const nested = path.join(root, 'nested'); - await fs.ensureDir(path.join(nested, 'deep')); - await fs.writeJson(path.join(nested, 'package.json'), { name: 'nested' }, { spaces: 2 }); - await fs.writeFile(path.join(root, 'yarn.lock'), '\n'); - const found = await findProjectRoot(path.join(nested, 'deep')); - await assertEqual(found, root, 'lockfile at root should outrank nested package.json'); - return { name: 'package-vs-lock-priority', ok: true }; -} - -async function run() { - const tests = [ - testSentinel, - testOtherSentinels, - testGitCliAndMarker, - testHgMarkerOrCli, - testWorkspacePnpm, - testPackageJsonWorkspaces, - testLockfiles, - testLanguageConfigs, - testPreferOuterOnTie, - testBazelWorkspace, - testNx, - testTurbo, - testRush, - testGoWorkAndMod, - testDenoJson, - testGradleSettings, - testMavenPom, - testSbtBuild, - testComposer, - testCargo, - testNixFlake, - testChangesetConfig, - testEnvCustomMarker, - testPackageLowPriorityVsLock, - testSvnMarker, - testSymlinkStart, - testSubmoduleLikeInnerGitFile, - ]; - - const results = []; - for (const t of tests) { - try { - const r = await t(); - results.push({ ...r, ok: true }); - console.log(`✔ ${r.name}${r.skipped ? ' (skipped)' : ''}`); - } catch (error) { - console.error(`✖ ${t.name}:`, error && error.message ? error.message : error); - results.push({ name: t.name, ok: false, error: String(error) }); - } - } - - const failed = results.filter((r) => !r.ok); - console.log('\nSummary:'); - for (const r of results) { - console.log(`- ${r.name}: ${r.ok ? 'ok' : 'FAIL'}${r.skipped ? ' (skipped)' : ''}`); - } - - if (failed.length > 0) { - process.exitCode = 1; - } -} - -run().catch((error) => { - console.error('Fatal error:', error); - process.exit(1); -}); diff --git a/tools/flattener/xml.js b/tools/flattener/xml.js deleted file mode 100644 index 229f9a880..000000000 --- a/tools/flattener/xml.js +++ /dev/null @@ -1,82 +0,0 @@ -const fs = require('fs-extra'); -const { escapeXml } = require('../lib/xml-utils'); - -function indentFileContent(content) { - if (typeof content !== 'string') { - return String(content); - } - return content.split('\n').map((line) => ` ${line}`); -} - -function generateXMLOutput(aggregatedContent, outputPath) { - const { textFiles } = aggregatedContent; - const writeStream = fs.createWriteStream(outputPath, { encoding: 'utf8' }); - - return new Promise((resolve, reject) => { - writeStream.on('error', reject); - writeStream.on('finish', resolve); - - writeStream.write('\n'); - writeStream.write('\n'); - - // Sort files by path for deterministic order - const filesSorted = [...textFiles].sort((a, b) => a.path.localeCompare(b.path)); - let index = 0; - - const writeNext = () => { - if (index >= filesSorted.length) { - writeStream.write('\n'); - writeStream.end(); - return; - } - - const file = filesSorted[index++]; - const p = escapeXml(file.path); - const content = typeof file.content === 'string' ? file.content : ''; - - if (content.length === 0) { - writeStream.write(`\t\n`); - setTimeout(writeNext, 0); - return; - } - - const needsCdata = content.includes('<') || content.includes('&') || content.includes(']]>'); - if (needsCdata) { - // Open tag and CDATA on their own line with tab indent; content lines indented with two tabs - writeStream.write(`\t" inside content, trim trailing newlines, indent each line with two tabs - const safe = content.replaceAll(']]>', ']]]]>'); - const trimmed = safe.replace(/[\r\n]+$/, ''); - const indented = - trimmed.length > 0 - ? trimmed - .split('\n') - .map((line) => `\t\t${line}`) - .join('\n') - : ''; - writeStream.write(indented); - // Close CDATA and attach closing tag directly after the last content line - writeStream.write(']]>\n'); - } else { - // Write opening tag then newline; indent content with two tabs; attach closing tag directly after last content char - writeStream.write(`\t\n`); - const trimmed = content.replace(/[\r\n]+$/, ''); - const indented = - trimmed.length > 0 - ? trimmed - .split('\n') - .map((line) => `\t\t${line}`) - .join('\n') - : ''; - writeStream.write(indented); - writeStream.write(`\n`); - } - - setTimeout(writeNext, 0); - }; - - writeNext(); - }); -} - -module.exports = { generateXMLOutput }; diff --git a/tools/schema/agent.js b/tools/schema/agent.js index 7d106e616..b6a36a985 100644 --- a/tools/schema/agent.js +++ b/tools/schema/agent.js @@ -210,7 +210,6 @@ function buildAgentSchema(expectedModule) { critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(), menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }), prompts: z.array(buildPromptSchema()).optional(), - webskip: z.boolean().optional(), discussion: z.boolean().optional(), conversational_knowledge: z.array(z.object({}).passthrough()).min(1).optional(), }) From f383b9befe7f5301a7d182b369eccd6437c5cab9 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 16:09:18 -0600 Subject: [PATCH 07/38] fix to draft-changelog, and changelog entry for next version bumb --- .claude/skills/draft-changelog/SKILL.md | 2 +- .../draft-changelog/prompts/instructions.md | 26 ++++++++++- CHANGELOG.md | 43 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/.claude/skills/draft-changelog/SKILL.md b/.claude/skills/draft-changelog/SKILL.md index f25f8c48f..1ebebe3ff 100644 --- a/.claude/skills/draft-changelog/SKILL.md +++ b/.claude/skills/draft-changelog/SKILL.md @@ -1,6 +1,6 @@ --- name: draft-changelog -description: Analyzes changes since the last release and generates a draft changelog entry +description: Analyzes changes since last release and updates CHANGELOG.md ONLY. Does NOT trigger releases. disable-model-invocation: true --- diff --git a/.claude/skills/draft-changelog/prompts/instructions.md b/.claude/skills/draft-changelog/prompts/instructions.md index a9b98bb8f..ef3feccef 100644 --- a/.claude/skills/draft-changelog/prompts/instructions.md +++ b/.claude/skills/draft-changelog/prompts/instructions.md @@ -1,5 +1,17 @@ # Draft Changelog Execution +## ⚠️ IMPORTANT - READ FIRST + +**This skill ONLY updates CHANGELOG.md. That is its entire purpose.** + +- **DO** update CHANGELOG.md with the new version entry +- **DO** present the draft for user review before editing +- **DO NOT** trigger any GitHub release workflows +- **DO NOT** run any other skills or workflows automatically +- **DO NOT** make any commits + +After the changelog is complete, you may suggest the user can run `/release-module` if they want to proceed with the actual release — but NEVER trigger it yourself. + ## Input Project path (or run from project root) @@ -53,6 +65,18 @@ Guidelines: - Clear, concise language - For breaking changes, clearly indicate impact -## Step 4: Present Draft +## Step 4: Present Draft & Update CHANGELOG.md Show the draft with current version, last tag, commit count, and options to edit/retry. + +When user accepts: +1. Update CHANGELOG.md with the new entry (insert at top, after `# Changelog` header) +2. STOP. That's it. You're done. + +You may optionally suggest: *"When ready, you can run `/release-module` to create the actual release."* + +**DO NOT:** +- Trigger any GitHub workflows +- Run any other skills +- Make any commits +- Do anything beyond updating CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2420869b4..67b3aa54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## [6.0.0-Beta.6] + +**Release: February 4, 2026** + +### 🌟 Key Highlights + +1. **Cross-File Reference Validator**: Comprehensive tool to detect broken file references, preventing 59 known bugs (~25% of historical issues) +2. **New AutocompleteMultiselect Prompt**: Searchable multi-select with improved tool/IDE selection UX +3. **Critical Installer Fixes**: Windows CRLF parsing, Gemini CLI TOML support, file extension preservation +4. **Codebase Cleanup**: Removed dead Excalidraw/flattener artifacts (-3,798 lines) + +### 🎁 Features + +* **Cross-File Reference Validator** — Validates ~483 references across ~217 source files, detecting absolute path leaks and broken references (PR #1494) +* **AutocompleteMultiselect Prompt** — Upgraded `@clack/prompts` to v1.0.0 with custom searchable multiselect, Tab-to-fill-placeholder behavior, and improved tool/IDE selection UX (PR #1514) +* **OT Domains** — Added `process_control` and `building_automation` domains with high complexity ratings (PR #1510) +* **Documentation Reference Pages** — Added `docs/reference/agents.md`, `commands.md`, and `testing.md` (PR #1525) + +### 🐛 Bug Fixes + +* **Critical Installer Fixes** — Fixed CRLF line ending parsing on Windows, Gemini CLI TOML support, file extension preservation, Codex task generation, Windows path handling, and CSV parsing (PR #1492) +* **Double Tool Questioning** — Removed redundant tool questioning during installation (df176d42) +* **QA Agent Rename** — Renamed Quinn agent to `qa` for naming consistency (PR #1508) +* **Documentation Organization** — Fixed documentation ordering and links, hide BMGD pages from main LLM docs (PR #1525) + +### ♻️ Refactoring + +* **Excalidraw/Flattener Removal** — Removed dead artifacts no longer supported beyond beta: Excalidraw workflows, flattener tool, and 12+ diagram creation workflows (-3,798 lines) (f699a368) +* **Centralized Constants** — Centralized `BMAD_FOLDER_NAME` to reduce hardcoded strings (PR #1492) +* **Cross-Platform Paths** — Fixed path separator inconsistencies in agent IDs (PR #1492) + +### 📚 Documentation + +* **BMGD Diataxis Refactor** — Refactored BMGD documentation using Diataxis principles for better organization (PR #1502) +* **Generate Project Context** — Restored `generate-project-context` workflow for brownfield project analysis (PR #1491) + +### 🔧 Maintenance + +* **Dependency Updates** — Upgraded `@clack/prompts` from v0.11.0 to v1.0.0 and added `@clack/core` (PR #1514) +* **CI Integration** — Added `validate:refs` to CI quality workflow with warning annotations (PR #1494) + +--- + ## [6.0.0-Beta.5] ### 🎁 Features From 89d0c34eb725547b143b2f4346e01f5eaab7a58d Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 16:10:53 -0600 Subject: [PATCH 08/38] 6.0.0-Beta.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3888b281..ba768eff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.6", "license": "MIT", "dependencies": { "@clack/core": "^1.0.0", diff --git a/package.json b/package.json index 8798a6208..96a1814f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "bmad-method", - "version": "6.0.0-Beta.5", + "version": "6.0.0-Beta.6", "description": "Breakthrough Method of Agile AI-driven Development", "keywords": [ "agile", From 5276d58b2ca2b84104baa903761ce44aaafd2ece Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 17:04:34 -0600 Subject: [PATCH 09/38] fix internal project skills and rename to prefix with bmad-os --- .claude/skills/changelog-social/SKILL.md | 13 +- .claude/skills/draft-changelog/SKILL.md | 2 +- .claude/skills/gh-triage/SKILL.md | 2 +- .claude/skills/release-module/SKILL.md | 2 +- .../release-module/prompts/instructions.md | 20 - .../validation-report-prd-workflow.md | 433 ------------------ 6 files changed, 14 insertions(+), 458 deletions(-) delete mode 100644 src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md diff --git a/.claude/skills/changelog-social/SKILL.md b/.claude/skills/changelog-social/SKILL.md index e28b7abda..42e0bc3cf 100644 --- a/.claude/skills/changelog-social/SKILL.md +++ b/.claude/skills/changelog-social/SKILL.md @@ -1,5 +1,5 @@ --- -name: changelog-social +name: bmad-os-changelog-social description: Generate social media announcements for Discord, Twitter, and LinkedIn from the latest changelog entry. Use when user asks to create release announcements, social posts, or share changelog updates. Reads CHANGELOG.md in current working directory. Reference examples/ for tone and format. disable-model-invocation: true --- @@ -154,7 +154,13 @@ Read the appropriate example file before generating to match the established sty ## Output Format -Present both announcements in clearly labeled sections: +**CRITICAL: ALWAYS write to files** - Create files in `_bmad-output/social/` directory: + +1. `{repo-name}-discord-{version}.md` - Discord announcement +2. `{repo-name}-twitter-{version}.md` - Twitter post +3. `{repo-name}-linkedin-{version}.md` - LinkedIn post (if applicable) + +Also present a preview in the chat: ```markdown ## Discord Announcement @@ -166,4 +172,7 @@ Present both announcements in clearly labeled sections: [paste Twitter content here] ``` +Files created: +- `_bmad-output/social/{filename}` + Offer to make adjustments if the user wants different emphasis, tone, or content. diff --git a/.claude/skills/draft-changelog/SKILL.md b/.claude/skills/draft-changelog/SKILL.md index 1ebebe3ff..a246e069f 100644 --- a/.claude/skills/draft-changelog/SKILL.md +++ b/.claude/skills/draft-changelog/SKILL.md @@ -1,5 +1,5 @@ --- -name: draft-changelog +name: bmad-os-draft-changelog description: Analyzes changes since last release and updates CHANGELOG.md ONLY. Does NOT trigger releases. disable-model-invocation: true --- diff --git a/.claude/skills/gh-triage/SKILL.md b/.claude/skills/gh-triage/SKILL.md index 6a6d7c838..e5688f3ba 100644 --- a/.claude/skills/gh-triage/SKILL.md +++ b/.claude/skills/gh-triage/SKILL.md @@ -1,5 +1,5 @@ --- -name: gh-triage +name: bmad-os-gh-triage description: Fetch all GitHub issues via gh CLI and use AI agents to deeply analyze, cluster, and prioritize issues license: MIT disable-model-invocation: true diff --git a/.claude/skills/release-module/SKILL.md b/.claude/skills/release-module/SKILL.md index 3ba156577..17a718a32 100644 --- a/.claude/skills/release-module/SKILL.md +++ b/.claude/skills/release-module/SKILL.md @@ -1,5 +1,5 @@ --- -name: release-module +name: bmad-os-release-module description: Automates the complete release process for npm modules - version bump, changelog, git tag, npm publish, GitHub release disable-model-invocation: true --- diff --git a/.claude/skills/release-module/prompts/instructions.md b/.claude/skills/release-module/prompts/instructions.md index 39e45ac13..157ce0b33 100644 --- a/.claude/skills/release-module/prompts/instructions.md +++ b/.claude/skills/release-module/prompts/instructions.md @@ -42,26 +42,6 @@ Publish the package. Create release with changelog notes using `gh release create`. -### Step 10: Create Social Announcement - -Create a social media announcement file at `_bmad-output/social/{repo-name}-release.md`. - -Format: -```markdown -# {name} v{version} Released - -## Highlights -{2-3 bullet points of key features/changes} - -## Links -- GitHub: {release-url} -- npm: {npm-url} -``` - -### Step 11: Confirm Completion - -Show npm, GitHub, and social announcement file paths. - ## Error Handling Stop immediately on any step failure. Inform user and suggest fix. diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md b/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md deleted file mode 100644 index faa41ff64..000000000 --- a/src/bmm/workflows/2-plan-workflows/create-prd/validation-report-prd-workflow.md +++ /dev/null @@ -1,433 +0,0 @@ ---- -validationTarget: 'PRD Workflow Structure' -validationDate: '2026-01-08' -inputDocuments: [] -validationStepsCompleted: ['discovery', 'frontmatter-validation', 'content-validation', 'documentation-validation', 'integration-validation', 'corrections-applied'] -validationStatus: COMPLETE - PRODUCTION READY ---- - -# PRD Workflow Validation Report - -**Workflow Being Validated:** _bmad/bmm/workflows/2-plan-workflows/create-prd -**Validation Date:** 2026-01-08 -**Validator:** BMAD Workflow Validation System - ---- - -## Executive Summary - -This validation report assesses the PRD workflow structure against BMAD workflow standards. The PRD workflow is a tri-modal workflow system with Create, Validate, and Edit phases. - ---- - -## 1. File Structure & Size Analysis - -### Folder Structure - -``` -prd/ -├── workflow.md (main workflow file) -├── steps-c/ (Create steps - 12 files) -├── steps-v/ (Validation steps - 13 files) -├── steps-e/ (Edit steps - 5 files) -├── data/ -│ └── prd-purpose.md -└── templates/ - └── prd-template.md -``` - -**✅ Structure Status**: PASS - All required folders present - -### File Size Analysis - -#### Steps-C (Create Steps) - 12 files -| File | Lines | Status | -| ------------------------ | ----- | ------------------- | -| step-01-init.md | 191 | ⚠️ Approaching limit | -| step-01b-continue.md | 153 | ✅ Good | -| step-02-discovery.md | 197 | ⚠️ Approaching limit | -| step-03-success.md | 226 | ⚠️ Approaching limit | -| step-04-journeys.md | 213 | ⚠️ Approaching limit | -| step-05-domain.md | 193 | ⚠️ Approaching limit | -| step-06-innovation.md | 226 | ⚠️ Approaching limit | -| step-07-project-type.md | 225 | ⚠️ Approaching limit | -| step-08-scoping.md | 228 | ⚠️ Approaching limit | -| step-09-functional.md | 231 | ⚠️ Approaching limit | -| step-10-nonfunctional.md | 242 | ⚠️ Approaching limit | -| step-11-polish.md | 217 | ⚠️ Approaching limit | -| step-12-complete.md | 185 | ✅ Good | - -#### Steps-V (Validation Steps) - 13 files -| File | Lines | Status | -| ---------------------------------------------- | ----- | ------------------- | -| step-v-01-discovery.md | 217 | ⚠️ Approaching limit | -| step-v-02-format-detection.md | 191 | ⚠️ Approaching limit | -| step-v-02b-parity-check.md | 209 | ⚠️ Approaching limit | -| step-v-03-density-validation.md | 174 | ✅ Good | -| step-v-04-brief-coverage-validation.md | 214 | ⚠️ Approaching limit | -| step-v-05-measurability-validation.md | 228 | ⚠️ Approaching limit | -| step-v-06-traceability-validation.md | 217 | ⚠️ Approaching limit | -| step-v-07-implementation-leakage-validation.md | 205 | ⚠️ Approaching limit | -| step-v-08-domain-compliance-validation.md | 243 | ⚠️ Approaching limit | -| step-v-09-project-type-validation.md | 263 | ❌ Exceeds limit | -| step-v-10-smart-validation.md | 209 | ⚠️ Approaching limit | -| step-v-11-holistic-quality-validation.md | 264 | ❌ Exceeds limit | -| step-v-12-completeness-validation.md | 242 | ⚠️ Approaching limit | -| step-v-13-report-complete.md | 231 | ⚠️ Approaching limit | - -#### Steps-E (Edit Steps) - 5 files -| File | Lines | Status | -| ------------------------------- | ----- | ------------------- | -| step-e-01-discovery.md | 206 | ⚠️ Approaching limit | -| step-e-01b-legacy-conversion.md | 208 | ⚠️ Approaching limit | -| step-e-02-review.md | 249 | ⚠️ Approaching limit | -| step-e-03-edit.md | 253 | ❌ Exceeds limit | -| step-e-04-complete.md | 168 | ✅ Good | - -#### Data & Templates -| File | Lines | Status | -| ------------------------- | ----- | ------------------- | -| data/prd-purpose.md | 197 | ⚠️ Approaching limit | -| templates/prd-template.md | 10 | ✅ Good | -| workflow.md | 114 | ✅ Good | - -### File Size Statistics - -- **Total Files**: 32 markdown files -- **✅ Good (<200 lines)**: 6 files (18.8%) -- **⚠️ Approaching limit (200-250)**: 23 files (71.9%) -- **❌ Exceeds limit (>250)**: 3 files (9.4%) -- **Average lines per file**: 213.3 lines - -### ⚠️ Recommendations - -1. **Files Exceeding 250-line limit**: - - `step-v-09-project-type-validation.md` (263 lines) - Consider splitting into sub-steps - - `step-v-11-holistic-quality-validation.md` (264 lines) - Consider splitting into sub-steps - - `step-e-03-edit.md` (253 lines) - Consider splitting into sub-steps - -2. **Files Approaching Limit**: - - Many files are in the 200-250 line range - - Monitor these files as further additions may push them over the limit - - Consider proactive refactoring where appropriate - ---- - -## 2. Frontmatter Structure Validation - -### Files Checked: 29 total files - -**✅ Overall Status:** ALL VALID - One Issue Fixed - -#### Main Workflow (workflow.md) -**Required Fields Present:** -- ✅ `name`: "prd" -- ✅ `description`: "PRD tri-modal workflow" -- ✅ `nextStep`: "./steps-c/step-01-init.md" -- ✅ `validateWorkflow`: "./steps-v/step-v-01-discovery.md" -- ✅ `editWorkflow`: "./steps-e/step-e-01-discovery.md" (FIXED - was assess-workflow.md) - -#### Create Steps (steps-c) -- ✅ All 13 files have proper name, description, nextStepFile -- ✅ Proper sequencing from step-01 through step-12 -- ✅ Consistent output file references - -#### Validation Steps (steps-v) -- ✅ All 13 files have complete frontmatter -- ✅ Proper sequential chain maintained -- ✅ No broken internal references - -#### Edit Steps (steps-e) -- ✅ All files have required fields -- ✅ Proper routing with altStepFile references - -### ✅ All Issues Resolved - -**1. Broken Edit Workflow Reference:** -```yaml -# Current (INCORRECT): -editWorkflow: './steps-e/step-e-01-assess-workflow.md' - -# Should be: -editWorkflow: './steps-e/step-e-01-discovery.md' -``` - -**2. Step Numbering Gap:** -- Original `step-11-complete.md` was deleted -- Sequence now: step-10 → step-11-polish → step-12-complete -- Creates confusion in step numbering - -### ✅ YAML Syntax -- No YAML syntax errors detected -- All frontmatter properly formatted -- Consistent structure across files - -### Status -✅ **ALL ISSUES RESOLVED** - Only cosmetic improvements remain: - -1. **✅ FIXED**: Edit workflow path corrected in workflow.md -2. **⚠️ OPTIONAL**: Address step numbering gap for clarity -3. **⚠️ OPTIONAL**: Rename step-01b-continue.md to step-01a-continue.md for consistency - ---- - -## 3. Step File Content Validation - -### Content Quality Assessment: 4.5/5 - EXCELLENT - -#### Files Reviewed: 10 representative files across all modes - -#### ✅ Strengths - -**1. Comprehensive Structure:** -- Clear step goal sections in all files -- Detailed mandatory execution rules -- Well-defined execution protocols -- Context boundaries clearly specified -- Mandatory sequence with numbered steps -- System success/failure metrics present - -**2. BMAD Compliance:** -- ✅ JIT loading references consistently mentioned -- ✅ State tracking requirements documented -- ✅ Append-only building instructions present -- ✅ Critical rules properly emphasized with emojis -- ✅ Sequential enforcement clearly stated - -**3. Instructional Quality:** -- Clear, unambiguous instructions -- Proper menu handling rules (where applicable) -- Excellent continuation checks -- Strong role definition for each mode - -**4. Role Clarity:** -- Create Mode: "Product-focused PM facilitator" -- Validate Mode: "Validation Architect and Quality Assurance Specialist" -- Edit Mode: "PRD improvement specialist" - -#### ⚠️ Minor Improvement Opportunities - -**1. Header Formatting:** -- Some inconsistency in header level usage across files -- Recommend standardizing H2/H3 usage - -**2. Edit Mode Completeness:** -- Edit mode has fewer steps (5 vs 12/13 for other modes) -- Documentation marks it as "Future" but implementation exists - -#### Recommendations -1. **LOW PRIORITY**: Standardize header formatting across all step files -2. **LOW PRIORITY**: Complete remaining edit mode steps for parity -3. **MAINTAIN**: Current excellent quality standards - ---- - -## 4. Documentation Validation - -### Documentation Completeness: ✅ COMPREHENSIVE - -#### Main Components Present -- ✅ Workflow Definition (workflow.md) -- ✅ Purpose Document (data/prd-purpose.md) -- ✅ Template (templates/prd-template.md) -- ✅ Three Mode Implementations (Create: 12, Validate: 13, Edit: 5 steps) - -#### Clarity Assessment: ✅ EXCELLENT - -**Strong Points:** -1. Clear mode determination (commands, flags, menu selection) -2. Detailed routing instructions for each mode -3. Comprehensive workflow architecture explanation -4. Well-defined critical rules with visual emphasis -5. Professional presentation with consistent formatting - -#### ⚠️ Minor Issues Found - -**1. Step Count Mismatch:** -- workflow.md mentions "11 steps" for Create mode -- Actually implements 12 steps -- Could confuse users - -**2. Edit Mode Status:** -- workflow.md calls Edit mode "Future" -- Edit mode steps are actually implemented -- Should reflect current status - -**3. Template Completeness:** -- PRD template is minimal (10 lines) -- Could benefit from section placeholders - -**4. Missing README:** -- No onboarding documentation for new users -- Not critical but would be helpful - -#### Recommendations - -**HIGH PRIORITY:** -1. Fix step count reference to match implementation (12 steps) -2. Update edit mode documentation to "Implemented" - -**MEDIUM PRIORITY:** -3. Enhance PRD template with section structure -4. Add quick-start README for new users - -**LOW PRIORITY:** -5. Add troubleshooting section -6. Document external dependencies (domain-complexity.csv, project-types.csv) - ---- - -## 5. Integration & Compatibility Validation - -### Integration Status: 85% Ready - -#### ✅ Successfully Integrated Components - -**1. Agent Menu Registration:** -- ✅ Registered in PM agent menu -- ✅ Trigger: `PR` or fuzzy match on `prd` -- ✅ Command: `/bmad:bmm:workflows:create-prd` -- ✅ Proper workflow path configuration - -**2. External Workflow References:** -- ✅ Party-mode workflow: Exists at `{project-root}/_bmad/core/workflows/party-mode/workflow.md` -- ✅ Advanced-elicitation task: Exists at `{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml` - -**3. Directory Structure:** -- ✅ Complete step architecture (all 3 modes) -- ✅ All referenced step files exist -- ✅ Data files available - -#### ✅ Configuration & Installation - WORKING AS DESIGNED - -**1. BMM Config Reference:** -- Path: `{project-root}/_bmad/bmm/config.yaml` -- **Status:** ✅ Correct installation-time placeholder -- Resolves to actual config during workflow installation -- **Note:** This is expected behavior, not an issue - -**2. Planning Artifacts Folder:** -- Reference: `{planning_artifacts}/prd.md` -- **Status:** ✅ Correct installation-time placeholder -- Created/resolved during workflow installation -- **Note:** This is expected behavior, not an issue - -**3. Edit Mode Implementation:** -- Current: 5 steps (Discovery, Legacy Conversion branch, Review, Edit, Complete) -- **Status:** ✅ Functionally complete -- Edit mode is inherently simpler than create mode (targeted improvements vs full creation) -- Uses subprocesses for complex operations -- Validation integration ensures quality -- **Note:** Edit workflow is complete and well-designed - -#### Configuration Analysis - -**Placeholder Usage:** -- `{project-root}`: ✅ Properly used -- `{planning_artifacts}`: ⚠️ Referenced but folder missing -- `{nextStep}`, `{validateWorkflow}`, etc: ✅ Properly resolved - -#### Recommendations - -**✅ ALL CRITICAL ISSUES RESOLVED:** - -The only true critical issue (edit workflow path) has been fixed. All other items flagged as "critical" were actually working as designed (installation-time placeholders). - -**LOW PRIORITY:** -3. Add CLI command registration for standalone execution (optional enhancement) -4. Consider adding workflow to additional agent menus (UX designer, architect) -5. Create standalone execution documentation (nice-to-have) -6. Address step numbering gap if desired (cosmetic) - ---- - -## 6. Executive Summary & Overall Assessment - -### Overall Validation Status: ✅ PRODUCTION-READY - -#### Validation Scores by Category - -| Category | Status | Score | Notes | -| -------------------------- | ----------- | ------ | --------------------------------------------- | -| **File Structure & Size** | ⚠️ WARNINGS | 7/10 | 3 files exceed 250-line limit, 23 approaching | -| **Frontmatter Validation** | ✅ PASS | 9/10 | One broken path reference | -| **Step Content Quality** | ✅ EXCELLENT | 9.5/10 | High-quality instructional design | -| **Documentation** | ✅ EXCELLENT | 9/10 | Comprehensive, minor inconsistencies | -| **Integration** | ✅ PASS | 9/10 | All paths correct (one issue fixed) | -| **BMAD Compliance** | ✅ EXCELLENT | 9.5/10 | Strong adherence to standards | - -**Overall Score: 9.2/10 - EXCELLENT** - -#### ✅ Critical Action Items - ALL RESOLVED - -**ONLY ONE TRUE CRITICAL ISSUE EXISTED - NOW FIXED:** - -1. **✅ FIXED: Edit Workflow Path** - - File: `workflow.md` ✓ RESOLVED - - Changed from: `./steps-e/step-e-01-assess-workflow.md` - - Changed to: `./steps-e/step-e-01-discovery.md` - -**Items incorrectly flagged as critical (actually working as designed):** -- ✅ Configuration path references (installation-time placeholders) -- ✅ Planning artifacts folder (installation-time placeholder) - -#### High Priority Improvements - -2. **⚠️ Split Large Step Files** (>250 lines): - - `step-v-09-project-type-validation.md` (263 lines) - - `step-v-11-holistic-quality-validation.md` (264 lines) - - `step-e-03-edit.md` (253 lines) - -3. **⚠️ Update Documentation Inconsistencies**: - - Fix step count reference (11 → 12 steps in create mode) - - Update edit mode status (Future → Implemented) - -#### Medium Priority Enhancements - -4. **Enhance PRD Template** (currently minimal at 10 lines) -5. **Add quick-start README** for new users -6. **Address step numbering gap** (cosmetic - missing step-11-complete.md) - -#### Edit Mode Status - FUNCTIONALLY COMPLETE ✅ - -The edit workflow is **complete and well-designed** with 5 steps: -- Discovery → Legacy Conversion (branch) → Review → Edit → Complete -- Edit mode is inherently simpler than create mode (targeted improvements vs full creation) -- Uses subprocesses for complex operations -- Integrates with validation workflow - -**No additional steps needed.** - -### Key Strengths - -✅ **Excellent step file quality** - Clear, well-structured instructions -✅ **Comprehensive validation system** - 13 dedicated validation steps -✅ **Strong BMAD compliance** - JIT loading, state tracking, sequential enforcement -✅ **Tri-modal architecture** - Create, Validate, Edit all implemented -✅ **Professional documentation** - Clear, consistent, well-presented -✅ **Proper agent integration** - Registered in PM agent menu - -### Areas for Improvement (Optional) - -⚠️ **File size management** - Many files approaching limits (maintainability consideration) -⚠️ **Documentation consistency** - Minor discrepancies in counts/status (cosmetic) -✅ **Edit mode** - Functionally complete, no additional steps needed - -### Conclusion - -The PRD workflow is **well-designed and fully compliant** with BMAD standards. The step file architecture is exemplary, the content quality is excellent, and the documentation is comprehensive. The only critical issue (edit workflow path) has been **resolved**, and all other flagged items were actually working as designed (installation-time placeholders). - -**Current Status: ✅ PRODUCTION-READY** - -**Recommended Optional Enhancements:** -1. Split the 3 files exceeding 250-line limit (maintainability) -2. Update documentation inconsistencies (step counts, edit mode status) -3. Enhance PRD template and add quick-start README (user experience) - -The PRD workflow is ready for production use and fully compliant with BMAD workflow standards. - ---- - -**Validation Completed:** 2026-01-08 -**Validation Method:** Systematic subprocess analysis with maximum context coverage -**Validator:** BMAD Workflow Validation System (Wendy - Workflow Building Master) From 731bee26eae76b39b8ffc6f3d4a08e925b3ef93f Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 20:42:33 -0600 Subject: [PATCH 10/38] additional slash commands added that were missing from trivariate workflows --- CHANGELOG.md | 1 - src/bmm/module-help.csv | 16 ++--- .../create-product-brief/workflow.md | 1 - .../research/workflow-domain-research.md | 54 +++++++++++++++ .../research/workflow-market-research.md | 54 +++++++++++++++ .../research/workflow-technical-research.md | 54 +++++++++++++++ .../workflows/1-analysis/research/workflow.md | 1 - .../create-prd/workflow-validate-prd.md | 65 +++++++++++++++++++ .../2-plan-workflows/create-prd/workflow.md | 1 - .../create-ux-design/workflow.md | 1 - .../workflow.md | 1 - .../create-architecture/workflow.md | 1 - .../create-epics-and-stories/workflow.md | 1 - .../code-review/workflow.yaml | 2 - .../correct-course/workflow.yaml | 2 - .../create-story/workflow.yaml | 2 - .../4-implementation/dev-story/workflow.yaml | 2 - .../retrospective/workflow.yaml | 2 - .../sprint-planning/workflow.yaml | 2 - .../sprint-status/workflow.yaml | 3 - .../bmad-quick-flow/quick-spec/workflow.md | 1 - .../workflows/document-project/workflow.yaml | 2 - src/bmm/workflows/qa/automate/workflow.yaml | 2 - .../installers/lib/core/manifest-generator.js | 6 +- tools/cli/installers/lib/modules/manager.js | 4 -- 25 files changed, 238 insertions(+), 43 deletions(-) create mode 100644 src/bmm/workflows/1-analysis/research/workflow-domain-research.md create mode 100644 src/bmm/workflows/1-analysis/research/workflow-market-research.md create mode 100644 src/bmm/workflows/1-analysis/research/workflow-technical-research.md create mode 100644 src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b3aa54c..8508f9a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1244,7 +1244,6 @@ Located in `src/modules/bmb/workflows/agent/data/`: - **Workflow Vendoring**: Web bundler performs automatic cross-module dependency vendoring - **BMGD Module Extraction**: Game development split into standalone 4-phase structure -- **Enhanced Dependency Resolution**: Better handling of web_bundle: false workflows - **Advanced Elicitation Fix**: Added missing CSV files to workflow bundles - **Claude Code Fix**: Resolved README slash command installation regression diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index 968f1b594..48b49d4e3 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -10,19 +10,15 @@ bmm,anytime,Mermaid Generate,MG,,_bmad/bmm/agents/tech-writer/tech-writer.agent. bmm,anytime,Validate Document,VD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.",planning_artifacts,"validation report", bmm,anytime,Explain Concept,EC,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Create clear technical explanations with examples and diagrams for complex concepts. Breaks down into digestible sections using task-oriented approach.",project_knowledge,"explanation", bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=_bmad/bmm/data/project-context-template.md,"Expert Guided Facilitation through a single or multiple techniques",planning_artifacts,"brainstorming session", -bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=market,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents", -bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=domain,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project_knowledge","research documents", -bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow.md,bmad-bmm-research,false,analyst,Create Mode research_type=technical,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project_knowledge","research documents", -bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief", -bmm,1-analysis,Validate Brief,VB,40,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-validate-brief,false,analyst,Validate Mode,"Validates product brief completeness",planning_artifacts,"brief validation report", -bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-create-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd, -bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md,bmad-bmm-validate-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report", +bmm,1-analysis,Market Research,MR,20,_bmad/bmm/workflows/1-analysis/research/workflow-market-research.md,bmad-bmm-market-research,false,analyst,Create Mode,"Market analysis competitive landscape customer needs and trends","planning_artifacts|project-knowledge","research documents", +bmm,1-analysis,Domain Research,DR,21,_bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md,bmad-bmm-domain-research,false,analyst,Create Mode,"Industry domain deep dive subject matter expertise and terminology","planning_artifacts|project_knowledge","research documents", +bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md,bmad-bmm-technical-research,false,analyst,Create Mode,"Technical feasibility architecture options and implementation approaches","planning_artifacts|project_knowledge","research documents", +bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-product-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief", +bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md,bmad-bmm-create-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd, +bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md,bmad-bmm-validate-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report", bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design", -bmm,2-planning,Validate UX,VU,40,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Validate Mode,"Validates UX design deliverables",planning_artifacts,"ux validation report", bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture, -bmm,3-solutioning,Validate Architecture,VA,20,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,false,architect,Validate Mode,"Validates architecture completeness",planning_artifacts,"architecture validation report", bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories", -bmm,3-solutioning,Validate Epics and Stories,VE,40,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,false,pm,Validate Mode,"Validates epics and stories completeness",planning_artifacts,"epics validation report", bmm,3-solutioning,Check Implementation Readiness,IR,70,_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md,bmad-bmm-check-implementation-readiness,true,architect,Validate Mode,"Ensure PRD UX Architecture and Epics Stories are aligned",planning_artifacts,"readiness report", bmm,4-implementation,Sprint Planning,SP,10,_bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml,bmad-bmm-sprint-planning,true,sm,Create Mode,"Generate sprint plan for development tasks - this kicks off the implementation phase by producing a plan the implementation agents will follow in sequence for every story in the plan.",implementation_artifacts,"sprint status", bmm,4-implementation,Sprint Status,SS,20,_bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml,bmad-bmm-sprint-status,false,sm,Create Mode,"Anytime: Summarize sprint status and route to next workflow",,, diff --git a/src/bmm/workflows/1-analysis/create-product-brief/workflow.md b/src/bmm/workflows/1-analysis/create-product-brief/workflow.md index c17b18215..9d5e83f19 100644 --- a/src/bmm/workflows/1-analysis/create-product-brief/workflow.md +++ b/src/bmm/workflows/1-analysis/create-product-brief/workflow.md @@ -1,7 +1,6 @@ --- name: create-product-brief description: Create comprehensive product briefs through collaborative step-by-step discovery as creative Business Analyst working with the user as peers. -web_bundle: true --- # Product Brief Workflow diff --git a/src/bmm/workflows/1-analysis/research/workflow-domain-research.md b/src/bmm/workflows/1-analysis/research/workflow-domain-research.md new file mode 100644 index 000000000..91fcbaa9a --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-domain-research.md @@ -0,0 +1,54 @@ +--- +name: domain-research +description: Conduct domain research covering industry analysis, regulations, technology trends, and ecosystem dynamics using current web data and verified sources. +--- + +# Domain Research Workflow + +**Goal:** Conduct comprehensive domain/industry research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a domain research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **domain/industry research**. + +**What domain, industry, or sector do you want to research?** + +For example: +- 'The healthcare technology industry' +- 'Sustainable packaging regulations in Europe' +- 'Construction and building materials sector' +- 'Or any other domain you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Domain**: "What specific aspect of [domain] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO DOMAIN RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "domain"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/domain-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./domain-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for domain research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow-market-research.md b/src/bmm/workflows/1-analysis/research/workflow-market-research.md new file mode 100644 index 000000000..5669e6f24 --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-market-research.md @@ -0,0 +1,54 @@ +--- +name: market-research +description: Conduct market research covering market size, growth, competition, and customer insights using current web data and verified sources. +--- + +# Market Research Workflow + +**Goal:** Conduct comprehensive market research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a market research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **market research**. + +**What topic, problem, or area do you want to research?** + +For example: +- 'The electric vehicle market in Europe' +- 'Plant-based food alternatives market' +- 'Mobile payment solutions in Southeast Asia' +- 'Or anything else you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Topic**: "What exactly about [topic] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO MARKET RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "market"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/market-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./market-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for market research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow-technical-research.md b/src/bmm/workflows/1-analysis/research/workflow-technical-research.md new file mode 100644 index 000000000..2ac5420ce --- /dev/null +++ b/src/bmm/workflows/1-analysis/research/workflow-technical-research.md @@ -0,0 +1,54 @@ +--- +name: technical-research +description: Conduct technical research covering technology evaluation, architecture decisions, and implementation approaches using current web data and verified sources. +--- + +# Technical Research Workflow + +**Goal:** Conduct comprehensive technical research using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. + +**Your Role:** You are a technical research facilitator working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. + +## PREREQUISITE + +**⛔ Web search required.** If unavailable, abort and tell the user. + +## CONFIGURATION + +Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as a system-generated value + +## QUICK TOPIC DISCOVERY + +"Welcome {{user_name}}! Let's get started with your **technical research**. + +**What technology, tool, or technical area do you want to research?** + +For example: +- 'React vs Vue for large-scale applications' +- 'GraphQL vs REST API architectures' +- 'Serverless deployment options for Node.js' +- 'Or any other technical topic you have in mind...'" + +### Topic Clarification + +Based on the user's topic, briefly clarify: +1. **Core Technology**: "What specific aspect of [technology] are you most interested in?" +2. **Research Goals**: "What do you hope to achieve with this research?" +3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" + +## ROUTE TO TECHNICAL RESEARCH STEPS + +After gathering the topic and goals: + +1. Set `research_type = "technical"` +2. Set `research_topic = [discovered topic from discussion]` +3. Set `research_goals = [discovered goals from discussion]` +4. Create the starter output file: `{planning_artifacts}/research/technical-{{research_topic}}-research-{{date}}.md` with exact copy of the `./research.template.md` contents +5. Load: `./technical-steps/step-01-init.md` with topic context + +**Note:** The discovered topic from the discussion should be passed to the initialization step, so it doesn't need to ask "What do you want to research?" again - it can focus on refining the scope for technical research. + +**✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`** diff --git a/src/bmm/workflows/1-analysis/research/workflow.md b/src/bmm/workflows/1-analysis/research/workflow.md index 64f62bef1..c2dc3e6b9 100644 --- a/src/bmm/workflows/1-analysis/research/workflow.md +++ b/src/bmm/workflows/1-analysis/research/workflow.md @@ -1,7 +1,6 @@ --- name: research description: Conduct comprehensive research across multiple domains using current web data and verified sources - Market, Technical, Domain and other research types. -web_bundle: true --- # Research Workflow diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md new file mode 100644 index 000000000..67a1aafc8 --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md @@ -0,0 +1,65 @@ +--- +name: validate-prd +description: Validate an existing PRD against BMAD standards - comprehensive review for completeness, clarity, and quality +main_config: '{project-root}/_bmad/bmm/config.yaml' +validateWorkflow: './steps-v/step-v-01-discovery.md' +--- + +# PRD Validate Workflow + +**Goal:** Validate existing PRDs against BMAD standards through comprehensive review. + +**Your Role:** Validation Architect and Quality Assurance Specialist. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Validate Workflow + +"**Validate Mode: Validating an existing PRD against BMAD standards.**" + +Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." + +Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md index b13d7a7cf..63241afb5 100644 --- a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md @@ -5,7 +5,6 @@ main_config: '{project-root}/_bmad/bmm/config.yaml' nextStep: './steps-c/step-01-init.md' validateWorkflow: './steps-v/step-v-01-discovery.md' editWorkflow: './steps-e/step-e-01-discovery.md' -web_bundle: true --- # PRD Workflow (Tri-Modal) diff --git a/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md b/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md index d74cb4878..4af87c39a 100644 --- a/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +++ b/src/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md @@ -1,7 +1,6 @@ --- name: create-ux-design description: Work with a peer UX Design expert to plan your applications UX patterns, look and feel. -web_bundle: true --- # Create UX Design Workflow diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md b/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md index d7eb5969e..49d2afab9 100644 --- a/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md @@ -1,7 +1,6 @@ --- name: check-implementation-readiness description: 'Critical validation workflow that assesses PRD, Architecture, and Epics & Stories for completeness and alignment before implementation. Uses adversarial review approach to find gaps and issues.' -web_bundle: false --- # Implementation Readiness diff --git a/src/bmm/workflows/3-solutioning/create-architecture/workflow.md b/src/bmm/workflows/3-solutioning/create-architecture/workflow.md index d36c328e8..b75b4a46c 100644 --- a/src/bmm/workflows/3-solutioning/create-architecture/workflow.md +++ b/src/bmm/workflows/3-solutioning/create-architecture/workflow.md @@ -1,7 +1,6 @@ --- name: create-architecture description: Collaborative architectural decision facilitation for AI-agent consistency. Replaces template-driven architecture with intelligent, adaptive conversation that produces a decision-focused architecture document optimized for preventing agent conflicts. -web_bundle: true --- # Architecture Workflow diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md b/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md index a1e78a028..a0e232ab8 100644 --- a/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md @@ -1,7 +1,6 @@ --- name: create-epics-and-stories description: 'Transform PRD requirements and Architecture decisions into comprehensive stories organized by user value. This workflow requires completed PRD + Architecture documents (UX recommended if UI exists) and breaks down requirements into implementation-ready epics and user stories that incorporate all available technical and design context. Creates detailed, actionable stories with complete acceptance criteria for development teams.' -web_bundle: true --- # Create Epics and Stories diff --git a/src/bmm/workflows/4-implementation/code-review/workflow.yaml b/src/bmm/workflows/4-implementation/code-review/workflow.yaml index dd75c60d1..5b5f6b2fc 100644 --- a/src/bmm/workflows/4-implementation/code-review/workflow.yaml +++ b/src/bmm/workflows/4-implementation/code-review/workflow.yaml @@ -46,5 +46,3 @@ input_file_patterns: sharded_index: "{planning_artifacts}/*epic*/index.md" sharded_single: "{planning_artifacts}/*epic*/epic-{{epic_num}}.md" load_strategy: "SELECTIVE_LOAD" - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml index c85402eca..318b5a7dc 100644 --- a/src/bmm/workflows/4-implementation/correct-course/workflow.yaml +++ b/src/bmm/workflows/4-implementation/correct-course/workflow.yaml @@ -54,5 +54,3 @@ instructions: "{installed_path}/instructions.md" validation: "{installed_path}/checklist.md" checklist: "{installed_path}/checklist.md" default_output_file: "{planning_artifacts}/sprint-change-proposal-{date}.md" - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/create-story/workflow.yaml b/src/bmm/workflows/4-implementation/create-story/workflow.yaml index 57716af54..1f3ac9784 100644 --- a/src/bmm/workflows/4-implementation/create-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/create-story/workflow.yaml @@ -55,5 +55,3 @@ input_file_patterns: whole: "{planning_artifacts}/*epic*.md" sharded: "{planning_artifacts}/*epic*/*.md" load_strategy: "SELECTIVE_LOAD" # Only load needed epic - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml index 0646d20f4..daf152b71 100644 --- a/src/bmm/workflows/4-implementation/dev-story/workflow.yaml +++ b/src/bmm/workflows/4-implementation/dev-story/workflow.yaml @@ -21,5 +21,3 @@ story_file: "" # Explicit story path; auto-discovered if empty implementation_artifacts: "{config_source}:implementation_artifacts" sprint_status: "{implementation_artifacts}/sprint-status.yaml" project_context: "**/project-context.md" - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml index 7cbf9f662..b92ecaf1b 100644 --- a/src/bmm/workflows/4-implementation/retrospective/workflow.yaml +++ b/src/bmm/workflows/4-implementation/retrospective/workflow.yaml @@ -53,5 +53,3 @@ input_file_patterns: sprint_status_file: "{implementation_artifacts}/sprint-status.yaml" story_directory: "{implementation_artifacts}" retrospectives_folder: "{implementation_artifacts}" - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml index 7f8686cd5..7b157633c 100644 --- a/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-planning/workflow.yaml @@ -49,5 +49,3 @@ input_file_patterns: # Output configuration default_output_file: "{status_file}" - -web_bundle: false diff --git a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml index fe8b5ff35..8946f0291 100644 --- a/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml +++ b/src/bmm/workflows/4-implementation/sprint-status/workflow.yaml @@ -28,6 +28,3 @@ input_file_patterns: description: "Sprint status file generated by sprint-planning" whole: "{implementation_artifacts}/sprint-status.yaml" load_strategy: "FULL_LOAD" - -# No web bundle needed -web_bundle: false diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md b/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md index bb6c877a7..7c41b948d 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md @@ -2,7 +2,6 @@ name: quick-spec description: Conversational spec engineering - ask questions, investigate code, produce implementation-ready tech-spec. main_config: '{project-root}/_bmad/bmm/config.yaml' -web_bundle: true # Checkpoint handler paths advanced_elicitation: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml' diff --git a/src/bmm/workflows/document-project/workflow.yaml b/src/bmm/workflows/document-project/workflow.yaml index cd9a09de5..4667d7c0b 100644 --- a/src/bmm/workflows/document-project/workflow.yaml +++ b/src/bmm/workflows/document-project/workflow.yaml @@ -20,5 +20,3 @@ validation: "{installed_path}/checklist.md" # Required data files - CRITICAL for project type detection and documentation requirements documentation_requirements_csv: "{installed_path}/documentation-requirements.csv" - -web_bundle: false diff --git a/src/bmm/workflows/qa/automate/workflow.yaml b/src/bmm/workflows/qa/automate/workflow.yaml index b08727462..847365d7b 100644 --- a/src/bmm/workflows/qa/automate/workflow.yaml +++ b/src/bmm/workflows/qa/automate/workflow.yaml @@ -45,5 +45,3 @@ execution_hints: interactive: false autonomous: true iterative: false - -web_bundle: false diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 1e6e3c30a..8259fe226 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -159,7 +159,11 @@ class ManifestGenerator { // Recurse into subdirectories const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name; await findWorkflows(fullPath, newRelativePath); - } else if (entry.name === 'workflow.yaml' || entry.name === 'workflow.md') { + } else if ( + entry.name === 'workflow.yaml' || + entry.name === 'workflow.md' || + (entry.name.startsWith('workflow-') && entry.name.endsWith('.md')) + ) { // Parse workflow file (both YAML and MD formats) if (debug) { console.log(`[DEBUG] Found workflow file: ${fullPath}`); diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 1f523fba3..f06f5483c 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -781,10 +781,6 @@ class ModuleManager { return; } - // Remove web_bundle section using regex to preserve formatting - // Match the web_bundle key and all its content (including nested items) - // This handles both web_bundle: false and web_bundle: {...} - // Find the line that starts web_bundle const lines = yamlContent.split('\n'); let startIdx = -1; From bd620e38e5d82417a21439a762ce15c2bf1bcaca Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 21:34:47 -0600 Subject: [PATCH 11/38] trivariate workflows added --- src/bmm/agents/analyst.agent.yaml | 16 +- src/bmm/agents/pm.agent.yaml | 9 +- src/bmm/module-help.csv | 1 + .../workflows/1-analysis/research/workflow.md | 172 ------------------ .../create-prd/workflow-create-prd.md | 63 +++++++ .../create-prd/workflow-edit-prd.md | 65 +++++++ .../2-plan-workflows/create-prd/workflow.md | 149 --------------- .../installers/lib/core/manifest-generator.js | 38 +--- 8 files changed, 146 insertions(+), 367 deletions(-) delete mode 100644 src/bmm/workflows/1-analysis/research/workflow.md create mode 100644 src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md create mode 100644 src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md delete mode 100644 src/bmm/workflows/2-plan-workflows/create-prd/workflow.md diff --git a/src/bmm/agents/analyst.agent.yaml b/src/bmm/agents/analyst.agent.yaml index f63420a5b..c340f69c1 100644 --- a/src/bmm/agents/analyst.agent.yaml +++ b/src/bmm/agents/analyst.agent.yaml @@ -1,5 +1,3 @@ -# Business Analyst Agent Definition - agent: metadata: id: "_bmad/bmm/agents/analyst.md" @@ -23,9 +21,17 @@ agent: data: "{project-root}/_bmad/bmm/data/project-context-template.md" description: "[BP] Brainstorm Project: Expert Guided Facilitation through a single or multiple techniques with a final report" - - trigger: RS or fuzzy match on research - exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow.md" - description: "[RS] Research: Choose from or specify market, domain, competitive analysis, or technical research" + - trigger: MR or fuzzy match on market-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-market-research.md" + description: "[MR] Market Research: Market analysis, competitive landscape, customer needs and trends" + + - trigger: DR or fuzzy match on domain-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md" + description: "[DR] Domain Research: Industry domain deep dive, subject matter expertise and terminology" + + - trigger: TR or fuzzy match on technical-research + exec: "{project-root}/_bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md" + description: "[TR] Technical Research: Technical feasibility, architecture options and implementation approaches" - trigger: CB or fuzzy match on product-brief exec: "{project-root}/_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md" diff --git a/src/bmm/agents/pm.agent.yaml b/src/bmm/agents/pm.agent.yaml index 1fa22545e..9ce0bf32f 100644 --- a/src/bmm/agents/pm.agent.yaml +++ b/src/bmm/agents/pm.agent.yaml @@ -1,6 +1,3 @@ -# Product Manager Agent Definition -# This file defines the PM agent for the BMAD BMM module - agent: metadata: id: "_bmad/bmm/agents/pm.md" @@ -22,15 +19,15 @@ agent: menu: - trigger: CP or fuzzy match on create-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md" description: "[CP] Create PRD: Expert led facilitation to produce your Product Requirements Document" - trigger: VP or fuzzy match on validate-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md" description: "[VP] Validate PRD: Validate a Product Requirements Document is comprehensive, lean, well organized and cohesive" - trigger: EP or fuzzy match on edit-prd - exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow.md" + exec: "{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md" description: "[EP] Edit PRD: Update an existing Product Requirements Document" - trigger: CE or fuzzy match on epics-stories diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index 48b49d4e3..635bb8a81 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -16,6 +16,7 @@ bmm,1-analysis,Technical Research,TR,22,_bmad/bmm/workflows/1-analysis/research/ bmm,1-analysis,Create Brief,CB,30,_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md,bmad-bmm-create-product-brief,false,analyst,Create Mode,"A guided experience to nail down your product idea",planning_artifacts,"product brief", bmm,2-planning,Create PRD,CP,10,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md,bmad-bmm-create-prd,true,pm,Create Mode,"Expert led facilitation to produce your Product Requirements Document",planning_artifacts,prd, bmm,2-planning,Validate PRD,VP,20,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md,bmad-bmm-validate-prd,false,pm,Validate Mode,"Validate PRD is comprehensive lean well organized and cohesive",planning_artifacts,"prd validation report", +bmm,2-planning,Edit PRD,EP,25,_bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md,bmad-bmm-edit-prd,false,pm,Edit Mode,"Improve and enhance an existing PRD",planning_artifacts,"updated prd", bmm,2-planning,Create UX,CU,30,_bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md,bmad-bmm-create-ux-design,false,ux-designer,Create Mode,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project",planning_artifacts,"ux design", bmm,3-solutioning,Create Architecture,CA,10,_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md,bmad-bmm-create-architecture,true,architect,Create Mode,"Guided Workflow to document technical decisions",planning_artifacts,architecture, bmm,3-solutioning,Create Epics and Stories,CE,30,_bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md,bmad-bmm-create-epics-and-stories,true,pm,Create Mode,"Create the Epics and Stories Listing",planning_artifacts,"epics and stories", diff --git a/src/bmm/workflows/1-analysis/research/workflow.md b/src/bmm/workflows/1-analysis/research/workflow.md deleted file mode 100644 index c2dc3e6b9..000000000 --- a/src/bmm/workflows/1-analysis/research/workflow.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -name: research -description: Conduct comprehensive research across multiple domains using current web data and verified sources - Market, Technical, Domain and other research types. ---- - -# Research Workflow - -**Goal:** Conduct comprehensive, exhaustive research across multiple domains using current web data and verified sources to produce complete research documents with compelling narratives and proper citations. - -**Document Standards:** - -- **Comprehensive Coverage**: Exhaustive research with no critical gaps -- **Source Verification**: Every factual claim backed by web sources with URL citations -- **Document Length**: As long as needed to fully cover the research topic -- **Professional Structure**: Compelling narrative introduction, detailed TOC, and comprehensive summary -- **Authoritative Sources**: Multiple independent sources for all critical claims - -**Your Role:** You are a research facilitator and web data analyst working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. - -**Final Deliverable**: A complete research document that serves as an authoritative reference on the research topic with: - -- Compelling narrative introduction -- Comprehensive table of contents -- Detailed research sections with proper citations -- Executive summary and conclusions - -## WORKFLOW ARCHITECTURE - -This uses **micro-file architecture** with **routing-based discovery**: - -- Each research type has its own step folder -- Step 01 discovers research type and routes to appropriate sub-workflow -- Sequential progression within each research type -- Document state tracked in output frontmatter - -## INITIALIZATION - -### Configuration Loading - -Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve: - -- `project_name`, `output_folder`, , `planning_artifacts`, `user_name` -- `communication_language`, `document_output_language`, `user_skill_level` -- `date` as a system-generated value - -### Paths - -- `installed_path` = `{project-root}/_bmad/bmm/workflows/1-analysis/research` -- `template_path` = `{installed_path}/research.template.md` -- `default_output_file` = `{planning_artifacts}/research/{{research_type}}-{{topic}}-research-{{date}}.md` (dynamic based on research type) - -## PREREQUISITE - -**⛔ Web search required.** If unavailable, abort and tell the user. - -## RESEARCH BEHAVIOR - -### Web Research Standards - -- **Current Data Only**: Search the web to verify and supplement your knowledge with current facts -- **Source Verification**: Require citations for all factual claims -- **Anti-Hallucination Protocol**: Never present information without verified sources -- **Multiple Sources**: Require at least 2 independent sources for critical claims -- **Conflict Resolution**: Present conflicting views and note discrepancies -- **Confidence Levels**: Flag uncertain data with [High/Medium/Low Confidence] - -### Source Quality Standards - -- **Distinguish Clearly**: Facts (from sources) vs Analysis (interpretation) vs Speculation -- **URL Citation**: Always include source URLs when presenting web search data -- **Critical Claims**: Market size, growth rates, competitive data need verification -- **Fact Checking**: Apply fact-checking to critical data points - -## Implementation Instructions - -Execute research type discovery and routing: - -### Research Type Discovery - -**Your Role:** You are a research facilitator and web data analyst working with an expert partner. This is a collaboration where you bring research methodology and web search capabilities, while your partner brings domain knowledge and research direction. - -**Research Standards:** - -- **Anti-Hallucination Protocol**: Never present information without verified sources -- **Current Data Only**: Search the web to verify and supplement your knowledge with current facts -- **Source Citation**: Always include URLs for factual claims from web searches -- **Multiple Sources**: Require 2+ independent sources for critical claims -- **Conflict Resolution**: Present conflicting views and note discrepancies -- **Confidence Levels**: Flag uncertain data with [High/Medium/Low Confidence] - -### Collaborative Research Discovery - -"Welcome {{user_name}}! I'm excited to work with you as your research partner. I bring web research capabilities with rigorous source verification, while you bring the domain expertise and research direction. - -**Let me help you clarify what you'd like to research.** - -**First, tell me: What specific topic, problem, or area do you want to research?** - -For example: - -- 'The electric vehicle market in Europe' -- 'Cloud migration strategies for healthcare' -- 'AI implementation in financial services' -- 'Sustainable packaging regulations' -- 'Or anything else you have in mind...' - -### Topic Exploration and Clarification - -Based on the user's initial topic, explore and refine the research scope: - -#### Topic Clarification Questions: - -1. **Core Topic**: "What exactly about [topic] are you most interested in?" -2. **Research Goals**: "What do you hope to achieve with this research?" -3. **Scope**: "Should we focus broadly or dive deep into specific aspects?" -4. **Timeline**: "Are you looking at current state, historical context, or future trends?" -5. **Application**: "How will you use this research? (product development, strategy, academic, etc.)" - -#### Context Building: - -- **Initial Input**: User provides topic or research interest -- **Collaborative Refinement**: Work together to clarify scope and objectives -- **Goal Alignment**: Ensure research direction matches user needs -- **Research Boundaries**: Establish clear focus areas and deliverables - -### Research Type Identification - -After understanding the research topic and goals, identify the most appropriate research approach: - -**Research Type Options:** - -1. **Market Research** - Market size, growth, competition, customer insights - _Best for: Understanding market dynamics, customer behavior, competitive landscape_ - -2. **Domain Research** - Industry analysis, regulations, technology trends in specific domain - _Best for: Understanding industry context, regulatory environment, ecosystem_ - -3. **Technical Research** - Technology evaluation, architecture decisions, implementation approaches - _Best for: Technical feasibility, technology selection, implementation strategies_ - -**Recommendation**: Based on [topic] and [goals], I recommend [suggested research type] because [specific rationale]. - -**What type of research would work best for your needs?** - -### Research Type Routing - -Based on user selection, route to appropriate sub-workflow with the discovered topic using the following IF block sets of instructions. YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - -#### If Market Research: - -- Set `research_type = "market"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/market-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./market-steps/step-01-init.md` with topic context - -#### If Domain Research: - -- Set `research_type = "domain"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/domain-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./domain-steps/step-01-init.md` with topic context - -#### If Technical Research: - -- Set `research_type = "technical"` -- Set `research_topic = [discovered topic from discussion]` -- Create the starter output file: `{planning_artifacts}/research/technical-{{research_topic}}-research-{{date}}.md` with exact copy of the ./research.template.md contents -- Load: `./technical-steps/step-01-init.md` with topic context - -**Important**: The discovered topic from the collaborative discussion should be passed to the research initialization steps, so they don't need to ask "What do you want to research?" again - they can focus on refining the scope for their specific research type. - -**Note:** All research workflows require web search for current data and source verification. diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md new file mode 100644 index 000000000..7d10ec3ed --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md @@ -0,0 +1,63 @@ +--- +name: create-prd +description: Create a comprehensive PRD (Product Requirements Document) through structured workflow facilitation +main_config: '{project-root}/_bmad/bmm/config.yaml' +nextStep: './steps-c/step-01-init.md' +--- + +# PRD Create Workflow + +**Goal:** Create comprehensive PRDs through structured workflow facilitation. + +**Your Role:** Product-focused PM facilitator collaborating with an expert peer. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Create Workflow + +"**Create Mode: Creating a new PRD from scratch.**" + +Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md new file mode 100644 index 000000000..5cb05af53 --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md @@ -0,0 +1,65 @@ +--- +name: edit-prd +description: Edit and improve an existing PRD - enhance clarity, completeness, and quality +main_config: '{project-root}/_bmad/bmm/config.yaml' +editWorkflow: './steps-e/step-e-01-discovery.md' +--- + +# PRD Edit Workflow + +**Goal:** Edit and improve existing PRDs through structured enhancement workflow. + +**Your Role:** PRD improvement specialist. + +You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. + +## WORKFLOW ARCHITECTURE + +This uses **step-file architecture** for disciplined execution: + +### Core Principles + +- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly +- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so +- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed +- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document +- **Append-Only Building**: Build documents by appending content as directed to the output file + +### Step Processing Rules + +1. **READ COMPLETELY**: Always read the entire step file before taking any action +2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate +3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection +4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) +5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step +6. **LOAD NEXT**: When directed, read fully and follow the next step file + +### Critical Rules (NO EXCEPTIONS) + +- 🛑 **NEVER** load multiple step files simultaneously +- 📖 **ALWAYS** read entire step file before execution +- 🚫 **NEVER** skip steps or optimize the sequence +- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step +- 🎯 **ALWAYS** follow the exact instructions in the step file +- ⏸️ **ALWAYS** halt at menus and wait for user input +- 📋 **NEVER** create mental todo lists from future steps + +## INITIALIZATION SEQUENCE + +### 1. Configuration Loading + +Load and read full config from {main_config} and resolve: + +- `project_name`, `output_folder`, `planning_artifacts`, `user_name` +- `communication_language`, `document_output_language`, `user_skill_level` +- `date` as system-generated current datetime + +✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. + +### 2. Route to Edit Workflow + +"**Edit Mode: Improving an existing PRD.**" + +Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." + +Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md b/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md deleted file mode 100644 index 63241afb5..000000000 --- a/src/bmm/workflows/2-plan-workflows/create-prd/workflow.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: create-prd -description: PRD tri-modal workflow - Create, Validate, or Edit comprehensive PRDs -main_config: '{project-root}/_bmad/bmm/config.yaml' -nextStep: './steps-c/step-01-init.md' -validateWorkflow: './steps-v/step-v-01-discovery.md' -editWorkflow: './steps-e/step-e-01-discovery.md' ---- - -# PRD Workflow (Tri-Modal) - -**Goal:** Create, Validate, or Edit comprehensive PRDs through structured workflows. - -**Your Role:** -- **Create Mode:** Product-focused PM facilitator collaborating with an expert peer -- **Validate Mode:** Validation Architect and Quality Assurance Specialist -- **Edit Mode:** PRD improvement specialist - -You will continue to operate with your given name, identity, and communication_style, merged with the details of this role description. - ---- - -## MODE DETERMINATION - -### Detect Workflow Mode - -Determine which mode to invoke based on: - -1. **Command/Invocation:** - - "create prd" or "new prd" → Create mode - - "validate prd" or "check prd" → Validate mode - - "edit prd" or "improve prd" → Edit mode - -2. **Context Detection:** - - If invoked with -c flag → Create mode - - If invoked with -v flag → Validate mode - - If invoked with -e flag → Edit mode - -3. **Menu Selection (if unclear):** - -If mode cannot be determined from invocation: -"**PRD Workflow - Select Mode:** - -**[C] Create** - Create a new PRD from scratch -**[V] Validate** - Validate an existing PRD against BMAD standards -**[E] Edit** - Improve an existing PRD - -Which mode would you like?" - -Wait for user selection. - -### Route to Appropriate Workflow - -**IF Create Mode:** -"**Create Mode: Creating a new PRD from scratch.**" -Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) - -**IF Validate Mode:** -"**Validate Mode: Validating an existing PRD against BMAD standards.**" -Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." -Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) - -**IF Edit Mode:** -"**Edit Mode: Improving an existing PRD.**" -Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." -Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) - ---- - -## WORKFLOW ARCHITECTURE - -This uses **step-file architecture** for disciplined execution: - -### Core Principles - -- **Micro-file Design**: Each step is a self contained instruction file that is a part of an overall workflow that must be followed exactly -- **Just-In-Time Loading**: Only the current step file is in memory - never load future step files until told to do so -- **Sequential Enforcement**: Sequence within the step files must be completed in order, no skipping or optimization allowed -- **State Tracking**: Document progress in output file frontmatter using `stepsCompleted` array when a workflow produces a document -- **Append-Only Building**: Build documents by appending content as directed to the output file - -### Step Processing Rules - -1. **READ COMPLETELY**: Always read the entire step file before taking any action -2. **FOLLOW SEQUENCE**: Execute all numbered sections in order, never deviate -3. **WAIT FOR INPUT**: If a menu is presented, halt and wait for user selection -4. **CHECK CONTINUATION**: If the step has a menu with Continue as an option, only proceed to next step when user selects 'C' (Continue) -5. **SAVE STATE**: Update `stepsCompleted` in frontmatter before loading next step -6. **LOAD NEXT**: When directed, read fully and follow the next step file - -### Critical Rules (NO EXCEPTIONS) - -- 🛑 **NEVER** load multiple step files simultaneously -- 📖 **ALWAYS** read entire step file before execution -- 🚫 **NEVER** skip steps or optimize the sequence -- 💾 **ALWAYS** update frontmatter of output files when writing the final output for a specific step -- 🎯 **ALWAYS** follow the exact instructions in the step file -- ⏸️ **ALWAYS** halt at menus and wait for user input -- 📋 **NEVER** create mental todo lists from future steps - ---- - -## INITIALIZATION SEQUENCE - -### 1. Mode Determination - -**Check if mode was specified in the command invocation:** - -- If user invoked with "create prd" or "new prd" or "build prd" or "-c" or "--create" → Set mode to **create** -- If user invoked with "validate prd" or "review prd" or "check prd" or "-v" or "--validate" → Set mode to **validate** -- If user invoked with "edit prd" or "modify prd" or "improve prd" or "-e" or "--edit" → Set mode to **edit** - -**If mode is still unclear, ask user:** - -"**PRD Workflow - Select Mode:** - -**[C] Create** - Create a new PRD from scratch -**[V] Validate** - Validate an existing PRD against BMAD standards -**[E] Edit** - Improve an existing PRD - -Which mode would you like?" - -Wait for user selection. - -### 2. Configuration Loading - -Load and read full config from {main_config} and resolve: - -- `project_name`, `output_folder`, `planning_artifacts`, `user_name` -- `communication_language`, `document_output_language`, `user_skill_level` -- `date` as system-generated current datetime - -✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the configured `{communication_language}`. - -### 3. Route to Appropriate Workflow - -**IF mode == create:** -"**Create Mode: Creating a new PRD from scratch.**" -Read fully and follow: `{nextStep}` (steps-c/step-01-init.md) - -**IF mode == validate:** -"**Validate Mode: Validating an existing PRD against BMAD standards.**" -Prompt for PRD path: "Which PRD would you like to validate? Please provide the path to the PRD.md file." -Then read fully and follow: `{validateWorkflow}` (steps-v/step-v-01-discovery.md) - -**IF mode == edit:** -"**Edit Mode: Improving an existing PRD.**" -Prompt for PRD path: "Which PRD would you like to edit? Please provide the path to the PRD.md file." -Then read fully and follow: `{editWorkflow}` (steps-e/step-e-01-discovery.md) diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 8259fe226..caea790eb 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -733,47 +733,15 @@ class ManifestGenerator { async writeWorkflowManifest(cfgDir) { const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - const parseCsvLine = (line) => { - const columns = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || []; - return columns.map((c) => c.replaceAll(/^"|"$/g, '')); - }; - - // Read existing manifest to preserve entries - const existingEntries = new Map(); - if (await fs.pathExists(csvPath)) { - const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - const parts = parseCsvLine(line); - if (parts.length >= 4) { - const [name, description, module, workflowPath] = parts; - existingEntries.set(`${module}:${name}`, { - name, - description, - module, - path: workflowPath, - }); - } - } - } - } // Create CSV header - standalone column removed, everything is canonicalized to 4 columns let csv = 'name,description,module,path\n'; - // Combine existing and new workflows + // Build workflows map from discovered workflows only + // Old entries are NOT preserved - the manifest reflects what actually exists on disk const allWorkflows = new Map(); - // Add existing entries - for (const [key, value] of existingEntries) { - allWorkflows.set(key, value); - } - - // Add/update new workflows + // Only add workflows that were actually discovered in this scan for (const workflow of this.workflows) { const key = `${workflow.module}:${workflow.name}`; allWorkflows.set(key, { From d37ee7f27ad767f760e5e4ef20f890917cc109d9 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 22:36:11 -0600 Subject: [PATCH 12/38] add installer warning when newer version is available --- tools/cli/bmad-cli.js | 45 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/tools/cli/bmad-cli.js b/tools/cli/bmad-cli.js index ad3aac341..2a5b8d387 100755 --- a/tools/cli/bmad-cli.js +++ b/tools/cli/bmad-cli.js @@ -1,6 +1,48 @@ const { program } = require('commander'); const path = require('node:path'); const fs = require('node:fs'); +const { execSync } = require('node:child_process'); + +// Check for updates - do this asynchronously so it doesn't block startup +const packageJson = require('../../package.json'); +const packageName = 'bmad-method'; +checkForUpdate().catch(() => { + // Silently ignore errors - version check is best-effort +}); + +async function checkForUpdate() { + try { + // For beta versions, check the beta tag; otherwise check latest + const isBeta = + packageJson.version.includes('Beta') || + packageJson.version.includes('beta') || + packageJson.version.includes('alpha') || + packageJson.version.includes('rc'); + const tag = isBeta ? 'beta' : 'latest'; + + const result = execSync(`npm view ${packageName}@${tag} version`, { + encoding: 'utf8', + stdio: 'pipe', + timeout: 5000, + }).trim(); + + if (result && result !== packageJson.version) { + console.warn(''); + console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗'); + console.warn(' ║ UPDATE AVAILABLE ║'); + console.warn(' ║ ║'); + console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`); + console.warn(' ║ ║'); + console.warn(' ║ To update,exir and first run: ║'); + console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`); + console.warn(' ║ ║'); + console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝'); + console.warn(''); + } + } catch { + // Silently fail - network issues or npm not available + } +} // Fix for stdin issues when running through npm on Windows // Ensures keyboard interaction works properly with CLI prompts @@ -20,9 +62,6 @@ if (process.stdin.isTTY) { } } -// Load package.json from root for version info -const packageJson = require('../../package.json'); - // Load all command modules const commandsPath = path.join(__dirname, 'commands'); const commandFiles = fs.readdirSync(commandsPath).filter((file) => file.endsWith('.js')); From dcc55dd0d56bc51ad7ee161ceaaa3d292ef0538c Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Wed, 4 Feb 2026 22:43:22 -0600 Subject: [PATCH 13/38] Update CHANGELOG for v6.0.0-Beta.7 - Direct workflow invocation via slash commands - Installer multi-workflow file support - Version checking in CLI --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8508f9a6d..7911a7d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [6.0.0-Beta.7] + +**Release: February 4, 2026** + +### 🌟 Key Highlights + +1. **Direct Workflow Invocation** — Agent workflows can now be run directly via slash commands instead of only through agent orchestration +2. **Installer Workflow Support** — Installer now picks up `workflow-*.md` files, enabling multiple workflow files per directory + +### 🎁 Features + +* **Slash Command Workflow Access** — Research and PRD workflows now accessible via direct slash commands: `/domain-research`, `/market-research`, `/technical-research`, `/create-prd`, `/edit-prd`, `/validate-prd` (bd620e38, 731bee26) +* **Version Checking** — CLI now checks npm for newer versions and displays a warning banner when updates are available (d37ee7f2) + +### ♻️ Refactoring + +* **Workflow File Splitting** — Split monolithic `workflow.md` files into specific `workflow-*.md` files for individual workflow invocation (bd620e38) +* **Installer Multi-Workflow Support** — Installer manifest generator now supports `workflow-*.md` pattern, allowing multiple workflow files per directory (731bee26) +* **Internal Skill Renaming** — Renamed internal project skills to use `bmad-os-` prefix for consistent naming (5276d58b) + +--- + ## [6.0.0-Beta.6] **Release: February 4, 2026** From 47a70cc92d2ddc6da5f53814e407997961f1d6ca Mon Sep 17 00:00:00 2001 From: Adam Biggs Date: Thu, 5 Feb 2026 16:55:41 -0800 Subject: [PATCH 14/38] fix: route OpenCode agents to correct .opencode/agent/ directory (#1549) --- tools/cli/installers/lib/ide/platform-codes.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 2ca32aed5..b329d283c 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -124,8 +124,13 @@ platforms: category: ide description: "OpenCode terminal coding assistant" installer: - target_dir: .opencode/command - template_type: opencode + targets: + - target_dir: .opencode/agent + template_type: opencode + artifact_types: [agents] + - target_dir: .opencode/command + template_type: opencode + artifact_types: [workflows, tasks, tools] qwen: name: "QwenCoder" From 2aab028f96b5d1cc800640793ec23ad4f7570f7f Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Thu, 5 Feb 2026 17:57:40 -0700 Subject: [PATCH 15/38] docs: rename brownfield to established projects (#1539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: rename brownfield to established projects Flatten how-to/brownfield/ subdirectory and replace jargon term "brownfield" with more accessible "established projects" throughout. - brownfield/index.md → established-projects.md - brownfield/quick-fix-in-brownfield.md → quick-fixes.md - brownfield-faq.md → established-projects-faq.md - Update all internal links and references * docs: remove redundant phrase from quick-fixes description * docs: restore natural language in established-projects body --- docs/_STYLE_GUIDE.md | 4 +- docs/explanation/brownfield-faq.md | 55 ------------------- docs/explanation/established-projects-faq.md | 48 ++++++++++++++++ .../index.md => established-projects.md} | 12 ++-- ...ck-fix-in-brownfield.md => quick-fixes.md} | 4 +- docs/reference/workflow-map.md | 2 +- 6 files changed, 57 insertions(+), 68 deletions(-) delete mode 100644 docs/explanation/brownfield-faq.md create mode 100644 docs/explanation/established-projects-faq.md rename docs/how-to/{brownfield/index.md => established-projects.md} (84%) rename docs/how-to/{brownfield/quick-fix-in-brownfield.md => quick-fixes.md} (93%) diff --git a/docs/_STYLE_GUIDE.md b/docs/_STYLE_GUIDE.md index e5fb51ff7..3e78387af 100644 --- a/docs/_STYLE_GUIDE.md +++ b/docs/_STYLE_GUIDE.md @@ -147,7 +147,7 @@ your-project/ | **Concept** | `what-are-agents.md` | | **Feature** | `quick-flow.md` | | **Philosophy** | `why-solutioning-matters.md` | -| **FAQ** | `brownfield-faq.md` | +| **FAQ** | `established-projects-faq.md` | ### General Template @@ -325,7 +325,7 @@ Add italic context at definition start for limited-scope terms: - `*BMad Method/Enterprise.*` - `*Phase N.*` - `*BMGD.*` -- `*Brownfield.*` +- `*Established projects.*` ### Glossary Checklist diff --git a/docs/explanation/brownfield-faq.md b/docs/explanation/brownfield-faq.md deleted file mode 100644 index 1c9b3b822..000000000 --- a/docs/explanation/brownfield-faq.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "Brownfield Development FAQ" -description: Common questions about brownfield development in the BMad Method ---- -Quick answers to common questions about brownfield (existing codebase) development in the BMad Method (BMM). - -## Questions - -- [Questions](#questions) - - [What is brownfield vs greenfield?](#what-is-brownfield-vs-greenfield) - - [Do I have to run document-project for brownfield?](#do-i-have-to-run-document-project-for-brownfield) - - [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project) - - [Can I use Quick Spec Flow for brownfield projects?](#can-i-use-quick-spec-flow-for-brownfield-projects) - - [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices) - -### What is brownfield vs greenfield? - -- **Greenfield** — New project, starting from scratch, clean slate -- **Brownfield** — Existing project, working with established codebase and patterns - -### Do I have to run document-project for brownfield? - -Highly recommended, especially if: - -- No existing documentation -- Documentation is outdated -- AI agents need context about existing code - -You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system. - -### What if I forget to run document-project? - -Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date. - -### Can I use Quick Spec Flow for brownfield projects? - -Yes! Quick Spec Flow works great for brownfield. It will: - -- Auto-detect your existing stack -- Analyze brownfield code patterns -- Detect conventions and ask for confirmation -- Generate context-rich tech-spec that respects existing code - -Perfect for bug fixes and small features in existing codebases. - -### What if my existing code doesn't follow best practices? - -Quick Spec Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide: - -- **Yes** → Maintain consistency with current codebase -- **No** → Establish new standards (document why in tech-spec) - -BMM respects your choice — it won't force modernization, but it will offer it. - -**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it! diff --git a/docs/explanation/established-projects-faq.md b/docs/explanation/established-projects-faq.md new file mode 100644 index 000000000..e940b4dbb --- /dev/null +++ b/docs/explanation/established-projects-faq.md @@ -0,0 +1,48 @@ +--- +title: "Established Projects FAQ" +description: Common questions about using BMad Method on established projects +--- +Quick answers to common questions about working on established projects with the BMad Method (BMM). + +## Questions + +- [Do I have to run document-project first?](#do-i-have-to-run-document-project-first) +- [What if I forget to run document-project?](#what-if-i-forget-to-run-document-project) +- [Can I use Quick Flow for established projects?](#can-i-use-quick-flow-for-established-projects) +- [What if my existing code doesn't follow best practices?](#what-if-my-existing-code-doesnt-follow-best-practices) + +### Do I have to run document-project first? + +Highly recommended, especially if: + +- No existing documentation +- Documentation is outdated +- AI agents need context about existing code + +You can skip it if you have comprehensive, up-to-date documentation including `docs/index.md` or will use other tools or techniques to aid in discovery for the agent to build on an existing system. + +### What if I forget to run document-project? + +Don't worry about it - you can do it at any time. You can even do it during or after a project to help keep docs up to date. + +### Can I use Quick Flow for established projects? + +Yes! Quick Flow works great for established projects. It will: + +- Auto-detect your existing stack +- Analyze existing code patterns +- Detect conventions and ask for confirmation +- Generate context-rich tech-spec that respects existing code + +Perfect for bug fixes and small features in existing codebases. + +### What if my existing code doesn't follow best practices? + +Quick Flow detects your conventions and asks: "Should I follow these existing conventions?" You decide: + +- **Yes** → Maintain consistency with current codebase +- **No** → Establish new standards (document why in tech-spec) + +BMM respects your choice — it won't force modernization, but it will offer it. + +**Have a question not answered here?** Please [open an issue](https://github.com/bmad-code-org/BMAD-METHOD/issues) or ask in [Discord](https://discord.gg/gk8jAdXWmj) so we can add it! diff --git a/docs/how-to/brownfield/index.md b/docs/how-to/established-projects.md similarity index 84% rename from docs/how-to/brownfield/index.md rename to docs/how-to/established-projects.md index 75bab690b..b756691cc 100644 --- a/docs/how-to/brownfield/index.md +++ b/docs/how-to/established-projects.md @@ -1,15 +1,11 @@ --- -title: "Brownfield Development" +title: "Established Projects" description: How to use BMad Method on existing codebases --- Use BMad Method effectively when working on existing projects and legacy codebases. -## What is Brownfield Development? - -**Brownfield** refers to working on existing projects with established codebases and patterns, as opposed to **greenfield** which means starting from scratch with a clean slate. - -This guide covers the essential workflow for onboarding to brownfield projects with BMad Method. +This guide covers the essential workflow for onboarding to existing projects with BMad Method. :::note[Prerequisites] - BMad Method installed (`npx bmad-method install`) @@ -80,5 +76,5 @@ Pay close attention here to prevent reinventing the wheel or making decisions th ## More Information -- **[Quick Fix in Brownfield](/docs/how-to/brownfield/quick-fix-in-brownfield.md)** - Bug fixes and ad-hoc changes -- **[Brownfield FAQ](/docs/explanation/brownfield-faq.md)** - Common questions about brownfield development +- **[Quick Fixes](/docs/how-to/quick-fixes.md)** - Bug fixes and ad-hoc changes +- **[Established Projects FAQ](/docs/explanation/established-projects-faq.md)** - Common questions about working on established projects diff --git a/docs/how-to/brownfield/quick-fix-in-brownfield.md b/docs/how-to/quick-fixes.md similarity index 93% rename from docs/how-to/brownfield/quick-fix-in-brownfield.md rename to docs/how-to/quick-fixes.md index 9dc430f11..5b6cfe35c 100644 --- a/docs/how-to/brownfield/quick-fix-in-brownfield.md +++ b/docs/how-to/quick-fixes.md @@ -1,6 +1,6 @@ --- -title: "How to Make Quick Fixes in Brownfield Projects" -description: How to make quick fixes and ad-hoc changes in brownfield projects +title: "Quick Fixes" +description: How to make quick fixes and ad-hoc changes --- Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow. diff --git a/docs/reference/workflow-map.md b/docs/reference/workflow-map.md index 1425c4698..0df3d3ec8 100644 --- a/docs/reference/workflow-map.md +++ b/docs/reference/workflow-map.md @@ -73,7 +73,7 @@ Skip phases 1-3 for small, well-understood work. Each document becomes context for the next phase. The PRD tells the architect what constraints matter. The architecture tells the dev agent which patterns to follow. Story files give focused, complete context for implementation. Without this structure, agents make inconsistent decisions. -For brownfield projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand. +For established projects, `document-project` creates or updates `project-context.md` - what exists in the codebase and the rules all implementation workflows must observe. Run it just before Phase 4, and again when something significant changes - structure, architecture, or those rules. You can also edit `project-context.md` by hand. All implementation workflows load `project-context.md` if it exists. Additional context per workflow: From 311b237d854792b2e3619f8d77fc246ca76c6649 Mon Sep 17 00:00:00 2001 From: Davor Racic Date: Fri, 6 Feb 2026 02:00:52 +0100 Subject: [PATCH 16/38] fix: trim activation header to avoid YAML formatting issues in kilo installer (#1537) * fix: trim activation header to avoid YAML formatting issues in kilo installer * refactor: convert kilo installer to use YAML object serialization and add workflow support - Replace string concatenation with yaml.parse/stringify for proper YAML handling - Add workflow command generation and export to .kilocode/workflows/ - Implement clearBmadWorkflows to remove old BMAD workflow files - Convert createModeEntry to createModeObject returning structured objects - Update cleanup to use YAML parsing for proper mode filtering - Update installCustomAgentLauncher to use object-based config * fix: add task and tool command generation to kilo installer --------- Co-authored-by: Brian --- tools/cli/installers/lib/ide/kilo.js | 238 +++++++++++++++------------ 1 file changed, 130 insertions(+), 108 deletions(-) diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js index 45e380218..52fd17c90 100644 --- a/tools/cli/installers/lib/ide/kilo.js +++ b/tools/cli/installers/lib/ide/kilo.js @@ -1,7 +1,10 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); const chalk = require('chalk'); +const yaml = require('yaml'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); /** * KiloCode IDE setup handler @@ -22,76 +25,94 @@ class KiloSetup extends BaseIdeSetup { async setup(projectDir, bmadDir, options = {}) { console.log(chalk.cyan(`Setting up ${this.name}...`)); - // Check for existing .kilocodemodes file + // Clean up any old BMAD installation first + await this.cleanup(projectDir); + + // Load existing config (may contain non-BMAD modes and other settings) const kiloModesPath = path.join(projectDir, this.configFile); - let existingModes = []; - let existingContent = ''; + let config = {}; if (await this.pathExists(kiloModesPath)) { - existingContent = await this.readFile(kiloModesPath); - // Parse existing modes - const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); - for (const match of modeMatches) { - existingModes.push(match[1]); + const existingContent = await this.readFile(kiloModesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + // If parsing fails, start fresh but warn user + console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh')); + config = {}; } - console.log(chalk.yellow(`Found existing .kilocodemodes file with ${existingModes.length} modes`)); + } + + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; } // Generate agent launchers const agentGen = new AgentCommandGenerator(this.bmadFolderName); const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - // Create modes content - let newModesContent = ''; + // Create mode objects and add to config let addedCount = 0; - let skippedCount = 0; for (const artifact of agentArtifacts) { - const slug = `bmad-${artifact.module}-${artifact.name}`; - - // Skip if already exists - if (existingModes.includes(slug)) { - console.log(chalk.dim(` Skipping ${slug} - already exists`)); - skippedCount++; - continue; - } - - const modeEntry = await this.createModeEntry(artifact, projectDir); - - newModesContent += modeEntry; + const modeObject = await this.createModeObject(artifact, projectDir); + config.customModes.push(modeObject); addedCount++; } - // Build final content - let finalContent = ''; - if (existingContent) { - finalContent = existingContent.trim() + '\n' + newModesContent; - } else { - finalContent = 'customModes:\n' + newModesContent; - } - - // Write .kilocodemodes file + // Write .kilocodemodes file with proper YAML structure + const finalContent = yaml.stringify(config, { lineWidth: 0 }); await this.writeFile(kiloModesPath, finalContent); + // Generate workflow commands + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + + // Write to .kilocode/workflows/ directory + const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); + await this.ensureDir(workflowsDir); + + // Clear old BMAD workflows before writing new ones + await this.clearBmadWorkflows(workflowsDir); + + // Write workflow files + const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts); + + // Generate task and tool commands + const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); + const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + + // Write task/tool files to workflows directory (same location as workflows) + await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts); + const taskCount = taskToolCounts.tasks || 0; + const toolCount = taskToolCounts.tools || 0; + console.log(chalk.green(`✓ ${this.name} configured:`)); console.log(chalk.dim(` - ${addedCount} modes added`)); - if (skippedCount > 0) { - console.log(chalk.dim(` - ${skippedCount} modes skipped (already exist)`)); - } + console.log(chalk.dim(` - ${workflowCount} workflows exported`)); + console.log(chalk.dim(` - ${taskCount} tasks exported`)); + console.log(chalk.dim(` - ${toolCount} tools exported`)); console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); + console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`)); console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode')); return { success: true, modes: addedCount, - skipped: skippedCount, + workflows: workflowCount, + tasks: taskCount, + tools: toolCount, }; } /** - * Create a mode entry for an agent + * Create a mode object for an agent + * @param {Object} artifact - Agent artifact + * @param {string} projectDir - Project directory + * @returns {Object} Mode object for YAML serialization */ - async createModeEntry(artifact, projectDir) { + async createModeObject(artifact, projectDir) { // Extract metadata from launcher content const titleMatch = artifact.content.match(/title="([^"]+)"/); const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name); @@ -102,8 +123,8 @@ class KiloSetup extends BaseIdeSetup { const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; - // Get the activation header from central template - const activationHeader = await this.getAgentCommandHeader(); + // Get the activation header from central template (trim to avoid YAML formatting issues) + const activationHeader = (await this.getAgentCommandHeader()).trim(); const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/); const roleDefinition = roleDefinitionMatch @@ -113,22 +134,15 @@ class KiloSetup extends BaseIdeSetup { // Get relative path const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); - // Build mode entry (KiloCode uses same schema as Roo) - const slug = `bmad-${artifact.module}-${artifact.name}`; - let modeEntry = ` - slug: ${slug}\n`; - modeEntry += ` name: '${icon} ${title}'\n`; - modeEntry += ` roleDefinition: ${roleDefinition}\n`; - modeEntry += ` whenToUse: ${whenToUse}\n`; - modeEntry += ` customInstructions: |\n`; - modeEntry += ` ${activationHeader} 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`; - modeEntry += ` groups:\n`; - modeEntry += ` - read\n`; - modeEntry += ` - edit\n`; - modeEntry += ` - browser\n`; - modeEntry += ` - command\n`; - modeEntry += ` - mcp\n`; - - return modeEntry; + // Build mode object (KiloCode uses same schema as Roo) + return { + slug: `bmad-${artifact.module}-${artifact.name}`, + name: `${icon} ${title}`, + roleDefinition: roleDefinition, + whenToUse: whenToUse, + customInstructions: `${activationHeader} 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`, + groups: ['read', 'edit', 'browser', 'command', 'mcp'], + }; } /** @@ -141,6 +155,22 @@ class KiloSetup extends BaseIdeSetup { .join(' '); } + /** + * Clear old BMAD workflow files from workflows directory + * @param {string} workflowsDir - Workflows directory path + */ + async clearBmadWorkflows(workflowsDir) { + const fs = require('fs-extra'); + if (!(await fs.pathExists(workflowsDir))) return; + + const entries = await fs.readdir(workflowsDir); + for (const entry of entries) { + if (entry.startsWith('bmad-') && entry.endsWith('.md')) { + await fs.remove(path.join(workflowsDir, entry)); + } + } + } + /** * Cleanup KiloCode configuration */ @@ -151,28 +181,29 @@ class KiloSetup extends BaseIdeSetup { if (await fs.pathExists(kiloModesPath)) { const content = await fs.readFile(kiloModesPath, 'utf8'); - // Remove BMAD modes only - const lines = content.split('\n'); - const filteredLines = []; - let skipMode = false; - let removedCount = 0; + try { + const config = yaml.parse(content) || {}; - for (const line of lines) { - if (/^\s*- slug: bmad-/.test(line)) { - skipMode = true; - removedCount++; - } else if (skipMode && /^\s*- slug: /.test(line)) { - skipMode = false; - } + if (Array.isArray(config.customModes)) { + const originalCount = config.customModes.length; + // Remove BMAD modes only (keep non-BMAD modes) + config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-')); + const removedCount = originalCount - config.customModes.length; - if (!skipMode) { - filteredLines.push(line); + if (removedCount > 0) { + await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); + console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`)); + } } + } catch { + // If parsing fails, leave file as-is + console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup')); } - - await fs.writeFile(kiloModesPath, filteredLines.join('\n')); - console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`)); } + + // Clean up workflow files + const workflowsDir = path.join(projectDir, '.kilocode', 'workflows'); + await this.clearBmadWorkflows(workflowsDir); } /** @@ -185,31 +216,28 @@ class KiloSetup extends BaseIdeSetup { */ async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { const kilocodemodesPath = path.join(projectDir, this.configFile); - let existingContent = ''; + let config = {}; // Read existing .kilocodemodes file if (await this.pathExists(kilocodemodesPath)) { - existingContent = await this.readFile(kilocodemodesPath); + const existingContent = await this.readFile(kilocodemodesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + config = {}; + } } - // Create custom agent mode entry + // Ensure customModes array exists + if (!Array.isArray(config.customModes)) { + config.customModes = []; + } + + // Create custom agent mode object const slug = `bmad-custom-${agentName.toLowerCase()}`; - const modeEntry = ` - slug: ${slug} - name: 'BMAD Custom: ${agentName}' - description: | - Custom BMAD agent: ${agentName} - - **⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent! - - This is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file. - prompt: | - @${agentPath} - always: false - permissions: all -`; // Check if mode already exists - if (existingContent.includes(slug)) { + if (config.customModes.some((mode) => mode.slug === slug)) { return { ide: 'kilo', path: this.configFile, @@ -219,24 +247,18 @@ class KiloSetup extends BaseIdeSetup { }; } - // Build final content - let finalContent = ''; - if (existingContent) { - // Find customModes section or add it - if (existingContent.includes('customModes:')) { - // Append to existing customModes - finalContent = existingContent + modeEntry; - } else { - // Add customModes section - finalContent = existingContent.trim() + '\n\ncustomModes:\n' + modeEntry; - } - } else { - // Create new .kilocodemodes file with customModes - finalContent = 'customModes:\n' + modeEntry; - } + // Add custom mode object + config.customModes.push({ + slug: slug, + name: `BMAD Custom: ${agentName}`, + description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`, + prompt: `@${agentPath}\n`, + always: false, + permissions: 'all', + }); - // Write .kilocodemodes file - await this.writeFile(kilocodemodesPath, finalContent); + // Write .kilocodemodes file with proper YAML structure + await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 })); return { ide: 'kilo', From 420e720242ab601e6b3ff37c06b7fa8a2df068cf Mon Sep 17 00:00:00 2001 From: Michael Pursifull Date: Thu, 5 Feb 2026 19:04:31 -0600 Subject: [PATCH 17/38] fix: bmad-help reads project docs and respects communication_language (#1535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: bmad-help agent reads project docs and respects communication_language The help task fabricated tech stack information instead of reading actual project documentation (#1460) and ignored communication_language config (#1457). Three changes: 1. Step 2 now also extracts communication_language and project_knowledge from the active module's config.yaml 2. New step 3 scans project_knowledge path for documentation and uses discovered facts as grounding context, with anti-hallucination guard 3. Step 7 enforces {communication_language} in all output Chose inline config resolution over workflow.yaml conversion to match existing core task patterns (index-docs, shard-doc). Fixes #1460 Relates to #1457 * fix: clarify config extraction wording in help task Remove "active module" reference from step 2 — config extraction happens during the all-modules scan, before module detection in step 4. Changed to "each scanned module's config" to match the existing iteration pattern. Addresses CodeRabbit review feedback on PR #1535. --------- Co-authored-by: Brian --- src/core/tasks/help.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/tasks/help.md b/src/core/tasks/help.md index 4e060ea1f..c3c3fab11 100644 --- a/src/core/tasks/help.md +++ b/src/core/tasks/help.md @@ -54,13 +54,15 @@ Determine what was just completed: 1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv` -2. **Resolve output locations** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. +2. **Resolve output locations and config** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. Also extract `communication_language` and `project_knowledge` from each scanned module's config. -3. **Detect active module** — Use MODULE DETECTION above +3. **Ground in project knowledge** — If `project_knowledge` resolves to an existing path, read available documentation files (architecture docs, project overview, tech stack references) for grounding context. Use discovered project facts when composing any project-specific output. Never fabricate project-specific details — if documentation is unavailable, state so. -4. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. +4. **Detect active module** — Use MODULE DETECTION above -5. **Present recommendations** — Show next steps based on: +5. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. + +6. **Present recommendations** — Show next steps based on: - Completed workflows detected - Phase/sequence ordering (ROUTING RULES) - Artifact presence @@ -74,9 +76,10 @@ Determine what was just completed: - **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)") - Brief **description** -6. **Additional guidance to convey**: +7. **Additional guidance to convey**: + - Present all output in `{communication_language}` - Run each workflow in a **fresh context window** - For **validation workflows**: recommend using a different high-quality LLM if available - For conversational requests: match the user's tone while presenting clearly -7. Return to the calling process after presenting recommendations. +8. Return to the calling process after presenting recommendations. From 068a9dc45aa23e697264e912cf30ae170e586158 Mon Sep 17 00:00:00 2001 From: Drickon <41375613+Drickon@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:06:26 -0600 Subject: [PATCH 18/38] fix: remove --prefer-offline flag to prevent stale cache errors (#1531) The --prefer-offline flag causes npm to use cached package metadata, which can be stale and fail to resolve recently published versions. Also updates deprecated --production flag to --omit=dev. Fixes #1438 --- tools/cli/installers/lib/modules/manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index f06f5483c..c55dae838 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -417,7 +417,7 @@ class ModuleManager { if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); try { - execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { + execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, stdio: 'pipe', timeout: 120_000, // 2 minute timeout @@ -442,7 +442,7 @@ class ModuleManager { if (packageJsonNewer) { const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); try { - execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress --legacy-peer-deps', { + execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, stdio: 'pipe', timeout: 120_000, // 2 minute timeout From 22601f81984dbc4701a569e263c32debb0713e58 Mon Sep 17 00:00:00 2001 From: Vladimir Hrusovsky Date: Fri, 6 Feb 2026 02:13:11 +0100 Subject: [PATCH 19/38] feat: add non-interactive installation support (#1520) * feat: add non-interactive installation support Add command-line flags to support non-interactive installation for CI/CD pipelines and automated deployments: - --directory: Installation directory - --modules: Comma-separated module IDs - --tools: Tool/IDE IDs (use "none" to skip) - --custom-content: Custom module paths - --action: Action type for existing installations - --user-name, --communication-language, --document-output-language, --output-folder: Core config - -y, --yes: Accept all defaults When flags are provided, prompts are skipped. Missing values gracefully fall back to interactive prompts. * fix: complete non-interactive installation support - Fix validation checks using truthy instead of !== true - Add skipPrompts flag to skip module config prompts with --yes - Add getDefaultModules() for automatic module selection with --yes - Fix IDE selection to use array check instead of length check Co-Authored-By: AiderDesk --------- Co-authored-by: Brian --- README.md | 8 + docs/non-interactive-installation.md | 314 +++++++++++++++ tools/cli/commands/install.js | 19 +- .../installers/lib/core/config-collector.js | 87 +++-- tools/cli/installers/lib/core/installer.js | 5 +- tools/cli/lib/ui.js | 369 +++++++++++++++--- 6 files changed, 714 insertions(+), 88 deletions(-) create mode 100644 docs/non-interactive-installation.md diff --git a/README.md b/README.md index 25828f395..6e1f3a9b0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,14 @@ npx bmad-method install Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsurf, etc.) in the project folder. +**Non-Interactive Installation**: For CI/CD pipelines or automated deployments, use command-line flags: + +```bash +npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes +``` + +See [Non-Interactive Installation Guide](docs/non-interactive-installation.md) for all available options. + > **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like: - `/bmad-help How should I build a web app for my TShirt Business that can scale to millions?` diff --git a/docs/non-interactive-installation.md b/docs/non-interactive-installation.md new file mode 100644 index 000000000..7541ecc20 --- /dev/null +++ b/docs/non-interactive-installation.md @@ -0,0 +1,314 @@ +--- +title: Non-Interactive Installation +description: Install BMAD using command-line flags for CI/CD pipelines and automated deployments +--- + +# Non-Interactive Installation + +BMAD now supports non-interactive installation through command-line flags. This is particularly useful for: + +- Automated deployments and CI/CD pipelines +- Scripted installations +- Batch installations across multiple projects +- Quick installations with known configurations + +## Installation Modes + +### 1. Fully Interactive (Default) + +Run without any flags to use the traditional interactive prompts: + +```bash +npx bmad-method install +``` + +### 2. Fully Non-Interactive + +Provide all required flags to skip all prompts: + +```bash +npx bmad-method install \ + --directory /path/to/project \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "John Doe" \ + --communication-language English \ + --document-output-language English \ + --output-folder _bmad-output +``` + +### 3. Semi-Interactive (Graceful Fallback) + +Provide some flags and let BMAD prompt for the rest: + +```bash +npx bmad-method install \ + --directory /path/to/project \ + --modules bmm +``` + +In this case, BMAD will: +- Use the provided directory and modules +- Prompt for tool selection +- Prompt for core configuration + +### 4. Quick Install with Defaults + +Use the `-y` or `--yes` flag to accept all defaults: + +```bash +npx bmad-method install --yes +``` + +This will: +- Install to the current directory +- Skip custom content prompts +- Use default values for all configuration +- Use previously configured tools (or skip tool configuration if none exist) + +### 5. Install Without Tools + +To skip tool/IDE configuration entirely: + +**Option 1: Use --tools none** +```bash +npx bmad-method install --directory ~/myapp --modules bmm --tools none +``` + +**Option 2: Use --yes flag (if no tools were previously configured)** +```bash +npx bmad-method install --yes +``` + +**Option 3: Omit --tools and select "None" in the interactive prompt** +```bash +npx bmad-method install --directory ~/myapp --modules bmm +# Then select "⚠ None - I am not installing any tools" when prompted +``` + +## Available Flags + +### Installation Options + +| Flag | Description | Example | +|------|-------------|---------| +| `--directory ` | Installation directory | `--directory ~/projects/myapp` | +| `--modules ` | Comma-separated module IDs | `--modules bmm,bmb` | +| `--tools ` | Comma-separated tool/IDE IDs (use "none" to skip) | `--tools claude-code,cursor` or `--tools none` | +| `--custom-content ` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` | +| `--action ` | Action for existing installations | `--action quick-update` | + +### Core Configuration + +| Flag | Description | Default | +|------|-------------|---------| +| `--user-name ` | Name for agents to use | System username | +| `--communication-language ` | Agent communication language | English | +| `--document-output-language ` | Document output language | English | +| `--output-folder ` | Output folder path | _bmad-output | + +### Other Options + +| Flag | Description | +|------|-------------| +| `-y, --yes` | Accept all defaults and skip prompts | +| `-d, --debug` | Enable debug output for manifest generation | + +## Action Types + +When working with existing installations, use the `--action` flag: + +- `install` - Fresh installation (default for new directories) +- `update` - Modify existing installation (change modules/config) +- `quick-update` - Refresh installation without changing configuration +- `compile-agents` - Recompile agents with customizations only + +Example: + +```bash +npx bmad-method install --action quick-update +``` + +## Module IDs + +Available module IDs for the `--modules` flag: + +### Core Modules +- `bmm` - BMad Method Master +- `bmb` - BMad Builder + +### External Modules +Check the [BMad registry](https://github.com/bmad-code-org) for available external modules. + +## Tool/IDE IDs + +Available tool IDs for the `--tools` flag: + +- `claude-code` - Claude Code CLI +- `cursor` - Cursor IDE +- `windsurf` - Windsurf IDE +- `vscode` - Visual Studio Code +- `jetbrains` - JetBrains IDEs +- And more... + +Run the interactive installer once to see all available tools. + +## Examples + +### Basic Installation + +Install BMM module with Claude Code: + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --tools claude-code \ + --user-name "Development Team" +``` + +### Installation Without Tools + +Install without configuring any tools/IDEs: + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --tools none \ + --user-name "Development Team" +``` + +### Full Installation with Multiple Modules + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "John Doe" \ + --communication-language English \ + --document-output-language English \ + --output-folder _output +``` + +### Update Existing Installation + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --action update \ + --modules bmm,bmb,custom-module +``` + +### Quick Update (Preserve Settings) + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --action quick-update +``` + +### Installation with Custom Content + +```bash +npx bmad-method install \ + --directory ~/projects/myapp \ + --modules bmm \ + --custom-content ~/my-custom-module,~/another-module \ + --tools claude-code +``` + +### CI/CD Pipeline Installation + +```bash +#!/bin/bash +# install-bmad.sh + +npx bmad-method install \ + --directory "${GITHUB_WORKSPACE}" \ + --modules bmm \ + --tools claude-code \ + --user-name "CI Bot" \ + --communication-language English \ + --document-output-language English \ + --output-folder _bmad-output \ + --yes +``` + +## Environment-Specific Installations + +### Development Environment + +```bash +npx bmad-method install \ + --directory . \ + --modules bmm,bmb \ + --tools claude-code,cursor \ + --user-name "${USER}" +``` + +### Production Environment + +```bash +npx bmad-method install \ + --directory /opt/app \ + --modules bmm \ + --tools claude-code \ + --user-name "Production Team" \ + --output-folder /var/bmad-output +``` + +## Validation and Error Handling + +BMAD validates all provided flags: + +- **Directory**: Must be a valid path with write permissions +- **Modules**: Will warn about invalid module IDs (but won't fail) +- **Tools**: Will warn about invalid tool IDs (but won't fail) +- **Custom Content**: Each path must contain a valid `module.yaml` file +- **Action**: Must be one of: install, update, quick-update, compile-agents + +Invalid values will either: +1. Show an error and exit (for critical options like directory) +2. Show a warning and skip (for optional items like custom content) +3. Fall back to interactive prompts (for missing required values) + +## Tips and Best Practices + +1. **Use absolute paths** for `--directory` to avoid ambiguity +2. **Test flags locally** before using in CI/CD pipelines +3. **Combine with `-y`** for truly unattended installations +4. **Check module availability** by running the interactive installer once +5. **Use `--debug`** flag if you encounter issues during installation +6. **Skip tool configuration** with `--tools none` for server/CI environments where IDEs aren't needed +7. **Partial flags are OK** - Omit flags and let BMAD prompt for missing values interactively + +## Troubleshooting + +### Installation fails with "Invalid directory" + +Check that: +- The directory path exists or its parent exists +- You have write permissions +- The path is absolute or correctly relative to current directory + +### Module not found + +- Verify the module ID is correct (check available modules in interactive mode) +- External modules may need to be available in the registry + +### Custom content path invalid + +Ensure each custom content path: +- Points to a directory +- Contains a `module.yaml` file in the root +- Has a `code` field in the `module.yaml` + +## Feedback and Issues + +If you encounter any issues with non-interactive installation: + +1. Run with `--debug` flag for detailed output +2. Try the interactive mode to verify the issue +3. Report issues on GitHub: diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index ede133a82..6a6622d1d 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -9,7 +9,22 @@ const ui = new UI(); module.exports = { command: 'install', description: 'Install BMAD Core agents and tools', - options: [['-d, --debug', 'Enable debug output for manifest generation']], + options: [ + ['-d, --debug', 'Enable debug output for manifest generation'], + ['--directory ', 'Installation directory (default: current directory)'], + ['--modules ', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'], + [ + '--tools ', + 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.', + ], + ['--custom-content ', 'Comma-separated list of paths to custom modules/agents/workflows'], + ['--action ', 'Action type for existing installations: install, update, quick-update, or compile-agents'], + ['--user-name ', 'Name for agents to use (default: system username)'], + ['--communication-language ', 'Language for agent communication (default: English)'], + ['--document-output-language ', 'Language for document output (default: English)'], + ['--output-folder ', 'Output folder path relative to project root (default: _bmad-output)'], + ['-y, --yes', 'Accept all defaults and skip prompts where possible'], + ], action: async (options) => { try { // Set debug flag as environment variable for all components @@ -18,7 +33,7 @@ module.exports = { console.log(chalk.cyan('Debug mode enabled\n')); } - const config = await ui.promptInstall(); + const config = await ui.promptInstall(options); // Handle cancel if (config.actionType === 'cancel') { diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index f4eaf5e3d..b49075ae0 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -136,10 +136,12 @@ class ConfigCollector { * @param {string} projectDir - Target project directory * @param {Object} options - Additional options * @param {Map} options.customModulePaths - Map of module ID to source path for custom modules + * @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag) */ async collectAllConfigurations(modules, projectDir, options = {}) { // Store custom module paths for use in collectModuleConfig this.customModulePaths = options.customModulePaths || new Map(); + this.skipPrompts = options.skipPrompts || false; await this.loadExistingConfig(projectDir); // Check if core was already collected (e.g., in early collection phase) @@ -583,47 +585,60 @@ class ConfigCollector { // If there are questions to ask, prompt for accepting defaults vs customizing if (questions.length > 0) { const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; - console.log(); - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - let customize = true; - if (moduleName === 'core') { - // Core module: no confirm prompt, so add spacing manually to match visual style - console.log(chalk.gray('│')); + + // Skip prompts mode: use all defaults without asking + if (this.skipPrompts) { + console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName)); + // Use defaults for all questions + for (const question of questions) { + const hasDefault = question.default !== undefined && question.default !== null && question.default !== ''; + if (hasDefault && typeof question.default !== 'function') { + allAnswers[question.name] = question.default; + } + } } else { - // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) - const customizeAnswer = await prompts.prompt([ - { - type: 'confirm', - name: 'customize', - message: 'Accept Defaults (no to customize)?', - default: true, - }, - ]); - customize = customizeAnswer.customize; - } + console.log(); + console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); + let customize = true; + if (moduleName === 'core') { + // Core module: no confirm prompt, so add spacing manually to match visual style + console.log(chalk.gray('│')); + } else { + // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) + const customizeAnswer = await prompts.prompt([ + { + type: 'confirm', + name: 'customize', + message: 'Accept Defaults (no to customize)?', + default: true, + }, + ]); + customize = customizeAnswer.customize; + } - if (customize && moduleName !== 'core') { - // Accept defaults - only ask questions that have NO default value - const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); + if (customize && moduleName !== 'core') { + // Accept defaults - only ask questions that have NO default value + const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); - if (questionsWithoutDefaults.length > 0) { - console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); - const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); + if (questionsWithoutDefaults.length > 0) { + console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); + const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); + Object.assign(allAnswers, promptedAnswers); + } + + // For questions with defaults that weren't asked, we need to process them with their default values + const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== ''); + for (const question of questionsWithDefaults) { + // Skip function defaults - these are dynamic and will be evaluated later + if (typeof question.default === 'function') { + continue; + } + allAnswers[question.name] = question.default; + } + } else { + const promptedAnswers = await prompts.prompt(questions); Object.assign(allAnswers, promptedAnswers); } - - // For questions with defaults that weren't asked, we need to process them with their default values - const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== ''); - for (const question of questionsWithDefaults) { - // Skip function defaults - these are dynamic and will be evaluated later - if (typeof question.default === 'function') { - continue; - } - allAnswers[question.name] = question.default; - } - } else { - const promptedAnswers = await prompts.prompt(questions); - Object.assign(allAnswers, promptedAnswers); } } diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index edb15112c..cfba0ab94 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -353,11 +353,13 @@ class Installer { const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core'); moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), { customModulePaths, + skipPrompts: config.skipPrompts, }); } else { // Core not collected yet, include it moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), { customModulePaths, + skipPrompts: config.skipPrompts, }); } } @@ -680,7 +682,8 @@ class Installer { } else { // Pass pre-selected IDEs from early prompt (if available) // This allows IDE selection to happen before file copying, improving UX - const preSelectedIdes = config.ides && config.ides.length > 0 ? config.ides : null; + // Use config.ides if it's an array (even if empty), null means prompt + const preSelectedIdes = Array.isArray(config.ides) ? config.ides : null; toolSelection = await this.collectToolConfigurations( path.resolve(config.directory), config.modules, diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index cc135fea3..2ed7ab9f1 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -26,9 +26,10 @@ const choiceUtils = { Separator }; class UI { /** * Prompt for installation configuration + * @param {Object} options - Command-line options from install command * @returns {Object} Installation configuration */ - async promptInstall() { + async promptInstall(options = {}) { CLIUtils.displayLogo(); // Display version-specific start message from install-messages.yaml @@ -36,7 +37,20 @@ class UI { const messageLoader = new MessageLoader(); messageLoader.displayStartMessage(); - const confirmedDirectory = await this.getConfirmedDirectory(); + // Get directory from options or prompt + let confirmedDirectory; + if (options.directory) { + // Use provided directory from command-line + const expandedDir = this.expandUserPath(options.directory); + const validation = this.validateDirectorySync(expandedDir); + if (validation) { + throw new Error(`Invalid directory: ${validation}`); + } + confirmedDirectory = expandedDir; + console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory)); + } else { + confirmedDirectory = await this.getConfirmedDirectory(); + } // Preflight: Check for legacy BMAD v4 footprints immediately after getting directory const { Detector } = require('../installers/lib/core/detector'); @@ -218,11 +232,21 @@ class UI { // Common actions choices.push({ name: 'Modify BMAD Installation', value: 'update' }); - actionType = await prompts.select({ - message: 'How would you like to proceed?', - choices: choices, - default: choices[0].value, - }); + // Check if action is provided via command-line + if (options.action) { + const validActions = choices.map((c) => c.value); + if (!validActions.includes(options.action)) { + throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`); + } + actionType = options.action; + console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType)); + } else { + actionType = await prompts.select({ + message: 'How would you like to proceed?', + choices: choices, + default: choices[0].value, + }); + } // Handle quick update separately if (actionType === 'quick-update') { @@ -253,30 +277,94 @@ class UI { console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`)); // Unified module selection - all modules in one grouped multiselect - let selectedModules = await this.selectAllModules(installedModuleIds); + let selectedModules; + if (options.modules) { + // Use modules from command-line + selectedModules = options.modules + .split(',') + .map((m) => m.trim()) + .filter(Boolean); + console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', '))); + } else { + selectedModules = await this.selectAllModules(installedModuleIds); + } // After module selection, ask about custom modules console.log(''); - const changeCustomModules = await prompts.confirm({ - message: 'Modify custom modules, agents, or workflows?', - default: false, - }); - let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; - if (changeCustomModules) { - customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules); - } else { - // Preserve existing custom modules if user doesn't want to modify them - const { Installer } = require('../installers/lib/core/installer'); - const installer = new Installer(); - const { bmadDir } = await installer.findBmadDir(confirmedDirectory); - const cacheDir = path.join(bmadDir, '_config', 'custom'); - if (await fs.pathExists(cacheDir)) { - const entries = await fs.readdir(cacheDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - customModuleResult.selectedCustomModules.push(entry.name); + if (options.customContent) { + // Use custom content from command-line + const paths = options.customContent + .split(',') + .map((p) => p.trim()) + .filter(Boolean); + console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', '))); + + // Build custom content config similar to promptCustomContentSource + const customPaths = []; + const selectedModuleIds = []; + + for (const customPath of paths) { + const expandedPath = this.expandUserPath(customPath); + const validation = this.validateCustomContentPathSync(expandedPath); + if (validation) { + console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`)); + continue; + } + + // Read module metadata + let moduleMeta; + try { + const moduleYamlPath = path.join(expandedPath, 'module.yaml'); + const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); + const yaml = require('yaml'); + moduleMeta = yaml.parse(moduleYaml); + } catch (error) { + console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`)); + continue; + } + + if (!moduleMeta.code) { + console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`)); + continue; + } + + customPaths.push(expandedPath); + selectedModuleIds.push(moduleMeta.code); + } + + if (customPaths.length > 0) { + customModuleResult = { + selectedCustomModules: selectedModuleIds, + customContentConfig: { + hasCustomContent: true, + paths: customPaths, + selectedModuleIds: selectedModuleIds, + }, + }; + } + } else { + const changeCustomModules = await prompts.confirm({ + message: 'Modify custom modules, agents, or workflows?', + default: false, + }); + + if (changeCustomModules) { + customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules); + } else { + // Preserve existing custom modules if user doesn't want to modify them + const { Installer } = require('../installers/lib/core/installer'); + const installer = new Installer(); + const { bmadDir } = await installer.findBmadDir(confirmedDirectory); + + const cacheDir = path.join(bmadDir, '_config', 'custom'); + if (await fs.pathExists(cacheDir)) { + const entries = await fs.readdir(cacheDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + customModuleResult.selectedCustomModules.push(entry.name); + } } } } @@ -288,9 +376,9 @@ class UI { } // Get tool selection - const toolSelection = await this.promptToolSelection(confirmedDirectory); + const toolSelection = await this.promptToolSelection(confirmedDirectory, options); - const coreConfig = await this.collectCoreConfig(confirmedDirectory); + const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); return { actionType: 'update', @@ -309,16 +397,80 @@ class UI { const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); // Unified module selection - all modules in one grouped multiselect - let selectedModules = await this.selectAllModules(installedModuleIds); + let selectedModules; + if (options.modules) { + // Use modules from command-line + selectedModules = options.modules + .split(',') + .map((m) => m.trim()) + .filter(Boolean); + console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', '))); + } else if (options.yes) { + // Use default modules when --yes flag is set + selectedModules = await this.getDefaultModules(installedModuleIds); + console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', '))); + } else { + selectedModules = await this.selectAllModules(installedModuleIds); + } // Ask about custom content (local modules/agents/workflows) - const wantsCustomContent = await prompts.confirm({ - message: 'Add custom modules, agents, or workflows from your computer?', - default: false, - }); + if (options.customContent) { + // Use custom content from command-line + const paths = options.customContent + .split(',') + .map((p) => p.trim()) + .filter(Boolean); + console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', '))); - if (wantsCustomContent) { - customContentConfig = await this.promptCustomContentSource(); + // Build custom content config similar to promptCustomContentSource + const customPaths = []; + const selectedModuleIds = []; + + for (const customPath of paths) { + const expandedPath = this.expandUserPath(customPath); + const validation = this.validateCustomContentPathSync(expandedPath); + if (validation) { + console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`)); + continue; + } + + // Read module metadata + let moduleMeta; + try { + const moduleYamlPath = path.join(expandedPath, 'module.yaml'); + const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8'); + const yaml = require('yaml'); + moduleMeta = yaml.parse(moduleYaml); + } catch (error) { + console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`)); + continue; + } + + if (!moduleMeta.code) { + console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`)); + continue; + } + + customPaths.push(expandedPath); + selectedModuleIds.push(moduleMeta.code); + } + + if (customPaths.length > 0) { + customContentConfig = { + hasCustomContent: true, + paths: customPaths, + selectedModuleIds: selectedModuleIds, + }; + } + } else if (!options.yes) { + const wantsCustomContent = await prompts.confirm({ + message: 'Add custom modules, agents, or workflows from your computer?', + default: false, + }); + + if (wantsCustomContent) { + customContentConfig = await this.promptCustomContentSource(); + } } // Add custom content modules if any were selected @@ -327,8 +479,8 @@ class UI { } selectedModules = selectedModules.filter((m) => m !== 'core'); - let toolSelection = await this.promptToolSelection(confirmedDirectory); - const coreConfig = await this.collectCoreConfig(confirmedDirectory); + let toolSelection = await this.promptToolSelection(confirmedDirectory, options); + const coreConfig = await this.collectCoreConfig(confirmedDirectory, options); return { actionType: 'install', @@ -339,6 +491,7 @@ class UI { skipIde: toolSelection.skipIde, coreConfig: coreConfig, customContent: customContentConfig, + skipPrompts: options.yes || false, }; } @@ -348,9 +501,10 @@ class UI { * 1. Recommended tools - standard multiselect for 3 preferred tools * 2. Additional tools - autocompleteMultiselect with search capability * @param {string} projectDir - Project directory to check for existing IDEs + * @param {Object} options - Command-line options * @returns {Object} Tool configuration */ - async promptToolSelection(projectDir) { + async promptToolSelection(projectDir, options = {}) { // Check for existing configured IDEs - use findBmadDir to detect custom folder names const { Detector } = require('../installers/lib/core/detector'); const { Installer } = require('../installers/lib/core/installer'); @@ -449,7 +603,37 @@ class UI { }; }); - const selectedIdes = await prompts.autocompleteMultiselect({ + let selectedIdes = []; + + // Check if tools are provided via command-line + if (options.tools) { + // Check for explicit "none" value to skip tool installation + if (options.tools.toLowerCase() === 'none') { + console.log(chalk.cyan('Skipping tool configuration (--tools none)')); + return { ides: [], skipIde: true }; + } else { + selectedIdes = options.tools + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', '))); + this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + return { ides: selectedIdes, skipIde: false }; + } + } else if (options.yes) { + // If --yes flag is set, skip tool prompt and use previously configured tools or empty + if (configuredIdes.length > 0) { + console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', '))); + this.displaySelectedTools(configuredIdes, preferredIdes, allTools); + return { ides: configuredIdes, skipIde: false }; + } else { + console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)')); + return { ides: [], skipIde: true }; + } + } + + // Interactive mode + const interactiveSelectedIdes = await prompts.autocompleteMultiselect({ message: 'Select tools:', options: allToolOptions, initialValues: configuredIdes.length > 0 ? configuredIdes : undefined, @@ -457,12 +641,12 @@ class UI { maxItems: 8, }); - const allSelectedIdes = selectedIdes || []; + selectedIdes = interactiveSelectedIdes || []; // ───────────────────────────────────────────────────────────────────────────── // STEP 3: Confirm if no tools selected // ───────────────────────────────────────────────────────────────────────────── - if (allSelectedIdes.length === 0) { + if (selectedIdes.length === 0) { console.log(''); const confirmNoTools = await prompts.confirm({ message: 'No tools selected. Continue without installing any tools?', @@ -481,11 +665,11 @@ class UI { } // Display selected tools - this.displaySelectedTools(allSelectedIdes, preferredIdes, allTools); + this.displaySelectedTools(selectedIdes, preferredIdes, allTools); return { - ides: allSelectedIdes, - skipIde: allSelectedIdes.length === 0, + ides: selectedIdes, + skipIde: selectedIdes.length === 0, }; } @@ -573,15 +757,75 @@ class UI { /** * Collect core configuration * @param {string} directory - Installation directory + * @param {Object} options - Command-line options * @returns {Object} Core configuration */ - async collectCoreConfig(directory) { + async collectCoreConfig(directory, options = {}) { const { ConfigCollector } = require('../installers/lib/core/config-collector'); const configCollector = new ConfigCollector(); - // Load existing configs first if they exist - await configCollector.loadExistingConfig(directory); - // Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) - await configCollector.collectModuleConfig('core', directory, false, true); + + // If options are provided, set them directly + if (options.userName || options.communicationLanguage || options.documentOutputLanguage || options.outputFolder) { + const coreConfig = {}; + if (options.userName) { + coreConfig.user_name = options.userName; + console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName)); + } + if (options.communicationLanguage) { + coreConfig.communication_language = options.communicationLanguage; + console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage)); + } + if (options.documentOutputLanguage) { + coreConfig.document_output_language = options.documentOutputLanguage; + console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage)); + } + if (options.outputFolder) { + coreConfig.output_folder = options.outputFolder; + console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder)); + } + + // Load existing config to merge with provided options + await configCollector.loadExistingConfig(directory); + + // Merge provided options with existing config (or defaults) + const existingConfig = configCollector.collectedConfig.core || {}; + configCollector.collectedConfig.core = { ...existingConfig, ...coreConfig }; + + // If not all options are provided, collect the missing ones interactively (unless --yes flag) + if ( + !options.yes && + (!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder) + ) { + await configCollector.collectModuleConfig('core', directory, false, true); + } + } else if (options.yes) { + // Use all defaults when --yes flag is set + await configCollector.loadExistingConfig(directory); + const existingConfig = configCollector.collectedConfig.core || {}; + + // If no existing config, use defaults + if (Object.keys(existingConfig).length === 0) { + let safeUsername; + try { + safeUsername = os.userInfo().username; + } catch { + safeUsername = process.env.USER || process.env.USERNAME || 'User'; + } + const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); + configCollector.collectedConfig.core = { + user_name: defaultUsername, + communication_language: 'English', + document_output_language: 'English', + output_folder: '_bmad-output', + }; + console.log(chalk.cyan('Using default configuration (--yes flag)')); + } + } else { + // Load existing configs first if they exist + await configCollector.loadExistingConfig(directory); + // Now collect with existing values as defaults (false = don't skip loading, true = skip completion message) + await configCollector.collectModuleConfig('core', directory, false, true); + } const coreConfig = configCollector.collectedConfig.core; // Ensure we always have a core config object, even if empty @@ -885,6 +1129,33 @@ class UI { return selected ? selected.filter((m) => m !== '__NONE__') : []; } + /** + * Get default modules for non-interactive mode + * @param {Set} installedModuleIds - Already installed module IDs + * @returns {Array} Default module codes + */ + async getDefaultModules(installedModuleIds = new Set()) { + const { ModuleManager } = require('../installers/lib/modules/manager'); + const moduleManager = new ModuleManager(); + const { modules: localModules } = await moduleManager.listAvailable(); + + const defaultModules = []; + + // Add default-selected local modules (typically BMM) + for (const mod of localModules) { + if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) { + defaultModules.push(mod.id); + } + } + + // If no defaults found, use 'bmm' as the fallback default + if (defaultModules.length === 0) { + defaultModules.push('bmm'); + } + + return defaultModules; + } + /** * Prompt for directory selection * @returns {Object} Directory answer from prompt From 8be371359572abe979d46f75f3de6929d9209952 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Fri, 6 Feb 2026 05:58:09 -0700 Subject: [PATCH 20/38] docs: add modules reference page and site fixes (#1540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add official external modules reference page * chore: remove obsolete docs and basement files * fix: update robots.txt URLs to docs.bmad-method.org * fix: generate robots.txt dynamically from site base URL Replace static robots.txt with an Astro endpoint that uses the configured site URL, so sitemap references are correct on both fork deployments and production. * fix: unify site URL resolution in build-docs.js build-docs.js had its own hardcoded fallback URL (bmad-code-org.github.io) instead of using the shared getSiteUrl() function, causing URL mismatches between robots.txt, llms.txt, and sitemaps. Now all components resolve the site URL through the same function. Renamed site-url.js to .mjs to avoid Node ESM detection warnings. * fix: correct module names and relocate prompt file - CIS: "Creative Innovation Suite" → "Creative Intelligence Suite" - GDS: "Game Dev Suite" → "Game Dev Studio" - Move _prompt-external-modules-page.md from docs/ to tools/docs/ * refactor: convert build-docs to ESM, eliminate mutable globals - Convert build-docs.js to build-docs.mjs (CJS → ESM) - Import getSiteUrl directly, remove async import workaround - Kill mutable SITE_URL global, call getSiteUrl() where needed - Clean up Banner.astro variable naming - Update package.json and CI workflow for .mjs extension --- .github/workflows/docs.yaml | 2 +- docs/reference/modules.md | 73 +++ package.json | 2 +- tools/{build-docs.js => build-docs.mjs} | 40 +- tools/cli/external-official-modules.yaml | 4 +- tools/docs/_prompt-external-modules-page.md | 59 +++ tools/docs/index.md | 2 - .../_basement/components/WorkflowGuide.astro | 444 ------------------ website/_basement/pages/workflow-guide.astro | 17 - website/astro.config.mjs | 2 +- website/src/components/Banner.astro | 7 +- website/src/lib/{site-url.js => site-url.mjs} | 0 .../robots.txt => src/pages/robots.txt.ts} | 17 +- 13 files changed, 175 insertions(+), 494 deletions(-) create mode 100644 docs/reference/modules.md rename tools/{build-docs.js => build-docs.mjs} (92%) create mode 100644 tools/docs/_prompt-external-modules-page.md delete mode 100644 tools/docs/index.md delete mode 100644 website/_basement/components/WorkflowGuide.astro delete mode 100644 website/_basement/pages/workflow-guide.astro rename website/src/lib/{site-url.js => site-url.mjs} (100%) rename website/{public/robots.txt => src/pages/robots.txt.ts} (61%) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 7e5de881b..e28eac969 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -8,7 +8,7 @@ on: - "docs/**" - "src/modules/*/docs/**" - "website/**" - - "tools/build-docs.js" + - "tools/build-docs.mjs" - ".github/workflows/docs.yaml" workflow_dispatch: diff --git a/docs/reference/modules.md b/docs/reference/modules.md new file mode 100644 index 000000000..c1b9c497e --- /dev/null +++ b/docs/reference/modules.md @@ -0,0 +1,73 @@ +--- +title: Official Modules +--- + +BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite). + +:::tip[Installing Modules] +Run `npx bmad-method install` and select the modules you want. The installer handles downloading, configuration, and IDE integration automatically. +::: + +## BMad Builder + +Create custom agents, workflows, and domain-specific modules with guided assistance. BMad Builder is the meta-module for extending the framework itself. + +- **Code:** `bmb` +- **npm:** [`bmad-builder`](https://www.npmjs.com/package/bmad-builder) +- **GitHub:** [bmad-code-org/bmad-builder](https://github.com/bmad-code-org/bmad-builder) + +**Provides:** + +- Agent Builder -- create specialized AI agents with custom expertise and tool access +- Workflow Builder -- design structured processes with steps and decision points +- Module Builder -- package agents and workflows into shareable, publishable modules +- Interactive setup with YAML configuration and npm publishing support + +## Creative Intelligence Suite + +AI-powered tools for structured creativity, ideation, and innovation during early-stage development. The suite provides multiple agents that facilitate brainstorming, design thinking, and problem-solving using proven frameworks. + +- **Code:** `cis` +- **npm:** [`bmad-creative-intelligence-suite`](https://www.npmjs.com/package/bmad-creative-intelligence-suite) +- **GitHub:** [bmad-code-org/bmad-module-creative-intelligence-suite](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) + +**Provides:** + +- Innovation Strategist, Design Thinking Coach, and Brainstorming Coach agents +- Problem Solver and Creative Problem Solver for systematic and lateral thinking +- Storyteller and Presentation Master for narratives and pitches +- Ideation frameworks including SCAMPER, Reverse Brainstorming, and problem reframing + +## Game Dev Studio + +Structured game development workflows adapted for Unity, Unreal, Godot, and custom engines. Supports rapid prototyping through Quick Flow and full-scale production with epic-driven sprints. + +- **Code:** `gds` +- **npm:** [`bmad-game-dev-studio`](https://www.npmjs.com/package/bmad-game-dev-studio) +- **GitHub:** [bmad-code-org/bmad-module-game-dev-studio](https://github.com/bmad-code-org/bmad-module-game-dev-studio) + +**Provides:** + +- Game Design Document (GDD) generation workflow +- Quick Dev mode for rapid prototyping +- Narrative design support for characters, dialogue, and world-building +- Coverage for 21+ game types with engine-specific architecture guidance + +## Test Architect (TEA) + +Enterprise-grade test strategy, automation guidance, and release gate decisions through an expert agent and nine structured workflows. TEA goes well beyond the built-in QA agent with risk-based prioritization and requirements traceability. + +- **Code:** `tea` +- **npm:** [`bmad-method-test-architecture-enterprise`](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise) +- **GitHub:** [bmad-code-org/bmad-method-test-architecture-enterprise](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) + +**Provides:** + +- Murat agent (Master Test Architect and Quality Advisor) +- Workflows for test design, ATDD, automation, test review, and traceability +- NFR assessment, CI setup, and framework scaffolding +- P0-P3 prioritization with optional Playwright Utils and MCP integrations + +## Community Modules + +Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates. diff --git a/package.json b/package.json index 96a1814f1..7b5fd9a29 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "scripts": { "bmad:install": "node tools/cli/bmad-cli.js install", "bundle": "node tools/cli/bundlers/bundle-web.js all", - "docs:build": "node tools/build-docs.js", + "docs:build": "node tools/build-docs.mjs", "docs:dev": "astro dev --root website", "docs:fix-links": "node tools/fix-doc-links.js", "docs:preview": "astro preview --root website", diff --git a/tools/build-docs.js b/tools/build-docs.mjs similarity index 92% rename from tools/build-docs.js rename to tools/build-docs.mjs index dfb2c0a8e..c6c57a99d 100644 --- a/tools/build-docs.js +++ b/tools/build-docs.mjs @@ -9,19 +9,20 @@ * build/site/ - Final Astro output (deployable) */ -const { execSync } = require('node:child_process'); -const fs = require('node:fs'); -const path = require('node:path'); -const archiver = require('archiver'); +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import archiver from 'archiver'; +import { getSiteUrl } from '../website/src/lib/site-url.mjs'; // ============================================================================= // Configuration // ============================================================================= -const PROJECT_ROOT = path.dirname(__dirname); +const PROJECT_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const BUILD_DIR = path.join(PROJECT_ROOT, 'build'); -const SITE_URL = process.env.SITE_URL || 'https://bmad-code-org.github.io/BMAD-METHOD'; const REPO_URL = 'https://github.com/bmad-code-org/BMAD-METHOD'; // DO NOT CHANGE THESE VALUES! @@ -143,39 +144,40 @@ function buildAstroSite() { function generateLlmsTxt(outputDir) { console.log(' → Generating llms.txt...'); + const siteUrl = getSiteUrl(); const content = [ '# BMAD Method Documentation', '', '> AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.', '', - `Documentation: ${SITE_URL}`, + `Documentation: ${siteUrl}`, `Repository: ${REPO_URL}`, - `Full docs: ${SITE_URL}/llms-full.txt`, + `Full docs: ${siteUrl}/llms-full.txt`, '', '## Quick Start', '', - `- **[Quick Start](${SITE_URL}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`, - `- **[Installation](${SITE_URL}/docs/getting-started/installation)** - Installation guide`, + `- **[Quick Start](${siteUrl}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`, + `- **[Installation](${siteUrl}/docs/getting-started/installation)** - Installation guide`, '', '## Core Concepts', '', - `- **[Scale Adaptive System](${SITE_URL}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`, - `- **[Quick Flow](${SITE_URL}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`, - `- **[Party Mode](${SITE_URL}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`, + `- **[Scale Adaptive System](${siteUrl}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`, + `- **[Quick Flow](${siteUrl}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`, + `- **[Party Mode](${siteUrl}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`, '', '## Modules', '', - `- **[BMM - Method](${SITE_URL}/docs/modules/bmm/quick-start)** - Core methodology module`, - `- **[BMB - Builder](${SITE_URL}/docs/modules/bmb/)** - Agent and workflow builder`, - `- **[BMGD - Game Dev](${SITE_URL}/docs/modules/bmgd/quick-start)** - Game development module`, + `- **[BMM - Method](${siteUrl}/docs/modules/bmm/quick-start)** - Core methodology module`, + `- **[BMB - Builder](${siteUrl}/docs/modules/bmb/)** - Agent and workflow builder`, + `- **[BMGD - Game Dev](${siteUrl}/docs/modules/bmgd/quick-start)** - Game development module`, '', '---', '', '## Quick Links', '', - `- [Full Documentation (llms-full.txt)](${SITE_URL}/llms-full.txt) - Complete docs for AI context`, - `- [Source Bundle](${SITE_URL}/downloads/bmad-sources.zip) - Complete source code`, - `- [Prompts Bundle](${SITE_URL}/downloads/bmad-prompts.zip) - Agent prompts and workflows`, + `- [Full Documentation (llms-full.txt)](${siteUrl}/llms-full.txt) - Complete docs for AI context`, + `- [Source Bundle](${siteUrl}/downloads/bmad-sources.zip) - Complete source code`, + `- [Prompts Bundle](${siteUrl}/downloads/bmad-prompts.zip) - Agent prompts and workflows`, '', ].join('\n'); diff --git a/tools/cli/external-official-modules.yaml b/tools/cli/external-official-modules.yaml index 436dc01df..431ded4a3 100644 --- a/tools/cli/external-official-modules.yaml +++ b/tools/cli/external-official-modules.yaml @@ -16,7 +16,7 @@ modules: url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite module-definition: src/module.yaml code: cis - name: "BMad Creative Innovation Suite" + name: "BMad Creative Intelligence Suite" description: "Creative tools for writing, brainstorming, and more" defaultSelected: false type: bmad-org @@ -26,7 +26,7 @@ modules: url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git module-definition: src/module.yaml code: gds - name: "BMad Game Dev Suite" + name: "BMad Game Dev Studio" description: "Game development agents and workflows" defaultSelected: false type: bmad-org diff --git a/tools/docs/_prompt-external-modules-page.md b/tools/docs/_prompt-external-modules-page.md new file mode 100644 index 000000000..f5e124373 --- /dev/null +++ b/tools/docs/_prompt-external-modules-page.md @@ -0,0 +1,59 @@ +# Prompt: Generate External Modules Reference Page + +## Goal + +Create a reference documentation page at `docs/reference/modules.md` that lists all official external BMad modules with descriptions and links. + +## Source of Truth + +Read `tools/cli/external-official-modules.yaml` — this is the authoritative registry of official external modules. Use the module names, codes, npm package names, and repository URLs from this file. + +## Research Step + +For each module in the registry, visit its GitHub repository (url in the YAML record) +and read its README to get: +- A 1-2 sentence description of what the module does +- The key agents and workflows it provides (if listed) +- Any notable features or use cases + +## Output Format + +Create `docs/reference/modules.md` following the project's Reference Catalog structure (see `docs/_STYLE_GUIDE.md`): + +``` +1. Title + Hook +2. Items (## for each module) + - Brief description (one sentence) + - **Key Info:** as flat list (code, npm package, GitHub link) +3. Installation note +``` + +## Style +use @docs/_STYLE_GUIDE.md + +## Frontmatter + +```yaml +--- +title: Official Modules +--- +``` + +## Content Requirements + +- Start with a brief intro explaining that BMad extends through official modules selected during installation +- For each module include: + - `##` header with module name + - 1-2 sentence description (sourced from GitHub README, not just the registry's short description) + - Key info list: module code, npm package (linked), GitHub repo (linked) + - Brief bullet list of what it provides (agents, workflows, key features) — keep to 3-5 bullets +- Include a `:::tip` admonition about how to install modules (via `npx bmad-method` installer) +- Mention that community modules and a marketplace are coming +- Do NOT include built-in modules (core, bmm) — this page is specifically for external/add-on modules + +## Existing Pages for Reference + +Look at these files to match the tone and style of existing reference docs: +- `docs/reference/agents.md` +- `docs/reference/commands.md` +- `docs/reference/testing.md` diff --git a/tools/docs/index.md b/tools/docs/index.md deleted file mode 100644 index 8ac7bc86f..000000000 --- a/tools/docs/index.md +++ /dev/null @@ -1,2 +0,0 @@ -# Tool and Repo Maintainability Documentation - diff --git a/website/_basement/components/WorkflowGuide.astro b/website/_basement/components/WorkflowGuide.astro deleted file mode 100644 index d9dc7e197..000000000 --- a/website/_basement/components/WorkflowGuide.astro +++ /dev/null @@ -1,444 +0,0 @@ ---- ---- - -
-
- /bmad-help - Run this anytime to see what to do next — or ask it a question like "what should I do to build a web app?" -
- -

Loading agents is optional. If your IDE supports slash commands, you can run workflows directly.

- -
-
- - - -
-
- -
-

Select a track above to see the workflow.

- -
- -
Analysis
- -
- /brainstorm-project - Analyst - -

Guided ideation using 60+ techniques to explore your project idea and create brainstorm notes.

-
-
- -
- /research - Analyst - -

Market, technical, or competitive research producing a structured research document.

-
-
- -
- /product-brief - Analyst - -

Combines brainstorm and research into a foundation document covering problem, users, and MVP scope.

-
-
- - -
Planning
- -
- /quick-spec - Barry - -

Analyzes your codebase, auto-detects stack, and produces tech-spec.md with implementation-ready story files.

-
-
- -
- /create-prd - PM - -

Creates PRD.md with user personas, requirements, success metrics, and risks.

-
-
- -
- /create-ux-design - UX Designer - -

Creates ux-design.md with user journeys, wireframes, and a design system.

-
-
- - -
Solutioning
- -
- /create-architecture - Architect - -

Designs system architecture with ADRs covering data, API, security, and deployment decisions.

-
-
- -
- /create-epics-and-stories - PM - -

Breaks PRD and architecture into epic files with prioritized, technically-informed stories.

-
-
- -
- /implementation-readiness - Architect - -

Validates cohesion across all planning documents to confirm you're ready to build.

-
-
- - -
Implementation
- -
- /sprint-planning - SM - -

Initializes sprint-status.yaml to track all stories through development. Run once.

-
-
- - -
- ↻ Repeat for each story - -
- /create-story - SM - -

Prepares a story file with full context and acceptance criteria from the epic.

-
-
- -
- /dev-story - DEV - -

Implements production code and tests following architecture patterns.

-
-
- -
- /code-review - DEV - -

Reviews code for quality, architecture alignment, tests, and security.

-
-
-
- -
- /epic-retrospective - SM - -

Captures learnings from a completed epic to improve the next one.

-
-
-
-
- - - - diff --git a/website/_basement/pages/workflow-guide.astro b/website/_basement/pages/workflow-guide.astro deleted file mode 100644 index f9f929dfc..000000000 --- a/website/_basement/pages/workflow-guide.astro +++ /dev/null @@ -1,17 +0,0 @@ ---- -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; -import WorkflowGuide from '../components/WorkflowGuide.astro'; ---- - - -

- This interactive guide helps you understand which workflows to run, which agents to use, and what outputs to expect at each phase. Select your project's track to see the relevant path. -

- -
diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 23a3179c4..d59de430a 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -4,7 +4,7 @@ import starlight from '@astrojs/starlight'; import sitemap from '@astrojs/sitemap'; import rehypeMarkdownLinks from './src/rehype-markdown-links.js'; import rehypeBasePaths from './src/rehype-base-paths.js'; -import { getSiteUrl } from './src/lib/site-url.js'; +import { getSiteUrl } from './src/lib/site-url.mjs'; const siteUrl = getSiteUrl(); const urlParts = new URL(siteUrl); diff --git a/website/src/components/Banner.astro b/website/src/components/Banner.astro index f1e460705..d0c94e5dc 100644 --- a/website/src/components/Banner.astro +++ b/website/src/components/Banner.astro @@ -1,12 +1,11 @@ --- -import { getSiteUrl } from '../lib/site-url.js'; +import { getSiteUrl } from '../lib/site-url.mjs'; -const SITE_URL = getSiteUrl(); -const fullDocsUrl = `${SITE_URL}/llms-full.txt`; +const llmsFullUrl = `${getSiteUrl()}/llms-full.txt`; ---
- 🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context. + 🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context.
diff --git a/website/src/rehype-base-paths.js b/website/src/rehype-base-paths.js index c61600912..317f07043 100644 --- a/website/src/rehype-base-paths.js +++ b/website/src/rehype-base-paths.js @@ -3,7 +3,6 @@ * * Transforms: * /img/foo.png → /BMAD-METHOD/img/foo.png (when base is /BMAD-METHOD/) - * /downloads/file.zip → /BMAD-METHOD/downloads/file.zip * /llms.txt → /BMAD-METHOD/llms.txt * * Only affects absolute paths (/) - relative paths and external URLs are unchanged. From 4c36c94c2d8443d5017db1ba3d8686c8ca9ef7f3 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Feb 2026 08:17:41 -0700 Subject: [PATCH 34/38] chore: configure dual-mode AI code review (#1511) Add Augment Code Review (audit mode) and CodeRabbit (adversarial mode): Augment (.augment/code_review_guidelines.yaml): - Workflow structure and step validation rules - Agent definition validation - Path placeholder enforcement - JIT loading and HALT requirements CodeRabbit (.coderabbit.yaml): - Raven-style adversarial reviewer persona - Finds logical contradictions and missing implementations - No rule anchoring - reasons freely Supporting changes: - .gitignore: exclude .augment/ from ignore - eslint.config.mjs: ignore .augment/ directory fix: clarify .augment gitignore pattern and eslint comment Add documentation comment to .gitignore explaining the .augment/* exception pattern, and replace misleading eslint comment about "underscores per their spec" with accurate description of vendor config directory exclusion. Addresses CodeRabbit findings F10 and F11 from PR #1511 review. Co-Authored-By: Claude Opus 4.6 fix: remove redundant eslint ignore patterns The broader glob patterns (dir/**) already match all files recursively, making the more specific sub-patterns (dir/**/*.js, dir/**/*.md, etc.) completely redundant. Similarly, _bmad*/** already covers _bmad/**. Co-Authored-By: Claude Opus 4.6 fix: synchronize ignore baselines across CodeRabbit and Augment configs Expand path exclusions in both PR review tools to a shared baseline: - Mutual config exclusions (each tool ignores its own and others configs) - Build output, vendored/generated files, package metadata, binary/media - Test fixtures, non-project dirs, AI assistant dirs, build temp - Generated reports CodeRabbit goes from 1 exclusion to 32; Augment from 12 to 32. ESLint already had comprehensive ignores and is unchanged. Addresses CodeRabbit findings F2 and F4 from PR #1511 review. Co-Authored-By: Claude Opus 4.6 fix: correct project name in Augment review guidelines fix: remove instruction that explicitly encourages false positives --- .augment/code_review_guidelines.yaml | 271 +++++++++++++++++++++++++++ .coderabbit.yaml | 67 +++++-- .gitignore | 4 +- eslint.config.mjs | 8 +- 4 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 .augment/code_review_guidelines.yaml diff --git a/.augment/code_review_guidelines.yaml b/.augment/code_review_guidelines.yaml new file mode 100644 index 000000000..02e4f2b95 --- /dev/null +++ b/.augment/code_review_guidelines.yaml @@ -0,0 +1,271 @@ +# Augment Code Review Guidelines for BMAD-METHOD +# https://docs.augmentcode.com/codereview/overview +# Focus: Workflow validation and quality + +file_paths_to_ignore: + # --- Shared baseline: tool configs --- + - ".coderabbit.yaml" + - ".augment/**" + - "eslint.config.mjs" + # --- Shared baseline: build output --- + - "dist/**" + - "build/**" + - "coverage/**" + # --- Shared baseline: vendored/generated --- + - "node_modules/**" + - "**/*.min.js" + - "**/*.generated.*" + - "**/*.bundle.md" + # --- Shared baseline: package metadata --- + - "package-lock.json" + # --- Shared baseline: binary/media --- + - "*.png" + - "*.jpg" + - "*.svg" + # --- Shared baseline: test fixtures --- + - "test/fixtures/**" + - "test/template-test-generator/**" + - "tools/template-test-generator/test-scenarios/**" + # --- Shared baseline: non-project dirs --- + - "_bmad*/**" + - "website/**" + - "z*/**" + - "sample-project/**" + - "test-project-install/**" + # --- Shared baseline: AI assistant dirs --- + - ".claude/**" + - ".codex/**" + - ".agent/**" + - ".agentvibes/**" + - ".kiro/**" + - ".roo/**" + - ".github/chatmodes/**" + # --- Shared baseline: build temp --- + - ".bundler-temp/**" + # --- Shared baseline: generated reports --- + - "**/validation-report-*.md" + - "CHANGELOG.md" + +areas: + # ============================================ + # WORKFLOW STRUCTURE RULES + # ============================================ + workflow_structure: + description: "Workflow folder organization and required components" + globs: + - "src/**/workflows/**" + rules: + - id: "workflow_entry_point_required" + description: "Every workflow folder must have workflow.yaml, workflow.md, or workflow.xml as entry point" + severity: "high" + + - id: "sharded_workflow_steps_folder" + description: "Sharded workflows (using workflow.md) must have steps/ folder with numbered files (step-01-*.md, step-02-*.md)" + severity: "high" + + - id: "standard_workflow_instructions" + description: "Standard workflows using workflow.yaml must include instructions.md for execution guidance" + severity: "medium" + + - id: "workflow_step_limit" + description: "Workflows should have 5-10 steps maximum to prevent context loss in LLM execution" + severity: "medium" + + # ============================================ + # WORKFLOW ENTRY FILE RULES + # ============================================ + workflow_definitions: + description: "Workflow entry files (workflow.yaml, workflow.md, workflow.xml)" + globs: + - "src/**/workflows/**/workflow.yaml" + - "src/**/workflows/**/workflow.md" + - "src/**/workflows/**/workflow.xml" + rules: + - id: "workflow_name_required" + description: "Workflow entry files must define 'name' field in frontmatter or root element" + severity: "high" + + - id: "workflow_description_required" + description: "Workflow entry files must include 'description' explaining the workflow's purpose" + severity: "high" + + - id: "workflow_config_source" + description: "Workflows should reference config_source for variable resolution (e.g., {project-root}/_bmad/module/config.yaml)" + severity: "medium" + + - id: "workflow_installed_path" + description: "Workflows should define installed_path for relative file references within the workflow" + severity: "medium" + + - id: "valid_step_references" + description: "Step file references in workflow entry must point to existing files" + severity: "high" + + # ============================================ + # SHARDED WORKFLOW STEP RULES + # ============================================ + workflow_steps: + description: "Individual step files in sharded workflows" + globs: + - "src/**/workflows/**/steps/step-*.md" + rules: + - id: "step_goal_required" + description: "Each step must clearly state its goal (## STEP GOAL, ## YOUR TASK, or step n='X' goal='...')" + severity: "high" + + - id: "step_mandatory_rules" + description: "Step files should include MANDATORY EXECUTION RULES section with universal agent behavior rules" + severity: "medium" + + - id: "step_context_boundaries" + description: "Step files should define CONTEXT BOUNDARIES explaining available context and limits" + severity: "medium" + + - id: "step_success_metrics" + description: "Step files should include SUCCESS METRICS section with ✅ checkmarks for validation criteria" + severity: "medium" + + - id: "step_failure_modes" + description: "Step files should include FAILURE MODES section with ❌ marks for anti-patterns to avoid" + severity: "medium" + + - id: "step_next_step_reference" + description: "Step files should reference the next step file path for sequential execution" + severity: "medium" + + - id: "step_no_forward_loading" + description: "Steps must NOT load future step files until current step completes - just-in-time loading only" + severity: "high" + + - id: "valid_file_references" + description: "File path references using {variable}/filename.md must point to existing files" + severity: "high" + + - id: "step_naming" + description: "Step files must be named step-NN-description.md (e.g., step-01-init.md, step-02-context.md)" + severity: "medium" + + - id: "halt_before_menu" + description: "Steps presenting user menus ([C] Continue, [a] Advanced, etc.) must HALT and wait for response" + severity: "high" + + # ============================================ + # XML WORKFLOW/TASK RULES + # ============================================ + xml_workflows: + description: "XML-based workflows and tasks" + globs: + - "src/**/workflows/**/*.xml" + - "src/**/tasks/**/*.xml" + rules: + - id: "xml_task_id_required" + description: "XML tasks must have unique 'id' attribute on root task element" + severity: "high" + + - id: "xml_llm_instructions" + description: "XML workflows should include section with critical execution instructions for the agent" + severity: "medium" + + - id: "xml_step_numbering" + description: "XML steps should use n='X' attribute for sequential numbering" + severity: "medium" + + - id: "xml_action_tags" + description: "Use for required actions, for user input (must HALT), for jumps, for conditionals" + severity: "medium" + + - id: "xml_ask_must_halt" + description: " tags require agent to HALT and wait for user response before continuing" + severity: "high" + + # ============================================ + # WORKFLOW CONTENT QUALITY + # ============================================ + workflow_content: + description: "Content quality and consistency rules for all workflow files" + globs: + - "src/**/workflows/**/*.md" + - "src/**/workflows/**/*.yaml" + rules: + - id: "communication_language_variable" + description: "Workflows should use {communication_language} variable for agent output language consistency" + severity: "low" + + - id: "path_placeholders_required" + description: "Use path placeholders (e.g. {project-root}, {installed_path}, {output_folder}) instead of hardcoded paths" + severity: "medium" + + - id: "no_time_estimates" + description: "Workflows should NOT include time estimates - AI development speed varies significantly" + severity: "low" + + - id: "facilitator_not_generator" + description: "Workflow agents should act as facilitators (guide user input) not content generators (create without input)" + severity: "medium" + + - id: "no_skip_optimization" + description: "Workflows must execute steps sequentially - no skipping or 'optimizing' step order" + severity: "high" + + # ============================================ + # AGENT DEFINITIONS + # ============================================ + agent_definitions: + description: "Agent YAML configuration files" + globs: + - "src/**/*.agent.yaml" + rules: + - id: "agent_metadata_required" + description: "Agent files must have metadata section with id, name, title, icon, and module" + severity: "high" + + - id: "agent_persona_required" + description: "Agent files must define persona with role, identity, communication_style, and principles" + severity: "high" + + - id: "agent_menu_valid_workflows" + description: "Menu triggers must reference valid workflow paths that exist" + severity: "high" + + # ============================================ + # TEMPLATES + # ============================================ + templates: + description: "Template files for workflow outputs" + globs: + - "src/**/template*.md" + - "src/**/templates/**/*.md" + rules: + - id: "placeholder_syntax" + description: "Use {variable_name} or {{variable_name}} syntax consistently for placeholders" + severity: "medium" + + - id: "template_sections_marked" + description: "Template sections that need generation should be clearly marked (e.g., )" + severity: "low" + + # ============================================ + # DOCUMENTATION + # ============================================ + documentation: + description: "Documentation files" + globs: + - "docs/**/*.md" + - "README.md" + - "CONTRIBUTING.md" + rules: + - id: "valid_internal_links" + description: "Internal markdown links must point to existing files" + severity: "medium" + + # ============================================ + # BUILD TOOLS + # ============================================ + build_tools: + description: "Build scripts and tooling" + globs: + - "tools/**" + rules: + - id: "script_error_handling" + description: "Scripts should handle errors gracefully with proper exit codes" + severity: "medium" diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 58eb549f0..9b7f85774 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -17,21 +17,66 @@ reviews: base_branches: - main path_filters: + # --- Shared baseline: tool configs --- + - "!.coderabbit.yaml" + - "!.augment/**" + - "!eslint.config.mjs" + # --- Shared baseline: build output --- + - "!dist/**" + - "!build/**" + - "!coverage/**" + # --- Shared baseline: vendored/generated --- - "!**/node_modules/**" + - "!**/*.min.js" + - "!**/*.generated.*" + - "!**/*.bundle.md" + # --- Shared baseline: package metadata --- + - "!package-lock.json" + # --- Shared baseline: binary/media --- + - "!*.png" + - "!*.jpg" + - "!*.svg" + # --- Shared baseline: test fixtures --- + - "!test/fixtures/**" + - "!test/template-test-generator/**" + - "!tools/template-test-generator/test-scenarios/**" + # --- Shared baseline: non-project dirs --- + - "!_bmad*/**" + - "!website/**" + - "!z*/**" + - "!sample-project/**" + - "!test-project-install/**" + # --- Shared baseline: AI assistant dirs --- + - "!.claude/**" + - "!.codex/**" + - "!.agent/**" + - "!.agentvibes/**" + - "!.kiro/**" + - "!.roo/**" + - "!.github/chatmodes/**" + # --- Shared baseline: build temp --- + - "!.bundler-temp/**" + # --- Shared baseline: generated reports --- + - "!**/validation-report-*.md" + - "!CHANGELOG.md" path_instructions: - path: "**/*" instructions: | - Focus on inconsistencies, contradictions, edge cases and serious issues. - Avoid commenting on minor issues such as linting, formatting and style issues. - When providing code suggestions, use GitHub's suggestion format: - ```suggestion - - ``` - - path: "**/*.js" - instructions: | - CLI tooling code. Check for: missing error handling on fs operations, - path.join vs string concatenation, proper cleanup in error paths. - Flag any process.exit() without error message. + You are a cynical, jaded reviewer with zero patience for sloppy work. + This PR was submitted by a clueless weasel and you expect to find problems. + Be skeptical of everything. + Look for what's missing, not just what's wrong. + Use a precise, professional tone — no profanity or personal attacks. + + Review with extreme skepticism — assume problems exist. + Find at least 10 issues to fix or improve. + + Do NOT: + - Comment on formatting, linting, or style + - Give "looks good" passes + - Anchor on any specific ruleset — reason freely + + If you find zero issues, re-analyze — this is suspicious. chat: auto_reply: true # Response to mentions in comments, a la @coderabbit review issue_enrichment: diff --git a/.gitignore b/.gitignore index 6af83303b..0f130a3b3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,9 @@ z*/ _bmad _bmad-output .clinerules -.augment +# .augment/ is gitignored except tracked config files — add exceptions explicitly +.augment/* +!.augment/code_review_guidelines.yaml .crush .cursor .iflow diff --git a/eslint.config.mjs b/eslint.config.mjs index e361b1cdf..90dbf1553 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,11 +12,7 @@ export default [ 'coverage/**', '**/*.min.js', 'test/template-test-generator/**', - 'test/template-test-generator/**/*.js', - 'test/template-test-generator/**/*.md', 'test/fixtures/**', - 'test/fixtures/**/*.yaml', - '_bmad/**', '_bmad*/**', // Build output 'build/**', @@ -36,6 +32,10 @@ export default [ 'tools/template-test-generator/test-scenarios/**', 'src/modules/*/sub-modules/**', '.bundler-temp/**', + // Augment vendor config — not project code, naming conventions + // are dictated by Augment and can't be changed, so exclude + // the entire directory from linting + '.augment/**', ], }, From 045b1fe148fed95836851d2c8138a6ac12be9cb1 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Feb 2026 09:18:16 -0700 Subject: [PATCH 35/38] chore: sync package-lock.json after archiver removal (#1580) Removes leftover archiver dependencies from the lock file following #1577. Co-authored-by: Claude Opus 4.6 --- package-lock.json | 474 +--------------------------------------------- 1 file changed, 3 insertions(+), 471 deletions(-) diff --git a/package-lock.json b/package-lock.json index d03230bf0..748fd0255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "@astrojs/sitemap": "^3.6.0", "@astrojs/starlight": "^0.37.5", "@eslint/js": "^9.33.0", - "archiver": "^7.0.1", "astro": "^5.16.0", "c8": "^10.1.3", "eslint": "^9.33.0", @@ -2030,9 +2029,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -3950,19 +3949,6 @@ "win32" ] }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4095,131 +4081,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/archiver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", - "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.2", - "async": "^3.2.4", - "buffer-crc32": "^1.0.0", - "readable-stream": "^4.0.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^3.0.0", - "zip-stream": "^6.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", - "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.0.0", - "graceful-fs": "^4.2.0", - "is-stream": "^2.0.1", - "lazystream": "^1.0.0", - "lodash": "^4.17.15", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/archiver-utils/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4964,21 +4825,6 @@ "node": ">= 0.4" } }, - "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -5132,21 +4978,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, "node_modules/base-64": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", @@ -5356,16 +5187,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5864,23 +5685,6 @@ "dev": true, "license": "ISC" }, - "node_modules/compress-commons": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", - "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "crc32-stream": "^6.0.0", - "is-stream": "^2.0.1", - "normalize-path": "^3.0.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5937,40 +5741,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", - "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", - "dev": true, - "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7090,16 +6860,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -7107,26 +6867,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7212,13 +6952,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8545,13 +8278,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -9628,52 +9354,6 @@ "node": ">= 8" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9830,13 +9510,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.iteratee": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.iteratee/-/lodash.iteratee-4.7.0.tgz", @@ -12328,23 +12001,6 @@ "node": ">=6" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -12442,81 +12098,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dev": true, - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readable-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -13517,18 +13098,6 @@ "dev": true, "license": "MIT" }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13870,18 +13439,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -13984,16 +13541,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -15144,21 +14691,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zip-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", - "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "archiver-utils": "^5.0.0", - "compress-commons": "^6.0.2", - "readable-stream": "^4.0.0" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", From cb73c05cf6ba047f5a7716afff3c3bb6f5ab31ac Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Feb 2026 09:27:57 -0700 Subject: [PATCH 36/38] fix: use pull_request_target for CodeRabbit review trigger (#1583) The workflow was failing with 403 "Resource not accessible by integration" on fork PRs because pull_request events get read-only GITHUB_TOKEN permissions for cross-repository PRs. Switching to pull_request_target runs the workflow in the base repo context, granting write permissions needed to post the @coderabbitai review comment. This is safe because the workflow only posts a comment and does not check out or execute any code from the PR branch. Co-authored-by: Claude Opus 4.6 --- .github/workflows/coderabbit-review.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coderabbit-review.yaml b/.github/workflows/coderabbit-review.yaml index db59d22f4..fb284d664 100644 --- a/.github/workflows/coderabbit-review.yaml +++ b/.github/workflows/coderabbit-review.yaml @@ -1,7 +1,7 @@ name: Trigger CodeRabbit on Ready for Review on: - pull_request: + pull_request_target: types: [ready_for_review] jobs: From ecf7fbcb2cf88e67f61e0928ac9283824bb69f37 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Feb 2026 09:43:16 -0700 Subject: [PATCH 37/38] docs: add description front matter to documentation pages (#1566) --- docs/_STYLE_GUIDE.md | 1 + docs/bmgd/game-types.md | 1 + docs/bmgd/quick-flow-workflows.md | 1 + docs/how-to/customize-bmad.md | 1 + docs/how-to/shard-large-documents.md | 1 + docs/index.md | 1 + docs/reference/agents.md | 1 + docs/reference/modules.md | 1 + docs/reference/testing.md | 1 + 9 files changed, 9 insertions(+) diff --git a/docs/_STYLE_GUIDE.md b/docs/_STYLE_GUIDE.md index 3e78387af..801314cd0 100644 --- a/docs/_STYLE_GUIDE.md +++ b/docs/_STYLE_GUIDE.md @@ -1,5 +1,6 @@ --- title: "Documentation Style Guide" +description: Project-specific documentation conventions based on Google style and Diataxis structure --- This project adheres to the [Google Developer Documentation Style Guide](https://developers.google.com/style) and uses [Diataxis](https://diataxis.fr/) to structure content. Only project-specific conventions follow. diff --git a/docs/bmgd/game-types.md b/docs/bmgd/game-types.md index 2ea407218..87870ea10 100644 --- a/docs/bmgd/game-types.md +++ b/docs/bmgd/game-types.md @@ -1,5 +1,6 @@ --- title: "Game Types Reference" +description: 24 game type templates with genre-specific GDD sections for BMGD draft: true --- diff --git a/docs/bmgd/quick-flow-workflows.md b/docs/bmgd/quick-flow-workflows.md index 69ac7506a..239f263a6 100644 --- a/docs/bmgd/quick-flow-workflows.md +++ b/docs/bmgd/quick-flow-workflows.md @@ -1,5 +1,6 @@ --- title: "Quick Flow Workflows" +description: Create tech specs and execute implementations with BMGD Quick Flow draft: true --- diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md index f23a101c6..3c356373f 100644 --- a/docs/how-to/customize-bmad.md +++ b/docs/how-to/customize-bmad.md @@ -1,5 +1,6 @@ --- title: "BMad Method Customization Guide" +description: Customize agents, workflows, and modules while preserving update compatibility --- The ability to customize the BMad Method and its core to your needs, while still being able to get updates and enhancements is a critical idea within the BMad Ecosystem. diff --git a/docs/how-to/shard-large-documents.md b/docs/how-to/shard-large-documents.md index 367164a90..45925e5c6 100644 --- a/docs/how-to/shard-large-documents.md +++ b/docs/how-to/shard-large-documents.md @@ -1,5 +1,6 @@ --- title: "Document Sharding Guide" +description: Split large markdown files into smaller organized files for better context management --- Use the `shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management. diff --git a/docs/index.md b/docs/index.md index 4e1b3cd6e..8b626a63d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,6 @@ --- title: Welcome to the BMad Method +description: AI-driven development framework with specialized agents, guided workflows, and intelligent planning --- The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework that helps you build software through the whole process from ideation and planning all the way through agentic implementation. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity, whether you're fixing a bug or building an enterprise platform. diff --git a/docs/reference/agents.md b/docs/reference/agents.md index 00a2da9d5..87a0927c3 100644 --- a/docs/reference/agents.md +++ b/docs/reference/agents.md @@ -1,5 +1,6 @@ --- title: Agents +description: Default BMM agents with their menu triggers and primary workflows --- This page lists the default BMM (Agile suite) agents that install with BMAD Method, along with their menu triggers and primary workflows. diff --git a/docs/reference/modules.md b/docs/reference/modules.md index c1b9c497e..a0f6fdd6f 100644 --- a/docs/reference/modules.md +++ b/docs/reference/modules.md @@ -1,5 +1,6 @@ --- title: Official Modules +description: Add-on modules for building custom agents, creative intelligence, game development, and testing --- BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite). diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 562526fb2..86af5294b 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -1,5 +1,6 @@ --- title: Testing Options +description: Built-in QA agent and the standalone Test Architect module for advanced testing --- # Testing Options From b1bfce9aa7aaafded09d33b39a91e9ea86a82ea2 Mon Sep 17 00:00:00 2001 From: Davor Racic Date: Sun, 8 Feb 2026 07:40:13 +0100 Subject: [PATCH 38/38] refactor: Complete @clack/prompts Migration & Installer Output Consolidation (#1586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): complete @clack/prompts migration Full migration of BMAD CLI installer from legacy terminal libraries (chalk, ora, boxen, figlet, wrap-ansi, cli-table3, readline) to unified @clack/prompts v1.0.0 visual system. Foundation (prompts.js + cli-utils.js): - Extended prompts.js wrapper with box, spinner, progress, taskLog, path, autocomplete, selectKey, stream, color re-export - Refactored cli-utils.js: displayLogo uses box(), sections use note(), steps use log.step(), removed boxen/figlet/wrap-ansi/cli-table3 UI orchestration (ui.js): - Replaced ~100 console.log+chalk calls with log.*, note(), box() - Replaced ora spinner with @clack spinner - Module selection: autocompleteMultiselect with locked core module, bulleted post-selection display, maxItems for no-scroll Spinner migration (installer.js): - Replaced 40+ ora spinner calls with @clack spinner - All spinner.stop() calls include meaningful messages - Failure paths use spinner.error() (red cross) instead of stop() Readline migration (agent/installer.js + config-collector.js): - Migrated readline prompts to @clack text/confirm/select - Fixed chalk.dim bug (chalk was never imported) - Removed chalk from config-collector.js IDE handlers + modules (7 files): - Replaced chalk+ora across all IDE handlers and module manager - Fixed options.installer undefined bug in manager.js update() Cleanup: - Removed ora, boxen, figlet, wrap-ansi, cli-table3 from dependencies - chalk stays (used outside tools/cli/ scope) - Replaced hand-drawn Unicode update box in bmad-cli.js with box() - Added process.stdin.setMaxListeners(25) for sequential prompts Spinner wrapper adds isSpinning state tracking (not native to @clack). Removed dead groupMultiselect and sortKey sort calls. Ref: tech-spec-installer-clack-migration-ui-enhancement.md Co-Authored-By: Claude Opus 4.6 * feat(cli): consolidate installer output to single spinner + summary Replace ~40 lines of output from 15+ spinner start/stop cycles with a single animated spinner during installation and a final note() summary block showing checkmarks per step. Key changes: - Add results collector pattern in install() method - Replace spinner.stop/start pairs with addResult + spinner.message - Add renderInstallSummary() using prompts.note() with colored output - Propagate silent flag through IDE handlers and module manager - Add spinner race condition guards (start while spinning, stop while stopped) - Add no-op spinner pattern for silent external module cloning - Fix stdin listener limit to be defensive with Math.max - Add GIT_TERMINAL_PROMPT=0 for non-interactive git operations - Merge locked values into initialValue for autocomplete prompts Co-Authored-By: Claude Opus 4.6 * fix(cli): resolve code review findings from @clack/prompts migration Address 31 issues across 14 CLI files found during PR #1586 review (Augment Code + CodeRabbit): - Fix bmadDir ReferenceError by hoisting declaration before try block - Wrap console.log monkey-patch in try/finally for safe restoration - Fix transformWorkflowPath dead code and undefined return path - Fix broken symlink crash in _config-driven.js and codex.js cleanup - Pass installer instance through update() for agent recompilation - Fix @clack/prompts API: defaultValue→default, initialValue→default - Use nullish coalescing (??) instead of logical OR for falsy values - Forward options in recursive promptToolSelection calls - Remove no-op replaceAll('_bmad','_bmad') in manager and generator - Remove unused confirm prompt in config-collector hasNoConfig branch - Guard spinner.message() when spinner is not running - Add missing methods to silent spinner stub (cancel, clear, isSpinning) - Wrap install.js error handler with inner try/catch + console fallback - Gate codex per-entry error log with silent flag - Add return statements to all stream wrapper methods - Remove dead variables (availableNames, hasCustomContentItems) - Filter core module from update flow selection - Replace borderColor ternary chain with object map - Fix Kilo "agents" label to "modes" in IDE manager - Normalize error return shape for unsupported IDEs - Fix spinner message timing before dependency resolution - Guard undefined moduleDir in dependency-resolver - Fix workflowsInstalled counter inflation in custom handler - Migrate console.warn calls to prompts.log.warn - Replace console.log() with prompts.log.message('') - Fix legacyBmadPath hardcoded to .bmad instead of entry.name - Fix focusedValue never assigned breaking SPACE toggle and hints Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- package-lock.json | 390 +-------------- package.json | 5 - tools/cli/bmad-cli.js | 30 +- tools/cli/commands/install.js | 37 +- tools/cli/commands/status.js | 18 +- .../installers/lib/core/config-collector.js | 63 +-- .../lib/core/dependency-resolver.js | 36 +- tools/cli/installers/lib/core/installer.js | 463 +++++++++--------- tools/cli/installers/lib/custom/handler.js | 23 +- tools/cli/installers/lib/ide/_base-ide.js | 6 +- .../cli/installers/lib/ide/_config-driven.js | 49 +- tools/cli/installers/lib/ide/codex.js | 100 ++-- tools/cli/installers/lib/ide/kilo.js | 27 +- tools/cli/installers/lib/ide/kiro-cli.js | 14 +- tools/cli/installers/lib/ide/manager.js | 54 +- .../lib/ide/shared/agent-command-generator.js | 1 - .../ide/shared/task-tool-command-generator.js | 1 - .../ide/shared/workflow-command-generator.js | 21 +- tools/cli/installers/lib/message-loader.js | 12 +- tools/cli/installers/lib/modules/manager.js | 159 +++--- tools/cli/lib/agent/installer.js | 74 +-- tools/cli/lib/cli-utils.js | 153 ++---- tools/cli/lib/prompts.js | 237 +++++++-- tools/cli/lib/ui.js | 438 ++++++++--------- 24 files changed, 1048 insertions(+), 1363 deletions(-) diff --git a/package-lock.json b/package-lock.json index 748fd0255..9f0ce7e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,20 +12,15 @@ "@clack/core": "^1.0.0", "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", - "boxen": "^5.1.2", "chalk": "^4.1.2", - "cli-table3": "^0.6.5", "commander": "^14.0.0", "csv-parse": "^6.1.0", - "figlet": "^1.8.0", "fs-extra": "^11.3.0", "glob": "^11.0.3", "ignore": "^7.0.5", "js-yaml": "^4.1.0", - "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.6.3", - "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", "yaml": "^2.7.0" }, @@ -777,16 +772,6 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@ctrl/tinycolor": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", @@ -2029,9 +2014,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -3993,6 +3978,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.1.0" @@ -4985,26 +4971,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -5042,59 +5008,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, - "node_modules/boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5163,30 +5082,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5255,6 +5150,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5433,18 +5329,6 @@ "node": ">=0.8.0" } }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5461,33 +5345,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, "node_modules/cli-truncate": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", @@ -5560,15 +5417,6 @@ "node": ">=8" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5942,18 +5790,6 @@ "node": ">=0.10.0" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -7034,21 +6870,6 @@ } } }, - "node_modules/figlet": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz", - "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==", - "license": "MIT", - "dependencies": { - "commander": "^14.0.0" - }, - "bin": { - "figlet": "bin/index.js" - }, - "engines": { - "node": ">= 17.0.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7906,26 +7727,6 @@ "@babel/runtime": "^7.23.2" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -8022,6 +7823,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -8206,15 +8008,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8250,18 +8043,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -9523,22 +9304,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -10985,6 +10750,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11296,6 +11062,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11344,81 +11111,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", @@ -12726,26 +12418,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -13098,15 +12770,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -13691,18 +13354,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -14152,6 +13803,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -14317,15 +13969,6 @@ "makeerror": "1.0.12" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -14362,18 +14005,6 @@ "node": ">=4" } }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "license": "MIT", - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14388,6 +14019,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -14444,6 +14076,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14453,6 +14086,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index 8102b499c..54d9646a0 100644 --- a/package.json +++ b/package.json @@ -68,20 +68,15 @@ "@clack/core": "^1.0.0", "@clack/prompts": "^1.0.0", "@kayvan/markdown-tree-parser": "^1.6.1", - "boxen": "^5.1.2", "chalk": "^4.1.2", - "cli-table3": "^0.6.5", "commander": "^14.0.0", "csv-parse": "^6.1.0", - "figlet": "^1.8.0", "fs-extra": "^11.3.0", "glob": "^11.0.3", "ignore": "^7.0.5", "js-yaml": "^4.1.0", - "ora": "^5.4.1", "picocolors": "^1.1.1", "semver": "^7.6.3", - "wrap-ansi": "^7.0.0", "xml2js": "^0.6.2", "yaml": "^2.7.0" }, diff --git a/tools/cli/bmad-cli.js b/tools/cli/bmad-cli.js index 2a5b8d387..bcd599293 100755 --- a/tools/cli/bmad-cli.js +++ b/tools/cli/bmad-cli.js @@ -2,6 +2,14 @@ const { program } = require('commander'); const path = require('node:path'); const fs = require('node:fs'); const { execSync } = require('node:child_process'); +const prompts = require('./lib/prompts'); + +// The installer flow uses many sequential @clack/prompts, each adding keypress +// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings. +if (process.stdin?.setMaxListeners) { + const currentLimit = process.stdin.getMaxListeners(); + process.stdin.setMaxListeners(Math.max(currentLimit, 50)); +} // Check for updates - do this asynchronously so it doesn't block startup const packageJson = require('../../package.json'); @@ -27,17 +35,17 @@ async function checkForUpdate() { }).trim(); if (result && result !== packageJson.version) { - console.warn(''); - console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗'); - console.warn(' ║ UPDATE AVAILABLE ║'); - console.warn(' ║ ║'); - console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`); - console.warn(' ║ ║'); - console.warn(' ║ To update,exir and first run: ║'); - console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`); - console.warn(' ║ ║'); - console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝'); - console.warn(''); + const color = await prompts.getColor(); + const updateMsg = [ + `You are using version ${packageJson.version} but ${result} is available.`, + '', + 'To update, exit and first run:', + ` npm cache clean --force && npx bmad-method@${tag} install`, + ].join('\n'); + await prompts.box(updateMsg, 'Update Available', { + rounded: true, + formatBorder: color.yellow, + }); } } catch { // Silently fail - network issues or npm not available diff --git a/tools/cli/commands/install.js b/tools/cli/commands/install.js index 6a6622d1d..961a1a9fa 100644 --- a/tools/cli/commands/install.js +++ b/tools/cli/commands/install.js @@ -1,5 +1,5 @@ -const chalk = require('chalk'); const path = require('node:path'); +const prompts = require('../lib/prompts'); const { Installer } = require('../installers/lib/core/installer'); const { UI } = require('../lib/ui'); @@ -30,14 +30,14 @@ module.exports = { // Set debug flag as environment variable for all components if (options.debug) { process.env.BMAD_DEBUG_MANIFEST = 'true'; - console.log(chalk.cyan('Debug mode enabled\n')); + await prompts.log.info('Debug mode enabled'); } const config = await ui.promptInstall(options); // Handle cancel if (config.actionType === 'cancel') { - console.log(chalk.yellow('Installation cancelled.')); + await prompts.log.warn('Installation cancelled.'); process.exit(0); return; } @@ -45,13 +45,13 @@ module.exports = { // Handle quick update separately if (config.actionType === 'quick-update') { const result = await installer.quickUpdate(config); - console.log(chalk.green('\n✨ Quick update complete!')); - console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`)); + await prompts.log.success('Quick update complete!'); + await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`); // Display version-specific end message const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayEndMessage(); + await messageLoader.displayEndMessage(); process.exit(0); return; @@ -60,8 +60,8 @@ module.exports = { // Handle compile agents separately if (config.actionType === 'compile-agents') { const result = await installer.compileAgents(config); - console.log(chalk.green('\n✨ Agent recompilation complete!')); - console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`)); + await prompts.log.success('Agent recompilation complete!'); + await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`); process.exit(0); return; } @@ -80,21 +80,22 @@ module.exports = { // Display version-specific end message from install-messages.yaml const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayEndMessage(); + await messageLoader.displayEndMessage(); process.exit(0); } } catch (error) { - // Check if error has a complete formatted message - if (error.fullMessage) { - console.error(error.fullMessage); - if (error.stack) { - console.error('\n' + chalk.dim(error.stack)); + try { + if (error.fullMessage) { + await prompts.log.error(error.fullMessage); + } else { + await prompts.log.error(`Installation failed: ${error.message}`); } - } else { - // Generic error handling for all other errors - console.error(chalk.red('Installation failed:'), error.message); - console.error(chalk.dim(error.stack)); + if (error.stack) { + await prompts.log.message(error.stack); + } + } catch { + console.error(error.fullMessage || error.message || error); } process.exit(1); } diff --git a/tools/cli/commands/status.js b/tools/cli/commands/status.js index 5df2cfacd..ec931fe46 100644 --- a/tools/cli/commands/status.js +++ b/tools/cli/commands/status.js @@ -1,5 +1,5 @@ -const chalk = require('chalk'); const path = require('node:path'); +const prompts = require('../lib/prompts'); const { Installer } = require('../installers/lib/core/installer'); const { Manifest } = require('../installers/lib/core/manifest'); const { UI } = require('../lib/ui'); @@ -21,9 +21,9 @@ module.exports = { // Check if bmad directory exists const fs = require('fs-extra'); if (!(await fs.pathExists(bmadDir))) { - console.log(chalk.yellow('No BMAD installation found in the current directory.')); - console.log(chalk.dim(`Expected location: ${bmadDir}`)); - console.log(chalk.dim('\nRun "bmad install" to set up a new installation.')); + await prompts.log.warn('No BMAD installation found in the current directory.'); + await prompts.log.message(`Expected location: ${bmadDir}`); + await prompts.log.message('Run "bmad install" to set up a new installation.'); process.exit(0); return; } @@ -32,8 +32,8 @@ module.exports = { const manifestData = await manifest._readRaw(bmadDir); if (!manifestData) { - console.log(chalk.yellow('No BMAD installation manifest found.')); - console.log(chalk.dim('\nRun "bmad install" to set up a new installation.')); + await prompts.log.warn('No BMAD installation manifest found.'); + await prompts.log.message('Run "bmad install" to set up a new installation.'); process.exit(0); return; } @@ -46,7 +46,7 @@ module.exports = { const availableUpdates = await manifest.checkForUpdates(bmadDir); // Display status - ui.displayStatus({ + await ui.displayStatus({ installation, modules, availableUpdates, @@ -55,9 +55,9 @@ module.exports = { process.exit(0); } catch (error) { - console.error(chalk.red('Status check failed:'), error.message); + await prompts.log.error(`Status check failed: ${error.message}`); if (process.env.BMAD_DEBUG) { - console.error(chalk.dim(error.stack)); + await prompts.log.message(error.stack); } process.exit(1); } diff --git a/tools/cli/installers/lib/core/config-collector.js b/tools/cli/installers/lib/core/config-collector.js index b49075ae0..1a0f50d29 100644 --- a/tools/cli/installers/lib/core/config-collector.js +++ b/tools/cli/installers/lib/core/config-collector.js @@ -1,7 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); -const chalk = require('chalk'); const { getProjectRoot, getModulePath } = require('../../../lib/project-root'); const { CLIUtils } = require('../../../lib/cli-utils'); const prompts = require('../../../lib/prompts'); @@ -260,15 +259,9 @@ class ConfigCollector { // If module has no config keys at all, handle it specially if (hasNoConfig && moduleConfig.subheader) { - // Add blank line for better readability (matches other modules) - console.log(); const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; - - // Display the module name in color first (matches other modules) - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - - // Show the subheader since there's no configuration to ask about - console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`)); + await prompts.log.step(moduleDisplayName); + await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); return false; // No new fields } @@ -322,7 +315,7 @@ class ConfigCollector { } // Show "no config" message for modules with no new questions (that have config keys) - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module already up to date`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`); return false; // No new fields } @@ -350,15 +343,15 @@ class ConfigCollector { if (questions.length > 0) { // Only show header if we actually have questions - CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); - console.log(); // Line break before questions + await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); + await prompts.log.message(''); const promptedAnswers = await prompts.prompt(questions); // Merge prompted answers with static answers Object.assign(allAnswers, promptedAnswers); } else if (newStaticKeys.length > 0) { // Only static fields, no questions - show no config message - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configuration updated`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`); } // Store all answers for cross-referencing @@ -588,7 +581,7 @@ class ConfigCollector { // Skip prompts mode: use all defaults without asking if (this.skipPrompts) { - console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName)); + await prompts.log.info(`Using default configuration for ${moduleDisplayName}`); // Use defaults for all questions for (const question of questions) { const hasDefault = question.default !== undefined && question.default !== null && question.default !== ''; @@ -597,12 +590,10 @@ class ConfigCollector { } } } else { - console.log(); - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); + await prompts.log.step(moduleDisplayName); let customize = true; if (moduleName === 'core') { - // Core module: no confirm prompt, so add spacing manually to match visual style - console.log(chalk.gray('│')); + // Core module: no confirm prompt, continues directly } else { // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) const customizeAnswer = await prompts.prompt([ @@ -621,7 +612,7 @@ class ConfigCollector { const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); if (questionsWithoutDefaults.length > 0) { - console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); + await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`); const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); Object.assign(allAnswers, promptedAnswers); } @@ -747,32 +738,15 @@ class ConfigCollector { const hasNoConfig = actualConfigKeys.length === 0; if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) { - // Module explicitly has no configuration - show with special styling - // Add blank line for better readability (matches other modules) - console.log(); - - // Display the module name in color first (matches other modules) - console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); - - // Ask user if they want to accept defaults or customize on the next line - const { customize } = await prompts.prompt([ - { - type: 'confirm', - name: 'customize', - message: 'Accept Defaults (no to customize)?', - default: true, - }, - ]); - - // Show the subheader if available, otherwise show a default message + await prompts.log.step(moduleDisplayName); if (moduleConfig.subheader) { - console.log(chalk.dim(` ✓ ${moduleConfig.subheader}`)); + await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); } else { - console.log(chalk.dim(` ✓ No custom configuration required`)); + await prompts.log.message(` \u2713 No custom configuration required`); } } else { // Module has config but just no questions to ask - console.log(chalk.dim(` ✓ ${moduleName.toUpperCase()} module configured`)); + await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`); } } @@ -981,14 +955,15 @@ class ConfigCollector { } // Add current value indicator for existing configs + const color = await prompts.getColor(); if (existingValue !== null && existingValue !== undefined) { if (typeof existingValue === 'boolean') { - message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`); + message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`); } else if (Array.isArray(existingValue)) { - message += chalk.dim(` (current: ${existingValue.join(', ')})`); + message += color.dim(` (current: ${existingValue.join(', ')})`); } else if (questionType !== 'list') { // Show the cleaned value (without {project-root}/) for display - message += chalk.dim(` (current: ${existingValue})`); + message += color.dim(` (current: ${existingValue})`); } } else if (item.example && questionType === 'input') { // Show example for input fields @@ -998,7 +973,7 @@ class ConfigCollector { exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig); exampleText = exampleText.replace('{project-root}/', ''); } - message += chalk.dim(` (e.g., ${exampleText})`); + message += color.dim(` (e.g., ${exampleText})`); } // Build the question object diff --git a/tools/cli/installers/lib/core/dependency-resolver.js b/tools/cli/installers/lib/core/dependency-resolver.js index ee8a8a124..3fb282c5d 100644 --- a/tools/cli/installers/lib/core/dependency-resolver.js +++ b/tools/cli/installers/lib/core/dependency-resolver.js @@ -1,8 +1,8 @@ const fs = require('fs-extra'); const path = require('node:path'); const glob = require('glob'); -const chalk = require('chalk'); const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); /** * Dependency Resolver for BMAD modules @@ -24,7 +24,7 @@ class DependencyResolver { */ async resolve(bmadDir, selectedModules = [], options = {}) { if (options.verbose) { - console.log(chalk.cyan('Resolving module dependencies...')); + await prompts.log.info('Resolving module dependencies...'); } // Always include core as base @@ -50,7 +50,7 @@ class DependencyResolver { // Report results (only in verbose mode) if (options.verbose) { - this.reportResults(organizedFiles, selectedModules); + await this.reportResults(organizedFiles, selectedModules); } return { @@ -90,8 +90,12 @@ class DependencyResolver { } } + if (!moduleDir) { + continue; + } + if (!(await fs.pathExists(moduleDir))) { - console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`)); + await prompts.log.warn('Module directory not found: ' + moduleDir); continue; } @@ -179,7 +183,7 @@ class DependencyResolver { } } } catch (error) { - console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`)); + await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message); } } @@ -658,8 +662,8 @@ class DependencyResolver { /** * Report resolution results */ - reportResults(organized, selectedModules) { - console.log(chalk.green('\n✓ Dependency resolution complete')); + async reportResults(organized, selectedModules) { + await prompts.log.success('Dependency resolution complete'); for (const [module, files] of Object.entries(organized)) { const isSelected = selectedModules.includes(module) || module === 'core'; @@ -667,31 +671,31 @@ class DependencyResolver { files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length; if (totalFiles > 0) { - console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`)); - console.log(chalk.dim(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`)); + await prompts.log.info(` ${module.toUpperCase()} module:`); + await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`); if (files.agents.length > 0) { - console.log(chalk.dim(` Agents: ${files.agents.length}`)); + await prompts.log.message(` Agents: ${files.agents.length}`); } if (files.tasks.length > 0) { - console.log(chalk.dim(` Tasks: ${files.tasks.length}`)); + await prompts.log.message(` Tasks: ${files.tasks.length}`); } if (files.templates.length > 0) { - console.log(chalk.dim(` Templates: ${files.templates.length}`)); + await prompts.log.message(` Templates: ${files.templates.length}`); } if (files.data.length > 0) { - console.log(chalk.dim(` Data files: ${files.data.length}`)); + await prompts.log.message(` Data files: ${files.data.length}`); } if (files.other.length > 0) { - console.log(chalk.dim(` Other files: ${files.other.length}`)); + await prompts.log.message(` Other files: ${files.other.length}`); } } } if (this.missingDependencies.size > 0) { - console.log(chalk.yellow('\n ⚠ Missing dependencies:')); + await prompts.log.warn('Missing dependencies:'); for (const missing of this.missingDependencies) { - console.log(chalk.yellow(` - ${missing}`)); + await prompts.log.warn(` - ${missing}`); } } } diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index cfba0ab94..1e161bdc8 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -1,7 +1,5 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); -const ora = require('ora'); const { Detector } = require('./detector'); const { Manifest } = require('./manifest'); const { ModuleManager } = require('../modules/manager'); @@ -166,32 +164,32 @@ class Installer { const newlySelectedIdes = toolConfig.ides.filter((ide) => !previouslyConfiguredIdes.includes(ide)); if (newlySelectedIdes.length > 0) { - console.log('\n'); // Add spacing before IDE questions - // Collect configuration for IDEs that support it for (const ide of newlySelectedIdes) { try { const handler = this.ideManager.handlers.get(ide); if (!handler) { - console.warn(chalk.yellow(`Warning: IDE '${ide}' handler not found`)); + await prompts.log.warn(`Warning: IDE '${ide}' handler not found`); continue; } // Check if this IDE handler has a collectConfiguration method // (custom installers like Codex, Kilo, Kiro-cli may have this) if (typeof handler.collectConfiguration === 'function') { - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); + await prompts.log.info(`Configuring ${ide}...`); ideConfigurations[ide] = await handler.collectConfiguration({ selectedModules: selectedModules || [], projectDir, bmadDir, }); + } else { + // Config-driven IDEs don't need configuration - mark as ready + ideConfigurations[ide] = { _noConfigNeeded: true }; } - // Most config-driven IDEs don't need configuration - silently skip } catch (error) { // IDE doesn't support configuration or has an error - console.warn(chalk.yellow(`Warning: Could not load configuration for ${ide}: ${error.message}`)); + await prompts.log.warn(`Warning: Could not load configuration for ${ide}: ${error.message}`); } } } @@ -199,7 +197,7 @@ class Installer { // Log which IDEs are already configured and being kept const keptIdes = toolConfig.ides.filter((ide) => previouslyConfiguredIdes.includes(ide)); if (keptIdes.length > 0) { - console.log(chalk.dim(`\nKeeping existing configuration for: ${keptIdes.join(', ')}`)); + await prompts.log.message(`Keeping existing configuration for: ${keptIdes.join(', ')}`); } } @@ -229,16 +227,17 @@ class Installer { // Only display logo if core config wasn't already collected (meaning we're not continuing from UI) if (!hasCoreConfig) { // Display BMAD logo - CLIUtils.displayLogo(); + await CLIUtils.displayLogo(); // Display welcome message - CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version); + await CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version); } // Note: Legacy V4 detection now happens earlier in UI.promptInstall() // before any config collection, so we don't need to check again here const projectDir = path.resolve(config.directory); + const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); // If core config was pre-collected (from interactive mode), use it if (config.coreConfig && Object.keys(config.coreConfig).length > 0) { @@ -372,41 +371,36 @@ class Installer { // Tool selection will be collected after we determine if it's a reinstall/update/new install - const spinner = ora('Preparing installation...').start(); + const spinner = await prompts.spinner(); + spinner.start('Preparing installation...'); try { - // Resolve target directory (path.resolve handles platform differences) - const projectDir = path.resolve(config.directory); - - // Always use the standard _bmad folder name - const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME); - // Create a project directory if it doesn't exist (user already confirmed) if (!(await fs.pathExists(projectDir))) { - spinner.text = 'Creating installation directory...'; + spinner.message('Creating installation directory...'); try { // fs.ensureDir handles platform-specific directory creation // It will recursively create all necessary parent directories await fs.ensureDir(projectDir); } catch (error) { - spinner.fail('Failed to create installation directory'); - console.error(chalk.red(`Error: ${error.message}`)); + spinner.error('Failed to create installation directory'); + await prompts.log.error(`Error: ${error.message}`); // More detailed error for common issues if (error.code === 'EACCES') { - console.error(chalk.red('Permission denied. Check parent directory permissions.')); + await prompts.log.error('Permission denied. Check parent directory permissions.'); } else if (error.code === 'ENOSPC') { - console.error(chalk.red('No space left on device.')); + await prompts.log.error('No space left on device.'); } throw new Error(`Cannot create directory: ${projectDir}`); } } // Check existing installation - spinner.text = 'Checking for existing installation...'; + spinner.message('Checking for existing installation...'); const existingInstall = await this.detector.detect(bmadDir); if (existingInstall.installed && !config.force && !config._quickUpdate) { - spinner.stop(); + spinner.stop('Existing installation detected'); // Check if user already decided what to do (from early menu in ui.js) let action = null; @@ -414,9 +408,9 @@ class Installer { action = 'update'; } else { // Fallback: Ask the user (backwards compatibility for other code paths) - console.log(chalk.yellow('\n⚠️ Existing BMAD installation detected')); - console.log(chalk.dim(` Location: ${bmadDir}`)); - console.log(chalk.dim(` Version: ${existingInstall.version}`)); + await prompts.log.warn('Existing BMAD installation detected'); + await prompts.log.message(` Location: ${bmadDir}`); + await prompts.log.message(` Version: ${existingInstall.version}`); const promptResult = await this.promptUpdateAction(); action = promptResult.action; @@ -438,17 +432,17 @@ class Installer { // If there are modules to remove, ask for confirmation if (modulesToRemove.length > 0) { const prompts = require('../../../lib/prompts'); - spinner.stop(); + if (spinner.isSpinning) { + spinner.stop('Reviewing module changes'); + } - console.log(''); - console.log(chalk.yellow.bold('⚠️ Modules to be removed:')); + await prompts.log.warn('Modules to be removed:'); for (const moduleId of modulesToRemove) { const moduleInfo = existingInstall.modules.find((m) => m.id === moduleId); const displayName = moduleInfo?.name || moduleId; const modulePath = path.join(bmadDir, moduleId); - console.log(chalk.red(` - ${displayName} (${modulePath})`)); + await prompts.log.error(` - ${displayName} (${modulePath})`); } - console.log(''); const confirmRemoval = await prompts.confirm({ message: `Remove ${modulesToRemove.length} module(s) from BMAD installation?`, @@ -462,15 +456,15 @@ class Installer { try { if (await fs.pathExists(modulePath)) { await fs.remove(modulePath); - console.log(chalk.dim(` ✓ Removed: ${moduleId}`)); + await prompts.log.message(` Removed: ${moduleId}`); } } catch (error) { - console.warn(chalk.yellow(` Warning: Failed to remove ${moduleId}: ${error.message}`)); + await prompts.log.warn(` Warning: Failed to remove ${moduleId}: ${error.message}`); } } - console.log(chalk.green(` ✓ Removed ${modulesToRemove.length} module(s)`)); + await prompts.log.success(` Removed ${modulesToRemove.length} module(s)`); } else { - console.log(chalk.dim(' → Module removal cancelled')); + await prompts.log.message(' Module removal cancelled'); // Add the modules back to the selection since user cancelled removal for (const moduleId of modulesToRemove) { if (!config.modules) config.modules = []; @@ -503,7 +497,7 @@ class Installer { // Also store in configCollector for use during config collection this.configCollector.collectedConfig.core = existingCoreConfig; } catch (error) { - console.warn(chalk.yellow(`Warning: Could not read existing core config: ${error.message}`)); + await prompts.log.warn(`Warning: Could not read existing core config: ${error.message}`); } } @@ -554,7 +548,7 @@ class Installer { await fs.ensureDir(path.dirname(backupPath)); await fs.copy(customFile, backupPath); } - spinner.succeed(`Backed up ${customFiles.length} custom files`); + spinner.stop(`Backed up ${customFiles.length} custom files`); config._tempBackupDir = tempBackupDir; } @@ -571,14 +565,14 @@ class Installer { await fs.ensureDir(path.dirname(tempBackupPath)); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); + spinner.stop(`Backed up ${modifiedFiles.length} modified files`); config._tempModifiedBackupDir = tempModifiedBackupDir; } } } else if (existingInstall.installed && config._quickUpdate) { // Quick update mode - automatically treat as update without prompting - spinner.text = 'Preparing quick update...'; + spinner.message('Preparing quick update...'); config._isUpdate = true; config._existingInstall = existingInstall; @@ -636,7 +630,7 @@ class Installer { await fs.ensureDir(path.dirname(backupPath)); await fs.copy(customFile, backupPath); } - spinner.succeed(`Backed up ${customFiles.length} custom files`); + spinner.stop(`Backed up ${customFiles.length} custom files`); config._tempBackupDir = tempBackupDir; } @@ -652,14 +646,14 @@ class Installer { await fs.ensureDir(path.dirname(tempBackupPath)); await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true }); } - spinner.succeed(`Backed up ${modifiedFiles.length} modified files`); + spinner.stop(`Backed up ${modifiedFiles.length} modified files`); config._tempModifiedBackupDir = tempModifiedBackupDir; } } // Now collect tool configurations after we know if it's a reinstall // Skip for quick update since we already have the IDE list - spinner.stop(); + spinner.stop('Pre-checks complete'); let toolSelection; if (config._quickUpdate) { // Quick update already has IDEs configured, use saved configurations @@ -698,19 +692,23 @@ class Installer { config.skipIde = toolSelection.skipIde; const ideConfigurations = toolSelection.configurations; + // Results collector for consolidated summary + const results = []; + const addResult = (step, status, detail = '') => results.push({ step, status, detail }); + if (spinner.isSpinning) { - spinner.text = 'Continuing installation...'; + spinner.message('Installing...'); } else { - spinner.start('Continuing installation...'); + spinner.start('Installing...'); } // Create bmad directory structure - spinner.text = 'Creating directory structure...'; + spinner.message('Creating directory structure...'); await this.createDirectoryStructure(bmadDir); // Cache custom modules if any if (customModulePaths && customModulePaths.size > 0) { - spinner.text = 'Caching custom modules...'; + spinner.message('Caching custom modules...'); const { CustomModuleCache } = require('./custom-module-cache'); const customCache = new CustomModuleCache(bmadDir); @@ -725,16 +723,16 @@ class Installer { // Update module manager with the cached paths this.moduleManager.setCustomModulePaths(customModulePaths); - spinner.succeed('Custom modules cached'); + addResult('Custom modules cached', 'ok'); } const projectRoot = getProjectRoot(); // Step 1: Install core module first (if requested) if (config.installCore) { - spinner.start('Installing BMAD core...'); + spinner.message('Installing BMAD core...'); await this.installCoreWithDependencies(bmadDir, { core: {} }); - spinner.succeed('Core installed'); + addResult('Core', 'ok', 'installed'); // Generate core config file await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); @@ -804,13 +802,13 @@ class Installer { bmadDir: bmadDir, // Pass bmadDir so we can check cache }); + spinner.message('Resolving dependencies...'); + const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, { verbose: config.verbose, moduleManager: tempModuleManager, }); - spinner.succeed('Dependencies resolved'); - // Install modules with their dependencies if (allModules && allModules.length > 0) { const installedModuleNames = new Set(); @@ -824,7 +822,7 @@ class Installer { // Show appropriate message based on whether this is a quick update const isQuickUpdate = config._quickUpdate || false; - spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); + spinner.message(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`); // Check if this is a custom module let isCustomModule = false; @@ -898,6 +896,7 @@ class Installer { moduleConfig: collectedModuleConfig, isQuickUpdate: config._quickUpdate || false, installer: this, + silent: true, }, ); @@ -915,7 +914,7 @@ class Installer { } } - spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`); + addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); } // Install partial modules (only dependencies) @@ -929,9 +928,8 @@ class Installer { files.data.length + files.other.length; if (totalFiles > 0) { - spinner.start(`Installing ${module} dependencies...`); + spinner.message(`Installing ${module} dependencies...`); await this.installPartialModule(module, bmadDir, files); - spinner.succeed(`${module} dependencies installed`); } } } @@ -940,9 +938,9 @@ class Installer { // All content is now installed as modules - no separate custom content handling needed // Generate clean config.yaml files for each installed module - spinner.start('Generating module configurations...'); + spinner.message('Generating module configurations...'); await this.generateModuleConfigs(bmadDir, moduleConfigs); - spinner.succeed('Module configurations generated'); + addResult('Configurations', 'ok', 'generated'); // Create agent configuration files // Note: Legacy createAgentConfigs removed - using YAML customize system instead @@ -957,7 +955,7 @@ class Installer { // Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup // This must happen BEFORE mergeModuleHelpCatalogs because it depends on agent-manifest.csv - spinner.start('Generating workflow and agent manifests...'); + spinner.message('Generating workflow and agent manifests...'); const manifestGen = new ManifestGenerator(); // For quick update, we need ALL installed modules in the manifest @@ -985,15 +983,17 @@ class Installer { // Custom modules are now included in the main modules list - no separate tracking needed - spinner.succeed( - `Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`, + addResult( + 'Manifests', + 'ok', + `${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools`, ); // Merge all module-help.csv files into bmad-help.csv // This must happen AFTER generateManifests because it depends on agent-manifest.csv - spinner.start('Generating workflow help catalog...'); + spinner.message('Generating workflow help catalog...'); await this.mergeModuleHelpCatalogs(bmadDir); - spinner.succeed('Workflow help catalog generated'); + addResult('Help catalog', 'ok'); // Configure IDEs and copy documentation if (!config.skipIde && config.ides && config.ides.length > 0) { @@ -1004,64 +1004,63 @@ class Installer { const validIdes = config.ides.filter((ide) => ide && typeof ide === 'string'); if (validIdes.length === 0) { - console.log(chalk.yellow('⚠️ No valid IDEs selected. Skipping IDE configuration.')); + addResult('IDE configuration', 'warn', 'no valid IDEs selected'); } else { // Check if any IDE might need prompting (no pre-collected config) const needsPrompting = validIdes.some((ide) => !ideConfigurations[ide]); - if (!needsPrompting) { - spinner.start('Configuring IDEs...'); - } - // Temporarily suppress console output if not verbose const originalLog = console.log; if (!config.verbose) { console.log = () => {}; } - for (const ide of validIdes) { - // Only show spinner if we have pre-collected config (no prompts expected) - if (ideConfigurations[ide] && !needsPrompting) { - spinner.text = `Configuring ${ide}...`; - } else if (!ideConfigurations[ide]) { - // Stop spinner before prompting - if (spinner.isSpinning) { - spinner.stop(); + try { + for (const ide of validIdes) { + if (!needsPrompting || ideConfigurations[ide]) { + // All IDEs pre-configured, or this specific IDE has config: keep spinner running + spinner.message(`Configuring ${ide}...`); + } else { + // This IDE needs prompting: stop spinner to allow user interaction + if (spinner.isSpinning) { + spinner.stop('Ready for IDE configuration'); + } + } + + // Silent when this IDE has pre-collected config (no prompts for THIS IDE) + const ideHasConfig = Boolean(ideConfigurations[ide]); + const setupResult = await this.ideManager.setup(ide, projectDir, bmadDir, { + selectedModules: allModules || [], + preCollectedConfig: ideConfigurations[ide] || null, + verbose: config.verbose, + silent: ideHasConfig, + }); + + // Save IDE configuration for future updates + if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { + await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); + } + + // Collect result for summary + if (setupResult.success) { + addResult(ide, 'ok', setupResult.detail || ''); + } else { + addResult(ide, 'error', setupResult.error || 'failed'); + } + + // Restart spinner if we stopped it for prompting + if (needsPrompting && !spinner.isSpinning) { + spinner.start('Configuring IDEs...'); } - console.log(chalk.cyan(`\nConfiguring ${ide}...`)); } - - // Pass pre-collected configuration to avoid re-prompting - await this.ideManager.setup(ide, projectDir, bmadDir, { - selectedModules: allModules || [], - preCollectedConfig: ideConfigurations[ide] || null, - verbose: config.verbose, - }); - - // Save IDE configuration for future updates - if (ideConfigurations[ide] && !ideConfigurations[ide]._alreadyConfigured) { - await this.ideConfigManager.saveIdeConfig(bmadDir, ide, ideConfigurations[ide]); - } - - // Restart spinner if we stopped it - if (!ideConfigurations[ide] && !spinner.isSpinning) { - spinner.start('Configuring IDEs...'); - } - } - - // Restore console.log - console.log = originalLog; - - if (spinner.isSpinning) { - spinner.succeed(`Configured: ${validIdes.join(', ')}`); - } else { - console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`)); + } finally { + console.log = originalLog; } } } // Run module-specific installers after IDE setup - spinner.start('Running module-specific installers...'); + spinner.message('Running module-specific installers...'); // Create a conditional logger based on verbose mode const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; @@ -1073,20 +1072,21 @@ class Installer { // Run core module installer if core was installed if (config.installCore || resolution.byModule.core) { - spinner.text = 'Running core module installer...'; + spinner.message('Running core module installer...'); await this.moduleManager.runModuleInstaller('core', bmadDir, { installedIDEs: config.ides || [], moduleConfig: moduleConfigs.core || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } // Run installers for user-selected modules if (config.modules && config.modules.length > 0) { for (const moduleName of config.modules) { - spinner.text = `Running ${moduleName} module installer...`; + spinner.message(`Running ${moduleName} module installer...`); // Pass installed IDEs and module config to module installer await this.moduleManager.runModuleInstaller(moduleName, bmadDir, { @@ -1094,11 +1094,12 @@ class Installer { moduleConfig: moduleConfigs[moduleName] || {}, coreConfig: moduleConfigs.core || {}, logger: moduleLogger, + silent: true, }); } } - spinner.succeed('Module-specific installers completed'); + addResult('Module installers', 'ok'); // Note: Manifest files are already created by ManifestGenerator above // No need to create legacy manifest.csv anymore @@ -1108,7 +1109,7 @@ class Installer { let modifiedFiles = []; if (config._isUpdate) { if (config._customFiles && config._customFiles.length > 0) { - spinner.start(`Restoring ${config._customFiles.length} custom files...`); + spinner.message(`Restoring ${config._customFiles.length} custom files...`); for (const originalPath of config._customFiles) { const relativePath = path.relative(bmadDir, originalPath); @@ -1125,7 +1126,6 @@ class Installer { await fs.remove(config._tempBackupDir); } - spinner.succeed(`Restored ${config._customFiles.length} custom files`); customFiles = config._customFiles; } @@ -1134,7 +1134,7 @@ class Installer { // Restore modified files as .bak files if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) { - spinner.start(`Restoring ${modifiedFiles.length} modified files as .bak...`); + spinner.message(`Restoring ${modifiedFiles.length} modified files as .bak...`); for (const modifiedFile of modifiedFiles) { const relativePath = path.relative(bmadDir, modifiedFile.path); @@ -1149,37 +1149,20 @@ class Installer { // Clean up temp backup await fs.remove(config._tempModifiedBackupDir); - - spinner.succeed(`Restored ${modifiedFiles.length} modified files as .bak`); } } } - spinner.stop(); + // Stop the single installation spinner + spinner.stop('Installation complete'); - // Report custom and modified files if any were found - if (customFiles.length > 0) { - console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`)); - } - - if (modifiedFiles.length > 0) { - console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`)); - console.log( - chalk.dim( - '\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.', - ), - ); - console.log(chalk.dim('Remove these .bak files it no longer needed\n')); - } - - // Display completion message - const { UI } = require('../../../lib/ui'); - const ui = new UI(); - ui.showInstallSummary({ - path: bmadDir, + // Render consolidated summary + await this.renderInstallSummary(results, { + bmadDir, modules: config.modules, ides: config.ides, customFiles: customFiles.length > 0 ? customFiles : undefined, + modifiedFiles: modifiedFiles.length > 0 ? modifiedFiles : undefined, }); return { @@ -1190,16 +1173,63 @@ class Installer { projectDir: projectDir, }; } catch (error) { - spinner.fail('Installation failed'); + spinner.error('Installation failed'); throw error; } } + /** + * Render a consolidated install summary using prompts.note() + * @param {Array} results - Array of {step, status: 'ok'|'error'|'warn', detail} + * @param {Object} context - {bmadDir, modules, ides, customFiles, modifiedFiles} + */ + async renderInstallSummary(results, context = {}) { + const color = await prompts.getColor(); + + // Build step lines with status indicators + const lines = []; + for (const r of results) { + let icon; + if (r.status === 'ok') { + icon = color.green('\u2713'); + } else if (r.status === 'warn') { + icon = color.yellow('!'); + } else { + icon = color.red('\u2717'); + } + const detail = r.detail ? color.dim(` (${r.detail})`) : ''; + lines.push(` ${icon} ${r.step}${detail}`); + } + + // Add context info + lines.push(''); + if (context.bmadDir) { + lines.push(` Installed to: ${color.dim(context.bmadDir)}`); + } + if (context.modules && context.modules.length > 0) { + lines.push(` Modules: ${color.dim(context.modules.join(', '))}`); + } + if (context.ides && context.ides.length > 0) { + lines.push(` Tools: ${color.dim(context.ides.join(', '))}`); + } + + // Custom/modified file warnings + if (context.customFiles && context.customFiles.length > 0) { + lines.push(` ${color.cyan(`Custom files preserved: ${context.customFiles.length}`)}`); + } + if (context.modifiedFiles && context.modifiedFiles.length > 0) { + lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`); + } + + await prompts.note(lines.join('\n'), 'BMAD is ready to use!'); + } + /** * Update existing installation */ async update(config) { - const spinner = ora('Checking installation...').start(); + const spinner = await prompts.spinner(); + spinner.start('Checking installation...'); try { const projectDir = path.resolve(config.directory); @@ -1207,11 +1237,11 @@ class Installer { const existingInstall = await this.detector.detect(bmadDir); if (!existingInstall.installed) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`No BMAD installation found at ${bmadDir}`); } - spinner.text = 'Analyzing update requirements...'; + spinner.message('Analyzing update requirements...'); // Compare versions and determine what needs updating const currentVersion = existingInstall.version; @@ -1265,8 +1295,8 @@ class Installer { } if (customModuleSources.size > 0) { - spinner.stop(); - console.log(chalk.yellow('\nChecking custom module sources before update...')); + spinner.stop('Update analysis complete'); + await prompts.log.warn('Checking custom module sources before update...'); const projectRoot = getProjectRoot(); await this.handleMissingCustomSources( @@ -1281,43 +1311,43 @@ class Installer { } if (config.dryRun) { - spinner.stop(); - console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n')); - console.log(chalk.bold('Current version:'), currentVersion); - console.log(chalk.bold('New version:'), newVersion); - console.log(chalk.bold('Core:'), existingInstall.hasCore ? 'Will be updated' : 'Not installed'); + spinner.stop('Dry run analysis complete'); + let dryRunContent = `Current version: ${currentVersion}\n`; + dryRunContent += `New version: ${newVersion}\n`; + dryRunContent += `Core: ${existingInstall.hasCore ? 'Will be updated' : 'Not installed'}`; if (existingInstall.modules.length > 0) { - console.log(chalk.bold('\nModules to update:')); + dryRunContent += '\n\nModules to update:'; for (const mod of existingInstall.modules) { - console.log(` - ${mod.id}`); + dryRunContent += `\n - ${mod.id}`; } } + await prompts.note(dryRunContent, 'Update Preview (Dry Run)'); return; } // Perform actual update if (existingInstall.hasCore) { - spinner.text = 'Updating core...'; + spinner.message('Updating core...'); await this.updateCore(bmadDir, config.force); } for (const module of existingInstall.modules) { - spinner.text = `Updating module: ${module.id}...`; - await this.moduleManager.update(module.id, bmadDir, config.force); + spinner.message(`Updating module: ${module.id}...`); + await this.moduleManager.update(module.id, bmadDir, config.force, { installer: this }); } // Update manifest - spinner.text = 'Updating manifest...'; + spinner.message('Updating manifest...'); await this.manifest.update(bmadDir, { version: newVersion, updateDate: new Date().toISOString(), }); - spinner.succeed('Update complete'); + spinner.stop('Update complete'); return { success: true }; } catch (error) { - spinner.fail('Update failed'); + spinner.error('Update failed'); throw error; } } @@ -1492,10 +1522,10 @@ class Installer { } if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Merged module-help from: ${moduleName}`)); + await prompts.log.message(` Merged module-help from: ${moduleName}`); } } catch (error) { - console.warn(chalk.yellow(` Warning: Failed to read module-help.csv from ${moduleName}:`, error.message)); + await prompts.log.warn(` Warning: Failed to read module-help.csv from ${moduleName}: ${error.message}`); } } } @@ -1537,7 +1567,7 @@ class Installer { this.installedFiles.add(outputPath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Generated bmad-help.csv: ${allRows.length} workflows`)); + await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`); } } @@ -1728,6 +1758,7 @@ class Installer { skipModuleInstaller: true, // We'll run it later after IDE setup moduleConfig: moduleConfig, // Pass module config for conditional filtering installer: this, + silent: true, }, ); @@ -1907,7 +1938,7 @@ class Installer { // Check for localskip="true" in the agent tag const agentMatch = content.match(/]*\slocalskip="true"[^>]*>/); if (agentMatch) { - console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`)); + await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`); continue; // Skip this agent } } @@ -1994,7 +2025,7 @@ class Installer { if (await fs.pathExists(genericTemplatePath)) { await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); + await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`); } } } @@ -2029,8 +2060,8 @@ class Installer { * @returns {Object} Update result */ async quickUpdate(config) { - const ora = require('ora'); - const spinner = ora('Starting quick update...').start(); + const spinner = await prompts.spinner(); + spinner.start('Starting quick update...'); try { const projectDir = path.resolve(config.directory); @@ -2038,11 +2069,11 @@ class Installer { // Check if bmad directory exists if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); } - spinner.text = 'Detecting installed modules and configuration...'; + spinner.message('Detecting installed modules and configuration...'); // Detect existing installation const existingInstall = await this.detector.detect(bmadDir); @@ -2169,14 +2200,14 @@ class Installer { } } - spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`); + spinner.stop(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`); if (skippedModules.length > 0) { - console.log(chalk.yellow(`⚠️ Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`)); + await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`); } // Load existing configs and collect new fields (if any) - console.log(chalk.cyan('\n📋 Checking for new configuration options...')); + await prompts.log.info('Checking for new configuration options...'); await this.configCollector.loadExistingConfig(projectDir); let promptedForNewFields = false; @@ -2196,7 +2227,7 @@ class Installer { } if (!promptedForNewFields) { - console.log(chalk.green('✓ All configuration is up to date, no new options to configure')); + await prompts.log.success('All configuration is up to date, no new options to configure'); } // Add metadata @@ -2228,7 +2259,7 @@ class Installer { // Only succeed the spinner if it's still spinning // (install method might have stopped it if folder name changed) if (spinner.isSpinning) { - spinner.succeed('Quick update complete!'); + spinner.stop('Quick update complete!'); } return { @@ -2240,7 +2271,7 @@ class Installer { ides: configuredIdes, }; } catch (error) { - spinner.fail('Quick update failed'); + spinner.error('Quick update failed'); throw error; } } @@ -2251,12 +2282,12 @@ class Installer { * @returns {Object} Compilation result */ async compileAgents(config) { - const ora = require('ora'); - const chalk = require('chalk'); + // Using @clack prompts const { ModuleManager } = require('../modules/manager'); const { getSourcePath } = require('../../../lib/project-root'); - const spinner = ora('Recompiling agents with customizations...').start(); + const spinner = await prompts.spinner(); + spinner.start('Recompiling agents with customizations...'); try { const projectDir = path.resolve(config.directory); @@ -2264,7 +2295,7 @@ class Installer { // Check if bmad directory exists if (!(await fs.pathExists(bmadDir))) { - spinner.fail('No BMAD installation found'); + spinner.stop('No BMAD installation found'); throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`); } @@ -2306,7 +2337,7 @@ class Installer { // Process each installed module for (const moduleId of installedModules) { - spinner.text = `Recompiling agents in ${moduleId}...`; + spinner.message(`Recompiling agents in ${moduleId}...`); // Get source path let sourcePath; @@ -2322,7 +2353,7 @@ class Installer { } if (!sourcePath) { - console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`)); + await prompts.log.warn(`Source not found for module ${moduleId}, skipping...`); continue; } @@ -2340,7 +2371,7 @@ class Installer { } } - spinner.succeed('Agent recompilation complete!'); + spinner.stop('Agent recompilation complete!'); return { success: true, @@ -2348,7 +2379,7 @@ class Installer { modules: installedModules, }; } catch (error) { - spinner.fail('Agent recompilation failed'); + spinner.error('Agent recompilation failed'); throw error; } } @@ -2370,19 +2401,14 @@ class Installer { * @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version) */ async handleLegacyV4Migration(_projectDir, _legacyV4) { - console.log(''); - console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected')); - console.log(chalk.yellow('─'.repeat(80))); - console.log(chalk.yellow('Found .bmad-method folder from BMAD v4 installation.')); - console.log(''); - - console.log(chalk.dim('Before continuing with installation, we recommend:')); - console.log(chalk.dim(' 1. Remove the .bmad-method folder, OR')); - console.log(chalk.dim(' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)')); - console.log(''); - - console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.')); - console.log(''); + await prompts.note( + 'Found .bmad-method folder from BMAD v4 installation.\n\n' + + 'Before continuing with installation, we recommend:\n' + + ' 1. Remove the .bmad-method folder, OR\n' + + ' 2. Back it up by renaming it to another name (e.g., bmad-method-backup)\n\n' + + 'If your v4 installation set up rules or commands, you should remove those as well.', + 'Legacy BMAD v4 detected', + ); const proceed = await prompts.select({ message: 'What would you like to do?', @@ -2402,16 +2428,11 @@ class Installer { }); if (proceed === 'exit') { - console.log(''); - console.log(chalk.cyan('Please remove the .bmad-method folder and any v4 rules/commands,')); - console.log(chalk.cyan('then run the installer again.')); - console.log(''); + await prompts.log.info('Please remove the .bmad-method folder and any v4 rules/commands, then run the installer again.'); process.exit(0); } - console.log(''); - console.log(chalk.yellow('⚠️ Proceeding with installation despite legacy v4 folder')); - console.log(''); + await prompts.log.warn('Proceeding with installation despite legacy v4 folder'); } /** @@ -2465,7 +2486,7 @@ class Installer { return files; } catch (error) { - console.warn('Warning: Could not read files-manifest.csv:', error.message); + await prompts.log.warn('Could not read files-manifest.csv: ' + error.message); return []; } } @@ -2637,22 +2658,16 @@ class Installer { }; } - // Stop any spinner for interactive prompts - const currentSpinner = ora(); - if (currentSpinner.isSpinning) { - currentSpinner.stop(); - } - - console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`)); + await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`); let keptCount = 0; let updatedCount = 0; let removedCount = 0; for (const missing of customModulesWithMissingSources) { - console.log(chalk.dim(` • ${missing.name} (${missing.id})`)); - console.log(chalk.dim(` Original source: ${missing.relativePath}`)); - console.log(chalk.dim(` Full path: ${missing.sourcePath}`)); + await prompts.log.message( + `${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`, + ); const choices = [ { @@ -2722,26 +2737,27 @@ class Installer { }); updatedCount++; - console.log(chalk.green(`✓ Updated source location`)); + await prompts.log.success('Updated source location'); break; } case 'remove': { // Extra confirmation for destructive remove - console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`)); - console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`)); + await prompts.log.error( + `WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`, + ); const confirmDelete = await prompts.confirm({ - message: chalk.red.bold('Are you absolutely sure you want to delete this module?'), + message: 'Are you absolutely sure you want to delete this module?', default: false, }); if (confirmDelete) { const typedConfirm = await prompts.text({ - message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'), + message: 'Type "DELETE" to confirm permanent deletion:', validate: (input) => { if (input !== 'DELETE') { - return chalk.red('You must type "DELETE" exactly to proceed'); + return 'You must type "DELETE" exactly to proceed'; } return; // clack expects undefined for valid input }, @@ -2753,12 +2769,12 @@ class Installer { if (await fs.pathExists(modulePath)) { const fsExtra = require('fs-extra'); await fsExtra.remove(modulePath); - console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`)); + await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`); } await this.manifest.removeModule(bmadDir, missing.id); await this.manifest.removeCustomModule(bmadDir, missing.id); - console.log(chalk.yellow(` ✓ Removed from manifest`)); + await prompts.log.warn('Removed from manifest'); // Also remove from installedModules list if (installedModules && installedModules.includes(missing.id)) { @@ -2769,13 +2785,13 @@ class Installer { } removedCount++; - console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`)); + await prompts.log.error(`"${missing.name}" has been permanently removed`); } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); + await prompts.log.message('Removal cancelled - module will be kept'); keptCount++; } } else { - console.log(chalk.dim(' Removal cancelled - module will be kept')); + await prompts.log.message('Removal cancelled - module will be kept'); keptCount++; } @@ -2784,7 +2800,7 @@ class Installer { case 'keep': { keptCount++; keptModulesWithoutSources.push(missing.id); - console.log(chalk.dim(` Module will be kept as-is`)); + await prompts.log.message('Module will be kept as-is'); break; } @@ -2794,10 +2810,11 @@ class Installer { // Show summary if (keptCount > 0 || updatedCount > 0 || removedCount > 0) { - console.log(chalk.dim(`\nSummary for custom modules with missing sources:`)); - if (keptCount > 0) console.log(chalk.dim(` • ${keptCount} module(s) kept as-is`)); - if (updatedCount > 0) console.log(chalk.dim(` • ${updatedCount} module(s) updated with new sources`)); - if (removedCount > 0) console.log(chalk.red(` • ${removedCount} module(s) permanently deleted`)); + let summary = 'Summary for custom modules with missing sources:'; + if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`; + if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`; + if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`; + await prompts.log.message(summary); } return { diff --git a/tools/cli/installers/lib/custom/handler.js b/tools/cli/installers/lib/custom/handler.js index 8c730cf32..6256e3cd2 100644 --- a/tools/cli/installers/lib/custom/handler.js +++ b/tools/cli/installers/lib/custom/handler.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); const { FileOps } = require('../../../lib/file-ops'); const { XmlHandler } = require('../../../lib/xml-handler'); @@ -88,7 +88,7 @@ class CustomHandler { try { config = yaml.parse(configContent); } catch (parseError) { - console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message)); + await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message); return null; } @@ -111,7 +111,7 @@ class CustomHandler { isInstallConfig: isInstallConfig, // Track which type this is }; } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message)); + await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message); return null; } } @@ -268,14 +268,13 @@ class CustomHandler { } results.filesCopied++; + if (entry.name.endsWith('.md')) { + results.workflowsInstalled++; + } if (fileTrackingCallback) { fileTrackingCallback(targetPath); } } - - if (entry.name.endsWith('.md')) { - results.workflowsInstalled++; - } } catch (error) { results.errors.push(`Failed to copy ${entry.name}: ${error.message}`); } @@ -322,7 +321,7 @@ class CustomHandler { await fs.writeFile(customizePath, templateContent, 'utf8'); // Only show customize creation in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`)); + await prompts.log.message(' Created customize: custom-' + agentName + '.customize.yaml'); } } } @@ -346,14 +345,10 @@ class CustomHandler { // Only show compilation details in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log( - chalk.dim( - ` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, - ), - ); + await prompts.log.message(' Compiled agent: ' + agentName + ' -> ' + path.relative(targetAgentsPath, targetMdPath)); } } catch (error) { - console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message)); + await prompts.log.warn(' Failed to compile agent ' + agentName + ': ' + error.message); results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`); } } diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index dce8aee9f..9bfbdcf30 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { XmlHandler } = require('../../../lib/xml-handler'); +const prompts = require('../../../lib/prompts'); const { getSourcePath } = require('../../../lib/project-root'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); @@ -53,7 +53,7 @@ class BaseIdeSetup { * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Default implementation - can be overridden if (this.configDir) { const configPath = path.join(projectDir, this.configDir); @@ -61,7 +61,7 @@ class BaseIdeSetup { const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); if (await fs.pathExists(bmadRulesPath)) { await fs.remove(bmadRulesPath); - console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); + if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`); } } } diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 486889267..7eb2533ed 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); +const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); @@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Setup result */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Clean up any old BMAD installation first - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); if (!this.installerConfig) { return { success: false, reason: 'no-config' }; @@ -102,7 +102,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.tools = taskToolResult.tools || 0; } - this.printSummary(results, target_dir); + await this.printSummary(results, target_dir, options); return { success: true, results }; } @@ -439,32 +439,28 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {Object} results - Installation results * @param {string} targetDir - Target directory (relative) */ - printSummary(results, targetDir) { - console.log(chalk.green(`\n✓ ${this.name} configured:`)); - if (results.agents > 0) { - console.log(chalk.dim(` - ${results.agents} agents installed`)); - } - if (results.workflows > 0) { - console.log(chalk.dim(` - ${results.workflows} workflow commands generated`)); - } - if (results.tasks > 0 || results.tools > 0) { - console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`)); - } - console.log(chalk.dim(` - Destination: ${targetDir}`)); + async printSummary(results, targetDir, options = {}) { + if (options.silent) return; + const parts = []; + if (results.agents > 0) parts.push(`${results.agents} agents`); + if (results.workflows > 0) parts.push(`${results.workflows} workflows`); + if (results.tasks > 0) parts.push(`${results.tasks} tasks`); + if (results.tools > 0) parts.push(`${results.tools} tools`); + await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`); } /** * Cleanup IDE configuration * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { // Clean all target directories if (this.installerConfig?.targets) { for (const target of this.installerConfig.targets) { - await this.cleanupTarget(projectDir, target.target_dir); + await this.cleanupTarget(projectDir, target.target_dir, options); } } else if (this.installerConfig?.target_dir) { - await this.cleanupTarget(projectDir, this.installerConfig.target_dir); + await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options); } } @@ -473,7 +469,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {string} projectDir - Project directory * @param {string} targetDir - Target directory to clean */ - async cleanupTarget(projectDir, targetDir) { + async cleanupTarget(projectDir, targetDir, options = {}) { const targetPath = path.join(projectDir, targetDir); if (!(await fs.pathExists(targetPath))) { @@ -496,25 +492,22 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} let removedCount = 0; for (const entry of entries) { - // Skip non-strings or undefined entries if (!entry || typeof entry !== 'string') { continue; } if (entry.startsWith('bmad')) { const entryPath = path.join(targetPath, entry); - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - removedCount++; - } else if (stat.isDirectory()) { + try { await fs.remove(entryPath); removedCount++; + } catch { + // Skip entries that can't be removed (broken symlinks, permission errors) } } } - if (removedCount > 0) { - console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`)); + if (removedCount > 0 && !options.silent) { + await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`); } } } diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 29f595f6c..8e91e003b 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -1,7 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const os = require('node:os'); -const chalk = require('chalk'); const { BaseIdeSetup } = require('./_base-ide'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); @@ -43,12 +42,11 @@ class CodexSetup extends BaseIdeSetup { default: 'global', }); - // Display detailed instructions for the chosen option - console.log(''); + // Show brief confirmation hint (detailed instructions available via verbose) if (installLocation === 'project') { - console.log(this.getProjectSpecificInstructions()); + await prompts.log.info('Prompts installed to: /.codex/prompts (requires CODEX_HOME)'); } else { - console.log(this.getGlobalInstructions()); + await prompts.log.info('Prompts installed to: ~/.codex/prompts'); } // Confirm the choice @@ -58,7 +56,7 @@ class CodexSetup extends BaseIdeSetup { }); if (!confirmed) { - console.log(chalk.yellow("\n Let's choose a different installation option.\n")); + await prompts.log.warn("Let's choose a different installation option."); } } @@ -72,7 +70,7 @@ class CodexSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Always use CLI mode const mode = 'cli'; @@ -84,7 +82,7 @@ class CodexSetup extends BaseIdeSetup { const destDir = this.getCodexPromptDir(projectDir, installLocation); await fs.ensureDir(destDir); - await this.clearOldBmadFiles(destDir); + await this.clearOldBmadFiles(destDir, options); // Collect artifacts and write using underscore format const agentGen = new AgentCommandGenerator(this.bmadFolderName); @@ -124,16 +122,11 @@ class CodexSetup extends BaseIdeSetup { const written = agentCount + workflowCount + tasksWritten; - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - Mode: CLI`)); - console.log(chalk.dim(` - ${counts.agents} agents exported`)); - console.log(chalk.dim(` - ${counts.tasks} tasks exported`)); - console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`)); - if (counts.workflowLaunchers > 0) { - console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`)); + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`, + ); } - console.log(chalk.dim(` - ${written} Codex prompt files written`)); - console.log(chalk.dim(` - Destination: ${destDir}`)); return { success: true, @@ -262,7 +255,7 @@ class CodexSetup extends BaseIdeSetup { return written; } - async clearOldBmadFiles(destDir) { + async clearOldBmadFiles(destDir, options = {}) { if (!(await fs.pathExists(destDir))) { return; } @@ -272,7 +265,7 @@ class CodexSetup extends BaseIdeSetup { entries = await fs.readdir(destDir); } catch (error) { // Directory exists but can't be read - skip cleanup - console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`)); + if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); return; } @@ -291,15 +284,11 @@ class CodexSetup extends BaseIdeSetup { const entryPath = path.join(destDir, entry); try { - const stat = await fs.stat(entryPath); - if (stat.isFile()) { - await fs.remove(entryPath); - } else if (stat.isDirectory()) { - await fs.remove(entryPath); - } + await fs.remove(entryPath); } catch (error) { - // Skip files that can't be processed - console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`)); + if (!options.silent) { + await prompts.log.message(` Skipping ${entry}: ${error.message}`); + } } } } @@ -315,22 +304,16 @@ class CodexSetup extends BaseIdeSetup { */ getGlobalInstructions(destDir) { const lines = [ + 'IMPORTANT: Codex Configuration', '', - chalk.bold.cyan('═'.repeat(70)), - chalk.bold.yellow(' IMPORTANT: Codex Configuration'), - chalk.bold.cyan('═'.repeat(70)), + '/prompts installed globally to your HOME DIRECTORY.', '', - chalk.white(' /prompts installed globally to your HOME DIRECTORY.'), - '', - chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'), - chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"), - '', - chalk.green(' ✓ You can now use /commands in Codex CLI'), - chalk.dim(' Example: /bmad_bmm_pm'), - chalk.dim(' Type / to see all available commands'), - '', - chalk.bold.cyan('═'.repeat(70)), + 'These prompts reference a specific _bmad path.', + "To use with other projects, you'd need to copy the _bmad dir.", '', + 'You can now use /commands in Codex CLI', + ' Example: /bmad_bmm_pm', + ' Type / to see all available commands', ]; return lines.join('\n'); } @@ -345,43 +328,34 @@ class CodexSetup extends BaseIdeSetup { const isWindows = os.platform() === 'win32'; const commonLines = [ + 'Project-Specific Codex Configuration', '', - chalk.bold.cyan('═'.repeat(70)), - chalk.bold.yellow(' Project-Specific Codex Configuration'), - chalk.bold.cyan('═'.repeat(70)), + `Prompts will be installed to: ${destDir || '/.codex/prompts'}`, '', - chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '/.codex/prompts'), - '', - chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'), + 'REQUIRED: You must set CODEX_HOME to use these prompts', '', ]; const windowsLines = [ - chalk.bold(' Create a codex.cmd file in your project root:'), + 'Create a codex.cmd file in your project root:', '', - chalk.green(' @echo off'), - chalk.green(' set CODEX_HOME=%~dp0.codex'), - chalk.green(' codex %*'), + ' @echo off', + ' set CODEX_HOME=%~dp0.codex', + ' codex %*', '', - chalk.dim(String.raw` Then run: .\codex instead of codex`), - chalk.dim(' (The %~dp0 gets the directory of the .cmd file)'), + String.raw`Then run: .\codex instead of codex`, + '(The %~dp0 gets the directory of the .cmd file)', ]; const unixLines = [ - chalk.bold(' Add this alias to your ~/.bashrc or ~/.zshrc:'), + 'Add this alias to your ~/.bashrc or ~/.zshrc:', '', - chalk.green(' alias codex=\'CODEX_HOME="$PWD/.codex" codex\''), - '', - chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'), - chalk.dim(' (The $PWD uses your current working directory)'), - ]; - const closingLines = [ - '', - chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'), - '', - chalk.bold.cyan('═'.repeat(70)), + ' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'', '', + 'After adding, run: source ~/.bashrc (or source ~/.zshrc)', + '(The $PWD uses your current working directory)', ]; + const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex']; const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines]; diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js index 52fd17c90..2e5734391 100644 --- a/tools/cli/installers/lib/ide/kilo.js +++ b/tools/cli/installers/lib/ide/kilo.js @@ -1,7 +1,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); const yaml = require('yaml'); +const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); @@ -23,10 +23,10 @@ class KiloSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); // Clean up any old BMAD installation first - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); // Load existing config (may contain non-BMAD modes and other settings) const kiloModesPath = path.join(projectDir, this.configFile); @@ -38,7 +38,7 @@ class KiloSetup extends BaseIdeSetup { config = yaml.parse(existingContent) || {}; } catch { // If parsing fails, start fresh but warn user - console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh')); + await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh'); config = {}; } } @@ -88,14 +88,11 @@ class KiloSetup extends BaseIdeSetup { const taskCount = taskToolCounts.tasks || 0; const toolCount = taskToolCounts.tools || 0; - console.log(chalk.green(`✓ ${this.name} configured:`)); - console.log(chalk.dim(` - ${addedCount} modes added`)); - console.log(chalk.dim(` - ${workflowCount} workflows exported`)); - console.log(chalk.dim(` - ${taskCount} tasks exported`)); - console.log(chalk.dim(` - ${toolCount} tools exported`)); - console.log(chalk.dim(` - Configuration file: ${this.configFile}`)); - console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`)); - console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode')); + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, + ); + } return { success: true, @@ -174,7 +171,7 @@ class KiloSetup extends BaseIdeSetup { /** * Cleanup KiloCode configuration */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const fs = require('fs-extra'); const kiloModesPath = path.join(projectDir, this.configFile); @@ -192,12 +189,12 @@ class KiloSetup extends BaseIdeSetup { if (removedCount > 0) { await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); - console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`)); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); } } } catch { // If parsing fails, leave file as-is - console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup')); + if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); } } diff --git a/tools/cli/installers/lib/ide/kiro-cli.js b/tools/cli/installers/lib/ide/kiro-cli.js index 612ea5fa4..150dca189 100644 --- a/tools/cli/installers/lib/ide/kiro-cli.js +++ b/tools/cli/installers/lib/ide/kiro-cli.js @@ -1,7 +1,7 @@ const path = require('node:path'); const { BaseIdeSetup } = require('./_base-ide'); -const chalk = require('chalk'); const fs = require('fs-extra'); +const prompts = require('../../../lib/prompts'); const yaml = require('yaml'); /** @@ -18,7 +18,7 @@ class KiroCliSetup extends BaseIdeSetup { * Cleanup old BMAD installation before reinstalling * @param {string} projectDir - Project directory */ - async cleanup(projectDir) { + async cleanup(projectDir, options = {}) { const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir); if (await fs.pathExists(bmadAgentsDir)) { @@ -29,7 +29,7 @@ class KiroCliSetup extends BaseIdeSetup { await fs.remove(path.join(bmadAgentsDir, file)); } } - console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`)); + if (!options.silent) await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`); } } @@ -40,9 +40,9 @@ class KiroCliSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { - console.log(chalk.cyan(`Setting up ${this.name}...`)); + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - await this.cleanup(projectDir); + await this.cleanup(projectDir, options); const kiroDir = path.join(projectDir, this.configDir); const agentsDir = path.join(kiroDir, this.agentsDir); @@ -52,7 +52,7 @@ class KiroCliSetup extends BaseIdeSetup { // Create BMad agents from source YAML files await this.createBmadAgentsFromSource(agentsDir, projectDir); - console.log(chalk.green(`✓ ${this.name} configured with BMad agents`)); + if (!options.silent) await prompts.log.success(`${this.name} configured with BMad agents`); } /** @@ -70,7 +70,7 @@ class KiroCliSetup extends BaseIdeSetup { try { await this.processAgentFile(agentFile, agentsDir, projectDir); } catch (error) { - console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`)); + await prompts.log.warn(`Failed to process ${agentFile}: ${error.message}`); } } } diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 7d00588c0..ad3352502 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,7 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); -const chalk = require('chalk'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); +const prompts = require('../../../lib/prompts'); /** * IDE Manager - handles IDE-specific setup @@ -49,7 +49,7 @@ class IdeManager { */ async loadHandlers() { // Load custom installer files - this.loadCustomInstallerFiles(); + await this.loadCustomInstallerFiles(); // Load config-driven handlers from platform-codes.yaml await this.loadConfigDrivenHandlers(); @@ -59,7 +59,7 @@ class IdeManager { * Load custom installer files (unique installation logic) * These files have special installation patterns that don't fit the config-driven model */ - loadCustomInstallerFiles() { + async loadCustomInstallerFiles() { const ideDir = __dirname; const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js']; @@ -81,7 +81,7 @@ class IdeManager { } } } catch (error) { - console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`)); + await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`); } } } @@ -171,17 +171,45 @@ class IdeManager { const handler = this.handlers.get(ideName.toLowerCase()); if (!handler) { - console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`)); - console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', '))); - return { success: false, reason: 'unsupported' }; + await prompts.log.warn(`IDE '${ideName}' is not yet supported`); + await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`); + return { success: false, ide: ideName, error: 'unsupported IDE' }; } try { - await handler.setup(projectDir, bmadDir, options); - return { success: true, ide: ideName }; + const handlerResult = await handler.setup(projectDir, bmadDir, options); + // Build detail string from handler-returned data + let detail = ''; + if (handlerResult && handlerResult.results) { + // Config-driven handlers return { success, results: { agents, workflows, tasks, tools } } + const r = handlerResult.results; + const parts = []; + if (r.agents > 0) parts.push(`${r.agents} agents`); + if (r.workflows > 0) parts.push(`${r.workflows} workflows`); + if (r.tasks > 0) parts.push(`${r.tasks} tasks`); + if (r.tools > 0) parts.push(`${r.tools} tools`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.counts) { + // Codex handler returns { success, counts: { agents, workflows, tasks }, written } + const c = handlerResult.counts; + const parts = []; + if (c.agents > 0) parts.push(`${c.agents} agents`); + if (c.workflows > 0) parts.push(`${c.workflows} workflows`); + if (c.tasks > 0) parts.push(`${c.tasks} tasks`); + detail = parts.join(', '); + } else if (handlerResult && handlerResult.modes !== undefined) { + // Kilo handler returns { success, modes, workflows, tasks, tools } + const parts = []; + if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`); + if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`); + if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`); + if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); + detail = parts.join(', '); + } + return { success: true, ide: ideName, detail, handlerResult }; } catch (error) { - console.error(chalk.red(`Failed to setup ${ideName}:`), error.message); - return { success: false, error: error.message }; + await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); + return { success: false, ide: ideName, error: error.message }; } } @@ -254,7 +282,7 @@ class IdeManager { const handler = this.handlers.get(ideName.toLowerCase()); if (!handler) { - console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported for custom agent installation`)); + await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`); continue; } @@ -266,7 +294,7 @@ class IdeManager { } } } catch (error) { - console.warn(chalk.yellow(`⚠️ Failed to install ${ideName} launcher: ${error.message}`)); + await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`); } } diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index caf60614f..0915c306b 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,6 +1,5 @@ const path = require('node:path'); const fs = require('fs-extra'); -const chalk = require('chalk'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index b293fc0e0..ece1c8630 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -1,7 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); -const chalk = require('chalk'); const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils'); /** diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index 5a23fda2f..d94e77db1 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -1,7 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); -const chalk = require('chalk'); +const prompts = require('../../../../lib/prompts'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** @@ -22,7 +22,7 @@ class WorkflowCommandGenerator { const workflows = await this.loadWorkflowManifest(bmadDir); if (!workflows) { - console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.')); + await prompts.log.warn('Workflow manifest not found. Skipping command generation.'); return { generated: 0 }; } @@ -157,8 +157,7 @@ class WorkflowCommandGenerator { .replaceAll('{{module}}', workflow.module) .replaceAll('{{description}}', workflow.description) .replaceAll('{{workflow_path}}', workflowPath) - .replaceAll('_bmad', this.bmadFolderName) - .replaceAll('_bmad', '_bmad'); + .replaceAll('_bmad', this.bmadFolderName); } /** @@ -238,15 +237,15 @@ When running any workflow: const match = workflowPath.match(/\/src\/bmm\/(.+)/); if (match) { transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`; - } else if (workflowPath.includes('/src/core/')) { - const match = workflowPath.match(/\/src\/core\/(.+)/); - if (match) { - transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`; - } } - - return transformed; + } else if (workflowPath.includes('/src/core/')) { + const match = workflowPath.match(/\/src\/core\/(.+)/); + if (match) { + transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`; + } } + + return transformed; } async loadWorkflowManifest(bmadDir) { diff --git a/tools/cli/installers/lib/message-loader.js b/tools/cli/installers/lib/message-loader.js index dd1126693..7198f0328 100644 --- a/tools/cli/installers/lib/message-loader.js +++ b/tools/cli/installers/lib/message-loader.js @@ -1,7 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); const yaml = require('yaml'); -const chalk = require('chalk'); +const prompts = require('../../lib/prompts'); /** * Load and display installer messages from messages.yaml @@ -51,22 +51,20 @@ class MessageLoader { /** * Display the start message (after logo, before prompts) */ - displayStartMessage() { + async displayStartMessage() { const message = this.getStartMessage(); if (message) { - console.log(chalk.cyan(message)); - console.log(); + await prompts.log.info(message); } } /** * Display the end message (after installation completes) */ - displayEndMessage() { + async displayEndMessage() { const message = this.getEndMessage(); if (message) { - console.log(); - console.log(chalk.cyan(message)); + await prompts.log.info(message); } } diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index c55dae838..0af4312fc 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -1,8 +1,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); -const chalk = require('chalk'); -const ora = require('ora'); +const prompts = require('../../../lib/prompts'); const { XmlHandler } = require('../../../lib/xml-handler'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { filterCustomizationData } = require('../../../lib/agent/compiler'); @@ -17,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); * @class ModuleManager * @requires fs-extra * @requires yaml - * @requires chalk + * @requires prompts * @requires XmlHandler * * @example @@ -152,26 +151,26 @@ class ModuleManager { // File hasn't been modified by user, safe to update await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Updated sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Updated sidecar file: ${relativeToBmad}`); } } else { // User has modified the file, preserve it if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Preserving user-modified file: ${relativeToBmad}`)); + await prompts.log.message(` Preserving user-modified file: ${relativeToBmad}`); } } } else { // First time seeing this file in manifest, copy it await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Added new sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Added new sidecar file: ${relativeToBmad}`); } } } else { // New installation await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Copied sidecar file: ${relativeToBmad}`)); + await prompts.log.message(` Copied sidecar file: ${relativeToBmad}`); } } @@ -288,7 +287,7 @@ class ModuleManager { moduleInfo.dependencies = config.dependencies || []; moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected; } catch (error) { - console.warn(`Failed to read config for ${defaultName}:`, error.message); + await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`); } return moduleInfo; @@ -299,7 +298,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the module to find (from module.yaml) * @returns {string|null} Path to the module source or null if not found */ - async findModuleSource(moduleCode) { + async findModuleSource(moduleCode, options = {}) { const projectRoot = getProjectRoot(); // First check custom module paths if they exist @@ -316,7 +315,7 @@ class ModuleManager { } // Check external official modules - const externalSource = await this.findExternalModuleSource(moduleCode); + const externalSource = await this.findExternalModuleSource(moduleCode, options); if (externalSource) { return externalSource; } @@ -348,7 +347,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string} Path to the cloned repository */ - async cloneExternalModule(moduleCode) { + async cloneExternalModule(moduleCode, options = {}) { const { execSync } = require('node:child_process'); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); @@ -358,10 +357,32 @@ class ModuleManager { const cacheDir = this.getExternalCacheDir(); const moduleCacheDir = path.join(cacheDir, moduleCode); + const silent = options.silent || false; // Create cache directory if it doesn't exist await fs.ensureDir(cacheDir); + // Helper to create a spinner or a no-op when silent + const createSpinner = async () => { + if (silent) { + return { + start() {}, + stop() {}, + error() {}, + message() {}, + cancel() {}, + clear() {}, + get isSpinning() { + return false; + }, + get isCancelled() { + return false; + }, + }; + } + return await prompts.spinner(); + }; + // Track if we need to install dependencies let needsDependencyInstall = false; let wasNewClone = false; @@ -369,21 +390,30 @@ class ModuleManager { // Check if already cloned if (await fs.pathExists(moduleCacheDir)) { // Try to update if it's a git repo - const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start(); + const fetchSpinner = await createSpinner(); + fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); // Fetch and reset to remote - works better with shallow clones than pull - execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' }); - execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }); + execSync('git fetch origin --depth 1', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); + execSync('git reset --hard origin/HEAD', { + cwd: moduleCacheDir, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, + }); const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); - fetchSpinner.succeed(`Fetched ${moduleInfo.name}`); + fetchSpinner.stop(`Fetched ${moduleInfo.name}`); // Force dependency install if we got new code if (currentRef !== newRef) { needsDependencyInstall = true; } } catch { - fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`); + fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`); // If update fails, remove and re-clone await fs.remove(moduleCacheDir); wasNewClone = true; @@ -394,14 +424,16 @@ class ModuleManager { // Clone if not exists or was removed if (wasNewClone) { - const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start(); + const fetchSpinner = await createSpinner(); + fetchSpinner.start(`Fetching ${moduleInfo.name}...`); try { execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, { - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, }); - fetchSpinner.succeed(`Fetched ${moduleInfo.name}`); + fetchSpinner.stop(`Fetched ${moduleInfo.name}`); } catch (error) { - fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`); + fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`); throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`); } } @@ -415,17 +447,18 @@ class ModuleManager { // Force install if we updated or cloned new if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { - const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); + const installSpinner = await createSpinner(); + installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); - installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`); + installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { - installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`); - console.warn(chalk.yellow(` Warning: ${error.message}`)); + installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } else { // Check if package.json is newer than node_modules @@ -440,17 +473,18 @@ class ModuleManager { } if (packageJsonNewer) { - const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start(); + const installSpinner = await createSpinner(); + installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`); try { execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { cwd: moduleCacheDir, - stdio: 'pipe', + stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000, // 2 minute timeout }); - installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`); + installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); } catch (error) { - installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`); - console.warn(chalk.yellow(` Warning: ${error.message}`)); + installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); + if (!silent) await prompts.log.warn(` Warning: ${error.message}`); } } } @@ -464,7 +498,7 @@ class ModuleManager { * @param {string} moduleCode - Code of the external module * @returns {string|null} Path to the module source or null if not found */ - async findExternalModuleSource(moduleCode) { + async findExternalModuleSource(moduleCode, options = {}) { const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); if (!moduleInfo) { @@ -472,7 +506,7 @@ class ModuleManager { } // Clone the external module repo - const cloneDir = await this.cloneExternalModule(moduleCode); + const cloneDir = await this.cloneExternalModule(moduleCode, options); // The module-definition specifies the path to module.yaml relative to repo root // We need to return the directory containing module.yaml @@ -493,7 +527,7 @@ class ModuleManager { * @param {Object} options.logger - Logger instance for output */ async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { - const sourcePath = await this.findModuleSource(moduleName); + const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); const targetPath = path.join(bmadDir, moduleName); // Check if source module exists @@ -514,14 +548,14 @@ class ModuleManager { const customContent = await fs.readFile(rootCustomConfigPath, 'utf8'); customConfig = yaml.parse(customContent); } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message)); + await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); } } else if (await fs.pathExists(moduleInstallerCustomPath)) { try { const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8'); customConfig = yaml.parse(customContent); } catch (error) { - console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message)); + await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); } } @@ -529,7 +563,7 @@ class ModuleManager { if (customConfig) { options.moduleConfig = { ...options.moduleConfig, ...customConfig }; if (options.logger) { - options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`)); + options.logger.log(` Merged custom configuration for ${moduleName}`); } } @@ -582,7 +616,7 @@ class ModuleManager { * @param {string} bmadDir - Target bmad directory * @param {boolean} force - Force update (overwrite modifications) */ - async update(moduleName, bmadDir, force = false) { + async update(moduleName, bmadDir, force = false, options = {}) { const sourcePath = await this.findModuleSource(moduleName); const targetPath = path.join(bmadDir, moduleName); @@ -599,7 +633,7 @@ class ModuleManager { if (force) { // Force update - remove and reinstall await fs.remove(targetPath); - return await this.install(moduleName, bmadDir); + return await this.install(moduleName, bmadDir, null, { installer: options.installer }); } else { // Selective update - preserve user modifications await this.syncModule(sourcePath, targetPath); @@ -673,7 +707,7 @@ class ModuleManager { const config = yaml.parse(configContent); Object.assign(moduleInfo, config); } catch (error) { - console.warn(`Failed to read installed module config:`, error.message); + await prompts.log.warn(`Failed to read installed module config: ${error.message}`); } } @@ -735,7 +769,7 @@ class ModuleManager { // Check for localskip="true" in the agent tag const agentMatch = content.match(/]*\slocalskip="true"[^>]*>/); if (agentMatch) { - console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`)); + await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`); continue; // Skip this agent } } @@ -768,7 +802,6 @@ class ModuleManager { // IMPORTANT: Replace escape sequence and placeholder BEFORE parsing YAML // Otherwise parsing will fail on the placeholder - yamlContent = yamlContent.replaceAll('_bmad', '_bmad'); yamlContent = yamlContent.replaceAll('_bmad', this.bmadFolderName); try { @@ -838,7 +871,7 @@ class ModuleManager { await fs.writeFile(targetFile, strippedYaml, 'utf8'); } catch { // If anything fails, just copy the file as-is - console.warn(chalk.yellow(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`)); + await prompts.log.warn(` Warning: Could not process ${path.basename(sourceFile)}, copying as-is`); await fs.copy(sourceFile, targetFile, { overwrite: true }); } } @@ -890,7 +923,7 @@ class ModuleManager { await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); // Only show customize creation in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`)); + await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`); } // Store original hash for modification detection @@ -990,10 +1023,10 @@ class ModuleManager { const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer); if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) { - console.log(chalk.dim(` Sidecar files processed: ${copiedFiles.length} files`)); + await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`); } } else if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log(chalk.yellow(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`)); + await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`); } } @@ -1012,14 +1045,12 @@ class ModuleManager { // Only show compilation details in verbose mode if (process.env.BMAD_VERBOSE_INSTALL === 'true') { - console.log( - chalk.dim( - ` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, - ), + await prompts.log.message( + ` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, ); } } catch (error) { - console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message)); + await prompts.log.warn(` Failed to compile agent ${agentName}: ${error.message}`); } } } @@ -1139,11 +1170,11 @@ class ModuleManager { } if (!workflowsVendored) { - console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`)); + await prompts.log.info(`\n Vendoring cross-module workflows for ${moduleName}...`); workflowsVendored = true; } - console.log(chalk.dim(` Processing: ${agentFile}`)); + await prompts.log.message(` Processing: ${agentFile}`); for (const item of workflowInstallItems) { const sourceWorkflowPath = item.workflow; // Where to copy FROM @@ -1155,7 +1186,7 @@ class ModuleManager { // Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_bmad)\/([^/]+)\/workflows\/(.+)/); if (!sourceMatch) { - console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`)); + await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`); continue; } @@ -1166,7 +1197,7 @@ class ModuleManager { // Example: {project-root}/_bmad/bmgd/workflows/4-production/create-story/workflow.yaml const installMatch = installWorkflowPath.match(/\{project-root\}\/(_bmad)\/([^/]+)\/workflows\/(.+)/); if (!installMatch) { - console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`)); + await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`); continue; } @@ -1179,15 +1210,13 @@ class ModuleManager { // Check if source workflow exists if (!(await fs.pathExists(actualSourceWorkflowPath))) { - console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`)); + await prompts.log.warn(` Source workflow not found: ${actualSourceWorkflowPath}`); continue; } // Copy the entire workflow folder - console.log( - chalk.dim( - ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`, - ), + await prompts.log.message( + ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(/\/workflow\.yaml$/, '')} → ${moduleName}/workflows/${installWorkflowSubPath.replace(/\/workflow\.yaml$/, '')}`, ); await fs.ensureDir(path.dirname(actualDestWorkflowPath)); @@ -1203,7 +1232,7 @@ class ModuleManager { } if (workflowsVendored) { - console.log(chalk.green(` ✓ Workflow vendoring complete\n`)); + await prompts.log.success(` Workflow vendoring complete\n`); } } @@ -1225,7 +1254,7 @@ class ModuleManager { if (updatedYaml !== yamlContent) { await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8'); - console.log(chalk.dim(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`)); + await prompts.log.message(` Updated config_source to: ${this.bmadFolderName}/${newModuleName}/config.yaml`); } } @@ -1241,7 +1270,7 @@ class ModuleManager { if (moduleName === 'core') { sourcePath = getSourcePath('core'); } else { - sourcePath = await this.findModuleSource(moduleName); + sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); if (!sourcePath) { // No source found, skip module installer return; @@ -1280,11 +1309,11 @@ class ModuleManager { }); if (!result) { - console.warn(chalk.yellow(`Module installer for ${moduleName} returned false`)); + await prompts.log.warn(`Module installer for ${moduleName} returned false`); } } } catch (error) { - console.error(chalk.red(`Error running module installer for ${moduleName}: ${error.message}`)); + await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`); } } @@ -1306,7 +1335,7 @@ class ModuleManager { await fs.writeFile(configPath, configContent, 'utf8'); } catch (error) { - console.warn(`Failed to process module config:`, error.message); + await prompts.log.warn(`Failed to process module config: ${error.message}`); } } } diff --git a/tools/cli/lib/agent/installer.js b/tools/cli/lib/agent/installer.js index a76504530..c9e0dd916 100644 --- a/tools/cli/lib/agent/installer.js +++ b/tools/cli/lib/agent/installer.js @@ -6,7 +6,7 @@ const fs = require('node:fs'); const path = require('node:path'); const yaml = require('yaml'); -const readline = require('node:readline'); +const prompts = require('../prompts'); const { compileAgent, compileAgentFile } = require('./compiler'); const { extractInstallConfig, getDefaultValues } = require('./template-engine'); @@ -149,83 +149,47 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = { return { ...defaults, ...presetAnswers }; } - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - const question = (prompt) => - new Promise((resolve) => { - rl.question(prompt, resolve); - }); - const answers = { ...defaults, ...presetAnswers }; - console.log('\n📝 Agent Configuration\n'); - if (installConfig.description) { - console.log(` ${installConfig.description}\n`); - } + await prompts.note(installConfig.description || '', 'Agent Configuration'); for (const q of installConfig.questions) { // Skip questions for variables that are already set (e.g., custom_name set upfront) if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) { - console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`)); + await prompts.log.message(` ${q.var}: ${answers[q.var]} (already set)`); continue; } - let response; - switch (q.type) { case 'text': { - const defaultHint = q.default ? ` (default: ${q.default})` : ''; - response = await question(` ${q.prompt}${defaultHint}: `); - answers[q.var] = response || q.default || ''; - + const response = await prompts.text({ + message: q.prompt, + default: q.default ?? '', + }); + answers[q.var] = response ?? q.default ?? ''; break; } case 'boolean': { - const defaultHint = q.default ? ' [Y/n]' : ' [y/N]'; - response = await question(` ${q.prompt}${defaultHint}: `); - if (response === '') { - answers[q.var] = q.default; - } else { - answers[q.var] = response.toLowerCase().startsWith('y'); - } - + const response = await prompts.confirm({ + message: q.prompt, + default: q.default, + }); + answers[q.var] = response; break; } case 'choice': { - console.log(` ${q.prompt}`); - for (const [idx, opt] of q.options.entries()) { - const marker = opt.value === q.default ? '* ' : ' '; - console.log(` ${marker}${idx + 1}. ${opt.label}`); - } - const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1; - let validChoice = false; - let choiceIdx; - while (!validChoice) { - response = await question(` Choice (default: ${defaultIdx}): `); - if (response) { - choiceIdx = parseInt(response, 10) - 1; - if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) { - console.log(` Invalid choice. Please enter 1-${q.options.length}`); - } else { - validChoice = true; - } - } else { - choiceIdx = defaultIdx - 1; - validChoice = true; - } - } - answers[q.var] = q.options[choiceIdx].value; - + const response = await prompts.select({ + message: q.prompt, + options: q.options.map((o) => ({ value: o.value, label: o.label })), + initialValue: q.default, + }); + answers[q.var] = response; break; } // No default } } - rl.close(); return answers; } diff --git a/tools/cli/lib/cli-utils.js b/tools/cli/lib/cli-utils.js index da1933631..569f1c44c 100644 --- a/tools/cli/lib/cli-utils.js +++ b/tools/cli/lib/cli-utils.js @@ -1,9 +1,6 @@ -const chalk = require('chalk'); -const boxen = require('boxen'); -const wrapAnsi = require('wrap-ansi'); -const figlet = require('figlet'); const path = require('node:path'); const os = require('node:os'); +const prompts = require('./prompts'); const CLIUtils = { /** @@ -19,27 +16,32 @@ const CLIUtils = { }, /** - * Display BMAD logo - * @param {boolean} clearScreen - Whether to clear the screen first (default: true for initial display only) + * Display BMAD logo using @clack intro + box + * @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen) */ - displayLogo(clearScreen = true) { - if (clearScreen) { - console.clear(); - } - + async displayLogo(_clearScreen = true) { const version = this.getVersion(); + const color = await prompts.getColor(); // ASCII art logo - const logo = ` - ██████╗ ███╗ ███╗ █████╗ ██████╗ ™ - ██╔══██╗████╗ ████║██╔══██╗██╔══██╗ - ██████╔╝██╔████╔██║███████║██║ ██║ - ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║ - ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝ - ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝`; + const logo = [ + ' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™', + ' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗', + ' ██████╔╝██╔████╔██║███████║██║ ██║', + ' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║', + ' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝', + ' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝', + ] + .map((line) => color.yellow(line)) + .join('\n'); - console.log(chalk.cyan(logo)); - console.log(chalk.dim(` Build More, Architect Dreams`) + chalk.cyan.bold(` v${version}`) + '\n'); + const tagline = ' Build More, Architect Dreams'; + + await prompts.box(`${logo}\n${tagline}`, `v${version}`, { + contentAlign: 'center', + rounded: true, + formatBorder: color.blue, + }); }, /** @@ -47,13 +49,8 @@ const CLIUtils = { * @param {string} title - Section title * @param {string} subtitle - Optional subtitle */ - displaySection(title, subtitle = null) { - console.log('\n' + chalk.cyan('═'.repeat(80))); - console.log(chalk.cyan.bold(` ${title}`)); - if (subtitle) { - console.log(chalk.dim(` ${subtitle}`)); - } - console.log(chalk.cyan('═'.repeat(80)) + '\n'); + async displaySection(title, subtitle = null) { + await prompts.note(subtitle || '', title); }, /** @@ -61,25 +58,21 @@ const CLIUtils = { * @param {string|Array} content - Content to display * @param {Object} options - Box options */ - displayBox(content, options = {}) { - const defaultOptions = { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'cyan', - ...options, - }; - - // Handle array content + async displayBox(content, options = {}) { let text = content; if (Array.isArray(content)) { text = content.join('\n\n'); } - // Wrap text to prevent overflow - const wrapped = wrapAnsi(text, 76, { hard: true, wordWrap: true }); + const color = await prompts.getColor(); + const borderColor = options.borderColor || 'cyan'; + const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue }; + const formatBorder = colorMap[borderColor] || color.cyan; - console.log(boxen(wrapped, defaultOptions)); + await prompts.box(text, options.title, { + rounded: options.borderStyle === 'round' || options.borderStyle === undefined, + formatBorder, + }); }, /** @@ -88,14 +81,9 @@ const CLIUtils = { * @param {string} header - Custom header from module.yaml * @param {string} subheader - Custom subheader from module.yaml */ - displayModuleConfigHeader(moduleName, header = null, subheader = null) { - // Simple blue banner with custom header/subheader if provided - console.log('\n' + chalk.cyan('─'.repeat(80))); - console.log(chalk.cyan(header || `Configuring ${moduleName.toUpperCase()} Module`)); - if (subheader) { - console.log(chalk.dim(`${subheader}`)); - } - console.log(chalk.cyan('─'.repeat(80)) + '\n'); + async displayModuleConfigHeader(moduleName, header = null, subheader = null) { + const title = header || `Configuring ${moduleName.toUpperCase()} Module`; + await prompts.note(subheader || '', title); }, /** @@ -104,14 +92,9 @@ const CLIUtils = { * @param {string} header - Custom header from module.yaml * @param {string} subheader - Custom subheader from module.yaml */ - displayModuleNoConfig(moduleName, header = null, subheader = null) { - // Show full banner with header/subheader, just like modules with config - console.log('\n' + chalk.cyan('─'.repeat(80))); - console.log(chalk.cyan(header || `${moduleName.toUpperCase()} Module - No Custom Configuration`)); - if (subheader) { - console.log(chalk.dim(`${subheader}`)); - } - console.log(chalk.cyan('─'.repeat(80)) + '\n'); + async displayModuleNoConfig(moduleName, header = null, subheader = null) { + const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`; + await prompts.note(subheader || '', title); }, /** @@ -120,42 +103,33 @@ const CLIUtils = { * @param {number} total - Total steps * @param {string} description - Step description */ - displayStep(current, total, description) { + async displayStep(current, total, description) { const progress = `[${current}/${total}]`; - console.log('\n' + chalk.cyan(progress) + ' ' + chalk.bold(description)); - console.log(chalk.dim('─'.repeat(80 - progress.length - 1)) + '\n'); + await prompts.log.step(`${progress} ${description}`); }, /** * Display completion message * @param {string} message - Completion message */ - displayComplete(message) { - console.log( - '\n' + - boxen(chalk.green('✨ ' + message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'green', - }), - ); + async displayComplete(message) { + const color = await prompts.getColor(); + await prompts.box(`\u2728 ${message}`, 'Complete', { + rounded: true, + formatBorder: color.green, + }); }, /** * Display error message * @param {string} message - Error message */ - displayError(message) { - console.log( - '\n' + - boxen(chalk.red('✗ ' + message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'red', - }), - ); + async displayError(message) { + const color = await prompts.getColor(); + await prompts.box(`\u2717 ${message}`, 'Error', { + rounded: true, + formatBorder: color.red, + }); }, /** @@ -163,7 +137,7 @@ const CLIUtils = { * @param {Array} items - Items to display * @param {string} prefix - Item prefix */ - formatList(items, prefix = '•') { + formatList(items, prefix = '\u2022') { return items.map((item) => ` ${prefix} ${item}`).join('\n'); }, @@ -178,25 +152,6 @@ const CLIUtils = { } }, - /** - * Display table - * @param {Array} data - Table data - * @param {Object} options - Table options - */ - displayTable(data, options = {}) { - const Table = require('cli-table3'); - const table = new Table({ - style: { - head: ['cyan'], - border: ['dim'], - }, - ...options, - }); - - for (const row of data) table.push(row); - console.log(table.toString()); - }, - /** * Display module completion message * @param {string} moduleName - Name of the completed module diff --git a/tools/cli/lib/prompts.js b/tools/cli/lib/prompts.js index c51d68d19..24500700b 100644 --- a/tools/cli/lib/prompts.js +++ b/tools/cli/lib/prompts.js @@ -89,11 +89,51 @@ async function note(message, title) { /** * Display a spinner for async operations - * @returns {Object} Spinner controller with start, stop, message methods + * Wraps @clack/prompts spinner with isSpinning state tracking + * @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning */ async function spinner() { const clack = await getClack(); - return clack.spinner(); + const s = clack.spinner(); + let spinning = false; + + return { + start: (msg) => { + if (spinning) { + s.message(msg); + } else { + spinning = true; + s.start(msg); + } + }, + stop: (msg) => { + if (spinning) { + spinning = false; + s.stop(msg); + } + }, + message: (msg) => { + if (spinning) s.message(msg); + }, + error: (msg) => { + spinning = false; + s.error(msg); + }, + cancel: (msg) => { + spinning = false; + s.cancel(msg); + }, + clear: () => { + spinning = false; + s.clear(); + }, + get isSpinning() { + return spinning; + }, + get isCancelled() { + return s.isCancelled; + }, + }; } /** @@ -190,31 +230,6 @@ async function multiselect(options) { return result; } -/** - * Grouped multi-select prompt for categorized options - * @param {Object} options - Prompt options - * @param {string} options.message - The question to ask - * @param {Object} options.options - Object mapping group names to arrays of choices - * @param {Array} [options.initialValues] - Array of initially selected values - * @param {boolean} [options.required=false] - Whether at least one must be selected - * @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole - * @returns {Promise} Array of selected values - */ -async function groupMultiselect(options) { - const clack = await getClack(); - - const result = await clack.groupMultiselect({ - message: options.message, - options: options.options, - initialValues: options.initialValues, - required: options.required || false, - selectableGroups: options.selectableGroups || false, - }); - - await handleCancel(result); - return result; -} - /** * Default filter function for autocomplete - case-insensitive label matching * @param {string} search - Search string @@ -237,6 +252,7 @@ function defaultAutocompleteFilter(search, option) { * @param {boolean} [options.required=false] - Whether at least one must be selected * @param {number} [options.maxItems=5] - Maximum visible items in scrollable list * @param {Function} [options.filter] - Custom filter function (search, option) => boolean + * @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off * @returns {Promise} Array of selected values */ async function autocompleteMultiselect(options) { @@ -245,6 +261,7 @@ async function autocompleteMultiselect(options) { const color = await getPicocolors(); const filterFn = options.filter ?? defaultAutocompleteFilter; + const lockedSet = new Set(options.lockedValues || []); const prompt = new core.AutocompletePrompt({ options: options.options, @@ -255,7 +272,7 @@ async function autocompleteMultiselect(options) { return 'Please select at least one item'; } }, - initialValue: options.initialValues, + initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])], render() { const barColor = this.state === 'error' ? color.yellow : color.cyan; const bar = barColor(clack.S_BAR); @@ -280,9 +297,17 @@ async function autocompleteMultiselect(options) { // Render option with checkbox const renderOption = (opt, isHighlighted) => { const isSelected = this.selectedValues.includes(opt.value); + const isLocked = lockedSet.has(opt.value); const label = opt.label ?? String(opt.value ?? ''); - const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : ''; - const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE); + const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : ''; + + let checkbox; + if (isLocked) { + checkbox = color.green(clack.S_CHECKBOX_SELECTED); + const lockHint = color.dim(' (always installed)'); + return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`; + } + checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE); return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`; }; @@ -322,6 +347,18 @@ async function autocompleteMultiselect(options) { }, }); + // Prevent locked values from being toggled off + if (lockedSet.size > 0) { + const originalToggle = prompt.toggleSelected.bind(prompt); + prompt.toggleSelected = function (value) { + // If locked and already selected, skip the toggle (would deselect) + if (lockedSet.has(value) && this.selectedValues.includes(value)) { + return; + } + originalToggle(value); + }; + } + // === FIX: Make SPACE always act as selection key (not search input) === // Override _isActionKey to treat SPACE like TAB - always an action key // This prevents SPACE from being added to the search input @@ -335,8 +372,9 @@ async function autocompleteMultiselect(options) { // Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true) prompt.on('key', (char, key) => { - if (key && key.name === 'space' && !prompt.isNavigating && prompt.focusedValue !== undefined) { - prompt.toggleSelected(prompt.focusedValue); + if (key && key.name === 'space' && !prompt.isNavigating) { + const focused = prompt.filteredOptions[prompt.cursor]; + if (focused) prompt.toggleSelected(focused.value); } }); // === END FIX === @@ -520,6 +558,131 @@ const log = { }, }; +/** + * Display cancellation message + * @param {string} [message='Operation cancelled'] - The cancellation message + */ +async function cancel(message = 'Operation cancelled') { + const clack = await getClack(); + clack.cancel(message); +} + +/** + * Display content in a styled box + * @param {string} content - The box content + * @param {string} [title] - Optional title + * @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.) + */ +async function box(content, title, options) { + const clack = await getClack(); + clack.box(content, title, options); +} + +/** + * Create a progress bar for visualizing task completion + * @param {Object} [options] - Progress options (max, style, etc.) + * @returns {Promise} Progress controller with start, advance, stop methods + */ +async function progress(options) { + const clack = await getClack(); + return clack.progress(options); +} + +/** + * Create a task log for displaying scrolling subprocess output + * @param {Object} options - TaskLog options (title, limit, retainLog) + * @returns {Promise} TaskLog controller with message, success, error methods + */ +async function taskLog(options) { + const clack = await getClack(); + return clack.taskLog(options); +} + +/** + * File system path prompt with autocomplete + * @param {Object} options - Path options + * @param {string} options.message - The prompt message + * @param {string} [options.initialValue] - Initial path value + * @param {boolean} [options.directory=false] - Only allow directories + * @param {Function} [options.validate] - Validation function + * @returns {Promise} Selected path + */ +async function pathPrompt(options) { + const clack = await getClack(); + const result = await clack.path(options); + await handleCancel(result); + return result; +} + +/** + * Autocomplete single-select prompt with type-ahead filtering + * @param {Object} options - Autocomplete options + * @param {string} options.message - The prompt message + * @param {Array} options.options - Array of choices [{value, label, hint?}] + * @param {string} [options.placeholder] - Placeholder text + * @param {number} [options.maxItems] - Maximum visible items + * @param {Function} [options.filter] - Custom filter function + * @returns {Promise} Selected value + */ +async function autocomplete(options) { + const clack = await getClack(); + const result = await clack.autocomplete(options); + await handleCancel(result); + return result; +} + +/** + * Key-based instant selection prompt + * @param {Object} options - SelectKey options + * @param {string} options.message - The prompt message + * @param {Array} options.options - Array of choices [{value, label, hint?}] + * @returns {Promise} Selected value + */ +async function selectKey(options) { + const clack = await getClack(); + const result = await clack.selectKey(options); + await handleCancel(result); + return result; +} + +/** + * Stream messages with dynamic content (for LLMs, generators, etc.) + */ +const stream = { + async info(generator) { + const clack = await getClack(); + return clack.stream.info(generator); + }, + async success(generator) { + const clack = await getClack(); + return clack.stream.success(generator); + }, + async step(generator) { + const clack = await getClack(); + return clack.stream.step(generator); + }, + async warn(generator) { + const clack = await getClack(); + return clack.stream.warn(generator); + }, + async error(generator) { + const clack = await getClack(); + return clack.stream.error(generator); + }, + async message(generator, options) { + const clack = await getClack(); + return clack.stream.message(generator, options); + }, +}; + +/** + * Get the color utility (picocolors instance from @clack/prompts) + * @returns {Promise} The color utility (picocolors) + */ +async function getColor() { + return await getPicocolors(); +} + /** * Execute an array of Inquirer-style questions using @clack/prompts * This provides compatibility with dynamic question arrays @@ -619,20 +782,28 @@ async function prompt(questions) { module.exports = { getClack, + getColor, handleCancel, intro, outro, + cancel, note, + box, spinner, + progress, + taskLog, select, multiselect, - groupMultiselect, autocompleteMultiselect, + autocomplete, + selectKey, confirm, text, + path: pathPrompt, password, group, tasks, log, + stream, prompt, }; diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 811931f5f..9134b4e28 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -1,4 +1,3 @@ -const chalk = require('chalk'); const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); @@ -30,12 +29,12 @@ class UI { * @returns {Object} Installation configuration */ async promptInstall(options = {}) { - CLIUtils.displayLogo(); + await CLIUtils.displayLogo(); // Display version-specific start message from install-messages.yaml const { MessageLoader } = require('../installers/lib/message-loader'); const messageLoader = new MessageLoader(); - messageLoader.displayStartMessage(); + await messageLoader.displayStartMessage(); // Get directory from options or prompt let confirmedDirectory; @@ -47,7 +46,7 @@ class UI { throw new Error(`Invalid directory: ${validation}`); } confirmedDirectory = expandedDir; - console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory)); + await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`); } else { confirmedDirectory = await this.getConfirmedDirectory(); } @@ -75,7 +74,7 @@ class UI { for (const entry of entries) { if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) { hasLegacyBmadFolder = true; - legacyBmadPath = path.join(confirmedDirectory, '.bmad'); + legacyBmadPath = path.join(confirmedDirectory, entry.name); bmadDir = legacyBmadPath; // Check if it has _cfg folder @@ -98,38 +97,30 @@ class UI { // Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha) // Show version warning instead of offering conversion if (hasLegacyBmadFolder || hasLegacyCfg) { - console.log(''); - console.log(chalk.yellow.bold('⚠️ LEGACY INSTALLATION DETECTED')); - console.log(chalk.yellow('─'.repeat(80))); - console.log( - chalk.yellow( - 'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.', - ), + await prompts.log.warn('LEGACY INSTALLATION DETECTED'); + await prompts.note( + 'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' + + 'this is from an old BMAD version that is out of date for automatic upgrade,\n' + + 'manual intervention required.\n\n' + + 'You have a legacy version installed (v4 or alpha).\n' + + 'Legacy installations may have compatibility issues.\n\n' + + 'For the best experience, we strongly recommend:\n' + + ' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' + + ' 2. Run a fresh installation\n\n' + + 'If you do not want to start fresh, you can attempt to proceed beyond this\n' + + 'point IF you have ensured the bmad folder is named _bmad, and under it there\n' + + 'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' + + 'you would need to rename it _config, and then restart the installer.\n\n' + + 'Benefits of a fresh install:\n' + + ' \u2022 Cleaner configuration without legacy artifacts\n' + + ' \u2022 All new features properly configured\n' + + ' \u2022 Fewer potential conflicts\n\n' + + 'If you have already produced output from an earlier alpha version, you can\n' + + 'still retain those artifacts. After installation, ensure you configured during\n' + + 'install the proper file locations for artifacts depending on the module you\n' + + 'are using, or move the files to the proper locations.', + 'Legacy Installation Detected', ); - console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).')); - console.log(''); - console.log(chalk.dim('Legacy installations may have compatibility issues.')); - console.log(''); - console.log(chalk.dim('For the best experience, we strongly recommend:')); - console.log(chalk.dim(' 1. Delete your current BMAD installation folder (.bmad or bmad)')); - console.log( - chalk.dim( - ' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.', - ), - ); - console.log(''); - console.log(chalk.dim('Benefits of a fresh install:')); - console.log(chalk.dim(' • Cleaner configuration without legacy artifacts')); - console.log(chalk.dim(' • All new features properly configured')); - console.log(chalk.dim(' • Fewer potential conflicts')); - console.log(chalk.dim('')); - console.log( - chalk.dim( - 'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.', - ), - ); - console.log(chalk.yellow('─'.repeat(80))); - console.log(''); const proceed = await prompts.select({ message: 'How would you like to proceed?', @@ -147,37 +138,33 @@ class UI { }); if (proceed === 'cancel') { - console.log(''); - console.log(chalk.cyan('To do a fresh install:')); - console.log(chalk.dim(' 1. Delete the existing bmad folder in your project')); - console.log(chalk.dim(" 2. Run 'bmad install' again")); - console.log(''); + await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install'); process.exit(0); return; } - const ora = require('ora'); - const spinner = ora('Updating folder structure...').start(); + const s = await prompts.spinner(); + s.start('Updating folder structure...'); try { // Handle .bmad folder if (hasLegacyBmadFolder) { const newBmadPath = path.join(confirmedDirectory, '_bmad'); await fs.move(legacyBmadPath, newBmadPath); bmadDir = newBmadPath; - spinner.succeed('Renamed ".bmad" to "_bmad"'); + s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`); } // Handle _cfg folder (either from .bmad or standalone) const cfgPath = path.join(bmadDir, '_cfg'); if (await fs.pathExists(cfgPath)) { - spinner.start('Renaming configuration folder...'); + s.start('Renaming configuration folder...'); const newCfgPath = path.join(bmadDir, '_config'); await fs.move(cfgPath, newCfgPath); - spinner.succeed('Renamed "_cfg" to "_config"'); + s.stop('Renamed "_cfg" to "_config"'); } } catch (error) { - spinner.fail('Failed to update folder structure'); - console.error(chalk.red(`Error: ${error.message}`)); + s.stop('Failed to update folder structure'); + await prompts.log.error(`Error: ${error.message}`); process.exit(1); } } @@ -239,7 +226,7 @@ class UI { throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`); } actionType = options.action; - console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType)); + await prompts.log.info(`Using action from command-line: ${actionType}`); } else { actionType = await prompts.select({ message: 'How would you like to proceed?', @@ -274,7 +261,7 @@ class UI { // Get existing installation info const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); - console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`)); + await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`); // Unified module selection - all modules in one grouped multiselect let selectedModules; @@ -284,13 +271,13 @@ class UI { .split(',') .map((m) => m.trim()) .filter(Boolean); - console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', '))); + await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); } else { selectedModules = await this.selectAllModules(installedModuleIds); + selectedModules = selectedModules.filter((m) => m !== 'core'); } // After module selection, ask about custom modules - console.log(''); let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; if (options.customContent) { @@ -299,7 +286,7 @@ class UI { .split(',') .map((p) => p.trim()) .filter(Boolean); - console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', '))); + await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); // Build custom content config similar to promptCustomContentSource const customPaths = []; @@ -309,7 +296,7 @@ class UI { const expandedPath = this.expandUserPath(customPath); const validation = this.validateCustomContentPathSync(expandedPath); if (validation) { - console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`)); + await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); continue; } @@ -321,12 +308,12 @@ class UI { const yaml = require('yaml'); moduleMeta = yaml.parse(moduleYaml); } catch (error) { - console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`)); + await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); continue; } if (!moduleMeta.code) { - console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`)); + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); continue; } @@ -404,11 +391,11 @@ class UI { .split(',') .map((m) => m.trim()) .filter(Boolean); - console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', '))); + await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); } else if (options.yes) { // Use default modules when --yes flag is set selectedModules = await this.getDefaultModules(installedModuleIds); - console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', '))); + await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`); } else { selectedModules = await this.selectAllModules(installedModuleIds); } @@ -420,7 +407,7 @@ class UI { .split(',') .map((p) => p.trim()) .filter(Boolean); - console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', '))); + await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); // Build custom content config similar to promptCustomContentSource const customPaths = []; @@ -430,7 +417,7 @@ class UI { const expandedPath = this.expandUserPath(customPath); const validation = this.validateCustomContentPathSync(expandedPath); if (validation) { - console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`)); + await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); continue; } @@ -442,12 +429,12 @@ class UI { const yaml = require('yaml'); moduleMeta = yaml.parse(moduleYaml); } catch (error) { - console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`)); + await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); continue; } if (!moduleMeta.code) { - console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`)); + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); continue; } @@ -531,7 +518,7 @@ class UI { const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value)); const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id)); if (unknownTools.length > 0) { - console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`)); + await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`); } // ───────────────────────────────────────────────────────────────────────────── @@ -569,21 +556,20 @@ class UI { const selectedIdes = upgradeSelected || []; if (selectedIdes.length === 0) { - console.log(''); const confirmNoTools = await prompts.confirm({ message: 'No tools selected. Continue without installing any tools?', default: false, }); if (!confirmNoTools) { - return this.promptToolSelection(projectDir); + return this.promptToolSelection(projectDir, options); } return { ides: [], skipIde: true }; } // Display selected tools - this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); return { ides: selectedIdes, skipIde: false }; } @@ -609,25 +595,25 @@ class UI { if (options.tools) { // Check for explicit "none" value to skip tool installation if (options.tools.toLowerCase() === 'none') { - console.log(chalk.cyan('Skipping tool configuration (--tools none)')); + await prompts.log.info('Skipping tool configuration (--tools none)'); return { ides: [], skipIde: true }; } else { selectedIdes = options.tools .split(',') .map((t) => t.trim()) .filter(Boolean); - console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', '))); - this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); return { ides: selectedIdes, skipIde: false }; } } else if (options.yes) { // If --yes flag is set, skip tool prompt and use previously configured tools or empty if (configuredIdes.length > 0) { - console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', '))); - this.displaySelectedTools(configuredIdes, preferredIdes, allTools); + await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`); + await this.displaySelectedTools(configuredIdes, preferredIdes, allTools); return { ides: configuredIdes, skipIde: false }; } else { - console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)')); + await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)'); return { ides: [], skipIde: true }; } } @@ -647,7 +633,6 @@ class UI { // STEP 3: Confirm if no tools selected // ───────────────────────────────────────────────────────────────────────────── if (selectedIdes.length === 0) { - console.log(''); const confirmNoTools = await prompts.confirm({ message: 'No tools selected. Continue without installing any tools?', default: false, @@ -655,7 +640,7 @@ class UI { if (!confirmNoTools) { // User wants to select tools - recurse - return this.promptToolSelection(projectDir); + return this.promptToolSelection(projectDir, options); } return { @@ -665,7 +650,7 @@ class UI { } // Display selected tools - this.displaySelectedTools(selectedIdes, preferredIdes, allTools); + await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); return { ides: selectedIdes, @@ -708,15 +693,12 @@ class UI { * Display installation summary * @param {Object} result - Installation result */ - showInstallSummary(result) { - // Clean, simple completion message - console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!')); - - // Show installation summary in a simple format - console.log(chalk.dim(`Installed to: ${result.path}`)); + async showInstallSummary(result) { + let summary = `Installed to: ${result.path}`; if (result.modules && result.modules.length > 0) { - console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`)); + summary += `\nModules: ${result.modules.join(', ')}`; } + await prompts.note(summary, 'BMAD is ready to use!'); } /** @@ -769,19 +751,19 @@ class UI { const coreConfig = {}; if (options.userName) { coreConfig.user_name = options.userName; - console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName)); + await prompts.log.info(`Using user name from command-line: ${options.userName}`); } if (options.communicationLanguage) { coreConfig.communication_language = options.communicationLanguage; - console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage)); + await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`); } if (options.documentOutputLanguage) { coreConfig.document_output_language = options.documentOutputLanguage; - console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage)); + await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`); } if (options.outputFolder) { coreConfig.output_folder = options.outputFolder; - console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder)); + await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`); } // Load existing config to merge with provided options @@ -818,7 +800,7 @@ class UI { document_output_language: 'English', output_folder: '_bmad-output', }; - console.log(chalk.cyan('Using default configuration (--yes flag)')); + await prompts.log.info('Using default configuration (--yes flag)'); } } else { // Load existing configs first if they exist @@ -839,11 +821,11 @@ class UI { * @returns {Array} Module choices for prompt */ async getModuleChoices(installedModuleIds, customContentConfig = null) { + const color = await prompts.getColor(); const moduleChoices = []; const isNewInstallation = installedModuleIds.size === 0; const customContentItems = []; - const hasCustomContentItems = false; // Add custom content items if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) { @@ -855,7 +837,7 @@ class UI { const customInfo = await customHandler.getCustomInfo(customFile); if (customInfo) { customContentItems.push({ - name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`, + name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`, value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content checked: true, // Default to selected since user chose to provide custom content path: customInfo.path, // Track path to avoid duplicates @@ -883,7 +865,7 @@ class UI { if (!isDuplicate) { allCustomModules.push({ - name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`, + name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`, value: mod.id, checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id), hint: mod.description || undefined, @@ -934,22 +916,20 @@ class UI { ...choicesWithDefaults, { value: '__NONE__', - label: '⚠ None / I changed my mind - skip module installation', + label: '\u26A0 None / I changed my mind - skip module installation', checked: false, }, ]; const selected = await prompts.multiselect({ - message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select modules to install (use arrow keys, space to toggle):', choices: choicesWithSkipOption, required: true, }); // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.'); return []; } @@ -982,8 +962,7 @@ class UI { */ async selectExternalModules(externalModuleChoices, defaultSelections = []) { // Build a message showing available modules - const availableNames = externalModuleChoices.map((c) => c.name).join(', '); - const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`; + const message = 'Select official BMad modules to install (use arrow keys, space to toggle):'; // Mark choices as checked based on defaultSelections const choicesWithDefaults = externalModuleChoices.map((choice) => ({ @@ -1009,9 +988,7 @@ class UI { // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no external modules will be installed.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.'); return []; } @@ -1033,100 +1010,98 @@ class UI { const externalManager = new ExternalModuleManager(); const externalModules = await externalManager.listAvailable(); - // Build grouped options - const groupedOptions = {}; + // Build flat options list with group hints for autocompleteMultiselect + const allOptions = []; const initialValues = []; + const lockedValues = ['core']; + + // Core module is always installed — show it locked at the top + allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' }); + initialValues.push('core'); // Helper to build module entry with proper sorting and selection - const buildModuleEntry = (mod, value) => { + const buildModuleEntry = (mod, value, group) => { const isInstalled = installedModuleIds.has(value); - const isDefault = mod.defaultSelected === true; return { - label: mod.description ? `${mod.name} — ${mod.description}` : mod.name, + label: mod.name, value, - // For sorting: defaultSelected=0, others=1 - sortKey: isDefault ? 0 : 1, - // Pre-select if default selected OR already installed - selected: isDefault || isInstalled, + hint: mod.description || group, + // Pre-select only if already installed (not on fresh install) + selected: isInstalled, }; }; - // Group 1: BMad Core (BMM, BMB) - const coreModules = []; + // Local modules (BMM, BMB, etc.) + const localEntries = []; for (const mod of localModules) { - if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) { - const entry = buildModuleEntry(mod, mod.id); - coreModules.push(entry); + if (!mod.isCustom && mod.id !== 'core') { + const entry = buildModuleEntry(mod, mod.id, 'Local'); + localEntries.push(entry); if (entry.selected) { initialValues.push(mod.id); } } } - // Sort: defaultSelected first, then others - coreModules.sort((a, b) => a.sortKey - b.sortKey); - // Remove sortKey from final entries - if (coreModules.length > 0) { - groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint }))); // Group 2: BMad Official Modules (type: bmad-org) const officialModules = []; for (const mod of externalModules) { if (mod.type === 'bmad-org') { - const entry = buildModuleEntry(mod, mod.code); + const entry = buildModuleEntry(mod, mod.code, 'Official'); officialModules.push(entry); if (entry.selected) { initialValues.push(mod.code); } } } - officialModules.sort((a, b) => a.sortKey - b.sortKey); - if (officialModules.length > 0) { - groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint }))); // Group 3: Community Modules (type: community) const communityModules = []; for (const mod of externalModules) { if (mod.type === 'community') { - const entry = buildModuleEntry(mod, mod.code); + const entry = buildModuleEntry(mod, mod.code, 'Community'); communityModules.push(entry); if (entry.selected) { initialValues.push(mod.code); } } } - communityModules.sort((a, b) => a.sortKey - b.sortKey); - if (communityModules.length > 0) { - groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value })); - } + allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { + // "None" option at the end + label: '\u26A0 None - Skip module installation', + value: '__NONE__', + }); - // Add "None" option at the end - groupedOptions[' '] = [ - { - label: '⚠ None - Skip module installation', - value: '__NONE__', - }, - ]; - - const selected = await prompts.groupMultiselect({ - message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, - options: groupedOptions, + const selected = await prompts.autocompleteMultiselect({ + message: 'Select modules to install:', + options: allOptions, initialValues: initialValues.length > 0 ? initialValues : undefined, + lockedValues, required: true, - selectableGroups: false, + maxItems: allOptions.length, }); // If user selected both "__NONE__" and other items, honor the "None" choice if (selected && selected.includes('__NONE__') && selected.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.')); - console.log(); + await prompts.log.warn('"None" was selected, so no modules will be installed.'); return []; } // Filter out the special '__NONE__' value - return selected ? selected.filter((m) => m !== '__NONE__') : []; + const result = selected ? selected.filter((m) => m !== '__NONE__') : []; + + // Display selected modules as bulleted list + if (result.length > 0) { + const moduleLines = result.map((moduleId) => { + const opt = allOptions.find((o) => o.value === moduleId); + return ` \u2022 ${opt?.label || moduleId}`; + }); + await prompts.log.message('Selected modules:\n' + moduleLines.join('\n')); + } + + return result; } /** @@ -1185,7 +1160,7 @@ class UI { * @param {string} directory - The directory path */ async displayDirectoryInfo(directory) { - console.log(chalk.cyan('\nResolved installation path:'), chalk.bold(directory)); + await prompts.log.info(`Resolved installation path: ${directory}`); const dirExists = await fs.pathExists(directory); if (dirExists) { @@ -1201,12 +1176,10 @@ class UI { const hasBmadInstall = (await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml'))); - console.log( - chalk.gray(`Directory exists and contains ${files.length} item(s)`) + - (hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''), - ); + const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : ''; + await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`); } else { - console.log(chalk.gray('Directory exists and is empty')); + await prompts.log.message('Directory exists and is empty'); } } } @@ -1227,7 +1200,7 @@ class UI { }); if (!proceed) { - console.log(chalk.yellow("\nLet's try again with a different path.\n")); + await prompts.log.warn("Let's try again with a different path."); } return proceed; @@ -1239,7 +1212,7 @@ class UI { }); if (!create) { - console.log(chalk.yellow("\nLet's try again with a different path.\n")); + await prompts.log.warn("Let's try again with a different path."); } return create; @@ -1459,7 +1432,7 @@ class UI { return configs; } catch { // If loading fails, return empty configs - console.warn('Warning: Could not load existing configurations'); + await prompts.log.warn('Could not load existing configurations'); return configs; } } @@ -1590,7 +1563,7 @@ class UI { name: moduleData.name || moduleData.code, }); - console.log(chalk.green(`✓ Confirmed local custom module: ${moduleData.name || moduleData.code}`)); + await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`); } // Ask if user wants to add these to the installation @@ -1656,11 +1629,11 @@ class UI { }; // Ask user about custom modules - console.log(chalk.cyan('\n⚙️ Custom Modules')); + await prompts.log.info('Custom Modules'); if (cachedCustomModules.length > 0) { - console.log(chalk.dim('Found custom modules in your installation:')); + await prompts.log.message('Found custom modules in your installation:'); } else { - console.log(chalk.dim('No custom modules currently installed.')); + await prompts.log.message('No custom modules currently installed.'); } // Build choices dynamically based on whether we have existing modules @@ -1686,14 +1659,14 @@ class UI { case 'keep': { // Keep all existing custom modules result.selectedCustomModules = cachedCustomModules.map((m) => m.id); - console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`)); + await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`); break; } case 'select': { // Let user choose which to keep const selectChoices = cachedCustomModules.map((m) => ({ - name: `${m.name} ${chalk.gray(`(${m.id})`)}`, + name: `${m.name} (${m.id})`, value: m.id, checked: m.checked, })); @@ -1709,16 +1682,14 @@ class UI { ]; const keepModules = await prompts.multiselect({ - message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select custom modules to keep (use arrow keys, space to toggle):', choices: choicesWithSkip, required: true, }); // If user selected both "__NONE__" and other modules, honor the "None" choice if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) { - console.log(); - console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.')); - console.log(); + await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.'); result.selectedCustomModules = []; } else { // Filter out the special '__NONE__' value @@ -1743,13 +1714,13 @@ class UI { case 'remove': { // Remove all custom modules - console.log(chalk.yellow('All custom modules will be removed from the installation')); + await prompts.log.warn('All custom modules will be removed from the installation'); break; } case 'cancel': { // User cancelled - no custom modules - console.log(chalk.dim('No custom modules will be added')); + await prompts.log.message('No custom modules will be added'); break; } } @@ -1782,30 +1753,26 @@ class UI { return true; // Not legacy, proceed } - console.log(''); - console.log(chalk.yellow.bold('⚠️ VERSION WARNING')); - console.log(chalk.yellow('─'.repeat(80))); - + let warningContent; if (installedVersion === 'unknown') { - console.log(chalk.yellow('Unable to detect your installed BMAD version.')); - console.log(chalk.yellow('This appears to be a legacy or unsupported installation.')); + warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.'; } else { - console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`)); - console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).')); + warningContent = + `You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).'; } - console.log(''); - console.log(chalk.dim('For the best experience, we recommend:')); - console.log(chalk.dim(' 1. Delete your current BMAD installation folder')); - console.log(chalk.dim(` (the "${bmadFolderName}/" folder in your project)`)); - console.log(chalk.dim(' 2. Run a fresh installation')); - console.log(''); - console.log(chalk.dim('Benefits of a fresh install:')); - console.log(chalk.dim(' • Cleaner configuration without legacy artifacts')); - console.log(chalk.dim(' • All new features properly configured')); - console.log(chalk.dim(' • Fewer potential conflicts')); - console.log(chalk.yellow('─'.repeat(80))); - console.log(''); + warningContent += + '\n\nFor the best experience, we recommend:\n' + + ' 1. Delete your current BMAD installation folder\n' + + ` (the "${bmadFolderName}/" folder in your project)\n` + + ' 2. Run a fresh installation\n\n' + + 'Benefits of a fresh install:\n' + + ' \u2022 Cleaner configuration without legacy artifacts\n' + + ' \u2022 All new features properly configured\n' + + ' \u2022 Fewer potential conflicts'; + + await prompts.log.warn('VERSION WARNING'); + await prompts.note(warningContent, 'Version Warning'); const proceed = await prompts.select({ message: 'How would you like to proceed?', @@ -1823,11 +1790,10 @@ class UI { }); if (proceed === 'cancel') { - console.log(''); - console.log(chalk.cyan('To do a fresh install:')); - console.log(chalk.dim(` 1. Delete the "${bmadFolderName}/" folder in your project`)); - console.log(chalk.dim(" 2. Run 'bmad install' again")); - console.log(''); + await prompts.note( + `1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again", + 'To do a fresh install', + ); } return proceed === 'proceed'; @@ -1838,41 +1804,34 @@ class UI { * @param {Array} modules - Array of module info objects with version info * @param {Array} availableUpdates - Array of available updates */ - displayModuleVersions(modules, availableUpdates = []) { - console.log(''); - console.log(chalk.cyan.bold('📦 Module Versions')); - console.log(chalk.gray('─'.repeat(80))); - + async displayModuleVersions(modules, availableUpdates = []) { // Group modules by source const builtIn = modules.filter((m) => m.source === 'built-in'); const external = modules.filter((m) => m.source === 'external'); const custom = modules.filter((m) => m.source === 'custom'); const unknown = modules.filter((m) => m.source === 'unknown'); - const displayGroup = (group, title) => { + const lines = []; + const formatGroup = (group, title) => { if (group.length === 0) return; - - console.log(chalk.yellow(`\n${title}`)); - for (const module of group) { - const updateInfo = availableUpdates.find((u) => u.name === module.name); - const versionDisplay = module.version || chalk.gray('unknown'); - + lines.push(title); + for (const mod of group) { + const updateInfo = availableUpdates.find((u) => u.name === mod.name); + const versionDisplay = mod.version || 'unknown'; if (updateInfo) { - console.log( - ` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} → ${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`, - ); + lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`); } else { - console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`); + lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`); } } }; - displayGroup(builtIn, 'Built-in Modules'); - displayGroup(external, 'External Modules (Official)'); - displayGroup(custom, 'Custom Modules'); - displayGroup(unknown, 'Other Modules'); + formatGroup(builtIn, 'Built-in Modules'); + formatGroup(external, 'External Modules (Official)'); + formatGroup(custom, 'Custom Modules'); + formatGroup(unknown, 'Other Modules'); - console.log(''); + await prompts.note(lines.join('\n'), 'Module Versions'); } /** @@ -1885,12 +1844,10 @@ class UI { return []; } - console.log(''); - console.log(chalk.cyan.bold('🔄 Available Updates')); - console.log(chalk.gray('─'.repeat(80))); + await prompts.log.info('Available Updates'); const choices = availableUpdates.map((update) => ({ - name: `${update.name} ${chalk.dim(`(v${update.installedVersion} → v${update.latestVersion})`)}`, + name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`, value: update.name, checked: true, // Default to selecting all updates })); @@ -1916,7 +1873,7 @@ class UI { // Allow specific selection const selected = await prompts.multiselect({ - message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`, + message: 'Select modules to update (use arrow keys, space to toggle):', choices: choices, required: true, }); @@ -1928,34 +1885,29 @@ class UI { * Display status of all installed modules * @param {Object} statusData - Status data with modules, installation info, and available updates */ - displayStatus(statusData) { + async displayStatus(statusData) { const { installation, modules, availableUpdates, bmadDir } = statusData; - console.log(''); - console.log(chalk.cyan.bold('📋 BMAD Status')); - console.log(chalk.gray('─'.repeat(80))); - // Installation info - console.log(chalk.yellow('\nInstallation')); - console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`); - console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`); - console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`); - console.log( - ` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`, - ); + const infoLines = [ + `Version: ${installation.version || 'unknown'}`, + `Location: ${bmadDir}`, + `Installed: ${new Date(installation.installDate).toLocaleDateString()}`, + `Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`, + ]; + + await prompts.note(infoLines.join('\n'), 'BMAD Status'); // Module versions - this.displayModuleVersions(modules, availableUpdates); + await this.displayModuleVersions(modules, availableUpdates); // Update summary if (availableUpdates.length > 0) { - console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`)); - console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`)); + await prompts.log.warn(`${availableUpdates.length} update(s) available`); + await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update'); } else { - console.log(chalk.green.bold('\n✓ All modules are up to date')); + await prompts.log.success('All modules are up to date'); } - - console.log(''); } /** @@ -1964,19 +1916,17 @@ class UI { * @param {Array} preferredIdes - Array of preferred IDE objects * @param {Array} allTools - Array of all tool objects */ - displaySelectedTools(selectedIdes, preferredIdes, allTools) { + async displaySelectedTools(selectedIdes, preferredIdes, allTools) { if (selectedIdes.length === 0) return; const preferredValues = new Set(preferredIdes.map((ide) => ide.value)); - - console.log(''); - console.log(chalk.dim(' Selected tools:')); - for (const ideValue of selectedIdes) { + const toolLines = selectedIdes.map((ideValue) => { const tool = allTools.find((t) => t.value === ideValue); const name = tool?.name || ideValue; - const marker = preferredValues.has(ideValue) ? ' ⭐' : ''; - console.log(chalk.dim(` • ${name}${marker}`)); - } + const marker = preferredValues.has(ideValue) ? ' \u2B50' : ''; + return ` \u2022 ${name}${marker}`; + }); + await prompts.log.message('Selected tools:\n' + toolLines.join('\n')); } }