BMAD-METHOD/tools/installer/modules/bma-runtime-package.js

84 lines
3.1 KiB
JavaScript

const path = require('node:path');
const fs = require('../fs-native');
const STORY_SKILL = 'bmad-story-automator';
const REVIEW_SKILL = 'bmad-story-automator-review';
async function installBmaRuntimePackage(moduleName, sourcePath, targetPath, fileTrackingCallback = null) {
if (moduleName !== 'bma') return false;
const storyTarget = path.join(targetPath, STORY_SKILL);
const reviewTarget = path.join(targetPath, REVIEW_SKILL);
if (!(await fs.pathExists(path.join(storyTarget, 'SKILL.md')))) {
throw new Error(`BMad Automator payload missing installed skill: ${STORY_SKILL}`);
}
if (!(await fs.pathExists(path.join(reviewTarget, 'SKILL.md')))) {
throw new Error(`BMad Automator payload missing installed skill: ${REVIEW_SKILL}`);
}
const repoRoot = await findAutomatorRepoRoot(sourcePath);
const sourceRoot = path.join(repoRoot, 'source');
const runtimeFiles = {
pyproject: path.join(sourceRoot, 'pyproject.toml'),
readme: path.join(sourceRoot, 'README.md'),
license: path.join(sourceRoot, 'LICENSE'),
scripts: path.join(sourceRoot, 'scripts'),
src: path.join(sourceRoot, 'src'),
};
for (const [label, requiredPath] of Object.entries(runtimeFiles)) {
if (!(await fs.pathExists(requiredPath))) {
throw new Error(`BMad Automator runtime ${label} missing: ${requiredPath}`);
}
}
await copyTracked(runtimeFiles.pyproject, path.join(storyTarget, 'pyproject.toml'), fileTrackingCallback);
await copyTracked(runtimeFiles.readme, path.join(storyTarget, 'README.md'), fileTrackingCallback);
await copyTracked(runtimeFiles.license, path.join(storyTarget, 'LICENSE'), fileTrackingCallback);
await copyTracked(runtimeFiles.scripts, path.join(storyTarget, 'scripts'), fileTrackingCallback);
await copyTracked(runtimeFiles.src, path.join(storyTarget, 'src'), fileTrackingCallback);
await fs.chmod(path.join(storyTarget, 'scripts', 'story-automator'), 0o755);
return true;
}
async function findAutomatorRepoRoot(sourcePath) {
let current = path.resolve(sourcePath);
for (let depth = 0; depth < 8; depth += 1) {
if (
(await fs.pathExists(path.join(current, 'source', 'scripts', 'story-automator'))) &&
(await fs.pathExists(path.join(current, 'payload', '.claude', 'skills', STORY_SKILL, 'SKILL.md')))
) {
return current;
}
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
throw new Error(`BMad Automator runtime source not found above: ${sourcePath}`);
}
async function copyTracked(source, target, fileTrackingCallback) {
await fs.remove(target);
await fs.copy(source, target, { overwrite: true });
await trackRecursive(target, fileTrackingCallback);
}
async function trackRecursive(target, fileTrackingCallback) {
if (!fileTrackingCallback) return;
const stat = await fs.stat(target);
if (stat.isFile()) {
fileTrackingCallback(target);
return;
}
if (!stat.isDirectory()) return;
const entries = await fs.readdir(target, { withFileTypes: true });
for (const entry of entries) {
await trackRecursive(path.join(target, entry.name), fileTrackingCallback);
}
}
module.exports = {
installBmaRuntimePackage,
};