From e6f911d7910d2c825b2d742be93253dc85cf9afe Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Sat, 13 Dec 2025 14:06:35 +0800 Subject: [PATCH] remove dead and unused functionality - the web bundler will be replaced --- src/core/module.yaml | 21 +- src/modules/bmb/module.yaml | 2 - src/modules/bmm/module.yaml | 9 +- src/modules/bmm/tasks/daily-standup.xml | 85 -- src/modules/cis/module.yaml | 3 - tools/cli/bundlers/bundle-web.js | 179 --- tools/cli/bundlers/test-analyst.js | 28 - tools/cli/bundlers/test-bundler.js | 118 -- tools/cli/bundlers/web-bundler.js | 1754 ----------------------- tools/cli/lib/replace-project-root.js | 239 --- tools/cli/regenerate-manifests.js | 27 - tools/cli/test-yaml-builder.js | 43 - tools/flattener/ignoreRules.js | 2 +- 13 files changed, 12 insertions(+), 2498 deletions(-) delete mode 100644 src/modules/bmm/tasks/daily-standup.xml delete mode 100755 tools/cli/bundlers/bundle-web.js delete mode 100644 tools/cli/bundlers/test-analyst.js delete mode 100755 tools/cli/bundlers/test-bundler.js delete mode 100644 tools/cli/bundlers/web-bundler.js delete mode 100644 tools/cli/lib/replace-project-root.js delete mode 100644 tools/cli/regenerate-manifests.js delete mode 100644 tools/cli/test-yaml-builder.js diff --git a/src/core/module.yaml b/src/core/module.yaml index 22712f9a..af89aaa0 100644 --- a/src/core/module.yaml +++ b/src/core/module.yaml @@ -2,31 +2,26 @@ header: "BMADโ„ข Core Configuration" subheader: "Configure the core settings for your BMADโ„ข installation.\nThese settings will be used across all modules and agents." user_name: - prompt: "What shall the agents call you?" + prompt: "What shall the agents call you (TIP: Use a team name if using with a group)?" default: "BMad" result: "{value}" communication_language: - prompt: "Preferred Chat Language/Style? (English, Mandarin, English Pirate, etc...)" + prompt: "Preferred chat language/style? (English, Mandarin, English Pirate, etc...)" default: "English" result: "{value}" document_output_language: - prompt: "Preferred Document Output Language?" + prompt: "Preferred document output language?" default: "{communication_language}" result: "{value}" +output_folder: + prompt: "Where should AI generated artifacts be saved across all modules?" + default: "docs" + result: "{project-root}/{value}" + agent_sidecar_folder: prompt: "Where should users agent sidecar memory folders be stored?" default: ".bmad-user-memory" result: "{project-root}/{value}" - -output_folder: - prompt: "Where should AI Generated Artifacts be saved across all modules?" - default: "docs" - result: "{project-root}/{value}" - -install_user_docs: - prompt: "Install user documentation and optimized agent intelligence to each selected modules docs folder?" - default: true - result: "{value}" diff --git a/src/modules/bmb/module.yaml b/src/modules/bmb/module.yaml index 1329cd63..77dc1ce3 100644 --- a/src/modules/bmb/module.yaml +++ b/src/modules/bmb/module.yaml @@ -11,8 +11,6 @@ subheader: "Configure the settings for the BoMB Factory!\nThe agent, workflow an ## user_name ## communication_language ## output_folder -## install_user_docs -## kb_install custom_stand_alone_location: prompt: "Where do custom agents and workflows get stored?" diff --git a/src/modules/bmm/module.yaml b/src/modules/bmm/module.yaml index ed988217..2dd59d28 100644 --- a/src/modules/bmm/module.yaml +++ b/src/modules/bmm/module.yaml @@ -11,8 +11,6 @@ subheader: "Agent and Workflow Configuration for this module" ## user_name ## communication_language ## output_folder -## install_user_docs -## kb_install project_name: prompt: "What is the title of your project you will be working on?" @@ -21,9 +19,8 @@ project_name: user_skill_level: prompt: - - "What is your technical experience level?" - - "This affects how agents explain concepts to you (NOT document content)." - - "Documents are always concise for LLM efficiency." + - "What is your development experience level?" + - "This affects how agents explain concepts in chat." default: "intermediate" result: "{value}" single-select: @@ -35,7 +32,7 @@ user_skill_level: label: "Expert - Deep technical knowledge, be direct and technical" sprint_artifacts: - prompt: "Where should Sprint Artifacts be stored (sprint status, stories, story context, temp context, etc...)?" + prompt: "Where should sprint artifacts be stored (sprint status, stories, retrospectives)?" default: "{output_folder}/sprint-artifacts" result: "{project-root}/{value}" diff --git a/src/modules/bmm/tasks/daily-standup.xml b/src/modules/bmm/tasks/daily-standup.xml deleted file mode 100644 index d41c362c..00000000 --- a/src/modules/bmm/tasks/daily-standup.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER - DO NOT skip steps or change the sequence - HALT immediately when halt-conditions are met - Each action tag within a step tag is a REQUIRED action to complete that step - Sections outside flow (validation, output, critical-context) provide essential context - review and apply throughout execution - - - - Check for stories folder at {project-root}{output_folder}/stories/ - Find current story by identifying highest numbered story file - Read story status (In Progress, Ready for Review, etc.) - Extract agent notes from Dev Agent Record, TEA Results, PO Notes sections - Check for next story references from epics - Identify blockers from story sections - - - - - ๐Ÿƒ DAILY STANDUP - Story-{{number}}: {{title}} - - Current Sprint Status: - - Active Story: story-{{number}} ({{status}} - {{percentage}}% complete) - - Next in Queue: story-{{next-number}}: {{next-title}} - - Blockers: {{blockers-from-story}} - - Team assembled based on story participants: - {{ List Agents from {project-root}/bmad/_cfg/agent-manifest.csv }} - - - - - Each agent provides three items referencing real story data - What I see: Their perspective on current work, citing story sections (1-2 sentences) - What concerns me: Issues from their domain or story blockers (1-2 sentences) - What I suggest: Actionable recommendations for progress (1-2 sentences) - - - - - ๐Ÿ“‹ STANDUP SUMMARY: - Key Items from Story File: - - {{completion-percentage}}% complete ({{tasks-complete}}/{{total-tasks}} tasks) - - Blocker: {{main-blocker}} - - Next: {{next-story-reference}} - - Action Items: - - {{agent}}: {{action-item}} - - {{agent}}: {{action-item}} - - {{agent}}: {{action-item}} - - Need extended discussion? Use *party-mode for detailed breakout. - - - - - - - Primary: Sarah (PO), Mary (Analyst), Winston (Architect) - Secondary: Murat (TEA), James (Dev) - - - Primary: Sarah (PO), Bob (SM), James (Dev) - Secondary: Murat (TEA) - - - Primary: Winston (Architect), James (Dev), Murat (TEA) - Secondary: Sarah (PO) - - - Primary: James (Dev), Murat (TEA), Winston (Architect) - Secondary: Sarah (PO) - - - - - This task extends party-mode with agile-specific structure - Time-box responses (standup = brief) - Focus on actionable items from real story data when available - End with clear next steps - No deep dives (suggest breakout if needed) - If no stories folder detected, run general standup format - - \ No newline at end of file diff --git a/src/modules/cis/module.yaml b/src/modules/cis/module.yaml index cd4cdfb2..b188b0ad 100644 --- a/src/modules/cis/module.yaml +++ b/src/modules/cis/module.yaml @@ -10,6 +10,3 @@ subheader: "No Configuration needed - uses Core Config only." ## user_name ## communication_language ## output_folder -## install_user_docs -## kb_install - diff --git a/tools/cli/bundlers/bundle-web.js b/tools/cli/bundlers/bundle-web.js deleted file mode 100755 index 8bb84868..00000000 --- a/tools/cli/bundlers/bundle-web.js +++ /dev/null @@ -1,179 +0,0 @@ -const { WebBundler } = require('./web-bundler'); -const chalk = require('chalk'); -const { program } = require('commander'); -const path = require('node:path'); -const fs = require('fs-extra'); - -program.name('bundle-web').description('Generate web bundles for BMAD agents and teams').version('1.0.0'); - -program - .command('all') - .description('Bundle all modules') - .option('-o, --output ', 'Output directory', 'web-bundles') - .action(async (options) => { - try { - const bundler = new WebBundler(null, options.output); - await bundler.bundleAll(); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('rebundle') - .description('Clean and rebundle all modules') - .option('-o, --output ', 'Output directory', 'web-bundles') - .action(async (options) => { - try { - // Clean output directory first - const outputDir = path.isAbsolute(options.output) ? options.output : path.join(process.cwd(), options.output); - - if (await fs.pathExists(outputDir)) { - console.log(chalk.cyan(`๐Ÿงน Cleaning ${options.output}...`)); - await fs.emptyDir(outputDir); - } - - // Bundle all - const bundler = new WebBundler(null, options.output); - await bundler.bundleAll(); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('module ') - .description('Bundle a specific module') - .option('-o, --output ', 'Output directory', 'web-bundles') - .action(async (moduleName, options) => { - try { - const bundler = new WebBundler(null, options.output); - const result = await bundler.bundleModule(moduleName); - - if (result.agents.length === 0 && result.teams.length === 0) { - console.log(chalk.yellow(`No agents or teams found in module: ${moduleName}`)); - } else { - console.log(chalk.green(`\nโœจ Successfully bundled ${result.agents.length} agents and ${result.teams.length} teams`)); - } - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('agent ') - .description('Bundle a specific agent') - .option('-o, --output ', 'Output directory', 'web-bundles') - .action(async (moduleName, agentFile, options) => { - try { - const bundler = new WebBundler(null, options.output); - - // Ensure .md extension - if (!agentFile.endsWith('.md')) { - agentFile += '.md'; - } - - // Pre-discover module for complete manifests - await bundler.preDiscoverModule(moduleName); - - await bundler.bundleAgent(moduleName, agentFile, false); - console.log(chalk.green(`\nโœจ Successfully bundled agent: ${agentFile}`)); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('team ') - .description('Bundle a specific team') - .option('-o, --output ', 'Output directory', 'web-bundles') - .action(async (moduleName, teamFile, options) => { - try { - const bundler = new WebBundler(null, options.output); - - // Ensure .yaml or .yml extension - if (!teamFile.endsWith('.yaml') && !teamFile.endsWith('.yml')) { - teamFile += '.yaml'; - } - - // Pre-discover module for complete manifests - await bundler.preDiscoverModule(moduleName); - - await bundler.bundleTeam(moduleName, teamFile); - console.log(chalk.green(`\nโœจ Successfully bundled team: ${teamFile}`)); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('list') - .description('List available modules and agents') - .action(async () => { - try { - const bundler = new WebBundler(); - const modules = await bundler.discoverModules(); - - console.log(chalk.cyan.bold('\n๐Ÿ“ฆ Available Modules:\n')); - - for (const module of modules) { - console.log(chalk.bold(` ${module}/`)); - - const modulePath = path.join(bundler.modulesPath, module); - const agents = await bundler.discoverAgents(modulePath); - const teams = await bundler.discoverTeams(modulePath); - - if (agents.length > 0) { - console.log(chalk.gray(' agents/')); - for (const agent of agents) { - console.log(chalk.gray(` - ${agent}`)); - } - } - - if (teams.length > 0) { - console.log(chalk.gray(' teams/')); - for (const team of teams) { - console.log(chalk.gray(` - ${team}`)); - } - } - } - - console.log(''); - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -program - .command('clean') - .description('Remove all web bundles') - .action(async () => { - try { - const fs = require('fs-extra'); - const outputDir = path.join(process.cwd(), 'web-bundles'); - - if (await fs.pathExists(outputDir)) { - await fs.remove(outputDir); - console.log(chalk.green('โœ“ Web bundles directory cleaned')); - } else { - console.log(chalk.yellow('Web bundles directory does not exist')); - } - } catch (error) { - console.error(chalk.red('Error:'), error.message); - process.exit(1); - } - }); - -// Parse command line arguments -program.parse(process.argv); - -// Show help if no command provided -if (process.argv.slice(2).length === 0) { - program.outputHelp(); -} diff --git a/tools/cli/bundlers/test-analyst.js b/tools/cli/bundlers/test-analyst.js deleted file mode 100644 index 88c75954..00000000 --- a/tools/cli/bundlers/test-analyst.js +++ /dev/null @@ -1,28 +0,0 @@ -const { WebBundler } = require('./web-bundler'); -const chalk = require('chalk'); -const path = require('node:path'); - -async function testAnalystBundle() { - console.log(chalk.cyan.bold('\n๐Ÿงช Testing Analyst Agent Bundle\n')); - - try { - const bundler = new WebBundler(); - - // Load web activation first - await bundler.loadWebActivation(); - - // Bundle just the analyst agent from bmm module - // Only bundle the analyst for testing - const agentPath = path.join(bundler.modulesPath, 'bmm', 'agents', 'analyst.md'); - await bundler.bundleAgent('bmm', 'analyst.md'); - - console.log(chalk.green.bold('\nโœ… Test completed successfully!\n')); - } catch (error) { - console.error(chalk.red('\nโŒ Test failed:'), error.message); - console.error(error.stack); - process.exit(1); - } -} - -// Run test -testAnalystBundle(); diff --git a/tools/cli/bundlers/test-bundler.js b/tools/cli/bundlers/test-bundler.js deleted file mode 100755 index 6e17cc2e..00000000 --- a/tools/cli/bundlers/test-bundler.js +++ /dev/null @@ -1,118 +0,0 @@ -const { WebBundler } = require('./web-bundler'); -const chalk = require('chalk'); -const fs = require('fs-extra'); -const path = require('node:path'); - -async function testWebBundler() { - console.log(chalk.cyan.bold('\n๐Ÿงช Testing Web Bundler\n')); - - const bundler = new WebBundler(); - let passedTests = 0; - let failedTests = 0; - - // Test 1: Load web activation - try { - await bundler.loadWebActivation(); - console.log(chalk.green('โœ“ Web activation loaded successfully')); - passedTests++; - } catch (error) { - console.error(chalk.red('โœ— Failed to load web activation:'), error.message); - failedTests++; - } - - // Test 2: Discover modules - try { - const modules = await bundler.discoverModules(); - console.log(chalk.green(`โœ“ Discovered ${modules.length} modules:`, modules.join(', '))); - passedTests++; - } catch (error) { - console.error(chalk.red('โœ— Failed to discover modules:'), error.message); - failedTests++; - } - - // Test 3: Bundle analyst agent - try { - const result = await bundler.bundleAgent('bmm', 'analyst.md'); - - // Check if bundle was created - const bundlePath = path.join(bundler.outputDir, 'bmm', 'agents', 'analyst.xml'); - if (await fs.pathExists(bundlePath)) { - const content = await fs.readFile(bundlePath, 'utf8'); - - // Validate bundle structure - const hasAgent = content.includes(''); - const activationBeforePersona = content.indexOf(''); - const hasManifests = - content.includes('') && content.includes(''); - const hasDependencies = content.includes(''); - - console.log(chalk.green('โœ“ Analyst bundle created successfully')); - console.log(chalk.gray(` - Has agent tag: ${hasAgent ? 'โœ“' : 'โœ—'}`)); - console.log(chalk.gray(` - Has activation: ${hasActivation ? 'โœ“' : 'โœ—'}`)); - console.log(chalk.gray(` - Has persona: ${hasPersona ? 'โœ“' : 'โœ—'}`)); - console.log(chalk.gray(` - Activation before persona: ${activationBeforePersona ? 'โœ“' : 'โœ—'}`)); - console.log(chalk.gray(` - Has manifests: ${hasManifests ? 'โœ“' : 'โœ—'}`)); - console.log(chalk.gray(` - Has dependencies: ${hasDependencies ? 'โœ“' : 'โœ—'}`)); - - if (hasAgent && hasActivation && hasPersona && activationBeforePersona && hasManifests && hasDependencies) { - passedTests++; - } else { - console.error(chalk.red('โœ— Bundle structure validation failed')); - failedTests++; - } - } else { - console.error(chalk.red('โœ— Bundle file not created')); - failedTests++; - } - } catch (error) { - console.error(chalk.red('โœ— Failed to bundle analyst agent:'), error.message); - failedTests++; - } - - // Test 4: Bundle a different agent (architect which exists) - try { - const result = await bundler.bundleAgent('bmm', 'architect.md'); - const bundlePath = path.join(bundler.outputDir, 'bmm', 'agents', 'architect.xml'); - - if (await fs.pathExists(bundlePath)) { - console.log(chalk.green('โœ“ Architect bundle created successfully')); - passedTests++; - } else { - console.error(chalk.red('โœ— Architect bundle file not created')); - failedTests++; - } - } catch (error) { - console.error(chalk.red('โœ— Failed to bundle architect agent:'), error.message); - failedTests++; - } - - // Test 5: Bundle all agents in a module - try { - const results = await bundler.bundleModule('bmm'); - console.log(chalk.green(`โœ“ Bundled ${results.agents.length} agents from bmm module`)); - passedTests++; - } catch (error) { - console.error(chalk.red('โœ— Failed to bundle bmm module:'), error.message); - failedTests++; - } - - // Summary - console.log(chalk.bold('\n๐Ÿ“Š Test Results:')); - console.log(chalk.green(` Passed: ${passedTests}`)); - console.log(chalk.red(` Failed: ${failedTests}`)); - - if (failedTests === 0) { - console.log(chalk.green.bold('\nโœ… All tests passed!\n')); - } else { - console.log(chalk.red.bold(`\nโŒ ${failedTests} test(s) failed\n`)); - process.exit(1); - } -} - -// Run tests -testWebBundler().catch((error) => { - console.error(chalk.red('Fatal error:'), error); - process.exit(1); -}); diff --git a/tools/cli/bundlers/web-bundler.js b/tools/cli/bundlers/web-bundler.js deleted file mode 100644 index f0d10715..00000000 --- a/tools/cli/bundlers/web-bundler.js +++ /dev/null @@ -1,1754 +0,0 @@ -const path = require('node:path'); -const fs = require('fs-extra'); -const chalk = require('chalk'); -const yaml = require('js-yaml'); -const { DependencyResolver } = require('../installers/lib/core/dependency-resolver'); -const { XmlHandler } = require('../lib/xml-handler'); -const { YamlXmlBuilder } = require('../lib/yaml-xml-builder'); -const { AgentPartyGenerator } = require('../lib/agent-party-generator'); -const xml2js = require('xml2js'); -const { getProjectRoot, getSourcePath, getModulePath } = require('../lib/project-root'); - -class WebBundler { - constructor(sourceDir = null, outputDir = 'web-bundles') { - this.sourceDir = sourceDir || getSourcePath(); - this.outputDir = path.isAbsolute(outputDir) ? outputDir : path.join(getProjectRoot(), outputDir); - this.modulesPath = getSourcePath('modules'); - this.utilityPath = getSourcePath('utility'); - - this.dependencyResolver = new DependencyResolver(); - this.xmlHandler = new XmlHandler(); - this.yamlBuilder = new YamlXmlBuilder(); - - // Cache for resolved dependencies to avoid duplicates - this.dependencyCache = new Map(); - - // Discovered agents and teams for manifest generation - this.discoveredAgents = []; - this.discoveredTeams = []; - - // Temporary directory for generated manifests - this.tempDir = path.join(process.cwd(), '.bundler-temp'); - this.tempManifestDir = path.join(this.tempDir, '.bmad', '_cfg'); - - // Bundle statistics - this.stats = { - totalAgents: 0, - bundledAgents: 0, - skippedAgents: 0, - failedAgents: 0, - invalidXml: 0, - warnings: [], - }; - } - - /** - * Main entry point to bundle all modules - */ - async bundleAll() { - console.log(chalk.cyan.bold('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•')); - console.log(chalk.cyan.bold(' ๐Ÿš€ Web Bundle Generation')); - console.log(chalk.cyan.bold('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n')); - - try { - // Vendor cross-module workflows FIRST - const modules = await this.discoverModules(); - for (const module of modules) { - await this.vendorCrossModuleWorkflows(module); - } - - // Pre-discover all modules to generate complete manifests - for (const module of modules) { - await this.preDiscoverModule(module); - } - - // Create temporary manifest files - await this.createTempManifests(); - - // Process all modules - for (const module of modules) { - await this.bundleModule(module); - } - - // Display summary - this.displaySummary(); - } finally { - // Clean up temp files - await this.cleanupTempFiles(); - } - } - - /** - * Bundle a specific module - */ - async bundleModule(moduleName) { - const modulePath = path.join(this.modulesPath, moduleName); - - if (!(await fs.pathExists(modulePath))) { - console.log(chalk.yellow(`Module ${moduleName} not found`)); - return { module: moduleName, agents: [], teams: [] }; - } - - console.log(chalk.bold(`\n๐Ÿ“ฆ Bundling module: ${moduleName}`)); - - const results = { - module: moduleName, - agents: [], - teams: [], - }; - - // Vendor cross-module workflows first (if not already done by bundleAll) - await this.vendorCrossModuleWorkflows(moduleName); - - // Pre-discover all agents and teams for manifest generation - await this.preDiscoverModule(moduleName); - - // Ensure temp manifests exist (might not exist if called directly) - if (!(await fs.pathExists(this.tempManifestDir))) { - await this.createTempManifests(); - } - - // Process agents - const agents = await this.discoverAgents(modulePath); - for (const agent of agents) { - try { - await this.bundleAgent(moduleName, agent, false); // false = don't track again - results.agents.push(agent); - } catch (error) { - console.error(` Failed to bundle agent ${agent}:`, error.message); - } - } - - // Process teams - const teams = await this.discoverTeams(modulePath); - for (const team of teams) { - try { - await this.bundleTeam(moduleName, team); - results.teams.push(team); - } catch (error) { - console.error(` Failed to bundle team ${team}:`, error.message); - } - } - - return results; - } - - /** - * Bundle a single agent - */ - async bundleAgent(moduleName, agentFile, shouldTrack = true) { - const agentName = agentFile.endsWith('.agent.yaml') ? path.basename(agentFile, '.agent.yaml') : path.basename(agentFile, '.md'); - this.stats.totalAgents++; - - console.log(chalk.dim(` โ†’ Processing: ${agentName}`)); - - // Vendor cross-module workflows first (if not already done) - await this.vendorCrossModuleWorkflows(moduleName); - - const agentPath = path.join(this.modulesPath, moduleName, 'agents', agentFile); - - // Check if agent file exists - if (!(await fs.pathExists(agentPath))) { - this.stats.failedAgents++; - console.log(chalk.red(` โœ— Agent file not found`)); - throw new Error(`Agent file not found: ${agentPath}`); - } - - let content; - let agentXml; - - // Handle YAML agents - build in-memory to XML - if (agentFile.endsWith('.agent.yaml')) { - // Check for webskip flag in YAML before building - const yamlContent = await fs.readFile(agentPath, 'utf8'); - const agentYaml = yaml.load(yamlContent); - - if (agentYaml?.agent?.webskip === true) { - this.stats.skippedAgents++; - console.log(chalk.gray(` โŠ˜ Skipped (webskip="true")`)); - return; - } - - // Build agent from YAML (no customize file for web bundles) - const xmlContent = await this.yamlBuilder.buildFromYaml(agentPath, null, { - includeMetadata: false, // Don't include build metadata in web bundles - forWebBundle: true, // Use web-specific activation fragments - }); - - content = xmlContent; - agentXml = this.extractAgentXml(xmlContent); - } else { - // Legacy MD format - read and extract XML - content = await fs.readFile(agentPath, 'utf8'); - agentXml = this.extractAgentXml(content); - } - - if (!agentXml) { - this.stats.failedAgents++; - console.log(chalk.red(` โœ— No agent XML found in ${agentFile}`)); - return; - } - - // Check if agent has bundle="false" attribute - if (this.shouldSkipBundling(agentXml)) { - this.stats.skippedAgents++; - console.log(chalk.gray(` โŠ˜ Skipped (bundle="false")`)); - return; - } - - // Process {project-root} references in agent XML - agentXml = this.processProjectRootReferences(agentXml); - - // Track for manifest generation BEFORE generating manifests (if not pre-discovered) - if (shouldTrack) { - const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName); - if (agentDetails) { - this.discoveredAgents.push(agentDetails); - } - } - - // Resolve dependencies with warning tracking - const dependencyWarnings = []; - const { dependencies, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, dependencyWarnings); - - if (dependencyWarnings.length > 0) { - this.stats.warnings.push({ agent: agentName, warnings: dependencyWarnings }); - } - - // Check for module's default-party.csv and include it as agent manifest - const defaultPartyPath = path.join(this.modulesPath, moduleName, 'teams', 'default-party.csv'); - if (await fs.pathExists(defaultPartyPath)) { - const partyContent = await fs.readFile(defaultPartyPath, 'utf8'); - // Process any placeholders in the CSV content - const processedPartyContent = this.processProjectRootReferences(partyContent); - // Wrap as text to preserve raw CSV format in CDATA - const wrappedParty = this.wrapContentInXml(processedPartyContent, 'bmad/_cfg/agent-manifest.csv', 'text'); - dependencies.set('bmad/_cfg/agent-manifest.csv', wrappedParty); - console.log(chalk.gray(` + Added party manifest from module default-party.csv`)); - } - - // Remove commands for skipped workflows from agent XML - if (skippedWorkflows.length > 0) { - agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows); - } - - // Build the bundle (no manifests for individual agents) - const bundle = this.buildAgentBundle(agentXml, dependencies); - - // Validate XML - const isValid = await this.validateXml(bundle); - if (!isValid) { - this.stats.invalidXml++; - console.log(chalk.red(` โš  Invalid XML generated!`)); - } - - // Format XML for readability - const formattedBundle = this.formatXml(bundle); - - // Write bundle to output - const outputPath = path.join(this.outputDir, moduleName, 'agents', `${agentName}.xml`); - await fs.ensureDir(path.dirname(outputPath)); - await fs.writeFile(outputPath, formattedBundle, 'utf8'); - - this.stats.bundledAgents++; - const statusIcon = isValid ? chalk.green('โœ“') : chalk.yellow('โš '); - console.log(` ${statusIcon} Bundled: ${agentName}.xml${isValid ? '' : chalk.yellow(' (invalid XML)')}`); - } - - /** - * Bundle a team - includes orchestrator and all agents with their dependencies - */ - async bundleTeam(moduleName, teamFile) { - const teamName = path.basename(teamFile, path.extname(teamFile)); - console.log(chalk.dim(` โ†’ Processing team: ${teamName}`)); - - const teamPath = path.join(this.modulesPath, moduleName, 'teams', teamFile); - - // Check if team file exists - if (!(await fs.pathExists(teamPath))) { - console.log(chalk.red(` โœ— Team file not found`)); - throw new Error(`Team file not found: ${teamPath}`); - } - - // Read and parse team YAML - const teamContent = await fs.readFile(teamPath, 'utf8'); - const teamConfig = yaml.load(teamContent); - - if (!teamConfig || !teamConfig.bundle) { - console.log(chalk.red(` โœ— Invalid team configuration`)); - return; - } - - // Start building the team bundle - const dependencies = new Map(); - const processed = new Set(); - const allAgentXmls = []; - const warnings = []; - - // Check if team has a party CSV file (agent manifest) - const hasPartyFile = teamConfig.party && teamConfig.party.endsWith('.csv'); - if (hasPartyFile) { - // Load the party CSV and add it as bmad/_cfg/agent-manifest.csv - const partyPath = path.join(path.dirname(teamPath), teamConfig.party.replace(/^\.\//, '')); - if (await fs.pathExists(partyPath)) { - const partyContent = await fs.readFile(partyPath, 'utf8'); - // Process any placeholders in the CSV content - const processedPartyContent = this.processProjectRootReferences(partyContent); - // Wrap as text/csv to preserve raw CSV format in CDATA - const wrappedParty = this.wrapContentInXml(processedPartyContent, 'bmad/_cfg/agent-manifest.csv', 'text'); - dependencies.set('bmad/_cfg/agent-manifest.csv', wrappedParty); - console.log(chalk.gray(` + Added agent manifest from: ${teamConfig.party}`)); - } else { - console.log(chalk.yellow(` โš  Party file not found: ${partyPath}`)); - } - } - - // 1. First, always add the bmad-web-orchestrator (XML file only, no transformation needed) - const orchestratorXmlPath = path.join(this.sourceDir, 'core', 'agents', 'bmad-web-orchestrator.agent.xml'); - - if (await fs.pathExists(orchestratorXmlPath)) { - // Read the XML file directly - no transformation needed - const xmlContent = await fs.readFile(orchestratorXmlPath, 'utf8'); - let orchestratorXml = xmlContent.trim(); - - // Process {project-root} references - orchestratorXml = this.processProjectRootReferences(orchestratorXml); - - // Inject help/exit menu items only (orchestrator has its own activation) - orchestratorXml = this.injectHelpExitMenuItems(orchestratorXml); - - // Resolve orchestrator dependencies - const { dependencies: orchDeps } = await this.resolveAgentDependencies(orchestratorXml, 'core', warnings); - - // Merge orchestrator dependencies - for (const [id, content] of orchDeps) { - if (!dependencies.has(id)) { - dependencies.set(id, content); - } - } - - // Add orchestrator XML first - allAgentXmls.push(orchestratorXml); - console.log(chalk.gray(` + Added orchestrator: bmad-web-orchestrator`)); - } else { - console.log(chalk.yellow(` โš  Orchestrator not found at: ${orchestratorXmlPath}`)); - } - - // 2. Determine which agents to include - let agentsToBundle = []; - - if (teamConfig.agents === '*' || (Array.isArray(teamConfig.agents) && teamConfig.agents.includes('*'))) { - // Include all agents from the module - const agentsPath = path.join(this.modulesPath, moduleName, 'agents'); - if (await fs.pathExists(agentsPath)) { - const agentFiles = await fs.readdir(agentsPath); - agentsToBundle = agentFiles - .filter((file) => file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme'))) - .map((file) => file.replace(/\.(agent\.yaml|md)$/, '')); - } - } else if (Array.isArray(teamConfig.agents)) { - // Include specific agents listed - agentsToBundle = teamConfig.agents; - } else { - console.log(chalk.yellow(` โš  No agents specified in team configuration`)); - } - - // 3. Process each agent and their dependencies - for (const agentName of agentsToBundle) { - // Try YAML first, then MD - let agentPath = path.join(this.modulesPath, moduleName, 'agents', `${agentName}.agent.yaml`); - let isYaml = await fs.pathExists(agentPath); - - if (!isYaml) { - agentPath = path.join(this.modulesPath, moduleName, 'agents', `${agentName}.md`); - if (!(await fs.pathExists(agentPath))) { - console.log(chalk.yellow(` โš  Agent not found: ${agentName}`)); - continue; - } - } - - let agentXml; - - if (isYaml) { - // Check for webskip flag in YAML - const yamlContent = await fs.readFile(agentPath, 'utf8'); - const agentYaml = yaml.load(yamlContent); - - if (agentYaml?.agent?.webskip === true) { - console.log(chalk.gray(` โŠ˜ Skipped agent (webskip="true"): ${agentName}`)); - continue; - } - - // Build YAML agent in-memory - skip activation for team agents (orchestrator handles it) - const xmlContent = await this.yamlBuilder.buildFromYaml(agentPath, null, { - includeMetadata: false, - skipActivation: true, // Skip activation for team agents - }); - agentXml = this.extractAgentXml(xmlContent); - } else { - // Read legacy MD agent - const agentContent = await fs.readFile(agentPath, 'utf8'); - agentXml = this.extractAgentXml(agentContent); - } - - if (!agentXml) { - console.log(chalk.yellow(` โš  No XML found in agent: ${agentName}`)); - continue; - } - - // Skip agents with bundle="false" - if (this.shouldSkipBundling(agentXml)) { - console.log(chalk.gray(` โŠ˜ Skipped agent (bundle="false"): ${agentName}`)); - continue; - } - - // Process {project-root} references - agentXml = this.processProjectRootReferences(agentXml); - - // Resolve agent dependencies - const agentWarnings = []; - const { dependencies: agentDeps, skippedWorkflows } = await this.resolveAgentDependencies(agentXml, moduleName, agentWarnings); - - if (agentWarnings.length > 0) { - warnings.push({ agent: agentName, warnings: agentWarnings }); - } - - // Remove commands for skipped workflows from agent XML - if (skippedWorkflows.length > 0) { - agentXml = this.removeSkippedWorkflowCommands(agentXml, skippedWorkflows); - } - - // Merge agent dependencies (deduplicate) - for (const [id, content] of agentDeps) { - if (!dependencies.has(id)) { - dependencies.set(id, content); - } - } - - // Skip web activation injection for team agents - orchestrator handles everything - // Only inject help/exit menu items if missing - agentXml = this.injectHelpExitMenuItems(agentXml); - - // Add agent XML to the collection - allAgentXmls.push(agentXml); - console.log(chalk.gray(` + Added agent: ${agentName}`)); - } - - // 4. Build the team bundle XML - const bundle = this.buildTeamBundle(teamConfig.bundle, allAgentXmls, dependencies); - - // 5. Validate XML - const isValid = await this.validateXml(bundle); - if (!isValid) { - console.log(chalk.red(` โš  Invalid XML generated for team!`)); - } - - // Format XML for readability - const formattedBundle = this.formatXml(bundle); - - // 6. Write bundle to output - const outputPath = path.join(this.outputDir, moduleName, 'teams', `${teamName}.xml`); - await fs.ensureDir(path.dirname(outputPath)); - await fs.writeFile(outputPath, formattedBundle, 'utf8'); - - const statusIcon = isValid ? chalk.green('โœ“') : chalk.yellow('โš '); - console.log(` ${statusIcon} Bundled team: ${teamName}.xml${isValid ? '' : chalk.yellow(' (invalid XML)')}`); - - // Track warnings - if (warnings.length > 0) { - this.stats.warnings.push(...warnings); - } - } - - /** - * Build the final team bundle XML - */ - buildTeamBundle(teamMetadata, agentXmls, dependencies) { - const parts = ['', '', ' ', ' ']; - - for (const agentXml of agentXmls) { - // Indent each agent XML properly (add 4 spaces to each line) - const indentedAgent = agentXml - .split('\n') - .map((line) => ' ' + line) - .join('\n'); - parts.push(indentedAgent); - } - - parts.push(' '); - - // Add all dependencies - if (dependencies && dependencies.size > 0) { - parts.push('', ' ', ' '); - - for (const [id, content] of dependencies) { - // All dependencies are now consistently wrapped in elements - // Indent properly (add 4 spaces to each line) - const indentedContent = content - .split('\n') - .map((line) => ' ' + line) - .join('\n'); - parts.push(indentedContent); - } - - parts.push(' '); - } - - parts.push(''); - - return parts.join('\n'); - } - - /** - * Vendor cross-module workflows for a module - * Scans source agent YAML files for workflow-install attributes and copies workflows - */ - async vendorCrossModuleWorkflows(moduleName) { - const modulePath = path.join(this.modulesPath, moduleName); - const agentsPath = path.join(modulePath, 'agents'); - - if (!(await fs.pathExists(agentsPath))) { - return; - } - - // Find all agent YAML files - const files = await fs.readdir(agentsPath); - const yamlFiles = files.filter((f) => f.endsWith('.agent.yaml')); - - for (const agentFile of yamlFiles) { - const agentPath = path.join(agentsPath, agentFile); - const agentYaml = yaml.load(await fs.readFile(agentPath, 'utf8')); - - const menuItems = agentYaml?.agent?.menu || []; - const workflowInstallItems = menuItems.filter((item) => item['workflow-install']); - - for (const item of workflowInstallItems) { - const sourceWorkflowPath = item.workflow; - const installWorkflowPath = item['workflow-install']; - - if (!sourceWorkflowPath || !installWorkflowPath) { - continue; - } - - // Parse paths to extract module and workflow location - // Support both {project-root}/bmad/... and {project-root}/.bmad/... patterns - const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:\.?bmad)\/([^/]+)\/workflows\/(.+)/); - const installMatch = installWorkflowPath.match(/\{project-root\}\/(?:\.?bmad)\/([^/]+)\/workflows\/(.+)/); - - if (!sourceMatch || !installMatch) { - continue; - } - - const sourceModule = sourceMatch[1]; - const sourceWorkflowRelPath = sourceMatch[2]; - const installModule = installMatch[1]; - const installWorkflowRelPath = installMatch[2]; - - // Build actual filesystem paths - const actualSourceWorkflowPath = path.join(this.modulesPath, sourceModule, 'workflows', sourceWorkflowRelPath); - const actualDestWorkflowPath = path.join(this.modulesPath, installModule, 'workflows', installWorkflowRelPath); - - // Check if source workflow exists - if (!(await fs.pathExists(actualSourceWorkflowPath))) { - console.log(chalk.yellow(` โš  Source workflow not found for vendoring: ${sourceWorkflowPath}`)); - continue; - } - - // Check if destination already exists (skip if already vendored) - if (await fs.pathExists(actualDestWorkflowPath)) { - continue; - } - - // Get workflow directory (workflow.yaml is in a directory with other files) - const sourceWorkflowDir = path.dirname(actualSourceWorkflowPath); - const destWorkflowDir = path.dirname(actualDestWorkflowPath); - - // Copy entire workflow directory - await fs.copy(sourceWorkflowDir, destWorkflowDir, { overwrite: false }); - - // Update config_source in the vendored workflow.yaml - const workflowYamlPath = actualDestWorkflowPath; - if (await fs.pathExists(workflowYamlPath)) { - await this.updateWorkflowConfigSource(workflowYamlPath, installModule); - } - - console.log(chalk.dim(` โ†’ Vendored workflow: ${sourceWorkflowRelPath} โ†’ ${installModule}/workflows/${installWorkflowRelPath}`)); - } - } - } - - /** - * Update config_source in a vendored workflow YAML file - */ - async updateWorkflowConfigSource(workflowYamlPath, newModuleName) { - let yamlContent = await fs.readFile(workflowYamlPath, 'utf8'); - - // Replace config_source with new module reference - // Support both old format (bmad) and new format (.bmad) - const configSourcePattern = /config_source:\s*["']?\{project-root\}\/(?:\.?bmad)\/[^/]+\/config\.yaml["']?/g; - const newConfigSource = `config_source: "{project-root}/.bmad/${newModuleName}/config.yaml"`; - - const updatedYaml = yamlContent.replaceAll(configSourcePattern, newConfigSource); - await fs.writeFile(workflowYamlPath, updatedYaml, 'utf8'); - } - - /** - * Pre-discover all agents and teams in a module for manifest generation - */ - async preDiscoverModule(moduleName) { - const modulePath = path.join(this.modulesPath, moduleName); - - // Clear any previously discovered agents for this module - this.discoveredAgents = this.discoveredAgents.filter((a) => a.module !== moduleName); - - // Discover agents - const agentsPath = path.join(modulePath, 'agents'); - if (await fs.pathExists(agentsPath)) { - const files = await fs.readdir(agentsPath); - for (const file of files) { - if (file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme'))) { - const agentPath = path.join(agentsPath, file); - let content; - - if (file.endsWith('.agent.yaml')) { - // Check for webskip flag in YAML - const yamlContent = await fs.readFile(agentPath, 'utf8'); - const agentYaml = yaml.load(yamlContent); - - if (agentYaml?.agent?.webskip === true) { - continue; // Skip this agent - } - - // Build YAML agent in-memory - content = await this.yamlBuilder.buildFromYaml(agentPath, null, { - includeMetadata: false, - }); - } else { - // Read legacy MD agent - content = await fs.readFile(agentPath, 'utf8'); - } - - const agentXml = this.extractAgentXml(content); - - if (agentXml) { - // Skip agents with bundle="false" - if (this.shouldSkipBundling(agentXml)) { - continue; - } - - const agentName = file.endsWith('.agent.yaml') ? path.basename(file, '.agent.yaml') : path.basename(file, '.md'); - // Use the shared generator to extract agent details (pass full content) - const agentDetails = AgentPartyGenerator.extractAgentDetails(content, moduleName, agentName); - if (agentDetails) { - this.discoveredAgents.push(agentDetails); - } - } - } - } - } - - // TODO: Discover teams when implemented - } - - /** - * Extract agent XML from markdown content - */ - extractAgentXml(content) { - // Try 4 backticks first (can contain 3 backtick blocks inside) - let match = content.match(/````xml\s*([\s\S]*?)````/); - if (!match) { - // Fall back to 3 backticks if no 4-backtick block found - match = content.match(/```xml\s*([\s\S]*?)```/); - } - - if (match) { - const xmlContent = match[1]; - const agentMatch = xmlContent.match(/]*>[\s\S]*?<\/agent>/); - return agentMatch ? agentMatch[0] : null; - } - - // Fall back to direct extraction - match = content.match(/]*>[\s\S]*?<\/agent>/); - return match ? match[0] : null; - } - - /** - * Resolve all dependencies for an agent - */ - async resolveAgentDependencies(agentXml, moduleName, warnings = []) { - const dependencies = new Map(); - const processed = new Set(); - const skippedWorkflows = []; - - // Extract file references from agent XML - const { refs, workflowRefs } = this.extractFileReferences(agentXml); - - // Process regular file references - for (const ref of refs) { - await this.processFileDependency(ref, dependencies, processed, moduleName, warnings); - } - - // Process workflow references with special handling - for (const workflowRef of workflowRefs) { - const result = await this.processWorkflowDependency(workflowRef, dependencies, processed, moduleName, warnings); - if (result && result.skipped) { - skippedWorkflows.push(workflowRef); - } - } - - return { dependencies, skippedWorkflows }; - } - - /** - * Extract file references from agent XML - */ - extractFileReferences(xml) { - const refs = new Set(); - const workflowRefs = new Set(); - - // Remove agent id attribute to prevent it from being treated as a dependency - // The id attribute is just a metadata identifier, not a file reference - const xmlWithoutAgentId = xml.replace(/]*id="[^"]*"[^>]*>/, (match) => { - return match.replace(/\sid="[^"]*"/, ''); - }); - - // Match various file reference patterns - const patterns = [ - /exec="([^"]+)"/g, // Command exec paths - /tmpl="([^"]+)"/g, // Template paths - /data="([^"]+)"/g, // Data file paths - /file="([^"]+)"/g, // Generic file refs - /src="([^"]+)"/g, // Source paths - /system-prompts="([^"]+)"/g, - /tools="([^"]+)"/g, - /knowledge="([^"]+)"/g, - /{project-root}\/([^"'\s<>]+)/g, // Legacy {project-root} paths - /\bbmad\/([^"'\s<>]+)/g, // Direct bmad/ paths (after .bmad replacement) - ]; - - for (const pattern of patterns) { - let match; - // Use the XML with agent id removed for pattern matching - while ((match = pattern.exec(xmlWithoutAgentId)) !== null) { - let filePath = match[1]; - // Remove {project-root} prefix if present - filePath = filePath.replace(/^{project-root}\//, ''); - // Remove .bmad prefix if present (should be rare, mostly replaced already) - filePath = filePath.replace(/^.bmad\//, 'bmad/'); - - // For bmad/ pattern, prepend 'bmad/' since it was captured without it - if (pattern.source.includes(String.raw`\bbmad\/`)) { - filePath = 'bmad/' + filePath; - } - - // Skip obvious placeholder/example paths - if (filePath && !filePath.includes('path/to/') && !filePath.includes('example') && !filePath.includes('...')) { - refs.add(filePath); - } - } - } - - // Extract workflow references from agent files - const workflowPatterns = [ - /workflow="([^"]+)"/g, // Menu items with workflow attribute - /validate-workflow="([^"]+)"/g, // Validation workflow references - ]; - - for (const pattern of workflowPatterns) { - let match; - // Use original xml for workflow patterns (they don't conflict with agent id) - while ((match = pattern.exec(xml)) !== null) { - let workflowPath = match[1]; - workflowPath = workflowPath.replace(/^{project-root}\//, ''); - // Remove .bmad prefix if present and replace with bmad - workflowPath = workflowPath.replace(/^.bmad\//, 'bmad/'); - - // Skip obvious placeholder/example paths - if (workflowPath && workflowPath.endsWith('.yaml') && !workflowPath.includes('path/to/') && !workflowPath.includes('example')) { - workflowRefs.add(workflowPath); - } - } - } - - return { refs: [...refs], workflowRefs: [...workflowRefs] }; - } - - /** - * Remove commands from agent XML that reference skipped workflows - */ - removeSkippedWorkflowCommands(agentXml, skippedWorkflows) { - let modifiedXml = agentXml; - - // For each skipped workflow, find and remove menu items and commands - for (const workflowPath of skippedWorkflows) { - // Need to escape special regex characters in the path - const escapedPath = workflowPath.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); - - // Pattern 1: Remove tags with workflow attribute - // Match: ... - const itemWorkflowPattern = new RegExp(`\\s*]*workflow="[^"]*${escapedPath}"[^>]*>.*?\\s*`, 'gs'); - modifiedXml = modifiedXml.replace(itemWorkflowPattern, ''); - } - - return modifiedXml; - } - - /** - * Process a file dependency recursively - */ - async processFileDependency(filePath, dependencies, processed, moduleName, warnings = []) { - // Skip workflow YAML files - they're handled by processWorkflowDependency - if (filePath.includes('/workflow') && filePath.endsWith('workflow.yaml')) { - return; - } - - // Skip if already processed - if (processed.has(filePath)) { - return; - } - processed.add(filePath); - - // Skip agent-manifest.csv manifest for web bundles (agents are already bundled) - if (filePath === 'bmad/_cfg/agent-manifest.csv' || filePath.endsWith('/agent-manifest.csv')) { - return; - } - - // Handle wildcard patterns - if (filePath.includes('*')) { - await this.processWildcardDependency(filePath, dependencies, processed, moduleName, warnings); - return; - } - - // Resolve actual file path - const actualPath = this.resolveFilePath(filePath, moduleName); - - if (!actualPath || !(await fs.pathExists(actualPath))) { - warnings.push(filePath); - return; - } - - // Skip if it's a directory - const stats = await fs.stat(actualPath); - if (stats.isDirectory()) { - // Silently skip directories - they're not file dependencies - return; - } - - // Read file content - let content = await fs.readFile(actualPath, 'utf8'); - - // Process {project-root} references - content = this.processProjectRootReferences(content); - - // Extract dependencies from frontmatter if present - const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (frontmatterMatch) { - const frontmatter = frontmatterMatch[1]; - // Look for dependencies in frontmatter - const depMatch = frontmatter.match(/dependencies:\s*\[(.*?)\]/); - if (depMatch) { - const deps = depMatch[1].match(/['"]([^'"]+)['"]/g); - if (deps) { - for (const dep of deps) { - let depPath = dep.replaceAll(/['"]/g, '').replace(/^{project-root}\//, ''); - depPath = depPath.replace(/^.bmad\//, 'bmad/'); - if (depPath && !processed.has(depPath)) { - await this.processFileDependency(depPath, dependencies, processed, moduleName, warnings); - } - } - } - } - // Look for template references - const templateMatch = frontmatter.match(/template:\s*\[(.*?)\]/); - if (templateMatch) { - const templates = templateMatch[1].match(/['"]([^'"]+)['"]/g); - if (templates) { - for (const template of templates) { - let templatePath = template.replaceAll(/['"]/g, '').replace(/^{project-root}\//, ''); - templatePath = templatePath.replace(/^.bmad\//, 'bmad/'); - if (templatePath && !processed.has(templatePath)) { - await this.processFileDependency(templatePath, dependencies, processed, moduleName, warnings); - } - } - } - } - } - - // Extract XML from markdown if applicable - const ext = path.extname(actualPath).toLowerCase(); - let processedContent = content; - - switch (ext) { - case '.md': { - // Try to extract XML from markdown - handle both 3 and 4 backtick blocks - // First try 4 backticks (which can contain 3 backtick blocks inside) - let xmlMatches = [...content.matchAll(/````xml\s*([\s\S]*?)````/g)]; - - // If no 4-backtick blocks, try 3 backticks - if (xmlMatches.length === 0) { - xmlMatches = [...content.matchAll(/```xml\s*([\s\S]*?)```/g)]; - } - - const xmlBlocks = []; - - for (const match of xmlMatches) { - if (match[1]) { - xmlBlocks.push(match[1].trim()); - } - } - - if (xmlBlocks.length > 0) { - // For XML content, just include it directly (it's already valid XML) - processedContent = xmlBlocks.join('\n\n'); - } else { - // No XML blocks found, skip non-XML markdown files - return; - } - - break; - } - case '.csv': { - // CSV files need special handling - convert to XML file-index - const lines = content.split('\n').filter((line) => line.trim()); - if (lines.length === 0) return; - - const headers = lines[0].split(',').map((h) => h.trim()); - const rows = lines.slice(1); - - const indexParts = [``]; - indexParts.push(' '); - - // Track files referenced in CSV for additional bundling - const referencedFiles = new Set(); - - for (const row of rows) { - const values = row.split(',').map((v) => v.trim()); - if (values.every((v) => !v)) continue; - - indexParts.push(' '); - for (const [i, header] of headers.entries()) { - const value = values[i] || ''; - const tagName = header.toLowerCase().replaceAll(/[^a-z0-9]/g, '_'); - indexParts.push(` <${tagName}>${value}`); - - // Track referenced files - if (header.toLowerCase().includes('file') && value.endsWith('.md')) { - // Build path relative to CSV location - const csvDir = path.dirname(actualPath); - const refPath = path.join(csvDir, value); - if (fs.existsSync(refPath)) { - const refId = filePath.replace('index.csv', value); - referencedFiles.add(refId); - } - } - } - indexParts.push(' '); - } - - indexParts.push(' ', ''); - - // Store the XML version wrapped in a file element - const csvXml = indexParts.join('\n'); - const wrappedCsv = `\n${csvXml}\n`; - dependencies.set(filePath, wrappedCsv); - - // Process referenced files from CSV - for (const refId of referencedFiles) { - if (!processed.has(refId)) { - await this.processFileDependency(refId, dependencies, processed, moduleName, warnings); - } - } - - return; - } - case '.xml': { - // XML files can be included directly - processedContent = content; - break; - } - default: { - // For other non-XML file types, skip them - return; - } - } - - // Determine file type for wrapping - let fileType = 'text'; - if (ext === '.xml' || (ext === '.md' && processedContent.trim().startsWith('<'))) { - fileType = 'xml'; - } else - switch (ext) { - case '.yaml': - case '.yml': { - fileType = 'yaml'; - - break; - } - case '.json': { - fileType = 'json'; - - break; - } - case '.md': { - fileType = 'md'; - - break; - } - // No default - } - - // Wrap content in file element and store - const wrappedContent = this.wrapContentInXml(processedContent, filePath, fileType); - dependencies.set(filePath, wrappedContent); - - // Recursively scan for more dependencies - const { refs: nestedRefs } = this.extractFileReferences(processedContent); - for (const ref of nestedRefs) { - await this.processFileDependency(ref, dependencies, processed, moduleName, warnings); - } - } - - /** - * Process a workflow YAML file and its bundle files - */ - async processWorkflowDependency(workflowPath, dependencies, processed, moduleName, warnings = []) { - // Skip if already processed - if (processed.has(workflowPath)) { - return { skipped: false }; - } - processed.add(workflowPath); - - // Resolve actual file path - const actualPath = this.resolveFilePath(workflowPath, moduleName); - - if (!actualPath || !(await fs.pathExists(actualPath))) { - warnings.push(workflowPath); - return { skipped: true }; - } - - // Read and parse YAML file - const yamlContent = await fs.readFile(actualPath, 'utf8'); - let workflowConfig; - - try { - workflowConfig = yaml.load(yamlContent); - } catch (error) { - warnings.push(`${workflowPath} (invalid YAML: ${error.message})`); - return { skipped: true }; - } - - // Check if web_bundle is explicitly set to false - if (workflowConfig.web_bundle === false) { - // Mark this workflow as skipped so we can remove the command from agent - return { skipped: true, workflowPath }; - } - - // Create YAML content with only web_bundle section (flattened) - let bundleYamlContent; - if (workflowConfig.web_bundle && typeof workflowConfig.web_bundle === 'object') { - // Only include the web_bundle content, flattened to root level - bundleYamlContent = yaml.dump(workflowConfig.web_bundle); - } else { - // If no web_bundle section, include full YAML - bundleYamlContent = yamlContent; - } - - // Process {project-root} and .bmad references in the YAML content - bundleYamlContent = this.processProjectRootReferences(bundleYamlContent); - - // Include the YAML file with only web_bundle content, wrapped in XML - // Process the workflow path to create a clean ID - let yamlId = workflowPath.replace(/^{project-root}\//, ''); - yamlId = yamlId.replace(/^.bmad\//, 'bmad/'); - const wrappedYaml = this.wrapContentInXml(bundleYamlContent, yamlId, 'yaml'); - dependencies.set(yamlId, wrappedYaml); - - // Always include core workflow task when processing workflows - await this.includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings); - - // Check if advanced elicitation is enabled - if (workflowConfig.web_bundle && workflowConfig.web_bundle.use_advanced_elicitation) { - await this.includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings); - } - - // Process web_bundle_files if they exist - if (workflowConfig.web_bundle && workflowConfig.web_bundle.web_bundle_files) { - const bundleFiles = workflowConfig.web_bundle.web_bundle_files; - - for (const bundleFilePath of bundleFiles) { - // Process the file path to create a clean ID for checking if already processed - let cleanFilePath = bundleFilePath.replace(/^{project-root}\//, ''); - cleanFilePath = cleanFilePath.replace(/^.bmad\//, 'bmad/'); - - if (processed.has(cleanFilePath)) { - continue; - } - - const bundleActualPath = this.resolveFilePath(bundleFilePath, moduleName); - - if (!bundleActualPath || !(await fs.pathExists(bundleActualPath))) { - // Use the cleaned path in warnings (with .bmad replaced) - warnings.push(cleanFilePath); - continue; - } - - // Check if this is another workflow.yaml file - if so, recursively process it - if (bundleFilePath.endsWith('workflow.yaml')) { - // Recursively process this workflow and its dependencies - await this.processWorkflowDependency(bundleFilePath, dependencies, processed, moduleName, warnings); - } else { - // Regular file - process normally - processed.add(cleanFilePath); - - // Read the file content - let fileContent = await fs.readFile(bundleActualPath, 'utf8'); - const fileExt = path.extname(bundleActualPath).toLowerCase().replace('.', ''); - - // Process {project-root} references before wrapping - fileContent = this.processProjectRootReferences(fileContent); - - // Wrap in XML with proper escaping - const wrappedContent = this.wrapContentInXml(fileContent, cleanFilePath, fileExt); - dependencies.set(cleanFilePath, wrappedContent); - } - } - } - - return { skipped: false }; - } - - /** - * Include core workflow task files - */ - async includeCoreWorkflowFiles(dependencies, processed, moduleName, warnings = []) { - const coreWorkflowPath = 'bmad/core/tasks/workflow.xml'; - - if (processed.has(coreWorkflowPath)) { - return; - } - processed.add(coreWorkflowPath); - - const actualPath = this.resolveFilePath(coreWorkflowPath, moduleName); - - if (!actualPath || !(await fs.pathExists(actualPath))) { - warnings.push(coreWorkflowPath); - return; - } - - let fileContent = await fs.readFile(actualPath, 'utf8'); - // Process {project-root} and .bmad references - fileContent = this.processProjectRootReferences(fileContent); - const wrappedContent = this.wrapContentInXml(fileContent, coreWorkflowPath, 'xml'); - dependencies.set(coreWorkflowPath, wrappedContent); - } - - /** - * Include advanced elicitation files - */ - async includeAdvancedElicitationFiles(dependencies, processed, moduleName, warnings = []) { - const elicitationFiles = ['bmad/core/tasks/advanced-elicitation.xml', 'bmad/core/tasks/advanced-elicitation-methods.csv']; - - for (const filePath of elicitationFiles) { - if (processed.has(filePath)) { - continue; - } - processed.add(filePath); - - const actualPath = this.resolveFilePath(filePath, moduleName); - - if (!actualPath || !(await fs.pathExists(actualPath))) { - warnings.push(filePath); - continue; - } - - let fileContent = await fs.readFile(actualPath, 'utf8'); - // Process {project-root} and .bmad references - fileContent = this.processProjectRootReferences(fileContent); - const fileExt = path.extname(actualPath).toLowerCase().replace('.', ''); - const wrappedContent = this.wrapContentInXml(fileContent, filePath, fileExt); - dependencies.set(filePath, wrappedContent); - } - } - - /** - * Wrap file content in XML with proper escaping - */ - wrapContentInXml(content, id, type = 'text') { - // For XML files, include directly without CDATA (they're already valid XML) - if (type === 'xml') { - // XML files can be included directly as they're already well-formed - // Just wrap in a file element - return `\n${content}\n`; - } - - // For all other file types, use CDATA to preserve content exactly - // Escape any ]]> sequences in the content by splitting CDATA sections - // Replace ]]> with ]]]]> to properly escape it within CDATA - const escapedContent = content.replaceAll(']]>', ']]]]>'); - - // Use CDATA to preserve content exactly as-is, including special characters - return ``; - } - - /** - * Process wildcard dependency patterns - */ - async processWildcardDependency(pattern, dependencies, processed, moduleName, warnings = []) { - // Remove {project-root} prefix - pattern = pattern.replace(/^{project-root}\//, ''); - // Replace .bmad with bmad - pattern = pattern.replace(/^.bmad\//, 'bmad/'); - - // Get directory and file pattern - const lastSlash = pattern.lastIndexOf('/'); - const dirPath = pattern.slice(0, Math.max(0, lastSlash)); - const filePattern = pattern.slice(Math.max(0, lastSlash + 1)); - - // Resolve directory path without checking file existence - let dir; - if (dirPath.startsWith('bmad/')) { - // Remove bmad/ prefix - const actualPath = dirPath.replace(/^bmad\//, ''); - - // Try different path mappings for directories - const possibleDirs = [ - // Try as module path: bmad/cis/... -> src/modules/cis/... - path.join(this.sourceDir, 'modules', actualPath), - // Try as direct path: bmad/core/... -> src/core/... - path.join(this.sourceDir, actualPath), - ]; - - for (const testDir of possibleDirs) { - if (fs.existsSync(testDir)) { - dir = testDir; - break; - } - } - } - - if (!dir) { - warnings.push(`${pattern} (could not resolve directory)`); - return; - } - if (!(await fs.pathExists(dir))) { - warnings.push(pattern); - return; - } - - // Read directory and match files - const files = await fs.readdir(dir); - let matchedFiles = []; - - if (filePattern === '*.*') { - matchedFiles = files; - } else if (filePattern.startsWith('*.')) { - const ext = filePattern.slice(1); - matchedFiles = files.filter((f) => f.endsWith(ext)); - } else { - // Simple glob matching - const regex = new RegExp('^' + filePattern.replace('*', '.*') + '$'); - matchedFiles = files.filter((f) => regex.test(f)); - } - - // Process each matched file - for (const file of matchedFiles) { - const fullPath = dirPath + '/' + file; - if (!processed.has(fullPath)) { - await this.processFileDependency(fullPath, dependencies, processed, moduleName, warnings); - } - } - } - - /** - * Resolve file path relative to project - */ - resolveFilePath(filePath, moduleName) { - // Remove {project-root} prefix - filePath = filePath.replace(/^{project-root}\//, ''); - - // Check temp directory first for _cfg files - if (filePath.startsWith('bmad/_cfg/')) { - const filename = filePath.split('/').pop(); - const tempPath = path.join(this.tempManifestDir, filename); - if (fs.existsSync(tempPath)) { - return tempPath; - } - } - - let actualPath = filePath; - - if (filePath.startsWith('bmad/')) { - // Remove bmad/ prefix - actualPath = filePath.replace(/^bmad\//, ''); - - // Check if it's a module-specific file (cis, bmm, etc) or core file - const parts = actualPath.split('/'); - const firstPart = parts[0]; - - // Try different path mappings - const possiblePaths = [ - // Try in temp directory first - path.join(this.tempDir, filePath), - // Try as module path: bmad/cis/... -> src/modules/cis/... - path.join(this.sourceDir, 'modules', actualPath), - // Try as direct path: bmad/core/... -> src/core/... - path.join(this.sourceDir, actualPath), - // Try without any prefix in src - path.join(this.sourceDir, parts.slice(1).join('/')), - // Try in project root - path.join(this.sourceDir, '..', actualPath), - // Try original with bmad - path.join(this.sourceDir, '..', filePath), - ]; - - for (const testPath of possiblePaths) { - if (fs.existsSync(testPath)) { - return testPath; - } - } - } - - // Try standard paths for non-bmad files - const basePaths = [ - this.sourceDir, // src directory - path.join(this.modulesPath, moduleName), // Current module - path.join(this.sourceDir, '..'), // Project root - ]; - - for (const basePath of basePaths) { - const fullPath = path.join(basePath, actualPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - return null; - } - - /** - * Process and remove {project-root} references - */ - processProjectRootReferences(content) { - // Remove {project-root}/ prefix (with slash) - content = content.replaceAll('{project-root}/', ''); - // Also remove {project-root} without slash - content = content.replaceAll('{project-root}', ''); - return content; - } - - /** - * Escape special XML characters in text content - */ - escapeXmlText(text) { - return text - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", '''); - } - - /** - * Escape XML content while preserving XML tags - */ - escapeXmlContent(content) { - const tagPattern = /<([^>]+)>/g; - const parts = []; - let lastIndex = 0; - let match; - - while ((match = tagPattern.exec(content)) !== null) { - if (match.index > lastIndex) { - parts.push(this.escapeXmlText(content.slice(lastIndex, match.index))); - } - parts.push('<' + match[1] + '>'); - lastIndex = match.index + match[0].length; - } - - if (lastIndex < content.length) { - parts.push(this.escapeXmlText(content.slice(lastIndex))); - } - - return parts.join(''); - } - - /** - * Inject help and exit menu items into agent XML - */ - injectHelpExitMenuItems(agentXml) { - // Check if menu already has help and exit - const hasHelp = agentXml.includes('cmd="*help"') || agentXml.includes('trigger="*help"'); - const hasExit = agentXml.includes('cmd="*exit"') || agentXml.includes('trigger="*exit"'); - - if (hasHelp && hasExit) { - return agentXml; // Already has both, skip injection - } - - // Find the menu section - const menuMatch = agentXml.match(/([\s\S]*?<\/menu>)/); - if (!menuMatch) { - return agentXml; // No menu found, skip injection - } - - const menuContent = menuMatch[1]; - const menuClosingMatch = menuContent.match(/(\s*)<\/menu>/); - if (!menuClosingMatch) { - return agentXml; - } - - const indent = menuClosingMatch[1]; - const menuItems = []; - - if (!hasHelp) { - menuItems.push(`${indent}[M] Redisplay Menu Options`); - } - - if (!hasExit) { - menuItems.push(`${indent}[D] Dismiss Agent`); - } - - if (menuItems.length === 0) { - return agentXml; - } - - // Inject menu items before closing tag - const newMenuContent = menuContent.replace(/(\s*)<\/menu>/, `\n${menuItems.join('\n')}\n${indent}`); - return agentXml.replace(menuContent, newMenuContent); - } - - /** - * Inject web activation instructions into agent XML - */ - injectWebActivation(agentXml) { - // First, always inject help/exit menu items - agentXml = this.injectHelpExitMenuItems(agentXml); - - // Load the web activation template - const activationPath = path.join(this.sourceDir, 'utility', 'models', 'agent-activation-web.xml'); - - if (!fs.existsSync(activationPath)) { - console.warn(chalk.yellow('Warning: agent-activation-web.xml not found, skipping activation injection')); - return agentXml; - } - - const activationXml = fs.readFileSync(activationPath, 'utf8'); - - // For web bundles, ALWAYS replace existing activation with web activation - // This is because fragment-based activation assumes filesystem access which won't work in web bundles - const hasActivation = agentXml.includes(']*>[\s\S]*?<\/activation>/, activationXml); - return injectedXml; - } - - // Check for critical-actions block (legacy) - const hasCriticalActions = agentXml.includes('[\s\S]*?<\/critical-actions>/, activationXml); - return injectedXml; - } - - // If no critical-actions, inject before closing tag - const closingTagMatch = agentXml.match(/(\s*)<\/agent>/); - if (!closingTagMatch) { - console.warn(chalk.yellow('Warning: Could not find tag for activation injection')); - return agentXml; - } - - // Inject the activation block before the closing tag - // Properly indent each line of the activation XML - const indent = closingTagMatch[1]; - const indentedActivation = activationXml - .split('\n') - .map((line) => (line.trim() ? indent + line : '')) - .join('\n'); - - const injectedXml = agentXml.replace(/(\s*)<\/agent>/, `\n${indentedActivation}\n${indent}`); - - return injectedXml; - } - - /** - * Build the final agent bundle XML - */ - buildAgentBundle(agentXml, dependencies) { - // Web activation is now handled by fragments during YAML building - // agentXml = this.injectWebActivation(agentXml); - - const parts = [ - '', - '', - ' ', - ' ' + agentXml.replaceAll('\n', '\n '), - ]; - - // Add dependencies (all are now consistently wrapped in elements) - if (dependencies && dependencies.size > 0) { - parts.push('\n '); - for (const [id, content] of dependencies) { - // All dependencies are now wrapped in elements - // Indent properly - const indentedContent = content - .split('\n') - .map((line) => ' ' + line) - .join('\n'); - parts.push(indentedContent); - } - } - - parts.push(''); - - return parts.join('\n'); - } - - /** - * Discover all modules - */ - async discoverModules() { - const modules = []; - - if (!(await fs.pathExists(this.modulesPath))) { - console.log(chalk.yellow('No modules directory found')); - return modules; - } - - const entries = await fs.readdir(this.modulesPath, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory()) { - modules.push(entry.name); - } - } - - return modules; - } - - /** - * Discover agents in a module - */ - async discoverAgents(modulePath) { - const agents = []; - const agentsPath = path.join(modulePath, 'agents'); - - if (!(await fs.pathExists(agentsPath))) { - return agents; - } - - const files = await fs.readdir(agentsPath); - - for (const file of files) { - // Look for .agent.yaml files (new format) or .md files (legacy format) - if (file.endsWith('.agent.yaml') || (file.endsWith('.md') && !file.toLowerCase().includes('readme'))) { - agents.push(file); - } - } - - return agents; - } - - /** - * Discover all teams in a module - */ - async discoverTeams(modulePath) { - const teams = []; - const teamsPath = path.join(modulePath, 'teams'); - - if (!(await fs.pathExists(teamsPath))) { - return teams; - } - - const files = await fs.readdir(teamsPath); - - for (const file of files) { - if (file.endsWith('.yaml') || file.endsWith('.yml')) { - teams.push(file); - } - } - - return teams; - } - - /** - * Extract agent name from XML - */ - getAgentName(xml) { - const match = xml.match(/]*name="([^"]+)"/); - return match ? match[1] : 'Unknown'; - } - - /** - * Extract agent description from XML - */ - getAgentDescription(xml) { - const match = xml.match(/([^<]+)<\/description>/); - return match ? match[1] : ''; - } - - /** - * Check if agent should be skipped for bundling - */ - shouldSkipBundling(xml) { - // Check for bundle="false" attribute in the agent tag - const match = xml.match(/]*bundle="false"[^>]*>/); - return match !== null; - } - - /** - * Create temporary manifest files - */ - async createTempManifests() { - // Ensure temp directory exists - await fs.ensureDir(this.tempManifestDir); - - // Generate agent-manifest.csv using shared generator - const agentPartyPath = path.join(this.tempManifestDir, 'agent-manifest.csv'); - await AgentPartyGenerator.writeAgentParty(agentPartyPath, this.discoveredAgents, { forWeb: true }); - - console.log(chalk.dim(' โœ“ Created temporary manifest files')); - } - - /** - * Clean up temporary files - */ - async cleanupTempFiles() { - if (await fs.pathExists(this.tempDir)) { - await fs.remove(this.tempDir); - console.log(chalk.dim('\nโœ“ Cleaned up temporary files')); - } - } - - /** - * Validate XML content - */ - async validateXml(xmlContent) { - try { - await xml2js.parseStringPromise(xmlContent, { - strict: true, - explicitArray: false, - }); - return true; - } catch { - return false; - } - } - - /** - * Format XML content for readability - */ - formatXml(xml) { - const TAB = ' '; // 2 spaces - let result = ''; - let depth = 0; - - // Split by tags while preserving them - const parts = xml.split(/(<[^>]+>)/g); - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (!part) continue; - - if (part.startsWith(''); - const tagName = part.match(/<(\w+)/)?.[1]; - - // Check if next part is simple text content - const nextPart = parts[i + 1]; - const hasSimpleContent = nextPart && !nextPart.startsWith('<') && nextPart.trim().length > 0 && nextPart.trim().length <= 100; - - if (hasSimpleContent && parts[i + 2] && parts[i + 2] === ``) { - // Simple tag with inline content: content - result += TAB.repeat(depth) + part + nextPart.trim() + parts[i + 2] + '\n'; - i += 2; // Skip content and closing tag - } else { - // Multi-line tag - result += TAB.repeat(depth) + part + '\n'; - if (!isSelfClosing) { - depth++; - } - } - } else { - // Text content between tags - const trimmed = part.trim(); - if (trimmed) { - result += TAB.repeat(depth) + trimmed + '\n'; - } - } - } - - return result; - } - - /** - * Display summary statistics - */ - displaySummary() { - console.log(chalk.cyan.bold('\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•')); - console.log(chalk.cyan.bold(' SUMMARY')); - console.log(chalk.cyan.bold('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n')); - - console.log(chalk.bold('Bundle Statistics:')); - console.log(` Total agents found: ${this.stats.totalAgents}`); - console.log(` Successfully bundled: ${chalk.green(this.stats.bundledAgents)}`); - if (this.stats.skippedAgents > 0) { - console.log(` Skipped (webskip/bundle): ${chalk.gray(this.stats.skippedAgents)}`); - } - - if (this.stats.failedAgents > 0) { - console.log(` Failed to bundle: ${chalk.red(this.stats.failedAgents)}`); - } - - if (this.stats.invalidXml > 0) { - console.log(` Invalid XML bundles: ${chalk.yellow(this.stats.invalidXml)}`); - } - - // Display warnings summary - // Check if there are actually any warnings with content - const hasActualWarnings = this.stats.warnings.some((w) => w && w.warnings && w.warnings.length > 0); - - if (hasActualWarnings) { - console.log(chalk.yellow('\nโš  Missing Dependencies by Agent:')); - - // Group and display warnings by agent - for (const agentWarning of this.stats.warnings) { - if (agentWarning && agentWarning.warnings && agentWarning.warnings.length > 0) { - console.log(chalk.bold(`\n ${agentWarning.agent}:`)); - // Display unique warnings for this agent - const uniqueWarnings = [...new Set(agentWarning.warnings)]; - for (const warning of uniqueWarnings) { - console.log(chalk.dim(` โ€ข ${warning}`)); - } - } - } - } else { - console.log(chalk.green('\nโœ“ No missing dependencies')); - } - - // Final status - if (this.stats.invalidXml > 0) { - console.log(chalk.yellow('\nโš  Some bundles have invalid XML. Please review the output.')); - } else if (this.stats.failedAgents > 0) { - console.log(chalk.yellow('\nโš  Some agents failed to bundle. Please review the errors.')); - } else { - console.log(chalk.green('\nโœจ All bundles generated successfully!')); - } - - console.log(chalk.cyan.bold('\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n')); - } -} - -module.exports = { WebBundler }; diff --git a/tools/cli/lib/replace-project-root.js b/tools/cli/lib/replace-project-root.js deleted file mode 100644 index 8230d7fb..00000000 --- a/tools/cli/lib/replace-project-root.js +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Utility function to replace {project-root} placeholders with actual installation target - * Used during BMAD installation to set correct paths in agent and task files - */ - -const fs = require('node:fs'); -const path = require('node:path'); - -/** - * Replace {project-root} and {output_folder}/ placeholders in a single file - * @param {string} filePath - Path to the file to process - * @param {string} projectRoot - The actual project root path to substitute (must include trailing slash) - * @param {string} docOut - The document output path (with leading slash) - * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing - * @returns {boolean} - True if replacements were made, false otherwise - */ -function replacePlaceholdersInFile(filePath, projectRoot, docOut = '/docs', removeCompletely = false) { - try { - let content = fs.readFileSync(filePath, 'utf8'); - const originalContent = content; - - if (removeCompletely) { - // Remove placeholders entirely (for bundling) - content = content.replaceAll('{project-root}', ''); - content = content.replaceAll('{output_folder}/', ''); - } else { - // Handle the combined pattern first to avoid double slashes - if (projectRoot && docOut) { - // Replace {project-root}{output_folder}/ combinations first - // Remove leading slash from docOut since projectRoot has trailing slash - // Add trailing slash to docOut - const docOutNoLeadingSlash = docOut.replace(/^\//, ''); - const docOutWithTrailingSlash = docOutNoLeadingSlash.endsWith('/') ? docOutNoLeadingSlash : docOutNoLeadingSlash + '/'; - content = content.replaceAll('{project-root}{output_folder}/', projectRoot + docOutWithTrailingSlash); - } - - // Then replace remaining individual placeholders - if (projectRoot) { - content = content.replaceAll('{project-root}', projectRoot); - } - - if (docOut) { - // For standalone {output_folder}/, keep the leading slash and add trailing slash - const docOutWithTrailingSlash = docOut.endsWith('/') ? docOut : docOut + '/'; - content = content.replaceAll('{output_folder}/', docOutWithTrailingSlash); - } - } - - if (content !== originalContent) { - fs.writeFileSync(filePath, content, 'utf8'); - return true; - } - - return false; - } catch (error) { - console.error(`Error processing file ${filePath}:`, error.message); - return false; - } -} - -/** - * Legacy function name for backward compatibility - */ -function replaceProjectRootInFile(filePath, projectRoot, removeCompletely = false) { - return replacePlaceholdersInFile(filePath, projectRoot, '/docs', removeCompletely); -} - -/** - * Recursively replace {project-root} and {output_folder}/ in all files in a directory - * @param {string} dirPath - Directory to process - * @param {string} projectRoot - The actual project root path to substitute (or null to remove) - * @param {string} docOut - The document output path (with leading slash) - * @param {Array} extensions - File extensions to process (default: ['.md', '.xml', '.yaml']) - * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing - * @param {boolean} verbose - If true, show detailed output for each file - * @returns {Object} - Stats object with counts of files processed and modified - */ -function replacePlaceholdersInDirectory( - dirPath, - projectRoot, - docOut = '/docs', - extensions = ['.md', '.xml', '.yaml'], - removeCompletely = false, - verbose = false, -) { - const stats = { - processed: 0, - modified: 0, - errors: 0, - }; - - function processDirectory(currentPath) { - try { - const items = fs.readdirSync(currentPath, { withFileTypes: true }); - - for (const item of items) { - const fullPath = path.join(currentPath, item.name); - - if (item.isDirectory()) { - // Skip node_modules and .git directories - if (item.name !== 'node_modules' && item.name !== '.git') { - processDirectory(fullPath); - } - } else if (item.isFile()) { - // Check if file has one of the target extensions - const ext = path.extname(item.name).toLowerCase(); - if (extensions.includes(ext)) { - stats.processed++; - if (replacePlaceholdersInFile(fullPath, projectRoot, docOut, removeCompletely)) { - stats.modified++; - if (verbose) { - console.log(`โœ“ Updated: ${fullPath}`); - } - } - } - } - } - } catch (error) { - console.error(`Error processing directory ${currentPath}:`, error.message); - stats.errors++; - } - } - - processDirectory(dirPath); - return stats; -} - -/** - * Legacy function for backward compatibility - */ -function replaceProjectRootInDirectory(dirPath, projectRoot, extensions = ['.md', '.xml'], removeCompletely = false) { - return replacePlaceholdersInDirectory(dirPath, projectRoot, '/docs', extensions, removeCompletely); -} - -/** - * Replace placeholders in a list of specific files - * @param {Array} filePaths - Array of file paths to process - * @param {string} projectRoot - The actual project root path to substitute (or null to remove) - * @param {string} docOut - The document output path (with leading slash) - * @param {boolean} removeCompletely - If true, removes placeholders entirely instead of replacing - * @returns {Object} - Stats object with counts of files processed and modified - */ -function replacePlaceholdersInFiles(filePaths, projectRoot, docOut = '/docs', removeCompletely = false) { - const stats = { - processed: 0, - modified: 0, - errors: 0, - }; - - for (const filePath of filePaths) { - stats.processed++; - try { - if (replacePlaceholdersInFile(filePath, projectRoot, docOut, removeCompletely)) { - stats.modified++; - console.log(`โœ“ Updated: ${filePath}`); - } - } catch (error) { - console.error(`Error processing file ${filePath}:`, error.message); - stats.errors++; - } - } - - return stats; -} - -/** - * Legacy function for backward compatibility - */ -function replaceProjectRootInFiles(filePaths, projectRoot, removeCompletely = false) { - return replacePlaceholdersInFiles(filePaths, projectRoot, '/docs', removeCompletely); -} - -/** - * Main installation helper - replaces {project-root} and {output_folder}/ during BMAD installation - * @param {string} installPath - Path where BMAD is being installed - * @param {string} targetProjectRoot - The project root to set in the files (slash will be added) - * @param {string} docsOutputPath - The documentation output path (relative to project root) - * @param {boolean} verbose - If true, show detailed output - * @returns {Object} - Installation stats - */ -function processInstallation(installPath, targetProjectRoot, docsOutputPath = 'docs', verbose = false) { - // Ensure project root has trailing slash since usage is like {project-root}/bmad - const projectRootWithSlash = targetProjectRoot.endsWith('/') ? targetProjectRoot : targetProjectRoot + '/'; - - // Ensure docs path has leading slash (for internal use) but will add trailing slash during replacement - const normalizedDocsPath = docsOutputPath.replaceAll(/^\/+|\/+$/g, ''); - const docOutPath = normalizedDocsPath ? `/${normalizedDocsPath}` : '/docs'; - - if (verbose) { - console.log(`\nReplacing {project-root} with: ${projectRootWithSlash}`); - console.log(`Replacing {output_folder}/ with: ${docOutPath}/`); - console.log(`Processing files in: ${installPath}\n`); - } - - const stats = replacePlaceholdersInDirectory(installPath, projectRootWithSlash, docOutPath, ['.md', '.xml', '.yaml'], false, verbose); - - if (verbose) { - console.log('\n--- Installation Processing Complete ---'); - } - console.log(`Files processed: ${stats.processed}`); - console.log(`Files modified: ${stats.modified}`); - if (stats.errors > 0) { - console.log(`Errors encountered: ${stats.errors}`); - } - - return stats; -} - -/** - * Bundle helper - removes {project-root}/ references for web bundling - * @param {string} bundlePath - Path where files are being bundled - * @returns {Object} - Bundle stats - */ -function processBundleRemoval(bundlePath) { - console.log(`\nRemoving {project-root}/ references for bundling`); - console.log(`Processing files in: ${bundlePath}\n`); - - const stats = replaceProjectRootInDirectory(bundlePath, null, ['.md', '.xml'], true); - - console.log('\n--- Bundle Processing Complete ---'); - console.log(`Files processed: ${stats.processed}`); - console.log(`Files modified: ${stats.modified}`); - if (stats.errors > 0) { - console.log(`Errors encountered: ${stats.errors}`); - } - - return stats; -} - -module.exports = { - replacePlaceholdersInFile, - replacePlaceholdersInDirectory, - replacePlaceholdersInFiles, - replaceProjectRootInFile, - replaceProjectRootInDirectory, - replaceProjectRootInFiles, - processInstallation, - processBundleRemoval, -}; diff --git a/tools/cli/regenerate-manifests.js b/tools/cli/regenerate-manifests.js deleted file mode 100644 index c370497b..00000000 --- a/tools/cli/regenerate-manifests.js +++ /dev/null @@ -1,27 +0,0 @@ -const path = require('node:path'); -const { ManifestGenerator } = require('./installers/lib/core/manifest-generator'); - -async function regenerateManifests() { - const generator = new ManifestGenerator(); - const targetDir = process.argv[2]; - - // List of modules to include in manifests - const selectedModules = ['bmb', 'bmm', 'cis']; - - console.log('Regenerating manifests with relative paths...'); - console.log('Target directory: .bmad'); - - try { - const result = await generator.generateManifests('.bmad', selectedModules, [], { ides: [] }); - console.log('โœ“ Manifests generated successfully:'); - console.log(` - ${result.workflows} workflows`); - console.log(` - ${result.agents} agents`); - console.log(` - ${result.tasks} tasks`); - console.log(` - ${result.files} files in files-manifest.csv`); - } catch (error) { - console.error('Error generating manifests:', error); - process.exit(1); - } -} - -regenerateManifests(); diff --git a/tools/cli/test-yaml-builder.js b/tools/cli/test-yaml-builder.js deleted file mode 100644 index 1c5bf9bd..00000000 --- a/tools/cli/test-yaml-builder.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Test script for YAML โ†’ XML agent builder - * Usage: node tools/cli/test-yaml-builder.js - */ - -const path = require('node:path'); -const { YamlXmlBuilder } = require('./lib/yaml-xml-builder'); -const { getProjectRoot } = require('./lib/project-root'); - -async function test() { - console.log('Testing YAML โ†’ XML Agent Builder\n'); - - const builder = new YamlXmlBuilder(); - const projectRoot = getProjectRoot(); - - // Paths - const agentYamlPath = path.join(projectRoot, 'src/modules/bmm/agents/pm.agent.yaml'); - const outputPath = path.join(projectRoot, 'test-output-pm.md'); - - console.log(`Source: ${agentYamlPath}`); - console.log(`Output: ${outputPath}\n`); - - try { - const result = await builder.buildAgent( - agentYamlPath, - null, // No customize file for this test - outputPath, - { includeMetadata: true }, - ); - - console.log('โœ“ Build successful!'); - console.log(` Output: ${result.outputPath}`); - console.log(` Source hash: ${result.sourceHash}`); - console.log('\nGenerated XML file at:', outputPath); - console.log('Review the output to verify correctness.\n'); - } catch (error) { - console.error('โœ— Build failed:', error.message); - console.error(error.stack); - process.exit(1); - } -} - -test(); diff --git a/tools/flattener/ignoreRules.js b/tools/flattener/ignoreRules.js index 512f7166..b825edea 100644 --- a/tools/flattener/ignoreRules.js +++ b/tools/flattener/ignoreRules.js @@ -6,7 +6,7 @@ const ignore = require('ignore'); // These complement .gitignore and are applied regardless of VCS presence. const DEFAULT_PATTERNS = [ // Project/VCS - '**/.bmad-method/**', + '**/_bmad/**', '**/.git/**', '**/.svn/**', '**/.hg/**',