installation for remote modules now indicates its getching or installing so it does not appear to be hung when caching the remote in the local npm cache

This commit is contained in:
Brian Madison
2026-01-18 08:11:35 -06:00
parent 966ca5db0b
commit 28e6dded4d
6 changed files with 152 additions and 122 deletions

View File

@@ -6,28 +6,25 @@ modules:
url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
module-definition: src/module.yaml
code: cis
name: "CIS: Creative Innovation Suite"
header: "CIS: Creative Innovation Suite"
subheader: "Unleash your creativity with the BMad CIS!"
description: ""
name: "Creative Innovation Suite"
description: "Creative tools for writing, brainstorming, and more"
defaultSelected: false
type: bmad-org
bmad-game-dev-studio:
url: https://github.com/bmad-code-org/bmad-module-game-dev-studio.git
module-definition: src/module.yaml
code: BGDS
name: "BGDS: BMad Game Dev Suite"
header: "BGDS: BMad Game Dev Suite"
subheader: "Explore and create the groundwork for your game ideas with the BMad Game Dev suite!"
description: "Very similar to the BMad Method - but a focus on the slightly different needs of Game Development - with multiple platforms and game type specifics included to explore"
name: "BMad Game Dev Suite"
description: "Game development agents and workflows"
defaultSelected: false
type: bmad-org
# bmad-whiteport-design-system:
# url: https://github.com/bmad-code-org/bmad-method-wds-expansion
# module-definition: src/module.yaml
# code: WDS
# name: "WDS: Whiteport UX Design System"
# header: "WDS: Whiteport UX Design System"
# subheader: "Professional Designer UX Design Module Expansion to the BMad MEthod"
# description: "Experienced UX Designers can leverage the WDS with or without the BMad Method to harness their existing skills and tools (such as figma) while also utilizing an industry leading design framework."
# defaultSelected: false
whiteport-design-system:
url: https://github.com/bmad-code-org/bmad-method-wds-expansion
module-definition: src/module.yaml
code: WDS
name: "Whiteport UX Design System"
description: "UX design framework with Figma integration"
defaultSelected: false
type: community

View File

@@ -53,6 +53,7 @@ class ExternalModuleManager {
subheader: moduleConfig.subheader,
description: moduleConfig.description || '',
defaultSelected: moduleConfig.defaultSelected === true,
type: moduleConfig.type || 'community', // bmad-org or community
isExternal: true,
});
}
@@ -93,6 +94,7 @@ class ExternalModuleManager {
subheader: moduleConfig.subheader,
description: moduleConfig.description || '',
defaultSelected: moduleConfig.defaultSelected === true,
type: moduleConfig.type || 'community', // bmad-org or community
isExternal: true,
};
}

View File

@@ -422,7 +422,7 @@ class ModuleManager {
// Check if already cloned
if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo
const updateSpinner = ora(`Updating ${moduleInfo.name} from remote repository...`).start();
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
execSync('git fetch --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
@@ -430,15 +430,13 @@ class ModuleManager {
execSync('git pull --ff-only', { cwd: moduleCacheDir, stdio: 'pipe' });
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
if (currentRef === newRef) {
updateSpinner.succeed(`${moduleInfo.name} is already up to date`);
} else {
updateSpinner.succeed(`Updated ${moduleInfo.name} to latest version`);
// Force dependency install since we got new code
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code
if (currentRef !== newRef) {
needsDependencyInstall = true;
}
} catch {
updateSpinner.warn(`Update failed, re-downloading ${moduleInfo.name}`);
fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone
await fs.remove(moduleCacheDir);
wasNewClone = true;
@@ -449,14 +447,14 @@ class ModuleManager {
// Clone if not exists or was removed
if (wasNewClone) {
const cloneSpinner = ora(`Downloading ${moduleInfo.name} from remote repository...`).start();
const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: 'pipe',
});
cloneSpinner.succeed(`Downloaded ${moduleInfo.name}`);
fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
} catch (error) {
cloneSpinner.fail(`Failed to download ${moduleInfo.name}`);
fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
}
}

View File

@@ -253,51 +253,8 @@ class UI {
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
// Ask about BMad Method Module (bmm)
const wantsBmm = await prompts.confirm({
message:
'Select the BMad Method Module for installation?\n ---> This is the Full BMad Method Agile AI Driven Development Framework Including BMad Quick Flow',
default: installedModuleIds.has('bmm'),
});
// Ask about BMad Builder Module (bmb)
const wantsBmb = await prompts.confirm({
message: 'Select the BMad Builder Module for installation?\n ---> Create Your Own Custom BMad Agents, Workflows and Modules',
default: installedModuleIds.has('bmb'),
});
let selectedOfficialModules = [];
if (wantsBmm) {
selectedOfficialModules.push('bmm');
}
if (wantsBmb) {
selectedOfficialModules.push('bmb');
}
// Ask about other external modules
// Check if any external modules are already installed (not bmm, bmb, or core)
const installedExternalModules = [...installedModuleIds].filter((id) => !['bmm', 'bmb', 'core'].includes(id));
let selectedExternalModules = [];
// If external modules are already installed, skip confirm and go straight to selection
// Otherwise ask if they want to choose external modules
if (installedExternalModules.length > 0) {
const externalModuleChoices = await this.getExternalModuleChoices();
selectedExternalModules = await this.selectExternalModules(externalModuleChoices, installedExternalModules);
} else {
const wantsExternalModules = await prompts.confirm({
message: 'Would you like to choose any other Recommended BMad Core Modules for installation?',
default: false,
});
if (wantsExternalModules) {
const externalModuleChoices = await this.getExternalModuleChoices();
selectedExternalModules = await this.selectExternalModules(externalModuleChoices, []);
}
}
// Combine official and external modules
let selectedModules = [...selectedOfficialModules, ...selectedExternalModules];
// Unified module selection - all modules in one grouped multiselect
let selectedModules = await this.selectAllModules(installedModuleIds);
// After module selection, ask about custom modules
console.log('');
@@ -352,36 +309,10 @@ class UI {
// This section is only for new installations (update returns early above)
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Ask about BMad Method Module (this repo)
const wantsBmm = await prompts.confirm({
message:
'Select the BMad Method Module for installation?\n ---> This is the Full BMad Method Agile AI Driven Development Framework Including BMad Quick Flow',
default: true,
});
// Unified module selection - all modules in one grouped multiselect
let selectedModules = await this.selectAllModules(installedModuleIds);
// Ask about BMad Builder Module
const wantsBmg = await prompts.confirm({
message: 'Select the BMad Builder Module for installation?\n ---> Create Your Own Custom BMad Agents, Workflows and Modules',
default: false,
});
let selectedOfficialModules = [];
if (wantsBmm) {
selectedOfficialModules.push('bmm');
}
const wantsExternalModules = await prompts.confirm({
message: 'Would you like to choose any other Recommended BMad Core Modules for installation?\n',
default: true,
});
let selectedExternalModules = [];
if (wantsExternalModules) {
const externalModuleChoices = await this.getExternalModuleChoices();
selectedExternalModules = await this.selectExternalModules(externalModuleChoices);
}
// Ask about custom content
// Ask about custom content (local modules/agents/workflows)
const wantsCustomContent = await prompts.confirm({
message: 'Would you like to install a locally stored custom module (this includes custom agents and workflows also)?',
default: false,
@@ -391,19 +322,9 @@ class UI {
customContentConfig = await this.promptCustomContentSource();
}
// Store the selected modules for later
customContentConfig._selectedOfficialModules = selectedOfficialModules;
customContentConfig._selectedExternalModules = selectedExternalModules;
// Build the final list of selected modules
let selectedModules = [
...(customContentConfig._selectedOfficialModules || []),
...(customContentConfig._selectedExternalModules || []),
];
// Add custom content modules if any were selected
if (customContentConfig && customContentConfig.selectedModuleIds) {
selectedModules = [...selectedModules, ...customContentConfig.selectedModuleIds];
selectedModules.push(...customContentConfig.selectedModuleIds);
}
selectedModules = selectedModules.filter((m) => m !== 'core');
@@ -513,7 +434,7 @@ class UI {
let selectedIdes = [];
selectedIdes = await prompts.groupMultiselect({
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
message: `Select tools to configure ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: true,
@@ -663,6 +584,7 @@ class UI {
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates
hint: customInfo.description || undefined,
});
}
}
@@ -689,6 +611,7 @@ class UI {
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
});
}
}
@@ -710,6 +633,7 @@ class UI {
name: mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
});
}
}
@@ -741,7 +665,7 @@ class UI {
];
const selected = await prompts.multiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkipOption,
required: true,
});
@@ -770,6 +694,7 @@ class UI {
name: mod.name,
value: mod.code, // Use the code (e.g., 'cis') as the value
checked: mod.defaultSelected || false,
hint: mod.description || undefined, // Show description as hint
module: mod, // Store full module info for later use
}));
}
@@ -783,7 +708,7 @@ class UI {
async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules
const availableNames = externalModuleChoices.map((c) => c.name).join(', ');
const message = `Select official BMad modules to install ${availableNames ? chalk.dim(`(${availableNames})`) : ''} ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`;
const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`;
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({
@@ -819,6 +744,116 @@ class UI {
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Select all modules (core + official + community) using grouped multiselect
* @param {Set} installedModuleIds - Currently installed module IDs
* @returns {Array} Selected module codes
*/
async selectAllModules(installedModuleIds = new Set()) {
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const { modules: localModules } = await moduleManager.listAvailable();
// Get external modules
const externalManager = new ExternalModuleManager();
const externalModules = await externalManager.listAvailable();
// Build grouped options
const groupedOptions = {};
const initialValues = [];
// Helper to build module entry with proper sorting and selection
const buildModuleEntry = (mod, value) => {
const isInstalled = installedModuleIds.has(value);
const isDefault = mod.defaultSelected === true;
return {
label: mod.description ? `${mod.name}${mod.description}` : mod.name,
value,
// For sorting: defaultSelected=0, others=1
sortKey: isDefault ? 0 : 1,
// Pre-select if default selected OR already installed
selected: isDefault || isInstalled,
};
};
// Group 1: BMad Core (BMM, BMB)
const coreModules = [];
for (const mod of localModules) {
if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) {
const entry = buildModuleEntry(mod, mod.id);
coreModules.push(entry);
if (entry.selected) {
initialValues.push(mod.id);
}
}
}
// Sort: defaultSelected first, then others
coreModules.sort((a, b) => a.sortKey - b.sortKey);
// Remove sortKey from final entries
if (coreModules.length > 0) {
groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value }));
}
// Group 2: BMad Official Modules (type: bmad-org)
const officialModules = [];
for (const mod of externalModules) {
if (mod.type === 'bmad-org') {
const entry = buildModuleEntry(mod, mod.code);
officialModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
officialModules.sort((a, b) => a.sortKey - b.sortKey);
if (officialModules.length > 0) {
groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value }));
}
// Group 3: Community Modules (type: community)
const communityModules = [];
for (const mod of externalModules) {
if (mod.type === 'community') {
const entry = buildModuleEntry(mod, mod.code);
communityModules.push(entry);
if (entry.selected) {
initialValues.push(mod.code);
}
}
}
communityModules.sort((a, b) => a.sortKey - b.sortKey);
if (communityModules.length > 0) {
groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value }));
}
// Add "None" option at the end
groupedOptions[' '] = [
{
label: '⚠ None - Skip module installation',
value: '__NONE__',
},
];
const selected = await prompts.groupMultiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: true,
selectableGroups: false,
});
// If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) {
console.log();
console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.'));
console.log();
return [];
}
// Filter out the special '__NONE__' value
return selected ? selected.filter((m) => m !== '__NONE__') : [];
}
/**
* Prompt for directory selection
* @returns {Object} Directory answer from prompt
@@ -1372,7 +1407,7 @@ class UI {
];
const keepModules = await prompts.multiselect({
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkip,
required: true,
});