From ea99b7ece5cf8cc9b18240dbe6c2ba066561a4fe Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Fri, 10 Apr 2026 20:24:50 -0700 Subject: [PATCH] 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. --- test/test-installation-components.js | 49 -- tools/installer/cli-utils.js | 137 ----- tools/installer/core/manifest.js | 577 ------------------ .../installer/ide/shared/module-injections.js | 136 ----- tools/installer/ide/shared/path-utils.js | 145 ----- .../modules/custom-module-manager.js | 27 - tools/installer/modules/external-manager.js | 40 -- tools/installer/modules/official-modules.js | 52 +- tools/installer/modules/registry-client.js | 11 - tools/installer/prompts.js | 106 ---- 10 files changed, 2 insertions(+), 1278 deletions(-) delete mode 100644 tools/installer/ide/shared/module-injections.js diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 45c3ea19c..10639bab8 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -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(''); // ============================================================ diff --git a/tools/installer/cli-utils.js b/tools/installer/cli-utils.js index a0efdbe06..b2b7b0979 100644 --- a/tools/installer/cli-utils.js +++ b/tools/installer/cli-utils.js @@ -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 }; diff --git a/tools/installer/core/manifest.js b/tools/installer/core/manifest.js index 1ba776ffd..aaa86649a 100644 --- a/tools/installer/core/manifest.js +++ b/tools/installer/core/manifest.js @@ -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 }; diff --git a/tools/installer/ide/shared/module-injections.js b/tools/installer/ide/shared/module-injections.js deleted file mode 100644 index 3090c5da4..000000000 --- a/tools/installer/ide/shared/module-injections.js +++ /dev/null @@ -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('')) { - 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, -}; diff --git a/tools/installer/ide/shared/path-utils.js b/tools/installer/ide/shared/path-utils.js index 35fc263f4..6d7c2c9fa 100644 --- a/tools/installer/ide/shared/path-utils.js +++ b/tools/installer/ide/shared/path-utils.js @@ -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, }; diff --git a/tools/installer/modules/custom-module-manager.js b/tools/installer/modules/custom-module-manager.js index 3e921e317..e0f8b7085 100644 --- a/tools/installer/modules/custom-module-manager.js +++ b/tools/installer/modules/custom-module-manager.js @@ -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 ───────────────────────────────────────────────────── /** diff --git a/tools/installer/modules/external-manager.js b/tools/installer/modules/external-manager.js index f9f9ff06e..0b8f5074c 100644 --- a/tools/installer/modules/external-manager.js +++ b/tools/installer/modules/external-manager.js @@ -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 diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 2e18c1a15..6158a7863 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -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); diff --git a/tools/installer/modules/registry-client.js b/tools/installer/modules/registry-client.js index 31965e00c..53d220678 100644 --- a/tools/installer/modules/registry-client.js +++ b/tools/installer/modules/registry-client.js @@ -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} Parsed JSON content - */ - async fetchJson(url, timeout) { - const content = await this.fetch(url, timeout); - return JSON.parse(content); - } } module.exports = { RegistryClient }; diff --git a/tools/installer/prompts.js b/tools/installer/prompts.js index 24500700b..4f46e69b1 100644 --- a/tools/installer/prompts.js +++ b/tools/installer/prompts.js @@ -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 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} 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} 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} 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} 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} 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, };