refactor(installer): remove dead dependency resolver

No skills declare cross-module dependencies. The resolver ran four
resolution passes on every install, but both consumers
(installCoreWithDependencies, installModuleWithDependencies) ignored
the resolution data and copied entire module directories anyway.
The sole non-dead consumer (installPartialModule) never fired.

Delete dependency-resolver.js (743 lines), three dead wrapper methods,
and all feeding/filtering logic in install(). Official modules now
call installCore/moduleManager.install directly.
This commit is contained in:
Alex Verkhovsky 2026-03-21 03:12:00 -06:00
parent 9f7ec48a89
commit eade619d17
2 changed files with 18 additions and 954 deletions

View File

@ -1,743 +0,0 @@
const fs = require('fs-extra');
const path = require('node:path');
const glob = require('glob');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/**
* Dependency Resolver for BMAD modules
* Handles cross-module dependencies and ensures all required files are included
*/
class DependencyResolver {
constructor() {
this.dependencies = new Map();
this.resolvedFiles = new Set();
this.missingDependencies = new Set();
}
/**
* Resolve all dependencies for selected modules
* @param {string} bmadDir - BMAD installation directory
* @param {Array} selectedModules - Modules explicitly selected by user
* @param {Object} options - Resolution options
* @returns {Object} Resolution results with all required files
*/
async resolve(bmadDir, selectedModules = [], options = {}) {
if (options.verbose) {
await prompts.log.info('Resolving module dependencies...');
}
// Always include core as base
const modulesToProcess = new Set(['core', ...selectedModules]);
// First pass: collect all explicitly selected files
const primaryFiles = await this.collectPrimaryFiles(bmadDir, modulesToProcess, options);
// Second pass: parse and resolve dependencies
const allDependencies = await this.parseDependencies(primaryFiles);
// Third pass: resolve dependency paths and collect files
const resolvedDeps = await this.resolveDependencyPaths(bmadDir, allDependencies);
// Fourth pass: check for transitive dependencies
const transitiveDeps = await this.resolveTransitiveDependencies(bmadDir, resolvedDeps);
// Combine all files
const allFiles = new Set([...primaryFiles.map((f) => f.path), ...resolvedDeps, ...transitiveDeps]);
// Organize by module
const organizedFiles = this.organizeByModule(bmadDir, allFiles);
// Report results (only in verbose mode)
if (options.verbose) {
await this.reportResults(organizedFiles, selectedModules);
}
return {
primaryFiles,
dependencies: resolvedDeps,
transitiveDependencies: transitiveDeps,
allFiles: [...allFiles],
byModule: organizedFiles,
missing: [...this.missingDependencies],
};
}
/**
* Collect primary files from selected modules
*/
async collectPrimaryFiles(bmadDir, modules, options = {}) {
const files = [];
const { moduleManager } = options;
for (const module of modules) {
// Skip external modules - they're installed from cache, not from source
if (moduleManager && (await moduleManager.isExternalModule(module))) {
continue;
}
// Handle both source (src/) and installed (bmad/) directory structures
let moduleDir;
// Check if this is a source directory (has 'src' subdirectory)
const srcDir = path.join(bmadDir, 'src');
if (await fs.pathExists(srcDir)) {
// Source directory structure: src/core-skills or src/bmm-skills
if (module === 'core') {
moduleDir = path.join(srcDir, 'core-skills');
} else if (module === 'bmm') {
moduleDir = path.join(srcDir, 'bmm-skills');
}
}
if (!moduleDir) {
continue;
}
if (!(await fs.pathExists(moduleDir))) {
await prompts.log.warn('Module directory not found: ' + moduleDir);
continue;
}
// Collect agents
const agentsDir = path.join(moduleDir, 'agents');
if (await fs.pathExists(agentsDir)) {
const agentFiles = await glob.glob('*.md', { cwd: agentsDir });
for (const file of agentFiles) {
const agentPath = path.join(agentsDir, file);
// Check for localskip attribute
const content = await fs.readFile(agentPath, 'utf8');
const hasLocalSkip = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
if (hasLocalSkip) {
continue; // Skip agents marked for web-only
}
files.push({
path: agentPath,
type: 'agent',
module,
name: path.basename(file, '.md'),
});
}
}
// Collect tasks
const tasksDir = path.join(moduleDir, 'tasks');
if (await fs.pathExists(tasksDir)) {
const taskFiles = await glob.glob('*.md', { cwd: tasksDir });
for (const file of taskFiles) {
files.push({
path: path.join(tasksDir, file),
type: 'task',
module,
name: path.basename(file, '.md'),
});
}
}
}
return files;
}
/**
* Parse dependencies from file content
*/
async parseDependencies(files) {
const allDeps = new Set();
for (const file of files) {
const content = await fs.readFile(file.path, 'utf8');
// Parse YAML frontmatter for explicit dependencies
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) {
try {
// Pre-process to handle backticks in YAML values
let yamlContent = frontmatterMatch[1];
// Quote values with backticks to make them valid YAML
yamlContent = yamlContent.replaceAll(/: `([^`]+)`/g, ': "$1"');
const frontmatter = yaml.parse(yamlContent);
if (frontmatter.dependencies) {
const deps = Array.isArray(frontmatter.dependencies) ? frontmatter.dependencies : [frontmatter.dependencies];
for (const dep of deps) {
allDeps.add({
from: file.path,
dependency: dep,
type: 'explicit',
});
}
}
// Check for template dependencies
if (frontmatter.template) {
const templates = Array.isArray(frontmatter.template) ? frontmatter.template : [frontmatter.template];
for (const template of templates) {
allDeps.add({
from: file.path,
dependency: template,
type: 'template',
});
}
}
} catch (error) {
await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message);
}
}
// Parse content for command references (cross-module dependencies)
const commandRefs = this.parseCommandReferences(content);
for (const ref of commandRefs) {
allDeps.add({
from: file.path,
dependency: ref,
type: 'command',
});
}
// Parse for file path references
const fileRefs = this.parseFileReferences(content);
for (const ref of fileRefs) {
// Determine type based on path format
// Paths starting with bmad/ are absolute references to the bmad installation
const depType = ref.startsWith('bmad/') ? 'bmad-path' : 'file';
allDeps.add({
from: file.path,
dependency: ref,
type: depType,
});
}
}
return allDeps;
}
/**
* Parse command references from content
*/
parseCommandReferences(content) {
const refs = new Set();
// Match @task-{name} or @agent-{name} or @{module}-{type}-{name}
const commandPattern = /@(task-|agent-|bmad-)([a-z0-9-]+)/g;
let match;
while ((match = commandPattern.exec(content)) !== null) {
refs.add(match[0]);
}
// Match file paths like bmad/core/agents/analyst
const pathPattern = /bmad\/(core|bmm|cis)\/(agents|tasks)\/([a-z0-9-]+)/g;
while ((match = pathPattern.exec(content)) !== null) {
refs.add(match[0]);
}
return [...refs];
}
/**
* Parse file path references from content
*/
parseFileReferences(content) {
const refs = new Set();
// Match relative paths like ../templates/file.yaml or ./data/file.md
const relativePattern = /['"](\.\.?\/[^'"]+\.(md|yaml|yml|xml|json|txt|csv))['"]/g;
let match;
while ((match = relativePattern.exec(content)) !== null) {
refs.add(match[1]);
}
// Parse exec attributes in command tags
const execPattern = /exec="([^"]+)"/g;
while ((match = execPattern.exec(content)) !== null) {
let execPath = match[1];
if (execPath && execPath !== '*') {
// Remove {project-root} prefix to get the actual path
// Usage is like {project-root}/bmad/core/tasks/foo.md
if (execPath.includes('{project-root}')) {
execPath = execPath.replace('{project-root}', '');
}
refs.add(execPath);
}
}
// Parse tmpl attributes in command tags
const tmplPattern = /tmpl="([^"]+)"/g;
while ((match = tmplPattern.exec(content)) !== null) {
let tmplPath = match[1];
if (tmplPath && tmplPath !== '*') {
// Remove {project-root} prefix to get the actual path
// Usage is like {project-root}/bmad/core/tasks/foo.md
if (tmplPath.includes('{project-root}')) {
tmplPath = tmplPath.replace('{project-root}', '');
}
refs.add(tmplPath);
}
}
return [...refs];
}
/**
* Resolve dependency paths to actual files
*/
async resolveDependencyPaths(bmadDir, dependencies) {
const resolved = new Set();
for (const dep of dependencies) {
const resolvedPaths = await this.resolveSingleDependency(bmadDir, dep);
for (const path of resolvedPaths) {
resolved.add(path);
}
}
return resolved;
}
/**
* Resolve a single dependency to file paths
*/
async resolveSingleDependency(bmadDir, dep) {
const paths = [];
switch (dep.type) {
case 'explicit':
case 'file': {
let depPath = dep.dependency;
// Handle {project-root} prefix if present
if (depPath.includes('{project-root}')) {
// Remove {project-root} and resolve as bmad path
depPath = depPath.replace('{project-root}', '');
if (depPath.startsWith('bmad/')) {
const bmadPath = depPath.replace(/^bmad\//, '');
// Handle glob patterns
if (depPath.includes('*')) {
// Extract the base path and pattern
const pathParts = bmadPath.split('/');
const module = pathParts[0];
const filePattern = pathParts.at(-1);
const middlePath = pathParts.slice(1, -1).join('/');
let basePath;
if (module === 'core') {
basePath = path.join(bmadDir, 'core', middlePath);
} else {
basePath = path.join(bmadDir, 'modules', module, middlePath);
}
if (await fs.pathExists(basePath)) {
const files = await glob.glob(filePattern, { cwd: basePath });
for (const file of files) {
paths.push(path.join(basePath, file));
}
}
} else {
// Direct path
if (bmadPath.startsWith('core/')) {
const corePath = path.join(bmadDir, bmadPath);
if (await fs.pathExists(corePath)) {
paths.push(corePath);
}
} else {
const parts = bmadPath.split('/');
const module = parts[0];
const rest = parts.slice(1).join('/');
const modulePath = path.join(bmadDir, 'modules', module, rest);
if (await fs.pathExists(modulePath)) {
paths.push(modulePath);
}
}
}
}
} else {
// Regular relative path handling
const sourceDir = path.dirname(dep.from);
// Handle glob patterns
if (depPath.includes('*')) {
const basePath = path.resolve(sourceDir, path.dirname(depPath));
const pattern = path.basename(depPath);
if (await fs.pathExists(basePath)) {
const files = await glob.glob(pattern, { cwd: basePath });
for (const file of files) {
paths.push(path.join(basePath, file));
}
}
} else {
// Direct file reference
const fullPath = path.resolve(sourceDir, depPath);
if (await fs.pathExists(fullPath)) {
paths.push(fullPath);
} else {
this.missingDependencies.add(`${depPath} (referenced by ${path.basename(dep.from)})`);
}
}
}
break;
}
case 'command': {
// Resolve command references to actual files
const commandPath = await this.resolveCommandToPath(bmadDir, dep.dependency);
if (commandPath) {
paths.push(commandPath);
}
break;
}
case 'bmad-path': {
// Resolve bmad/ paths (from {project-root}/bmad/... references)
// These are paths relative to the src directory structure
const bmadPath = dep.dependency.replace(/^bmad\//, '');
// Try to resolve as if it's in src structure
// bmad/core/tasks/foo.md -> src/core-skills/tasks/foo.md
// bmad/bmm/tasks/bar.md -> src/bmm-skills/tasks/bar.md (bmm is directly under src/)
// bmad/cis/agents/bar.md -> src/modules/cis/agents/bar.md
if (bmadPath.startsWith('core/')) {
const corePath = path.join(bmadDir, bmadPath);
if (await fs.pathExists(corePath)) {
paths.push(corePath);
} else {
// Not found, but don't report as missing since it might be installed later
}
} else {
// It's a module path like bmm/tasks/foo.md or cis/agents/bar.md
const parts = bmadPath.split('/');
const module = parts[0];
const rest = parts.slice(1).join('/');
let modulePath;
if (module === 'bmm') {
// bmm is directly under src/
modulePath = path.join(bmadDir, module, rest);
} else {
// Other modules are under modules/
modulePath = path.join(bmadDir, 'modules', module, rest);
}
if (await fs.pathExists(modulePath)) {
paths.push(modulePath);
} else {
// Not found, but don't report as missing since it might be installed later
}
}
break;
}
case 'template': {
// Resolve template references
let templateDep = dep.dependency;
// Handle {project-root} prefix if present
if (templateDep.includes('{project-root}')) {
// Remove {project-root} and treat as bmad-path
templateDep = templateDep.replace('{project-root}', '');
// Now resolve as a bmad path
if (templateDep.startsWith('bmad/')) {
const bmadPath = templateDep.replace(/^bmad\//, '');
if (bmadPath.startsWith('core/')) {
const corePath = path.join(bmadDir, bmadPath);
if (await fs.pathExists(corePath)) {
paths.push(corePath);
}
} else {
// Module path like cis/templates/brainstorm.md
const parts = bmadPath.split('/');
const module = parts[0];
const rest = parts.slice(1).join('/');
const modulePath = path.join(bmadDir, 'modules', module, rest);
if (await fs.pathExists(modulePath)) {
paths.push(modulePath);
}
}
}
} else {
// Regular relative template path
const sourceDir = path.dirname(dep.from);
const templatePath = path.resolve(sourceDir, templateDep);
if (await fs.pathExists(templatePath)) {
paths.push(templatePath);
} else {
this.missingDependencies.add(`Template: ${dep.dependency}`);
}
}
break;
}
// No default
}
return paths;
}
/**
* Resolve command reference to file path
*/
async resolveCommandToPath(bmadDir, command) {
// Parse command format: @task-name or @agent-name or bmad/module/type/name
if (command.startsWith('@task-')) {
const taskName = command.slice(6);
// Search all modules for this task
for (const module of ['core', 'bmm', 'cis']) {
const taskPath =
module === 'core'
? path.join(bmadDir, 'core', 'tasks', `${taskName}.md`)
: path.join(bmadDir, 'modules', module, 'tasks', `${taskName}.md`);
if (await fs.pathExists(taskPath)) {
return taskPath;
}
}
} else if (command.startsWith('@agent-')) {
const agentName = command.slice(7);
// Search all modules for this agent
for (const module of ['core', 'bmm', 'cis']) {
const agentPath =
module === 'core'
? path.join(bmadDir, 'core', 'agents', `${agentName}.md`)
: path.join(bmadDir, 'modules', module, 'agents', `${agentName}.md`);
if (await fs.pathExists(agentPath)) {
return agentPath;
}
}
} else if (command.startsWith('bmad/')) {
// Direct path reference
const parts = command.split('/');
if (parts.length >= 4) {
const [, module, type, ...nameParts] = parts;
const name = nameParts.join('/'); // Handle nested paths
// Check if name already has extension
const fileName = name.endsWith('.md') ? name : `${name}.md`;
const filePath =
module === 'core' ? path.join(bmadDir, 'core', type, fileName) : path.join(bmadDir, 'modules', module, type, fileName);
if (await fs.pathExists(filePath)) {
return filePath;
}
}
}
// Don't report as missing if it's a self-reference within the module being installed
if (!command.includes('cis') || command.includes('brain')) {
// Only report missing if it's a true external dependency
// this.missingDependencies.add(`Command: ${command}`);
}
return null;
}
/**
* Resolve transitive dependencies (dependencies of dependencies)
*/
async resolveTransitiveDependencies(bmadDir, directDeps) {
const transitive = new Set();
const processed = new Set();
// Process each direct dependency
for (const depPath of directDeps) {
if (processed.has(depPath)) continue;
processed.add(depPath);
// Only process markdown and YAML files for transitive deps
if ((depPath.endsWith('.md') || depPath.endsWith('.yaml') || depPath.endsWith('.yml')) && (await fs.pathExists(depPath))) {
const content = await fs.readFile(depPath, 'utf8');
const subDeps = await this.parseDependencies([
{
path: depPath,
type: 'dependency',
module: this.getModuleFromPath(bmadDir, depPath),
name: path.basename(depPath),
},
]);
const resolvedSubDeps = await this.resolveDependencyPaths(bmadDir, subDeps);
for (const subDep of resolvedSubDeps) {
if (!directDeps.has(subDep)) {
transitive.add(subDep);
}
}
}
}
return transitive;
}
/**
* Get module name from file path
*/
getModuleFromPath(bmadDir, filePath) {
const relative = path.relative(bmadDir, filePath);
const parts = relative.split(path.sep);
// Handle source directory structure (src/core-skills, src/bmm-skills, or src/modules/xxx)
if (parts[0] === 'src') {
if (parts[1] === 'core-skills') {
return 'core';
} else if (parts[1] === 'bmm-skills') {
return 'bmm';
} else if (parts[1] === 'modules' && parts.length > 2) {
return parts[2];
}
}
// Check if it's in modules directory (installed structure)
if (parts[0] === 'modules' && parts.length > 1) {
return parts[1];
}
// Otherwise return the first part (core, etc.)
// But don't return 'src' as a module name
if (parts[0] === 'src') {
return 'unknown';
}
return parts[0] || 'unknown';
}
/**
* Organize files by module
*/
organizeByModule(bmadDir, files) {
const organized = {};
for (const file of files) {
const module = this.getModuleFromPath(bmadDir, file);
if (!organized[module]) {
organized[module] = {
agents: [],
tasks: [],
tools: [],
templates: [],
data: [],
other: [],
};
}
// Get relative path correctly based on module structure
let moduleBase;
// Check if file is in source directory structure
if (file.includes('/src/core-skills/') || file.includes('/src/bmm-skills/')) {
if (module === 'core') {
moduleBase = path.join(bmadDir, 'src', 'core-skills');
} else if (module === 'bmm') {
moduleBase = path.join(bmadDir, 'src', 'bmm-skills');
}
} else {
moduleBase = module === 'core' ? path.join(bmadDir, 'core') : path.join(bmadDir, 'modules', module);
}
const relative = path.relative(moduleBase, file);
if (relative.startsWith('agents/') || file.includes('/agents/')) {
organized[module].agents.push(file);
} else if (relative.startsWith('tasks/') || file.includes('/tasks/')) {
organized[module].tasks.push(file);
} else if (relative.startsWith('tools/') || file.includes('/tools/')) {
organized[module].tools.push(file);
} else if (relative.includes('data/')) {
organized[module].data.push(file);
} else {
organized[module].other.push(file);
}
}
return organized;
}
/**
* Report resolution results
*/
async reportResults(organized, selectedModules) {
await prompts.log.success('Dependency resolution complete');
for (const [module, files] of Object.entries(organized)) {
const isSelected = selectedModules.includes(module) || module === 'core';
const totalFiles =
files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;
if (totalFiles > 0) {
await prompts.log.info(` ${module.toUpperCase()} module:`);
await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`);
if (files.agents.length > 0) {
await prompts.log.message(` Agents: ${files.agents.length}`);
}
if (files.tasks.length > 0) {
await prompts.log.message(` Tasks: ${files.tasks.length}`);
}
if (files.templates.length > 0) {
await prompts.log.message(` Templates: ${files.templates.length}`);
}
if (files.data.length > 0) {
await prompts.log.message(` Data files: ${files.data.length}`);
}
if (files.other.length > 0) {
await prompts.log.message(` Other files: ${files.other.length}`);
}
}
}
if (this.missingDependencies.size > 0) {
await prompts.log.warn('Missing dependencies:');
for (const missing of this.missingDependencies) {
await prompts.log.warn(` - ${missing}`);
}
}
}
/**
* Create a bundle for web deployment
* @param {Object} resolution - Resolution results from resolve()
* @returns {Object} Bundle data ready for web
*/
async createWebBundle(resolution) {
const bundle = {
metadata: {
created: new Date().toISOString(),
modules: Object.keys(resolution.byModule),
totalFiles: resolution.allFiles.length,
},
agents: {},
tasks: {},
templates: {},
data: {},
};
// Bundle all files by type
for (const filePath of resolution.allFiles) {
if (!(await fs.pathExists(filePath))) continue;
const content = await fs.readFile(filePath, 'utf8');
const relative = path.relative(path.dirname(resolution.primaryFiles[0]?.path || '.'), filePath);
if (filePath.includes('/agents/')) {
bundle.agents[relative] = content;
} else if (filePath.includes('/tasks/')) {
bundle.tasks[relative] = content;
} else if (filePath.includes('template')) {
bundle.templates[relative] = content;
} else {
bundle.data[relative] = content;
}
}
return bundle;
}
}
module.exports = { DependencyResolver };

View File

@ -6,7 +6,6 @@ const { ModuleManager } = require('../modules/manager');
const { IdeManager } = require('../ide/manager'); const { IdeManager } = require('../ide/manager');
const { FileOps } = require('../../../lib/file-ops'); const { FileOps } = require('../../../lib/file-ops');
const { Config } = require('../../../lib/config'); const { Config } = require('../../../lib/config');
const { DependencyResolver } = require('./dependency-resolver');
const { ConfigCollector } = require('./config-collector'); const { ConfigCollector } = require('./config-collector');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
@ -25,7 +24,6 @@ class Installer {
this.ideManager = new IdeManager(); this.ideManager = new IdeManager();
this.fileOps = new FileOps(); this.fileOps = new FileOps();
this.config = new Config(); this.config = new Config();
this.dependencyResolver = new DependencyResolver();
this.configCollector = new ConfigCollector(); this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager(); this.ideConfigManager = new IdeConfigManager();
this.installedFiles = new Set(); // Track all installed files this.installedFiles = new Set(); // Track all installed files
@ -543,20 +541,6 @@ class Installer {
allModules = allModules.filter((m) => m !== 'core'); allModules = allModules.filter((m) => m !== 'core');
} }
// For dependency resolution, we only need regular modules (not custom modules)
// Custom modules are already installed in _bmad and don't need dependency resolution from source
const regularModulesForResolution = allModules.filter((module) => {
// Check if this is a custom module
const isCustom =
customModulePaths.has(module) ||
(finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
(finalCustomContent &&
finalCustomContent.selected &&
finalCustomContent.selectedFiles &&
finalCustomContent.selectedFiles.some((f) => f.includes(module)));
return !isCustom;
});
// Stop spinner before tasks() takes over progress display // Stop spinner before tasks() takes over progress display
spinner.stop('Preparation complete'); spinner.stop('Preparation complete');
@ -565,9 +549,6 @@ class Installer {
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
const isQuickUpdate = config._quickUpdate || false; const isQuickUpdate = config._quickUpdate || false;
// Shared resolution result across task callbacks (closure-scoped, not on `this`)
let taskResolution;
// Collect directory creation results for output after tasks() completes // Collect directory creation results for output after tasks() completes
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] }; const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
@ -579,7 +560,7 @@ class Installer {
installTasks.push({ installTasks.push({
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core', title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
task: async (message) => { task: async (message) => {
await this.installCoreWithDependencies(bmadDir, { core: {} }); await this.installCore(bmadDir);
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed'); addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} }); await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
return isQuickUpdate ? 'Core updated' : 'Core installed'; return isQuickUpdate ? 'Core updated' : 'Core installed';
@ -587,29 +568,11 @@ class Installer {
}); });
} }
// Dependency resolution task
installTasks.push({
title: 'Resolving dependencies',
task: async (message) => {
// Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({
bmadDir: bmadDir,
});
taskResolution = await this.dependencyResolver.resolve(srcDir, regularModulesForResolution, {
verbose: config.verbose,
moduleManager: tempModuleManager,
});
return 'Dependencies resolved';
},
});
// Module installation task // Module installation task
if (allModules && allModules.length > 0) { if (allModules && allModules.length > 0) {
installTasks.push({ installTasks.push({
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`, title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
task: async (message) => { task: async (message) => {
const resolution = taskResolution;
const installedModuleNames = new Set(); const installedModuleNames = new Set();
for (const moduleName of allModules) { for (const moduleName of allModules) {
@ -680,40 +643,30 @@ class Installer {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig }, [moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
}); });
} else { } else {
if (!resolution || !resolution.byModule) { // Official module — copy entire module directory
addResult(`Module: ${moduleName}`, 'warn', 'skipped (no resolution data)');
continue;
}
if (moduleName === 'core') { if (moduleName === 'core') {
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]); await this.installCore(bmadDir);
} else { } else {
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]); const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
await this.moduleManager.install(
moduleName,
bmadDir,
(filePath) => {
this.installedFiles.add(filePath);
},
{
skipModuleInstaller: true,
moduleConfig: moduleConfig,
installer: this,
silent: true,
},
);
} }
} }
addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed'); addResult(`Module: ${moduleName}`, 'ok', isQuickUpdate ? 'updated' : 'installed');
} }
// Install partial modules (only dependencies)
if (!resolution || !resolution.byModule) {
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
}
for (const [module, files] of Object.entries(resolution.byModule)) {
if (!allModules.includes(module) && module !== 'core') {
const totalFiles =
files.agents.length +
files.tasks.length +
files.tools.length +
files.templates.length +
files.data.length +
files.other.length;
if (totalFiles > 0) {
message(`Installing ${module} dependencies...`);
await this.installPartialModule(module, bmadDir, files);
}
}
}
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`; return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
}, },
}); });
@ -723,11 +676,6 @@ class Installer {
installTasks.push({ installTasks.push({
title: 'Creating module directories', title: 'Creating module directories',
task: async (message) => { task: async (message) => {
const resolution = taskResolution;
if (!resolution || !resolution.byModule) {
addResult('Module directories', 'warn', 'no resolution data');
return 'Module directories skipped (no resolution data)';
}
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose; const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
const moduleLogger = { const moduleLogger = {
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined), log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
@ -736,7 +684,7 @@ class Installer {
}; };
// Core module directories // Core module directories
if (config.installCore || resolution.byModule.core) { if (config.installCore) {
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, { const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
installedIDEs: config.ides || [], installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {}, moduleConfig: moduleConfigs.core || {},
@ -844,9 +792,6 @@ class Installer {
// Now run configuration generation // Now run configuration generation
await prompts.tasks([configTask]); await prompts.tasks([configTask]);
// Resolution is now available via closure-scoped taskResolution
const resolution = taskResolution;
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// IDE SETUP: Keep as spinner since it may prompt for user input // IDE SETUP: Keep as spinner since it may prompt for user input
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
@ -1418,144 +1363,6 @@ class Installer {
return { customFiles, modifiedFiles }; return { customFiles, modifiedFiles };
} }
/**
* Install core with resolved dependencies
* @param {string} bmadDir - BMAD installation directory
* @param {Object} coreFiles - Core files to install
*/
async installCoreWithDependencies(bmadDir, coreFiles) {
const sourcePath = getModulePath('core');
const targetPath = path.join(bmadDir, 'core');
await this.installCore(bmadDir);
}
/**
* Install module with resolved dependencies
* @param {string} moduleName - Module name
* @param {string} bmadDir - BMAD installation directory
* @param {Object} moduleFiles - Module files to install
*/
async installModuleWithDependencies(moduleName, bmadDir, moduleFiles) {
// Get module configuration for conditional installation
const moduleConfig = this.configCollector.collectedConfig[moduleName] || {};
// Use existing module manager for full installation with file tracking
// Note: Module-specific installers are called separately after IDE setup
await this.moduleManager.install(
moduleName,
bmadDir,
(filePath) => {
this.installedFiles.add(filePath);
},
{
skipModuleInstaller: true, // We'll run it later after IDE setup
moduleConfig: moduleConfig, // Pass module config for conditional filtering
installer: this,
silent: true,
},
);
// Dependencies are already included in full module install
}
/**
* Install partial module (only dependencies needed by other modules)
*/
async installPartialModule(moduleName, bmadDir, files) {
const sourceBase = getModulePath(moduleName);
const targetBase = path.join(bmadDir, moduleName);
// Create module directory
await fs.ensureDir(targetBase);
// Copy only the required dependency files
if (files.agents && files.agents.length > 0) {
const agentsDir = path.join(targetBase, 'agents');
await fs.ensureDir(agentsDir);
for (const agentPath of files.agents) {
const fileName = path.basename(agentPath);
const sourcePath = path.join(sourceBase, 'agents', fileName);
const targetPath = path.join(agentsDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFile(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
}
if (files.tasks && files.tasks.length > 0) {
const tasksDir = path.join(targetBase, 'tasks');
await fs.ensureDir(tasksDir);
for (const taskPath of files.tasks) {
const fileName = path.basename(taskPath);
const sourcePath = path.join(sourceBase, 'tasks', fileName);
const targetPath = path.join(tasksDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFile(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
}
if (files.tools && files.tools.length > 0) {
const toolsDir = path.join(targetBase, 'tools');
await fs.ensureDir(toolsDir);
for (const toolPath of files.tools) {
const fileName = path.basename(toolPath);
const sourcePath = path.join(sourceBase, 'tools', fileName);
const targetPath = path.join(toolsDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFile(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
}
if (files.templates && files.templates.length > 0) {
const templatesDir = path.join(targetBase, 'templates');
await fs.ensureDir(templatesDir);
for (const templatePath of files.templates) {
const fileName = path.basename(templatePath);
const sourcePath = path.join(sourceBase, 'templates', fileName);
const targetPath = path.join(templatesDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFile(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
}
if (files.data && files.data.length > 0) {
for (const dataPath of files.data) {
// Preserve directory structure for data files
const relative = path.relative(sourceBase, dataPath);
const targetPath = path.join(targetBase, relative);
await fs.ensureDir(path.dirname(targetPath));
if (await fs.pathExists(dataPath)) {
await this.copyFile(dataPath, targetPath);
this.installedFiles.add(targetPath);
}
}
}
// Create a marker file to indicate this is a partial installation
const markerPath = path.join(targetBase, '.partial');
await fs.writeFile(
markerPath,
`This module contains only dependencies required by other modules.\nInstalled: ${new Date().toISOString()}\n`,
);
}
/** /**
* Generate clean config.yaml files for each installed module * Generate clean config.yaml files for each installed module
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory