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:
parent
1937552da3
commit
382ab8ed45
|
|
@ -527,8 +527,13 @@ 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 path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
||||||
|
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if we already have this module from manifest
|
// Skip if we already have this module from manifest
|
||||||
if (customModulePaths.has(moduleId)) {
|
if (customModulePaths.has(moduleId)) {
|
||||||
|
|
@ -542,15 +547,12 @@ class Installer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedPath = path.join(cacheDir, moduleId);
|
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update module manager with the new custom module paths from cache
|
// Update module manager with the new custom module paths from cache
|
||||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
|
|
@ -609,8 +611,13 @@ 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 path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
||||||
|
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if we already have this module from manifest
|
// Skip if we already have this module from manifest
|
||||||
if (customModulePaths.has(moduleId)) {
|
if (customModulePaths.has(moduleId)) {
|
||||||
|
|
@ -624,15 +631,12 @@ class Installer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedPath = path.join(cacheDir, moduleId);
|
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update module manager with the new custom module paths from cache
|
// Update module manager with the new custom module paths from cache
|
||||||
this.moduleManager.setCustomModulePaths(customModulePaths);
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
|
|
@ -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,15 +2376,35 @@ 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 path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
||||||
|
if (!(await fs.pathExists(cachedPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!cachedModule.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if we already have this module from manifest
|
// Skip if we already have this module from manifest
|
||||||
if (customModuleSources.has(moduleId)) {
|
if (customModuleSources.has(moduleId)) {
|
||||||
|
|
@ -2395,8 +2418,6 @@ class Installer {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedPath = path.join(cacheDir, moduleId);
|
|
||||||
|
|
||||||
// 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)) {
|
||||||
|
|
@ -2410,7 +2431,6 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Load saved IDE configurations
|
// Load saved IDE configurations
|
||||||
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue