Compare commits
No commits in common. "7899a38a886307aff1f264a030011ba2ee603873" and "99b7940df74fce47c6b8bfe875b8ceedd1939850" have entirely different histories.
7899a38a88
...
99b7940df7
|
|
@ -1,6 +1,5 @@
|
||||||
# Fallback module registry — used only when the BMad Marketplace repo
|
# This file allows these modules under bmad-code-org to also be installed with the bmad method installer, while
|
||||||
# (bmad-code-org/bmad-plugins-marketplace) is unreachable.
|
# allowing us to keep the source of these projects in separate repos.
|
||||||
# The remote registry/official.yaml is the source of truth.
|
|
||||||
|
|
||||||
modules:
|
modules:
|
||||||
bmad-builder:
|
bmad-builder:
|
||||||
|
|
@ -42,3 +41,13 @@ modules:
|
||||||
defaultSelected: false
|
defaultSelected: false
|
||||||
type: bmad-org
|
type: bmad-org
|
||||||
npmPackage: bmad-method-test-architecture-enterprise
|
npmPackage: bmad-method-test-architecture-enterprise
|
||||||
|
|
||||||
|
whiteport-design-studio:
|
||||||
|
url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
||||||
|
module-definition: src/module.yaml
|
||||||
|
code: wds
|
||||||
|
name: "Whiteport Design Studio (For UX Professionals)"
|
||||||
|
description: "Whiteport Design Studio (For UX Professionals)"
|
||||||
|
defaultSelected: false
|
||||||
|
type: community
|
||||||
|
npmPackage: bmad-method-wds-expansion
|
||||||
|
|
@ -225,20 +225,13 @@ class ConfigDrivenIdeSetup {
|
||||||
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
||||||
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
||||||
if (this.installerConfig?.legacy_targets) {
|
if (this.installerConfig?.legacy_targets) {
|
||||||
const legacyDirsExist = await Promise.all(
|
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||||
this.installerConfig.legacy_targets.map((d) =>
|
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||||
this.isGlobalPath(d) ? fs.pathExists(d.replace(/^~/, os.homedir())) : fs.pathExists(path.join(projectDir, d)),
|
if (this.isGlobalPath(legacyDir)) {
|
||||||
),
|
await this.warnGlobalLegacy(legacyDir, options);
|
||||||
);
|
} else {
|
||||||
if (legacyDirsExist.some(Boolean)) {
|
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
||||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
await this.removeEmptyParents(projectDir, legacyDir);
|
||||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
|
||||||
if (this.isGlobalPath(legacyDir)) {
|
|
||||||
await this.warnGlobalLegacy(legacyDir, options);
|
|
||||||
} else {
|
|
||||||
await this.cleanupTarget(projectDir, legacyDir, options, null);
|
|
||||||
await this.removeEmptyParents(projectDir, legacyDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,67 @@
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const https = require('node:https');
|
|
||||||
const { execSync } = require('node:child_process');
|
const { execSync } = require('node:child_process');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
|
|
||||||
const REGISTRY_RAW_URL = 'https://raw.githubusercontent.com/bmad-code-org/bmad-plugins-marketplace/main/registry/official.yaml';
|
|
||||||
const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages official modules from the remote BMad marketplace registry.
|
* Manages external official modules defined in external-official-modules.yaml
|
||||||
* Fetches registry/official.yaml from GitHub; falls back to the bundled
|
* These are modules hosted in external repositories that can be installed
|
||||||
* external-official-modules.yaml when the network is unavailable.
|
|
||||||
*
|
*
|
||||||
* @class ExternalModuleManager
|
* @class ExternalModuleManager
|
||||||
*/
|
*/
|
||||||
class ExternalModuleManager {
|
class ExternalModuleManager {
|
||||||
constructor() {}
|
constructor() {
|
||||||
|
this.externalModulesConfigPath = path.join(__dirname, '../external-official-modules.yaml');
|
||||||
/**
|
this.cachedModules = null;
|
||||||
* Fetch a URL and return the response body as a string.
|
|
||||||
* @param {string} url - URL to fetch
|
|
||||||
* @param {number} timeout - Timeout in ms (default 10s)
|
|
||||||
* @returns {Promise<string>} Response body
|
|
||||||
*/
|
|
||||||
_fetch(url, timeout = 10_000) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const req = https
|
|
||||||
.get(url, { timeout }, (res) => {
|
|
||||||
// Follow one redirect (GitHub sometimes 301s)
|
|
||||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
||||||
return this._fetch(res.headers.location, timeout).then(resolve, reject);
|
|
||||||
}
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => (data += chunk));
|
|
||||||
res.on('end', () => resolve(data));
|
|
||||||
})
|
|
||||||
.on('error', reject)
|
|
||||||
.on('timeout', () => {
|
|
||||||
req.destroy();
|
|
||||||
reject(new Error('Request timed out'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the official modules registry from GitHub, falling back to the
|
* Load and parse the external-official-modules.yaml file
|
||||||
* bundled YAML file if the fetch fails.
|
* @returns {Object} Parsed YAML content with modules object
|
||||||
* @returns {Object} Parsed YAML content with modules array
|
|
||||||
*/
|
*/
|
||||||
async loadExternalModulesConfig() {
|
async loadExternalModulesConfig() {
|
||||||
if (this.cachedModules) {
|
if (this.cachedModules) {
|
||||||
return this.cachedModules;
|
return this.cachedModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try remote registry first
|
|
||||||
try {
|
try {
|
||||||
const content = await this._fetch(REGISTRY_RAW_URL);
|
const content = await fs.readFile(this.externalModulesConfigPath, 'utf8');
|
||||||
const config = yaml.parse(content);
|
|
||||||
if (config?.modules?.length) {
|
|
||||||
this.cachedModules = config;
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Fall through to local fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to bundled file
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
|
|
||||||
const config = yaml.parse(content);
|
const config = yaml.parse(content);
|
||||||
this.cachedModules = config;
|
this.cachedModules = config;
|
||||||
await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await prompts.log.warn(`Failed to load modules config: ${error.message}`);
|
await prompts.log.warn(`Failed to load external modules config: ${error.message}`);
|
||||||
return { modules: [] };
|
return { modules: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a module entry from either the remote registry format
|
* Get list of available external modules
|
||||||
* (snake_case, array) or the legacy bundled format (kebab-case, object map).
|
|
||||||
* @param {Object} mod - Raw module config from YAML
|
|
||||||
* @param {string} [key] - Key name (only for legacy map format)
|
|
||||||
* @returns {Object} Normalized module info
|
|
||||||
*/
|
|
||||||
_normalizeModule(mod, key) {
|
|
||||||
return {
|
|
||||||
key: key || mod.name,
|
|
||||||
url: mod.repository || mod.url,
|
|
||||||
moduleDefinition: mod.module_definition || mod['module-definition'],
|
|
||||||
code: mod.code,
|
|
||||||
name: mod.display_name || mod.name,
|
|
||||||
description: mod.description || '',
|
|
||||||
defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
|
|
||||||
type: mod.type || 'bmad-org',
|
|
||||||
npmPackage: mod.npm_package || mod.npmPackage || null,
|
|
||||||
builtIn: mod.built_in === true,
|
|
||||||
isExternal: mod.built_in !== true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of available modules from the registry
|
|
||||||
* @returns {Array<Object>} Array of module info objects
|
* @returns {Array<Object>} Array of module info objects
|
||||||
*/
|
*/
|
||||||
async listAvailable() {
|
async listAvailable() {
|
||||||
const config = await this.loadExternalModulesConfig();
|
const config = await this.loadExternalModulesConfig();
|
||||||
|
|
||||||
// Remote format: modules is an array
|
|
||||||
if (Array.isArray(config.modules)) {
|
|
||||||
return config.modules.map((mod) => this._normalizeModule(mod));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy bundled format: modules is an object map
|
|
||||||
const modules = [];
|
const modules = [];
|
||||||
for (const [key, mod] of Object.entries(config.modules || {})) {
|
|
||||||
modules.push(this._normalizeModule(mod, key));
|
for (const [key, moduleConfig] of Object.entries(config.modules || {})) {
|
||||||
|
modules.push({
|
||||||
|
key,
|
||||||
|
url: moduleConfig.url,
|
||||||
|
moduleDefinition: moduleConfig['module-definition'],
|
||||||
|
code: moduleConfig.code,
|
||||||
|
name: moduleConfig.name,
|
||||||
|
header: moduleConfig.header,
|
||||||
|
subheader: moduleConfig.subheader,
|
||||||
|
description: moduleConfig.description || '',
|
||||||
|
defaultSelected: moduleConfig.defaultSelected === true,
|
||||||
|
type: moduleConfig.type || 'community', // bmad-org or community
|
||||||
|
npmPackage: moduleConfig.npmPackage || null, // Include npm package name
|
||||||
|
isExternal: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,8 +81,27 @@ class ExternalModuleManager {
|
||||||
* @returns {Object|null} Module info or null if not found
|
* @returns {Object|null} Module info or null if not found
|
||||||
*/
|
*/
|
||||||
async getModuleByKey(key) {
|
async getModuleByKey(key) {
|
||||||
const modules = await this.listAvailable();
|
const config = await this.loadExternalModulesConfig();
|
||||||
return modules.find((m) => m.key === key) || null;
|
const moduleConfig = config.modules?.[key];
|
||||||
|
|
||||||
|
if (!moduleConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
url: moduleConfig.url,
|
||||||
|
moduleDefinition: moduleConfig['module-definition'],
|
||||||
|
code: moduleConfig.code,
|
||||||
|
name: moduleConfig.name,
|
||||||
|
header: moduleConfig.header,
|
||||||
|
subheader: moduleConfig.subheader,
|
||||||
|
description: moduleConfig.description || '',
|
||||||
|
defaultSelected: moduleConfig.defaultSelected === true,
|
||||||
|
type: moduleConfig.type || 'community', // bmad-org or community
|
||||||
|
npmPackage: moduleConfig.npmPackage || null, // Include npm package name
|
||||||
|
isExternal: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -196,7 +154,7 @@ class ExternalModuleManager {
|
||||||
const moduleInfo = await this.getModuleByCode(moduleCode);
|
const moduleInfo = await this.getModuleByCode(moduleCode);
|
||||||
|
|
||||||
if (!moduleInfo) {
|
if (!moduleInfo) {
|
||||||
throw new Error(`External module '${moduleCode}' not found in the BMad registry`);
|
throw new Error(`External module '${moduleCode}' not found in external-official-modules.yaml`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheDir = this.getExternalCacheDir();
|
const cacheDir = this.getExternalCacheDir();
|
||||||
|
|
@ -346,7 +304,7 @@ class ExternalModuleManager {
|
||||||
async findExternalModuleSource(moduleCode, options = {}) {
|
async findExternalModuleSource(moduleCode, options = {}) {
|
||||||
const moduleInfo = await this.getModuleByCode(moduleCode);
|
const moduleInfo = await this.getModuleByCode(moduleCode);
|
||||||
|
|
||||||
if (!moduleInfo || moduleInfo.builtIn) {
|
if (!moduleInfo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +349,6 @@ class ExternalModuleManager {
|
||||||
// Nothing found: return configured path (preserves old behavior for error messaging)
|
// Nothing found: return configured path (preserves old behavior for error messaging)
|
||||||
return path.dirname(configuredPath);
|
return path.dirname(configuredPath);
|
||||||
}
|
}
|
||||||
cachedModules = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { ExternalModuleManager };
|
module.exports = { ExternalModuleManager };
|
||||||
|
|
|
||||||
|
|
@ -569,36 +569,77 @@ class UI {
|
||||||
* @returns {Array} Selected module codes (excluding core)
|
* @returns {Array} Selected module codes (excluding core)
|
||||||
*/
|
*/
|
||||||
async selectAllModules(installedModuleIds = new Set()) {
|
async selectAllModules(installedModuleIds = new Set()) {
|
||||||
// Registry is the single source of truth for the module list
|
const { OfficialModules } = require('./modules/official-modules');
|
||||||
|
const officialModulesSource = new OfficialModules();
|
||||||
|
const { modules: localModules } = await officialModulesSource.listAvailable();
|
||||||
|
|
||||||
|
// Get external modules
|
||||||
const externalManager = new ExternalModuleManager();
|
const externalManager = new ExternalModuleManager();
|
||||||
const registryModules = await externalManager.listAvailable();
|
const externalModules = await externalManager.listAvailable();
|
||||||
|
|
||||||
// Build flat options list with group hints for autocompleteMultiselect
|
// Build flat options list with group hints for autocompleteMultiselect
|
||||||
const allOptions = [];
|
const allOptions = [];
|
||||||
const initialValues = [];
|
const initialValues = [];
|
||||||
const lockedValues = ['core'];
|
const lockedValues = ['core'];
|
||||||
|
|
||||||
|
// Core module is always installed — show it locked at the top
|
||||||
|
const coreVersion = await getMarketplaceVersion('core');
|
||||||
|
const coreLabel = coreVersion ? `BMad Core Module (v${coreVersion})` : 'BMad Core Module';
|
||||||
|
allOptions.push({ label: coreLabel, value: 'core', hint: 'Core configuration and shared resources' });
|
||||||
|
initialValues.push('core');
|
||||||
|
|
||||||
// Helper to build module entry with proper sorting and selection
|
// Helper to build module entry with proper sorting and selection
|
||||||
const buildModuleEntry = async (mod) => {
|
const buildModuleEntry = async (mod, value, group) => {
|
||||||
const isInstalled = installedModuleIds.has(mod.code);
|
const isInstalled = installedModuleIds.has(value);
|
||||||
const version = await getMarketplaceVersion(mod.code);
|
const version = await getMarketplaceVersion(value);
|
||||||
const label = version ? `${mod.name} (v${version})` : mod.name;
|
const label = version ? `${mod.name} (v${version})` : mod.name;
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: mod.code,
|
value,
|
||||||
hint: mod.description,
|
hint: mod.description || group,
|
||||||
|
// Pre-select only if already installed (not on fresh install)
|
||||||
selected: isInstalled,
|
selected: isInstalled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Registry order is display order; core is always locked
|
// Local modules (BMM, BMB, etc.)
|
||||||
for (const mod of registryModules) {
|
const localEntries = [];
|
||||||
const entry = await buildModuleEntry(mod);
|
for (const mod of localModules) {
|
||||||
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
if (mod.id !== 'core') {
|
||||||
if (entry.selected) {
|
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
||||||
initialValues.push(mod.code);
|
localEntries.push(entry);
|
||||||
|
if (entry.selected) {
|
||||||
|
initialValues.push(mod.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint })));
|
||||||
|
|
||||||
|
// Group 2: BMad Official Modules (type: bmad-org)
|
||||||
|
const officialModules = [];
|
||||||
|
for (const mod of externalModules) {
|
||||||
|
if (mod.type === 'bmad-org') {
|
||||||
|
const entry = await buildModuleEntry(mod, mod.code, 'Official');
|
||||||
|
officialModules.push(entry);
|
||||||
|
if (entry.selected) {
|
||||||
|
initialValues.push(mod.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
||||||
|
|
||||||
|
// Group 3: Community Modules (type: community)
|
||||||
|
const communityModules = [];
|
||||||
|
for (const mod of externalModules) {
|
||||||
|
if (mod.type === 'community') {
|
||||||
|
const entry = await buildModuleEntry(mod, mod.code, 'Community');
|
||||||
|
communityModules.push(entry);
|
||||||
|
if (entry.selected) {
|
||||||
|
initialValues.push(mod.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })));
|
||||||
|
|
||||||
const selected = await prompts.autocompleteMultiselect({
|
const selected = await prompts.autocompleteMultiselect({
|
||||||
message: 'Select modules to install:',
|
message: 'Select modules to install:',
|
||||||
|
|
@ -629,14 +670,16 @@ class UI {
|
||||||
* @returns {Array} Default module codes
|
* @returns {Array} Default module codes
|
||||||
*/
|
*/
|
||||||
async getDefaultModules(installedModuleIds = new Set()) {
|
async getDefaultModules(installedModuleIds = new Set()) {
|
||||||
const externalManager = new ExternalModuleManager();
|
const { OfficialModules } = require('./modules/official-modules');
|
||||||
const registryModules = await externalManager.listAvailable();
|
const officialModules = new OfficialModules();
|
||||||
|
const { modules: localModules } = await officialModules.listAvailable();
|
||||||
|
|
||||||
const defaultModules = [];
|
const defaultModules = [];
|
||||||
|
|
||||||
for (const mod of registryModules) {
|
// Add default-selected local modules (typically BMM)
|
||||||
if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
|
for (const mod of localModules) {
|
||||||
defaultModules.push(mod.code);
|
if (mod.defaultSelected === true || installedModuleIds.has(mod.id)) {
|
||||||
|
defaultModules.push(mod.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue