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
|
@@ -135,6 +135,22 @@ class OfficialModules {
|
|
|
135
135
|
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
|
136
136
|
|
|
137
137
|
if (!(await fs.pathExists(moduleConfigPath))) {
|
|
138
|
+
// Check resolution cache for strategy 5 modules (no module.yaml on disk)
|
|
139
|
+
const { CustomModuleManager } = require('./custom-module-manager');
|
|
140
|
+
const customMgr = new CustomModuleManager();
|
|
141
|
+
const resolved = customMgr.getResolution(defaultName);
|
|
142
|
+
if (resolved && resolved.synthesizedModuleYaml) {
|
|
143
|
+
return {
|
|
144
|
+
id: resolved.code,
|
|
145
|
+
path: modulePath,
|
|
146
|
+
name: resolved.name,
|
|
147
|
+
description: resolved.description,
|
|
148
|
+
version: resolved.version || '1.0.0',
|
|
149
|
+
source: sourceDescription,
|
|
150
|
+
dependencies: [],
|
|
151
|
+
defaultSelected: false,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
138
154
|
return null;
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -232,6 +248,14 @@ class OfficialModules {
|
|
|
232
248
|
* @param {Object} options.logger - Logger instance for output
|
|
233
249
|
*/
|
|
234
250
|
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
|
|
251
|
+
// Check if this module has a plugin resolution (custom marketplace install)
|
|
252
|
+
const { CustomModuleManager } = require('./custom-module-manager');
|
|
253
|
+
const customMgr = new CustomModuleManager();
|
|
254
|
+
const resolved = customMgr.getResolution(moduleName);
|
|
255
|
+
if (resolved) {
|
|
256
|
+
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
|
|
236
260
|
const targetPath = path.join(bmadDir, moduleName);
|
|
237
261
|
|
|
@@ -265,6 +289,62 @@ class OfficialModules {
|
|
|
265
289
|
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
266
290
|
}
|
|
267
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Install a module from a PluginResolver resolution result.
|
|
294
|
+
* Copies specific skill directories and places module-help.csv at the target root.
|
|
295
|
+
* @param {Object} resolved - ResolvedModule from PluginResolver
|
|
296
|
+
* @param {string} bmadDir - Target bmad directory
|
|
297
|
+
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
298
|
+
* @param {Object} options - Installation options
|
|
299
|
+
*/
|
|
300
|
+
async installFromResolution(resolved, bmadDir, fileTrackingCallback = null, options = {}) {
|
|
301
|
+
const targetPath = path.join(bmadDir, resolved.code);
|
|
302
|
+
|
|
303
|
+
if (await fs.pathExists(targetPath)) {
|
|
304
|
+
await fs.remove(targetPath);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await fs.ensureDir(targetPath);
|
|
308
|
+
|
|
309
|
+
// Copy each skill directory, flattened by leaf name
|
|
310
|
+
for (const skillPath of resolved.skillPaths) {
|
|
311
|
+
const skillDirName = path.basename(skillPath);
|
|
312
|
+
const skillTarget = path.join(targetPath, skillDirName);
|
|
313
|
+
await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, options.moduleConfig);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Place module-help.csv at the module root
|
|
317
|
+
if (resolved.moduleHelpCsvPath) {
|
|
318
|
+
// Strategies 1-4: copy the existing file
|
|
319
|
+
const helpTarget = path.join(targetPath, 'module-help.csv');
|
|
320
|
+
await fs.copy(resolved.moduleHelpCsvPath, helpTarget, { overwrite: true });
|
|
321
|
+
if (fileTrackingCallback) fileTrackingCallback(helpTarget);
|
|
322
|
+
} else if (resolved.synthesizedHelpCsv) {
|
|
323
|
+
// Strategy 5: write synthesized content
|
|
324
|
+
const helpTarget = path.join(targetPath, 'module-help.csv');
|
|
325
|
+
await fs.writeFile(helpTarget, resolved.synthesizedHelpCsv, 'utf8');
|
|
326
|
+
if (fileTrackingCallback) fileTrackingCallback(helpTarget);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Create directories declared in module.yaml (strategies 1-4 may have these)
|
|
330
|
+
if (!options.skipModuleInstaller) {
|
|
331
|
+
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Update manifest
|
|
335
|
+
const { Manifest } = require('../core/manifest');
|
|
336
|
+
const manifestObj = new Manifest();
|
|
337
|
+
|
|
338
|
+
await manifestObj.addModule(bmadDir, resolved.code, {
|
|
339
|
+
version: resolved.version || null,
|
|
340
|
+
source: 'custom',
|
|
341
|
+
npmPackage: null,
|
|
342
|
+
repoUrl: resolved.repoUrl || null,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return { success: true, module: resolved.code, path: targetPath, versionInfo: { version: resolved.version || '' } };
|
|
346
|
+
}
|
|
347
|
+
|
|
268
348
|
/**
|
|
269
349
|
* Update an existing module
|
|
270
350
|
* @param {string} moduleName - Name of the module to update
|
|
@@ -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 };
|