diff --git a/tools/installer/core/installer.js b/tools/installer/core/installer.js index 6ba197bde..ed04b07d1 100644 --- a/tools/installer/core/installer.js +++ b/tools/installer/core/installer.js @@ -12,6 +12,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); const { InstallPaths } = require('./install-paths'); const { ExternalModuleManager } = require('../modules/external-manager'); const { resolveModuleVersion } = require('../modules/version-resolver'); +const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema'); const { ExistingInstall } = require('./existing-install'); const { warnPreNativeSkillsLegacy } = require('./legacy-warnings'); @@ -942,8 +943,7 @@ class Installer { */ async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) { const allRows = []; - const headerRow = - 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs'; + const headerRow = MODULE_HELP_CSV_HEADER; const COLUMN_COUNT = 13; const PHASE_INDEX = 7; @@ -976,9 +976,19 @@ class Installer { const content = await fs.readFile(helpFilePath, 'utf8'); const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#')); + let headerWarned = false; for (const line of lines) { - // Skip header row + // Header row: warn on drift from canonical schema, then skip. + // Data rows are loaded positionally regardless, so the warning + // is advisory — the maintainer should rename their columns. if (line.startsWith('module,')) { + if (!headerWarned && line.trim() !== headerRow) { + await prompts.log.warn( + ` ${moduleName}/module-help.csv header does not match canonical schema. ` + + `Expected: ${headerRow} | Found: ${line.trim()} | Data loaded positionally.`, + ); + headerWarned = true; + } continue; } diff --git a/tools/installer/modules/module-help-schema.js b/tools/installer/modules/module-help-schema.js new file mode 100644 index 000000000..08951b808 --- /dev/null +++ b/tools/installer/modules/module-help-schema.js @@ -0,0 +1,13 @@ +/** + * Canonical schema for per-module `module-help.csv` files. + * + * Both the merger (`Installer.mergeModuleHelpCatalogs`) and the synthesizer + * (`PluginResolver._buildSynthesizedHelpCsv`) emit this exact header. The + * merger compares each per-module file's header against this string and + * warns on drift, so any rename here must be matched in external module + * authors' CSVs (or accepted as a positional fall-through with a warning). + */ +const MODULE_HELP_CSV_HEADER = + 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs'; + +module.exports = { MODULE_HELP_CSV_HEADER }; diff --git a/tools/installer/modules/plugin-resolver.js b/tools/installer/modules/plugin-resolver.js index 7f88b31bd..8cef26d27 100644 --- a/tools/installer/modules/plugin-resolver.js +++ b/tools/installer/modules/plugin-resolver.js @@ -1,6 +1,7 @@ const fs = require('../fs-native'); const path = require('node:path'); const yaml = require('yaml'); +const { MODULE_HELP_CSV_HEADER } = require('./module-help-schema'); /** * Resolves how to install a plugin from marketplace.json by analyzing @@ -338,9 +339,7 @@ class PluginResolver { * @returns {string} CSV content */ _buildSynthesizedHelpCsv(moduleName, skillInfos) { - const header = - 'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs'; - const rows = [header]; + const rows = [MODULE_HELP_CSV_HEADER]; for (const info of skillInfos) { const displayName = this._formatDisplayName(info.name || info.dirName);