@cloudkinetix/bmad-enhanced
Version:
Cloud-Kinetix enhanced fork of BMAD-METHOD - Breakthrough Method of Agile AI-driven Development with robust versioning and unified validation.
186 lines (157 loc) • 5.83 kB
JavaScript
const fs = require("fs").promises;
const path = require("path");
const yaml = require("js-yaml");
class DependencyResolver {
constructor(bmadCorePath) {
this.bmadCore = bmadCorePath;
this.cache = new Map();
}
async resolveAgentDependencies(agentId) {
const agentPath = path.join(this.bmadCore, "agents", `${agentId}.md`);
const agentContent = await fs.readFile(agentPath, "utf8");
// Extract YAML from markdown content
// Handle both Unix (\n) and Windows (\r\n) line endings
const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)\r?\n```/);
if (!yamlMatch) {
throw new Error(`No YAML configuration found in agent ${agentId}`);
}
// Clean up the YAML to handle command descriptions after dashes
let yamlContent = yamlMatch[1];
// Normalize line endings to Unix format for consistent processing
yamlContent = yamlContent.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
// Fix commands section: convert "- command - description" to just "- command"
yamlContent = yamlContent.replace(/^(\s*-)(\s*"[^"]+")(\s*-\s*.*)$/gm, "$1$2");
const agentConfig = yaml.load(yamlContent);
const dependencies = {
agent: {
id: agentId,
path: `agents#${agentId}`,
content: agentContent,
config: agentConfig,
},
resources: [],
};
// Personas are now embedded in agent configs, no need to resolve separately
// Resolve other dependencies
const depTypes = ["tasks", "templates", "checklists", "data", "utils"];
for (const depType of depTypes) {
const deps = agentConfig.dependencies?.[depType] || [];
for (const depId of deps) {
const resource = await this.loadResource(depType, depId);
if (resource) dependencies.resources.push(resource);
}
}
return dependencies;
}
async resolveTeamDependencies(teamId) {
// Handle both 'fullstack' and 'team-fullstack' formats
const teamFileName = teamId.startsWith("team-") ? teamId : `team-${teamId}`;
const teamPath = path.join(this.bmadCore, "agent-teams", `${teamFileName}.yaml`);
const teamContent = await fs.readFile(teamPath, "utf8");
const teamConfig = yaml.load(teamContent);
const dependencies = {
team: {
id: teamId,
path: `agent-teams#${teamId}`,
content: teamContent,
config: teamConfig,
},
agents: [],
resources: new Map(), // Use Map to deduplicate resources
};
// Always add bmad-orchestrator agent first if it's a team
const bmadAgent = await this.resolveAgentDependencies("bmad-orchestrator");
dependencies.agents.push(bmadAgent.agent);
bmadAgent.resources.forEach((res) => {
dependencies.resources.set(res.path, res);
});
// Resolve all agents in the team
let agentsToResolve = teamConfig.agents || [];
// Handle wildcard "*" - include all agents except bmad-master
if (agentsToResolve.includes("*")) {
const allAgents = await this.listAgents();
// Remove wildcard and add all agents except those already in the list and bmad-master
agentsToResolve = agentsToResolve.filter((a) => a !== "*");
for (const agent of allAgents) {
if (!agentsToResolve.includes(agent) && agent !== "bmad-master") {
agentsToResolve.push(agent);
}
}
}
for (const agentId of agentsToResolve) {
if (agentId === "bmad-orchestrator" || agentId === "bmad-master") continue; // Already added or excluded
const agentDeps = await this.resolveAgentDependencies(agentId);
dependencies.agents.push(agentDeps.agent);
// Add resources with deduplication
agentDeps.resources.forEach((res) => {
dependencies.resources.set(res.path, res);
});
}
// Resolve workflows
for (const workflowId of teamConfig.workflows || []) {
const resource = await this.loadResource("workflows", workflowId);
if (resource) dependencies.resources.set(resource.path, resource);
}
// Convert Map back to array
dependencies.resources = Array.from(dependencies.resources.values());
return dependencies;
}
async loadResource(type, id) {
const cacheKey = `${type}#${id}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const extensions = [".md", ".yaml"];
let content = null;
let filePath = null;
for (const ext of extensions) {
try {
filePath = path.join(this.bmadCore, type, `${id}${ext}`);
content = await fs.readFile(filePath, "utf8");
break;
} catch (e) {
// Try next extension
}
}
if (!content) {
console.warn(`Resource not found: ${type}/${id}`);
return null;
}
const resource = {
type,
id,
path: `${type}#${id}`,
content,
};
this.cache.set(cacheKey, resource);
return resource;
} catch (error) {
console.error(`Error loading resource ${type}/${id}:`, error.message);
return null;
}
}
async listAgents() {
try {
const files = await fs.readdir(path.join(this.bmadCore, "agents"));
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
} catch (error) {
return [];
}
}
async listTeams() {
try {
const files = await fs.readdir(path.join(this.bmadCore, "agent-teams"));
return files
.filter((f) => f.endsWith(".yaml"))
.map((f) => {
// Remove .yaml extension and team- prefix if present
const name = f.replace(".yaml", "");
return name.startsWith("team-") ? name.substring(5) : name;
});
} catch (error) {
return [];
}
}
}
module.exports = DependencyResolver;