bmad-method 6.5.1-next.0 → 6.5.1-next.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.5.1-next.0",
4
+ "version": "6.5.1-next.2",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -435,6 +435,9 @@ class ManifestGenerator {
435
435
  // this means user-scoped keys (e.g. user_name) could mis-file into the
436
436
  // team config, so the operator should notice.
437
437
  const scopeByModuleKey = {};
438
+ // Maps installer moduleName (may be full display name) → module code field
439
+ // from module.yaml, so TOML sections use [modules.<code>] not [modules.<name>].
440
+ const codeByModuleName = {};
438
441
  for (const moduleName of this.updatedModules) {
439
442
  const moduleYamlPath = await resolveInstalledModuleYaml(moduleName);
440
443
  if (!moduleYamlPath) {
@@ -447,6 +450,7 @@ class ManifestGenerator {
447
450
  try {
448
451
  const parsed = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8'));
449
452
  if (!parsed || typeof parsed !== 'object') continue;
453
+ if (parsed.code) codeByModuleName[moduleName] = parsed.code;
450
454
  scopeByModuleKey[moduleName] = {};
451
455
  for (const [key, value] of Object.entries(parsed)) {
452
456
  if (value && typeof value === 'object' && 'prompt' in value) {
@@ -545,6 +549,9 @@ class ManifestGenerator {
545
549
  if (moduleName === 'core') continue;
546
550
  const cfg = moduleConfigs[moduleName];
547
551
  if (!cfg || Object.keys(cfg).length === 0) continue;
552
+ // Use the module's code field from module.yaml as the TOML key so the
553
+ // section is [modules.mdo] not [modules.MDO: Maxio DevOps Operations].
554
+ const sectionKey = codeByModuleName[moduleName] || moduleName;
548
555
  // Only filter out spread-from-core pollution when we actually know
549
556
  // this module's prompt schema. For external/marketplace modules whose
550
557
  // module.yaml isn't in the src tree, fall through as all-team so we
@@ -552,14 +559,14 @@ class ManifestGenerator {
552
559
  const haveSchema = Object.keys(scopeByModuleKey[moduleName] || {}).length > 0;
553
560
  const { team: modTeam, user: modUser } = partition(moduleName, cfg, haveSchema);
554
561
  if (Object.keys(modTeam).length > 0) {
555
- teamLines.push(`[modules.${moduleName}]`);
562
+ teamLines.push(`[modules.${sectionKey}]`);
556
563
  for (const [key, value] of Object.entries(modTeam)) {
557
564
  teamLines.push(`${key} = ${formatTomlValue(value)}`);
558
565
  }
559
566
  teamLines.push('');
560
567
  }
561
568
  if (Object.keys(modUser).length > 0) {
562
- userLines.push(`[modules.${moduleName}]`);
569
+ userLines.push(`[modules.${sectionKey}]`);
563
570
  for (const [key, value] of Object.entries(modUser)) {
564
571
  userLines.push(`${key} = ${formatTomlValue(value)}`);
565
572
  }
@@ -86,6 +86,8 @@ function getExternalModuleCachePath(moduleName, ...segments) {
86
86
  * Built-in modules (core, bmm) live under <src>. External official modules are
87
87
  * cloned into ~/.bmad/cache/external-modules/<name>/ with varying internal
88
88
  * layouts (some at src/module.yaml, some at skills/module.yaml, some nested).
89
+ * Local custom-source modules are not cached; their path is read from the
90
+ * CustomModuleManager resolution cache set during the same install run.
89
91
  * This mirrors the candidate-path search in
90
92
  * ExternalModuleManager.findExternalModuleSource but performs no git/network
91
93
  * work, which keeps it safe to call during manifest writing.
@@ -97,26 +99,56 @@ async function resolveInstalledModuleYaml(moduleName) {
97
99
  const builtIn = path.join(getModulePath(moduleName), 'module.yaml');
98
100
  if (await fs.pathExists(builtIn)) return builtIn;
99
101
 
100
- const cacheRoot = getExternalModuleCachePath(moduleName);
101
- if (!(await fs.pathExists(cacheRoot))) return null;
102
-
103
- for (const dir of ['skills', 'src']) {
104
- const direct = path.join(cacheRoot, dir, 'module.yaml');
105
- if (await fs.pathExists(direct)) return direct;
106
-
107
- const dirPath = path.join(cacheRoot, dir);
108
- if (await fs.pathExists(dirPath)) {
109
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
110
- for (const entry of entries) {
111
- if (!entry.isDirectory()) continue;
112
- const nested = path.join(dirPath, entry.name, 'module.yaml');
113
- if (await fs.pathExists(nested)) return nested;
102
+ // Search a resolved root directory using the same candidate-path pattern.
103
+ async function searchRoot(root) {
104
+ for (const dir of ['skills', 'src']) {
105
+ const direct = path.join(root, dir, 'module.yaml');
106
+ if (await fs.pathExists(direct)) return direct;
107
+
108
+ const dirPath = path.join(root, dir);
109
+ if (await fs.pathExists(dirPath)) {
110
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (!entry.isDirectory()) continue;
113
+ const nested = path.join(dirPath, entry.name, 'module.yaml');
114
+ if (await fs.pathExists(nested)) return nested;
115
+ }
114
116
  }
115
117
  }
118
+
119
+ // BMB standard: {setup-skill}/assets/module.yaml (setup skill is any *-setup directory)
120
+ const rootEntries = await fs.readdir(root, { withFileTypes: true });
121
+ for (const entry of rootEntries) {
122
+ if (!entry.isDirectory() || !entry.name.endsWith('-setup')) continue;
123
+ const setupAssets = path.join(root, entry.name, 'assets', 'module.yaml');
124
+ if (await fs.pathExists(setupAssets)) return setupAssets;
125
+ }
126
+
127
+ const atRoot = path.join(root, 'module.yaml');
128
+ if (await fs.pathExists(atRoot)) return atRoot;
129
+ return null;
116
130
  }
117
131
 
118
- const atRoot = path.join(cacheRoot, 'module.yaml');
119
- if (await fs.pathExists(atRoot)) return atRoot;
132
+ const cacheRoot = getExternalModuleCachePath(moduleName);
133
+ if (await fs.pathExists(cacheRoot)) {
134
+ const found = await searchRoot(cacheRoot);
135
+ if (found) return found;
136
+ }
137
+
138
+ // Fallback: local custom-source modules store their source path in the
139
+ // CustomModuleManager resolution cache populated during the same install run.
140
+ // Match by code OR name since callers may use either form.
141
+ try {
142
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
143
+ for (const [, mod] of CustomModuleManager._resolutionCache) {
144
+ if ((mod.code === moduleName || mod.name === moduleName) && mod.localPath) {
145
+ const found = await searchRoot(mod.localPath);
146
+ if (found) return found;
147
+ }
148
+ }
149
+ } catch {
150
+ // Resolution cache unavailable — continue
151
+ }
120
152
 
121
153
  return null;
122
154
  }
@@ -2,6 +2,7 @@ const path = require('node:path');
2
2
  const os = require('node:os');
3
3
  const semver = require('semver');
4
4
  const fs = require('./fs-native');
5
+ const installerPackageJson = require('../../package.json');
5
6
  const { CLIUtils } = require('./cli-utils');
6
7
  const { ExternalModuleManager } = require('./modules/external-manager');
7
8
  const { resolveModuleVersion } = require('./modules/version-resolver');
@@ -128,6 +129,24 @@ class UI {
128
129
  await prompts.log.warn(warning);
129
130
  }
130
131
 
132
+ // When the user launched the installer from a prerelease (npx bmad-method@next),
133
+ // mirror that intent for external modules: seed the global channel to 'next' so
134
+ // the module picker's version labels resolve from main HEAD (matching what
135
+ // actually gets installed) and the interactive channel gate skips — the user
136
+ // already declared "next" intent by typing @next. Explicit channel flags
137
+ // override this seed.
138
+ if (
139
+ semver.prerelease(installerPackageJson.version) !== null &&
140
+ !channelOptions.global &&
141
+ channelOptions.nextSet.size === 0 &&
142
+ channelOptions.pins.size === 0
143
+ ) {
144
+ channelOptions.global = 'next';
145
+ await prompts.log.info(
146
+ 'Launched from a prerelease — installing all external modules from main HEAD (next channel). Pass --all-stable or --pin to override.',
147
+ );
148
+ }
149
+
131
150
  // Get directory from options or prompt
132
151
  let confirmedDirectory;
133
152
  if (options.directory) {
@@ -332,8 +351,10 @@ class UI {
332
351
 
333
352
  // Interactive channel gate: "Ready to install (all stable)? [Y/n]"
334
353
  // Only shown for fresh installs with no channel flags and an external module
335
- // selected. Non-interactive installs skip this and fall through to the
336
- // registry default (stable) or whatever flags were supplied.
354
+ // selected. Skipped for prerelease launches because channelOptions.global
355
+ // was already seeded to 'next' upstream. Non-interactive installs skip this
356
+ // and fall through to the registry default (stable) or whatever flags were
357
+ // supplied.
337
358
  await this._interactiveChannelGate({ options, channelOptions, selectedModules });
338
359
 
339
360
  let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@@ -1783,7 +1804,9 @@ class UI {
1783
1804
  *
1784
1805
  * Skipped when:
1785
1806
  * - running non-interactively (--yes)
1786
- * - the user already passed channel flags (--channel / --pin / --next)
1807
+ * - the user already passed channel flags (--channel / --pin / --next), OR
1808
+ * the installer was launched from a prerelease (which seeds
1809
+ * channelOptions.global = 'next' upstream in promptInstall)
1787
1810
  * - no externals/community modules are selected
1788
1811
  *
1789
1812
  * Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices.