bmad-method 6.9.1-next.14 → 6.9.1-next.15

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/bmad-modules.yaml CHANGED
@@ -7,6 +7,25 @@
7
7
  # default_channel (optional) — the install channel when the user does not
8
8
  # override with --channel/--pin/--next. Valid values: stable | next.
9
9
  # Omit to inherit the installer's hardcoded default (stable).
10
+ #
11
+ # deprecated (optional, default false) — when true, the module is hidden from
12
+ # the installer picker UNLESS it is already installed (so existing users can
13
+ # still see/manage it, but new users are not offered it).
14
+ # deprecation-message (optional) — surfaced in the picker hint for a deprecated
15
+ # module that is still installed; use it to point users to the replacement.
16
+ #
17
+ # marketplace-plugin (optional, default false) — when true, the module's
18
+ # installable skills are resolved from its .claude-plugin/marketplace.json via
19
+ # the plugin resolver (instead of copying the single module-definition dir).
20
+ # Use this for modules whose module.yaml does not sit alongside the skill
21
+ # folders. module-definition should still point at the real module.yaml so
22
+ # config/version lookups work.
23
+ #
24
+ # post-install-message (optional) — an "action needed" notice shown to the user
25
+ # after install completes, once for each install run that includes the module.
26
+ # Interactive installs require the user to acknowledge it (press Enter);
27
+ # non-interactive (--yes) installs print it and continue. Use for required
28
+ # follow-up steps (e.g. "run the X setup skill").
10
29
 
11
30
  modules:
12
31
  bmad-method-test-architecture-enterprise:
@@ -41,6 +60,29 @@ modules:
41
60
  type: experimental
42
61
  npmPackage: bmad-story-automator
43
62
  default_channel: next
63
+ deprecated: true
64
+ deprecation-message: "BMad Automator has been deprecated and is replaced by BMad Auto (bmad-auto). Install BMad Auto instead."
65
+
66
+ bmad-auto:
67
+ url: https://github.com/bmad-code-org/bmad-auto
68
+ module-definition: src/automator/data/skills/bmad-auto-setup/assets/module.yaml
69
+ code: bauto
70
+ name: "BMad Auto"
71
+ description: "Automation-mode skills driven by the bmad-auto orchestrator: unattended dev, adversarial review, and deferred-work sweep triage"
72
+ defaultSelected: false
73
+ type: bmad-org
74
+ default_channel: stable
75
+ # Skills live outside a single module.yaml dir; resolve them from
76
+ # .claude-plugin/marketplace.json via the plugin resolver (see external-manager).
77
+ marketplace-plugin: true
78
+ post-install-message: |
79
+ BMad Auto installed. To finish setup, run the bmad-auto-setup skill
80
+ from your agent:
81
+
82
+ > use the bmad-auto-setup skill
83
+
84
+ It installs the bmad-auto orchestrator tool and wires up the per-project
85
+ hooks and policy. The automation skills don't run until setup completes.
44
86
 
45
87
  bmad-creative-intelligence-suite:
46
88
  url: https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.9.1-next.14",
4
+ "version": "6.9.1-next.15",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -102,6 +102,11 @@ class Installer {
102
102
 
103
103
  const restoreResult = await this._restoreUserFiles(paths, updateState);
104
104
 
105
+ // Surface any "action needed" post-install messages for installed modules
106
+ // (e.g. run a setup skill) and let the user acknowledge them before the
107
+ // final summary, so "BMAD is ready to use!" stays the last thing shown.
108
+ await this._displayPostInstallMessages(config, officialModules);
109
+
105
110
  // Render consolidated summary
106
111
  await this.renderInstallSummary(results, {
107
112
  bmadDir: paths.bmadDir,
@@ -1246,6 +1251,56 @@ class Installer {
1246
1251
  });
1247
1252
  }
1248
1253
 
1254
+ /**
1255
+ * Display registry-defined post-install messages for the modules installed in
1256
+ * this run. These are "action needed" notices (e.g. "run the bmad-auto-setup
1257
+ * skill") that the user must see to finish setup. They are defined via the
1258
+ * `post-install-message` property on a module's bmad-modules.yaml entry.
1259
+ *
1260
+ * Interactive installs require the user to acknowledge each message (press
1261
+ * Enter); non-interactive (--yes / skipPrompts) installs print the message
1262
+ * and continue without blocking, so CI/scripted installs don't hang.
1263
+ *
1264
+ * @param {Object} config - Install config (config.modules, config.skipPrompts)
1265
+ * @param {Object} officialModules - OfficialModules instance (carries the registry)
1266
+ */
1267
+ async _displayPostInstallMessages(config, officialModules) {
1268
+ const moduleCodes = config.modules || [];
1269
+ if (moduleCodes.length === 0) return;
1270
+
1271
+ const externalManager = officialModules.externalModuleManager;
1272
+ if (!externalManager) return;
1273
+
1274
+ const color = await prompts.getColor();
1275
+
1276
+ for (const code of moduleCodes) {
1277
+ let moduleInfo;
1278
+ try {
1279
+ moduleInfo = await externalManager.getModuleByCode(code);
1280
+ } catch {
1281
+ continue; // Built-in modules (core/bmm) aren't in the registry — skip.
1282
+ }
1283
+
1284
+ const message = moduleInfo && moduleInfo.postInstallMessage;
1285
+ if (!message) continue;
1286
+
1287
+ await prompts.box(String(message).trim(), `⚑ Action needed — ${moduleInfo.name || code}`, {
1288
+ rounded: true,
1289
+ formatBorder: color.yellow,
1290
+ });
1291
+
1292
+ // Interactive: require the user to acknowledge before continuing. Skip the
1293
+ // blocking prompt in non-interactive installs (the message is still shown).
1294
+ if (!config.skipPrompts) {
1295
+ await prompts.text({
1296
+ message: 'Press Enter to acknowledge',
1297
+ placeholder: '',
1298
+ default: '',
1299
+ });
1300
+ }
1301
+ }
1302
+ }
1303
+
1249
1304
  /**
1250
1305
  * Quick update method - preserves all settings and only prompts for new config fields
1251
1306
  * @param {Object} config - Configuration with directory
@@ -62,6 +62,13 @@ class ExternalModuleManager {
62
62
  // ExternalModuleManager) sees resolutions made during install.
63
63
  static _resolutions = new Map();
64
64
 
65
+ // moduleCode → ResolvedModule (from PluginResolver). Populated for registry
66
+ // modules flagged `marketplace-plugin: true`, whose installable skills live
67
+ // outside a single module.yaml directory and must be resolved from
68
+ // .claude-plugin/marketplace.json. Shared across instances so install() can
69
+ // pick up the resolution computed during findExternalModuleSource.
70
+ static _pluginResolutions = new Map();
71
+
65
72
  constructor() {}
66
73
 
67
74
  /**
@@ -73,6 +80,15 @@ class ExternalModuleManager {
73
80
  return ExternalModuleManager._resolutions.get(moduleCode) || null;
74
81
  }
75
82
 
83
+ /**
84
+ * Get the cached marketplace-plugin resolution for a module (if any).
85
+ * @param {string} moduleCode
86
+ * @returns {Object|null} ResolvedModule from PluginResolver, or null
87
+ */
88
+ getPluginResolution(moduleCode) {
89
+ return ExternalModuleManager._pluginResolutions.get(moduleCode) || null;
90
+ }
91
+
76
92
  /**
77
93
  * Load the official modules registry from the bundled YAML file.
78
94
  * @returns {Object} Parsed YAML content with modules array
@@ -113,6 +129,10 @@ class ExternalModuleManager {
113
129
  npmPackage: mod.npm_package || mod.npmPackage || null,
114
130
  pluginName: mod.plugin_name || mod.pluginName || null,
115
131
  defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
132
+ deprecated: mod.deprecated === true,
133
+ deprecationMessage: mod.deprecation_message || mod['deprecation-message'] || mod.deprecationMessage || null,
134
+ marketplacePlugin: mod.marketplace_plugin === true || mod['marketplace-plugin'] === true || mod.marketplacePlugin === true,
135
+ postInstallMessage: mod.post_install_message || mod['post-install-message'] || mod.postInstallMessage || null,
116
136
  builtIn: mod.built_in === true,
117
137
  isExternal: mod.built_in !== true,
118
138
  };
@@ -468,6 +488,19 @@ class ExternalModuleManager {
468
488
  return null;
469
489
  }
470
490
 
491
+ // Marketplace-plugin modules (registry entries flagged `marketplace-plugin`)
492
+ // ship a .claude-plugin/marketplace.json and keep their module.yaml inside a
493
+ // skill's assets/ rather than in one directory alongside the skills. Resolve
494
+ // them through the PluginResolver (the same machinery custom-URL installs
495
+ // use) and return the directory that holds module.yaml so config/version/
496
+ // directory callers work; install() copies the resolved skill dirs.
497
+ if (moduleInfo.marketplacePlugin) {
498
+ const pluginMod = await this.resolvePluginModule(moduleCode, options);
499
+ if (pluginMod && pluginMod.moduleYamlPath) {
500
+ return path.dirname(pluginMod.moduleYamlPath);
501
+ }
502
+ }
503
+
471
504
  // Clone the external module repo
472
505
  const cloneDir = await this.cloneExternalModule(moduleCode, options);
473
506
 
@@ -521,6 +554,84 @@ class ExternalModuleManager {
521
554
  `The repository may have been restructured after this release was tagged.${channelHint}`,
522
555
  );
523
556
  }
557
+
558
+ /**
559
+ * Resolve a marketplace-plugin registry module to an installable plugin
560
+ * definition. Clones the repo (respecting the channel plan), reads its
561
+ * .claude-plugin/marketplace.json, and runs the PluginResolver against the
562
+ * plugin matching this module. The result (skillPaths + module.yaml +
563
+ * module-help.csv) is cached so install() can copy the resolved skill dirs.
564
+ *
565
+ * @param {string} moduleCode - Code of the external module
566
+ * @param {Object} options - Options passed to cloneExternalModule
567
+ * @returns {Promise<Object|null>} ResolvedModule from PluginResolver, or null
568
+ * when the module is not a marketplace plugin or cannot be resolved.
569
+ */
570
+ async resolvePluginModule(moduleCode, options = {}) {
571
+ const moduleInfo = await this.getModuleByCode(moduleCode);
572
+ if (!moduleInfo || moduleInfo.builtIn || !moduleInfo.marketplacePlugin) {
573
+ return null;
574
+ }
575
+
576
+ const cloneDir = await this.cloneExternalModule(moduleCode, options);
577
+
578
+ const marketplacePath = path.join(cloneDir, '.claude-plugin', 'marketplace.json');
579
+ if (!(await fs.pathExists(marketplacePath))) {
580
+ return null;
581
+ }
582
+
583
+ let marketplace;
584
+ try {
585
+ marketplace = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
586
+ } catch {
587
+ return null;
588
+ }
589
+
590
+ const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
591
+ if (plugins.length === 0) {
592
+ return null;
593
+ }
594
+
595
+ // Prefer the plugin whose name matches the registry's plugin_name (or code),
596
+ // falling back to any plugin that declares skills.
597
+ const preferredName = moduleInfo.pluginName || moduleInfo.code;
598
+ const ordered = [...plugins].sort((a, b) => {
599
+ const am = a?.name === preferredName ? 0 : 1;
600
+ const bm = b?.name === preferredName ? 0 : 1;
601
+ return am - bm;
602
+ });
603
+
604
+ const { PluginResolver } = require('./plugin-resolver');
605
+ const resolver = new PluginResolver();
606
+
607
+ for (const plugin of ordered) {
608
+ if (!plugin || !Array.isArray(plugin.skills) || plugin.skills.length === 0) {
609
+ continue;
610
+ }
611
+ let resolvedMods;
612
+ try {
613
+ resolvedMods = await resolver.resolve(cloneDir, plugin);
614
+ } catch {
615
+ continue;
616
+ }
617
+ if (!resolvedMods || resolvedMods.length === 0) {
618
+ continue;
619
+ }
620
+ // Match the registry code, then the preferred plugin name, then accept a
621
+ // lone resolved module.
622
+ const match =
623
+ resolvedMods.find((mod) => mod.code === moduleCode) ||
624
+ resolvedMods.find((mod) => mod.code === preferredName) ||
625
+ (resolvedMods.length === 1 ? resolvedMods[0] : null);
626
+ if (match) {
627
+ match.repoUrl = moduleInfo.url;
628
+ ExternalModuleManager._pluginResolutions.set(moduleCode, match);
629
+ return match;
630
+ }
631
+ }
632
+
633
+ return null;
634
+ }
524
635
  cachedModules = null;
525
636
  }
526
637
 
@@ -277,7 +277,29 @@ class OfficialModules {
277
277
  await fs.remove(targetPath);
278
278
  }
279
279
 
280
- await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
280
+ // Marketplace-plugin registry modules keep their installable skills outside
281
+ // the directory that holds module.yaml (sourcePath points at the -setup
282
+ // skill's assets/), so they cannot be installed by copying sourcePath. Copy
283
+ // the resolved skill directories instead, matching how custom marketplace
284
+ // installs lay out a module. Everything else (manifest, version info) flows
285
+ // through the standard external-module path below.
286
+ const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName);
287
+ if (moduleInfo && moduleInfo.marketplacePlugin) {
288
+ const pluginResolution = this.externalModuleManager.getPluginResolution(moduleName);
289
+ // Fail loud: copying sourcePath here would install only the -setup skill's
290
+ // assets/ (module.yaml + module-help.csv) and none of the skills — a
291
+ // silent, broken partial install. Abort instead.
292
+ if (!pluginResolution || !Array.isArray(pluginResolution.skillPaths) || pluginResolution.skillPaths.length === 0) {
293
+ throw new Error(
294
+ `Module '${moduleName}' is registered as a marketplace plugin but its skills could not be resolved ` +
295
+ `from .claude-plugin/marketplace.json (missing or malformed on the selected channel). ` +
296
+ `Aborting to avoid a partial install with no skills.`,
297
+ );
298
+ }
299
+ await this._copyResolvedSkills(pluginResolution, targetPath, fileTrackingCallback, options.moduleConfig);
300
+ } else {
301
+ await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
302
+ }
281
303
 
282
304
  if (!options.skipModuleInstaller) {
283
305
  await this.createModuleDirectories(moduleName, bmadDir, options);
@@ -304,41 +326,66 @@ class OfficialModules {
304
326
  }
305
327
 
306
328
  /**
307
- * Install a module from a PluginResolver resolution result.
308
- * Copies specific skill directories and places module-help.csv at the target root.
329
+ * Lay out a PluginResolver resolution on disk: copy each resolved skill
330
+ * directory (flattened by leaf name) into targetPath and place module-help.csv
331
+ * at the module root. Shared by both custom marketplace installs
332
+ * (installFromResolution) and official marketplace-plugin registry installs
333
+ * (install), so the two paths cannot drift.
309
334
  * @param {Object} resolved - ResolvedModule from PluginResolver
310
- * @param {string} bmadDir - Target bmad directory
335
+ * @param {string} targetPath - Destination module directory (e.g. bmadDir/<code>)
311
336
  * @param {Function} fileTrackingCallback - Optional callback to track installed files
312
- * @param {Object} options - Installation options
337
+ * @param {Object} moduleConfig - Module configuration passed to copy filtering
313
338
  */
314
- async installFromResolution(resolved, bmadDir, fileTrackingCallback = null, options = {}) {
315
- const targetPath = path.join(bmadDir, resolved.code);
316
-
317
- if (await fs.pathExists(targetPath)) {
318
- await fs.remove(targetPath);
319
- }
320
-
339
+ async _copyResolvedSkills(resolved, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
321
340
  await fs.ensureDir(targetPath);
322
341
 
323
- // Copy each skill directory, flattened by leaf name
342
+ // Copy each skill directory, flattened by leaf name. Leaf names must be
343
+ // unique — two skills that flatten to the same directory would silently
344
+ // overwrite each other, so fail loud instead.
345
+ const seenLeaves = new Map();
324
346
  for (const skillPath of resolved.skillPaths) {
325
347
  const skillDirName = path.basename(skillPath);
348
+ if (seenLeaves.has(skillDirName)) {
349
+ throw new Error(
350
+ `Cannot install module '${resolved.code}': skill directories '${seenLeaves.get(skillDirName)}' and ` +
351
+ `'${skillPath}' share the leaf name '${skillDirName}' and would overwrite each other. ` +
352
+ `Skill directory names must be unique.`,
353
+ );
354
+ }
355
+ seenLeaves.set(skillDirName, skillPath);
326
356
  const skillTarget = path.join(targetPath, skillDirName);
327
- await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, options.moduleConfig);
357
+ await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, moduleConfig);
328
358
  }
329
359
 
330
- // Place module-help.csv at the module root
360
+ // Place module-help.csv at the module root.
361
+ const helpTarget = path.join(targetPath, 'module-help.csv');
331
362
  if (resolved.moduleHelpCsvPath) {
332
- // Strategies 1-4: copy the existing file
333
- const helpTarget = path.join(targetPath, 'module-help.csv');
363
+ // Strategies 1-4: copy the existing file.
334
364
  await fs.copy(resolved.moduleHelpCsvPath, helpTarget, { overwrite: true });
335
365
  if (fileTrackingCallback) fileTrackingCallback(helpTarget);
336
366
  } else if (resolved.synthesizedHelpCsv) {
337
- // Strategy 5: write synthesized content
338
- const helpTarget = path.join(targetPath, 'module-help.csv');
367
+ // Strategy 5: write synthesized content.
339
368
  await fs.writeFile(helpTarget, resolved.synthesizedHelpCsv, 'utf8');
340
369
  if (fileTrackingCallback) fileTrackingCallback(helpTarget);
341
370
  }
371
+ }
372
+
373
+ /**
374
+ * Install a module from a PluginResolver resolution result.
375
+ * Copies specific skill directories and places module-help.csv at the target root.
376
+ * @param {Object} resolved - ResolvedModule from PluginResolver
377
+ * @param {string} bmadDir - Target bmad directory
378
+ * @param {Function} fileTrackingCallback - Optional callback to track installed files
379
+ * @param {Object} options - Installation options
380
+ */
381
+ async installFromResolution(resolved, bmadDir, fileTrackingCallback = null, options = {}) {
382
+ const targetPath = path.join(bmadDir, resolved.code);
383
+
384
+ if (await fs.pathExists(targetPath)) {
385
+ await fs.remove(targetPath);
386
+ }
387
+
388
+ await this._copyResolvedSkills(resolved, targetPath, fileTrackingCallback, options.moduleConfig);
342
389
 
343
390
  // Create directories declared in module.yaml (strategies 1-4 may have these)
344
391
  if (!options.skipModuleInstaller) {
@@ -944,25 +944,32 @@ class UI {
944
944
  }
945
945
  }
946
946
 
947
- // Add external registry modules (skip built-in duplicates)
948
- const externalRegistryModules = registryModules.filter((mod) => !mod.builtIn && !builtInCodes.has(mod.code));
947
+ // Add external registry modules (skip built-in duplicates and deprecated
948
+ // modules that are not already installed — deprecated modules stay visible
949
+ // only so existing users can continue to manage them).
950
+ const externalRegistryModules = registryModules.filter(
951
+ (mod) => !mod.builtIn && !builtInCodes.has(mod.code) && (!mod.deprecated || installedModuleIds.has(mod.code)),
952
+ );
949
953
  let externalRegistryEntries = [];
950
954
  if (externalRegistryModules.length > 0) {
951
955
  const spinner = await prompts.spinner();
952
956
  spinner.start('Checking latest module versions...');
953
957
 
954
958
  externalRegistryEntries = await Promise.all(
955
- externalRegistryModules.map(async (mod) => ({
956
- code: mod.code,
957
- entry: await buildModuleEntry(
959
+ externalRegistryModules.map(async (mod) => {
960
+ const entry = await buildModuleEntry(
958
961
  mod.code,
959
962
  mod.name,
960
963
  mod.description,
961
964
  mod.defaultSelected,
962
965
  mod.url || null,
963
966
  mod.defaultChannel || null,
964
- ),
965
- })),
967
+ );
968
+ if (mod.deprecated && mod.deprecationMessage) {
969
+ entry.hint = entry.hint ? `${entry.hint} — ${mod.deprecationMessage}` : mod.deprecationMessage;
970
+ }
971
+ return { code: mod.code, entry };
972
+ }),
966
973
  );
967
974
 
968
975
  spinner.stop('Checked latest module versions.');