Compare commits

..

2 Commits

Author SHA1 Message Date
bm c11d8decd7
Merge 550ea6a8dd into e36f219c81 2026-05-08 07:37:46 -03:00
bmad 550ea6a8dd
fix(installer): add automator skill module 2026-05-08 07:37:14 -03:00
9 changed files with 14 additions and 313 deletions

View File

@ -31,19 +31,13 @@ npx bmad-method install
The interactive flow asks you five things: The interactive flow asks you five things:
1. Installation directory (defaults to the current working directory) 1. Installation directory (defaults to the current working directory)
2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, baut) 2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea)
3. **"Ready to install (all stable)?"** — Yes accepts the latest released tag for every external module 3. **"Ready to install (all stable)?"** — Yes accepts the latest released tag for every external module
4. Which AI tools/IDEs to integrate with (claude-code, cursor, and others) 4. Which AI tools/IDEs to integrate with (claude-code, cursor, and others)
5. Per-module config (name, language, output folder) 5. Per-module config (name, language, output folder)
Accept the defaults and you land on the latest stable release of every module, configured for your chosen tool. Accept the defaults and you land on the latest stable release of every module, configured for your chosen tool.
:::caution[BMad Automator constraints]
`baut` installs runnable Automator skills only for the Claude Code entrypoint. Codex is supported as a worker target only, and worker sessions currently require `tmux` on macOS.
While experimental, `baut` defaults to the `next` channel so installs pick up the latest Automator payload compatibility fixes.
:::
:::tip[Just want the newest prerelease?] :::tip[Just want the newest prerelease?]
```bash ```bash
@ -59,7 +53,7 @@ Two independent axes control what ends up on disk.
### Axis 1: external module channels ### Axis 1: external module channels
Every external module — bmb, cis, gds, tea, baut, and any community module — installs on one of three channels: Every external module — bmb, cis, gds, tea, and any community module — installs on one of three channels:
| Channel | What gets installed | Who picks this | | Channel | What gets installed | Who picks this |
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- | | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- |

View File

@ -71,25 +71,6 @@ Enterprise-grade test strategy, automation guidance, and release gate decisions
- NFR assessment, CI setup, and framework scaffolding - NFR assessment, CI setup, and framework scaffolding
- P0-P3 prioritization with optional Playwright Utils and MCP integrations - P0-P3 prioritization with optional Playwright Utils and MCP integrations
## BMad Automator (Experimental)
Automates the BMad story build loop with a self-contained skill bundle sourced from the separate Automator repository's root `skills/` folder.
- **Code:** `baut`
- **npm:** [`bmad-story-automator`](https://www.npmjs.com/package/bmad-story-automator)
- **GitHub:** [bmad-code-org/bmad-automator](https://github.com/bmad-code-org/bmad-automator)
- **Default channel:** `next` while experimental, so installs receive the latest Automator payload compatibility fixes.
:::caution[Experimental Claude Code-only entrypoint]
BMad Automator only runs from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS.
:::
**Provides:**
- Story build-cycle automation across story creation, development, QA automation, review, and retrospective
- Resumable tmux orchestration state
- Claude Code entry skill plus Claude Code/Codex worker-session coordination
## Community Modules ## Community Modules
Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates. Community modules and a module marketplace are coming. Check the [BMad GitHub organization](https://github.com/bmad-code-org) for updates.

View File

@ -86,42 +86,6 @@ async function createTestBmadFixture() {
return fixtureDir; return fixtureDir;
} }
async function createAutomatorBmadFixture() {
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-fixture-'));
const fixtureDir = path.join(fixtureRoot, '_bmad');
await fs.ensureDir(path.join(fixtureDir, '_config'));
await fs.writeFile(
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
[
'canonicalId,name,description,module,path',
'"bmad-master","bmad-master","Minimal core skill","core","_bmad/core/bmad-master/SKILL.md"',
'"bmad-story-automator","bmad-story-automator","Automator skill","baut","_bmad/baut/bmad-story-automator/SKILL.md"',
'"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","baut","_bmad/baut/bmad-story-automator-review/SKILL.md"',
'',
].join('\n'),
);
const coreSkillDir = path.join(fixtureDir, 'core', 'bmad-master');
await fs.ensureDir(coreSkillDir);
await fs.writeFile(
path.join(coreSkillDir, 'SKILL.md'),
['---', 'name: bmad-master', 'description: Minimal core skill', '---', '', 'Core skill body.'].join('\n'),
);
for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) {
const skillDir = path.join(fixtureDir, 'baut', skillName);
await fs.ensureDir(skillDir);
await fs.writeFile(
path.join(skillDir, 'SKILL.md'),
['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'),
);
await fs.writeFile(path.join(skillDir, 'workflow.md'), `# ${skillName}\n\nAutomator workflow body.\n`);
}
return fixtureDir;
}
async function createAutomatorSourceRootFixture() { async function createAutomatorSourceRootFixture() {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-source-')); const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-source-'));
const sourceRoot = path.join(repoRoot, 'skills'); const sourceRoot = path.join(repoRoot, 'skills');
@ -3584,43 +3548,22 @@ async function runTests() {
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 45: Automator External Skill-Only Module${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 45: Automator External Skill-Only Module${colors.reset}\n`);
let tempProjectDir42;
let installedBmadDir42;
let automatorSourceFixture42; let automatorSourceFixture42;
let runtimeTargetRoot42; let runtimeTargetRoot42;
try { try {
const yaml42 = require('yaml');
const fallbackConfig42 = yaml42.parse(
await fs.readFile(path.join(__dirname, '..', 'tools', 'installer', 'modules', 'registry-fallback.yaml'), 'utf8'),
);
const automatorEntry42 = fallbackConfig42.modules['bmad-automator'];
assert(automatorEntry42?.code === 'baut', 'BMad Automator fallback registry code is baut');
assert(automatorEntry42?.['source-root'] === 'skills', 'BMad Automator fallback registry points at root skills');
const externalManager42 = new ExternalModuleManager(); const externalManager42 = new ExternalModuleManager();
const automatorInfo42 = await externalManager42.getModuleByCode('baut'); const automatorInfo42 = externalManager42._normalizeModule(automatorEntry42);
assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module');
assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental'); assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental');
assert(automatorInfo42.sourceRoot === 'skills', 'BMad Automator uses root skills source-root for pure skill payload'); assert(automatorInfo42.sourceRoot === 'skills', 'BMad Automator uses root skills source-root for pure skill payload');
assert(automatorInfo42.defaultChannel === 'next', 'BMad Automator defaults to next for latest payload compatibility fixes'); assert(automatorInfo42.defaultChannel === 'next', 'BMad Automator defaults to next for latest payload compatibility fixes');
assert(
automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'),
'BMad Automator is limited to Claude Code skill installation',
);
const normalizedInfo42 = externalManager42._normalizeModule({
name: 'bad-shapes',
code: 'bad',
repository: 'https://example.com/bad.git',
install_targets: 'claude-code',
worker_targets: { bad: true },
requirements: ['tmux', { bad: true }, false],
});
assert(
Array.isArray(normalizedInfo42.installTargets) && normalizedInfo42.installTargets.includes('claude-code'),
'External module install targets normalize scalar values to arrays',
);
assert(
Array.isArray(normalizedInfo42.workerTargets) && normalizedInfo42.workerTargets.length === 0,
'External module worker targets drop invalid shapes',
);
assert(
normalizedInfo42.requirements.length === 2 &&
normalizedInfo42.requirements.includes('tmux') &&
normalizedInfo42.requirements.includes('false'),
'External module requirements normalize scalar array entries',
);
automatorSourceFixture42 = await createAutomatorSourceRootFixture(); automatorSourceFixture42 = await createAutomatorSourceRootFixture();
runtimeTargetRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-runtime-target-')); runtimeTargetRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-runtime-target-'));
@ -3639,30 +3582,6 @@ async function runTests() {
await fs.remove(runtimeTargetRoot42).catch(() => {}); await fs.remove(runtimeTargetRoot42).catch(() => {});
runtimeTargetRoot42 = null; runtimeTargetRoot42 = null;
tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-'));
installedBmadDir42 = await createAutomatorBmadFixture();
const ideManager42 = new IdeManager();
await ideManager42.ensureInitialized();
const codexResult42 = await ideManager42.setup('codex', tempProjectDir42, installedBmadDir42, {
silent: true,
selectedModules: ['core', 'baut'],
});
assert(codexResult42.success === true, 'Codex setup succeeds with automator module selected');
assert(
await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-master', 'SKILL.md')),
'Codex setup still installs supported core skills',
);
assert(
!(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator', 'SKILL.md'))),
'Codex setup skips Claude Code-only automator skill',
);
assert(
!(await fs.pathExists(path.join(tempProjectDir42, '.agents', 'skills', 'bmad-story-automator-review', 'SKILL.md'))),
'Codex setup skips Claude Code-only automator review skill',
);
const escapeRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-')); const escapeRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-'));
const escapeRepo42 = path.join(escapeRoot42, 'repo'); const escapeRepo42 = path.join(escapeRoot42, 'repo');
await fs.ensureDir(escapeRepo42); await fs.ensureDir(escapeRepo42);
@ -3682,29 +3601,9 @@ async function runTests() {
await fs.remove(escapeRoot42).catch(() => {}); await fs.remove(escapeRoot42).catch(() => {});
} }
assert(rejectedEscapingSourceRoot42, 'External module source-root cannot escape cloned repository'); assert(rejectedEscapingSourceRoot42, 'External module source-root cannot escape cloned repository');
const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, {
silent: true,
selectedModules: ['core', 'baut'],
});
assert(claudeResult42.success === true, 'Claude Code setup succeeds with automator module selected');
assert(
await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator', 'SKILL.md')),
'Claude Code setup installs automator skill',
);
assert(
await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'SKILL.md')),
'Claude Code setup installs automator review skill',
);
assert(
await fs.pathExists(path.join(tempProjectDir42, '.claude', 'skills', 'bmad-story-automator-review', 'workflow.md')),
'Claude Code setup copies automator workflow files',
);
} catch (error) { } catch (error) {
assert(false, `Automator external skill-only module test succeeds: ${error.message}`); assert(false, `Automator external skill-only module test succeeds: ${error.message}`);
} finally { } finally {
if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {});
if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {});
if (automatorSourceFixture42) await fs.remove(automatorSourceFixture42.repoRoot).catch(() => {}); if (automatorSourceFixture42) await fs.remove(automatorSourceFixture42.repoRoot).catch(() => {});
if (runtimeTargetRoot42) await fs.remove(runtimeTargetRoot42).catch(() => {}); if (runtimeTargetRoot42) await fs.remove(runtimeTargetRoot42).catch(() => {});
} }

View File

@ -2,15 +2,9 @@
## Installing external repo BMad official modules ## Installing external repo BMad official modules
For external official modules to be discoverable during install, ensure an entry for the external repo is added to the marketplace `registry/official.yaml` source of truth. Add the same entry to `modules/registry-fallback.yaml` only when BMAD-METHOD needs a bundled fallback or a staged registry supplement. For external official modules to be discoverable during install, ensure an entry for the external repo is added to external-official-modules.yaml.
For community modules - this is handled through the marketplace community registry. For community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org.
Use `module-definition` for conventional module repos with `module.yaml`.
Use `source-root` for pure skill bundles that should be copied directly into `_bmad/<module-code>/`.
This keeps the external repo as the source of truth and avoids vendoring generated skill payloads into BMAD-METHOD.
Experimental modules can set `type: experimental` and `install-targets` to limit which IDE integrations receive their skills.
## Post-Install Notes ## Post-Install Notes

View File

@ -246,11 +246,6 @@ class ManifestGenerator {
for (const moduleName of this.updatedModules) { for (const moduleName of this.updatedModules) {
const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); const moduleYamlPath = await resolveInstalledModuleYaml(moduleName);
if (!moduleYamlPath) { if (!moduleYamlPath) {
if (await this._isSkillOnlyModule(moduleName)) {
// Pure source-root skill bundles intentionally ship no module.yaml,
// so there is no agent roster to emit and no warning to surface.
continue;
}
// External modules live in ~/.bmad/cache/external-modules, not src/modules. // External modules live in ~/.bmad/cache/external-modules, not src/modules.
// Warn rather than silently skip so missing agent rosters don't vanish // Warn rather than silently skip so missing agent rosters don't vanish
// from config.toml without notice. // from config.toml without notice.
@ -446,11 +441,6 @@ class ManifestGenerator {
for (const moduleName of this.updatedModules) { for (const moduleName of this.updatedModules) {
const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); const moduleYamlPath = await resolveInstalledModuleYaml(moduleName);
if (!moduleYamlPath) { if (!moduleYamlPath) {
if (await this._isSkillOnlyModule(moduleName)) {
// Pure source-root skill bundles intentionally ship no module.yaml,
// so there is no installer config schema to read or warning to surface.
continue;
}
console.warn( console.warn(
`[warn] writeCentralConfig: could not locate module.yaml for '${moduleName}'. ` + `[warn] writeCentralConfig: could not locate module.yaml for '${moduleName}'. ` +
`Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`, `Answers from this module will default to team scope — user-scoped keys may mis-file into config.toml.`,
@ -809,27 +799,6 @@ class ManifestGenerator {
return false; return false;
} }
async _isSkillOnlyModule(moduleName) {
const modulePath = path.join(this.bmadDir, moduleName);
if (!(await fs.pathExists(modulePath))) return false;
if (await fs.pathExists(path.join(modulePath, 'module.yaml'))) return false;
if (!(await this._moduleUsesSourceRoot(moduleName))) return false;
return this._hasSkillMdRecursive(modulePath);
}
async _moduleUsesSourceRoot(moduleName) {
if (!this.sourceRootModuleCodes) {
try {
const { ExternalModuleManager } = require('../modules/external-manager');
const externalModules = await new ExternalModuleManager().listAvailable();
this.sourceRootModuleCodes = new Set(externalModules.filter((mod) => mod.sourceRoot).map((mod) => mod.code));
} catch {
this.sourceRootModuleCodes = new Set();
}
}
return this.sourceRootModuleCodes.has(moduleName);
}
} }
/** /**

View File

@ -145,8 +145,6 @@ class ConfigDrivenIdeSetup {
this.platformConfig = platformConfig; this.platformConfig = platformConfig;
this.installerConfig = platformConfig.installer || null; this.installerConfig = platformConfig.installer || null;
this.bmadFolderName = BMAD_FOLDER_NAME; this.bmadFolderName = BMAD_FOLDER_NAME;
this.externalModuleManager = null;
this.moduleTargetCache = new Map();
// Set configDir from target_dir so detect() works // Set configDir from target_dir so detect() works
this.configDir = this.installerConfig?.target_dir || null; this.configDir = this.installerConfig?.target_dir || null;
@ -250,11 +248,9 @@ class ConfigDrivenIdeSetup {
await fs.ensureDir(targetPath); await fs.ensureDir(targetPath);
this.skillWriteTracker = new Set(); this.skillWriteTracker = new Set();
this.skippedUnsupported = 0;
const results = { skills: 0 }; const results = { skills: 0 };
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
results.skippedUnsupported = this.skippedUnsupported || 0;
results.skillDirectories = this.skillWriteTracker.size; results.skillDirectories = this.skillWriteTracker.size;
if (config.commands_target_dir) { if (config.commands_target_dir) {
@ -263,7 +259,6 @@ class ConfigDrivenIdeSetup {
await this.printSummary(results, target_dir, options); await this.printSummary(results, target_dir, options);
this.skillWriteTracker = null; this.skillWriteTracker = null;
this.skippedUnsupported = 0;
return { success: true, results }; return { success: true, results };
} }
@ -438,11 +433,6 @@ class ConfigDrivenIdeSetup {
const canonicalId = record.canonicalId; const canonicalId = record.canonicalId;
if (!canonicalId) continue; if (!canonicalId) continue;
if (!(await this.shouldInstallSkillRecord(record))) {
this.skippedUnsupported = (this.skippedUnsupported || 0) + 1;
continue;
}
// Derive source directory from path column // Derive source directory from path column
// path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md" // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/SKILL.md"
// Strip bmadFolderName prefix and join with bmadDir, then get dirname // Strip bmadFolderName prefix and join with bmadDir, then get dirname
@ -477,31 +467,6 @@ class ConfigDrivenIdeSetup {
return count; return count;
} }
async shouldInstallSkillRecord(record) {
const moduleName = record.module;
if (!moduleName) return true;
if (this.moduleTargetCache.has(moduleName)) {
const targets = this.moduleTargetCache.get(moduleName);
return !targets || targets.includes(this.name);
}
const { ExternalModuleManager } = require('../modules/external-manager');
this.externalModuleManager = this.externalModuleManager || new ExternalModuleManager();
let targets = null;
try {
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName);
targets = moduleInfo?.installTargets?.length ? moduleInfo.installTargets : null;
} catch (error) {
await prompts.log.warn(
`ExternalModuleManager.getModuleByCode failed for module '${moduleName}' while installing ${this.name}; installing skill anyway. ${error.message}`,
);
}
this.moduleTargetCache.set(moduleName, targets);
return !targets || targets.includes(this.name);
}
/** /**
* Print installation summary * Print installation summary
* @param {Object} results - Installation results * @param {Object} results - Installation results
@ -525,9 +490,6 @@ class ConfigDrivenIdeSetup {
await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`); await prompts.log.warn(` (${cmd.writeFailures} pointer writes failed — see warnings above)`);
} }
} }
if (results.skippedUnsupported > 0) {
await prompts.log.warn(`${this.name}: skipped ${results.skippedUnsupported} skill(s) that do not support this IDE`);
}
} }
/** /**

View File

@ -16,15 +16,6 @@ function normalizeChannelName(raw) {
return VALID_CHANNELS.has(lower) ? lower : null; return VALID_CHANNELS.has(lower) ? lower : null;
} }
function normalizeStringList(raw) {
if (raw == null || raw === '') return [];
const values = Array.isArray(raw) ? raw : [raw];
return values
.filter((value) => ['string', 'number', 'boolean'].includes(typeof value))
.map((value) => String(value).trim())
.filter(Boolean);
}
/** /**
* Conservative quoting for tag names passed to git commands. Tags are * Conservative quoting for tag names passed to git commands. Tags are
* user-typed (--pin) or come from the GitHub API. Only allow the semver * user-typed (--pin) or come from the GitHub API. Only allow the semver
@ -129,9 +120,6 @@ class ExternalModuleManager {
* @returns {Object} Normalized module info * @returns {Object} Normalized module info
*/ */
_normalizeModule(mod, key) { _normalizeModule(mod, key) {
const installTargets = mod.install_targets ?? mod['install-targets'] ?? mod.installTargets;
const workerTargets = mod.worker_targets ?? mod['worker-targets'] ?? mod.workerTargets;
return { return {
key: key || mod.name, key: key || mod.name,
url: mod.repository || mod.url, url: mod.repository || mod.url,
@ -143,27 +131,12 @@ class ExternalModuleManager {
defaultSelected: mod.default_selected === true || mod.defaultSelected === true, defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
type: mod.type || 'bmad-org', type: mod.type || 'bmad-org',
npmPackage: mod.npm_package || mod.npmPackage || null, npmPackage: mod.npm_package || mod.npmPackage || null,
installTargets: normalizeStringList(installTargets),
workerTargets: normalizeStringList(workerTargets),
requirements: normalizeStringList(mod.requirements),
installNote: mod.install_note || mod['install-note'] || mod.installNote || null,
defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable', defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
builtIn: mod.built_in === true, builtIn: mod.built_in === true,
isExternal: mod.built_in !== true, isExternal: mod.built_in !== true,
}; };
} }
async _loadFallbackModules() {
try {
const content = await fs.readFile(FALLBACK_CONFIG_PATH, 'utf8');
const config = yaml.parse(content);
if (Array.isArray(config.modules)) return config.modules.map((mod) => this._normalizeModule(mod));
return Object.entries(config.modules || {}).map(([key, mod]) => this._normalizeModule(mod, key));
} catch {
return [];
}
}
/** /**
* Get list of available modules from the registry * Get list of available modules from the registry
* @returns {Array<Object>} Array of module info objects * @returns {Array<Object>} Array of module info objects
@ -173,14 +146,7 @@ class ExternalModuleManager {
// Remote format: modules is an array // Remote format: modules is an array
if (Array.isArray(config.modules)) { if (Array.isArray(config.modules)) {
const modules = config.modules.map((mod) => this._normalizeModule(mod)); return config.modules.map((mod) => this._normalizeModule(mod));
const seenCodes = new Set(modules.map((mod) => mod.code));
for (const fallbackMod of await this._loadFallbackModules()) {
if (!seenCodes.has(fallbackMod.code)) {
modules.push(fallbackMod);
}
}
return modules;
} }
// Legacy bundled format: modules is an object map // Legacy bundled format: modules is an object map

View File

@ -61,14 +61,3 @@ modules:
type: experimental type: experimental
npmPackage: bmad-story-automator npmPackage: bmad-story-automator
default_channel: next default_channel: next
install-targets:
- claude-code
worker-targets:
- claude-code
- codex
requirements:
- Claude Code entrypoint
- Claude Code or Codex worker sessions
- tmux
- macOS
install-note: "Experimental: BMad Automator only works from Claude Code. It currently supports Claude Code and Codex worker sessions, and requires tmux on macOS."

View File

@ -259,7 +259,6 @@ class UI {
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
} }
await this.showSelectedExternalModuleNotes(selectedModules);
// Resolve custom sources from --custom-source flag // Resolve custom sources from --custom-source flag
if (options.customSource) { if (options.customSource) {
@ -288,7 +287,6 @@ class UI {
// Get tool selection // Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, options); const toolSelection = await this.promptToolSelection(confirmedDirectory, options);
await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides);
const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
...options, ...options,
@ -345,7 +343,6 @@ class UI {
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions); selectedModules = await this.selectAllModules(installedModuleIds, installedModuleVersions, channelOptions);
} }
await this.showSelectedExternalModuleNotes(selectedModules);
// Resolve custom sources from --custom-source flag // Resolve custom sources from --custom-source flag
if (options.customSource) { if (options.customSource) {
@ -369,7 +366,6 @@ class UI {
await this._interactiveChannelGate({ options, channelOptions, selectedModules }); await this._interactiveChannelGate({ options, channelOptions, selectedModules });
let toolSelection = await this.promptToolSelection(confirmedDirectory, options); let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
await this.showSelectedModuleIdeWarnings(selectedModules, toolSelection.ides);
const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, { const { moduleConfigs, setOverrides } = await this.collectModuleConfigs(confirmedDirectory, selectedModules, {
...options, ...options,
channelOptions, channelOptions,
@ -958,55 +954,6 @@ class UI {
return result; return result;
} }
async showSelectedExternalModuleNotes(selectedModuleIds, externalModules = null) {
if (!externalModules) {
const externalManager = new ExternalModuleManager();
try {
externalModules = await externalManager.listAvailable();
} catch (error) {
await prompts.log.warn(
`ExternalModuleManager.listAvailable failed while loading module notes; continuing without external module notes. ${error.message}`,
);
externalModules = [];
}
}
const notes = externalModules
.filter((mod) => selectedModuleIds.includes(mod.code) && mod.installNote)
.map((mod) => `${mod.name}: ${mod.installNote}`);
for (const note of notes) {
await prompts.log.warn(note);
}
}
async showSelectedModuleIdeWarnings(selectedModuleIds, selectedIdes = []) {
const externalManager = new ExternalModuleManager();
let externalModules = [];
try {
externalModules = await externalManager.listAvailable();
} catch (error) {
await prompts.log.warn(
`ExternalModuleManager.listAvailable failed while loading IDE compatibility warnings; continuing without external module warnings. ${error.message}`,
);
}
for (const mod of externalModules) {
if (!selectedModuleIds.includes(mod.code) || !mod.installTargets || mod.installTargets.length === 0) {
continue;
}
const hasInstallTarget = mod.installTargets.some((target) => selectedIdes.includes(target));
if (!hasInstallTarget) {
await prompts.log.warn(
`${mod.name}: runnable skills are installed only for ${mod.installTargets.join(
', ',
)}. Add that tool selection to use this module.`,
);
}
}
}
/** /**
* Browse and select community modules using category drill-down. * Browse and select community modules using category drill-down.
* Featured/promoted modules appear at the top. * Featured/promoted modules appear at the top.