BMAD-METHOD/tools/installer/lib/resource-locator.js

311 lines
8.3 KiB
JavaScript

/**
* Resource Locator - Centralized file path resolution and caching
* Reduces duplicate file system operations and memory usage
*/
const path = require('node:path')
const fs = require('fs-extra')
const moduleManager = require('./module-manager')
class ResourceLocator {
constructor () {
this._pathCache = new Map()
this._globCache = new Map()
this._bmadCorePath = null
this._expansionPacksPath = null
}
/**
* Get the base path for bmad-core
*/
getBmadCorePath () {
if (!this._bmadCorePath) {
this._bmadCorePath = path.join(__dirname, '../../../bmad-core')
}
return this._bmadCorePath
}
/**
* Get the base path for expansion packs
*/
getExpansionPacksPath () {
if (!this._expansionPacksPath) {
this._expansionPacksPath = path.join(__dirname, '../../../expansion-packs')
}
return this._expansionPacksPath
}
/**
* Find all files matching a pattern, with caching
* @param {string} pattern - Glob pattern
* @param {Object} options - Glob options
* @returns {Promise<string[]>} Array of matched file paths
*/
async findFiles (pattern, options = {}) {
const cacheKey = `${pattern}:${JSON.stringify(options)}`
if (this._globCache.has(cacheKey)) {
return this._globCache.get(cacheKey)
}
const { glob } = await moduleManager.getModules(['glob'])
const files = await glob(pattern, options)
// Cache for 5 minutes
this._globCache.set(cacheKey, files)
setTimeout(() => this._globCache.delete(cacheKey), 5 * 60 * 1000)
return files
}
/**
* Get agent path with caching
* @param {string} agentId - Agent identifier
* @returns {Promise<string|null>} Path to agent file or null if not found
*/
async getAgentPath (agentId) {
const cacheKey = `agent:${agentId}`
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
// Check in bmad-core
let agentPath = path.join(this.getBmadCorePath(), 'agents', `${agentId}.md`)
if (await fs.pathExists(agentPath)) {
this._pathCache.set(cacheKey, agentPath)
return agentPath
}
// Check in expansion packs
const expansionPacks = await this.getExpansionPacks()
for (const pack of expansionPacks) {
agentPath = path.join(pack.path, 'agents', `${agentId}.md`)
if (await fs.pathExists(agentPath)) {
this._pathCache.set(cacheKey, agentPath)
return agentPath
}
}
return null
}
/**
* Get available agents with metadata
* @returns {Promise<Array>} Array of agent objects
*/
async getAvailableAgents () {
const cacheKey = 'all-agents'
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
const agents = []
const yaml = require('js-yaml')
const { extractYamlFromAgent } = require('../../lib/yaml-utils')
// Get agents from bmad-core
const coreAgents = await this.findFiles('agents/*.md', {
cwd: this.getBmadCorePath()
})
for (const agentFile of coreAgents) {
const content = await fs.readFile(
path.join(this.getBmadCorePath(), agentFile),
'utf8'
)
const yamlContent = extractYamlFromAgent(content)
if (yamlContent) {
try {
const metadata = yaml.load(yamlContent)
agents.push({
id: path.basename(agentFile, '.md'),
name: metadata.agent_name || path.basename(agentFile, '.md'),
description: metadata.description || 'No description available',
source: 'core'
})
} catch (e) {
// Skip invalid agents
}
}
}
// Cache for 10 minutes
this._pathCache.set(cacheKey, agents)
setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000)
return agents
}
/**
* Get available expansion packs
* @returns {Promise<Array>} Array of expansion pack objects
*/
async getExpansionPacks () {
const cacheKey = 'expansion-packs'
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
const packs = []
const expansionPacksPath = this.getExpansionPacksPath()
if (await fs.pathExists(expansionPacksPath)) {
const entries = await fs.readdir(expansionPacksPath, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory()) {
const configPath = path.join(expansionPacksPath, entry.name, 'config.yaml')
if (await fs.pathExists(configPath)) {
try {
const yaml = require('js-yaml')
const config = yaml.load(await fs.readFile(configPath, 'utf8'))
packs.push({
id: entry.name,
name: config.name || entry.name,
version: config.version || '1.0.0',
description: config.description || 'No description available',
shortTitle: config['short-title'] || config.description || 'No description available',
author: config.author || 'Unknown',
path: path.join(expansionPacksPath, entry.name)
})
} catch (e) {
// Skip invalid packs
}
}
}
}
}
// Cache for 10 minutes
this._pathCache.set(cacheKey, packs)
setTimeout(() => this._pathCache.delete(cacheKey), 10 * 60 * 1000)
return packs
}
/**
* Get team configuration
* @param {string} teamId - Team identifier
* @returns {Promise<Object|null>} Team configuration or null
*/
async getTeamConfig (teamId) {
const cacheKey = `team:${teamId}`
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
const teamPath = path.join(this.getBmadCorePath(), 'agent-teams', `${teamId}.yaml`)
if (await fs.pathExists(teamPath)) {
try {
const yaml = require('js-yaml')
const content = await fs.readFile(teamPath, 'utf8')
const config = yaml.load(content)
this._pathCache.set(cacheKey, config)
return config
} catch (e) {
return null
}
}
return null
}
/**
* Get resource dependencies for an agent
* @param {string} agentId - Agent identifier
* @returns {Promise<Object>} Dependencies object
*/
async getAgentDependencies (agentId) {
const cacheKey = `deps:${agentId}`
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
const agentPath = await this.getAgentPath(agentId)
if (!agentPath) {
return { all: [], byType: {} }
}
const content = await fs.readFile(agentPath, 'utf8')
const { extractYamlFromAgent } = require('../../lib/yaml-utils')
const yamlContent = extractYamlFromAgent(content)
if (!yamlContent) {
return { all: [], byType: {} }
}
try {
const yaml = require('js-yaml')
const metadata = yaml.load(yamlContent)
const dependencies = metadata.dependencies || {}
// Flatten dependencies
const allDeps = []
const byType = {}
for (const [type, deps] of Object.entries(dependencies)) {
if (Array.isArray(deps)) {
byType[type] = deps
for (const dep of deps) {
allDeps.push(`.bmad-core/${type}/${dep}`)
}
}
}
const result = { all: allDeps, byType }
this._pathCache.set(cacheKey, result)
return result
} catch (e) {
return { all: [], byType: {} }
}
}
/**
* Clear all caches to free memory
*/
clearCache () {
this._pathCache.clear()
this._globCache.clear()
}
/**
* Get IDE configuration
* @param {string} ideId - IDE identifier
* @returns {Promise<Object|null>} IDE configuration or null
*/
async getIdeConfig (ideId) {
const cacheKey = `ide:${ideId}`
if (this._pathCache.has(cacheKey)) {
return this._pathCache.get(cacheKey)
}
const idePath = path.join(this.getBmadCorePath(), 'ide-rules', `${ideId}.yaml`)
if (await fs.pathExists(idePath)) {
try {
const yaml = require('js-yaml')
const content = await fs.readFile(idePath, 'utf8')
const config = yaml.load(content)
this._pathCache.set(cacheKey, config)
return config
} catch (e) {
return null
}
}
return null
}
}
// Singleton instance
const resourceLocator = new ResourceLocator()
module.exports = resourceLocator