chore(installer): remove 1,683 lines of dead code (#2247)
* 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.
* fix(installer): restore currentProjectDir writes for placeholder expansion
The previous commit removed the three assignments to
OfficialModules.currentProjectDir as dead code, but buildQuestion()
still reads the property to resolve {directory_name} placeholders in
module config defaults during interactive collection. Without the
writes, any module default containing {directory_name} would surface
the literal placeholder to users.
This commit is contained in:
parent
eabcd03f65
commit
ea99b7ece5
|
|
@ -1728,36 +1728,6 @@ async function runTests() {
|
|||
// ============================================================
|
||||
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 ---
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
// --- 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('');
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const prompts = require('./prompts');
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
@ -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
|
||||
* @param {string} moduleName - Module name (fallback if no custom header)
|
||||
|
|
@ -93,98 +48,6 @@ const CLIUtils = {
|
|||
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -107,117 +107,6 @@ class Manifest {
|
|||
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
|
||||
* @param {string} bmadDir - Path to bmad directory
|
||||
|
|
@ -310,62 +199,6 @@ class 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
|
||||
* @param {string} bmadDir - Path to bmad directory
|
||||
|
|
@ -403,27 +236,6 @@ class Manifest {
|
|||
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
|
||||
* @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
|
||||
* @param {string} moduleName - Module name/code
|
||||
|
|
@ -986,47 +450,6 @@ class Manifest {
|
|||
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -15,8 +15,6 @@
|
|||
* - 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';
|
||||
|
||||
// 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.
|
||||
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
||||
|
|
@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
|
|||
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 = {
|
||||
// New standard (dash-based)
|
||||
toDashName,
|
||||
toDashPath,
|
||||
resolveSkillName,
|
||||
customAgentDashName,
|
||||
isDashFormat,
|
||||
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,
|
||||
BMAD_FOLDER_NAME,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -109,46 +109,6 @@ class ExternalModuleManager {
|
|||
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
|
||||
* @returns {string} Path to the external modules cache directory
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class OfficialModules {
|
|||
// Config collection state (merged from ConfigCollector)
|
||||
this.collectedConfig = {};
|
||||
this._existingConfig = null;
|
||||
// Tracked during interactive config collection so {directory_name}
|
||||
// placeholder defaults can be resolved in buildQuestion().
|
||||
this.currentProjectDir = null;
|
||||
}
|
||||
|
||||
|
|
@ -500,32 +502,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
|
||||
* This replaces the security-risky module installer pattern with declarative config
|
||||
|
|
@ -699,29 +675,6 @@ class OfficialModules {
|
|||
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)
|
||||
* @param {string} sourcePath - Source module path
|
||||
|
|
@ -1091,7 +1044,6 @@ class OfficialModules {
|
|||
*/
|
||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||
this.currentProjectDir = projectDir;
|
||||
|
||||
// Load existing config if not already loaded
|
||||
if (!this._existingConfig) {
|
||||
await this.loadExistingConfig(projectDir);
|
||||
|
|
|
|||
|
|
@ -50,17 +50,6 @@ class RegistryClient {
|
|||
const content = await this.fetch(url, timeout);
|
||||
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 };
|
||||
|
|
|
|||
|
|
@ -498,26 +498,6 @@ async function password(options) {
|
|||
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
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Object} options - Autocomplete options
|
||||
|
|
@ -631,50 +575,6 @@ async function autocomplete(options) {
|
|||
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)
|
||||
* @returns {Promise<Object>} The color utility (picocolors)
|
||||
|
|
@ -790,20 +690,14 @@ module.exports = {
|
|||
note,
|
||||
box,
|
||||
spinner,
|
||||
progress,
|
||||
taskLog,
|
||||
select,
|
||||
multiselect,
|
||||
autocompleteMultiselect,
|
||||
autocomplete,
|
||||
selectKey,
|
||||
confirm,
|
||||
text,
|
||||
path: pathPrompt,
|
||||
password,
|
||||
group,
|
||||
tasks,
|
||||
log,
|
||||
stream,
|
||||
prompt,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue