diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 4e5fa7282..0b792cef7 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -126,6 +126,53 @@ async function createSkillCollisionFixture() { return { root: fixtureRoot, bmadDir: fixtureDir }; } +async function createCustomModuleManifestFixture() { + const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-manifest-')); + const bmadDir = path.join(fixtureRoot, '_bmad'); + const configDir = path.join(bmadDir, '_config'); + await fs.ensureDir(configDir); + + const minimalAgent = 'p'; + await fs.ensureDir(path.join(bmadDir, 'core', 'agents')); + await fs.writeFile(path.join(bmadDir, 'core', 'agents', 'test.md'), minimalAgent); + await fs.ensureDir(path.join(bmadDir, 'test-module', 'agents')); + await fs.writeFile(path.join(bmadDir, 'test-module', 'agents', 'test.md'), minimalAgent); + + await fs.writeFile( + path.join(configDir, 'manifest.yaml'), + [ + 'installation:', + ' version: 6.2.2', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + 'modules:', + ' - name: core', + ' version: 6.2.2', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + ' source: built-in', + ' npmPackage: null', + ' repoUrl: null', + ' - name: test-module', + ' version: null', + ' installDate: 2026-03-30T00:00:00.000Z', + ' lastUpdated: 2026-03-30T00:00:00.000Z', + ' source: custom', + ' npmPackage: null', + ' repoUrl: null', + 'customModules:', + ' - id: test-module', + ' name: "Test Module"', + ' sourcePath: "/tmp/test-module-source"', + 'ides:', + ' - codex', + '', + ].join('\n'), + ); + + return { root: fixtureRoot, bmadDir, manifestPath: path.join(configDir, 'manifest.yaml') }; +} + /** * Test Suite */ @@ -1713,6 +1760,35 @@ async function runTests() { console.log(''); + // ============================================================ + // Suite 33: Main manifest preserves customModules + // ============================================================ + console.log(`${colors.yellow}Test Suite 33: Preserve customModules in main manifest${colors.reset}\n`); + + let customManifestFixture = null; + try { + customManifestFixture = await createCustomModuleManifestFixture(); + const generator33 = new ManifestGenerator(); + await generator33.generateManifests(customManifestFixture.bmadDir, ['core', 'test-module'], [], { ides: ['codex'] }); + + const yaml = require('yaml'); + const updatedManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8')); + const customModule = updatedManifest.customModules?.find((entry) => entry.id === 'test-module'); + + assert(Array.isArray(updatedManifest.customModules), 'Main manifest keeps customModules array'); + assert(customModule !== undefined, 'Main manifest preserves existing custom module entry'); + assert( + customModule && customModule.sourcePath === '/tmp/test-module-source', + 'Main manifest preserves custom module sourcePath', + ); + } catch (error) { + assert(false, 'Main manifest preserves customModules test succeeds', error.message); + } finally { + if (customManifestFixture?.root) await fs.remove(customManifestFixture.root).catch(() => { }); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 111c88b54..7beeb847d 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -1144,8 +1144,35 @@ class Installer { const configuredIdes = existingInstall.ides; const projectRoot = path.dirname(bmadDir); - // Get custom module sources: first from --custom-content (re-cache from source), then from cache + // Get custom module sources: first from manifest, then from --custom-content, then from cache const customModuleSources = new Map(); + if (existingInstall.customModules) { + for (const customModule of existingInstall.customModules) { + if (!customModule?.id) continue; + + let sourcePath = customModule.sourcePath; + if (sourcePath && sourcePath.startsWith('_config')) { + sourcePath = path.join(bmadDir, sourcePath); + } else if (!sourcePath && customModule.relativePath) { + sourcePath = path.resolve(projectRoot, customModule.relativePath); + } else if (sourcePath && !path.isAbsolute(sourcePath)) { + sourcePath = path.resolve(sourcePath); + } + + if (!sourcePath || !(await fs.pathExists(sourcePath))) { + continue; + } + + customModuleSources.set(customModule.id, { + id: customModule.id, + name: customModule.name || customModule.id, + sourcePath, + relativePath: customModule.relativePath, + cached: false, + }); + } + } + if (config.customContent?.sources?.length > 0) { for (const source of config.customContent.sources) { if (source.id && source.path && (await fs.pathExists(source.path))) { diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index 65e0f4ed3..f96081494 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -381,6 +381,7 @@ class ManifestGenerator { // Read existing manifest to preserve install date let existingInstallDate = null; const existingModulesMap = new Map(); + let existingCustomModules = []; if (await fs.pathExists(manifestPath)) { try { @@ -402,6 +403,10 @@ class ManifestGenerator { } } } + + if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) { + existingCustomModules = existingManifest.customModules; + } } catch { // If we can't read existing manifest, continue with defaults } @@ -437,6 +442,7 @@ class ManifestGenerator { lastUpdated: new Date().toISOString(), }, modules: updatedModules, + customModules: existingCustomModules, ides: this.selectedIdes, };