fix installer to discover and install local src/modules

- listAvailable() now scans src/modules/ so local modules appear in the UI
- findModuleSource() checks src/modules/{code} before falling back to GitHub
- Handle menu object format (menu.items) alongside legacy flat array in
  vendorCrossModuleWorkflows, agent compiler, and agent analyzer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Sandberg 2026-02-21 16:28:49 +01:00
parent ab8f412c3c
commit 7d6180b6a2
3 changed files with 33 additions and 5 deletions

View File

@ -204,6 +204,21 @@ class ModuleManager {
}
}
// Scan src/modules/ for locally-defined modules
const localModulesPath = getSourcePath('modules');
if (await fs.pathExists(localModulesPath)) {
const moduleEntries = await fs.readdir(localModulesPath, { withFileTypes: true });
for (const entry of moduleEntries) {
if (entry.isDirectory()) {
const modulePath = path.join(localModulesPath, entry.name);
const moduleInfo = await this.getModuleInfo(modulePath, entry.name, 'src/modules');
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id)) {
modules.push(moduleInfo);
}
}
}
}
// Check for cached custom modules in _config/custom/
if (this.bmadDir) {
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
@ -308,6 +323,12 @@ class ModuleManager {
}
}
// Check src/modules/{moduleCode} for locally-defined modules (takes priority over external)
const localModulePath = getModulePath(moduleCode);
if (await fs.pathExists(localModulePath)) {
return localModulePath;
}
// Check external official modules
const externalSource = await this.findExternalModuleSource(moduleCode, options);
if (externalSource) {
@ -1150,7 +1171,9 @@ class ModuleManager {
const agentYaml = yaml.parse(await fs.readFile(agentPath, 'utf8'));
// Check if agent has menu items with workflow-install
const menuItems = agentYaml?.agent?.menu || [];
// menu can be an array OR an object with an `items` array
const menu = agentYaml?.agent?.menu;
const menuItems = Array.isArray(menu) ? menu : menu?.items || [];
const workflowInstallItems = menuItems.filter((item) => item['workflow-install']);
if (workflowInstallItems.length === 0) {

View File

@ -23,7 +23,9 @@ class AgentAnalyzer {
}
// Analyze menu items (support both 'menu' and legacy 'commands')
const menuItems = agentYaml.agent?.menu || agentYaml.agent?.commands || [];
// menu can be an array or an object with an `items` array
const rawMenu = agentYaml.agent?.menu || agentYaml.agent?.commands;
const menuItems = Array.isArray(rawMenu) ? rawMenu : rawMenu?.items || [];
for (const item of menuItems) {
// Track the menu item

View File

@ -309,8 +309,10 @@ async function compileToXml(agentYaml, agentName = '', targetPath = '') {
xml += buildMemoriesXml(agent.memories);
}
// Menu section
xml += buildMenuXml(agent.menu || []);
// Menu section — menu can be an array or an object with an `items` array
const menuData = agent.menu;
const menuItems = Array.isArray(menuData) ? menuData : menuData?.items || [];
xml += buildMenuXml(menuItems);
// Closing agent tag
xml += '</agent>\n';
@ -397,7 +399,8 @@ async function compileAgent(yamlContent, answers = {}, agentName = '', targetPat
// For menu: append to existing or create new
if (customizations.menu) {
const existing = agentYaml.agent.menu || [];
const existingMenu = agentYaml.agent.menu;
const existing = Array.isArray(existingMenu) ? existingMenu : existingMenu?.items || [];
agentYaml.agent.menu = [...existing, ...customizations.menu];
}