From 0fc644cead1e37db854854aafcb74786c36e562d Mon Sep 17 00:00:00 2001 From: cidemaxio Date: Fri, 24 Apr 2026 15:12:34 +0000 Subject: [PATCH] fix(installer): support local custom-source modules in resolveInstalledModuleYaml and TOML key - resolveInstalledModuleYaml: fall back to CustomModuleManager._resolutionCache for local custom-source modules (external cache path doesn't exist for these); refactor candidate-path search into shared searchRoot() helper; add *-setup/assets/module.yaml BMB standard path - manifest-generator: use module code field (not display name) as TOML section key [modules.X] --- tools/installer/core/manifest-generator.js | 11 +++- tools/installer/project-root.js | 62 ++++++++++++++++------ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/tools/installer/core/manifest-generator.js b/tools/installer/core/manifest-generator.js index eb1012036..f7b5d0084 100644 --- a/tools/installer/core/manifest-generator.js +++ b/tools/installer/core/manifest-generator.js @@ -435,6 +435,9 @@ class ManifestGenerator { // this means user-scoped keys (e.g. user_name) could mis-file into the // team config, so the operator should notice. const scopeByModuleKey = {}; + // Maps installer moduleName (may be full display name) → module code field + // from module.yaml, so TOML sections use [modules.] not [modules.]. + const codeByModuleName = {}; for (const moduleName of this.updatedModules) { const moduleYamlPath = await resolveInstalledModuleYaml(moduleName); if (!moduleYamlPath) { @@ -447,6 +450,7 @@ class ManifestGenerator { try { const parsed = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8')); if (!parsed || typeof parsed !== 'object') continue; + if (parsed.code) codeByModuleName[moduleName] = parsed.code; scopeByModuleKey[moduleName] = {}; for (const [key, value] of Object.entries(parsed)) { if (value && typeof value === 'object' && 'prompt' in value) { @@ -545,6 +549,9 @@ class ManifestGenerator { if (moduleName === 'core') continue; const cfg = moduleConfigs[moduleName]; if (!cfg || Object.keys(cfg).length === 0) continue; + // Use the module's code field from module.yaml as the TOML key so the + // section is [modules.mdo] not [modules.MDO: Maxio DevOps Operations]. + const sectionKey = codeByModuleName[moduleName] || moduleName; // Only filter out spread-from-core pollution when we actually know // this module's prompt schema. For external/marketplace modules whose // module.yaml isn't in the src tree, fall through as all-team so we @@ -552,14 +559,14 @@ class ManifestGenerator { const haveSchema = Object.keys(scopeByModuleKey[moduleName] || {}).length > 0; const { team: modTeam, user: modUser } = partition(moduleName, cfg, haveSchema); if (Object.keys(modTeam).length > 0) { - teamLines.push(`[modules.${moduleName}]`); + teamLines.push(`[modules.${sectionKey}]`); for (const [key, value] of Object.entries(modTeam)) { teamLines.push(`${key} = ${formatTomlValue(value)}`); } teamLines.push(''); } if (Object.keys(modUser).length > 0) { - userLines.push(`[modules.${moduleName}]`); + userLines.push(`[modules.${sectionKey}]`); for (const [key, value] of Object.entries(modUser)) { userLines.push(`${key} = ${formatTomlValue(value)}`); } diff --git a/tools/installer/project-root.js b/tools/installer/project-root.js index 1cdc30566..123bd5978 100644 --- a/tools/installer/project-root.js +++ b/tools/installer/project-root.js @@ -86,6 +86,8 @@ function getExternalModuleCachePath(moduleName, ...segments) { * Built-in modules (core, bmm) live under . External official modules are * cloned into ~/.bmad/cache/external-modules// with varying internal * layouts (some at src/module.yaml, some at skills/module.yaml, some nested). + * Local custom-source modules are not cached; their path is read from the + * CustomModuleManager resolution cache set during the same install run. * This mirrors the candidate-path search in * ExternalModuleManager.findExternalModuleSource but performs no git/network * work, which keeps it safe to call during manifest writing. @@ -97,26 +99,56 @@ async function resolveInstalledModuleYaml(moduleName) { const builtIn = path.join(getModulePath(moduleName), 'module.yaml'); if (await fs.pathExists(builtIn)) return builtIn; - const cacheRoot = getExternalModuleCachePath(moduleName); - if (!(await fs.pathExists(cacheRoot))) return null; + // Search a resolved root directory using the same candidate-path pattern. + async function searchRoot(root) { + for (const dir of ['skills', 'src']) { + const direct = path.join(root, dir, 'module.yaml'); + if (await fs.pathExists(direct)) return direct; - for (const dir of ['skills', 'src']) { - const direct = path.join(cacheRoot, dir, 'module.yaml'); - if (await fs.pathExists(direct)) return direct; - - const dirPath = path.join(cacheRoot, dir); - if (await fs.pathExists(dirPath)) { - const entries = await fs.readdir(dirPath, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const nested = path.join(dirPath, entry.name, 'module.yaml'); - if (await fs.pathExists(nested)) return nested; + const dirPath = path.join(root, dir); + if (await fs.pathExists(dirPath)) { + const entries = await fs.readdir(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const nested = path.join(dirPath, entry.name, 'module.yaml'); + if (await fs.pathExists(nested)) return nested; + } } } + + // BMB standard: {setup-skill}/assets/module.yaml (setup skill is any *-setup directory) + const rootEntries = await fs.readdir(root, { withFileTypes: true }); + for (const entry of rootEntries) { + if (!entry.isDirectory() || !entry.name.endsWith('-setup')) continue; + const setupAssets = path.join(root, entry.name, 'assets', 'module.yaml'); + if (await fs.pathExists(setupAssets)) return setupAssets; + } + + const atRoot = path.join(root, 'module.yaml'); + if (await fs.pathExists(atRoot)) return atRoot; + return null; } - const atRoot = path.join(cacheRoot, 'module.yaml'); - if (await fs.pathExists(atRoot)) return atRoot; + const cacheRoot = getExternalModuleCachePath(moduleName); + if (await fs.pathExists(cacheRoot)) { + const found = await searchRoot(cacheRoot); + if (found) return found; + } + + // Fallback: local custom-source modules store their source path in the + // CustomModuleManager resolution cache populated during the same install run. + // Match by code OR name since callers may use either form. + try { + const { CustomModuleManager } = require('./modules/custom-module-manager'); + for (const [, mod] of CustomModuleManager._resolutionCache) { + if ((mod.code === moduleName || mod.name === moduleName) && mod.localPath) { + const found = await searchRoot(mod.localPath); + if (found) return found; + } + } + } catch { + // Resolution cache unavailable — continue + } return null; }