const path = require('node:path'); const os = require('node:os'); const semver = require('semver'); const fs = require('./fs-native'); const installerPackageJson = require('../../package.json'); const { CLIUtils } = require('./cli-utils'); const { ExternalModuleManager } = require('./modules/external-manager'); const { resolveModuleVersion } = require('./modules/version-resolver'); const { Manifest } = require('./core/manifest'); const { parseChannelOptions, buildPlan, decideChannelForModule, orphanPinWarnings, bundledTargetWarnings, } = require('./modules/channel-plan'); const channelResolver = require('./modules/channel-resolver'); const prompts = require('./prompts'); const { parseSetEntries } = require('./set-overrides'); const manifest = new Manifest(); /** * Format a resolved version for display in installer labels. * Semver-like values are normalized to a single leading "v". * @param {string|null|undefined} version * @returns {string} */ function formatDisplayVersion(version) { const trimmed = typeof version === 'string' ? version.trim() : ''; if (!trimmed) return ''; const normalized = semver.valid(semver.coerce(trimmed)); if (normalized) { return `v${normalized}`; } return trimmed; } /** * Build the display label for a module, showing an upgrade arrow when an * installed semver differs from the latest resolvable semver. * @param {string} name * @param {string} latestVersion * @param {string} installedVersion * @returns {string} */ function buildModuleLabel(name, latestVersion, installedVersion = '') { const latestDisplay = formatDisplayVersion(latestVersion); if (!latestDisplay) return name; const installedDisplay = formatDisplayVersion(installedVersion); const latestSemver = semver.valid(semver.coerce(latestVersion || '')); const installedSemver = semver.valid(semver.coerce(installedVersion || '')); if (installedDisplay && latestSemver && installedSemver && semver.neq(installedSemver, latestSemver)) { return `${name} (${installedDisplay} → ${latestDisplay})`; } return `${name} (${latestDisplay})`; } /** * Resolve the version to show for a module picker entry. External modules use * the same channel/tag resolver as installs; bundled modules fall back to local * source metadata. * @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis') * @param {Object} options * @param {string|null} [options.repoUrl] - Module repository URL for tag resolution * @param {string|null} [options.registryDefault] - Registry default channel * @param {Object|null} [options.channelOptions] - Parsed installer channel options * @returns {Promise<{version: string, lookupAttempted: boolean, lookupSucceeded: boolean}>} */ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault = null, channelOptions = null } = {}) { if (repoUrl) { const plan = decideChannelForModule({ code: moduleCode, channelOptions, registryDefault, }); try { const resolved = await channelResolver.resolveChannel({ channel: plan.channel, pin: plan.pin, repoUrl, }); if (resolved?.version) { return { version: resolved.version, lookupAttempted: plan.channel === 'stable', lookupSucceeded: true, }; } } catch { // Fall back to local metadata when tag resolution is unavailable. } } const versionInfo = await resolveModuleVersion(moduleCode); return { version: versionInfo.version || '', lookupAttempted: !!repoUrl, lookupSucceeded: false, }; } /** * UI utilities for the installer */ class UI { async _retainUnavailableInstalledModules(selectedModules, installedModuleIds, bmadDir, options = {}) { const { OfficialModules } = require('./modules/official-modules'); const officialCodes = new Set(['core']); const builtInModules = (await new OfficialModules().listAvailable()).modules || []; for (const mod of builtInModules) { officialCodes.add(mod.id); } const externalManager = new ExternalModuleManager(); const registryModules = await externalManager.listAvailable(); for (const mod of registryModules) { officialCodes.add(mod.code); } const { CustomModuleManager } = require('./modules/custom-module-manager'); const customMgr = new CustomModuleManager(); const selectedSet = new Set(selectedModules); const preserveModules = []; for (const moduleId of installedModuleIds) { if (moduleId === 'core') continue; if (!selectedSet.has(moduleId) && !options.preserveUnselected) continue; if (officialCodes.has(moduleId)) continue; const customSource = await customMgr.findModuleSourceByCode(moduleId, { bmadDir }); if (!customSource) { preserveModules.push(moduleId); } } const preservedSet = new Set(preserveModules); return { selectedModules: selectedModules.filter((moduleId) => !preservedSet.has(moduleId)), preserveModules, }; } /** * Prompt for installation configuration * @param {Object} options - Command-line options from install command * @returns {Object} Installation configuration */ async promptInstall(options = {}) { await CLIUtils.displayLogo(); // Display version-specific start message from install-messages.yaml const { MessageLoader } = require('./message-loader'); const messageLoader = new MessageLoader(); await messageLoader.displayStartMessage(); // Probe for `uv` before any other prompts: it's becoming the de facto // runner for the Python scripts BMAD workflows shell out to // (`uv run