655 lines
21 KiB
JavaScript
Raw Normal View History

const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler');
const { getSourcePath } = require('../../../lib/project-root');
/**
* Base class for IDE-specific setup
* All IDE handlers should extend this class
*/
class BaseIdeSetup {
constructor(name, displayName = null, preferred = false) {
this.name = name;
this.displayName = displayName || name; // Human-readable name for UI
this.preferred = preferred; // Whether this IDE should be shown in preferred list
this.configDir = null; // Override in subclasses
this.rulesDir = null; // Override in subclasses
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
}
/**
* Set the bmad folder name for placeholder replacement
* @param {string} bmadFolderName - The bmad folder name
*/
setBmadFolderName(bmadFolderName) {
this.bmadFolderName = bmadFolderName;
}
Major Enhancements: - Installation path is now fully configurable, allowing users to specify custom installation directories during setup - Default installation location changed to .bmad (hidden directory) for cleaner project root organization Web Bundle Improvements: - All web bundles (single agent and team) now include party mode support for multi-agent collaboration! - Advanced elicitation capabilities integrated into standalone agents - All bundles enhanced with party mode agent manifests - Added default-party.csv files to bmm, bmgd, and cis module teams - The default party file is what will be used with single agent bundles. teams can customize for different party configurations before web bundling through a setting in the team yaml file - New web bundle outputs for all agents (analyst, architect, dev, pm, sm, tea, tech-writer, ux-designer, game-*, creative-squad) Phase 4 Workflow Updates (In Progress): - Initiated shift to separate phase 4 implementation artifacts from documentation - Phase 4 implementation artifacts (stories, code review, sprint plan, context files) will move to dedicated location outside docs folder - Installer questions and configuration added for artifact path selection - Updated workflow.yaml files for code-review, sprint-planning, story-context, epic-tech-context, and retrospective workflows to support this, but still might require some udpates Additional Changes: - New agent and action command header models for standardization - Enhanced web-bundle-activation-steps fragment - Updated web-bundler.js to support new structure - VS Code settings updated for new .bmad directory - Party mode instructions and workflow enhanced for better orchestration IDE Installer Updates: - Show version number of installer in cli - improved Installer UX - Gemini TOML Improved to have clear loading instructions with @ commands - All tools agent launcher mds improved to use a central file template critical indication isntead of hardcoding in 2 different locations.
2025-11-09 17:39:05 -06:00
/**
* Get the agent command activation header from the central template
2025-12-13 16:22:34 +08:00
* @returns {string} The activation header text
Major Enhancements: - Installation path is now fully configurable, allowing users to specify custom installation directories during setup - Default installation location changed to .bmad (hidden directory) for cleaner project root organization Web Bundle Improvements: - All web bundles (single agent and team) now include party mode support for multi-agent collaboration! - Advanced elicitation capabilities integrated into standalone agents - All bundles enhanced with party mode agent manifests - Added default-party.csv files to bmm, bmgd, and cis module teams - The default party file is what will be used with single agent bundles. teams can customize for different party configurations before web bundling through a setting in the team yaml file - New web bundle outputs for all agents (analyst, architect, dev, pm, sm, tea, tech-writer, ux-designer, game-*, creative-squad) Phase 4 Workflow Updates (In Progress): - Initiated shift to separate phase 4 implementation artifacts from documentation - Phase 4 implementation artifacts (stories, code review, sprint plan, context files) will move to dedicated location outside docs folder - Installer questions and configuration added for artifact path selection - Updated workflow.yaml files for code-review, sprint-planning, story-context, epic-tech-context, and retrospective workflows to support this, but still might require some udpates Additional Changes: - New agent and action command header models for standardization - Enhanced web-bundle-activation-steps fragment - Updated web-bundler.js to support new structure - VS Code settings updated for new .bmad directory - Party mode instructions and workflow enhanced for better orchestration IDE Installer Updates: - Show version number of installer in cli - improved Installer UX - Gemini TOML Improved to have clear loading instructions with @ commands - All tools agent launcher mds improved to use a central file template critical indication isntead of hardcoding in 2 different locations.
2025-11-09 17:39:05 -06:00
*/
async getAgentCommandHeader() {
2025-12-13 16:22:34 +08:00
const headerPath = path.join(getSourcePath(), 'src', 'utility', 'agent-components', 'agent-command-header.md');
return await fs.readFile(headerPath, 'utf8');
Major Enhancements: - Installation path is now fully configurable, allowing users to specify custom installation directories during setup - Default installation location changed to .bmad (hidden directory) for cleaner project root organization Web Bundle Improvements: - All web bundles (single agent and team) now include party mode support for multi-agent collaboration! - Advanced elicitation capabilities integrated into standalone agents - All bundles enhanced with party mode agent manifests - Added default-party.csv files to bmm, bmgd, and cis module teams - The default party file is what will be used with single agent bundles. teams can customize for different party configurations before web bundling through a setting in the team yaml file - New web bundle outputs for all agents (analyst, architect, dev, pm, sm, tea, tech-writer, ux-designer, game-*, creative-squad) Phase 4 Workflow Updates (In Progress): - Initiated shift to separate phase 4 implementation artifacts from documentation - Phase 4 implementation artifacts (stories, code review, sprint plan, context files) will move to dedicated location outside docs folder - Installer questions and configuration added for artifact path selection - Updated workflow.yaml files for code-review, sprint-planning, story-context, epic-tech-context, and retrospective workflows to support this, but still might require some udpates Additional Changes: - New agent and action command header models for standardization - Enhanced web-bundle-activation-steps fragment - Updated web-bundler.js to support new structure - VS Code settings updated for new .bmad directory - Party mode instructions and workflow enhanced for better orchestration IDE Installer Updates: - Show version number of installer in cli - improved Installer UX - Gemini TOML Improved to have clear loading instructions with @ commands - All tools agent launcher mds improved to use a central file template critical indication isntead of hardcoding in 2 different locations.
2025-11-09 17:39:05 -06:00
}
/**
* Main setup method - must be implemented by subclasses
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
throw new Error(`setup() must be implemented by ${this.name} handler`);
}
/**
* Cleanup IDE configuration
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
// Default implementation - can be overridden
if (this.configDir) {
const configPath = path.join(projectDir, this.configDir);
if (await fs.pathExists(configPath)) {
const bmadRulesPath = path.join(configPath, 'bmad');
if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath);
console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
}
}
}
}
feat: Complete BMAD agent creation system with install tooling, references, and field guidance ## Overview This commit represents a complete overhaul of the BMAD agent creation system, establishing clear standards for agent development, installation workflows, and persona design. The changes span documentation, tooling, reference implementations, and field-specific guidance. ## Key Components ### 1. Agent Installation Infrastructure **New CLI Command: `agent-install`** - Interactive agent installation with persona customization - Supports Simple (single YAML), Expert (sidecar files), and Module agents - Template variable processing with Handlebars-style syntax - Automatic compilation from YAML to XML (.md) format - Manifest tracking and IDE integration (Claude Code, Cursor, Windsurf, etc.) - Source preservation in `_cfg/custom/agents/` for reinstallation **Files Created:** - `tools/cli/commands/agent-install.js` - Main CLI command - `tools/cli/lib/agent/compiler.js` - YAML to XML compilation engine - `tools/cli/lib/agent/installer.js` - Installation orchestration - `tools/cli/lib/agent/template-engine.js` - Handlebars template processing **Compiler Features:** - Auto-injects frontmatter, activation, handlers, help/exit menu items - Smart handler inclusion (only includes action/workflow/exec/tmpl handlers actually used) - Proper XML escaping and formatting - Persona name customization (e.g., "Fred the Commit Poet") ### 2. Documentation Overhaul **Deleted Bloated/Outdated Docs (2,651 lines removed):** - Old verbose architecture docs - Redundant pattern files - Outdated workflow guides **Created Focused, Type-Specific Docs:** - `src/modules/bmb/docs/understanding-agent-types.md` - Architecture vs capability distinction - `src/modules/bmb/docs/simple-agent-architecture.md` - Self-contained agents - `src/modules/bmb/docs/expert-agent-architecture.md` - Agents with sidecar files - `src/modules/bmb/docs/module-agent-architecture.md` - Workflow-integrated agents - `src/modules/bmb/docs/agent-compilation.md` - YAML → XML process - `src/modules/bmb/docs/agent-menu-patterns.md` - Menu design patterns - `src/modules/bmb/docs/index.md` - Documentation hub **Net Result:** ~1,930 line reduction while adding MORE value through focused content ### 3. Create-Agent Workflow Enhancements **Critical Persona Field Guidance Added to Step 4:** Explains how the LLM interprets each persona field when the agent activates: - **role** → "What knowledge, skills, and capabilities do I possess?" - **identity** → "What background, experience, and context shape my responses?" - **communication_style** → "What verbal patterns, word choice, quirks, and phrasing do I use?" - **principles** → "What beliefs and operating philosophy drive my choices?" **Key Insight:** `communication_style` should ONLY describe HOW the agent talks, not restate role/identity/principles. The `communication-presets.csv` provides 60 pure communication styles with NO role/identity/principles mixed in. **Files Updated:** - `src/modules/bmb/workflows/create-agent/instructions.md` - Added persona field interpretation guide - `src/modules/bmb/workflows/create-agent/brainstorm-context.md` - Refined to 137 lines - `src/modules/bmb/workflows/create-agent/communication-presets.csv` - 60 styles across 13 categories ### 4. Reference Agent Cleanup **Removed install_config Personality Bloat:** Understanding: Future installer will handle personality customization, so stripped all personality toggles from reference agents. **commit-poet.agent.yaml** (Simple Agent): - BEFORE: 36 personality combinations (3 enthusiasm × 3 depths × 4 styles) = decision fatigue - AFTER: Single concise persona with pure communication style - Changed from verbose conditionals to: "Poetic drama and flair with every turn of a phrase. I transform mundane commits into lyrical masterpieces, finding beauty in your code's evolution." - Reduction: 248 lines → 153 lines (38% reduction) **journal-keeper.agent.yaml** (Expert Agent): - Stripped install_config, simplified communication_style - Shows proper Expert agent structure with sidecar files **security-engineer.agent.yaml & trend-analyst.agent.yaml** (Module Agents): - Added header comments explaining WHY Module Agent (design intent, not just location) - Clarified: Module agents are designed FOR ecosystem integration, not capability-limited **Files Updated:** - `src/modules/bmb/reference/agents/simple-examples/commit-poet.agent.yaml` - `src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper.agent.yaml` - `src/modules/bmb/reference/agents/module-examples/security-engineer.agent.yaml` - `src/modules/bmb/reference/agents/module-examples/trend-analyst.agent.yaml` ### 5. BMM Agent Voice Enhancement **Gave all 9 BMM agents distinct, memorable communication voices:** **Mary (analyst)** - The favorite! Changed from generic "systematic and probing" to: "Treats analysis like a treasure hunt - excited by every clue, thrilled when patterns emerge. Asks questions that spark 'aha!' moments while structuring insights with precision." **Other Notable Voices:** - **John (pm):** "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters." - **Winston (architect):** "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.' Champions boring technology that actually works." - **Amelia (dev):** "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision." - **Bob (sm):** "Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity." - **Sally (ux-designer):** "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair." **Pattern Applied:** Moved behaviors from communication_style to principles, keeping communication_style as PURE verbal patterns. **Files Updated:** - `src/modules/bmm/agents/analyst.agent.yaml` - `src/modules/bmm/agents/pm.agent.yaml` - `src/modules/bmm/agents/architect.agent.yaml` - `src/modules/bmm/agents/dev.agent.yaml` - `src/modules/bmm/agents/sm.agent.yaml` - `src/modules/bmm/agents/tea.agent.yaml` - `src/modules/bmm/agents/tech-writer.agent.yaml` - `src/modules/bmm/agents/ux-designer.agent.yaml` - `src/modules/bmm/agents/frame-expert.agent.yaml` ### 6. Linting Fixes **ESLint Compliance:** - Replaced all `'utf-8'` with `'utf8'` (unicorn/text-encoding-identifier-case) - Changed `variables.hasOwnProperty(varName)` to `Object.hasOwn(variables, varName)` (unicorn/prefer-object-has-own) - Replaced `JSON.parse(JSON.stringify(...))` with `structuredClone(...)` (unicorn/prefer-structured-clone) - Fixed empty YAML mapping values in sample files **Files Fixed:** - 7 JavaScript files across agent tooling (compiler, installer, commands, IDE integration) - 1 YAML sample file ## Architecture Decisions ### Agent Types Are About Architecture, Not Capability - **Simple:** Self-contained in single YAML (NOT limited in capability) - **Expert:** Includes sidecar files (templates, docs, etc.) - **Module:** Designed for BMAD ecosystem integration (workflows, cross-agent coordination) ### Persona Field Separation Critical for LLM Interpretation The LLM needs distinct fields to understand its role: - Mixing role/identity/principles into communication_style confuses the persona - Pure communication styles (from communication-presets.csv) have ZERO role/identity/principles content - Example DON'T: "Experienced analyst who uses systematic approaches..." (mixing identity + style) - Example DO: "Systematic and probing. Structures findings hierarchically." (pure style) ### Install-Time vs Runtime Configuration - Template variables ({{var}}) resolve at compile-time - Runtime variables ({user_name}, {bmad_folder}) resolve when agent activates - Future installer will handle personality customization, so agents should ship with single default persona ## Testing - All linting passes (ESLint with max-warnings=0) - Agent compilation tested with commit-poet, journal-keeper examples - Install workflow validated with Simple and Expert agent types - Manifest tracking and IDE integration verified ## Impact This establishes BMAD as having a complete, production-ready agent creation and installation system with: - Clear documentation for all agent types - Automated compilation and installation - Strong persona design guidance - Reference implementations showing best practices - Distinct, memorable agent voices throughout BMM module Co-Authored-By: BMad Builder <builder@bmad.dev> Co-Authored-By: Mary the Analyst <analyst@bmad.dev> Co-Authored-By: Paige the Tech Writer <tech-writer@bmad.dev>
2025-11-17 22:25:15 -06:00
/**
* Install a custom agent launcher - subclasses should override
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created command, or null if not supported
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
// Default implementation - subclasses can override
return null;
}
/**
* Detect whether this IDE already has configuration in the project
* Subclasses can override for custom logic
* @param {string} projectDir - Project directory
* @returns {boolean}
*/
async detect(projectDir) {
const pathsToCheck = [];
if (this.configDir) {
pathsToCheck.push(path.join(projectDir, this.configDir));
}
if (this.configFile) {
pathsToCheck.push(path.join(projectDir, this.configFile));
}
if (Array.isArray(this.detectionPaths)) {
for (const candidate of this.detectionPaths) {
if (!candidate) continue;
const resolved = path.isAbsolute(candidate) ? candidate : path.join(projectDir, candidate);
pathsToCheck.push(resolved);
}
}
for (const candidate of pathsToCheck) {
if (await fs.pathExists(candidate)) {
return true;
}
}
return false;
}
/**
* Get list of agents from BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @returns {Array} List of agent files
*/
async getAgents(bmadDir) {
const agents = [];
// Get core agents
const coreAgentsPath = path.join(bmadDir, 'core', 'agents');
if (await fs.pathExists(coreAgentsPath)) {
const coreAgents = await this.scanDirectory(coreAgentsPath, '.md');
agents.push(
...coreAgents.map((a) => ({
...a,
module: 'core',
})),
);
}
// Get module agents
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') {
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
if (await fs.pathExists(moduleAgentsPath)) {
const moduleAgents = await this.scanDirectory(moduleAgentsPath, '.md');
agents.push(
...moduleAgents.map((a) => ({
...a,
module: entry.name,
})),
);
}
}
}
// Get standalone agents from bmad/agents/ directory
const standaloneAgentsDir = path.join(bmadDir, 'agents');
if (await fs.pathExists(standaloneAgentsDir)) {
const agentDirs = await fs.readdir(standaloneAgentsDir, { withFileTypes: true });
for (const agentDir of agentDirs) {
if (!agentDir.isDirectory()) continue;
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
const agentFiles = await fs.readdir(agentDirPath);
for (const file of agentFiles) {
if (!file.endsWith('.md')) continue;
if (file.includes('.customize.')) continue;
const filePath = path.join(agentDirPath, file);
const content = await fs.readFile(filePath, 'utf8');
if (content.includes('localskip="true"')) continue;
agents.push({
name: file.replace('.md', ''),
path: filePath,
relativePath: path.relative(standaloneAgentsDir, filePath),
filename: file,
module: 'standalone', // Mark as standalone agent
});
}
}
}
return agents;
}
/**
* Get list of tasks from BMAD installation
* @param {string} bmadDir - BMAD installation directory
2025-10-26 19:38:38 -05:00
* @param {boolean} standaloneOnly - If true, only return standalone tasks
* @returns {Array} List of task files
*/
2025-10-26 19:38:38 -05:00
async getTasks(bmadDir, standaloneOnly = false) {
const tasks = [];
2025-10-26 19:38:38 -05:00
// Get core tasks (scan for both .md and .xml)
const coreTasksPath = path.join(bmadDir, 'core', 'tasks');
if (await fs.pathExists(coreTasksPath)) {
2025-10-26 19:38:38 -05:00
const coreTasks = await this.scanDirectoryWithStandalone(coreTasksPath, ['.md', '.xml']);
tasks.push(
...coreTasks.map((t) => ({
...t,
module: 'core',
})),
);
}
// Get module tasks
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') {
const moduleTasksPath = path.join(bmadDir, entry.name, 'tasks');
if (await fs.pathExists(moduleTasksPath)) {
2025-10-26 19:38:38 -05:00
const moduleTasks = await this.scanDirectoryWithStandalone(moduleTasksPath, ['.md', '.xml']);
tasks.push(
...moduleTasks.map((t) => ({
...t,
module: entry.name,
})),
);
}
}
}
2025-10-26 19:38:38 -05:00
// Filter by standalone if requested
if (standaloneOnly) {
return tasks.filter((t) => t.standalone === true);
}
return tasks;
}
/**
2025-10-26 19:38:38 -05:00
* Get list of tools from BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @param {boolean} standaloneOnly - If true, only return standalone tools
* @returns {Array} List of tool files
*/
async getTools(bmadDir, standaloneOnly = false) {
const tools = [];
// Get core tools (scan for both .md and .xml)
const coreToolsPath = path.join(bmadDir, 'core', 'tools');
if (await fs.pathExists(coreToolsPath)) {
const coreTools = await this.scanDirectoryWithStandalone(coreToolsPath, ['.md', '.xml']);
tools.push(
...coreTools.map((t) => ({
...t,
module: 'core',
})),
);
}
// Get module tools
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') {
const moduleToolsPath = path.join(bmadDir, entry.name, 'tools');
if (await fs.pathExists(moduleToolsPath)) {
const moduleTools = await this.scanDirectoryWithStandalone(moduleToolsPath, ['.md', '.xml']);
tools.push(
...moduleTools.map((t) => ({
...t,
module: entry.name,
})),
);
}
}
}
// Filter by standalone if requested
if (standaloneOnly) {
return tools.filter((t) => t.standalone === true);
}
return tools;
}
/**
* Get list of workflows from BMAD installation
* @param {string} bmadDir - BMAD installation directory
* @param {boolean} standaloneOnly - If true, only return standalone workflows
* @returns {Array} List of workflow files
*/
async getWorkflows(bmadDir, standaloneOnly = false) {
const workflows = [];
// Get core workflows
const coreWorkflowsPath = path.join(bmadDir, 'core', 'workflows');
if (await fs.pathExists(coreWorkflowsPath)) {
const coreWorkflows = await this.findWorkflowYamlFiles(coreWorkflowsPath);
workflows.push(
...coreWorkflows.map((w) => ({
...w,
module: 'core',
})),
);
}
// Get module workflows
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== 'core' && entry.name !== '_cfg' && entry.name !== 'agents') {
const moduleWorkflowsPath = path.join(bmadDir, entry.name, 'workflows');
if (await fs.pathExists(moduleWorkflowsPath)) {
const moduleWorkflows = await this.findWorkflowYamlFiles(moduleWorkflowsPath);
workflows.push(
...moduleWorkflows.map((w) => ({
...w,
module: entry.name,
})),
);
}
}
}
// Filter by standalone if requested
if (standaloneOnly) {
return workflows.filter((w) => w.standalone === true);
}
return workflows;
}
/**
* Recursively find workflow.yaml files
* @param {string} dir - Directory to search
* @returns {Array} List of workflow file info objects
*/
async findWorkflowYamlFiles(dir) {
const workflows = [];
if (!(await fs.pathExists(dir))) {
return workflows;
}
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Recursively search subdirectories
const subWorkflows = await this.findWorkflowYamlFiles(fullPath);
workflows.push(...subWorkflows);
} else if (entry.isFile() && entry.name === 'workflow.yaml') {
// Read workflow.yaml to get name and standalone property
try {
2025-12-13 18:35:07 +08:00
const yaml = require('yaml');
2025-10-26 19:38:38 -05:00
const content = await fs.readFile(fullPath, 'utf8');
const workflowData = yaml.parse(content);
2025-10-26 19:38:38 -05:00
if (workflowData && workflowData.name) {
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
});
}
} catch {
// Skip invalid workflow files
}
}
}
return workflows;
}
/**
* Scan a directory for files with specific extension(s)
* @param {string} dir - Directory to scan
2025-10-26 19:38:38 -05:00
* @param {string|Array<string>} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
* @returns {Array} List of file info objects
*/
async scanDirectory(dir, ext) {
const files = [];
if (!(await fs.pathExists(dir))) {
return files;
}
2025-10-26 19:38:38 -05:00
// Normalize ext to array
const extensions = Array.isArray(ext) ? ext : [ext];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Recursively scan subdirectories
const subFiles = await this.scanDirectory(fullPath, ext);
files.push(...subFiles);
2025-10-26 19:38:38 -05:00
} else if (entry.isFile()) {
// Check if file matches any of the extensions
const matchedExt = extensions.find((e) => entry.name.endsWith(e));
if (matchedExt) {
files.push({
name: path.basename(entry.name, matchedExt),
path: fullPath,
relativePath: path.relative(dir, fullPath),
filename: entry.name,
});
}
}
}
return files;
}
/**
* Scan a directory for files with specific extension(s) and check standalone attribute
* @param {string} dir - Directory to scan
* @param {string|Array<string>} ext - File extension(s) to match (e.g., '.md' or ['.md', '.xml'])
* @returns {Array} List of file info objects with standalone property
*/
async scanDirectoryWithStandalone(dir, ext) {
const files = [];
if (!(await fs.pathExists(dir))) {
return files;
}
// Normalize ext to array
const extensions = Array.isArray(ext) ? ext : [ext];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Recursively scan subdirectories
const subFiles = await this.scanDirectoryWithStandalone(fullPath, ext);
files.push(...subFiles);
} else if (entry.isFile()) {
// Check if file matches any of the extensions
const matchedExt = extensions.find((e) => entry.name.endsWith(e));
if (matchedExt) {
// Read file content to check for standalone attribute
let standalone = false;
try {
const content = await fs.readFile(fullPath, 'utf8');
// Check for standalone="true" in XML files
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;
} else if (entry.name.endsWith('.md')) {
// Check for standalone: true in YAML frontmatter
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
2025-12-13 18:35:07 +08:00
const yaml = require('yaml');
2025-10-26 19:38:38 -05:00
try {
const frontmatter = yaml.parse(frontmatterMatch[1]);
2025-10-26 19:38:38 -05:00
standalone = frontmatter.standalone === true;
} catch {
// Ignore YAML parse errors
}
}
}
} catch {
// If we can't read the file, assume not standalone
standalone = false;
}
files.push({
name: path.basename(entry.name, matchedExt),
path: fullPath,
relativePath: path.relative(dir, fullPath),
filename: entry.name,
standalone: standalone,
});
}
}
}
return files;
}
/**
* Create IDE command/rule file from agent or task
* @param {string} content - File content
* @param {Object} metadata - File metadata
* @param {string} projectDir - The actual project directory path
* @returns {string} Processed content
*/
processContent(content, metadata = {}, projectDir = null) {
// Replace placeholders
let processed = content;
// Inject activation block for agent files FIRST (before replacements)
if (metadata.name && content.includes('<agent')) {
processed = this.xmlHandler.injectActivationSimple(processed, metadata);
}
2025-10-15 21:17:09 -05:00
// Only replace {project-root} if a specific projectDir is provided
// Otherwise leave the placeholder intact
2025-10-02 21:45:59 -05:00
// Note: Don't add trailing slash - paths in source include leading slash
2025-10-15 21:17:09 -05:00
if (projectDir) {
processed = processed.replaceAll('{project-root}', projectDir);
}
processed = processed.replaceAll('{module}', metadata.module || 'core');
processed = processed.replaceAll('{agent}', metadata.name || '');
processed = processed.replaceAll('{task}', metadata.name || '');
return processed;
}
/**
* Ensure directory exists
* @param {string} dirPath - Directory path
*/
async ensureDir(dirPath) {
await fs.ensureDir(dirPath);
}
/**
2025-12-13 16:22:34 +08:00
* Write file with content (replaces _bmad placeholder)
* @param {string} filePath - File path
* @param {string} content - File content
*/
async writeFile(filePath, content) {
2025-12-13 16:22:34 +08:00
// Replace _bmad placeholder if present
if (typeof content === 'string' && content.includes('_bmad')) {
content = content.replaceAll('_bmad', this.bmadFolderName);
}
2025-12-13 16:22:34 +08:00
// Replace escape sequence _bmad with literal _bmad
if (typeof content === 'string' && content.includes('_bmad')) {
content = content.replaceAll('_bmad', '_bmad');
}
await this.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content, 'utf8');
}
/**
2025-12-13 16:22:34 +08:00
* Copy file from source to destination (replaces _bmad placeholder in text files)
* @param {string} source - Source file path
* @param {string} dest - Destination file path
*/
async copyFile(source, dest) {
// List of text file extensions that should have placeholder replacement
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
const ext = path.extname(source).toLowerCase();
await this.ensureDir(path.dirname(dest));
// Check if this is a text file that might contain placeholders
if (textExtensions.includes(ext)) {
try {
// Read the file content
let content = await fs.readFile(source, 'utf8');
2025-12-13 16:22:34 +08:00
// Replace _bmad placeholder with actual folder name
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', this.bmadFolderName);
}
2025-12-13 16:22:34 +08:00
// Replace escape sequence _bmad with literal _bmad
if (content.includes('_bmad')) {
content = content.replaceAll('_bmad', '_bmad');
}
// Write to dest with replaced content
await fs.writeFile(dest, content, 'utf8');
} catch {
// If reading as text fails, fall back to regular copy
await fs.copy(source, dest, { overwrite: true });
}
} else {
// Binary file or other file type - just copy directly
await fs.copy(source, dest, { overwrite: true });
}
}
/**
* Check if path exists
* @param {string} pathToCheck - Path to check
* @returns {boolean} True if path exists
*/
async exists(pathToCheck) {
return await fs.pathExists(pathToCheck);
}
/**
* Alias for exists method
* @param {string} pathToCheck - Path to check
* @returns {boolean} True if path exists
*/
async pathExists(pathToCheck) {
return await fs.pathExists(pathToCheck);
}
/**
* Read file content
* @param {string} filePath - File path
* @returns {string} File content
*/
async readFile(filePath) {
return await fs.readFile(filePath, 'utf8');
}
/**
* Format name as title
* @param {string} name - Name to format
* @returns {string} Formatted title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Flatten a relative path to a single filename for flat slash command naming
* Example: 'module/agents/name.md' -> 'bmad-module-agents-name.md'
* Used by IDEs that ignore directory structure for slash commands (e.g., Antigravity, Codex)
* @param {string} relativePath - Relative path to flatten
* @returns {string} Flattened filename with 'bmad-' prefix
*/
flattenFilename(relativePath) {
const sanitized = relativePath.replaceAll(/[/\\]/g, '-');
return `bmad-${sanitized}`;
}
/**
* Create agent configuration file
* @param {string} bmadDir - BMAD installation directory
* @param {Object} agent - Agent information
*/
async createAgentConfig(bmadDir, agent) {
const agentConfigDir = path.join(bmadDir, '_cfg', 'agents');
await this.ensureDir(agentConfigDir);
// Load agent config template
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
const templateContent = await this.readFile(templatePath);
const configContent = `# Agent Config: ${agent.name}
${templateContent}`;
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
await this.writeFile(configPath, configContent);
}
}
module.exports = { BaseIdeSetup };