bmad-method 6.2.3-next.30 → 6.2.3-next.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/compile-epic-context.md +62 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +26 -13
- package/tools/installer/commands/install.js +1 -0
- package/tools/installer/core/installer.js +8 -3
- package/tools/installer/core/manifest-generator.js +4 -2
- package/tools/installer/core/manifest.js +17 -10
- package/tools/installer/modules/custom-module-manager.js +430 -94
- package/tools/installer/modules/official-modules.js +80 -0
- package/tools/installer/modules/plugin-resolver.js +398 -0
- package/tools/installer/ui.js +248 -33
package/tools/installer/ui.js
CHANGED
|
@@ -158,6 +158,9 @@ class UI {
|
|
|
158
158
|
.map((m) => m.trim())
|
|
159
159
|
.filter(Boolean);
|
|
160
160
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
|
161
|
+
} else if (options.customSource) {
|
|
162
|
+
// Custom source without --modules: start with empty list (core added below)
|
|
163
|
+
selectedModules = [];
|
|
161
164
|
} else if (options.yes) {
|
|
162
165
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
|
163
166
|
await prompts.log.info(
|
|
@@ -167,6 +170,14 @@ class UI {
|
|
|
167
170
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// Resolve custom sources from --custom-source flag
|
|
174
|
+
if (options.customSource) {
|
|
175
|
+
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
|
176
|
+
for (const code of customCodes) {
|
|
177
|
+
if (!selectedModules.includes(code)) selectedModules.push(code);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
// Ensure core is in the modules list
|
|
171
182
|
if (!selectedModules.includes('core')) {
|
|
172
183
|
selectedModules.unshift('core');
|
|
@@ -202,6 +213,9 @@ class UI {
|
|
|
202
213
|
.map((m) => m.trim())
|
|
203
214
|
.filter(Boolean);
|
|
204
215
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
|
216
|
+
} else if (options.customSource) {
|
|
217
|
+
// Custom source without --modules: start with empty list (core added below)
|
|
218
|
+
selectedModules = [];
|
|
205
219
|
} else if (options.yes) {
|
|
206
220
|
// Use default modules when --yes flag is set
|
|
207
221
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
|
@@ -210,6 +224,14 @@ class UI {
|
|
|
210
224
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
// Resolve custom sources from --custom-source flag
|
|
228
|
+
if (options.customSource) {
|
|
229
|
+
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
|
230
|
+
for (const code of customCodes) {
|
|
231
|
+
if (!selectedModules.includes(code)) selectedModules.push(code);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
// Ensure core is in the modules list
|
|
214
236
|
if (!selectedModules.includes('core')) {
|
|
215
237
|
selectedModules.unshift('core');
|
|
@@ -818,13 +840,13 @@ class UI {
|
|
|
818
840
|
}
|
|
819
841
|
|
|
820
842
|
/**
|
|
821
|
-
* Prompt user to install modules from custom
|
|
843
|
+
* Prompt user to install modules from custom sources (Git URLs or local paths).
|
|
822
844
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
823
845
|
* @returns {Array} Selected custom module code strings
|
|
824
846
|
*/
|
|
825
847
|
async _addCustomUrlModules(installedModuleIds = new Set()) {
|
|
826
848
|
const addCustom = await prompts.confirm({
|
|
827
|
-
message: 'Would you like to install from a custom
|
|
849
|
+
message: 'Would you like to install from a custom source (Git URL or local path)?',
|
|
828
850
|
default: false,
|
|
829
851
|
});
|
|
830
852
|
if (!addCustom) return [];
|
|
@@ -835,61 +857,158 @@ class UI {
|
|
|
835
857
|
|
|
836
858
|
let addMore = true;
|
|
837
859
|
while (addMore) {
|
|
838
|
-
const
|
|
839
|
-
message: '
|
|
840
|
-
placeholder: 'https://github.com/owner/repo',
|
|
860
|
+
const sourceInput = await prompts.text({
|
|
861
|
+
message: 'Git URL or local path:',
|
|
862
|
+
placeholder: 'https://github.com/owner/repo or /path/to/module',
|
|
841
863
|
validate: (input) => {
|
|
842
|
-
if (!input || input.trim() === '') return '
|
|
843
|
-
const result = customMgr.
|
|
864
|
+
if (!input || input.trim() === '') return 'Source is required';
|
|
865
|
+
const result = customMgr.parseSource(input.trim());
|
|
844
866
|
return result.isValid ? undefined : result.error;
|
|
845
867
|
},
|
|
846
868
|
});
|
|
847
869
|
|
|
848
870
|
const s = await prompts.spinner();
|
|
849
|
-
s.start('
|
|
871
|
+
s.start('Resolving source...');
|
|
850
872
|
|
|
873
|
+
let sourceResult;
|
|
851
874
|
try {
|
|
852
|
-
|
|
853
|
-
s.stop('
|
|
875
|
+
sourceResult = await customMgr.resolveSource(sourceInput.trim(), { skipInstall: true, silent: true });
|
|
876
|
+
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
|
877
|
+
} catch (error) {
|
|
878
|
+
s.error('Failed to resolve source');
|
|
879
|
+
await prompts.log.error(` ${error.message}`);
|
|
880
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
854
883
|
|
|
884
|
+
if (sourceResult.parsed.type === 'local') {
|
|
885
|
+
await prompts.log.info('LOCAL MODULE: Pointing directly at local source (changes take effect on reinstall).');
|
|
886
|
+
} else {
|
|
855
887
|
await prompts.log.warn(
|
|
856
888
|
'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
|
|
857
889
|
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Resolve plugins based on discovery mode vs direct mode
|
|
893
|
+
s.start('Analyzing plugin structure...');
|
|
894
|
+
const allResolved = [];
|
|
895
|
+
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
|
896
|
+
|
|
897
|
+
if (sourceResult.mode === 'discovery') {
|
|
898
|
+
// Discovery mode: marketplace.json found, list available plugins
|
|
899
|
+
let plugins;
|
|
900
|
+
try {
|
|
901
|
+
plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
|
902
|
+
} catch (discoverError) {
|
|
903
|
+
s.error('Failed to discover modules');
|
|
904
|
+
await prompts.log.error(` ${discoverError.message}`);
|
|
905
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
858
908
|
|
|
909
|
+
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
|
859
910
|
for (const plugin of plugins) {
|
|
860
|
-
|
|
861
|
-
|
|
911
|
+
try {
|
|
912
|
+
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
|
913
|
+
if (resolved.length > 0) {
|
|
914
|
+
allResolved.push(...resolved);
|
|
915
|
+
} else {
|
|
916
|
+
// No skills array or empty - use plugin metadata as-is (legacy)
|
|
917
|
+
allResolved.push({
|
|
918
|
+
code: plugin.code,
|
|
919
|
+
name: plugin.displayName || plugin.name,
|
|
920
|
+
version: plugin.version,
|
|
921
|
+
description: plugin.description,
|
|
922
|
+
strategy: 0,
|
|
923
|
+
pluginName: plugin.name,
|
|
924
|
+
skillPaths: [],
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} catch (resolveError) {
|
|
928
|
+
await prompts.log.warn(` Could not resolve ${plugin.name}: ${resolveError.message}`);
|
|
929
|
+
}
|
|
862
930
|
}
|
|
931
|
+
} else {
|
|
932
|
+
// Direct mode: no marketplace.json, scan directory for skills and resolve
|
|
933
|
+
const directPlugin = {
|
|
934
|
+
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
|
935
|
+
source: '.',
|
|
936
|
+
skills: [],
|
|
937
|
+
};
|
|
863
938
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
939
|
+
// Scan for SKILL.md directories to populate skills array
|
|
940
|
+
try {
|
|
941
|
+
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
if (entry.isDirectory()) {
|
|
944
|
+
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
|
945
|
+
if (await fs.pathExists(skillMd)) {
|
|
946
|
+
directPlugin.skills.push(entry.name);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (scanError) {
|
|
951
|
+
s.error('Failed to scan directory');
|
|
952
|
+
await prompts.log.error(` ${scanError.message}`);
|
|
953
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
868
956
|
|
|
869
|
-
if (
|
|
870
|
-
// Pre-clone the repo so it's cached for the install pipeline
|
|
871
|
-
s.start('Cloning repository...');
|
|
957
|
+
if (directPlugin.skills.length > 0) {
|
|
872
958
|
try {
|
|
873
|
-
await customMgr.
|
|
874
|
-
|
|
875
|
-
} catch (
|
|
876
|
-
|
|
877
|
-
await prompts.log.error(` ${cloneError.message}`);
|
|
878
|
-
addMore = await prompts.confirm({ message: 'Try another URL?', default: false });
|
|
879
|
-
continue;
|
|
959
|
+
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
|
960
|
+
allResolved.push(...resolved);
|
|
961
|
+
} catch (resolveError) {
|
|
962
|
+
await prompts.log.warn(` Could not resolve: ${resolveError.message}`);
|
|
880
963
|
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
s.stop(`Found ${allResolved.length} installable module${allResolved.length === 1 ? '' : 's'}`);
|
|
881
967
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
968
|
+
if (allResolved.length === 0) {
|
|
969
|
+
await prompts.log.warn('No installable modules found in this source.');
|
|
970
|
+
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Build multiselect choices
|
|
975
|
+
// Already-installed modules are pre-checked (update). New modules are unchecked (opt-in).
|
|
976
|
+
// Unchecking an installed module means "skip update" - removal is handled elsewhere.
|
|
977
|
+
const choices = allResolved.map((mod) => {
|
|
978
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
979
|
+
const skillCount = mod.skillPaths ? mod.skillPaths.length : 0;
|
|
980
|
+
const skillStr = skillCount > 0 ? ` (${skillCount} skill${skillCount === 1 ? '' : 's'})` : '';
|
|
981
|
+
const alreadyInstalled = installedModuleIds.has(mod.code);
|
|
982
|
+
const hint = alreadyInstalled ? 'update' : undefined;
|
|
983
|
+
|
|
984
|
+
return {
|
|
985
|
+
name: `${mod.name}${versionStr}${skillStr}`,
|
|
986
|
+
value: mod.code,
|
|
987
|
+
hint,
|
|
988
|
+
checked: alreadyInstalled,
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// Show descriptions before the multiselect
|
|
993
|
+
for (const mod of allResolved) {
|
|
994
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
995
|
+
await prompts.log.info(` ${mod.name}${versionStr}\n ${mod.description}`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const selected = await prompts.multiselect({
|
|
999
|
+
message: 'Select modules to install:',
|
|
1000
|
+
choices,
|
|
1001
|
+
required: false,
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
if (selected && selected.length > 0) {
|
|
1005
|
+
for (const code of selected) {
|
|
1006
|
+
selectedModules.push(code);
|
|
885
1007
|
}
|
|
886
|
-
} catch (error) {
|
|
887
|
-
s.error('Failed to load module info');
|
|
888
|
-
await prompts.log.error(` ${error.message}`);
|
|
889
1008
|
}
|
|
890
1009
|
|
|
891
1010
|
addMore = await prompts.confirm({
|
|
892
|
-
message: 'Add another custom
|
|
1011
|
+
message: 'Add another custom source?',
|
|
893
1012
|
default: false,
|
|
894
1013
|
});
|
|
895
1014
|
}
|
|
@@ -901,6 +1020,102 @@ class UI {
|
|
|
901
1020
|
return selectedModules;
|
|
902
1021
|
}
|
|
903
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve custom sources from --custom-source CLI flag (non-interactive).
|
|
1025
|
+
* Auto-selects all discovered modules from each source.
|
|
1026
|
+
* @param {string} sourcesArg - Comma-separated Git URLs or local paths
|
|
1027
|
+
* @returns {Array} Module codes from all resolved sources
|
|
1028
|
+
*/
|
|
1029
|
+
async _resolveCustomSourcesCli(sourcesArg) {
|
|
1030
|
+
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
|
1031
|
+
const customMgr = new CustomModuleManager();
|
|
1032
|
+
const allCodes = [];
|
|
1033
|
+
|
|
1034
|
+
const sources = sourcesArg
|
|
1035
|
+
.split(',')
|
|
1036
|
+
.map((s) => s.trim())
|
|
1037
|
+
.filter(Boolean);
|
|
1038
|
+
|
|
1039
|
+
for (const source of sources) {
|
|
1040
|
+
const s = await prompts.spinner();
|
|
1041
|
+
s.start(`Resolving ${source}...`);
|
|
1042
|
+
|
|
1043
|
+
let sourceResult;
|
|
1044
|
+
try {
|
|
1045
|
+
sourceResult = await customMgr.resolveSource(source, { skipInstall: true, silent: true });
|
|
1046
|
+
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
s.error(`Failed to resolve ${source}`);
|
|
1049
|
+
await prompts.log.error(` ${error.message}`);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const s2 = await prompts.spinner();
|
|
1054
|
+
s2.start('Analyzing plugin structure...');
|
|
1055
|
+
const allResolved = [];
|
|
1056
|
+
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
|
1057
|
+
|
|
1058
|
+
if (sourceResult.mode === 'discovery') {
|
|
1059
|
+
try {
|
|
1060
|
+
const plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
|
1061
|
+
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
|
1062
|
+
for (const plugin of plugins) {
|
|
1063
|
+
try {
|
|
1064
|
+
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
|
1065
|
+
if (resolved.length > 0) {
|
|
1066
|
+
allResolved.push(...resolved);
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
// Skip unresolvable plugins
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
} catch (discoverError) {
|
|
1073
|
+
s2.error('Failed to discover modules');
|
|
1074
|
+
await prompts.log.error(` ${discoverError.message}`);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
// Direct mode: scan for SKILL.md directories
|
|
1079
|
+
const directPlugin = {
|
|
1080
|
+
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
|
1081
|
+
source: '.',
|
|
1082
|
+
skills: [],
|
|
1083
|
+
};
|
|
1084
|
+
try {
|
|
1085
|
+
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
|
1086
|
+
for (const entry of entries) {
|
|
1087
|
+
if (entry.isDirectory()) {
|
|
1088
|
+
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
|
1089
|
+
if (await fs.pathExists(skillMd)) {
|
|
1090
|
+
directPlugin.skills.push(entry.name);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
// Skip unreadable directories
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (directPlugin.skills.length > 0) {
|
|
1099
|
+
try {
|
|
1100
|
+
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
|
1101
|
+
allResolved.push(...resolved);
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Skip unresolvable
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`);
|
|
1108
|
+
|
|
1109
|
+
for (const mod of allResolved) {
|
|
1110
|
+
allCodes.push(mod.code);
|
|
1111
|
+
const versionStr = mod.version ? ` v${mod.version}` : '';
|
|
1112
|
+
await prompts.log.info(` Custom module: ${mod.name}${versionStr}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return allCodes;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
904
1119
|
/**
|
|
905
1120
|
* Get default modules for non-interactive mode
|
|
906
1121
|
* @param {Set} installedModuleIds - Already installed module IDs
|