bmad-method 6.9.1-next.13 → 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 +42 -0
- package/package.json +1 -1
- package/src/bmm-skills/4-implementation/bmad-code-review/steps/step-02-review.md +14 -8
- package/src/bmm-skills/4-implementation/bmad-dev-auto/step-04-review.md +9 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-04-review.md +9 -4
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +4 -1
- package/tools/installer/core/installer.js +55 -0
- package/tools/installer/modules/external-manager.js +111 -0
- package/tools/installer/modules/official-modules.js +66 -19
- package/tools/installer/ui.js +14 -7
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
|
@@ -7,23 +7,29 @@ failed_layers: '' # set at runtime: comma-separated list of layers that failed o
|
|
|
7
7
|
## RULES
|
|
8
8
|
|
|
9
9
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
10
|
-
- The Blind Hunter subagent receives NO project context — diff only.
|
|
11
|
-
- The Edge Case Hunter subagent receives diff and project read access.
|
|
12
|
-
- The Acceptance Auditor subagent receives diff, spec, and context docs.
|
|
13
10
|
- All review subagents must run at the same model capability as the current session.
|
|
14
11
|
|
|
15
12
|
## INSTRUCTIONS
|
|
16
13
|
|
|
17
14
|
1. If `{review_mode}` = `"no-spec"`, note to the user: "Acceptance Auditor skipped — no spec file provided."
|
|
18
15
|
|
|
19
|
-
2. Launch parallel
|
|
16
|
+
2. Launch Blind Hunter and Edge Case Hunter in parallel without prior conversation context. If `{review_mode}` = `"full"`, include the Acceptance Auditor in the same parallel launch. If subagents are not available, generate prompt files in `{implementation_artifacts}` for each applicable reviewer role and HALT. Ask the user to run each in a separate session (ideally a different LLM) and paste back the findings. When findings are pasted, resume from this point and proceed to step 3.
|
|
20
17
|
|
|
21
|
-
- **Blind Hunter** — prompt:
|
|
18
|
+
- **Blind Hunter** — prompt:
|
|
19
|
+
> Invoke the `bmad-review-adversarial-general` skill on this diff:
|
|
20
|
+
>
|
|
21
|
+
> {diff_output}
|
|
22
22
|
|
|
23
|
-
- **Edge Case Hunter** — prompt:
|
|
23
|
+
- **Edge Case Hunter** — prompt:
|
|
24
|
+
> Invoke the `bmad-review-edge-case-hunter` skill on this diff:
|
|
25
|
+
>
|
|
26
|
+
> {diff_output}
|
|
24
27
|
|
|
25
|
-
- **Acceptance Auditor** (only if `{review_mode}` = `"full"`) —
|
|
26
|
-
> You are an Acceptance Auditor. Review
|
|
28
|
+
- **Acceptance Auditor** (only if `{review_mode}` = `"full"`) — prompt:
|
|
29
|
+
> You are an Acceptance Auditor. Review the provided diff against `{spec_file}` and any loaded context docs. Check for: violations of acceptance criteria, deviations from spec intent, missing implementation of specified behavior, contradictions between spec constraints and actual code. Output findings as a Markdown list. Each finding: one-line title, which AC/constraint it violates, and evidence from the diff.
|
|
30
|
+
>
|
|
31
|
+
> Diff:
|
|
32
|
+
> {diff_output}
|
|
27
33
|
|
|
28
34
|
3. **Subagent failure handling**: If any subagent fails, times out, or returns empty results, append the layer name to `{failed_layers}` (comma-separated) and proceed with findings from the remaining layers.
|
|
29
35
|
|
|
@@ -8,7 +8,6 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
|
8
8
|
|
|
9
9
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
10
10
|
- No human interaction: do not ask questions or wait for approval in this step.
|
|
11
|
-
- Review subagents get no prior session context.
|
|
12
11
|
- All review subagents must run at the same model capability as the current session.
|
|
13
12
|
|
|
14
13
|
## INSTRUCTIONS
|
|
@@ -23,10 +22,16 @@ Do NOT `git add` anything — this is read-only inspection.
|
|
|
23
22
|
|
|
24
23
|
### Review
|
|
25
24
|
|
|
26
|
-
Launch
|
|
25
|
+
Launch Blind Hunter and Edge Case Hunter in parallel without prior conversation context.
|
|
27
26
|
|
|
28
|
-
- **Blind
|
|
29
|
-
|
|
27
|
+
- **Blind Hunter** — prompt:
|
|
28
|
+
> Invoke the `bmad-review-adversarial-general` skill on this diff:
|
|
29
|
+
>
|
|
30
|
+
> {diff_output}
|
|
31
|
+
- **Edge Case Hunter** — prompt:
|
|
32
|
+
> Invoke the `bmad-review-edge-case-hunter` skill on this diff:
|
|
33
|
+
>
|
|
34
|
+
> {diff_output}
|
|
30
35
|
|
|
31
36
|
### Classify
|
|
32
37
|
|
|
@@ -7,7 +7,6 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
|
7
7
|
## RULES
|
|
8
8
|
|
|
9
9
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
10
|
-
- Review subagents get NO conversation context.
|
|
11
10
|
- All review subagents must run at the same model capability as the current session.
|
|
12
11
|
|
|
13
12
|
## INSTRUCTIONS
|
|
@@ -22,10 +21,16 @@ Do NOT `git add` anything — this is read-only inspection.
|
|
|
22
21
|
|
|
23
22
|
### Review
|
|
24
23
|
|
|
25
|
-
Launch
|
|
24
|
+
Launch Blind Hunter and Edge Case Hunter in parallel without prior conversation context. If no subagents are available, generate two review prompt files in `{implementation_artifacts}` — one per reviewer role below — and HALT. Ask the human to run each in a separate session (ideally a different LLM) and paste back the findings.
|
|
26
25
|
|
|
27
|
-
- **Blind
|
|
28
|
-
|
|
26
|
+
- **Blind Hunter** — prompt:
|
|
27
|
+
> Invoke the `bmad-review-adversarial-general` skill on this diff:
|
|
28
|
+
>
|
|
29
|
+
> {diff_output}
|
|
30
|
+
- **Edge Case Hunter** — prompt:
|
|
31
|
+
> Invoke the `bmad-review-edge-case-hunter` skill on this diff:
|
|
32
|
+
>
|
|
33
|
+
> {diff_output}
|
|
29
34
|
|
|
30
35
|
### Classify
|
|
31
36
|
|
|
@@ -8,6 +8,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
|
8
8
|
|
|
9
9
|
- YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`
|
|
10
10
|
- NEVER auto-push.
|
|
11
|
+
- All review subagents must run at the same model capability as the current session.
|
|
11
12
|
|
|
12
13
|
## INSTRUCTIONS
|
|
13
14
|
|
|
@@ -19,7 +20,9 @@ Implement the clarified intent directly.
|
|
|
19
20
|
|
|
20
21
|
### Review
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Launch Blind Hunter without prior conversation context. If no subagents are available, generate one review prompt file in `{implementation_artifacts}` and HALT. Ask the human to run it in a separate session and paste back the findings.
|
|
24
|
+
|
|
25
|
+
- **Blind Hunter** — prompt: "Invoke the `bmad-review-adversarial-general` skill on the changed files."
|
|
23
26
|
|
|
24
27
|
### Classify
|
|
25
28
|
|
|
@@ -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
|
-
|
|
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
|
-
*
|
|
308
|
-
*
|
|
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}
|
|
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}
|
|
337
|
+
* @param {Object} moduleConfig - Module configuration passed to copy filtering
|
|
313
338
|
*/
|
|
314
|
-
async
|
|
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,
|
|
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) {
|
package/tools/installer/ui.js
CHANGED
|
@@ -944,25 +944,32 @@ class UI {
|
|
|
944
944
|
}
|
|
945
945
|
}
|
|
946
946
|
|
|
947
|
-
// Add external registry modules (skip built-in duplicates
|
|
948
|
-
|
|
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
|
-
|
|
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.');
|