From e3d672d561297e73e3d4f84e3317cadbf877d58e Mon Sep 17 00:00:00 2001 From: Curtis Ide Date: Sat, 14 Feb 2026 09:22:24 -0700 Subject: [PATCH 1/4] fix custom install bug --- tools/cli/lib/ui.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 224d147e3..46e9acab5 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -305,6 +305,7 @@ class UI { // Build custom content config similar to promptCustomContentSource const customPaths = []; const selectedModuleIds = []; + const sources = []; for (const customPath of paths) { const expandedPath = this.expandUserPath(customPath); @@ -333,6 +334,11 @@ class UI { customPaths.push(expandedPath); selectedModuleIds.push(moduleMeta.code); + sources.push({ + path: expandedPath, + id: moduleMeta.code, + name: moduleMeta.name || moduleMeta.code, + }); } if (customPaths.length > 0) { @@ -342,6 +348,9 @@ class UI { hasCustomContent: true, paths: customPaths, selectedModuleIds: selectedModuleIds, + sources, + selected: true, + selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), }, }; } @@ -446,6 +455,7 @@ class UI { // Build custom content config similar to promptCustomContentSource const customPaths = []; const selectedModuleIds = []; + const sources = []; for (const customPath of paths) { const expandedPath = this.expandUserPath(customPath); @@ -474,6 +484,11 @@ class UI { customPaths.push(expandedPath); selectedModuleIds.push(moduleMeta.code); + sources.push({ + path: expandedPath, + id: moduleMeta.code, + name: moduleMeta.name || moduleMeta.code, + }); } if (customPaths.length > 0) { @@ -481,6 +496,9 @@ class UI { hasCustomContent: true, paths: customPaths, selectedModuleIds: selectedModuleIds, + sources, + selected: true, + selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), }; } } else if (!options.yes) { From e2f702a7d0a6fc4d6bc94d1f5636cddc1a09a75c Mon Sep 17 00:00:00 2001 From: Curtis Ide Date: Sat, 14 Feb 2026 10:05:05 -0700 Subject: [PATCH 2/4] fix manager.js --- tools/cli/installers/lib/modules/manager.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index b4acc3aef..f162593b7 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -734,8 +734,10 @@ class ModuleManager { continue; } - // Skip config.yaml templates - we'll generate clean ones with actual values - if (file === 'config.yaml' || file.endsWith('/config.yaml')) { + // Skip module root config.yaml only - generated by config collector with actual values + // Workflow-level config.yaml (e.g. workflows/orchestrate-story/config.yaml) must be copied + // for custom modules that use workflow-specific configuration + if (file === 'config.yaml') { continue; } From 589f65a65448fa7df2ece6a372eab438db908753 Mon Sep 17 00:00:00 2001 From: Curtis Ide Date: Sun, 15 Feb 2026 09:11:18 -0700 Subject: [PATCH 3/4] From PR #1624: added empty module.yaml handling (skip + warn) and removed paths from the config to match promptCustomContentSource() --- tools/cli/lib/ui.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 46e9acab5..8c998e575 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -327,6 +327,11 @@ class UI { continue; } + if (!moduleMeta) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`); + continue; + } + if (!moduleMeta.code) { await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); continue; @@ -346,11 +351,10 @@ class UI { selectedCustomModules: selectedModuleIds, customContentConfig: { hasCustomContent: true, - paths: customPaths, - selectedModuleIds: selectedModuleIds, - sources, selected: true, + sources, selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), + selectedModuleIds: selectedModuleIds, }, }; } @@ -477,6 +481,11 @@ class UI { continue; } + if (!moduleMeta) { + await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`); + continue; + } + if (!moduleMeta.code) { await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); continue; @@ -494,11 +503,10 @@ class UI { if (customPaths.length > 0) { customContentConfig = { hasCustomContent: true, - paths: customPaths, - selectedModuleIds: selectedModuleIds, - sources, selected: true, + sources, selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), + selectedModuleIds: selectedModuleIds, }; } } else if (!options.yes) { From caff2fb815220791f65dffaa9db59afbf19915c2 Mon Sep 17 00:00:00 2001 From: Curtis Ide Date: Sun, 15 Feb 2026 09:19:29 -0700 Subject: [PATCH 4/4] fix: custom-content quick-update ENOENT, pass --custom-content through, add PR#1624 improvements to allow update installs to work using non-interactive mode --- tools/cli/installers/lib/core/installer.js | 155 ++++++++++++--------- tools/cli/lib/ui.js | 41 +++++- 2 files changed, 127 insertions(+), 69 deletions(-) diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index b7197d44d..8ee4960d8 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -527,28 +527,30 @@ class Installer { const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; + const moduleId = cachedModule.name; + const cachedPath = path.join(cacheDir, moduleId); - // Skip if we already have this module from manifest - if (customModulePaths.has(moduleId)) { - continue; - } + // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT + if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) { + continue; + } - // Check if this is an external official module - skip cache for those - const isExternal = await this.moduleManager.isExternalModule(moduleId); - if (isExternal) { - // External modules are handled via cloneExternalModule, not from cache - continue; - } + // Skip if we already have this module from manifest + if (customModulePaths.has(moduleId)) { + continue; + } - const cachedPath = path.join(cacheDir, moduleId); + // Check if this is an external official module - skip cache for those + const isExternal = await this.moduleManager.isExternalModule(moduleId); + if (isExternal) { + // External modules are handled via cloneExternalModule, not from cache + continue; + } - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModulePaths.set(moduleId, cachedPath); - } + // Check if this is actually a custom module (has module.yaml) + const moduleYamlPath = path.join(cachedPath, 'module.yaml'); + if (await fs.pathExists(moduleYamlPath)) { + customModulePaths.set(moduleId, cachedPath); } } @@ -609,28 +611,30 @@ class Installer { const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; + const moduleId = cachedModule.name; + const cachedPath = path.join(cacheDir, moduleId); - // Skip if we already have this module from manifest - if (customModulePaths.has(moduleId)) { - continue; - } + // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT + if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) { + continue; + } - // Check if this is an external official module - skip cache for those - const isExternal = await this.moduleManager.isExternalModule(moduleId); - if (isExternal) { - // External modules are handled via cloneExternalModule, not from cache - continue; - } + // Skip if we already have this module from manifest + if (customModulePaths.has(moduleId)) { + continue; + } - const cachedPath = path.join(cacheDir, moduleId); + // Check if this is an external official module - skip cache for those + const isExternal = await this.moduleManager.isExternalModule(moduleId); + if (isExternal) { + // External modules are handled via cloneExternalModule, not from cache + continue; + } - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - customModulePaths.set(moduleId, cachedPath); - } + // Check if this is actually a custom module (has module.yaml) + const moduleYamlPath = path.join(cachedPath, 'module.yaml'); + if (await fs.pathExists(moduleYamlPath)) { + customModulePaths.set(moduleId, cachedPath); } } @@ -949,12 +953,11 @@ class Installer { if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) { customInfo = config._customModuleSources.get(moduleName); isCustomModule = true; - if ( - customInfo.sourcePath && - (customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) && - !customInfo.path - ) - customInfo.path = customInfo.sourcePath; + if (customInfo.sourcePath && !customInfo.path) { + customInfo.path = path.isAbsolute(customInfo.sourcePath) + ? customInfo.sourcePath + : path.join(bmadDir, customInfo.sourcePath); + } } // Finally check regular custom content @@ -2373,41 +2376,58 @@ class Installer { const configuredIdes = existingInstall.ides || []; const projectRoot = path.dirname(bmadDir); - // Get custom module sources from cache + // Get custom module sources: first from --custom-content (re-cache from source), then from cache const customModuleSources = new Map(); + if (config.customContent?.sources?.length > 0) { + for (const source of config.customContent.sources) { + if (source.id && source.path && (await fs.pathExists(source.path))) { + customModuleSources.set(source.id, { + id: source.id, + name: source.name || source.id, + sourcePath: source.path, + cached: false, // From CLI, will be re-cached + }); + } + } + } const cacheDir = path.join(bmadDir, '_config', 'custom'); if (await fs.pathExists(cacheDir)) { const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); for (const cachedModule of cachedModules) { - if (cachedModule.isDirectory()) { - const moduleId = cachedModule.name; + const moduleId = cachedModule.name; + const cachedPath = path.join(cacheDir, moduleId); - // Skip if we already have this module from manifest - if (customModuleSources.has(moduleId)) { - continue; - } + // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT + if (!(await fs.pathExists(cachedPath))) { + continue; + } + if (!cachedModule.isDirectory()) { + continue; + } - // Check if this is an external official module - skip cache for those - const isExternal = await this.moduleManager.isExternalModule(moduleId); - if (isExternal) { - // External modules are handled via cloneExternalModule, not from cache - continue; - } + // Skip if we already have this module from manifest + if (customModuleSources.has(moduleId)) { + continue; + } - const cachedPath = path.join(cacheDir, moduleId); + // Check if this is an external official module - skip cache for those + const isExternal = await this.moduleManager.isExternalModule(moduleId); + if (isExternal) { + // External modules are handled via cloneExternalModule, not from cache + continue; + } - // Check if this is actually a custom module (has module.yaml) - const moduleYamlPath = path.join(cachedPath, 'module.yaml'); - if (await fs.pathExists(moduleYamlPath)) { - // For quick update, we always rebuild from cache - customModuleSources.set(moduleId, { - id: moduleId, - name: moduleId, // We'll read the actual name if needed - sourcePath: cachedPath, - cached: true, // Flag to indicate this is from cache - }); - } + // Check if this is actually a custom module (has module.yaml) + const moduleYamlPath = path.join(cachedPath, 'module.yaml'); + if (await fs.pathExists(moduleYamlPath)) { + // For quick update, we always rebuild from cache + customModuleSources.set(moduleId, { + id: moduleId, + name: moduleId, // We'll read the actual name if needed + sourcePath: cachedPath, + cached: true, // Flag to indicate this is from cache + }); } } } @@ -2544,6 +2564,7 @@ class Installer { _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer _customModuleSources: customModuleSources, // Pass custom module sources for updates _existingModules: installedModules, // Pass all installed modules for manifest generation + customContent: config.customContent, // Pass through for re-caching from source }; // Call the standard install method diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 8c998e575..ae99e300f 100644 --- a/tools/cli/lib/ui.js +++ b/tools/cli/lib/ui.js @@ -245,11 +245,48 @@ class UI { // Handle quick update separately if (actionType === 'quick-update') { - // Quick update doesn't install custom content - just updates existing modules + // Pass --custom-content through so installer can re-cache if cache is missing + let customContentForQuickUpdate = { hasCustomContent: false }; + if (options.customContent) { + const paths = options.customContent + .split(',') + .map((p) => p.trim()) + .filter(Boolean); + if (paths.length > 0) { + const customPaths = []; + const selectedModuleIds = []; + const sources = []; + for (const customPath of paths) { + const expandedPath = this.expandUserPath(customPath); + const validation = this.validateCustomContentPathSync(expandedPath); + if (validation) continue; + let moduleMeta; + try { + const moduleYamlPath = path.join(expandedPath, 'module.yaml'); + moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8')); + } catch { + continue; + } + if (!moduleMeta?.code) continue; + customPaths.push(expandedPath); + selectedModuleIds.push(moduleMeta.code); + sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code }); + } + if (customPaths.length > 0) { + customContentForQuickUpdate = { + hasCustomContent: true, + selected: true, + sources, + selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), + selectedModuleIds, + }; + } + } + } return { actionType: 'quick-update', directory: confirmedDirectory, - customContent: { hasCustomContent: false }, + customContent: customContentForQuickUpdate, skipPrompts: options.yes || false, }; }