bmad-method 6.2.3-next.9 → 6.3.0
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/.claude-plugin/marketplace.json +0 -3
- package/README.md +8 -9
- package/README_CN.md +1 -1
- package/README_VN.md +110 -0
- package/package.json +1 -1
- package/removals.txt +17 -0
- package/src/bmm-skills/2-plan-workflows/bmad-create-ux-design/steps/step-13-responsive-accessibility.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/data/prd-purpose.md +197 -0
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01-discovery.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-02-review.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-03-edit.md +1 -1
- package/src/bmm-skills/2-plan-workflows/bmad-edit-prd/steps-e/step-e-04-complete.md +1 -3
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-01-document-discovery.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-check-implementation-readiness/workflow.md +1 -1
- package/src/bmm-skills/3-solutioning/bmad-create-epics-and-stories/workflow.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +5 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/SKILL.md +29 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/generate-trail.md +38 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-01-orientation.md +105 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-02-walkthrough.md +89 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-03-detail-pass.md +106 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-04-testing.md +74 -0
- package/src/bmm-skills/4-implementation/bmad-checkpoint-preview/step-05-wrapup.md +24 -0
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-01-gather-context.md +38 -15
- package/src/bmm-skills/4-implementation/bmad-correct-course/checklist.md +2 -2
- package/src/bmm-skills/4-implementation/bmad-correct-course/workflow.md +8 -8
- package/src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests/checklist.md +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/spec-template.md +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +33 -6
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +20 -8
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-03-implement.md +2 -0
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +16 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -5
- package/src/bmm-skills/4-implementation/bmad-retrospective/workflow.md +134 -134
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/sprint-status-template.yaml +1 -1
- package/src/bmm-skills/4-implementation/bmad-sprint-planning/workflow.md +3 -3
- package/src/bmm-skills/4-implementation/bmad-sprint-status/workflow.md +2 -2
- package/src/bmm-skills/module-help.csv +2 -0
- package/src/core-skills/bmad-help/SKILL.md +4 -2
- package/src/core-skills/bmad-party-mode/SKILL.md +8 -6
- package/src/core-skills/module-help.csv +1 -0
- package/tools/installer/cli-utils.js +18 -9
- package/tools/installer/commands/install.js +1 -1
- package/tools/installer/core/existing-install.js +2 -8
- package/tools/installer/core/install-paths.js +0 -3
- package/tools/installer/core/installer.js +180 -463
- package/tools/installer/core/manifest-generator.js +8 -14
- package/tools/installer/core/manifest.js +94 -102
- package/tools/installer/ide/_config-driven.js +149 -38
- package/tools/installer/ide/shared/skill-manifest.js +1 -16
- package/tools/installer/install-messages.yaml +19 -26
- package/tools/installer/modules/community-manager.js +377 -0
- package/tools/installer/modules/custom-module-manager.js +644 -0
- package/tools/installer/modules/external-manager.js +65 -49
- package/tools/installer/modules/official-modules.js +117 -65
- package/tools/installer/modules/plugin-resolver.js +398 -0
- package/tools/installer/modules/registry-client.js +66 -0
- package/tools/installer/{external-official-modules.yaml → modules/registry-fallback.yaml} +3 -12
- package/tools/installer/ui.js +549 -666
- package/src/bmm-skills/4-implementation/bmad-agent-qa/SKILL.md +0 -61
- package/src/bmm-skills/4-implementation/bmad-agent-qa/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/SKILL.md +0 -53
- package/src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev/bmad-skill-manifest.yaml +0 -11
- package/src/bmm-skills/4-implementation/bmad-agent-sm/SKILL.md +0 -55
- package/src/bmm-skills/4-implementation/bmad-agent-sm/bmad-skill-manifest.yaml +0 -11
- package/tools/installer/core/custom-module-cache.js +0 -260
- package/tools/installer/custom-handler.js +0 -112
- package/tools/installer/modules/custom-modules.js +0 -197
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const yaml = require('yaml');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves how to install a plugin from marketplace.json by analyzing
|
|
7
|
+
* where module.yaml and module-help.csv live relative to the listed skills.
|
|
8
|
+
*
|
|
9
|
+
* Five strategies, tried in order:
|
|
10
|
+
* 1. Root module files at the common parent of all skills
|
|
11
|
+
* 2. A -setup skill with assets/module.yaml + assets/module-help.csv
|
|
12
|
+
* 3. Single standalone skill with both files in its assets/
|
|
13
|
+
* 4. Multiple standalone skills, each with both files in assets/
|
|
14
|
+
* 5. Fallback: synthesize from marketplace.json + SKILL.md frontmatter
|
|
15
|
+
*/
|
|
16
|
+
class PluginResolver {
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a plugin to one or more installable module definitions.
|
|
19
|
+
* @param {string} repoPath - Absolute path to the cloned repository root
|
|
20
|
+
* @param {Object} plugin - Plugin object from marketplace.json
|
|
21
|
+
* @param {string} plugin.name - Plugin identifier
|
|
22
|
+
* @param {string} [plugin.source] - Relative path from repo root
|
|
23
|
+
* @param {string} [plugin.version] - Semantic version
|
|
24
|
+
* @param {string} [plugin.description] - Plugin description
|
|
25
|
+
* @param {string[]} [plugin.skills] - Relative paths to skill directories
|
|
26
|
+
* @returns {Promise<ResolvedModule[]>} Array of resolved module definitions
|
|
27
|
+
*/
|
|
28
|
+
async resolve(repoPath, plugin) {
|
|
29
|
+
const skillRelPaths = plugin.skills || [];
|
|
30
|
+
|
|
31
|
+
// No skills array: legacy behavior - caller should use existing findModuleSource
|
|
32
|
+
if (skillRelPaths.length === 0) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve skill paths to absolute, constrain to repo root, filter non-existent
|
|
37
|
+
const repoRoot = path.resolve(repoPath);
|
|
38
|
+
const skillPaths = [];
|
|
39
|
+
for (const rel of skillRelPaths) {
|
|
40
|
+
const normalized = rel.replace(/^\.\//, '');
|
|
41
|
+
const abs = path.resolve(repoPath, normalized);
|
|
42
|
+
// Guard against path traversal (.. segments, absolute paths in marketplace.json)
|
|
43
|
+
if (!abs.startsWith(repoRoot + path.sep) && abs !== repoRoot) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (await fs.pathExists(abs)) {
|
|
47
|
+
skillPaths.push(abs);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (skillPaths.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try each strategy in order
|
|
56
|
+
const result =
|
|
57
|
+
(await this._tryRootModuleFiles(repoPath, plugin, skillPaths)) ||
|
|
58
|
+
(await this._trySetupSkill(repoPath, plugin, skillPaths)) ||
|
|
59
|
+
(await this._trySingleStandalone(repoPath, plugin, skillPaths)) ||
|
|
60
|
+
(await this._tryMultipleStandalone(repoPath, plugin, skillPaths)) ||
|
|
61
|
+
(await this._synthesizeFallback(repoPath, plugin, skillPaths));
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Strategy 1: Root Module Files ──────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if module.yaml + module-help.csv exist at the common parent of all skills.
|
|
70
|
+
*/
|
|
71
|
+
async _tryRootModuleFiles(repoPath, plugin, skillPaths) {
|
|
72
|
+
const commonParent = this._computeCommonParent(skillPaths);
|
|
73
|
+
const moduleYamlPath = path.join(commonParent, 'module.yaml');
|
|
74
|
+
const moduleHelpPath = path.join(commonParent, 'module-help.csv');
|
|
75
|
+
|
|
76
|
+
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
|
81
|
+
if (!moduleData) return null;
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
code: moduleData.code || plugin.name,
|
|
86
|
+
name: moduleData.name || plugin.name,
|
|
87
|
+
version: plugin.version || moduleData.module_version || null,
|
|
88
|
+
description: moduleData.description || plugin.description || '',
|
|
89
|
+
strategy: 1,
|
|
90
|
+
pluginName: plugin.name,
|
|
91
|
+
moduleYamlPath,
|
|
92
|
+
moduleHelpCsvPath: moduleHelpPath,
|
|
93
|
+
skillPaths,
|
|
94
|
+
synthesizedModuleYaml: null,
|
|
95
|
+
synthesizedHelpCsv: null,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Strategy 2: Setup Skill ────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Search for a skill ending in -setup with assets/module.yaml + assets/module-help.csv.
|
|
104
|
+
*/
|
|
105
|
+
async _trySetupSkill(repoPath, plugin, skillPaths) {
|
|
106
|
+
for (const skillPath of skillPaths) {
|
|
107
|
+
const dirName = path.basename(skillPath);
|
|
108
|
+
if (!dirName.endsWith('-setup')) continue;
|
|
109
|
+
|
|
110
|
+
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
|
111
|
+
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
|
112
|
+
|
|
113
|
+
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
|
118
|
+
if (!moduleData) continue;
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
code: moduleData.code || plugin.name,
|
|
123
|
+
name: moduleData.name || plugin.name,
|
|
124
|
+
version: plugin.version || moduleData.module_version || null,
|
|
125
|
+
description: moduleData.description || plugin.description || '',
|
|
126
|
+
strategy: 2,
|
|
127
|
+
pluginName: plugin.name,
|
|
128
|
+
moduleYamlPath,
|
|
129
|
+
moduleHelpCsvPath: moduleHelpPath,
|
|
130
|
+
skillPaths,
|
|
131
|
+
synthesizedModuleYaml: null,
|
|
132
|
+
synthesizedHelpCsv: null,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Strategy 3: Single Standalone Skill ────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One skill listed, with assets/module.yaml + assets/module-help.csv.
|
|
144
|
+
*/
|
|
145
|
+
async _trySingleStandalone(repoPath, plugin, skillPaths) {
|
|
146
|
+
if (skillPaths.length !== 1) return null;
|
|
147
|
+
|
|
148
|
+
const skillPath = skillPaths[0];
|
|
149
|
+
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
|
150
|
+
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
|
151
|
+
|
|
152
|
+
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
|
157
|
+
if (!moduleData) return null;
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
code: moduleData.code || plugin.name,
|
|
162
|
+
name: moduleData.name || plugin.name,
|
|
163
|
+
version: plugin.version || moduleData.module_version || null,
|
|
164
|
+
description: moduleData.description || plugin.description || '',
|
|
165
|
+
strategy: 3,
|
|
166
|
+
pluginName: plugin.name,
|
|
167
|
+
moduleYamlPath,
|
|
168
|
+
moduleHelpCsvPath: moduleHelpPath,
|
|
169
|
+
skillPaths,
|
|
170
|
+
synthesizedModuleYaml: null,
|
|
171
|
+
synthesizedHelpCsv: null,
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Strategy 4: Multiple Standalone Skills ─────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Multiple skills, each with assets/module.yaml + assets/module-help.csv.
|
|
180
|
+
* Each becomes its own installable module.
|
|
181
|
+
*/
|
|
182
|
+
async _tryMultipleStandalone(repoPath, plugin, skillPaths) {
|
|
183
|
+
if (skillPaths.length < 2) return null;
|
|
184
|
+
|
|
185
|
+
const resolved = [];
|
|
186
|
+
|
|
187
|
+
for (const skillPath of skillPaths) {
|
|
188
|
+
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
|
189
|
+
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
|
190
|
+
|
|
191
|
+
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
|
196
|
+
if (!moduleData) continue;
|
|
197
|
+
|
|
198
|
+
resolved.push({
|
|
199
|
+
code: moduleData.code || path.basename(skillPath),
|
|
200
|
+
name: moduleData.name || path.basename(skillPath),
|
|
201
|
+
version: plugin.version || moduleData.module_version || null,
|
|
202
|
+
description: moduleData.description || '',
|
|
203
|
+
strategy: 4,
|
|
204
|
+
pluginName: plugin.name,
|
|
205
|
+
moduleYamlPath,
|
|
206
|
+
moduleHelpCsvPath: moduleHelpPath,
|
|
207
|
+
skillPaths: [skillPath],
|
|
208
|
+
synthesizedModuleYaml: null,
|
|
209
|
+
synthesizedHelpCsv: null,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Only use strategy 4 if ALL skills have module files
|
|
214
|
+
if (resolved.length === skillPaths.length) {
|
|
215
|
+
return resolved;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Partial match: fall through to strategy 5
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Strategy 5: Fallback (Synthesized) ─────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* No module files found anywhere. Synthesize from marketplace.json metadata
|
|
226
|
+
* and SKILL.md frontmatter.
|
|
227
|
+
*/
|
|
228
|
+
async _synthesizeFallback(repoPath, plugin, skillPaths) {
|
|
229
|
+
const skillInfos = [];
|
|
230
|
+
|
|
231
|
+
for (const skillPath of skillPaths) {
|
|
232
|
+
const frontmatter = await this._parseSkillFrontmatter(skillPath);
|
|
233
|
+
skillInfos.push({
|
|
234
|
+
dirName: path.basename(skillPath),
|
|
235
|
+
name: frontmatter.name || path.basename(skillPath),
|
|
236
|
+
description: frontmatter.description || '',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const moduleName = this._formatDisplayName(plugin.name);
|
|
241
|
+
const code = plugin.name;
|
|
242
|
+
|
|
243
|
+
const synthesizedYaml = {
|
|
244
|
+
code,
|
|
245
|
+
name: moduleName,
|
|
246
|
+
description: plugin.description || '',
|
|
247
|
+
module_version: plugin.version || '1.0.0',
|
|
248
|
+
default_selected: false,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const synthesizedCsv = this._buildSynthesizedHelpCsv(moduleName, skillInfos);
|
|
252
|
+
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
code,
|
|
256
|
+
name: moduleName,
|
|
257
|
+
version: plugin.version || null,
|
|
258
|
+
description: plugin.description || '',
|
|
259
|
+
strategy: 5,
|
|
260
|
+
pluginName: plugin.name,
|
|
261
|
+
moduleYamlPath: null,
|
|
262
|
+
moduleHelpCsvPath: null,
|
|
263
|
+
skillPaths,
|
|
264
|
+
synthesizedModuleYaml: synthesizedYaml,
|
|
265
|
+
synthesizedHelpCsv: synthesizedCsv,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Compute the deepest common ancestor directory of an array of absolute paths.
|
|
274
|
+
* @param {string[]} absPaths - Absolute directory paths
|
|
275
|
+
* @returns {string} Common parent directory
|
|
276
|
+
*/
|
|
277
|
+
_computeCommonParent(absPaths) {
|
|
278
|
+
if (absPaths.length === 0) return '/';
|
|
279
|
+
if (absPaths.length === 1) return path.dirname(absPaths[0]);
|
|
280
|
+
|
|
281
|
+
const segments = absPaths.map((p) => p.split(path.sep));
|
|
282
|
+
const minLen = Math.min(...segments.map((s) => s.length));
|
|
283
|
+
const common = [];
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < minLen; i++) {
|
|
286
|
+
const segment = segments[0][i];
|
|
287
|
+
if (segments.every((s) => s[i] === segment)) {
|
|
288
|
+
common.push(segment);
|
|
289
|
+
} else {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return common.join(path.sep) || '/';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Read and parse a module.yaml file.
|
|
299
|
+
* @param {string} yamlPath - Absolute path to module.yaml
|
|
300
|
+
* @returns {Object|null} Parsed content or null on failure
|
|
301
|
+
*/
|
|
302
|
+
async _readModuleYaml(yamlPath) {
|
|
303
|
+
try {
|
|
304
|
+
const content = await fs.readFile(yamlPath, 'utf8');
|
|
305
|
+
return yaml.parse(content);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Extract name and description from a SKILL.md YAML frontmatter block.
|
|
313
|
+
* @param {string} skillDirPath - Absolute path to the skill directory
|
|
314
|
+
* @returns {Object} { name, description } or empty strings
|
|
315
|
+
*/
|
|
316
|
+
async _parseSkillFrontmatter(skillDirPath) {
|
|
317
|
+
const skillMdPath = path.join(skillDirPath, 'SKILL.md');
|
|
318
|
+
try {
|
|
319
|
+
const content = await fs.readFile(skillMdPath, 'utf8');
|
|
320
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
321
|
+
if (!match) return { name: '', description: '' };
|
|
322
|
+
|
|
323
|
+
const parsed = yaml.parse(match[1]);
|
|
324
|
+
return {
|
|
325
|
+
name: parsed.name || '',
|
|
326
|
+
description: parsed.description || '',
|
|
327
|
+
};
|
|
328
|
+
} catch {
|
|
329
|
+
return { name: '', description: '' };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build a synthesized module-help.csv from plugin metadata and skill frontmatter.
|
|
335
|
+
* Uses the standard 13-column format.
|
|
336
|
+
* @param {string} moduleName - Display name for the module column
|
|
337
|
+
* @param {Array<{dirName: string, name: string, description: string}>} skillInfos
|
|
338
|
+
* @returns {string} CSV content
|
|
339
|
+
*/
|
|
340
|
+
_buildSynthesizedHelpCsv(moduleName, skillInfos) {
|
|
341
|
+
const header = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
|
|
342
|
+
const rows = [header];
|
|
343
|
+
|
|
344
|
+
for (const info of skillInfos) {
|
|
345
|
+
const displayName = this._formatDisplayName(info.name || info.dirName);
|
|
346
|
+
const menuCode = this._generateMenuCode(info.name || info.dirName);
|
|
347
|
+
const description = this._escapeCSVField(info.description);
|
|
348
|
+
|
|
349
|
+
rows.push(`${moduleName},${info.dirName},${displayName},${menuCode},${description},activate,,anytime,,,false,,`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return rows.join('\n') + '\n';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Format a kebab-case or snake_case name into a display name.
|
|
357
|
+
* Strips common prefixes like "bmad-" or "bmad-agent-".
|
|
358
|
+
* @param {string} name - Raw name
|
|
359
|
+
* @returns {string} Formatted display name
|
|
360
|
+
*/
|
|
361
|
+
_formatDisplayName(name) {
|
|
362
|
+
let cleaned = name.replace(/^bmad-agent-/, '').replace(/^bmad-/, '');
|
|
363
|
+
return cleaned
|
|
364
|
+
.split(/[-_]/)
|
|
365
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
366
|
+
.join(' ');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Generate a short menu code from a skill name.
|
|
371
|
+
* Takes first letter of each significant word, uppercased, max 3 chars.
|
|
372
|
+
* @param {string} name - Skill name (kebab-case)
|
|
373
|
+
* @returns {string} Menu code (e.g., "CC" for "code-coach")
|
|
374
|
+
*/
|
|
375
|
+
_generateMenuCode(name) {
|
|
376
|
+
const cleaned = name.replace(/^bmad-agent-/, '').replace(/^bmad-/, '');
|
|
377
|
+
const words = cleaned.split(/[-_]/).filter((w) => w.length > 0);
|
|
378
|
+
return words
|
|
379
|
+
.map((w) => w.charAt(0).toUpperCase())
|
|
380
|
+
.join('')
|
|
381
|
+
.slice(0, 3);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Escape a value for CSV output (wrap in quotes if it contains commas, quotes, or newlines).
|
|
386
|
+
* @param {string} value
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
_escapeCSVField(value) {
|
|
390
|
+
if (!value) return '';
|
|
391
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
392
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
393
|
+
}
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = { PluginResolver };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const https = require('node:https');
|
|
2
|
+
const yaml = require('yaml');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared HTTP client for fetching registry data from GitHub.
|
|
6
|
+
* Used by ExternalModuleManager, CommunityModuleManager, and CustomModuleManager.
|
|
7
|
+
*/
|
|
8
|
+
class RegistryClient {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.timeout = options.timeout || 10_000;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a URL and return the response body as a string.
|
|
15
|
+
* Follows one redirect (GitHub sometimes 301s).
|
|
16
|
+
* @param {string} url - URL to fetch
|
|
17
|
+
* @param {number} [timeout] - Timeout in ms (overrides default)
|
|
18
|
+
* @returns {Promise<string>} Response body
|
|
19
|
+
*/
|
|
20
|
+
fetch(url, timeout) {
|
|
21
|
+
const timeoutMs = timeout || this.timeout;
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const req = https
|
|
24
|
+
.get(url, { timeout: timeoutMs }, (res) => {
|
|
25
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
26
|
+
return this.fetch(res.headers.location, timeoutMs).then(resolve, reject);
|
|
27
|
+
}
|
|
28
|
+
if (res.statusCode !== 200) {
|
|
29
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
30
|
+
}
|
|
31
|
+
let data = '';
|
|
32
|
+
res.on('data', (chunk) => (data += chunk));
|
|
33
|
+
res.on('end', () => resolve(data));
|
|
34
|
+
})
|
|
35
|
+
.on('error', reject)
|
|
36
|
+
.on('timeout', () => {
|
|
37
|
+
req.destroy();
|
|
38
|
+
reject(new Error('Request timed out'));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch a URL and parse the response as YAML.
|
|
45
|
+
* @param {string} url - URL to fetch
|
|
46
|
+
* @param {number} [timeout] - Timeout in ms
|
|
47
|
+
* @returns {Promise<Object>} Parsed YAML content
|
|
48
|
+
*/
|
|
49
|
+
async fetchYaml(url, timeout) {
|
|
50
|
+
const content = await this.fetch(url, timeout);
|
|
51
|
+
return yaml.parse(content);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch a URL and parse the response as JSON.
|
|
56
|
+
* @param {string} url - URL to fetch
|
|
57
|
+
* @param {number} [timeout] - Timeout in ms
|
|
58
|
+
* @returns {Promise<Object>} Parsed JSON content
|
|
59
|
+
*/
|
|
60
|
+
async fetchJson(url, timeout) {
|
|
61
|
+
const content = await this.fetch(url, timeout);
|
|
62
|
+
return JSON.parse(content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { RegistryClient };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# Fallback module registry — used only when the BMad Marketplace repo
|
|
2
|
+
# (bmad-code-org/bmad-plugins-marketplace) is unreachable.
|
|
3
|
+
# The remote registry/official.yaml is the source of truth.
|
|
3
4
|
|
|
4
5
|
modules:
|
|
5
6
|
bmad-builder:
|
|
@@ -41,13 +42,3 @@ modules:
|
|
|
41
42
|
defaultSelected: false
|
|
42
43
|
type: bmad-org
|
|
43
44
|
npmPackage: bmad-method-test-architecture-enterprise
|
|
44
|
-
|
|
45
|
-
whiteport-design-studio:
|
|
46
|
-
url: https://github.com/bmad-code-org/bmad-method-wds-expansion
|
|
47
|
-
module-definition: src/module.yaml
|
|
48
|
-
code: wds
|
|
49
|
-
name: "Whiteport Design Studio (For UX Professionals)"
|
|
50
|
-
description: "Whiteport Design Studio (For UX Professionals)"
|
|
51
|
-
defaultSelected: false
|
|
52
|
-
type: community
|
|
53
|
-
npmPackage: bmad-method-wds-expansion
|