feat(installer): add --custom-source CLI flag for non-interactive installs
Allows installing custom modules from Git URLs or local paths directly from the command line without interactive prompts: npx bmad-method install --custom-source /path/to/module npx bmad-method install --custom-source https://gitlab.com/org/repo npx bmad-method install --custom-source /path/one,https://host/org/repo Works alongside --modules and --yes flags. All discovered modules from each source are auto-selected.
This commit is contained in:
parent
225e5ee77b
commit
9ff131ac15
|
|
@ -22,6 +22,7 @@ module.exports = {
|
|||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||
['--document-output-language <lang>', 'Language for document output (default: English)'],
|
||||
['--output-folder <path>', 'Output folder path relative to project root (default: _bmad-output)'],
|
||||
['--custom-source <sources>', 'Comma-separated Git URLs or local paths to install custom modules from'],
|
||||
['-y, --yes', 'Accept all defaults and skip prompts where possible'],
|
||||
],
|
||||
action: async (options) => {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,14 @@ class UI {
|
|||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
if (options.customSource) {
|
||||
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
||||
for (const code of customCodes) {
|
||||
if (!selectedModules.includes(code)) selectedModules.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure core is in the modules list
|
||||
if (!selectedModules.includes('core')) {
|
||||
selectedModules.unshift('core');
|
||||
|
|
@ -210,6 +218,14 @@ class UI {
|
|||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
if (options.customSource) {
|
||||
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
||||
for (const code of customCodes) {
|
||||
if (!selectedModules.includes(code)) selectedModules.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure core is in the modules list
|
||||
if (!selectedModules.includes('core')) {
|
||||
selectedModules.unshift('core');
|
||||
|
|
@ -998,6 +1014,102 @@ class UI {
|
|||
return selectedModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve custom sources from --custom-source CLI flag (non-interactive).
|
||||
* Auto-selects all discovered modules from each source.
|
||||
* @param {string} sourcesArg - Comma-separated Git URLs or local paths
|
||||
* @returns {Array} Module codes from all resolved sources
|
||||
*/
|
||||
async _resolveCustomSourcesCli(sourcesArg) {
|
||||
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const allCodes = [];
|
||||
|
||||
const sources = sourcesArg
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const source of sources) {
|
||||
const s = await prompts.spinner();
|
||||
s.start(`Resolving ${source}...`);
|
||||
|
||||
let sourceResult;
|
||||
try {
|
||||
sourceResult = await customMgr.resolveSource(source, { skipInstall: true, silent: true });
|
||||
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
||||
} catch (error) {
|
||||
s.error(`Failed to resolve ${source}`);
|
||||
await prompts.log.error(` ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const s2 = await prompts.spinner();
|
||||
s2.start('Analyzing plugin structure...');
|
||||
const allResolved = [];
|
||||
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
||||
|
||||
if (sourceResult.mode === 'discovery') {
|
||||
try {
|
||||
const plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
||||
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
||||
if (resolved.length > 0) {
|
||||
allResolved.push(...resolved);
|
||||
}
|
||||
} catch {
|
||||
// Skip unresolvable plugins
|
||||
}
|
||||
}
|
||||
} catch (discoverError) {
|
||||
s2.error('Failed to discover modules');
|
||||
await prompts.log.error(` ${discoverError.message}`);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Direct mode: scan for SKILL.md directories
|
||||
const directPlugin = {
|
||||
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
||||
source: '.',
|
||||
skills: [],
|
||||
};
|
||||
try {
|
||||
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
||||
if (await fs.pathExists(skillMd)) {
|
||||
directPlugin.skills.push(entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip unreadable directories
|
||||
}
|
||||
|
||||
if (directPlugin.skills.length > 0) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
||||
allResolved.push(...resolved);
|
||||
} catch {
|
||||
// Skip unresolvable
|
||||
}
|
||||
}
|
||||
}
|
||||
s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`);
|
||||
|
||||
for (const mod of allResolved) {
|
||||
allCodes.push(mod.code);
|
||||
const versionStr = mod.version ? ` v${mod.version}` : '';
|
||||
await prompts.log.info(` Custom module: ${mod.name}${versionStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
return allCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default modules for non-interactive mode
|
||||
* @param {Set} installedModuleIds - Already installed module IDs
|
||||
|
|
|
|||
Loading…
Reference in New Issue