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, }; }