From 31d115290a4ab07b6c9df6b48f141a878eaf9cef Mon Sep 17 00:00:00 2001 From: pbean Date: Sat, 20 Jun 2026 15:58:35 -0700 Subject: [PATCH] fix(bmad-module): surface malformed plugin.json + fail CI custom-source resolve - bmad-module-lib: keep the legacy-fallback null return for malformed .claude-plugin/plugin.json, but warn so corruption is no longer indistinguishable from a missing file. - ui: in the non-interactive custom-source path, collect resolution failures and throw after attempting every source (and when a source yields no module), so a CI/--custom-source install fails instead of silently omitting the module. Co-Authored-By: Claude Opus 4.8 --- tools/installer/modules/bmad-module-lib.js | 8 +++++--- tools/installer/ui.js | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tools/installer/modules/bmad-module-lib.js b/tools/installer/modules/bmad-module-lib.js index 8a22c5995..a6021fb1c 100644 --- a/tools/installer/modules/bmad-module-lib.js +++ b/tools/installer/modules/bmad-module-lib.js @@ -81,9 +81,11 @@ async function readPluginManifest(dir) { if (parsed && typeof parsed === 'object' && parsed.bmad && typeof parsed.bmad === 'object') { return parsed; } - } catch { - // Malformed JSON — treat as "not a new-system module" and let the legacy - // resolver (or validateDeclaredPaths at install time) surface the problem. + } catch (error) { + // Malformed JSON — fall back to the legacy resolver (or validateDeclaredPaths + // at install time) rather than hard-failing, but warn so the corruption is + // not swallowed silently and looks indistinguishable from a missing file. + process.stderr.write(`[bmad-module] warning: ignoring invalid JSON in ${manifestPath}: ${error.message}\n`); } return null; } diff --git a/tools/installer/ui.js b/tools/installer/ui.js index d5756c6d8..86ae02706 100644 --- a/tools/installer/ui.js +++ b/tools/installer/ui.js @@ -1231,6 +1231,12 @@ class UI { .map((s) => s.trim()) .filter(Boolean); + // Non-interactive mode: a source the user explicitly asked for must not be + // silently dropped. Collect failures and throw after attempting every source + // so the install fails (non-zero exit) instead of completing with the + // requested module missing. + const failures = []; + for (const source of sources) { const s = await prompts.spinner(); s.start(`Resolving ${source}...`); @@ -1242,6 +1248,7 @@ class UI { } catch (error) { s.error(`Failed to resolve ${source}`); await prompts.log.error(` ${error.message}`); + failures.push(`${source}: ${error.message}`); continue; } @@ -1267,6 +1274,7 @@ class UI { } catch (discoverError) { s2.error('Failed to discover modules'); await prompts.log.error(` ${discoverError.message}`); + failures.push(`${source}: ${discoverError.message}`); continue; } } else { @@ -1299,12 +1307,20 @@ class UI { const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath); allResolved.push(...resolved); } catch (resolveError) { - await prompts.log.warn(` Could not resolve ${source}: ${resolveError.message}`); + s2.error(`Failed to resolve ${source}`); + await prompts.log.error(` ${resolveError.message}`); + failures.push(`${source}: ${resolveError.message}`); + continue; } } } s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`); + if (allResolved.length === 0) { + failures.push(`${source}: no installable module found at this source`); + continue; + } + for (const mod of allResolved) { allCodes.push(mod.code); const versionStr = mod.version ? ` v${mod.version}` : ''; @@ -1312,6 +1328,10 @@ class UI { } } + if (failures.length > 0) { + throw new Error(`Could not resolve ${failures.length} custom source(s):\n - ${failures.join('\n - ')}`); + } + return allCodes; }