feat(installer): install automator skill module
This commit is contained in:
parent
3778cc1082
commit
95941d7768
|
|
@ -31,7 +31,7 @@ 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)
|
2. Which modules to install (checkboxes for core, bmm, bmb, cis, gds, tea, bma)
|
||||||
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)
|
||||||
|
|
@ -53,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, and any community module — installs on one of three channels:
|
Every external module — bmb, cis, gds, tea, bma, and any community module — installs on one of three channels:
|
||||||
|
|
||||||
| Channel | What gets installed | Who picks this |
|
| Channel | What gets installed | Who picks this |
|
||||||
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------- |
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,24 @@ 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 pure skill bundle sourced from the separate Automator repository.
|
||||||
|
|
||||||
|
- **Code:** `bma`
|
||||||
|
- **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)
|
||||||
|
|
||||||
|
:::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.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const { Installer } = require('../tools/installer/core/installer');
|
||||||
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
|
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
|
||||||
const { OfficialModules } = require('../tools/installer/modules/official-modules');
|
const { OfficialModules } = require('../tools/installer/modules/official-modules');
|
||||||
const { IdeManager } = require('../tools/installer/ide/manager');
|
const { IdeManager } = require('../tools/installer/ide/manager');
|
||||||
|
const { ExternalModuleManager } = require('../tools/installer/modules/external-manager');
|
||||||
const { clearCache, loadPlatformCodes } = require('../tools/installer/ide/platform-codes');
|
const { clearCache, loadPlatformCodes } = require('../tools/installer/ide/platform-codes');
|
||||||
|
|
||||||
// ANSI colors
|
// ANSI colors
|
||||||
|
|
@ -85,6 +86,41 @@ 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","bma","_bmad/bma/bmad-story-automator/SKILL.md"',
|
||||||
|
'"bmad-story-automator-review","bmad-story-automator-review","Automator review skill","bma","_bmad/bma/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, 'bma', skillName);
|
||||||
|
await fs.ensureDir(skillDir);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(skillDir, 'SKILL.md'),
|
||||||
|
['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixtureDir;
|
||||||
|
}
|
||||||
|
|
||||||
async function createSkillCollisionFixture() {
|
async function createSkillCollisionFixture() {
|
||||||
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-'));
|
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-'));
|
||||||
const fixtureDir = path.join(fixtureRoot, '_bmad');
|
const fixtureDir = path.join(fixtureRoot, '_bmad');
|
||||||
|
|
@ -3237,6 +3273,66 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 45: Automator External Skill-Only Module
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 45: Automator External Skill-Only Module${colors.reset}\n`);
|
||||||
|
|
||||||
|
let tempProjectDir42;
|
||||||
|
let installedBmadDir42;
|
||||||
|
try {
|
||||||
|
const externalManager42 = new ExternalModuleManager();
|
||||||
|
const automatorInfo42 = await externalManager42.getModuleByCode('bma');
|
||||||
|
assert(automatorInfo42 !== null, 'BMad Automator is registered as an external module');
|
||||||
|
assert(automatorInfo42.type === 'experimental', 'BMad Automator is marked experimental');
|
||||||
|
assert(automatorInfo42.sourceRoot === 'payload/.claude/skills', 'BMad Automator uses source-root for pure skill payload');
|
||||||
|
assert(
|
||||||
|
automatorInfo42.installTargets.length === 1 && automatorInfo42.installTargets.includes('claude-code'),
|
||||||
|
'BMad Automator is limited to Claude Code skill installation',
|
||||||
|
);
|
||||||
|
|
||||||
|
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', 'bma'],
|
||||||
|
});
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
|
||||||
|
const claudeResult42 = await ideManager42.setup('claude-code', tempProjectDir42, installedBmadDir42, {
|
||||||
|
silent: true,
|
||||||
|
selectedModules: ['core', 'bma'],
|
||||||
|
});
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
assert(false, 'Automator external skill-only module test succeeds', error.message);
|
||||||
|
} finally {
|
||||||
|
if (tempProjectDir42) await fs.remove(tempProjectDir42).catch(() => {});
|
||||||
|
if (installedBmadDir42) await fs.remove(path.dirname(installedBmadDir42)).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
|
|
||||||
## 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 external-official-modules.yaml.
|
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 community modules - this will be handled in a different way. This file is only for registration of modules under the bmad-code-org.
|
For community modules - this is handled through the marketplace community registry.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,9 @@ 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)) {
|
||||||
|
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.
|
||||||
|
|
@ -441,6 +444,9 @@ 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)) {
|
||||||
|
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.`,
|
||||||
|
|
@ -799,6 +805,13 @@ 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;
|
||||||
|
return this._hasSkillMdRecursive(modulePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ 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;
|
||||||
|
|
@ -123,13 +125,16 @@ 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;
|
||||||
|
|
||||||
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +167,11 @@ 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
|
||||||
|
|
@ -196,6 +206,24 @@ 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();
|
||||||
|
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleName);
|
||||||
|
const targets = moduleInfo?.installTargets || null;
|
||||||
|
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
|
||||||
|
|
@ -207,6 +235,9 @@ class ConfigDrivenIdeSetup {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`);
|
await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`);
|
||||||
}
|
}
|
||||||
|
if (results.skippedUnsupported > 0) {
|
||||||
|
await prompts.log.warn(`${this.name}: skipped ${results.skippedUnsupported} skill(s) that do not support this IDE`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -124,18 +124,34 @@ class ExternalModuleManager {
|
||||||
key: key || mod.name,
|
key: key || mod.name,
|
||||||
url: mod.repository || mod.url,
|
url: mod.repository || mod.url,
|
||||||
moduleDefinition: mod.module_definition || mod['module-definition'],
|
moduleDefinition: mod.module_definition || mod['module-definition'],
|
||||||
|
sourceRoot: mod.source_root || mod['source-root'] || mod.sourceRoot || null,
|
||||||
code: mod.code,
|
code: mod.code,
|
||||||
name: mod.display_name || mod.name,
|
name: mod.display_name || mod.name,
|
||||||
description: mod.description || '',
|
description: mod.description || '',
|
||||||
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: mod.install_targets || mod['install-targets'] || mod.installTargets || null,
|
||||||
|
workerTargets: mod.worker_targets || mod['worker-targets'] || mod.workerTargets || [],
|
||||||
|
requirements: 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
|
||||||
|
|
@ -145,7 +161,14 @@ class ExternalModuleManager {
|
||||||
|
|
||||||
// Remote format: modules is an array
|
// Remote format: modules is an array
|
||||||
if (Array.isArray(config.modules)) {
|
if (Array.isArray(config.modules)) {
|
||||||
return config.modules.map((mod) => this._normalizeModule(mod));
|
const modules = 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
|
||||||
|
|
@ -489,6 +512,14 @@ class ExternalModuleManager {
|
||||||
// Clone the external module repo
|
// Clone the external module repo
|
||||||
const cloneDir = await this.cloneExternalModule(moduleCode, options);
|
const cloneDir = await this.cloneExternalModule(moduleCode, options);
|
||||||
|
|
||||||
|
if (moduleInfo.sourceRoot) {
|
||||||
|
const sourceRoot = path.join(cloneDir, moduleInfo.sourceRoot);
|
||||||
|
if (!(await fs.pathExists(sourceRoot))) {
|
||||||
|
throw new Error(`External module '${moduleCode}' source-root not found: ${moduleInfo.sourceRoot}`);
|
||||||
|
}
|
||||||
|
return sourceRoot;
|
||||||
|
}
|
||||||
|
|
||||||
// The module-definition specifies the path to module.yaml relative to repo root
|
// The module-definition specifies the path to module.yaml relative to repo root
|
||||||
// We need to return the directory containing module.yaml
|
// We need to return the directory containing module.yaml
|
||||||
const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'skills/module.yaml'
|
const moduleDefinitionPath = moduleInfo.moduleDefinition; // e.g., 'skills/module.yaml'
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@ 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) {
|
||||||
|
|
@ -287,6 +288,7 @@ 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,
|
||||||
|
|
@ -343,6 +345,7 @@ 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) {
|
||||||
|
|
@ -366,6 +369,7 @@ 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,
|
||||||
|
|
@ -954,6 +958,41 @@ class UI {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showSelectedExternalModuleNotes(selectedModuleIds, externalModules = null) {
|
||||||
|
if (!externalModules) {
|
||||||
|
const externalManager = new ExternalModuleManager();
|
||||||
|
externalModules = await externalManager.listAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
const externalModules = await externalManager.listAvailable();
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue