bmad-method 6.6.1-next.9 → 6.7.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/{tools/installer/modules/registry-fallback.yaml → bmad-modules.yaml} +29 -15
- package/package.json +1 -1
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +18 -11
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +13 -8
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +54 -57
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-template.md +40 -30
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md +126 -21
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/validation-report-template.html +193 -58
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +47 -13
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +27 -12
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +97 -0
- package/src/bmm-skills/module.yaml +2 -2
- package/src/core-skills/module.yaml +1 -1
- package/tools/installer/core/installer.js +1 -22
- package/tools/installer/core/manifest.js +0 -22
- package/tools/installer/modules/channel-plan.js +1 -1
- package/tools/installer/modules/external-manager.js +9 -27
- package/tools/installer/modules/official-modules.js +9 -48
- package/tools/installer/ui.js +12 -196
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/facilitation-guide.md +0 -79
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validation-render.md +0 -58
- package/src/bmm-skills/2-plan-workflows/bmad-prd/scripts/render-validation-html.py +0 -290
- package/tools/installer/modules/community-manager.js +0 -704
- package/tools/installer/modules/registry-client.js +0 -187
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Validate
|
|
2
|
+
|
|
3
|
+
The Validate intent playbook. Standalone — this intent critiques an existing PRD without changing it and ends after the user has seen the report; it does not run Finalize. The synthesis pipeline below is also reused for mid-session report requests during Create/Update.
|
|
4
|
+
|
|
5
|
+
## Orient
|
|
6
|
+
|
|
7
|
+
Source-extract against `.decision-log.md`, any original inputs, and the PRD/addendum themselves. Delegate to subagents per PRD Discipline → "Extract, don't ingest" (in SKILL.md); the parent assembles from extracts.
|
|
8
|
+
|
|
9
|
+
## Run the Reviewer Gate
|
|
10
|
+
|
|
11
|
+
Run the Reviewer Gate (see SKILL.md) against `prd.md` (and `addendum.md` if present). The rubric walker is the default entry in the gate menu; under Validate intent it additionally runs the synthesis pipeline below. The Finalize discipline pass during Create/Update does NOT render a report — findings stay in-conversation.
|
|
12
|
+
|
|
13
|
+
## Rubric-walker pipeline
|
|
14
|
+
|
|
15
|
+
The rubric walker is the primary review entry. Spawn it as a subagent with this prompt:
|
|
16
|
+
|
|
17
|
+
> You are validating a PRD against the quality rubric at `{workflow.validation_checklist_template}`. Read the full rubric first, then read `prd.md` (and `addendum.md` if present). Form a judgment per dimension — *strong / adequate / thin / broken* — and write findings only where they add information. Cite specific PRD locations and quote phrases. Severity ranks impact on the PRD's usefulness, not how easy the fix is. Write your review to `{doc_workspace}/review-rubric.md` in the format the rubric specifies. Return ONLY a compact summary (overall verdict, dimension verdicts, finding counts by severity, file path).
|
|
18
|
+
|
|
19
|
+
The Reviewer Gate may also dispatch additional reviewers from `{workflow.finalize_reviewers}` (adversarial-general by default) and any ad-hoc reviewers the parent judges warranted. Each writes its review to `{doc_workspace}/review-{slug}.md` and returns a compact summary. Run in parallel.
|
|
20
|
+
|
|
21
|
+
## Synthesis pipeline
|
|
22
|
+
|
|
23
|
+
Once every selected reviewer has returned, the parent synthesizes one consolidated report. **Do not skip this step under Validate intent** — it produces the persistent artifact the user opens.
|
|
24
|
+
|
|
25
|
+
### Inputs
|
|
26
|
+
|
|
27
|
+
- `{doc_workspace}/review-rubric.md` — primary, structured by the seven dimensions
|
|
28
|
+
- Zero or more `{doc_workspace}/review-{slug}.md` files — extra reviewers (adversarial, etc.)
|
|
29
|
+
- `{workflow.validation_report_template}` — the HTML skeleton
|
|
30
|
+
|
|
31
|
+
### What the synthesis pass does
|
|
32
|
+
|
|
33
|
+
1. Read every reviewer file in `{doc_workspace}/review-*.md`.
|
|
34
|
+
2. Fill the HTML skeleton:
|
|
35
|
+
- **Header.** PRD name, path. Grade derived from the rubric verdicts and severity counts: *Excellent* = all dimensions strong/adequate, no high/critical findings · *Good* = ≤1 thin dimension, no critical findings · *Fair* = multiple thin dimensions or any high finding · *Poor* = any broken dimension or any critical finding. Set the matching `grade-excellent | grade-good | grade-fair | grade-poor` class.
|
|
36
|
+
- **Synthesis block.** Lift the rubric's *Overall verdict* paragraph as the lead; if adversarial or ad-hoc reviewers materially shift the picture, add a second paragraph that names what they surfaced.
|
|
37
|
+
- **Dimension summary cards.** One per dimension that was assessed. Colored verdict text. Skip dimensions the rubric marked n/a for this PRD (e.g. downstream usability for a standalone PRD).
|
|
38
|
+
- **Dimension sections.** One `<section class="dimension">` per assessed dimension, in rubric order. `<details open>` for *thin* and *broken*; closed for *strong* and *adequate*. Each contains the dimension judgment (the prose from review-rubric.md) and the findings list.
|
|
39
|
+
- **Reviewer sections.** One `<section class="reviewer-section">` per extra reviewer that ran. The source file path goes in the `<span class="reviewer-source">`. Closed by default. Adversarial findings keep their adversarial voice — do not soften.
|
|
40
|
+
- **Mechanical notes.** Bullet list from the rubric's "Mechanical notes" section. Skip the block if empty.
|
|
41
|
+
- **Footer.** Rubric path, ISO timestamp.
|
|
42
|
+
3. Write the filled HTML to `{doc_workspace}/validation-report.html`.
|
|
43
|
+
4. Write the markdown twin to `{doc_workspace}/validation-report.md` (same content, grouped by severity rather than by dimension — see format below; this is the canonical form for downstream re-reading).
|
|
44
|
+
5. Open the HTML in the default browser:
|
|
45
|
+
```bash
|
|
46
|
+
python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('{doc_workspace}/validation-report.html').resolve().as_uri())"
|
|
47
|
+
```
|
|
48
|
+
Skip the open step in headless mode (see `references/headless.md`).
|
|
49
|
+
|
|
50
|
+
### Markdown twin format
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
# Validation Report — {prd_name}
|
|
54
|
+
|
|
55
|
+
- **PRD:** `{prd_path}`
|
|
56
|
+
- **Rubric:** `{rubric_path}`
|
|
57
|
+
- **Run at:** {ISO timestamp}
|
|
58
|
+
- **Grade:** {Excellent | Good | Fair | Poor}
|
|
59
|
+
|
|
60
|
+
## Overall verdict
|
|
61
|
+
{synthesis paragraphs}
|
|
62
|
+
|
|
63
|
+
## Dimension verdicts
|
|
64
|
+
- Decision-readiness — {verdict}
|
|
65
|
+
- Substance over theater — {verdict}
|
|
66
|
+
- (etc. for each assessed dimension)
|
|
67
|
+
|
|
68
|
+
## Findings by severity
|
|
69
|
+
|
|
70
|
+
### Critical (n)
|
|
71
|
+
**[Dimension or Reviewer]** — Title (§ location)
|
|
72
|
+
{Note}
|
|
73
|
+
Fix: {suggested fix}
|
|
74
|
+
|
|
75
|
+
### High (n)
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
### Medium (n)
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
### Low (n)
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
## Mechanical notes
|
|
85
|
+
- {bullet}
|
|
86
|
+
|
|
87
|
+
## Reviewer files
|
|
88
|
+
- `review-rubric.md`
|
|
89
|
+
- `review-adversarial-general.md` (if present)
|
|
90
|
+
- (etc.)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Re-running validation overwrites the consolidated report in place. The individual `review-*.md` files are preserved so the user can drill in.
|
|
94
|
+
|
|
95
|
+
## Close
|
|
96
|
+
|
|
97
|
+
Surface artifact paths; the rendered HTML/markdown is the persistent artifact. Always offer to roll findings into an Update.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
code: bmm
|
|
2
|
-
name: "BMad Method
|
|
3
|
-
description: "
|
|
2
|
+
name: "BMad Method"
|
|
3
|
+
description: "Full-lifecycle AI agile development: analysis, planning, architecture, implementation"
|
|
4
4
|
default_selected: true # This module will be selected by default for new installations
|
|
5
5
|
|
|
6
6
|
# Variables from Core Config inserted:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
code: core
|
|
2
2
|
name: "BMad Core Module"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Shared utilities across modules"
|
|
4
4
|
|
|
5
5
|
header: "BMad Core Configuration"
|
|
6
6
|
subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents."
|
|
@@ -640,13 +640,7 @@ class Installer {
|
|
|
640
640
|
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
|
641
641
|
const displayName = moduleInfo?.name || moduleName;
|
|
642
642
|
|
|
643
|
-
const
|
|
644
|
-
let communityResolution = null;
|
|
645
|
-
if (!externalResolution) {
|
|
646
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
647
|
-
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
648
|
-
}
|
|
649
|
-
const resolution = externalResolution || communityResolution;
|
|
643
|
+
const resolution = officialModules.externalModuleManager.getResolution(moduleName);
|
|
650
644
|
const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
|
|
651
645
|
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
652
646
|
moduleSourcePath: sourcePath,
|
|
@@ -1178,21 +1172,6 @@ class Installer {
|
|
|
1178
1172
|
}
|
|
1179
1173
|
}
|
|
1180
1174
|
|
|
1181
|
-
// Add installed community modules to available modules
|
|
1182
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
1183
|
-
const communityMgr = new CommunityModuleManager();
|
|
1184
|
-
const communityModules = await communityMgr.listAll();
|
|
1185
|
-
for (const communityModule of communityModules) {
|
|
1186
|
-
if (installedModules.includes(communityModule.code) && !availableModules.some((m) => m.id === communityModule.code)) {
|
|
1187
|
-
availableModules.push({
|
|
1188
|
-
id: communityModule.code,
|
|
1189
|
-
name: communityModule.displayName,
|
|
1190
|
-
isExternal: true,
|
|
1191
|
-
fromCommunity: true,
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
1175
|
// Add installed custom modules to available modules
|
|
1197
1176
|
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
|
1198
1177
|
const customMgr = new CustomModuleManager();
|
|
@@ -310,28 +310,6 @@ class Manifest {
|
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
// Check if this is a community module
|
|
314
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
315
|
-
const communityMgr = new CommunityModuleManager();
|
|
316
|
-
const communityInfo = await communityMgr.getModuleByCode(moduleName);
|
|
317
|
-
if (communityInfo) {
|
|
318
|
-
const communityResolution = communityMgr.getResolution(moduleName);
|
|
319
|
-
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
320
|
-
moduleSourcePath,
|
|
321
|
-
fallbackVersion: communityInfo.version,
|
|
322
|
-
});
|
|
323
|
-
return {
|
|
324
|
-
version: communityResolution?.version || versionInfo.version || communityInfo.version,
|
|
325
|
-
source: 'community',
|
|
326
|
-
npmPackage: communityInfo.npmPackage || null,
|
|
327
|
-
repoUrl: communityInfo.url || null,
|
|
328
|
-
channel: communityResolution?.channel || null,
|
|
329
|
-
sha: communityResolution?.sha || null,
|
|
330
|
-
registryApprovedTag: communityResolution?.registryApprovedTag || null,
|
|
331
|
-
registryApprovedSha: communityResolution?.registryApprovedSha || null,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
313
|
// Check if this is a custom module (from user-provided URL or local path)
|
|
336
314
|
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
|
337
315
|
const customMgr = new CustomModuleManager();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* We build the plan from:
|
|
8
8
|
* 1. CLI flags (--channel / --all-* / --next=CODE / --pin CODE=TAG)
|
|
9
9
|
* 2. Interactive answers (the "all stable?" gate + per-module picker)
|
|
10
|
-
* 3. Registry defaults (default_channel from
|
|
10
|
+
* 3. Registry defaults (default_channel from bmad-modules.yaml)
|
|
11
11
|
* 4. Hardcoded fallback 'stable'
|
|
12
12
|
*
|
|
13
13
|
* Precedence: --pin > --next=CODE > --channel (global) > registry default > 'stable'.
|
|
@@ -4,9 +4,9 @@ const path = require('node:path');
|
|
|
4
4
|
const { execSync } = require('node:child_process');
|
|
5
5
|
const yaml = require('yaml');
|
|
6
6
|
const prompts = require('../prompts');
|
|
7
|
-
const { RegistryClient } = require('./registry-client');
|
|
8
7
|
const { resolveChannel, tagExists, parseGitHubRepo } = require('./channel-resolver');
|
|
9
8
|
const { decideChannelForModule } = require('./channel-plan');
|
|
9
|
+
const { getProjectRoot } = require('../project-root');
|
|
10
10
|
|
|
11
11
|
const VALID_CHANNELS = new Set(['stable', 'next', 'pinned']);
|
|
12
12
|
|
|
@@ -46,15 +46,12 @@ async function writeChannelMarker(markerPath, data) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
50
|
-
const MARKETPLACE_REPO = 'bmad-plugins-marketplace';
|
|
51
|
-
const MARKETPLACE_REF = 'main';
|
|
52
|
-
const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
|
|
49
|
+
const REGISTRY_CONFIG_PATH = path.join(getProjectRoot(), 'bmad-modules.yaml');
|
|
53
50
|
|
|
54
51
|
/**
|
|
55
|
-
* Manages official modules from the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
52
|
+
* Manages official modules from the bundled registry file. The remote
|
|
53
|
+
* marketplace fetch has been retired; this repo is the single source of truth
|
|
54
|
+
* for which official modules exist and how they are displayed.
|
|
58
55
|
*
|
|
59
56
|
* @class ExternalModuleManager
|
|
60
57
|
*/
|
|
@@ -65,9 +62,7 @@ class ExternalModuleManager {
|
|
|
65
62
|
// ExternalModuleManager) sees resolutions made during install.
|
|
66
63
|
static _resolutions = new Map();
|
|
67
64
|
|
|
68
|
-
constructor() {
|
|
69
|
-
this._client = new RegistryClient();
|
|
70
|
-
}
|
|
65
|
+
constructor() {}
|
|
71
66
|
|
|
72
67
|
/**
|
|
73
68
|
* Get the most recent channel resolution for a module (if any).
|
|
@@ -79,8 +74,7 @@ class ExternalModuleManager {
|
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
/**
|
|
82
|
-
* Load the official modules registry from
|
|
83
|
-
* bundled YAML file if the fetch fails.
|
|
77
|
+
* Load the official modules registry from the bundled YAML file.
|
|
84
78
|
* @returns {Object} Parsed YAML content with modules array
|
|
85
79
|
*/
|
|
86
80
|
async loadExternalModulesConfig() {
|
|
@@ -88,23 +82,10 @@ class ExternalModuleManager {
|
|
|
88
82
|
return this.cachedModules;
|
|
89
83
|
}
|
|
90
84
|
|
|
91
|
-
// Try remote registry first
|
|
92
|
-
try {
|
|
93
|
-
const config = await this._client.fetchGitHubYaml(MARKETPLACE_OWNER, MARKETPLACE_REPO, 'registry/official.yaml', MARKETPLACE_REF);
|
|
94
|
-
if (config?.modules?.length) {
|
|
95
|
-
this.cachedModules = config;
|
|
96
|
-
return config;
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Fall through to local fallback
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Fallback to bundled file
|
|
103
85
|
try {
|
|
104
|
-
const content = await fs.readFile(
|
|
86
|
+
const content = await fs.readFile(REGISTRY_CONFIG_PATH, 'utf8');
|
|
105
87
|
const config = yaml.parse(content);
|
|
106
88
|
this.cachedModules = config;
|
|
107
|
-
await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
|
|
108
89
|
return config;
|
|
109
90
|
} catch (error) {
|
|
110
91
|
await prompts.log.warn(`Failed to load modules config: ${error.message}`);
|
|
@@ -130,6 +111,7 @@ class ExternalModuleManager {
|
|
|
130
111
|
defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
|
|
131
112
|
type: mod.type || 'bmad-org',
|
|
132
113
|
npmPackage: mod.npm_package || mod.npmPackage || null,
|
|
114
|
+
pluginName: mod.plugin_name || mod.pluginName || null,
|
|
133
115
|
defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
|
|
134
116
|
builtIn: mod.built_in === true,
|
|
135
117
|
isExternal: mod.built_in !== true,
|
|
@@ -231,14 +231,6 @@ class OfficialModules {
|
|
|
231
231
|
return externalSource;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// Check community modules (pass channelOptions for --next/--pin overrides)
|
|
235
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
236
|
-
const communityMgr = new CommunityModuleManager();
|
|
237
|
-
const communitySource = await communityMgr.findModuleSource(moduleCode, options);
|
|
238
|
-
if (communitySource) {
|
|
239
|
-
return communitySource;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
234
|
// Check custom modules (from user-provided URLs, already cloned to cache)
|
|
243
235
|
const { CustomModuleManager } = require('./custom-module-manager');
|
|
244
236
|
const customMgr = new CustomModuleManager();
|
|
@@ -269,21 +261,6 @@ class OfficialModules {
|
|
|
269
261
|
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
|
270
262
|
}
|
|
271
263
|
|
|
272
|
-
// Community modules whose cloned repo ships marketplace.json get the same
|
|
273
|
-
// skill-level install treatment as custom-source installs. If the in-process
|
|
274
|
-
// cache wasn't populated (e.g. caller skipped the pre-clone phase), fall
|
|
275
|
-
// back to resolving directly from `~/.bmad/cache/community-modules/<name>/`
|
|
276
|
-
// so we don't silently regress to the legacy half-install path.
|
|
277
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
278
|
-
const communityMgr = new CommunityModuleManager();
|
|
279
|
-
let communityResolved = communityMgr.getPluginResolution(moduleName);
|
|
280
|
-
if (!communityResolved) {
|
|
281
|
-
communityResolved = await communityMgr.resolveFromCache(moduleName);
|
|
282
|
-
}
|
|
283
|
-
if (communityResolved) {
|
|
284
|
-
return this.installFromResolution(communityResolved, bmadDir, fileTrackingCallback, options);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
264
|
const sourcePath = await this.findModuleSource(moduleName, {
|
|
288
265
|
silent: options.silent,
|
|
289
266
|
channelOptions: options.channelOptions,
|
|
@@ -310,14 +287,9 @@ class OfficialModules {
|
|
|
310
287
|
const manifestObj = new Manifest();
|
|
311
288
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
312
289
|
|
|
313
|
-
// Pick up channel resolution recorded by
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (!externalResolution) {
|
|
317
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
318
|
-
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
319
|
-
}
|
|
320
|
-
const resolution = externalResolution || communityResolution;
|
|
290
|
+
// Pick up channel resolution recorded by the external manager (the only
|
|
291
|
+
// manager that does pre-clone resolution now that community is retired).
|
|
292
|
+
const resolution = this.externalModuleManager.getResolution(moduleName);
|
|
321
293
|
|
|
322
294
|
await manifestObj.addModule(bmadDir, moduleName, {
|
|
323
295
|
version: resolution?.version || versionInfo.version,
|
|
@@ -326,8 +298,6 @@ class OfficialModules {
|
|
|
326
298
|
repoUrl: versionInfo.repoUrl,
|
|
327
299
|
channel: resolution?.channel,
|
|
328
300
|
sha: resolution?.sha,
|
|
329
|
-
registryApprovedTag: communityResolution?.registryApprovedTag,
|
|
330
|
-
registryApprovedSha: communityResolution?.registryApprovedSha,
|
|
331
301
|
});
|
|
332
302
|
|
|
333
303
|
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
@@ -375,27 +345,19 @@ class OfficialModules {
|
|
|
375
345
|
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
|
376
346
|
}
|
|
377
347
|
|
|
378
|
-
// Update manifest. For
|
|
379
|
-
// CommunityModuleManager (stable/next/pinned) and propagate the registry's
|
|
380
|
-
// approved tag/sha. For custom-source installs we derive channel from the
|
|
348
|
+
// Update manifest. For custom-source installs we derive channel from the
|
|
381
349
|
// cloneRef (present → pinned, absent → next; local paths have no channel).
|
|
382
350
|
const { Manifest } = require('../core/manifest');
|
|
383
351
|
const manifestObj = new Manifest();
|
|
384
352
|
|
|
385
353
|
const hasGitClone = !!resolved.repoUrl;
|
|
386
|
-
const isCommunity = resolved.communitySource === true;
|
|
387
354
|
const manifestEntry = {
|
|
388
|
-
version: resolved.
|
|
389
|
-
source:
|
|
355
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
|
|
356
|
+
source: 'custom',
|
|
390
357
|
npmPackage: null,
|
|
391
358
|
repoUrl: resolved.repoUrl || null,
|
|
392
359
|
};
|
|
393
|
-
if (
|
|
394
|
-
if (resolved.communityChannel) manifestEntry.channel = resolved.communityChannel;
|
|
395
|
-
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
396
|
-
if (resolved.registryApprovedTag) manifestEntry.registryApprovedTag = resolved.registryApprovedTag;
|
|
397
|
-
if (resolved.registryApprovedSha) manifestEntry.registryApprovedSha = resolved.registryApprovedSha;
|
|
398
|
-
} else if (hasGitClone) {
|
|
360
|
+
if (hasGitClone) {
|
|
399
361
|
manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
|
|
400
362
|
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
401
363
|
if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
|
|
@@ -408,11 +370,10 @@ class OfficialModules {
|
|
|
408
370
|
module: resolved.code,
|
|
409
371
|
path: targetPath,
|
|
410
372
|
// Mirror the manifestEntry.version precedence above so downstream summary
|
|
411
|
-
// lines show the same string we just wrote to disk (
|
|
412
|
-
// use the registry-approved tag via `communityVersion`; custom git-backed
|
|
373
|
+
// lines show the same string we just wrote to disk (custom git-backed
|
|
413
374
|
// installs show the cloned ref or 'main').
|
|
414
375
|
versionInfo: {
|
|
415
|
-
version: resolved.
|
|
376
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || ''),
|
|
416
377
|
},
|
|
417
378
|
};
|
|
418
379
|
}
|
package/tools/installer/ui.js
CHANGED
|
@@ -818,32 +818,18 @@ class UI {
|
|
|
818
818
|
// Phase 1: Official modules
|
|
819
819
|
const officialSelected = await this._selectOfficialModules(installedModuleIds, installedModuleVersions, channelOptions);
|
|
820
820
|
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
|
|
821
|
+
// Identify installed modules that aren't official (previously installed
|
|
822
|
+
// community modules or custom-source modules). Preserve them on update;
|
|
823
|
+
// they can be managed via --custom-source, uninstall, or a dedicated installer.
|
|
824
824
|
const externalManager = new ExternalModuleManager();
|
|
825
825
|
const registryModules = await externalManager.listAvailable();
|
|
826
826
|
const officialRegistryCodes = new Set(['core', 'bmm', ...registryModules.map((m) => m.code)]);
|
|
827
827
|
const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
|
|
828
828
|
|
|
829
|
-
// Phase 2:
|
|
830
|
-
// Returns { codes, didBrowse } so we know if the user entered the flow
|
|
831
|
-
const communityResult = await this._browseCommunityModules(installedModuleIds);
|
|
832
|
-
|
|
833
|
-
// Phase 3: Custom URL modules
|
|
829
|
+
// Phase 2: Custom URL modules
|
|
834
830
|
const customSelected = await this._addCustomUrlModules(installedModuleIds);
|
|
835
831
|
|
|
836
|
-
|
|
837
|
-
const allSelected = new Set([...officialSelected, ...communityResult.codes, ...customSelected]);
|
|
838
|
-
|
|
839
|
-
// Auto-include installed non-official modules that the user didn't get
|
|
840
|
-
// a chance to manage (they declined to browse). If they did browse,
|
|
841
|
-
// trust their selections - they could have deselected intentionally.
|
|
842
|
-
if (!communityResult.didBrowse) {
|
|
843
|
-
for (const code of installedNonOfficial) {
|
|
844
|
-
allSelected.add(code);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
832
|
+
const allSelected = new Set([...officialSelected, ...customSelected, ...installedNonOfficial]);
|
|
847
833
|
|
|
848
834
|
return [...allSelected];
|
|
849
835
|
}
|
|
@@ -954,166 +940,6 @@ class UI {
|
|
|
954
940
|
return result;
|
|
955
941
|
}
|
|
956
942
|
|
|
957
|
-
/**
|
|
958
|
-
* Browse and select community modules using category drill-down.
|
|
959
|
-
* Featured/promoted modules appear at the top.
|
|
960
|
-
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
961
|
-
* @returns {Object} { codes: string[], didBrowse: boolean }
|
|
962
|
-
*/
|
|
963
|
-
async _browseCommunityModules(installedModuleIds = new Set()) {
|
|
964
|
-
const browseCommunity = await prompts.confirm({
|
|
965
|
-
message: 'Would you like to browse community modules?',
|
|
966
|
-
default: false,
|
|
967
|
-
});
|
|
968
|
-
if (!browseCommunity) return { codes: [], didBrowse: false };
|
|
969
|
-
|
|
970
|
-
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
971
|
-
const communityMgr = new CommunityModuleManager();
|
|
972
|
-
|
|
973
|
-
const s = await prompts.spinner();
|
|
974
|
-
s.start('Loading community module catalog...');
|
|
975
|
-
|
|
976
|
-
let categories, featured, allCommunity;
|
|
977
|
-
try {
|
|
978
|
-
[categories, featured, allCommunity] = await Promise.all([
|
|
979
|
-
communityMgr.getCategoryList(),
|
|
980
|
-
communityMgr.listFeatured(),
|
|
981
|
-
communityMgr.listAll(),
|
|
982
|
-
]);
|
|
983
|
-
s.stop(`Community catalog loaded (${allCommunity.length} modules)`);
|
|
984
|
-
} catch (error) {
|
|
985
|
-
s.error('Failed to load community catalog');
|
|
986
|
-
await prompts.log.warn(` ${error.message}`);
|
|
987
|
-
return { codes: [], didBrowse: false };
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (allCommunity.length === 0) {
|
|
991
|
-
await prompts.log.info('No community modules are currently available.');
|
|
992
|
-
return { codes: [], didBrowse: false };
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
const selectedCodes = new Set();
|
|
996
|
-
let browsing = true;
|
|
997
|
-
|
|
998
|
-
while (browsing) {
|
|
999
|
-
const categoryChoices = [];
|
|
1000
|
-
|
|
1001
|
-
// Featured section at top
|
|
1002
|
-
if (featured.length > 0) {
|
|
1003
|
-
categoryChoices.push({
|
|
1004
|
-
value: '__featured__',
|
|
1005
|
-
label: `\u2605 Featured (${featured.length} module${featured.length === 1 ? '' : 's'})`,
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// Categories with module counts
|
|
1010
|
-
for (const cat of categories) {
|
|
1011
|
-
categoryChoices.push({
|
|
1012
|
-
value: cat.slug,
|
|
1013
|
-
label: `${cat.name} (${cat.moduleCount} module${cat.moduleCount === 1 ? '' : 's'})`,
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// Special actions at bottom
|
|
1018
|
-
categoryChoices.push(
|
|
1019
|
-
{ value: '__all__', label: '\u25CE View all community modules' },
|
|
1020
|
-
{ value: '__search__', label: '\u25CE Search by keyword' },
|
|
1021
|
-
{ value: '__done__', label: '\u2713 Done browsing' },
|
|
1022
|
-
);
|
|
1023
|
-
|
|
1024
|
-
const selectedCount = selectedCodes.size;
|
|
1025
|
-
const categoryChoice = await prompts.select({
|
|
1026
|
-
message: `Browse community modules${selectedCount > 0 ? ` (${selectedCount} selected)` : ''}:`,
|
|
1027
|
-
choices: categoryChoices,
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
if (categoryChoice === '__done__') {
|
|
1031
|
-
browsing = false;
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
let modulesToShow;
|
|
1036
|
-
switch (categoryChoice) {
|
|
1037
|
-
case '__featured__': {
|
|
1038
|
-
modulesToShow = featured;
|
|
1039
|
-
|
|
1040
|
-
break;
|
|
1041
|
-
}
|
|
1042
|
-
case '__all__': {
|
|
1043
|
-
modulesToShow = allCommunity;
|
|
1044
|
-
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
case '__search__': {
|
|
1048
|
-
const query = await prompts.text({
|
|
1049
|
-
message: 'Search community modules:',
|
|
1050
|
-
placeholder: 'e.g., design, testing, game',
|
|
1051
|
-
});
|
|
1052
|
-
if (!query || query.trim() === '') continue;
|
|
1053
|
-
modulesToShow = await communityMgr.searchByKeyword(query.trim());
|
|
1054
|
-
if (modulesToShow.length === 0) {
|
|
1055
|
-
await prompts.log.warn('No matching modules found.');
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
default: {
|
|
1062
|
-
modulesToShow = await communityMgr.listByCategory(categoryChoice);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Build options for autocompleteMultiselect
|
|
1067
|
-
const trustBadge = (tier) => {
|
|
1068
|
-
if (tier === 'bmad-certified') return '\u2713';
|
|
1069
|
-
if (tier === 'community-reviewed') return '\u25CB';
|
|
1070
|
-
return '\u26A0';
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
const options = modulesToShow.map((mod) => {
|
|
1074
|
-
const versionStr = mod.version ? ` (v${mod.version})` : '';
|
|
1075
|
-
const badge = trustBadge(mod.trustTier);
|
|
1076
|
-
return {
|
|
1077
|
-
label: `${mod.displayName}${versionStr} [${badge}]`,
|
|
1078
|
-
value: mod.code,
|
|
1079
|
-
hint: mod.description,
|
|
1080
|
-
};
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
// Pre-check modules that are already selected or installed
|
|
1084
|
-
const initialValues = modulesToShow.filter((m) => selectedCodes.has(m.code) || installedModuleIds.has(m.code)).map((m) => m.code);
|
|
1085
|
-
|
|
1086
|
-
const selected = await prompts.autocompleteMultiselect({
|
|
1087
|
-
message: 'Select community modules:',
|
|
1088
|
-
options,
|
|
1089
|
-
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
1090
|
-
required: false,
|
|
1091
|
-
maxItems: Math.min(options.length, 10),
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
// Update accumulated selections: sync with what user selected in this view
|
|
1095
|
-
const shownCodes = new Set(modulesToShow.map((m) => m.code));
|
|
1096
|
-
for (const code of shownCodes) {
|
|
1097
|
-
if (selected && selected.includes(code)) {
|
|
1098
|
-
selectedCodes.add(code);
|
|
1099
|
-
} else {
|
|
1100
|
-
selectedCodes.delete(code);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
if (selectedCodes.size > 0) {
|
|
1106
|
-
const moduleLines = [];
|
|
1107
|
-
for (const code of selectedCodes) {
|
|
1108
|
-
const mod = await communityMgr.getModuleByCode(code);
|
|
1109
|
-
moduleLines.push(` \u2022 ${mod?.displayName || code}`);
|
|
1110
|
-
}
|
|
1111
|
-
await prompts.log.message('Selected community modules:\n' + moduleLines.join('\n'));
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
return { codes: [...selectedCodes], didBrowse: true };
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
943
|
/**
|
|
1118
944
|
* Prompt user to install modules from custom sources (Git URLs or local paths).
|
|
1119
945
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
@@ -1121,7 +947,7 @@ class UI {
|
|
|
1121
947
|
*/
|
|
1122
948
|
async _addCustomUrlModules(installedModuleIds = new Set()) {
|
|
1123
949
|
const addCustom = await prompts.confirm({
|
|
1124
|
-
message: '
|
|
950
|
+
message: 'Do you want to install custom or community modules (Git URL or local path)?',
|
|
1125
951
|
default: false,
|
|
1126
952
|
});
|
|
1127
953
|
if (!addCustom) return [];
|
|
@@ -1885,19 +1711,14 @@ class UI {
|
|
|
1885
1711
|
const haveFlagIntent = channelOptions.global || channelOptions.nextSet.size > 0 || channelOptions.pins.size > 0;
|
|
1886
1712
|
if (haveFlagIntent) return;
|
|
1887
1713
|
|
|
1888
|
-
// Figure out which selected modules actually get a channel (externals
|
|
1889
|
-
//
|
|
1714
|
+
// Figure out which selected modules actually get a channel (externals only).
|
|
1715
|
+
// Bundled core/bmm and custom modules skip the picker.
|
|
1890
1716
|
const externalManager = new ExternalModuleManager();
|
|
1891
1717
|
const externals = await externalManager.listAvailable();
|
|
1892
1718
|
const externalByCode = new Map(externals.map((m) => [m.code, m]));
|
|
1893
1719
|
|
|
1894
|
-
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
1895
|
-
const communityMgr = new CommunityModuleManager();
|
|
1896
|
-
const community = await communityMgr.listAll();
|
|
1897
|
-
const communityByCode = new Map(community.map((m) => [m.code, m]));
|
|
1898
|
-
|
|
1899
1720
|
const channelSelectable = selectedModules.filter((code) => {
|
|
1900
|
-
const info = externalByCode.get(code)
|
|
1721
|
+
const info = externalByCode.get(code);
|
|
1901
1722
|
return info && !info.builtIn;
|
|
1902
1723
|
});
|
|
1903
1724
|
if (channelSelectable.length === 0) return;
|
|
@@ -1912,7 +1733,7 @@ class UI {
|
|
|
1912
1733
|
const { fetchStableTags, parseGitHubRepo } = require('./modules/channel-resolver');
|
|
1913
1734
|
|
|
1914
1735
|
for (const code of channelSelectable) {
|
|
1915
|
-
const info = externalByCode.get(code)
|
|
1736
|
+
const info = externalByCode.get(code);
|
|
1916
1737
|
const repoUrl = info.url;
|
|
1917
1738
|
|
|
1918
1739
|
// Try to pre-resolve the top stable tag so we can surface it in the picker.
|
|
@@ -1987,11 +1808,6 @@ class UI {
|
|
|
1987
1808
|
const externals = await externalManager.listAvailable();
|
|
1988
1809
|
const externalByCode = new Map(externals.map((m) => [m.code, m]));
|
|
1989
1810
|
|
|
1990
|
-
const { CommunityModuleManager } = require('./modules/community-manager');
|
|
1991
|
-
const communityMgr = new CommunityModuleManager();
|
|
1992
|
-
const community = await communityMgr.listAll();
|
|
1993
|
-
const communityByCode = new Map(community.map((m) => [m.code, m]));
|
|
1994
|
-
|
|
1995
1811
|
const { fetchStableTags, classifyUpgrade, releaseNotesUrl } = require('./modules/channel-resolver');
|
|
1996
1812
|
const { parseGitHubRepo } = require('./modules/channel-resolver');
|
|
1997
1813
|
|
|
@@ -2003,7 +1819,7 @@ class UI {
|
|
|
2003
1819
|
const existingWithChannel = selectedModules.filter((code) => {
|
|
2004
1820
|
const prev = existingByName.get(code);
|
|
2005
1821
|
if (!prev) return false;
|
|
2006
|
-
const info = externalByCode.get(code)
|
|
1822
|
+
const info = externalByCode.get(code);
|
|
2007
1823
|
return info && !info.builtIn;
|
|
2008
1824
|
});
|
|
2009
1825
|
if (existingWithChannel.length > 0) {
|
|
@@ -2018,7 +1834,7 @@ class UI {
|
|
|
2018
1834
|
const prev = existingByName.get(code);
|
|
2019
1835
|
if (!prev) continue;
|
|
2020
1836
|
|
|
2021
|
-
const info = externalByCode.get(code)
|
|
1837
|
+
const info = externalByCode.get(code);
|
|
2022
1838
|
if (!info) continue;
|
|
2023
1839
|
// Bundled modules (core/bmm) ship with the installer binary itself —
|
|
2024
1840
|
// their version is stapled to the CLI version, not a git tag. Skip
|