fix: improve code in the installer to be more memory efficient
This commit is contained in:
@@ -1,18 +1,11 @@
|
||||
const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const glob = require("glob");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
// Dynamic import for ES module
|
||||
let chalk;
|
||||
|
||||
// Initialize ES modules
|
||||
async function initializeModules() {
|
||||
if (!chalk) {
|
||||
chalk = (await import("chalk")).default;
|
||||
}
|
||||
}
|
||||
const chalk = require("chalk");
|
||||
const { createReadStream, createWriteStream, promises: fsPromises } = require('fs');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const resourceLocator = require('./resource-locator');
|
||||
|
||||
class FileManager {
|
||||
constructor() {
|
||||
@@ -23,10 +16,19 @@ class FileManager {
|
||||
async copyFile(source, destination) {
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(destination));
|
||||
await fs.copy(source, destination);
|
||||
|
||||
// Use streaming for large files (> 10MB)
|
||||
const stats = await fs.stat(source);
|
||||
if (stats.size > 10 * 1024 * 1024) {
|
||||
await pipeline(
|
||||
createReadStream(source),
|
||||
createWriteStream(destination)
|
||||
);
|
||||
} else {
|
||||
await fs.copy(source, destination);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
await initializeModules();
|
||||
console.error(chalk.red(`Failed to copy ${source}:`), error.message);
|
||||
return false;
|
||||
}
|
||||
@@ -35,10 +37,28 @@ class FileManager {
|
||||
async copyDirectory(source, destination) {
|
||||
try {
|
||||
await fs.ensureDir(destination);
|
||||
await fs.copy(source, destination);
|
||||
|
||||
// Use streaming copy for large directories
|
||||
const files = await resourceLocator.findFiles('**/*', {
|
||||
cwd: source,
|
||||
nodir: true
|
||||
});
|
||||
|
||||
// Process files in batches to avoid memory issues
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map(file =>
|
||||
this.copyFile(
|
||||
path.join(source, file),
|
||||
path.join(destination, file)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
await initializeModules();
|
||||
console.error(
|
||||
chalk.red(`Failed to copy directory ${source}:`),
|
||||
error.message
|
||||
@@ -48,7 +68,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
async copyGlobPattern(pattern, sourceDir, destDir, rootValue = null) {
|
||||
const files = glob.sync(pattern, { cwd: sourceDir });
|
||||
const files = await resourceLocator.findFiles(pattern, { cwd: sourceDir });
|
||||
const copied = [];
|
||||
|
||||
for (const file of files) {
|
||||
@@ -75,12 +95,15 @@ class FileManager {
|
||||
|
||||
async calculateFileHash(filePath) {
|
||||
try {
|
||||
const content = await fs.readFile(filePath);
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(content)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
// Use streaming for hash calculation to reduce memory usage
|
||||
const stream = createReadStream(filePath);
|
||||
const hash = crypto.createHash("sha256");
|
||||
|
||||
for await (const chunk of stream) {
|
||||
hash.update(chunk);
|
||||
}
|
||||
|
||||
return hash.digest("hex").slice(0, 16);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -94,7 +117,7 @@ class FileManager {
|
||||
);
|
||||
|
||||
// Read version from core-config.yaml
|
||||
const coreConfigPath = path.join(__dirname, "../../../bmad-core/core-config.yaml");
|
||||
const coreConfigPath = path.join(resourceLocator.getBmadCorePath(), "core-config.yaml");
|
||||
let coreVersion = "unknown";
|
||||
try {
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
|
||||
@@ -304,7 +327,6 @@ class FileManager {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await initializeModules();
|
||||
console.error(chalk.red(`Failed to modify core-config.yaml:`), error.message);
|
||||
return false;
|
||||
}
|
||||
@@ -312,22 +334,35 @@ class FileManager {
|
||||
|
||||
async copyFileWithRootReplacement(source, destination, rootValue) {
|
||||
try {
|
||||
// Read the source file content
|
||||
const fs = require('fs').promises;
|
||||
const content = await fs.readFile(source, 'utf8');
|
||||
// Check file size to determine if we should stream
|
||||
const stats = await fs.stat(source);
|
||||
|
||||
// Replace {root} with the specified root value
|
||||
const updatedContent = content.replace(/\{root\}/g, rootValue);
|
||||
|
||||
// Ensure directory exists
|
||||
await this.ensureDirectory(path.dirname(destination));
|
||||
|
||||
// Write the updated content
|
||||
await fs.writeFile(destination, updatedContent, 'utf8');
|
||||
if (stats.size > 5 * 1024 * 1024) { // 5MB threshold
|
||||
// Use streaming for large files
|
||||
const { Transform } = require('stream');
|
||||
const replaceStream = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
const modified = chunk.toString().replace(/\{root\}/g, rootValue);
|
||||
callback(null, modified);
|
||||
}
|
||||
});
|
||||
|
||||
await this.ensureDirectory(path.dirname(destination));
|
||||
await pipeline(
|
||||
createReadStream(source, { encoding: 'utf8' }),
|
||||
replaceStream,
|
||||
createWriteStream(destination, { encoding: 'utf8' })
|
||||
);
|
||||
} else {
|
||||
// Regular approach for smaller files
|
||||
const content = await fsPromises.readFile(source, 'utf8');
|
||||
const updatedContent = content.replace(/\{root\}/g, rootValue);
|
||||
await this.ensureDirectory(path.dirname(destination));
|
||||
await fsPromises.writeFile(destination, updatedContent, 'utf8');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await initializeModules();
|
||||
console.error(chalk.red(`Failed to copy ${source} with root replacement:`), error.message);
|
||||
return false;
|
||||
}
|
||||
@@ -335,11 +370,10 @@ class FileManager {
|
||||
|
||||
async copyDirectoryWithRootReplacement(source, destination, rootValue, fileExtensions = ['.md', '.yaml', '.yml']) {
|
||||
try {
|
||||
await initializeModules(); // Ensure chalk is initialized
|
||||
await this.ensureDirectory(destination);
|
||||
|
||||
// Get all files in source directory
|
||||
const files = glob.sync('**/*', {
|
||||
const files = await resourceLocator.findFiles('**/*', {
|
||||
cwd: source,
|
||||
nodir: true
|
||||
});
|
||||
@@ -369,7 +403,6 @@ class FileManager {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
await initializeModules();
|
||||
console.error(chalk.red(`Failed to copy directory ${source} with root replacement:`), error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
227
tools/installer/lib/ide-base-setup.js
Normal file
227
tools/installer/lib/ide-base-setup.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Base IDE Setup - Common functionality for all IDE setups
|
||||
* Reduces duplication and provides shared methods
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const yaml = require("js-yaml");
|
||||
const chalk = require("chalk");
|
||||
const fileManager = require("./file-manager");
|
||||
const resourceLocator = require("./resource-locator");
|
||||
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|
||||
|
||||
class BaseIdeSetup {
|
||||
constructor() {
|
||||
this._agentCache = new Map();
|
||||
this._pathCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all agent IDs with caching
|
||||
*/
|
||||
async getAllAgentIds(installDir) {
|
||||
const cacheKey = `all-agents:${installDir}`;
|
||||
if (this._agentCache.has(cacheKey)) {
|
||||
return this._agentCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const allAgents = new Set();
|
||||
|
||||
// Get core agents
|
||||
const coreAgents = await this.getCoreAgentIds(installDir);
|
||||
coreAgents.forEach(id => allAgents.add(id));
|
||||
|
||||
// Get expansion pack agents
|
||||
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
|
||||
for (const pack of expansionPacks) {
|
||||
const packAgents = await this.getExpansionPackAgents(pack.path);
|
||||
packAgents.forEach(id => allAgents.add(id));
|
||||
}
|
||||
|
||||
const result = Array.from(allAgents);
|
||||
this._agentCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get core agent IDs
|
||||
*/
|
||||
async getCoreAgentIds(installDir) {
|
||||
const coreAgents = [];
|
||||
const corePaths = [
|
||||
path.join(installDir, ".bmad-core", "agents"),
|
||||
path.join(installDir, "bmad-core", "agents")
|
||||
];
|
||||
|
||||
for (const agentsDir of corePaths) {
|
||||
if (await fileManager.pathExists(agentsDir)) {
|
||||
const files = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
|
||||
coreAgents.push(...files.map(file => path.basename(file, ".md")));
|
||||
break; // Use first found
|
||||
}
|
||||
}
|
||||
|
||||
return coreAgents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find agent path with caching
|
||||
*/
|
||||
async findAgentPath(agentId, installDir) {
|
||||
const cacheKey = `agent-path:${agentId}:${installDir}`;
|
||||
if (this._pathCache.has(cacheKey)) {
|
||||
return this._pathCache.get(cacheKey);
|
||||
}
|
||||
|
||||
// Use resource locator for efficient path finding
|
||||
let agentPath = await resourceLocator.getAgentPath(agentId);
|
||||
|
||||
if (!agentPath) {
|
||||
// Check installation-specific paths
|
||||
const possiblePaths = [
|
||||
path.join(installDir, ".bmad-core", "agents", `${agentId}.md`),
|
||||
path.join(installDir, "bmad-core", "agents", `${agentId}.md`),
|
||||
path.join(installDir, "common", "agents", `${agentId}.md`)
|
||||
];
|
||||
|
||||
for (const testPath of possiblePaths) {
|
||||
if (await fileManager.pathExists(testPath)) {
|
||||
agentPath = testPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (agentPath) {
|
||||
this._pathCache.set(cacheKey, agentPath);
|
||||
}
|
||||
return agentPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent title from metadata
|
||||
*/
|
||||
async getAgentTitle(agentId, installDir) {
|
||||
const agentPath = await this.findAgentPath(agentId, installDir);
|
||||
if (!agentPath) return agentId;
|
||||
|
||||
try {
|
||||
const content = await fileManager.readFile(agentPath);
|
||||
const yamlContent = extractYamlFromAgent(content);
|
||||
if (yamlContent) {
|
||||
const metadata = yaml.load(yamlContent);
|
||||
return metadata.agent_name || agentId;
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback to agent ID
|
||||
}
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installed expansion packs
|
||||
*/
|
||||
async getInstalledExpansionPacks(installDir) {
|
||||
const cacheKey = `expansion-packs:${installDir}`;
|
||||
if (this._pathCache.has(cacheKey)) {
|
||||
return this._pathCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const expansionPacks = [];
|
||||
|
||||
// Check for dot-prefixed expansion packs
|
||||
const dotExpansions = await resourceLocator.findFiles(".bmad-*", { cwd: installDir });
|
||||
|
||||
for (const dotExpansion of dotExpansions) {
|
||||
if (dotExpansion !== ".bmad-core") {
|
||||
const packPath = path.join(installDir, dotExpansion);
|
||||
const packName = dotExpansion.substring(1); // remove the dot
|
||||
expansionPacks.push({
|
||||
name: packName,
|
||||
path: packPath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check other dot folders that have config.yaml
|
||||
const allDotFolders = await resourceLocator.findFiles(".*", { cwd: installDir });
|
||||
for (const folder of allDotFolders) {
|
||||
if (!folder.startsWith(".bmad-") && folder !== ".bmad-core") {
|
||||
const packPath = path.join(installDir, folder);
|
||||
const configPath = path.join(packPath, "config.yaml");
|
||||
if (await fileManager.pathExists(configPath)) {
|
||||
expansionPacks.push({
|
||||
name: folder.substring(1), // remove the dot
|
||||
path: packPath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._pathCache.set(cacheKey, expansionPacks);
|
||||
return expansionPacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expansion pack agents
|
||||
*/
|
||||
async getExpansionPackAgents(packPath) {
|
||||
const agentsDir = path.join(packPath, "agents");
|
||||
if (!(await fileManager.pathExists(agentsDir))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const agentFiles = await resourceLocator.findFiles("*.md", { cwd: agentsDir });
|
||||
return agentFiles.map(file => path.basename(file, ".md"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent rule content (shared logic)
|
||||
*/
|
||||
async createAgentRuleContent(agentId, agentPath, installDir, format = 'mdc') {
|
||||
const agentContent = await fileManager.readFile(agentPath);
|
||||
const agentTitle = await this.getAgentTitle(agentId, installDir);
|
||||
const yamlContent = extractYamlFromAgent(agentContent);
|
||||
|
||||
let content = "";
|
||||
|
||||
if (format === 'mdc') {
|
||||
// MDC format for Cursor
|
||||
content = "---\n";
|
||||
content += "description: \n";
|
||||
content += "globs: []\n";
|
||||
content += "alwaysApply: false\n";
|
||||
content += "---\n\n";
|
||||
content += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
|
||||
content += `This rule is triggered when the user types \`@${agentId}\` and activates the ${agentTitle} agent persona.\n\n`;
|
||||
content += "## Agent Activation\n\n";
|
||||
content += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
|
||||
content += "```yaml\n";
|
||||
content += yamlContent || agentContent.replace(/^#.*$/m, "").trim();
|
||||
content += "\n```\n\n";
|
||||
content += "## File Reference\n\n";
|
||||
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
||||
content += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`;
|
||||
content += "## Usage\n\n";
|
||||
content += `When the user types \`@${agentId}\`, activate this ${agentTitle} persona and follow all instructions defined in the YAML configuration above.\n`;
|
||||
} else if (format === 'claude') {
|
||||
// Claude Code format
|
||||
content = `# /${agentId} Command\n\n`;
|
||||
content += `When this command is used, adopt the following agent persona:\n\n`;
|
||||
content += agentContent;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
clearCache() {
|
||||
this._agentCache.clear();
|
||||
this._pathCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseIdeSetup;
|
||||
@@ -1,26 +1,17 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const yaml = require("js-yaml");
|
||||
const chalk = require("chalk");
|
||||
const inquirer = require("inquirer");
|
||||
const fileManager = require("./file-manager");
|
||||
const configLoader = require("./config-loader");
|
||||
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|
||||
const BaseIdeSetup = require("./ide-base-setup");
|
||||
const resourceLocator = require("./resource-locator");
|
||||
|
||||
// Dynamic import for ES module
|
||||
let chalk;
|
||||
let inquirer;
|
||||
|
||||
// Initialize ES modules
|
||||
async function initializeModules() {
|
||||
if (!chalk) {
|
||||
chalk = (await import("chalk")).default;
|
||||
}
|
||||
if (!inquirer) {
|
||||
inquirer = (await import("inquirer")).default;
|
||||
}
|
||||
}
|
||||
|
||||
class IdeSetup {
|
||||
class IdeSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super();
|
||||
this.ideAgentConfig = null;
|
||||
}
|
||||
|
||||
@@ -42,7 +33,6 @@ class IdeSetup {
|
||||
}
|
||||
|
||||
async setup(ide, installDir, selectedAgent = null, spinner = null, preConfiguredSettings = null) {
|
||||
await initializeModules();
|
||||
const ideConfig = await configLoader.getIdeConfiguration(ide);
|
||||
|
||||
if (!ideConfig) {
|
||||
@@ -80,53 +70,17 @@ class IdeSetup {
|
||||
await fileManager.ensureDirectory(cursorRulesDir);
|
||||
|
||||
for (const agentId of agents) {
|
||||
// Find the agent file
|
||||
const agentPath = await this.findAgentPath(agentId, installDir);
|
||||
|
||||
if (agentPath) {
|
||||
const agentContent = await fileManager.readFile(agentPath);
|
||||
const mdcContent = await this.createAgentRuleContent(agentId, agentPath, installDir, 'mdc');
|
||||
const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`);
|
||||
|
||||
// Create MDC content with proper format
|
||||
let mdcContent = "---\n";
|
||||
mdcContent += "description: \n";
|
||||
mdcContent += "globs: []\n";
|
||||
mdcContent += "alwaysApply: false\n";
|
||||
mdcContent += "---\n\n";
|
||||
mdcContent += `# ${agentId.toUpperCase()} Agent Rule\n\n`;
|
||||
mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle(
|
||||
agentId,
|
||||
installDir
|
||||
)} agent persona.\n\n`;
|
||||
mdcContent += "## Agent Activation\n\n";
|
||||
mdcContent +=
|
||||
"CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n";
|
||||
mdcContent += "```yaml\n";
|
||||
// Extract just the YAML content from the agent file
|
||||
const yamlContent = extractYamlFromAgent(agentContent);
|
||||
if (yamlContent) {
|
||||
mdcContent += yamlContent;
|
||||
} else {
|
||||
// If no YAML found, include the whole content minus the header
|
||||
mdcContent += agentContent.replace(/^#.*$/m, "").trim();
|
||||
}
|
||||
mdcContent += "\n```\n\n";
|
||||
mdcContent += "## File Reference\n\n";
|
||||
const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/');
|
||||
mdcContent += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`;
|
||||
mdcContent += "## Usage\n\n";
|
||||
mdcContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle(
|
||||
agentId,
|
||||
installDir
|
||||
)} persona and follow all instructions defined in the YAML configuration above.\n`;
|
||||
|
||||
await fileManager.writeFile(mdcPath, mdcContent);
|
||||
console.log(chalk.green(`✓ Created rule: ${agentId}.mdc`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green(`\n✓ Created Cursor rules in ${cursorRulesDir}`));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -827,7 +781,6 @@ class IdeSetup {
|
||||
}
|
||||
|
||||
async setupGeminiCli(installDir) {
|
||||
await initializeModules();
|
||||
const geminiDir = path.join(installDir, ".gemini");
|
||||
const bmadMethodDir = path.join(geminiDir, "bmad-method");
|
||||
await fileManager.ensureDirectory(bmadMethodDir);
|
||||
@@ -928,8 +881,6 @@ class IdeSetup {
|
||||
}
|
||||
|
||||
async setupGitHubCopilot(installDir, selectedAgent, spinner = null, preConfiguredSettings = null) {
|
||||
await initializeModules();
|
||||
|
||||
// Configure VS Code workspace settings first to avoid UI conflicts with loading spinners
|
||||
await this.configureVsCodeSettings(installDir, spinner, preConfiguredSettings);
|
||||
|
||||
@@ -978,7 +929,6 @@ tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems
|
||||
}
|
||||
|
||||
async configureVsCodeSettings(installDir, spinner, preConfiguredSettings = null) {
|
||||
await initializeModules(); // Ensure inquirer is loaded
|
||||
const vscodeDir = path.join(installDir, ".vscode");
|
||||
const settingsPath = path.join(vscodeDir, "settings.json");
|
||||
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
const path = require("node:path");
|
||||
const fs = require("fs-extra");
|
||||
const chalk = require("chalk");
|
||||
const ora = require("ora");
|
||||
const inquirer = require("inquirer");
|
||||
const fileManager = require("./file-manager");
|
||||
const configLoader = require("./config-loader");
|
||||
const ideSetup = require("./ide-setup");
|
||||
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|
||||
|
||||
// Dynamic imports for ES modules
|
||||
let chalk, ora, inquirer;
|
||||
|
||||
// Initialize ES modules
|
||||
async function initializeModules() {
|
||||
if (!chalk) {
|
||||
chalk = (await import("chalk")).default;
|
||||
ora = (await import("ora")).default;
|
||||
inquirer = (await import("inquirer")).default;
|
||||
}
|
||||
}
|
||||
const resourceLocator = require("./resource-locator");
|
||||
|
||||
class Installer {
|
||||
async getCoreVersion() {
|
||||
const yaml = require("js-yaml");
|
||||
const fs = require("fs-extra");
|
||||
const coreConfigPath = path.join(__dirname, "../../../bmad-core/core-config.yaml");
|
||||
const coreConfigPath = path.join(resourceLocator.getBmadCorePath(), "core-config.yaml");
|
||||
try {
|
||||
const coreConfigContent = await fs.readFile(coreConfigPath, "utf8");
|
||||
const coreConfig = yaml.load(coreConfigContent);
|
||||
@@ -32,11 +25,8 @@ class Installer {
|
||||
}
|
||||
|
||||
async install(config) {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
|
||||
const spinner = ora("Analyzing installation directory...").start();
|
||||
|
||||
|
||||
try {
|
||||
// Store the original CWD where npx was executed
|
||||
const originalCwd = process.env.INIT_CWD || process.env.PWD || process.cwd();
|
||||
@@ -59,7 +49,7 @@ class Installer {
|
||||
// Check if directory exists and handle non-existent directories
|
||||
if (!(await fileManager.pathExists(installDir))) {
|
||||
spinner.stop();
|
||||
console.log(chalk.yellow(`\nThe directory ${chalk.bold(installDir)} does not exist.`));
|
||||
console.log(`\nThe directory ${installDir} does not exist.`);
|
||||
|
||||
const { action } = await inquirer.prompt([
|
||||
{
|
||||
@@ -84,7 +74,7 @@ class Installer {
|
||||
]);
|
||||
|
||||
if (action === 'cancel') {
|
||||
console.log(chalk.red('Installation cancelled.'));
|
||||
console.log('Installation cancelled.');
|
||||
process.exit(0);
|
||||
} else if (action === 'change') {
|
||||
const { newDirectory } = await inquirer.prompt([
|
||||
@@ -106,10 +96,10 @@ class Installer {
|
||||
} else if (action === 'create') {
|
||||
try {
|
||||
await fileManager.ensureDirectory(installDir);
|
||||
console.log(chalk.green(`✓ Created directory: ${installDir}`));
|
||||
console.log(`✓ Created directory: ${installDir}`);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to create directory: ${error.message}`));
|
||||
console.error(chalk.yellow('You may need to check permissions or use a different path.'));
|
||||
console.error(`Failed to create directory: ${error.message}`);
|
||||
console.error('You may need to check permissions or use a different path.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -161,14 +151,17 @@ class Installer {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail("Installation failed");
|
||||
// Check if modules were initialized
|
||||
if (spinner) {
|
||||
spinner.fail("Installation failed");
|
||||
} else {
|
||||
console.error("Installation failed:", error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async detectInstallationState(installDir) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
const state = {
|
||||
type: "clean",
|
||||
hasV4Manifest: false,
|
||||
@@ -212,8 +205,7 @@ class Installer {
|
||||
}
|
||||
|
||||
// Check if directory has other files
|
||||
const glob = require("glob");
|
||||
const files = glob.sync("**/*", {
|
||||
const files = await resourceLocator.findFiles("**/*", {
|
||||
cwd: installDir,
|
||||
nodir: true,
|
||||
ignore: ["**/.git/**", "**/node_modules/**"],
|
||||
@@ -233,8 +225,6 @@ class Installer {
|
||||
}
|
||||
|
||||
async performFreshInstall(config, installDir, spinner, options = {}) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
spinner.text = "Installing BMad Method...";
|
||||
|
||||
let files = [];
|
||||
@@ -242,7 +232,7 @@ class Installer {
|
||||
if (config.installType === "full") {
|
||||
// Full installation - copy entire .bmad-core folder as a subdirectory
|
||||
spinner.text = "Copying complete .bmad-core folder...";
|
||||
const sourceDir = configLoader.getBmadCorePath();
|
||||
const sourceDir = resourceLocator.getBmadCorePath();
|
||||
const bmadCoreDestDir = path.join(installDir, ".bmad-core");
|
||||
await fileManager.copyDirectoryWithRootReplacement(sourceDir, bmadCoreDestDir, ".bmad-core");
|
||||
|
||||
@@ -251,14 +241,12 @@ class Installer {
|
||||
await this.copyCommonItems(installDir, ".bmad-core", spinner);
|
||||
|
||||
// Get list of all files for manifest
|
||||
const glob = require("glob");
|
||||
files = glob
|
||||
.sync("**/*", {
|
||||
cwd: bmadCoreDestDir,
|
||||
nodir: true,
|
||||
ignore: ["**/.git/**", "**/node_modules/**"],
|
||||
})
|
||||
.map((file) => path.join(".bmad-core", file));
|
||||
const foundFiles = await resourceLocator.findFiles("**/*", {
|
||||
cwd: bmadCoreDestDir,
|
||||
nodir: true,
|
||||
ignore: ["**/.git/**", "**/node_modules/**"],
|
||||
});
|
||||
files = foundFiles.map((file) => path.join(".bmad-core", file));
|
||||
} else if (config.installType === "single-agent") {
|
||||
// Single agent installation
|
||||
spinner.text = `Installing ${config.agent} agent...`;
|
||||
@@ -275,10 +263,10 @@ class Installer {
|
||||
files.push(`.bmad-core/agents/${config.agent}.md`);
|
||||
|
||||
// Copy dependencies
|
||||
const dependencies = await configLoader.getAgentDependencies(
|
||||
const { all: dependencies } = await resourceLocator.getAgentDependencies(
|
||||
config.agent
|
||||
);
|
||||
const sourceBase = configLoader.getBmadCorePath();
|
||||
const sourceBase = resourceLocator.getBmadCorePath();
|
||||
|
||||
for (const dep of dependencies) {
|
||||
spinner.text = `Copying dependency: ${dep}`;
|
||||
@@ -328,7 +316,7 @@ class Installer {
|
||||
|
||||
// Get team dependencies
|
||||
const teamDependencies = await configLoader.getTeamDependencies(config.team);
|
||||
const sourceBase = configLoader.getBmadCorePath();
|
||||
const sourceBase = resourceLocator.getBmadCorePath();
|
||||
|
||||
// Install all team dependencies
|
||||
for (const dep of teamDependencies) {
|
||||
@@ -415,8 +403,6 @@ class Installer {
|
||||
}
|
||||
|
||||
async handleExistingV4Installation(config, installDir, state, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
spinner.stop();
|
||||
|
||||
const currentVersion = state.manifest.version;
|
||||
@@ -443,7 +429,7 @@ class Installer {
|
||||
const hasIntegrityIssues = hasMissingFiles || hasModifiedFiles;
|
||||
|
||||
if (hasIntegrityIssues) {
|
||||
console.log(chalk.red("\n⚠️ Installation issues detected:"));
|
||||
console.log(chalk.red("\n⚠️ Installation issues detected:"));
|
||||
if (hasMissingFiles) {
|
||||
console.log(chalk.red(` Missing files: ${integrity.missing.length}`));
|
||||
if (integrity.missing.length <= 5) {
|
||||
@@ -473,7 +459,7 @@ class Installer {
|
||||
let choices = [];
|
||||
|
||||
if (versionCompare < 0) {
|
||||
console.log(chalk.cyan("\n⬆️ Upgrade available for BMad core"));
|
||||
console.log(chalk.cyan("\n⬆️ Upgrade available for BMad core"));
|
||||
choices.push({ name: `Upgrade BMad core (v${currentVersion} → v${newVersion})`, value: "upgrade" });
|
||||
} else if (versionCompare === 0) {
|
||||
if (hasIntegrityIssues) {
|
||||
@@ -483,10 +469,10 @@ class Installer {
|
||||
value: "repair"
|
||||
});
|
||||
}
|
||||
console.log(chalk.yellow("\n⚠️ Same version already installed"));
|
||||
console.log(chalk.yellow("\n⚠️ Same version already installed"));
|
||||
choices.push({ name: `Force reinstall BMad core (v${currentVersion} - reinstall)`, value: "reinstall" });
|
||||
} else {
|
||||
console.log(chalk.yellow("\n⬇️ Installed version is newer than available"));
|
||||
console.log(chalk.yellow("\n⬇️ Installed version is newer than available"));
|
||||
choices.push({ name: `Downgrade BMad core (v${currentVersion} → v${newVersion})`, value: "reinstall" });
|
||||
}
|
||||
|
||||
@@ -515,7 +501,7 @@ class Installer {
|
||||
return await this.performReinstall(config, installDir, spinner);
|
||||
case "expansions":
|
||||
// Ask which expansion packs to install
|
||||
const availableExpansionPacks = await this.getAvailableExpansionPacks();
|
||||
const availableExpansionPacks = await resourceLocator.getExpansionPacks();
|
||||
|
||||
if (availableExpansionPacks.length === 0) {
|
||||
console.log(chalk.yellow("No expansion packs available."));
|
||||
@@ -528,7 +514,7 @@ class Installer {
|
||||
name: 'selectedPacks',
|
||||
message: 'Select expansion packs to install/update:',
|
||||
choices: availableExpansionPacks.map(pack => ({
|
||||
name: `${pack.name} v${pack.version} - ${pack.description}`,
|
||||
name: `${pack.name} (v${pack.version}) .${pack.id}`,
|
||||
value: pack.id,
|
||||
checked: state.expansionPacks[pack.id] !== undefined
|
||||
}))
|
||||
@@ -557,8 +543,6 @@ class Installer {
|
||||
}
|
||||
|
||||
async handleV3Installation(config, installDir, state, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
spinner.stop();
|
||||
|
||||
console.log(
|
||||
@@ -598,8 +582,6 @@ class Installer {
|
||||
}
|
||||
|
||||
async handleUnknownInstallation(config, installDir, state, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
spinner.stop();
|
||||
|
||||
console.log(chalk.yellow("\n⚠️ Directory contains existing files"));
|
||||
@@ -740,7 +722,7 @@ class Installer {
|
||||
|
||||
// Restore missing and modified files
|
||||
spinner.text = "Restoring files...";
|
||||
const sourceBase = configLoader.getBmadCorePath();
|
||||
const sourceBase = resourceLocator.getBmadCorePath();
|
||||
const filesToRestore = [...integrity.missing, ...integrity.modified];
|
||||
|
||||
for (const file of filesToRestore) {
|
||||
@@ -915,8 +897,6 @@ class Installer {
|
||||
|
||||
// Legacy method for backward compatibility
|
||||
async update() {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
console.log(chalk.yellow('The "update" command is deprecated.'));
|
||||
console.log(
|
||||
'Please use "install" instead - it will detect and offer to update existing installations.'
|
||||
@@ -935,9 +915,7 @@ class Installer {
|
||||
}
|
||||
|
||||
async listAgents() {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
const agents = await configLoader.getAvailableAgents();
|
||||
const agents = await resourceLocator.getAvailableAgents();
|
||||
|
||||
console.log(chalk.bold("\nAvailable BMad Agents:\n"));
|
||||
|
||||
@@ -951,9 +929,7 @@ class Installer {
|
||||
}
|
||||
|
||||
async listExpansionPacks() {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
const expansionPacks = await this.getAvailableExpansionPacks();
|
||||
const expansionPacks = await resourceLocator.getExpansionPacks();
|
||||
|
||||
console.log(chalk.bold("\nAvailable BMad Expansion Packs:\n"));
|
||||
|
||||
@@ -978,8 +954,6 @@ class Installer {
|
||||
}
|
||||
|
||||
async showStatus() {
|
||||
// Initialize ES modules
|
||||
await initializeModules();
|
||||
const installDir = await this.findInstallation();
|
||||
|
||||
if (!installDir) {
|
||||
@@ -1029,11 +1003,11 @@ class Installer {
|
||||
}
|
||||
|
||||
async getAvailableAgents() {
|
||||
return configLoader.getAvailableAgents();
|
||||
return resourceLocator.getAvailableAgents();
|
||||
}
|
||||
|
||||
async getAvailableExpansionPacks() {
|
||||
return configLoader.getAvailableExpansionPacks();
|
||||
return resourceLocator.getExpansionPacks();
|
||||
}
|
||||
|
||||
async getAvailableTeams() {
|
||||
@@ -1046,13 +1020,12 @@ class Installer {
|
||||
}
|
||||
|
||||
const installedFiles = [];
|
||||
const glob = require('glob');
|
||||
|
||||
for (const packId of selectedPacks) {
|
||||
spinner.text = `Installing expansion pack: ${packId}...`;
|
||||
|
||||
try {
|
||||
const expansionPacks = await this.getAvailableExpansionPacks();
|
||||
const expansionPacks = await resourceLocator.getExpansionPacks();
|
||||
const pack = expansionPacks.find(p => p.id === packId);
|
||||
|
||||
if (!pack) {
|
||||
@@ -1112,7 +1085,7 @@ class Installer {
|
||||
spinner.start();
|
||||
continue;
|
||||
} else if (action === 'cancel') {
|
||||
console.log(chalk.red('Installation cancelled.'));
|
||||
console.log('Installation cancelled.');
|
||||
process.exit(0);
|
||||
} else if (action === 'repair') {
|
||||
// Repair the expansion pack
|
||||
@@ -1151,7 +1124,7 @@ class Installer {
|
||||
spinner.start();
|
||||
continue;
|
||||
} else if (action === 'cancel') {
|
||||
console.log(chalk.red('Installation cancelled.'));
|
||||
console.log('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1134,7 @@ class Installer {
|
||||
await fileManager.removeDirectory(expansionDotFolder);
|
||||
}
|
||||
|
||||
const expansionPackDir = pack.packPath;
|
||||
const expansionPackDir = pack.path;
|
||||
|
||||
// Ensure dedicated dot folder exists for this expansion pack
|
||||
expansionDotFolder = path.join(installDir, `.${packId}`);
|
||||
@@ -1187,7 +1160,7 @@ class Installer {
|
||||
// Check if folder exists in expansion pack
|
||||
if (await fileManager.pathExists(sourceFolder)) {
|
||||
// Get all files in this folder
|
||||
const files = glob.sync('**/*', {
|
||||
const files = await resourceLocator.findFiles('**/*', {
|
||||
cwd: sourceFolder,
|
||||
nodir: true
|
||||
});
|
||||
@@ -1236,7 +1209,7 @@ class Installer {
|
||||
await this.copyCommonItems(installDir, `.${packId}`, spinner);
|
||||
|
||||
// Check and resolve core dependencies
|
||||
await this.resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner);
|
||||
await this.resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, pack, spinner);
|
||||
|
||||
// Check and resolve core agents referenced by teams
|
||||
await this.resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner);
|
||||
@@ -1252,30 +1225,30 @@ class Installer {
|
||||
};
|
||||
|
||||
// Get all files installed in this expansion pack
|
||||
const expansionPackFiles = glob.sync('**/*', {
|
||||
const foundFiles = await resourceLocator.findFiles('**/*', {
|
||||
cwd: expansionDotFolder,
|
||||
nodir: true
|
||||
}).map(f => path.join(`.${packId}`, f));
|
||||
});
|
||||
const expansionPackFiles = foundFiles.map(f => path.join(`.${packId}`, f));
|
||||
|
||||
await fileManager.createExpansionPackManifest(installDir, packId, expansionConfig, expansionPackFiles);
|
||||
|
||||
console.log(chalk.green(`✓ Installed expansion pack: ${pack.name} to ${`.${packId}`}`));
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to install expansion pack ${packId}: ${error.message}`));
|
||||
console.error(chalk.red(`Stack trace: ${error.stack}`));
|
||||
console.error(`Failed to install expansion pack ${packId}: ${error.message}`);
|
||||
console.error(`Stack trace: ${error.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
return installedFiles;
|
||||
}
|
||||
|
||||
async resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, spinner) {
|
||||
const glob = require('glob');
|
||||
async resolveExpansionPackCoreDependencies(installDir, expansionDotFolder, packId, pack, spinner) {
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Find all agent files in the expansion pack
|
||||
const agentFiles = glob.sync('agents/*.md', {
|
||||
const agentFiles = await resourceLocator.findFiles('agents/*.md', {
|
||||
cwd: expansionDotFolder
|
||||
});
|
||||
|
||||
@@ -1295,48 +1268,59 @@ class Installer {
|
||||
const deps = dependencies[depType] || [];
|
||||
|
||||
for (const dep of deps) {
|
||||
const depFileName = dep.endsWith('.md') ? dep : `${dep}.md`;
|
||||
const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep :
|
||||
(depType === 'templates' ? `${dep}.yaml` : `${dep}.md`);
|
||||
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
|
||||
// Check if dependency exists in expansion pack
|
||||
// Check if dependency exists in expansion pack dot folder
|
||||
if (!(await fileManager.pathExists(expansionDepPath))) {
|
||||
// Try to find it in core
|
||||
const coreDepPath = path.join(configLoader.getBmadCorePath(), depType, depFileName);
|
||||
// Try to find it in expansion pack source
|
||||
const sourceDepPath = path.join(pack.path, depType, depFileName);
|
||||
|
||||
if (await fileManager.pathExists(coreDepPath)) {
|
||||
spinner.text = `Copying core dependency ${dep} for ${packId}...`;
|
||||
|
||||
// Copy from core to expansion pack dot folder with {root} replacement
|
||||
if (await fileManager.pathExists(sourceDepPath)) {
|
||||
// Copy from expansion pack source
|
||||
spinner.text = `Copying ${packId} dependency ${dep}...`;
|
||||
const destPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
await fileManager.copyFileWithRootReplacement(coreDepPath, destPath, `.${packId}`);
|
||||
|
||||
console.log(chalk.dim(` Added core dependency: ${depType}/${depFileName}`));
|
||||
await fileManager.copyFileWithRootReplacement(sourceDepPath, destPath, `.${packId}`);
|
||||
console.log(chalk.dim(` Added ${packId} dependency: ${depType}/${depFileName}`));
|
||||
} else {
|
||||
console.warn(chalk.yellow(` Warning: Dependency ${depType}/${dep} not found in core or expansion pack`));
|
||||
// Try to find it in core
|
||||
const coreDepPath = path.join(resourceLocator.getBmadCorePath(), depType, depFileName);
|
||||
|
||||
if (await fileManager.pathExists(coreDepPath)) {
|
||||
spinner.text = `Copying core dependency ${dep} for ${packId}...`;
|
||||
|
||||
// Copy from core to expansion pack dot folder with {root} replacement
|
||||
const destPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
await fileManager.copyFileWithRootReplacement(coreDepPath, destPath, `.${packId}`);
|
||||
|
||||
console.log(chalk.dim(` Added core dependency: ${depType}/${depFileName}`));
|
||||
} else {
|
||||
console.warn(chalk.yellow(` Warning: Dependency ${depType}/${dep} not found in core or expansion pack`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` Warning: Could not parse agent dependencies: ${error.message}`));
|
||||
console.warn(` Warning: Could not parse agent dependencies: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async resolveExpansionPackCoreAgents(installDir, expansionDotFolder, packId, spinner) {
|
||||
const glob = require('glob');
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Find all team files in the expansion pack
|
||||
const teamFiles = glob.sync('agent-teams/*.yaml', {
|
||||
const teamFiles = await resourceLocator.findFiles('agent-teams/*.yaml', {
|
||||
cwd: expansionDotFolder
|
||||
});
|
||||
|
||||
// Also get existing agents in the expansion pack
|
||||
const existingAgents = new Set();
|
||||
const agentFiles = glob.sync('agents/*.md', {
|
||||
const agentFiles = await resourceLocator.findFiles('agents/*.md', {
|
||||
cwd: expansionDotFolder
|
||||
});
|
||||
for (const agentFile of agentFiles) {
|
||||
@@ -1362,7 +1346,7 @@ class Installer {
|
||||
for (const agentId of agents) {
|
||||
if (!existingAgents.has(agentId)) {
|
||||
// Agent not in expansion pack, try to get from core
|
||||
const coreAgentPath = path.join(configLoader.getBmadCorePath(), 'agents', `${agentId}.md`);
|
||||
const coreAgentPath = path.join(resourceLocator.getBmadCorePath(), 'agents', `${agentId}.md`);
|
||||
|
||||
if (await fileManager.pathExists(coreAgentPath)) {
|
||||
spinner.text = `Copying core agent ${agentId} for ${packId}...`;
|
||||
@@ -1389,13 +1373,14 @@ class Installer {
|
||||
const deps = dependencies[depType] || [];
|
||||
|
||||
for (const dep of deps) {
|
||||
const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep : `${dep}.md`;
|
||||
const depFileName = dep.endsWith('.md') || dep.endsWith('.yaml') ? dep :
|
||||
(depType === 'templates' ? `${dep}.yaml` : `${dep}.md`);
|
||||
const expansionDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
|
||||
// Check if dependency exists in expansion pack
|
||||
if (!(await fileManager.pathExists(expansionDepPath))) {
|
||||
// Try to find it in core
|
||||
const coreDepPath = path.join(configLoader.getBmadCorePath(), depType, depFileName);
|
||||
const coreDepPath = path.join(resourceLocator.getBmadCorePath(), depType, depFileName);
|
||||
|
||||
if (await fileManager.pathExists(coreDepPath)) {
|
||||
const destDepPath = path.join(expansionDotFolder, depType, depFileName);
|
||||
@@ -1415,7 +1400,7 @@ class Installer {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` Warning: Could not parse agent ${agentId} dependencies: ${error.message}`));
|
||||
console.warn(` Warning: Could not parse agent ${agentId} dependencies: ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1424,7 +1409,7 @@ class Installer {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(` Warning: Could not parse team file ${teamFile}: ${error.message}`));
|
||||
console.warn(` Warning: Could not parse team file ${teamFile}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1456,15 +1441,13 @@ class Installer {
|
||||
}
|
||||
|
||||
async installWebBundles(webBundlesDirectory, config, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
|
||||
try {
|
||||
// Find the dist directory in the BMad installation
|
||||
const distDir = configLoader.getDistPath();
|
||||
|
||||
if (!(await fileManager.pathExists(distDir))) {
|
||||
console.warn(chalk.yellow('Web bundles not found. Run "npm run build" to generate them.'));
|
||||
console.warn('Web bundles not found. Run "npm run build" to generate them.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1522,15 +1505,12 @@ class Installer {
|
||||
console.log(chalk.green(`✓ Installed ${copiedCount} selected web bundles to: ${webBundlesDirectory}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Failed to install web bundles: ${error.message}`));
|
||||
console.error(`Failed to install web bundles: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async copyCommonItems(installDir, targetSubdir, spinner) {
|
||||
// Ensure modules are initialized
|
||||
await initializeModules();
|
||||
|
||||
const glob = require('glob');
|
||||
const fs = require('fs').promises;
|
||||
const sourceBase = path.dirname(path.dirname(path.dirname(path.dirname(__filename)))); // Go up to project root
|
||||
const commonPath = path.join(sourceBase, 'common');
|
||||
@@ -1539,12 +1519,12 @@ class Installer {
|
||||
|
||||
// Check if common/ exists
|
||||
if (!(await fileManager.pathExists(commonPath))) {
|
||||
console.warn(chalk.yellow('Warning: common/ folder not found'));
|
||||
console.warn('Warning: common/ folder not found');
|
||||
return copiedFiles;
|
||||
}
|
||||
|
||||
// Copy all items from common/ to target
|
||||
const commonItems = glob.sync('**/*', {
|
||||
const commonItems = await resourceLocator.findFiles('**/*', {
|
||||
cwd: commonPath,
|
||||
nodir: true
|
||||
});
|
||||
@@ -1641,7 +1621,7 @@ class Installer {
|
||||
if (file.endsWith('install-manifest.yaml')) continue;
|
||||
|
||||
const relativePath = file.replace(`.${packId}/`, '');
|
||||
const sourcePath = path.join(pack.packPath, relativePath);
|
||||
const sourcePath = path.join(pack.path, relativePath);
|
||||
const destPath = path.join(installDir, file);
|
||||
|
||||
// Check if this is a common/ file that needs special processing
|
||||
@@ -1677,8 +1657,8 @@ class Installer {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to repair ${pack.name}`);
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
if (spinner) spinner.fail(`Failed to repair ${pack.name}`);
|
||||
console.error(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1730,7 +1710,7 @@ class Installer {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(chalk.yellow(`Warning: Could not cleanup legacy .yml files: ${error.message}`));
|
||||
console.warn(`Warning: Could not cleanup legacy .yml files: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
224
tools/installer/lib/memory-profiler.js
Normal file
224
tools/installer/lib/memory-profiler.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Memory Profiler - Track memory usage during installation
|
||||
* Helps identify memory leaks and optimize resource usage
|
||||
*/
|
||||
|
||||
const v8 = require('v8');
|
||||
|
||||
class MemoryProfiler {
|
||||
constructor() {
|
||||
this.checkpoints = [];
|
||||
this.startTime = Date.now();
|
||||
this.peakMemory = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory checkpoint
|
||||
* @param {string} label - Label for this checkpoint
|
||||
*/
|
||||
checkpoint(label) {
|
||||
const memUsage = process.memoryUsage();
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
|
||||
const checkpoint = {
|
||||
label,
|
||||
timestamp: Date.now() - this.startTime,
|
||||
memory: {
|
||||
rss: this.formatBytes(memUsage.rss),
|
||||
heapTotal: this.formatBytes(memUsage.heapTotal),
|
||||
heapUsed: this.formatBytes(memUsage.heapUsed),
|
||||
external: this.formatBytes(memUsage.external),
|
||||
arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0)
|
||||
},
|
||||
heap: {
|
||||
totalHeapSize: this.formatBytes(heapStats.total_heap_size),
|
||||
usedHeapSize: this.formatBytes(heapStats.used_heap_size),
|
||||
heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
|
||||
mallocedMemory: this.formatBytes(heapStats.malloced_memory),
|
||||
externalMemory: this.formatBytes(heapStats.external_memory)
|
||||
},
|
||||
raw: {
|
||||
heapUsed: memUsage.heapUsed
|
||||
}
|
||||
};
|
||||
|
||||
// Track peak memory
|
||||
if (memUsage.heapUsed > this.peakMemory) {
|
||||
this.peakMemory = memUsage.heapUsed;
|
||||
}
|
||||
|
||||
this.checkpoints.push(checkpoint);
|
||||
return checkpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force garbage collection (requires --expose-gc flag)
|
||||
*/
|
||||
forceGC() {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory usage summary
|
||||
*/
|
||||
getSummary() {
|
||||
const currentMemory = process.memoryUsage();
|
||||
|
||||
return {
|
||||
currentUsage: {
|
||||
rss: this.formatBytes(currentMemory.rss),
|
||||
heapTotal: this.formatBytes(currentMemory.heapTotal),
|
||||
heapUsed: this.formatBytes(currentMemory.heapUsed)
|
||||
},
|
||||
peakMemory: this.formatBytes(this.peakMemory),
|
||||
totalCheckpoints: this.checkpoints.length,
|
||||
runTime: `${((Date.now() - this.startTime) / 1000).toFixed(2)}s`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed report of memory usage
|
||||
*/
|
||||
getDetailedReport() {
|
||||
const summary = this.getSummary();
|
||||
const memoryGrowth = this.calculateMemoryGrowth();
|
||||
|
||||
return {
|
||||
summary,
|
||||
memoryGrowth,
|
||||
checkpoints: this.checkpoints,
|
||||
recommendations: this.getRecommendations(memoryGrowth)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate memory growth between checkpoints
|
||||
*/
|
||||
calculateMemoryGrowth() {
|
||||
if (this.checkpoints.length < 2) return [];
|
||||
|
||||
const growth = [];
|
||||
for (let i = 1; i < this.checkpoints.length; i++) {
|
||||
const prev = this.checkpoints[i - 1];
|
||||
const curr = this.checkpoints[i];
|
||||
|
||||
const heapDiff = curr.raw.heapUsed - prev.raw.heapUsed;
|
||||
|
||||
growth.push({
|
||||
from: prev.label,
|
||||
to: curr.label,
|
||||
heapGrowth: this.formatBytes(Math.abs(heapDiff)),
|
||||
isIncrease: heapDiff > 0,
|
||||
timeDiff: `${((curr.timestamp - prev.timestamp) / 1000).toFixed(2)}s`
|
||||
});
|
||||
}
|
||||
|
||||
return growth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommendations based on memory usage
|
||||
*/
|
||||
getRecommendations(memoryGrowth) {
|
||||
const recommendations = [];
|
||||
|
||||
// Check for large memory growth
|
||||
const largeGrowths = memoryGrowth.filter(g => {
|
||||
const bytes = this.parseBytes(g.heapGrowth);
|
||||
return bytes > 50 * 1024 * 1024; // 50MB
|
||||
});
|
||||
|
||||
if (largeGrowths.length > 0) {
|
||||
recommendations.push({
|
||||
type: 'warning',
|
||||
message: `Large memory growth detected in ${largeGrowths.length} operations`,
|
||||
details: largeGrowths.map(g => `${g.from} → ${g.to}: ${g.heapGrowth}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Check peak memory
|
||||
if (this.peakMemory > 500 * 1024 * 1024) { // 500MB
|
||||
recommendations.push({
|
||||
type: 'warning',
|
||||
message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`,
|
||||
suggestion: 'Consider processing files in smaller batches'
|
||||
});
|
||||
}
|
||||
|
||||
// Check for potential memory leaks
|
||||
const continuousGrowth = this.checkContinuousGrowth();
|
||||
if (continuousGrowth) {
|
||||
recommendations.push({
|
||||
type: 'error',
|
||||
message: 'Potential memory leak detected',
|
||||
details: 'Memory usage continuously increases without significant decreases'
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for continuous memory growth (potential leak)
|
||||
*/
|
||||
checkContinuousGrowth() {
|
||||
if (this.checkpoints.length < 5) return false;
|
||||
|
||||
let increasingCount = 0;
|
||||
for (let i = 1; i < this.checkpoints.length; i++) {
|
||||
if (this.checkpoints[i].raw.heapUsed > this.checkpoints[i - 1].raw.heapUsed) {
|
||||
increasingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If memory increases in more than 80% of checkpoints, might be a leak
|
||||
return increasingCount / (this.checkpoints.length - 1) > 0.8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable string
|
||||
*/
|
||||
formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse human-readable bytes back to number
|
||||
*/
|
||||
parseBytes(str) {
|
||||
const match = str.match(/^([\d.]+)\s*([KMGT]?B?)$/i);
|
||||
if (!match) return 0;
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
|
||||
const multipliers = {
|
||||
'B': 1,
|
||||
'KB': 1024,
|
||||
'MB': 1024 * 1024,
|
||||
'GB': 1024 * 1024 * 1024
|
||||
};
|
||||
|
||||
return value * (multipliers[unit] || 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear checkpoints to free memory
|
||||
*/
|
||||
clear() {
|
||||
this.checkpoints = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
module.exports = new MemoryProfiler();
|
||||
110
tools/installer/lib/module-manager.js
Normal file
110
tools/installer/lib/module-manager.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Module Manager - Centralized dynamic import management
|
||||
* Handles loading and caching of ES modules to reduce memory overhead
|
||||
*/
|
||||
|
||||
class ModuleManager {
|
||||
constructor() {
|
||||
this._cache = new Map();
|
||||
this._loadingPromises = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all commonly used ES modules at once
|
||||
* @returns {Promise<Object>} Object containing all loaded modules
|
||||
*/
|
||||
async initializeCommonModules() {
|
||||
const modules = await Promise.all([
|
||||
this.getModule('chalk'),
|
||||
this.getModule('ora'),
|
||||
this.getModule('inquirer')
|
||||
]);
|
||||
|
||||
return {
|
||||
chalk: modules[0],
|
||||
ora: modules[1],
|
||||
inquirer: modules[2]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a module by name, with caching
|
||||
* @param {string} moduleName - Name of the module to load
|
||||
* @returns {Promise<any>} The loaded module
|
||||
*/
|
||||
async getModule(moduleName) {
|
||||
// Return from cache if available
|
||||
if (this._cache.has(moduleName)) {
|
||||
return this._cache.get(moduleName);
|
||||
}
|
||||
|
||||
// If already loading, return the existing promise
|
||||
if (this._loadingPromises.has(moduleName)) {
|
||||
return this._loadingPromises.get(moduleName);
|
||||
}
|
||||
|
||||
// Start loading the module
|
||||
const loadPromise = this._loadModule(moduleName);
|
||||
this._loadingPromises.set(moduleName, loadPromise);
|
||||
|
||||
try {
|
||||
const module = await loadPromise;
|
||||
this._cache.set(moduleName, module);
|
||||
this._loadingPromises.delete(moduleName);
|
||||
return module;
|
||||
} catch (error) {
|
||||
this._loadingPromises.delete(moduleName);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to load a specific module
|
||||
* @private
|
||||
*/
|
||||
async _loadModule(moduleName) {
|
||||
switch (moduleName) {
|
||||
case 'chalk':
|
||||
return (await import('chalk')).default;
|
||||
case 'ora':
|
||||
return (await import('ora')).default;
|
||||
case 'inquirer':
|
||||
return (await import('inquirer')).default;
|
||||
case 'glob':
|
||||
return (await import('glob')).glob;
|
||||
case 'globSync':
|
||||
return (await import('glob')).globSync;
|
||||
default:
|
||||
throw new Error(`Unknown module: ${moduleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the module cache to free memory
|
||||
*/
|
||||
clearCache() {
|
||||
this._cache.clear();
|
||||
this._loadingPromises.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple modules at once
|
||||
* @param {string[]} moduleNames - Array of module names
|
||||
* @returns {Promise<Object>} Object with module names as keys
|
||||
*/
|
||||
async getModules(moduleNames) {
|
||||
const modules = await Promise.all(
|
||||
moduleNames.map(name => this.getModule(name))
|
||||
);
|
||||
|
||||
return moduleNames.reduce((acc, name, index) => {
|
||||
acc[name] = modules[index];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const moduleManager = new ModuleManager();
|
||||
|
||||
module.exports = moduleManager;
|
||||
310
tools/installer/lib/resource-locator.js
Normal file
310
tools/installer/lib/resource-locator.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user