diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 1bcdd6d40..284b05c07 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -122,6 +122,27 @@ async function createAutomatorBmadFixture() { return fixtureDir; } +async function createAutomatorSourceRootFixture() { + const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-source-')); + const sourceRoot = path.join(repoRoot, 'payload', '.claude', 'skills'); + + for (const skillName of ['bmad-story-automator', 'bmad-story-automator-review']) { + const skillDir = path.join(sourceRoot, skillName); + await fs.ensureDir(skillDir); + await fs.writeFile( + path.join(skillDir, 'SKILL.md'), + ['---', `name: ${skillName}`, 'description: Automator skill', '---', '', 'Automator body.'].join('\n'), + ); + } + + await fs.ensureDir(path.join(repoRoot, 'source', 'scripts')); + await fs.writeFile(path.join(repoRoot, 'source', 'scripts', 'story-automator'), '#!/usr/bin/env bash\n'); + await fs.ensureDir(path.join(repoRoot, 'source', 'src', 'story_automator')); + await fs.writeFile(path.join(repoRoot, 'source', 'src', 'story_automator', 'cli.py'), 'def main():\n return 0\n'); + + return { repoRoot, sourceRoot }; +} + async function createSkillCollisionFixture() { const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-collision-')); const fixtureDir = path.join(fixtureRoot, '_bmad'); @@ -3564,6 +3585,8 @@ async function runTests() { let tempProjectDir42; let installedBmadDir42; + let automatorSourceFixture42; + let runtimeTargetRoot42; try { const externalManager42 = new ExternalModuleManager(); const automatorInfo42 = await externalManager42.getModuleByCode('bma'); @@ -3597,6 +3620,23 @@ async function runTests() { 'External module requirements normalize scalar array entries', ); + automatorSourceFixture42 = await createAutomatorSourceRootFixture(); + runtimeTargetRoot42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-runtime-target-')); + const runtimeBmadDir42 = path.join(runtimeTargetRoot42, '_bmad'); + const officialModules42 = new OfficialModules(); + officialModules42.findModuleSource = async () => automatorSourceFixture42.sourceRoot; + await officialModules42.install('bma', runtimeBmadDir42, null, { skipModuleInstaller: true, silent: true }); + assert( + await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'scripts', 'story-automator')), + 'BMad Automator source-root install includes runtime helper', + ); + assert( + await fs.pathExists(path.join(runtimeBmadDir42, 'bma', 'bmad-story-automator', 'src', 'story_automator', 'cli.py')), + 'BMad Automator source-root install includes Python runtime source', + ); + await fs.remove(runtimeTargetRoot42).catch(() => {}); + runtimeTargetRoot42 = null; + tempProjectDir42 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-automator-target-')); installedBmadDir42 = await createAutomatorBmadFixture(); @@ -3663,6 +3703,8 @@ async function runTests() { } 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 (runtimeTargetRoot42) await fs.remove(runtimeTargetRoot42).catch(() => {}); } console.log(''); diff --git a/tools/installer/modules/official-modules.js b/tools/installer/modules/official-modules.js index 615daba86..9b4a9106d 100644 --- a/tools/installer/modules/official-modules.js +++ b/tools/installer/modules/official-modules.js @@ -301,6 +301,7 @@ class OfficialModules { } await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig); + await this.copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback); if (!options.skipModuleInstaller) { await this.createModuleDirectories(moduleName, bmadDir, options); @@ -572,6 +573,29 @@ class OfficialModules { } } + async copyAutomatorRuntimeIfNeeded(moduleName, sourcePath, targetPath, fileTrackingCallback = null) { + if (moduleName !== 'bma') return; + + const storyTarget = path.join(targetPath, 'bmad-story-automator'); + if (!(await fs.pathExists(path.join(storyTarget, 'SKILL.md')))) return; + + const repoRoot = path.resolve(sourcePath, '..', '..', '..'); + const runtimeRoot = path.join(repoRoot, 'source'); + const runtimeParts = [ + ['scripts', 'scripts'], + ['src', 'src'], + ]; + + for (const [sourceRel, targetRel] of runtimeParts) { + const sourceDir = path.join(runtimeRoot, sourceRel); + const targetDir = path.join(storyTarget, targetRel); + if (!(await fs.pathExists(sourceDir))) { + throw new Error(`BMad Automator runtime source missing: source/${sourceRel}`); + } + await this.copyModuleWithFiltering(sourceDir, targetDir, fileTrackingCallback); + } + } + /** * Create directories declared in module.yaml's `directories` key * This replaces the security-risky module installer pattern with declarative config