bmad-method 6.5.1-next.1 → 6.5.1-next.3

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.1",
4
+ "version": "6.5.1-next.3",
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
  }
@@ -1,5 +1,6 @@
1
1
  const path = require('node:path');
2
2
  const os = require('node:os');
3
+ const yaml = require('yaml');
3
4
  const fs = require('./fs-native');
4
5
 
5
6
  /**
@@ -86,6 +87,11 @@ function getExternalModuleCachePath(moduleName, ...segments) {
86
87
  * Built-in modules (core, bmm) live under <src>. External official modules are
87
88
  * cloned into ~/.bmad/cache/external-modules/<name>/ with varying internal
88
89
  * layouts (some at src/module.yaml, some at skills/module.yaml, some nested).
90
+ * Url-source custom modules are cloned into ~/.bmad/cache/custom-modules/<host>/<owner>/<repo>/
91
+ * and are resolved by walking the cache and matching `code` or `name` from the
92
+ * discovered module.yaml. Local custom-source modules are not cached; their
93
+ * path is read from the CustomModuleManager resolution cache set during the
94
+ * same install run.
89
95
  * This mirrors the candidate-path search in
90
96
  * ExternalModuleManager.findExternalModuleSource but performs no git/network
91
97
  * work, which keeps it safe to call during manifest writing.
@@ -97,26 +103,97 @@ async function resolveInstalledModuleYaml(moduleName) {
97
103
  const builtIn = path.join(getModulePath(moduleName), 'module.yaml');
98
104
  if (await fs.pathExists(builtIn)) return builtIn;
99
105
 
106
+ // Collect every module.yaml under a root using the standard candidate paths.
107
+ // Url-source repos can host multiple plugins (discovery mode), so we need all
108
+ // matches, not just the first. Returned in priority order.
109
+ async function searchRootAll(root) {
110
+ const results = [];
111
+ for (const dir of ['skills', 'src']) {
112
+ const direct = path.join(root, dir, 'module.yaml');
113
+ if (await fs.pathExists(direct)) results.push(direct);
114
+
115
+ const dirPath = path.join(root, dir);
116
+ if (await fs.pathExists(dirPath)) {
117
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
118
+ for (const entry of entries) {
119
+ if (!entry.isDirectory()) continue;
120
+ const nested = path.join(dirPath, entry.name, 'module.yaml');
121
+ if (await fs.pathExists(nested)) results.push(nested);
122
+ }
123
+ }
124
+ }
125
+
126
+ // BMB standard: {setup-skill}/assets/module.yaml (setup skill is any *-setup directory)
127
+ const rootEntries = await fs.readdir(root, { withFileTypes: true });
128
+ for (const entry of rootEntries) {
129
+ if (!entry.isDirectory() || !entry.name.endsWith('-setup')) continue;
130
+ const setupAssets = path.join(root, entry.name, 'assets', 'module.yaml');
131
+ if (await fs.pathExists(setupAssets)) results.push(setupAssets);
132
+ }
133
+
134
+ const atRoot = path.join(root, 'module.yaml');
135
+ if (await fs.pathExists(atRoot)) results.push(atRoot);
136
+ return results;
137
+ }
138
+
139
+ // Backwards-compatible single-result variant for the existing external-cache
140
+ // and resolution-cache fallbacks (one module per root by construction).
141
+ async function searchRoot(root) {
142
+ const all = await searchRootAll(root);
143
+ return all.length > 0 ? all[0] : null;
144
+ }
145
+
100
146
  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;
147
+ if (await fs.pathExists(cacheRoot)) {
148
+ const found = await searchRoot(cacheRoot);
149
+ if (found) return found;
150
+ }
151
+
152
+ // Fallback: local custom-source modules store their source path in the
153
+ // CustomModuleManager resolution cache populated during the same install run.
154
+ // Match by code OR name since callers may use either form.
155
+ try {
156
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
157
+ for (const [, mod] of CustomModuleManager._resolutionCache) {
158
+ if ((mod.code === moduleName || mod.name === moduleName) && mod.localPath) {
159
+ const found = await searchRoot(mod.localPath);
160
+ if (found) return found;
114
161
  }
115
162
  }
163
+ } catch {
164
+ // Resolution cache unavailable — continue
116
165
  }
117
166
 
118
- const atRoot = path.join(cacheRoot, 'module.yaml');
119
- if (await fs.pathExists(atRoot)) return atRoot;
167
+ // Fallback: url-source custom modules cloned to ~/.bmad/cache/custom-modules/.
168
+ // Walk every cached repo, enumerate ALL module.yaml files via searchRootAll
169
+ // (a single repo can host multiple plugins in discovery mode), and match by
170
+ // the yaml's `code` or `name` field. This works on re-install runs where
171
+ // _resolutionCache is empty and covers both discovery-mode (with marketplace.json)
172
+ // and direct-mode modules, since we identify repo roots by .bmad-source.json
173
+ // (written by cloneRepo) or .claude-plugin/ rather than by marketplace.json.
174
+ try {
175
+ const customCacheDir = path.join(os.homedir(), '.bmad', 'cache', 'custom-modules');
176
+ if (await fs.pathExists(customCacheDir)) {
177
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
178
+ const customMgr = new CustomModuleManager();
179
+ const repoRoots = await customMgr._findCacheRepoRoots(customCacheDir);
180
+ for (const { repoPath } of repoRoots) {
181
+ const candidates = await searchRootAll(repoPath);
182
+ for (const candidate of candidates) {
183
+ try {
184
+ const parsed = yaml.parse(await fs.readFile(candidate, 'utf8'));
185
+ if (parsed && (parsed.code === moduleName || parsed.name === moduleName)) {
186
+ return candidate;
187
+ }
188
+ } catch {
189
+ // Malformed yaml — skip
190
+ }
191
+ }
192
+ }
193
+ }
194
+ } catch {
195
+ // Custom-modules cache walk failed — continue
196
+ }
120
197
 
121
198
  return null;
122
199
  }