fix: installer live version for external modules (#2307)
* resolved merge conflict * fix: addressed PR comments * fix: use git tags for installer module versions
This commit is contained in:
parent
3d824d4c0f
commit
0533976753
|
|
@ -2622,6 +2622,229 @@ async function runTests() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Official module picker uses git tags for external module labels ---
|
||||||
|
{
|
||||||
|
const { UI } = require('../tools/installer/ui');
|
||||||
|
const prompts = require('../tools/installer/prompts');
|
||||||
|
const channelResolver = require('../tools/installer/modules/channel-resolver');
|
||||||
|
const { ExternalModuleManager } = require('../tools/installer/modules/external-manager');
|
||||||
|
|
||||||
|
const ui = new UI();
|
||||||
|
const originalOfficialListAvailable39 = OfficialModules.prototype.listAvailable;
|
||||||
|
const originalExternalListAvailable39 = ExternalModuleManager.prototype.listAvailable;
|
||||||
|
const originalAutocomplete39 = prompts.autocompleteMultiselect;
|
||||||
|
const originalSpinner39 = prompts.spinner;
|
||||||
|
const originalWarn39 = prompts.log.warn;
|
||||||
|
const originalMessage39 = prompts.log.message;
|
||||||
|
const originalResolveChannel39 = channelResolver.resolveChannel;
|
||||||
|
|
||||||
|
const seenLabels39 = [];
|
||||||
|
const spinnerStarts39 = [];
|
||||||
|
const spinnerStops39 = [];
|
||||||
|
const warnings39 = [];
|
||||||
|
|
||||||
|
OfficialModules.prototype.listAvailable = async function () {
|
||||||
|
return {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
id: 'core',
|
||||||
|
name: 'BMad Core Module',
|
||||||
|
description: 'always installed',
|
||||||
|
defaultSelected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ExternalModuleManager.prototype.listAvailable = async function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: 'bmb',
|
||||||
|
name: 'BMad Builder',
|
||||||
|
description: 'Builder module',
|
||||||
|
defaultSelected: false,
|
||||||
|
builtIn: false,
|
||||||
|
url: 'https://github.com/bmad-code-org/bmad-builder',
|
||||||
|
defaultChannel: 'stable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'tea',
|
||||||
|
name: 'Test Architect',
|
||||||
|
description: 'Test architecture module',
|
||||||
|
defaultSelected: false,
|
||||||
|
builtIn: false,
|
||||||
|
url: 'https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise',
|
||||||
|
defaultChannel: 'stable',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
channelResolver.resolveChannel = async function ({ repoUrl, channel }) {
|
||||||
|
if (channel !== 'stable') {
|
||||||
|
return { channel, version: channel === 'next' ? 'main' : 'unknown' };
|
||||||
|
}
|
||||||
|
if (repoUrl.includes('bmad-builder')) {
|
||||||
|
return { channel: 'stable', version: 'v1.7.0', ref: 'v1.7.0', resolvedFallback: false };
|
||||||
|
}
|
||||||
|
if (repoUrl.includes('bmad-method-test-architecture-enterprise')) {
|
||||||
|
return { channel: 'stable', version: 'v1.15.0', ref: 'v1.15.0', resolvedFallback: false };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected repo ${repoUrl}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
prompts.autocompleteMultiselect = async (options) => {
|
||||||
|
seenLabels39.push(...options.options.map((opt) => opt.label));
|
||||||
|
return ['core'];
|
||||||
|
};
|
||||||
|
prompts.spinner = async () => ({
|
||||||
|
start(message) {
|
||||||
|
spinnerStarts39.push(message);
|
||||||
|
},
|
||||||
|
stop(message) {
|
||||||
|
spinnerStops39.push(message);
|
||||||
|
},
|
||||||
|
error(message) {
|
||||||
|
spinnerStops39.push(`error:${message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prompts.log.warn = async (message) => {
|
||||||
|
warnings39.push(message);
|
||||||
|
};
|
||||||
|
prompts.log.message = async () => {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ui._selectOfficialModules(
|
||||||
|
new Set(['bmb']),
|
||||||
|
new Map([
|
||||||
|
['bmb', '1.1.0'],
|
||||||
|
['core', '6.2.0'],
|
||||||
|
]),
|
||||||
|
{ global: null, nextSet: new Set(), pins: new Map(), warnings: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
seenLabels39.includes('BMad Builder (v1.1.0 → v1.7.0)'),
|
||||||
|
'official module picker shows installed-to-latest arrow from git tags',
|
||||||
|
);
|
||||||
|
assert(seenLabels39.includes('Test Architect (v1.15.0)'), 'official module picker shows latest git-tag version for fresh installs');
|
||||||
|
assert(
|
||||||
|
spinnerStarts39.includes('Checking latest module versions...'),
|
||||||
|
'official module picker wraps external lookups in a single spinner',
|
||||||
|
);
|
||||||
|
assert(spinnerStops39.includes('Checked latest module versions.'), 'official module picker stops the version-check spinner');
|
||||||
|
assert(warnings39.length === 0, 'official module picker does not warn when tag lookups succeed');
|
||||||
|
} finally {
|
||||||
|
OfficialModules.prototype.listAvailable = originalOfficialListAvailable39;
|
||||||
|
ExternalModuleManager.prototype.listAvailable = originalExternalListAvailable39;
|
||||||
|
prompts.autocompleteMultiselect = originalAutocomplete39;
|
||||||
|
prompts.spinner = originalSpinner39;
|
||||||
|
prompts.log.warn = originalWarn39;
|
||||||
|
prompts.log.message = originalMessage39;
|
||||||
|
channelResolver.resolveChannel = originalResolveChannel39;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Official module picker warns and falls back to cached versions when tag lookups fail ---
|
||||||
|
{
|
||||||
|
const { UI } = require('../tools/installer/ui');
|
||||||
|
const prompts = require('../tools/installer/prompts');
|
||||||
|
const channelResolver = require('../tools/installer/modules/channel-resolver');
|
||||||
|
const { ExternalModuleManager } = require('../tools/installer/modules/external-manager');
|
||||||
|
|
||||||
|
const ui = new UI();
|
||||||
|
const tempCacheDir39 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-picker-cache-'));
|
||||||
|
const priorCacheEnv39 = process.env.BMAD_EXTERNAL_MODULES_CACHE;
|
||||||
|
const originalOfficialListAvailable39 = OfficialModules.prototype.listAvailable;
|
||||||
|
const originalExternalListAvailable39 = ExternalModuleManager.prototype.listAvailable;
|
||||||
|
const originalAutocomplete39 = prompts.autocompleteMultiselect;
|
||||||
|
const originalSpinner39 = prompts.spinner;
|
||||||
|
const originalWarn39 = prompts.log.warn;
|
||||||
|
const originalMessage39 = prompts.log.message;
|
||||||
|
const originalResolveChannel39 = channelResolver.resolveChannel;
|
||||||
|
|
||||||
|
const seenLabels39 = [];
|
||||||
|
const warnings39 = [];
|
||||||
|
|
||||||
|
process.env.BMAD_EXTERNAL_MODULES_CACHE = tempCacheDir39;
|
||||||
|
await fs.ensureDir(path.join(tempCacheDir39, 'bmb'));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(tempCacheDir39, 'bmb', 'package.json'),
|
||||||
|
JSON.stringify({ name: 'bmad-builder', version: '1.7.0' }, null, 2) + '\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
OfficialModules.prototype.listAvailable = async function () {
|
||||||
|
return {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
id: 'core',
|
||||||
|
name: 'BMad Core Module',
|
||||||
|
description: 'always installed',
|
||||||
|
defaultSelected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ExternalModuleManager.prototype.listAvailable = async function () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
code: 'bmb',
|
||||||
|
name: 'BMad Builder',
|
||||||
|
description: 'Builder module',
|
||||||
|
defaultSelected: false,
|
||||||
|
builtIn: false,
|
||||||
|
url: 'https://github.com/bmad-code-org/bmad-builder',
|
||||||
|
defaultChannel: 'stable',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
channelResolver.resolveChannel = async function () {
|
||||||
|
throw new Error('tag lookup unavailable');
|
||||||
|
};
|
||||||
|
|
||||||
|
prompts.autocompleteMultiselect = async (options) => {
|
||||||
|
seenLabels39.push(...options.options.map((opt) => opt.label));
|
||||||
|
return ['core'];
|
||||||
|
};
|
||||||
|
prompts.spinner = async () => ({
|
||||||
|
start() {},
|
||||||
|
stop() {},
|
||||||
|
error() {},
|
||||||
|
});
|
||||||
|
prompts.log.warn = async (message) => {
|
||||||
|
warnings39.push(message);
|
||||||
|
};
|
||||||
|
prompts.log.message = async () => {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ui._selectOfficialModules(new Set(), new Map(), { global: null, nextSet: new Set(), pins: new Map(), warnings: [] });
|
||||||
|
|
||||||
|
assert(
|
||||||
|
seenLabels39.includes('BMad Builder (v1.7.0)'),
|
||||||
|
'official module picker falls back to cached/local versions when tag lookup fails',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
warnings39.includes('Could not check latest module versions; showing cached/local versions.'),
|
||||||
|
'official module picker warns once when all latest-version lookups fail',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
OfficialModules.prototype.listAvailable = originalOfficialListAvailable39;
|
||||||
|
ExternalModuleManager.prototype.listAvailable = originalExternalListAvailable39;
|
||||||
|
prompts.autocompleteMultiselect = originalAutocomplete39;
|
||||||
|
prompts.spinner = originalSpinner39;
|
||||||
|
prompts.log.warn = originalWarn39;
|
||||||
|
prompts.log.message = originalMessage39;
|
||||||
|
channelResolver.resolveChannel = originalResolveChannel39;
|
||||||
|
if (priorCacheEnv39 === undefined) {
|
||||||
|
delete process.env.BMAD_EXTERNAL_MODULES_CACHE;
|
||||||
|
} else {
|
||||||
|
process.env.BMAD_EXTERNAL_MODULES_CACHE = priorCacheEnv39;
|
||||||
|
}
|
||||||
|
await fs.remove(tempCacheDir39).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
const https = require('node:https');
|
||||||
|
const { execFile } = require('node:child_process');
|
||||||
|
const { promisify } = require('node:util');
|
||||||
const fs = require('../fs-native');
|
const fs = require('../fs-native');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const { resolveModuleVersion } = require('../modules/version-resolver');
|
const { resolveModuleVersion } = require('../modules/version-resolver');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const NPM_LOOKUP_TIMEOUT_MS = 10_000;
|
||||||
|
const NPM_PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
||||||
|
|
||||||
|
function isValidNpmPackageName(packageName) {
|
||||||
|
return typeof packageName === 'string' && NPM_PACKAGE_NAME_PATTERN.test(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
class Manifest {
|
class Manifest {
|
||||||
/**
|
/**
|
||||||
* Create a new manifest
|
* Create a new manifest
|
||||||
|
|
@ -362,23 +373,22 @@ class Manifest {
|
||||||
* @returns {string|null} Latest version or null
|
* @returns {string|null} Latest version or null
|
||||||
*/
|
*/
|
||||||
async fetchNpmVersion(packageName) {
|
async fetchNpmVersion(packageName) {
|
||||||
try {
|
if (!isValidNpmPackageName(packageName)) {
|
||||||
const https = require('node:https');
|
return null;
|
||||||
const { execSync } = require('node:child_process');
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// Try using npm view first (more reliable)
|
// Try using npm view first (more reliable)
|
||||||
try {
|
try {
|
||||||
const result = execSync(`npm view ${packageName} version`, {
|
const { stdout } = await execFileAsync('npm', ['view', packageName, 'version'], {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
stdio: 'pipe',
|
timeout: NPM_LOOKUP_TIMEOUT_MS,
|
||||||
timeout: 10_000,
|
|
||||||
});
|
});
|
||||||
return result.trim();
|
return stdout.trim();
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback to npm registry API
|
// Fallback to npm registry API
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
https
|
const request = https.get(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, (res) => {
|
||||||
.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk) => (data += chunk));
|
res.on('data', (chunk) => (data += chunk));
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
|
|
@ -389,8 +399,14 @@ class Manifest {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.on('error', () => resolve(null));
|
|
||||||
|
request.setTimeout(NPM_LOOKUP_TIMEOUT_MS, () => {
|
||||||
|
request.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', () => resolve(null));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,107 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
|
const semver = require('semver');
|
||||||
const fs = require('./fs-native');
|
const fs = require('./fs-native');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||||
const { resolveModuleVersion } = require('./modules/version-resolver');
|
const { resolveModuleVersion } = require('./modules/version-resolver');
|
||||||
const { parseChannelOptions, buildPlan, orphanPinWarnings, bundledTargetWarnings } = require('./modules/channel-plan');
|
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 prompts = require('./prompts');
|
||||||
|
|
||||||
|
const manifest = new Manifest();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a module version from the freshest local metadata available.
|
* Format a resolved version for display in installer labels.
|
||||||
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
* Semver-like values are normalized to a single leading "v".
|
||||||
* @returns {string} Version string or empty string
|
* @param {string|null|undefined} version
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
async function getModuleVersion(moduleCode) {
|
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);
|
const versionInfo = await resolveModuleVersion(moduleCode);
|
||||||
return versionInfo.version || '';
|
return {
|
||||||
|
version: versionInfo.version || '',
|
||||||
|
lookupAttempted: !!repoUrl,
|
||||||
|
lookupSucceeded: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -122,7 +209,7 @@ class UI {
|
||||||
// Return early with modify configuration
|
// Return early with modify configuration
|
||||||
if (actionType === 'update') {
|
if (actionType === 'update') {
|
||||||
// Get existing installation info
|
// Get existing installation info
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
|
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`);
|
||||||
|
|
||||||
|
|
@ -144,7 +231,7 @@ class UI {
|
||||||
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve custom sources from --custom-source flag
|
// Resolve custom sources from --custom-source flag
|
||||||
|
|
@ -208,7 +295,7 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This section is only for new installations (update returns early above)
|
// This section is only for new installations (update returns early above)
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds, installedModuleVersions } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
// Unified module selection - all modules in one grouped multiselect
|
// Unified module selection - all modules in one grouped multiselect
|
||||||
let selectedModules;
|
let selectedModules;
|
||||||
|
|
@ -227,7 +314,7 @@ class UI {
|
||||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||||
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
|
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve custom sources from --custom-source flag
|
// Resolve custom sources from --custom-source flag
|
||||||
|
|
@ -526,7 +613,7 @@ class UI {
|
||||||
/**
|
/**
|
||||||
* Get existing installation info and installed modules
|
* Get existing installation info and installed modules
|
||||||
* @param {string} directory - Installation directory
|
* @param {string} directory - Installation directory
|
||||||
* @returns {Object} Object with existingInstall, installedModuleIds, and bmadDir
|
* @returns {Object} Object with existingInstall, installedModuleIds, installedModuleVersions, and bmadDir
|
||||||
*/
|
*/
|
||||||
async getExistingInstallation(directory) {
|
async getExistingInstallation(directory) {
|
||||||
const { ExistingInstall } = require('./core/existing-install');
|
const { ExistingInstall } = require('./core/existing-install');
|
||||||
|
|
@ -535,8 +622,26 @@ class UI {
|
||||||
const { bmadDir } = await installer.findBmadDir(directory);
|
const { bmadDir } = await installer.findBmadDir(directory);
|
||||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||||
const installedModuleIds = new Set(existingInstall.moduleIds);
|
const installedModuleIds = new Set(existingInstall.moduleIds);
|
||||||
|
const installedModuleVersions = new Map();
|
||||||
|
const manifestModules = await manifest.getAllModuleVersions(bmadDir);
|
||||||
|
|
||||||
return { existingInstall, installedModuleIds, bmadDir };
|
for (const module of manifestModules) {
|
||||||
|
if (module?.name && module.version) {
|
||||||
|
installedModuleVersions.set(module.name, module.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const module of existingInstall.modules) {
|
||||||
|
if (module?.id && module.version && module.version !== 'unknown' && !installedModuleVersions.has(module.id)) {
|
||||||
|
installedModuleVersions.set(module.id, module.version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingInstall.hasCore && existingInstall.version && !installedModuleVersions.has('core')) {
|
||||||
|
installedModuleVersions.set('core', existingInstall.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { existingInstall, installedModuleIds, installedModuleVersions, bmadDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -617,11 +722,13 @@ class UI {
|
||||||
/**
|
/**
|
||||||
* Select all modules across three tiers: official, community, and custom URL.
|
* Select all modules across three tiers: official, community, and custom URL.
|
||||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||||
|
* @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
|
||||||
|
* @param {Object|null} channelOptions - Parsed installer channel options
|
||||||
* @returns {Array} Selected module codes (excluding core)
|
* @returns {Array} Selected module codes (excluding core)
|
||||||
*/
|
*/
|
||||||
async selectAllModules(installedModuleIds = new Set()) {
|
async selectAllModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
|
||||||
// Phase 1: Official modules
|
// Phase 1: Official modules
|
||||||
const officialSelected = await this._selectOfficialModules(installedModuleIds);
|
const officialSelected = await this._selectOfficialModules(installedModuleIds, installedModuleVersions, channelOptions);
|
||||||
|
|
||||||
// Determine which installed modules are NOT official (community or custom).
|
// Determine which installed modules are NOT official (community or custom).
|
||||||
// These must be preserved even if the user declines to browse community/custom.
|
// These must be preserved even if the user declines to browse community/custom.
|
||||||
|
|
@ -657,9 +764,11 @@ class UI {
|
||||||
* Select official modules using autocompleteMultiselect.
|
* Select official modules using autocompleteMultiselect.
|
||||||
* Extracted from the original selectAllModules - unchanged behavior.
|
* Extracted from the original selectAllModules - unchanged behavior.
|
||||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||||
|
* @param {Map<string, string>} installedModuleVersions - Installed module versions from the local manifest
|
||||||
|
* @param {Object|null} channelOptions - Parsed installer channel options
|
||||||
* @returns {Array} Selected official module codes
|
* @returns {Array} Selected official module codes
|
||||||
*/
|
*/
|
||||||
async _selectOfficialModules(installedModuleIds = new Set()) {
|
async _selectOfficialModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
|
||||||
// Built-in modules (core, bmm) come from local source, not the registry
|
// Built-in modules (core, bmm) come from local source, not the registry
|
||||||
const { OfficialModules } = require('./modules/official-modules');
|
const { OfficialModules } = require('./modules/official-modules');
|
||||||
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
||||||
|
|
@ -672,15 +781,18 @@ class UI {
|
||||||
const initialValues = [];
|
const initialValues = [];
|
||||||
const lockedValues = ['core'];
|
const lockedValues = ['core'];
|
||||||
|
|
||||||
const buildModuleEntry = async (code, name, description, isDefault) => {
|
const buildModuleEntry = async (code, name, description, isDefault, repoUrl = null, registryDefault = null) => {
|
||||||
const isInstalled = installedModuleIds.has(code);
|
const isInstalled = installedModuleIds.has(code);
|
||||||
const version = await getModuleVersion(code);
|
const installedVersion = installedModuleVersions.get(code) || '';
|
||||||
const label = version ? `${name} (v${version})` : name;
|
const versionState = await getModuleVersion(code, { repoUrl, registryDefault, channelOptions });
|
||||||
|
const label = buildModuleLabel(name, versionState.version, installedVersion);
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: code,
|
value: code,
|
||||||
hint: description,
|
hint: description,
|
||||||
selected: isInstalled || isDefault,
|
selected: isInstalled || isDefault,
|
||||||
|
lookupAttempted: versionState.lookupAttempted,
|
||||||
|
lookupSucceeded: versionState.lookupSucceeded,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -697,12 +809,38 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add external registry modules (skip built-in duplicates)
|
// Add external registry modules (skip built-in duplicates)
|
||||||
for (const mod of registryModules) {
|
const externalRegistryModules = registryModules.filter((mod) => !mod.builtIn && !builtInCodes.has(mod.code));
|
||||||
if (mod.builtIn || builtInCodes.has(mod.code)) continue;
|
let externalRegistryEntries = [];
|
||||||
const entry = await buildModuleEntry(mod.code, mod.name, mod.description, mod.defaultSelected);
|
if (externalRegistryModules.length > 0) {
|
||||||
|
const spinner = await prompts.spinner();
|
||||||
|
spinner.start('Checking latest module versions...');
|
||||||
|
|
||||||
|
externalRegistryEntries = await Promise.all(
|
||||||
|
externalRegistryModules.map(async (mod) => ({
|
||||||
|
code: mod.code,
|
||||||
|
entry: await buildModuleEntry(
|
||||||
|
mod.code,
|
||||||
|
mod.name,
|
||||||
|
mod.description,
|
||||||
|
mod.defaultSelected,
|
||||||
|
mod.url || null,
|
||||||
|
mod.defaultChannel || null,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
spinner.stop('Checked latest module versions.');
|
||||||
|
|
||||||
|
const attemptedLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupAttempted).length;
|
||||||
|
const successfulLookups = externalRegistryEntries.filter(({ entry }) => entry.lookupSucceeded).length;
|
||||||
|
if (attemptedLookups > 0 && successfulLookups === 0) {
|
||||||
|
await prompts.log.warn('Could not check latest module versions; showing cached/local versions.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const { code, entry } of externalRegistryEntries) {
|
||||||
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue