fix(installer): support automator source-root installs
This commit is contained in:
parent
0eae7c4352
commit
dd55226d4b
|
|
@ -85,6 +85,23 @@ async function createTestBmadFixture() {
|
||||||
return fixtureDir;
|
return fixtureDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createSourceRootModuleFixture() {
|
||||||
|
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-module-'));
|
||||||
|
const sourceRoot = path.join(repoRoot, 'skills');
|
||||||
|
const skillNames = ['bmad-story-automator', 'bmad-story-automator-review'];
|
||||||
|
|
||||||
|
for (const skillName of skillNames) {
|
||||||
|
const skillDir = path.join(sourceRoot, skillName);
|
||||||
|
await fs.ensureDir(skillDir);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(skillDir, 'SKILL.md'),
|
||||||
|
['---', `name: ${skillName}`, `description: ${skillName} description`, '---', '', `${skillName} body`].join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { repoRoot, sourceRoot, skillNames };
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -164,6 +181,70 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test 4b: Source-root external modules
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 4b: Source-Root External Modules${colors.reset}\n`);
|
||||||
|
|
||||||
|
{
|
||||||
|
let sourceRootFixture;
|
||||||
|
let tempProjectDir;
|
||||||
|
try {
|
||||||
|
const { ExternalModuleManager } = require('../tools/installer/modules/external-manager');
|
||||||
|
const manager = new ExternalModuleManager();
|
||||||
|
|
||||||
|
const normalizedModule = manager._normalizeModule({
|
||||||
|
name: 'bmad-automator',
|
||||||
|
code: 'baut',
|
||||||
|
repository: 'https://github.com/bmad-code-org/bmad-automator',
|
||||||
|
source_root: 'skills',
|
||||||
|
type: 'experimental',
|
||||||
|
});
|
||||||
|
assert(normalizedModule.sourceRoot === 'skills', 'External module normalization preserves source_root');
|
||||||
|
|
||||||
|
sourceRootFixture = await createSourceRootModuleFixture();
|
||||||
|
tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-source-root-project-'));
|
||||||
|
|
||||||
|
manager.getModuleByCode = async () => normalizedModule;
|
||||||
|
manager.cloneExternalModule = async () => sourceRootFixture.repoRoot;
|
||||||
|
|
||||||
|
const resolvedSourceRoot = await manager.findExternalModuleSource('baut');
|
||||||
|
assert(resolvedSourceRoot === sourceRootFixture.sourceRoot, 'External module source_root resolves to the configured directory');
|
||||||
|
|
||||||
|
const officialModules = new OfficialModules();
|
||||||
|
officialModules.findModuleSource = async () => sourceRootFixture.sourceRoot;
|
||||||
|
await officialModules.install('baut', path.join(tempProjectDir, '_bmad'), null, {
|
||||||
|
skipModuleInstaller: true,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const skillName of sourceRootFixture.skillNames) {
|
||||||
|
assert(
|
||||||
|
await fs.pathExists(path.join(tempProjectDir, '_bmad', 'baut', skillName, 'SKILL.md')),
|
||||||
|
`Source-root install copies ${skillName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.getModuleByCode = async () => ({
|
||||||
|
code: 'escape',
|
||||||
|
sourceRoot: '../outside',
|
||||||
|
builtIn: false,
|
||||||
|
});
|
||||||
|
let rejectedEscapingSourceRoot = false;
|
||||||
|
try {
|
||||||
|
await manager.findExternalModuleSource('escape');
|
||||||
|
} catch (error) {
|
||||||
|
rejectedEscapingSourceRoot = error.message.includes('source-root escapes repository');
|
||||||
|
}
|
||||||
|
assert(rejectedEscapingSourceRoot, 'External module source-root cannot escape cloned repository');
|
||||||
|
} finally {
|
||||||
|
if (sourceRootFixture) await fs.remove(sourceRootFixture.repoRoot).catch(() => {});
|
||||||
|
if (tempProjectDir) await fs.remove(tempProjectDir).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Test 5: Kiro Native Skills Install
|
// Test 5: Kiro Native Skills Install
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ 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'] || 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 || '',
|
||||||
|
|
@ -471,6 +472,19 @@ 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 repoRoot = path.resolve(cloneDir);
|
||||||
|
const sourceRoot = path.resolve(repoRoot, moduleInfo.sourceRoot);
|
||||||
|
const relativeSourceRoot = path.relative(repoRoot, sourceRoot);
|
||||||
|
if (relativeSourceRoot === '..' || relativeSourceRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeSourceRoot)) {
|
||||||
|
throw new Error(`External module '${moduleCode}' source-root escapes repository: ${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'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue