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:
parent
9f7ec48a89
commit
eade619d17
|
|
@ -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 };
|
||||
|
|
@ -6,7 +6,6 @@ const { ModuleManager } = require('../modules/manager');
|
|||
const { IdeManager } = require('../ide/manager');
|
||||
const { FileOps } = require('../../../lib/file-ops');
|
||||
const { Config } = require('../../../lib/config');
|
||||
const { DependencyResolver } = require('./dependency-resolver');
|
||||
const { ConfigCollector } = require('./config-collector');
|
||||
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||
|
|
@ -25,7 +24,6 @@ class Installer {
|
|||
this.ideManager = new IdeManager();
|
||||
this.fileOps = new FileOps();
|
||||
this.config = new Config();
|
||||
this.dependencyResolver = new DependencyResolver();
|
||||
this.configCollector = new ConfigCollector();
|
||||
this.ideConfigManager = new IdeConfigManager();
|
||||
this.installedFiles = new Set(); // Track all installed files
|
||||
|
|
@ -543,20 +541,6 @@ class Installer {
|
|||
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
|
||||
spinner.stop('Preparation complete');
|
||||
|
||||
|
|
@ -565,9 +549,6 @@ class Installer {
|
|||
// ─────────────────────────────────────────────────────────────────────────
|
||||
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
|
||||
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
||||
|
||||
|
|
@ -579,7 +560,7 @@ class Installer {
|
|||
installTasks.push({
|
||||
title: isQuickUpdate ? 'Updating BMAD core' : 'Installing BMAD core',
|
||||
task: async (message) => {
|
||||
await this.installCoreWithDependencies(bmadDir, { core: {} });
|
||||
await this.installCore(bmadDir);
|
||||
addResult('Core', 'ok', isQuickUpdate ? 'updated' : 'installed');
|
||||
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
|
||||
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
|
||||
if (allModules && allModules.length > 0) {
|
||||
installTasks.push({
|
||||
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
||||
task: async (message) => {
|
||||
const resolution = taskResolution;
|
||||
const installedModuleNames = new Set();
|
||||
|
||||
for (const moduleName of allModules) {
|
||||
|
|
@ -680,40 +643,30 @@ class Installer {
|
|||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||
});
|
||||
} else {
|
||||
if (!resolution || !resolution.byModule) {
|
||||
addResult(`Module: ${moduleName}`, 'warn', 'skipped (no resolution data)');
|
||||
continue;
|
||||
}
|
||||
// Official module — copy entire module directory
|
||||
if (moduleName === 'core') {
|
||||
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
|
||||
await this.installCore(bmadDir);
|
||||
} 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');
|
||||
}
|
||||
|
||||
// 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'}`;
|
||||
},
|
||||
});
|
||||
|
|
@ -723,11 +676,6 @@ class Installer {
|
|||
installTasks.push({
|
||||
title: 'Creating module directories',
|
||||
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 moduleLogger = {
|
||||
log: async (msg) => (verboseMode ? await prompts.log.message(msg) : undefined),
|
||||
|
|
@ -736,7 +684,7 @@ class Installer {
|
|||
};
|
||||
|
||||
// Core module directories
|
||||
if (config.installCore || resolution.byModule.core) {
|
||||
if (config.installCore) {
|
||||
const result = await this.moduleManager.createModuleDirectories('core', bmadDir, {
|
||||
installedIDEs: config.ides || [],
|
||||
moduleConfig: moduleConfigs.core || {},
|
||||
|
|
@ -844,9 +792,6 @@ class Installer {
|
|||
// Now run configuration generation
|
||||
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
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -1418,144 +1363,6 @@ class Installer {
|
|||
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
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
|
|
|
|||
Loading…
Reference in New Issue