Merge branch 'main' into feat/uniform-customize-toml-all-workflows
This commit is contained in:
commit
5b1f563491
|
|
@ -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('');
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
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 crypto = require('node:crypto');
|
||||
const { resolveModuleVersion } = require('../modules/version-resolver');
|
||||
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 {
|
||||
/**
|
||||
* Create a new manifest
|
||||
|
|
@ -362,23 +373,22 @@ class Manifest {
|
|||
* @returns {string|null} Latest version or null
|
||||
*/
|
||||
async fetchNpmVersion(packageName) {
|
||||
try {
|
||||
const https = require('node:https');
|
||||
const { execSync } = require('node:child_process');
|
||||
if (!isValidNpmPackageName(packageName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try using npm view first (more reliable)
|
||||
try {
|
||||
const result = execSync(`npm view ${packageName} version`, {
|
||||
const { stdout } = await execFileAsync('npm', ['view', packageName, 'version'], {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
timeout: 10_000,
|
||||
timeout: NPM_LOOKUP_TIMEOUT_MS,
|
||||
});
|
||||
return result.trim();
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
// Fallback to npm registry API
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
.get(`https://registry.npmjs.org/${packageName}`, (res) => {
|
||||
return new Promise((resolve) => {
|
||||
const request = https.get(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
|
|
@ -389,8 +399,14 @@ class Manifest {
|
|||
resolve(null);
|
||||
}
|
||||
});
|
||||
})
|
||||
.on('error', () => resolve(null));
|
||||
});
|
||||
|
||||
request.setTimeout(NPM_LOOKUP_TIMEOUT_MS, () => {
|
||||
request.destroy();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
request.on('error', () => resolve(null));
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,107 @@
|
|||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const semver = require('semver');
|
||||
const fs = require('./fs-native');
|
||||
const { CLIUtils } = require('./cli-utils');
|
||||
const { ExternalModuleManager } = require('./modules/external-manager');
|
||||
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 manifest = new Manifest();
|
||||
|
||||
/**
|
||||
* Read a module version from the freshest local metadata available.
|
||||
* @param {string} moduleCode - Module code (e.g., 'core', 'bmm', 'cis')
|
||||
* @returns {string} Version string or empty string
|
||||
* 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}
|
||||
*/
|
||||
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);
|
||||
return versionInfo.version || '';
|
||||
return {
|
||||
version: versionInfo.version || '',
|
||||
lookupAttempted: !!repoUrl,
|
||||
lookupSucceeded: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -122,7 +209,7 @@ class UI {
|
|||
// Return early with modify configuration
|
||||
if (actionType === 'update') {
|
||||
// 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(', ')}`);
|
||||
|
||||
|
|
@ -144,7 +231,7 @@ class UI {
|
|||
`Non-interactive mode (--yes): using default modules (installed + defaults): ${selectedModules.join(', ')}`,
|
||||
);
|
||||
} else {
|
||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
|
|
@ -208,7 +295,7 @@ class UI {
|
|||
}
|
||||
|
||||
// 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
|
||||
let selectedModules;
|
||||
|
|
@ -227,7 +314,7 @@ class UI {
|
|||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`);
|
||||
} else {
|
||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
|
|
@ -526,7 +613,7 @@ class UI {
|
|||
/**
|
||||
* Get existing installation info and installed modules
|
||||
* @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) {
|
||||
const { ExistingInstall } = require('./core/existing-install');
|
||||
|
|
@ -535,8 +622,26 @@ class UI {
|
|||
const { bmadDir } = await installer.findBmadDir(directory);
|
||||
const existingInstall = await ExistingInstall.detect(bmadDir);
|
||||
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.
|
||||
* @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)
|
||||
*/
|
||||
async selectAllModules(installedModuleIds = new Set()) {
|
||||
async selectAllModules(installedModuleIds = new Set(), installedModuleVersions = new Map(), channelOptions = null) {
|
||||
// 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).
|
||||
// These must be preserved even if the user declines to browse community/custom.
|
||||
|
|
@ -657,9 +764,11 @@ class UI {
|
|||
* Select official modules using autocompleteMultiselect.
|
||||
* Extracted from the original selectAllModules - unchanged behavior.
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
const { OfficialModules } = require('./modules/official-modules');
|
||||
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
||||
|
|
@ -672,15 +781,18 @@ class UI {
|
|||
const initialValues = [];
|
||||
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 version = await getModuleVersion(code);
|
||||
const label = version ? `${name} (v${version})` : name;
|
||||
const installedVersion = installedModuleVersions.get(code) || '';
|
||||
const versionState = await getModuleVersion(code, { repoUrl, registryDefault, channelOptions });
|
||||
const label = buildModuleLabel(name, versionState.version, installedVersion);
|
||||
return {
|
||||
label,
|
||||
value: code,
|
||||
hint: description,
|
||||
selected: isInstalled || isDefault,
|
||||
lookupAttempted: versionState.lookupAttempted,
|
||||
lookupSucceeded: versionState.lookupSucceeded,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -697,12 +809,38 @@ class UI {
|
|||
}
|
||||
|
||||
// Add external registry modules (skip built-in duplicates)
|
||||
for (const mod of registryModules) {
|
||||
if (mod.builtIn || builtInCodes.has(mod.code)) continue;
|
||||
const entry = await buildModuleEntry(mod.code, mod.name, mod.description, mod.defaultSelected);
|
||||
const externalRegistryModules = registryModules.filter((mod) => !mod.builtIn && !builtInCodes.has(mod.code));
|
||||
let externalRegistryEntries = [];
|
||||
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 });
|
||||
if (entry.selected) {
|
||||
initialValues.push(mod.code);
|
||||
initialValues.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue