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 <noreply@anthropic.com>
This commit is contained in:
pbean 2026-06-20 15:58:35 -07:00
parent c845e78aab
commit 31d115290a
2 changed files with 26 additions and 4 deletions

View File

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

View File

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