chore(installer): remove dead code across installer modules

Delete 3 entirely dead files (agent-command-generator, bmad-artifacts,
module-injections) and remove ~50 unused exports from manifest.js,
cli-utils.js, prompts.js, path-utils.js, official-modules.js,
external-manager.js, custom-module-manager.js, and registry-client.js.
Removes corresponding dead tests.
This commit is contained in:
Alex Verkhovsky 2026-04-10 14:41:01 -07:00
parent eabcd03f65
commit 67a3c4c6f7
10 changed files with 0 additions and 1281 deletions

View File

@ -1728,36 +1728,6 @@ async function runTests() {
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 33: Community & Custom Module Managers${colors.reset}\n`);
// --- CustomModuleManager.validateGitHubUrl ---
{
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
const mgr = new CustomModuleManager();
const https1 = mgr.validateGitHubUrl('https://github.com/owner/repo');
assert(https1.isValid === true, 'validateGitHubUrl accepts HTTPS URL');
assert(https1.owner === 'owner' && https1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from HTTPS');
const https2 = mgr.validateGitHubUrl('https://github.com/owner/repo.git');
assert(https2.isValid === true, 'validateGitHubUrl accepts HTTPS URL with .git');
assert(https2.repo === 'repo', 'validateGitHubUrl strips .git suffix');
const ssh1 = mgr.validateGitHubUrl('git@github.com:owner/repo.git');
assert(ssh1.isValid === true, 'validateGitHubUrl accepts SSH URL');
assert(ssh1.owner === 'owner' && ssh1.repo === 'repo', 'validateGitHubUrl extracts owner/repo from SSH');
const bad1 = mgr.validateGitHubUrl('https://gitlab.com/owner/repo');
assert(bad1.isValid === false, 'validateGitHubUrl rejects non-GitHub URL');
const bad2 = mgr.validateGitHubUrl('');
assert(bad2.isValid === false, 'validateGitHubUrl rejects empty string');
const bad3 = mgr.validateGitHubUrl(null);
assert(bad3.isValid === false, 'validateGitHubUrl rejects null');
const bad4 = mgr.validateGitHubUrl('https://github.com/owner');
assert(bad4.isValid === false, 'validateGitHubUrl rejects URL without repo');
}
// --- CustomModuleManager._normalizeCustomModule --- // --- CustomModuleManager._normalizeCustomModule ---
{ {
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager'); const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
@ -1954,25 +1924,6 @@ async function runTests() {
assert(notFound === null, 'getModuleByCode returns null for unknown code'); assert(notFound === null, 'getModuleByCode returns null for unknown code');
} }
// --- CustomModuleManager URL edge cases ---
{
const { CustomModuleManager } = require('../tools/installer/modules/custom-module-manager');
const mgr = new CustomModuleManager();
// HTTP (not HTTPS) should work
const http = mgr.validateGitHubUrl('http://github.com/owner/repo');
assert(http.isValid === true, 'validateGitHubUrl accepts HTTP URL');
// Trailing slash should be rejected (strict matching)
const trailing = mgr.validateGitHubUrl('https://github.com/owner/repo/');
assert(trailing.isValid === false, 'validateGitHubUrl rejects trailing slash');
// SSH without .git should work
const sshNoDotGit = mgr.validateGitHubUrl('git@github.com:owner/repo');
assert(sshNoDotGit.isValid === true, 'validateGitHubUrl accepts SSH without .git');
assert(sshNoDotGit.repo === 'repo', 'validateGitHubUrl extracts repo from SSH without .git');
}
console.log(''); console.log('');
// ============================================================ // ============================================================

View File

@ -1,20 +1,6 @@
const path = require('node:path');
const os = require('node:os');
const prompts = require('./prompts'); const prompts = require('./prompts');
const CLIUtils = { const CLIUtils = {
/**
* Get version from package.json
*/
getVersion() {
try {
const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
return packageJson.version || 'Unknown';
} catch {
return 'Unknown';
}
},
/** /**
* Display BMAD logo and version using @clack intro + box * Display BMAD logo and version using @clack intro + box
*/ */
@ -52,37 +38,6 @@ const CLIUtils = {
}); });
}, },
/**
* Display section header
* @param {string} title - Section title
* @param {string} subtitle - Optional subtitle
*/
async displaySection(title, subtitle = null) {
await prompts.note(subtitle || '', title);
},
/**
* Display info box
* @param {string|Array} content - Content to display
* @param {Object} options - Box options
*/
async displayBox(content, options = {}) {
let text = content;
if (Array.isArray(content)) {
text = content.join('\n\n');
}
const color = await prompts.getColor();
const borderColor = options.borderColor || 'cyan';
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
const formatBorder = colorMap[borderColor] || color.cyan;
await prompts.box(text, options.title, {
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
formatBorder,
});
},
/** /**
* Display module configuration header * Display module configuration header
* @param {string} moduleName - Module name (fallback if no custom header) * @param {string} moduleName - Module name (fallback if no custom header)
@ -93,98 +48,6 @@ const CLIUtils = {
const title = header || `Configuring ${moduleName.toUpperCase()} Module`; const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
await prompts.note(subheader || '', title); await prompts.note(subheader || '', title);
}, },
/**
* Display module with no custom configuration
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
await prompts.note(subheader || '', title);
},
/**
* Display step indicator
* @param {number} current - Current step
* @param {number} total - Total steps
* @param {string} description - Step description
*/
async displayStep(current, total, description) {
const progress = `[${current}/${total}]`;
await prompts.log.step(`${progress} ${description}`);
},
/**
* Display completion message
* @param {string} message - Completion message
*/
async displayComplete(message) {
const color = await prompts.getColor();
await prompts.box(`\u2728 ${message}`, 'Complete', {
rounded: true,
formatBorder: color.green,
});
},
/**
* Display error message
* @param {string} message - Error message
*/
async displayError(message) {
const color = await prompts.getColor();
await prompts.box(`\u2717 ${message}`, 'Error', {
rounded: true,
formatBorder: color.red,
});
},
/**
* Format list for display
* @param {Array} items - Items to display
* @param {string} prefix - Item prefix
*/
formatList(items, prefix = '\u2022') {
return items.map((item) => ` ${prefix} ${item}`).join('\n');
},
/**
* Clear previous lines
* @param {number} lines - Number of lines to clear
*/
clearLines(lines) {
for (let i = 0; i < lines; i++) {
process.stdout.moveCursor(0, -1);
process.stdout.clearLine(1);
}
},
/**
* Display module completion message
* @param {string} moduleName - Name of the completed module
* @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
*/
displayModuleComplete(moduleName, clearScreen = false) {
// No longer clear screen or show boxes - just a simple completion message
// This is deprecated but kept for backwards compatibility
},
/**
* Expand path with ~ expansion
* @param {string} inputPath - Path to expand
* @returns {string} Expanded path
*/
expandPath(inputPath) {
if (!inputPath) return inputPath;
// Expand ~ to home directory
if (inputPath.startsWith('~')) {
return path.join(os.homedir(), inputPath.slice(1));
}
return inputPath;
},
}; };
module.exports = { CLIUtils }; module.exports = { CLIUtils };

View File

@ -107,117 +107,6 @@ class Manifest {
return null; return null;
} }
/**
* Update existing manifest
* @param {string} bmadDir - Path to bmad directory
* @param {Object} updates - Fields to update
* @param {Array} installedFiles - Updated list of installed files
*/
async update(bmadDir, updates, installedFiles = null) {
const yaml = require('yaml');
const manifest = (await this._readRaw(bmadDir)) || {
installation: {},
modules: [],
ides: [],
};
// Handle module updates
if (updates.modules) {
// If modules is being updated, we need to preserve detailed module info
const existingDetailed = manifest.modules || [];
const incomingNames = updates.modules;
// Build updated modules array
const updatedModules = [];
for (const name of incomingNames) {
const existing = existingDetailed.find((m) => m.name === name);
if (existing) {
// Preserve existing details, update lastUpdated if this module is being updated
updatedModules.push({
...existing,
lastUpdated: new Date().toISOString(),
});
} else {
// New module - add with minimal details
updatedModules.push({
name,
version: null,
installDate: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
source: 'unknown',
});
}
}
manifest.modules = updatedModules;
}
// Merge other updates
if (updates.version) {
manifest.installation.version = updates.version;
}
if (updates.installDate) {
manifest.installation.installDate = updates.installDate;
}
manifest.installation.lastUpdated = new Date().toISOString();
if (updates.ides) {
manifest.ides = updates.ides;
}
// Handle per-module version updates
if (updates.moduleVersions) {
for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
if (moduleIndex !== -1) {
manifest.modules[moduleIndex] = {
...manifest.modules[moduleIndex],
...versionInfo,
lastUpdated: new Date().toISOString(),
};
}
}
}
// Handle adding a new module with version info
if (updates.addModule) {
const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
const existing = manifest.modules.find((m) => m.name === name);
if (!existing) {
const entry = {
name,
version: version || null,
installDate: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
source: source || 'external',
npmPackage: npmPackage || null,
repoUrl: repoUrl || null,
};
if (localPath) entry.localPath = localPath;
manifest.modules.push(entry);
}
}
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
await fs.ensureDir(path.dirname(manifestPath));
// Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifest);
const yamlContent = yaml.stringify(cleanManifestData, {
indent: 2,
lineWidth: 0,
sortKeys: false,
});
// Ensure POSIX-compliant final newline
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
await fs.writeFile(manifestPath, content, 'utf8');
// Return the flattened format for compatibility
return this._flattenManifest(manifest);
}
/** /**
* Read raw manifest data without flattening * Read raw manifest data without flattening
* @param {string} bmadDir - Path to bmad directory * @param {string} bmadDir - Path to bmad directory
@ -310,62 +199,6 @@ class Manifest {
await this._writeRaw(bmadDir, manifest); await this._writeRaw(bmadDir, manifest);
} }
/**
* Remove a module from the manifest
* @param {string} bmadDir - Path to bmad directory
* @param {string} moduleName - Module name to remove
*/
async removeModule(bmadDir, moduleName) {
const manifest = await this._readRaw(bmadDir);
if (!manifest || !manifest.modules) {
return;
}
const index = manifest.modules.findIndex((m) => m.name === moduleName);
if (index !== -1) {
manifest.modules.splice(index, 1);
await this._writeRaw(bmadDir, manifest);
}
}
/**
* Update a single module's version info
* @param {string} bmadDir - Path to bmad directory
* @param {string} moduleName - Module name
* @param {Object} versionInfo - Version info to update
*/
async updateModuleVersion(bmadDir, moduleName, versionInfo) {
const manifest = await this._readRaw(bmadDir);
if (!manifest || !manifest.modules) {
return;
}
const index = manifest.modules.findIndex((m) => m.name === moduleName);
if (index !== -1) {
manifest.modules[index] = {
...manifest.modules[index],
...versionInfo,
lastUpdated: new Date().toISOString(),
};
await this._writeRaw(bmadDir, manifest);
}
}
/**
* Get version info for a specific module
* @param {string} bmadDir - Path to bmad directory
* @param {string} moduleName - Module name
* @returns {Object|null} Module version info or null
*/
async getModuleVersion(bmadDir, moduleName) {
const manifest = await this._readRaw(bmadDir);
if (!manifest || !manifest.modules) {
return null;
}
return manifest.modules.find((m) => m.name === moduleName) || null;
}
/** /**
* Get all modules with their version info * Get all modules with their version info
* @param {string} bmadDir - Path to bmad directory * @param {string} bmadDir - Path to bmad directory
@ -403,27 +236,6 @@ class Manifest {
await fs.writeFile(manifestPath, content, 'utf8'); await fs.writeFile(manifestPath, content, 'utf8');
} }
/**
* Add an IDE configuration to the manifest
* @param {string} bmadDir - Path to bmad directory
* @param {string} ideName - IDE name to add
*/
async addIde(bmadDir, ideName) {
const manifest = await this.read(bmadDir);
if (!manifest) {
throw new Error('No manifest found');
}
if (!manifest.ides) {
manifest.ides = [];
}
if (!manifest.ides.includes(ideName)) {
manifest.ides.push(ideName);
await this.update(bmadDir, { ides: manifest.ides });
}
}
/** /**
* Calculate SHA256 hash of a file * Calculate SHA256 hash of a file
* @param {string} filePath - Path to file * @param {string} filePath - Path to file
@ -438,354 +250,6 @@ class Manifest {
} }
} }
/**
* Parse installed files to extract metadata
* @param {Array} installedFiles - List of installed file paths
* @param {string} bmadDir - Path to bmad directory for relative paths
* @returns {Array} Array of file metadata objects
*/
async parseInstalledFiles(installedFiles, bmadDir) {
const fileMetadata = [];
for (const filePath of installedFiles) {
const fileExt = path.extname(filePath).toLowerCase();
// Make path relative to parent of bmad directory, starting with 'bmad/'
const relativePath = 'bmad' + filePath.replace(bmadDir, '').replaceAll('\\', '/');
// Calculate file hash
const hash = await this.calculateFileHash(filePath);
// Handle markdown files - extract XML metadata if present
if (fileExt === '.md') {
try {
if (await fs.pathExists(filePath)) {
const content = await fs.readFile(filePath, 'utf8');
const metadata = this.extractXmlNodeAttributes(content, filePath, relativePath);
if (metadata) {
// Has XML metadata
metadata.hash = hash;
fileMetadata.push(metadata);
} else {
// No XML metadata - still track the file
fileMetadata.push({
file: relativePath,
type: 'md',
name: path.basename(filePath, fileExt),
title: null,
hash: hash,
});
}
}
} catch (error) {
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
}
}
// Handle other file types (CSV, JSON, YAML, etc.)
else {
fileMetadata.push({
file: relativePath,
type: fileExt.slice(1), // Remove the dot
name: path.basename(filePath, fileExt),
title: null,
hash: hash,
});
}
}
return fileMetadata;
}
/**
* Extract XML node attributes from MD file content
* @param {string} content - File content
* @param {string} filePath - File path for context
* @param {string} relativePath - Relative path starting with 'bmad/'
* @returns {Object|null} Extracted metadata or null
*/
extractXmlNodeAttributes(content, filePath, relativePath) {
// Look for XML blocks in code fences
const xmlBlockMatch = content.match(/```xml\s*([\s\S]*?)```/);
if (!xmlBlockMatch) {
return null;
}
const xmlContent = xmlBlockMatch[1];
// Extract root XML node (agent, task, template, etc.)
const rootNodeMatch = xmlContent.match(/<(\w+)([^>]*)>/);
if (!rootNodeMatch) {
return null;
}
const nodeType = rootNodeMatch[1];
const attributes = rootNodeMatch[2];
// Extract name and title attributes (id not needed since we have path)
const nameMatch = attributes.match(/name="([^"]*)"/);
const titleMatch = attributes.match(/title="([^"]*)"/);
return {
file: relativePath,
type: nodeType,
name: nameMatch ? nameMatch[1] : null,
title: titleMatch ? titleMatch[1] : null,
};
}
/**
* Generate CSV manifest content
* @param {Object} data - Manifest data
* @param {Array} fileMetadata - File metadata array
* @param {Object} moduleConfigs - Module configuration data
* @returns {string} CSV content
*/
generateManifestCsv(data, fileMetadata, moduleConfigs = {}) {
const timestamp = new Date().toISOString();
let csv = [];
// Header section
csv.push(
'# BMAD Manifest',
`# Generated: ${timestamp}`,
'',
'## Installation Info',
'Property,Value',
`Version,${data.version}`,
`InstallDate,${data.installDate || timestamp}`,
`LastUpdated,${data.lastUpdated || timestamp}`,
);
if (data.language) {
csv.push(`Language,${data.language}`);
}
csv.push('');
// Modules section
if (data.modules && data.modules.length > 0) {
csv.push('## Modules', 'Name,Version,ShortTitle');
for (const moduleName of data.modules) {
const config = moduleConfigs[moduleName] || {};
csv.push([moduleName, config.version || '', config['short-title'] || ''].map((v) => this.escapeCsv(v)).join(','));
}
csv.push('');
}
// IDEs section
if (data.ides && data.ides.length > 0) {
csv.push('## IDEs', 'IDE');
for (const ide of data.ides) {
csv.push(this.escapeCsv(ide));
}
csv.push('');
}
// Files section - NO LONGER USED
// Files are now tracked in files-manifest.csv by ManifestGenerator
return csv.join('\n');
}
/**
* Parse CSV manifest content back to object
* @param {string} csvContent - CSV content to parse
* @returns {Object} Parsed manifest data
*/
parseManifestCsv(csvContent) {
const result = {
modules: [],
ides: [],
files: [],
};
const lines = csvContent.split('\n');
let section = '';
for (const line_ of lines) {
const line = line_.trim();
// Skip empty lines and comments
if (!line || line.startsWith('#')) {
// Check for section headers
if (line.startsWith('## ')) {
section = line.slice(3).toLowerCase();
}
continue;
}
// Parse based on current section
switch (section) {
case 'installation info': {
// Skip header row
if (line === 'Property,Value') continue;
const [property, ...valueParts] = line.split(',');
const value = this.unescapeCsv(valueParts.join(','));
switch (property) {
// Path no longer stored in manifest
case 'Version': {
result.version = value;
break;
}
case 'InstallDate': {
result.installDate = value;
break;
}
case 'LastUpdated': {
result.lastUpdated = value;
break;
}
case 'Language': {
result.language = value;
break;
}
}
break;
}
case 'modules': {
// Skip header row
if (line === 'Name,Version,ShortTitle') continue;
const parts = this.parseCsvLine(line);
if (parts[0]) {
result.modules.push(parts[0]);
}
break;
}
case 'ides': {
// Skip header row
if (line === 'IDE') continue;
result.ides.push(this.unescapeCsv(line));
break;
}
case 'files': {
// Skip header rows (support both old and new format)
if (line === 'Type,Path,Name,Title' || line === 'Type,Path,Name,Title,Hash') continue;
const parts = this.parseCsvLine(line);
if (parts.length >= 2) {
result.files.push({
type: parts[0] || '',
file: parts[1] || '',
name: parts[2] || null,
title: parts[3] || null,
hash: parts[4] || null, // Hash column (may not exist in old manifests)
});
}
break;
}
// No default
}
}
return result;
}
/**
* Parse a CSV line handling quotes and commas
* @param {string} line - CSV line to parse
* @returns {Array} Array of values
*/
parseCsvLine(line) {
const result = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
// Escaped quote
current += '"';
i++;
} else {
// Toggle quote state
inQuotes = !inQuotes;
}
} else if (char === ',' && !inQuotes) {
// Field separator
result.push(this.unescapeCsv(current));
current = '';
} else {
current += char;
}
}
// Add the last field
result.push(this.unescapeCsv(current));
return result;
}
/**
* Escape CSV special characters
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
escapeCsv(text) {
if (!text) return '';
const str = String(text);
// If contains comma, newline, or quote, wrap in quotes and escape quotes
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
return '"' + str.replaceAll('"', '""') + '"';
}
return str;
}
/**
* Unescape CSV field
* @param {string} text - Text to unescape
* @returns {string} Unescaped text
*/
unescapeCsv(text) {
if (!text) return '';
// Remove surrounding quotes if present
if (text.startsWith('"') && text.endsWith('"')) {
text = text.slice(1, -1);
// Unescape doubled quotes
text = text.replaceAll('""', '"');
}
return text;
}
/**
* Load module configuration files
* @param {Array} modules - List of module names
* @returns {Object} Module configurations indexed by name
*/
async loadModuleConfigs(modules) {
const configs = {};
for (const moduleName of modules) {
// Handle core module differently - it's in src/core-skills not src/modules/core
const configPath =
moduleName === 'core'
? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
: path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
try {
if (await fs.pathExists(configPath)) {
const yaml = require('yaml');
const content = await fs.readFile(configPath, 'utf8');
configs[moduleName] = yaml.parse(content);
}
} catch (error) {
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
}
}
return configs;
}
/** /**
* Get module version info from source * Get module version info from source
* @param {string} moduleName - Module name/code * @param {string} moduleName - Module name/code
@ -986,47 +450,6 @@ class Manifest {
return updates; return updates;
} }
/**
* Compare two semantic versions
* @param {string} v1 - First version
* @param {string} v2 - Second version
* @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
*/
compareVersions(v1, v2) {
if (!v1 || !v2) return 0;
const normalize = (v) => {
// Remove leading 'v' if present
v = v.replace(/^v/, '');
// Handle prerelease tags
const parts = v.split('-');
const main = parts[0].split('.');
const prerelease = parts[1];
return { main, prerelease };
};
const n1 = normalize(v1);
const n2 = normalize(v2);
// Compare main version parts
for (let i = 0; i < 3; i++) {
const num1 = parseInt(n1.main[i] || '0', 10);
const num2 = parseInt(n2.main[i] || '0', 10);
if (num1 !== num2) {
return num1 < num2 ? -1 : 1;
}
}
// If main versions are equal, compare prerelease
if (n1.prerelease && n2.prerelease) {
return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
}
if (n1.prerelease) return -1; // Prerelease is older than stable
if (n2.prerelease) return 1; // Stable is newer than prerelease
return 0;
}
} }
module.exports = { Manifest }; module.exports = { Manifest };

View File

@ -1,136 +0,0 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const { glob } = require('glob');
const { getSourcePath } = require('../../project-root');
async function loadModuleInjectionConfig(handler, moduleName) {
const sourceModulesPath = getSourcePath('modules');
const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler);
const configPath = path.join(handlerBaseDir, 'injections.yaml');
if (!(await fs.pathExists(configPath))) {
return null;
}
const configContent = await fs.readFile(configPath, 'utf8');
const config = yaml.parse(configContent) || {};
return {
config,
handlerBaseDir,
configPath,
};
}
function shouldApplyInjection(injection, subagentChoices) {
if (!subagentChoices || subagentChoices.install === 'none') {
return false;
}
if (subagentChoices.install === 'all') {
return true;
}
if (subagentChoices.install === 'selective') {
const selected = subagentChoices.selected || [];
if (injection.requires === 'any' && selected.length > 0) {
return true;
}
if (injection.requires) {
const required = `${injection.requires}.md`;
return selected.includes(required);
}
if (injection.point) {
const selectedNames = selected.map((file) => file.replace('.md', ''));
return selectedNames.some((name) => injection.point.includes(name));
}
}
return false;
}
function filterAgentInstructions(content, selectedFiles) {
if (!selectedFiles || selectedFiles.length === 0) {
return '';
}
const selectedAgents = selectedFiles.map((file) => file.replace('.md', ''));
const lines = content.split('\n');
const filteredLines = [];
for (const line of lines) {
if (line.includes('<llm') || line.includes('</llm>')) {
filteredLines.push(line);
} else if (line.includes('subagent')) {
let shouldInclude = false;
for (const agent of selectedAgents) {
if (line.includes(agent)) {
shouldInclude = true;
break;
}
}
if (shouldInclude) {
filteredLines.push(line);
}
} else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
filteredLines.push(line);
}
}
if (filteredLines.length > 2) {
return filteredLines.join('\n');
}
return '';
}
async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) {
if (!subagentConfig || !subagentConfig.files) {
return [];
}
if (!subagentChoices || subagentChoices.install === 'none') {
return [];
}
let filesToCopy = subagentConfig.files;
if (subagentChoices.install === 'selective') {
filesToCopy = subagentChoices.selected || [];
}
const sourceDir = path.join(handlerBaseDir, subagentConfig.source || '');
const resolved = [];
for (const file of filesToCopy) {
// Use forward slashes for glob pattern (works on both Windows and Unix)
// Convert backslashes to forward slashes for glob compatibility
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
const pattern = `${normalizedSourceDir}/**/${file}`;
const matches = await glob(pattern);
if (matches.length > 0) {
const absolutePath = matches[0];
resolved.push({
file,
absolutePath,
relativePath: path.relative(sourceDir, absolutePath),
sourceDir,
});
}
}
return resolved;
}
module.exports = {
loadModuleInjectionConfig,
shouldApplyInjection,
filterAgentInstructions,
resolveSubagentFiles,
};

View File

@ -15,8 +15,6 @@
* - standalone/agents/fred.md bmad-agent-standalone-fred.md * - standalone/agents/fred.md bmad-agent-standalone-fred.md
*/ */
// Type segments - agents are included in naming, others are filtered out
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
const AGENT_SEGMENT = 'agents'; const AGENT_SEGMENT = 'agents';
// BMAD installation folder name - centralized constant for all installers // BMAD installation folder name - centralized constant for all installers
@ -194,125 +192,6 @@ function parseDashName(filename) {
}; };
} }
// ============================================================================
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
// ============================================================================
/**
* Convert hierarchical path to flat underscore-separated name (LEGACY)
* @deprecated Use toDashName instead
*/
function toUnderscoreName(module, type, name) {
const isAgent = type === AGENT_SEGMENT;
if (module === 'core') {
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
}
if (module === 'standalone') {
return isAgent ? `bmad_agent_standalone_${name}.md` : `bmad_standalone_${name}.md`;
}
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
}
/**
* Convert relative path to flat underscore-separated name (LEGACY)
* @deprecated Use toDashPath instead
*/
function toUnderscorePath(relativePath) {
// Strip common file extensions (same as toDashPath for consistency)
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
const parts = withoutExt.split(/[/\\]/);
const module = parts[0];
const type = parts[1];
const name = parts.slice(2).join('_');
return toUnderscoreName(module, type, name);
}
/**
* Create custom agent underscore name (LEGACY)
* @deprecated Use customAgentDashName instead
*/
function customAgentUnderscoreName(agentName) {
return `bmad_custom_${agentName}.md`;
}
/**
* Check if a filename uses underscore format (LEGACY)
* @deprecated Use isDashFormat instead
*/
function isUnderscoreFormat(filename) {
return filename.startsWith('bmad_') && filename.includes('_');
}
/**
* Extract parts from an underscore-formatted filename (LEGACY)
* @deprecated Use parseDashName instead
*/
function parseUnderscoreName(filename) {
const withoutExt = filename.replace('.md', '');
const parts = withoutExt.split('_');
if (parts.length < 2 || parts[0] !== 'bmad') {
return null;
}
const agentIndex = parts.indexOf('agent');
if (agentIndex !== -1) {
if (agentIndex === 1) {
// bmad_agent_... - check for standalone
if (parts.length >= 4 && parts[2] === 'standalone') {
return {
prefix: parts[0],
module: 'standalone',
type: 'agents',
name: parts.slice(3).join('_'),
};
}
return {
prefix: parts[0],
module: 'core',
type: 'agents',
name: parts.slice(agentIndex + 1).join('_'),
};
} else {
return {
prefix: parts[0],
module: parts[1],
type: 'agents',
name: parts.slice(agentIndex + 1).join('_'),
};
}
}
if (parts.length === 2) {
return {
prefix: parts[0],
module: 'core',
type: 'workflows',
name: parts[1],
};
}
// Check for standalone non-agent: bmad_standalone_name
if (parts[1] === 'standalone') {
return {
prefix: parts[0],
module: 'standalone',
type: 'workflows',
name: parts.slice(2).join('_'),
};
}
return {
prefix: parts[0],
module: parts[1],
type: 'workflows',
name: parts.slice(2).join('_'),
};
}
/** /**
* Resolve the skill name for an artifact. * Resolve the skill name for an artifact.
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available, * Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
return toDashPath(artifact.relativePath); return toDashPath(artifact.relativePath);
} }
// Backward compatibility aliases (colon format was same as underscore)
const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath;
const customAgentColonName = customAgentUnderscoreName;
const isColonFormat = isUnderscoreFormat;
const parseColonName = parseUnderscoreName;
module.exports = { module.exports = {
// New standard (dash-based)
toDashName, toDashName,
toDashPath, toDashPath,
resolveSkillName, resolveSkillName,
customAgentDashName, customAgentDashName,
isDashFormat, isDashFormat,
parseDashName, parseDashName,
// Legacy (underscore-based) - kept for backward compatibility
toUnderscoreName,
toUnderscorePath,
customAgentUnderscoreName,
isUnderscoreFormat,
parseUnderscoreName,
// Backward compatibility aliases
toColonName,
toColonPath,
customAgentColonName,
isColonFormat,
parseColonName,
TYPE_SEGMENTS,
AGENT_SEGMENT, AGENT_SEGMENT,
BMAD_FOLDER_NAME, BMAD_FOLDER_NAME,
}; };

View File

@ -155,33 +155,6 @@ class CustomModuleManager {
}; };
} }
/**
* @deprecated Use parseSource() instead. Kept for backward compatibility.
* Parse and validate a GitHub repository URL.
* @param {string} url - GitHub URL to validate
* @returns {Object} { owner, repo, isValid, error }
*/
validateGitHubUrl(url) {
if (!url || typeof url !== 'string') {
return { owner: null, repo: null, isValid: false, error: 'URL is required' };
}
const trimmed = url.trim();
// HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
if (httpsMatch) {
return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
}
// SSH format: git@github.com:owner/repo[.git]
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
if (sshMatch) {
return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
}
return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
}
// ─── Marketplace JSON ───────────────────────────────────────────────────── // ─── Marketplace JSON ─────────────────────────────────────────────────────
/** /**

View File

@ -109,46 +109,6 @@ class ExternalModuleManager {
return modules.find((m) => m.code === code) || null; return modules.find((m) => m.code === code) || null;
} }
/**
* Get module info by key
* @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
* @returns {Object|null} Module info or null if not found
*/
async getModuleByKey(key) {
const modules = await this.listAvailable();
return modules.find((m) => m.key === key) || null;
}
/**
* Check if a module code exists in external modules
* @param {string} code - The module code to check
* @returns {boolean} True if the module exists
*/
async hasModule(code) {
const module = await this.getModuleByCode(code);
return module !== null;
}
/**
* Get the URL for a module by code
* @param {string} code - The module code
* @returns {string|null} The URL or null if not found
*/
async getModuleUrl(code) {
const module = await this.getModuleByCode(code);
return module ? module.url : null;
}
/**
* Get the module definition path for a module by code
* @param {string} code - The module code
* @returns {string|null} The module definition path or null if not found
*/
async getModuleDefinition(code) {
const module = await this.getModuleByCode(code);
return module ? module.moduleDefinition : null;
}
/** /**
* Get the cache directory for external modules * Get the cache directory for external modules
* @returns {string} Path to the external modules cache directory * @returns {string} Path to the external modules cache directory

View File

@ -12,7 +12,6 @@ class OfficialModules {
// Config collection state (merged from ConfigCollector) // Config collection state (merged from ConfigCollector)
this.collectedConfig = {}; this.collectedConfig = {};
this._existingConfig = null; this._existingConfig = null;
this.currentProjectDir = null;
} }
/** /**
@ -500,32 +499,6 @@ class OfficialModules {
} }
} }
/**
* Find all .md agent files recursively in a directory
* @param {string} dir - Directory to search
* @returns {Array} List of .md agent file paths
*/
async findAgentMdFiles(dir) {
const agentFiles = [];
async function searchDirectory(searchDir) {
const entries = await fs.readdir(searchDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(searchDir, entry.name);
if (entry.isFile() && entry.name.endsWith('.md')) {
agentFiles.push(fullPath);
} else if (entry.isDirectory()) {
await searchDirectory(fullPath);
}
}
}
await searchDirectory(dir);
return agentFiles;
}
/** /**
* Create directories declared in module.yaml's `directories` key * Create directories declared in module.yaml's `directories` key
* This replaces the security-risky module installer pattern with declarative config * This replaces the security-risky module installer pattern with declarative config
@ -699,29 +672,6 @@ class OfficialModules {
return { createdDirs, movedDirs, createdWdsFolders }; return { createdDirs, movedDirs, createdWdsFolders };
} }
/**
* Private: Process module configuration
* @param {string} modulePath - Path to installed module
* @param {string} moduleName - Module name
*/
async processModuleConfig(modulePath, moduleName) {
const configPath = path.join(modulePath, 'config.yaml');
if (await fs.pathExists(configPath)) {
try {
let configContent = await fs.readFile(configPath, 'utf8');
// Replace path placeholders
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
configContent = configContent.replaceAll('{module}', moduleName);
await fs.writeFile(configPath, configContent, 'utf8');
} catch (error) {
await prompts.log.warn(`Failed to process module config: ${error.message}`);
}
}
}
/** /**
* Private: Sync module files (preserving user modifications) * Private: Sync module files (preserving user modifications)
* @param {string} sourcePath - Source module path * @param {string} sourcePath - Source module path
@ -1090,8 +1040,6 @@ class OfficialModules {
* @returns {boolean} True if new fields were prompted, false if all fields existed * @returns {boolean} True if new fields were prompted, false if all fields existed
*/ */
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) { async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
this.currentProjectDir = projectDir;
// Load existing config if not already loaded // Load existing config if not already loaded
if (!this._existingConfig) { if (!this._existingConfig) {
await this.loadExistingConfig(projectDir); await this.loadExistingConfig(projectDir);
@ -1382,7 +1330,6 @@ class OfficialModules {
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection) * @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
*/ */
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) { async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
this.currentProjectDir = projectDir;
// Load existing config if needed and not already loaded // Load existing config if needed and not already loaded
if (!skipLoadExisting && !this._existingConfig) { if (!skipLoadExisting && !this._existingConfig) {
await this.loadExistingConfig(projectDir); await this.loadExistingConfig(projectDir);

View File

@ -50,17 +50,6 @@ class RegistryClient {
const content = await this.fetch(url, timeout); const content = await this.fetch(url, timeout);
return yaml.parse(content); return yaml.parse(content);
} }
/**
* Fetch a URL and parse the response as JSON.
* @param {string} url - URL to fetch
* @param {number} [timeout] - Timeout in ms
* @returns {Promise<Object>} Parsed JSON content
*/
async fetchJson(url, timeout) {
const content = await this.fetch(url, timeout);
return JSON.parse(content);
}
} }
module.exports = { RegistryClient }; module.exports = { RegistryClient };

View File

@ -498,26 +498,6 @@ async function password(options) {
return result; return result;
} }
/**
* Group multiple prompts together
* @param {Object} prompts - Object of prompt functions
* @param {Object} [options] - Group options
* @returns {Promise<Object>} Object with all answers
*/
async function group(prompts, options = {}) {
const clack = await getClack();
const result = await clack.group(prompts, {
onCancel: () => {
clack.cancel('Operation cancelled');
process.exit(0);
},
...options,
});
return result;
}
/** /**
* Run tasks with spinner feedback * Run tasks with spinner feedback
* @param {Array} tasks - Array of task objects [{title, task, enabled?}] * @param {Array} tasks - Array of task objects [{title, task, enabled?}]
@ -578,42 +558,6 @@ async function box(content, title, options) {
clack.box(content, title, options); clack.box(content, title, options);
} }
/**
* Create a progress bar for visualizing task completion
* @param {Object} [options] - Progress options (max, style, etc.)
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
*/
async function progress(options) {
const clack = await getClack();
return clack.progress(options);
}
/**
* Create a task log for displaying scrolling subprocess output
* @param {Object} options - TaskLog options (title, limit, retainLog)
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
*/
async function taskLog(options) {
const clack = await getClack();
return clack.taskLog(options);
}
/**
* File system path prompt with autocomplete
* @param {Object} options - Path options
* @param {string} options.message - The prompt message
* @param {string} [options.initialValue] - Initial path value
* @param {boolean} [options.directory=false] - Only allow directories
* @param {Function} [options.validate] - Validation function
* @returns {Promise<string>} Selected path
*/
async function pathPrompt(options) {
const clack = await getClack();
const result = await clack.path(options);
await handleCancel(result);
return result;
}
/** /**
* Autocomplete single-select prompt with type-ahead filtering * Autocomplete single-select prompt with type-ahead filtering
* @param {Object} options - Autocomplete options * @param {Object} options - Autocomplete options
@ -631,50 +575,6 @@ async function autocomplete(options) {
return result; return result;
} }
/**
* Key-based instant selection prompt
* @param {Object} options - SelectKey options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @returns {Promise<any>} Selected value
*/
async function selectKey(options) {
const clack = await getClack();
const result = await clack.selectKey(options);
await handleCancel(result);
return result;
}
/**
* Stream messages with dynamic content (for LLMs, generators, etc.)
*/
const stream = {
async info(generator) {
const clack = await getClack();
return clack.stream.info(generator);
},
async success(generator) {
const clack = await getClack();
return clack.stream.success(generator);
},
async step(generator) {
const clack = await getClack();
return clack.stream.step(generator);
},
async warn(generator) {
const clack = await getClack();
return clack.stream.warn(generator);
},
async error(generator) {
const clack = await getClack();
return clack.stream.error(generator);
},
async message(generator, options) {
const clack = await getClack();
return clack.stream.message(generator, options);
},
};
/** /**
* Get the color utility (picocolors instance from @clack/prompts) * Get the color utility (picocolors instance from @clack/prompts)
* @returns {Promise<Object>} The color utility (picocolors) * @returns {Promise<Object>} The color utility (picocolors)
@ -790,20 +690,14 @@ module.exports = {
note, note,
box, box,
spinner, spinner,
progress,
taskLog,
select, select,
multiselect, multiselect,
autocompleteMultiselect, autocompleteMultiselect,
autocomplete, autocomplete,
selectKey,
confirm, confirm,
text, text,
path: pathPrompt,
password, password,
group,
tasks, tasks,
log, log,
stream,
prompt, prompt,
}; };