bmad-method 6.11.1-next.14 → 6.11.1-next.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.11.1-next.14",
4
+ "version": "6.11.1-next.15",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -13,7 +13,14 @@ const { InstallPaths } = require('./install-paths');
13
13
  const { ExternalModuleManager } = require('../modules/external-manager');
14
14
  const { resolveModuleVersion } = require('../modules/version-resolver');
15
15
  const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
16
- const { inferShimPreference, readInstalledSkillIds } = require('./shim-policy');
16
+ const {
17
+ formatRemovedShimNotice,
18
+ formatRetainedShimNotice,
19
+ inferShimPreference,
20
+ readInstalledShims,
21
+ readInstalledSkillIds,
22
+ selectShimOutcome,
23
+ } = require('./shim-policy');
17
24
 
18
25
  const { ExistingInstall } = require('./existing-install');
19
26
  const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
@@ -59,6 +66,24 @@ class Installer {
59
66
  }),
60
67
  };
61
68
 
69
+ const installedShims = existingInstall.installed ? await readInstalledShims(paths.bmadDir) : [];
70
+ const { retained: retainedShims, removed: removedShims } = selectShimOutcome({
71
+ installedShims,
72
+ availableShims,
73
+ install: shimPolicy.install,
74
+ });
75
+
76
+ // Reported here, not at the prompt, so --yes/--shims/scripted runs get it too.
77
+ if (retainedShims.length > 0) {
78
+ await prompts.note(formatRetainedShimNotice(retainedShims), 'Deprecated shim skills retained');
79
+ }
80
+ if (removedShims.length > 0) {
81
+ await prompts.note(formatRemovedShimNotice(removedShims, { canReinstall: shimPolicy.available }), 'Deprecated shim skills removed');
82
+ }
83
+
84
+ // The notices above scroll away on a long install; repeat them in the summary.
85
+ const shimStatus = { retained: retainedShims.length, removed: removedShims.length };
86
+
62
87
  try {
63
88
  await warnPreNativeSkillsLegacy({
64
89
  projectRoot: paths.projectRoot,
@@ -132,6 +157,7 @@ class Installer {
132
157
  customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
133
158
  modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
134
159
  preInstallVersions,
160
+ shimStatus,
135
161
  });
136
162
 
137
163
  return {
@@ -1261,6 +1287,12 @@ class Installer {
1261
1287
  if (context.modifiedFiles && context.modifiedFiles.length > 0) {
1262
1288
  lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
1263
1289
  }
1290
+ if (context.shimStatus?.retained > 0) {
1291
+ lines.push(` ${color.yellow(`Deprecated shim skills retained: ${context.shimStatus.retained}`)} (re-run to remove them)`);
1292
+ }
1293
+ if (context.shimStatus?.removed > 0) {
1294
+ lines.push(` ${color.green(`Deprecated shim skills removed: ${context.shimStatus.removed}`)}`);
1295
+ }
1264
1296
 
1265
1297
  // Next steps
1266
1298
  lines.push(
@@ -37,6 +37,7 @@ async function discoverShims(modulePath) {
37
37
  if (isShimSkill(metadata)) {
38
38
  shims.push({
39
39
  id: metadata.name || path.basename(dir),
40
+ description: typeof metadata.description === 'string' ? metadata.description : '',
40
41
  directory: dir,
41
42
  relativeDirectory: path.relative(modulePath, dir),
42
43
  });
@@ -54,25 +55,40 @@ async function discoverShims(modulePath) {
54
55
  return shims;
55
56
  }
56
57
 
57
- async function readInstalledSkillIds(bmadDir) {
58
- const ids = new Set();
58
+ async function readSkillManifest(bmadDir) {
59
59
  const manifestPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
60
- if (!(await fs.pathExists(manifestPath))) return ids;
60
+ if (!(await fs.pathExists(manifestPath))) return [];
61
61
 
62
62
  try {
63
63
  const content = await fs.readFile(manifestPath, 'utf8');
64
- const records = csv.parse(content, { columns: true, skip_empty_lines: true });
65
- for (const record of records) {
66
- if (record.canonicalId) ids.add(record.canonicalId);
67
- }
64
+ return csv.parse(content, { columns: true, skip_empty_lines: true });
68
65
  } catch {
69
66
  // A missing or unreadable legacy manifest means there is no reliable
70
67
  // evidence that compatibility shims were installed.
68
+ return [];
71
69
  }
70
+ }
72
71
 
72
+ async function readInstalledSkillIds(bmadDir) {
73
+ const ids = new Set();
74
+ for (const record of await readSkillManifest(bmadDir)) {
75
+ if (record.canonicalId) ids.add(record.canonicalId);
76
+ }
73
77
  return ids;
74
78
  }
75
79
 
80
+ // The installed manifest carries no lifecycle column, so the description
81
+ // prefix every shim ships with is the only record of what was a shim. This
82
+ // is the same signal validate-skills.js uses to exempt them.
83
+ async function readInstalledShims(bmadDir) {
84
+ const shims = [];
85
+ for (const record of await readSkillManifest(bmadDir)) {
86
+ if (!record.canonicalId || !/^\s*deprecated\b/i.test(record.description || '')) continue;
87
+ shims.push({ id: record.canonicalId, description: record.description || '', module: record.module || '' });
88
+ }
89
+ return shims;
90
+ }
91
+
76
92
  function inferShimPreference({ requested, persisted, availableShims = [], installedSkillIds = new Set(), existing = false }) {
77
93
  if (availableShims.length === 0) return false;
78
94
  if (typeof requested === 'boolean') return requested;
@@ -82,10 +98,62 @@ function inferShimPreference({ requested, persisted, availableShims = [], instal
82
98
  return availableShims.some((shim) => installedSkillIds.has(shim.id));
83
99
  }
84
100
 
101
+ // Removal is driven by what is installed, not by what this release ships: a
102
+ // shim retired from source is still deleted by the update cleanup.
103
+ function selectShimOutcome({ installedShims = [], availableShims = [], install = false }) {
104
+ const availableShimIds = new Set(availableShims.map((shim) => shim.id));
105
+ return {
106
+ retained: install ? availableShims : [],
107
+ removed: installedShims.filter((shim) => !(install && availableShimIds.has(shim.id))),
108
+ };
109
+ }
110
+
111
+ // Shim descriptions all open with "Deprecated — "; the notice heading says it once.
112
+ function describeShim(shim) {
113
+ const cleaned = (shim.description || '').replace(/^\s*deprecated\s*[-–—:]*\s*/i, '').trim();
114
+ const source = shim.module ? ` (${shim.module})` : '';
115
+ return cleaned ? ` ${shim.id}${source}: ${cleaned}` : ` ${shim.id}${source}`;
116
+ }
117
+
118
+ function formatRetainedShimNotice(availableShims = []) {
119
+ const lines = availableShims.map((shim) => describeShim(shim)).sort();
120
+
121
+ return [
122
+ `${availableShims.length} deprecated shim skill(s) are still installed. Each one only forwards to the skill that replaced it:`,
123
+ '',
124
+ ...lines,
125
+ '',
126
+ 'Shims will be removed with v7, and anything still calling the old name stops working then.',
127
+ 'Only keep a shim if you customized it and still need to move that customization to the replacement.',
128
+ 'Once you have, re-run Quick Update and answer No to this question so the shims come off.',
129
+ ].join('\n');
130
+ }
131
+
132
+ function formatRemovedShimNotice(removedShims = [], { canReinstall = true } = {}) {
133
+ const lines = removedShims.map((shim) => describeShim(shim)).sort();
134
+ const recovery = canReinstall
135
+ ? 'these shims, move it to the replacement, or re-run with --shims to put the shims back.'
136
+ : 'these shims, move it to the replacement. This release no longer ships them, so --shims cannot bring them back.';
137
+
138
+ return [
139
+ `${removedShims.length} deprecated shim skill(s) are being removed. Invoking these names will no longer work:`,
140
+ '',
141
+ ...lines,
142
+ '',
143
+ 'Each replacement named above is installed and ready. If you still had a customization on one of',
144
+ recovery,
145
+ ].join('\n');
146
+ }
147
+
85
148
  module.exports = {
149
+ describeShim,
150
+ formatRemovedShimNotice,
86
151
  discoverShims,
152
+ formatRetainedShimNotice,
87
153
  inferShimPreference,
88
154
  isShimSkill,
89
155
  parseSkillMetadata,
156
+ readInstalledShims,
90
157
  readInstalledSkillIds,
158
+ selectShimOutcome,
91
159
  };
@@ -316,7 +316,9 @@ async function autocompleteMultiselect(options) {
316
316
 
317
317
  switch (this.state) {
318
318
  case 'submit': {
319
- return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`;
319
+ const count = this.selectedValues.length;
320
+ const emptyHint = count === 0 && options.emptyLabel ? ` (${options.emptyLabel})` : '';
321
+ return `${title}${color.gray(clack.S_BAR)} ${color.dim(`${count} item${count === 1 ? '' : 's'} selected${emptyHint}`)}`;
320
322
  }
321
323
 
322
324
  case 'cancel': {
@@ -331,7 +333,18 @@ async function autocompleteMultiselect(options) {
331
333
 
332
334
  const errorLine = this.state === 'error' ? [`${bar} ${color.yellow(this.error)}`] : [];
333
335
 
334
- const headerLines = [...`${title}${bar}`.split('\n'), `${bar} ${searchDisplay}${matchCount}`, ...noMatchesLine, ...errorLine];
336
+ const emptyLine =
337
+ this.selectedValues.length === 0 && options.emptyLabel
338
+ ? [`${bar} ${color.dim(`Nothing selected: installs ${options.emptyLabel}`)}`]
339
+ : [];
340
+
341
+ const headerLines = [
342
+ ...`${title}${bar}`.split('\n'),
343
+ `${bar} ${searchDisplay}${matchCount}`,
344
+ ...noMatchesLine,
345
+ ...errorLine,
346
+ ...emptyLine,
347
+ ];
335
348
 
336
349
  const footerLines = [`${bar} ${color.dim(hints.join(' • '))}`, `${barEnd}`];
337
350
 
@@ -111,7 +111,7 @@ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault =
111
111
  * UI utilities for the installer
112
112
  */
113
113
  class UI {
114
- async _selectShimPreference({ selectedModules, bmadDir, existing, options, channelOptions }) {
114
+ async _selectShimPreference({ selectedModules, bmadDir, existing, options, channelOptions, quickUpdate = false }) {
115
115
  const { OfficialModules } = require('./modules/official-modules');
116
116
  const officialModules = new OfficialModules({ channelOptions });
117
117
  const availableShims = await officialModules.discoverShims(selectedModules, { channelOptions });
@@ -132,10 +132,20 @@ class UI {
132
132
 
133
133
  if (typeof options.shims === 'boolean' || options.yes) return currentValue;
134
134
 
135
- return prompts.confirm({
136
- message: `Install ${availableShims.length} deprecated compatibility shim skill(s)?`,
137
- default: currentValue,
138
- });
135
+ // clack's confirm never resolves without a TTY: a scripted run would exit mid-install.
136
+ if (!process.stdin.isTTY) return currentValue;
137
+
138
+ // Nothing to give up, so nothing to ask on every single update.
139
+ if (quickUpdate && !currentValue) return currentValue;
140
+
141
+ const verb = currentValue ? 'Keep' : 'Install';
142
+ const message =
143
+ `${verb} ${availableShims.length} deprecated compatibility shim skill(s)? Recommended: No. ` +
144
+ `If you say yes, the deprecated skills will exist as a skill that forwards to its replacement skill. ` +
145
+ `Shims will be removed with v7. You should only retain if you customized a shimmed skill and need to ` +
146
+ `still transition it to the replacement.`;
147
+
148
+ return prompts.confirm({ message, default: currentValue });
139
149
  }
140
150
 
141
151
  /**
@@ -346,11 +356,21 @@ class UI {
346
356
  // Quick update never shows the module picker, so this is the only
347
357
  // place an existing install of a deprecated module hears about it.
348
358
  await this._warnDeprecatedModules(existingInstall.moduleIds || []);
359
+
360
+ const installShims = await this._selectShimPreference({
361
+ selectedModules: existingInstall.moduleIds || [],
362
+ bmadDir,
363
+ existing: true,
364
+ options,
365
+ channelOptions,
366
+ quickUpdate: true,
367
+ });
368
+
349
369
  return {
350
370
  actionType: 'quick-update',
351
371
  directory: confirmedDirectory,
352
372
  skipPrompts: options.yes || false,
353
- installShims: options.shims,
373
+ installShims: installShims === undefined ? options.shims : installShims,
354
374
  };
355
375
  }
356
376
 
@@ -1116,9 +1136,9 @@ class UI {
1116
1136
  message: 'Select official modules to install:',
1117
1137
  options: allOptions,
1118
1138
  initialValues: initialValues.length > 0 ? initialValues : undefined,
1119
- // Not required: core is installed either way, so an empty selection is a
1120
- // legitimate "core only" install rather than a mistake to block on.
1139
+ // Core installs either way and is not a row here, so empty is a valid core-only install.
1121
1140
  required: false,
1141
+ emptyLabel: 'core only',
1122
1142
  maxItems: allOptions.length,
1123
1143
  });
1124
1144