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)'],
|
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||||
['--document-output-language <lang>', 'Language for document output (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)'],
|
['--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'],
|
['-y, --yes', 'Accept all defaults and skip prompts where possible'],
|
||||||
],
|
],
|
||||||
action: async (options) => {
|
action: async (options) => {
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,14 @@ class UI {
|
||||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
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
|
// Ensure core is in the modules list
|
||||||
if (!selectedModules.includes('core')) {
|
if (!selectedModules.includes('core')) {
|
||||||
selectedModules.unshift('core');
|
selectedModules.unshift('core');
|
||||||
|
|
@ -210,6 +218,14 @@ class UI {
|
||||||
selectedModules = await this.selectAllModules(installedModuleIds);
|
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
|
// Ensure core is in the modules list
|
||||||
if (!selectedModules.includes('core')) {
|
if (!selectedModules.includes('core')) {
|
||||||
selectedModules.unshift('core');
|
selectedModules.unshift('core');
|
||||||
|
|
@ -998,6 +1014,102 @@ class UI {
|
||||||
return selectedModules;
|
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
|
* Get default modules for non-interactive mode
|
||||||
* @param {Set} installedModuleIds - Already installed module IDs
|
* @param {Set} installedModuleIds - Already installed module IDs
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue