From 382ab8ed45551029e67ee2142af4742dbee91709 Mon Sep 17 00:00:00 2001 From: Curtis Ide <60450113+cidemaxio@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:44:28 -0700 Subject: [PATCH] Fix: --custom-content flag and workflow config.yaml copying (#1651) * fix custom install bug * fix manager.js * From PR #1624: added empty module.yaml handling (skip + warn) and removed paths from the config to match promptCustomContentSource() * 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/installers/lib/modules/manager.js | 6 +- tools/cli/lib/ui.js | 71 ++++++++- 3 files changed, 159 insertions(+), 73 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/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; } diff --git a/tools/cli/lib/ui.js b/tools/cli/lib/ui.js index 224d147e3..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, }; } @@ -305,6 +342,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); @@ -326,6 +364,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; @@ -333,6 +376,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) { @@ -340,7 +388,9 @@ class UI { selectedCustomModules: selectedModuleIds, customContentConfig: { hasCustomContent: true, - paths: customPaths, + selected: true, + sources, + selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), selectedModuleIds: selectedModuleIds, }, }; @@ -446,6 +496,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); @@ -467,6 +518,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; @@ -474,12 +530,19 @@ 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) { customContentConfig = { hasCustomContent: true, - paths: customPaths, + selected: true, + sources, + selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')), selectedModuleIds: selectedModuleIds, }; }