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
This commit is contained in:
Curtis Ide 2026-02-15 14:44:28 -07:00 committed by GitHub
parent 1937552da3
commit 382ab8ed45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 73 deletions

View File

@ -527,28 +527,30 @@ class Installer {
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) { 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 // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
if (customModulePaths.has(moduleId)) { if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
continue; continue;
} }
// Check if this is an external official module - skip cache for those // Skip if we already have this module from manifest
const isExternal = await this.moduleManager.isExternalModule(moduleId); if (customModulePaths.has(moduleId)) {
if (isExternal) { continue;
// External modules are handled via cloneExternalModule, not from cache }
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) // Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml'); const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) { if (await fs.pathExists(moduleYamlPath)) {
customModulePaths.set(moduleId, cachedPath); customModulePaths.set(moduleId, cachedPath);
}
} }
} }
@ -609,28 +611,30 @@ class Installer {
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) { 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 // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
if (customModulePaths.has(moduleId)) { if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
continue; continue;
} }
// Check if this is an external official module - skip cache for those // Skip if we already have this module from manifest
const isExternal = await this.moduleManager.isExternalModule(moduleId); if (customModulePaths.has(moduleId)) {
if (isExternal) { continue;
// External modules are handled via cloneExternalModule, not from cache }
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) // Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml'); const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) { if (await fs.pathExists(moduleYamlPath)) {
customModulePaths.set(moduleId, cachedPath); customModulePaths.set(moduleId, cachedPath);
}
} }
} }
@ -949,12 +953,11 @@ class Installer {
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) { if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
customInfo = config._customModuleSources.get(moduleName); customInfo = config._customModuleSources.get(moduleName);
isCustomModule = true; isCustomModule = true;
if ( if (customInfo.sourcePath && !customInfo.path) {
customInfo.sourcePath && customInfo.path = path.isAbsolute(customInfo.sourcePath)
(customInfo.sourcePath.startsWith('_config') || customInfo.sourcePath.includes('_config/custom')) && ? customInfo.sourcePath
!customInfo.path : path.join(bmadDir, customInfo.sourcePath);
) }
customInfo.path = customInfo.sourcePath;
} }
// Finally check regular custom content // Finally check regular custom content
@ -2373,41 +2376,58 @@ class Installer {
const configuredIdes = existingInstall.ides || []; const configuredIdes = existingInstall.ides || [];
const projectRoot = path.dirname(bmadDir); 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(); 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'); const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) { if (await fs.pathExists(cacheDir)) {
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true }); const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) { 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 // Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
if (customModuleSources.has(moduleId)) { if (!(await fs.pathExists(cachedPath))) {
continue; continue;
} }
if (!cachedModule.isDirectory()) {
continue;
}
// Check if this is an external official module - skip cache for those // Skip if we already have this module from manifest
const isExternal = await this.moduleManager.isExternalModule(moduleId); if (customModuleSources.has(moduleId)) {
if (isExternal) { continue;
// External modules are handled via cloneExternalModule, not from cache }
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) // Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml'); const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) { if (await fs.pathExists(moduleYamlPath)) {
// For quick update, we always rebuild from cache // For quick update, we always rebuild from cache
customModuleSources.set(moduleId, { customModuleSources.set(moduleId, {
id: moduleId, id: moduleId,
name: moduleId, // We'll read the actual name if needed name: moduleId, // We'll read the actual name if needed
sourcePath: cachedPath, sourcePath: cachedPath,
cached: true, // Flag to indicate this is from cache cached: true, // Flag to indicate this is from cache
}); });
}
} }
} }
} }
@ -2544,6 +2564,7 @@ class Installer {
_savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer _savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
_customModuleSources: customModuleSources, // Pass custom module sources for updates _customModuleSources: customModuleSources, // Pass custom module sources for updates
_existingModules: installedModules, // Pass all installed modules for manifest generation _existingModules: installedModules, // Pass all installed modules for manifest generation
customContent: config.customContent, // Pass through for re-caching from source
}; };
// Call the standard install method // Call the standard install method

View File

@ -734,8 +734,10 @@ class ModuleManager {
continue; continue;
} }
// Skip config.yaml templates - we'll generate clean ones with actual values // Skip module root config.yaml only - generated by config collector with actual values
if (file === 'config.yaml' || file.endsWith('/config.yaml')) { // 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; continue;
} }

View File

@ -245,11 +245,48 @@ class UI {
// Handle quick update separately // Handle quick update separately
if (actionType === 'quick-update') { 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 { return {
actionType: 'quick-update', actionType: 'quick-update',
directory: confirmedDirectory, directory: confirmedDirectory,
customContent: { hasCustomContent: false }, customContent: customContentForQuickUpdate,
skipPrompts: options.yes || false, skipPrompts: options.yes || false,
}; };
} }
@ -305,6 +342,7 @@ class UI {
// Build custom content config similar to promptCustomContentSource // Build custom content config similar to promptCustomContentSource
const customPaths = []; const customPaths = [];
const selectedModuleIds = []; const selectedModuleIds = [];
const sources = [];
for (const customPath of paths) { for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath); const expandedPath = this.expandUserPath(customPath);
@ -326,6 +364,11 @@ class UI {
continue; continue;
} }
if (!moduleMeta) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
continue;
}
if (!moduleMeta.code) { if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue; continue;
@ -333,6 +376,11 @@ class UI {
customPaths.push(expandedPath); customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code); selectedModuleIds.push(moduleMeta.code);
sources.push({
path: expandedPath,
id: moduleMeta.code,
name: moduleMeta.name || moduleMeta.code,
});
} }
if (customPaths.length > 0) { if (customPaths.length > 0) {
@ -340,7 +388,9 @@ class UI {
selectedCustomModules: selectedModuleIds, selectedCustomModules: selectedModuleIds,
customContentConfig: { customContentConfig: {
hasCustomContent: true, hasCustomContent: true,
paths: customPaths, selected: true,
sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds, selectedModuleIds: selectedModuleIds,
}, },
}; };
@ -446,6 +496,7 @@ class UI {
// Build custom content config similar to promptCustomContentSource // Build custom content config similar to promptCustomContentSource
const customPaths = []; const customPaths = [];
const selectedModuleIds = []; const selectedModuleIds = [];
const sources = [];
for (const customPath of paths) { for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath); const expandedPath = this.expandUserPath(customPath);
@ -467,6 +518,11 @@ class UI {
continue; continue;
} }
if (!moduleMeta) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
continue;
}
if (!moduleMeta.code) { if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue; continue;
@ -474,12 +530,19 @@ class UI {
customPaths.push(expandedPath); customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code); selectedModuleIds.push(moduleMeta.code);
sources.push({
path: expandedPath,
id: moduleMeta.code,
name: moduleMeta.name || moduleMeta.code,
});
} }
if (customPaths.length > 0) { if (customPaths.length > 0) {
customContentConfig = { customContentConfig = {
hasCustomContent: true, hasCustomContent: true,
paths: customPaths, selected: true,
sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds, selectedModuleIds: selectedModuleIds,
}; };
} }