bmad-method 6.7.0 → 6.7.1-next.0

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.7.0",
4
+ "version": "6.7.1-next.0",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -54,7 +54,7 @@ class Installer {
54
54
  }
55
55
 
56
56
  if (existingInstall.installed) {
57
- await this._removeDeselectedModules(existingInstall, config, paths);
57
+ await this._removeDeselectedModules(existingInstall, config, paths, originalConfig._preserveModules || []);
58
58
  updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
59
59
  await this._removeDeselectedIdes(existingInstall, config, paths);
60
60
  }
@@ -76,25 +76,23 @@ class Installer {
76
76
  const results = [];
77
77
  const addResult = (step, status, detail = '', meta = {}) => results.push({ step, status, detail, ...meta });
78
78
 
79
- // Capture previously installed skill IDs before they get overwritten
80
- const previousSkillIds = new Set();
81
- const prevCsvPath = path.join(paths.bmadDir, '_config', 'skill-manifest.csv');
82
- if (await fs.pathExists(prevCsvPath)) {
83
- try {
84
- const csvParse = require('csv-parse/sync');
85
- const content = await fs.readFile(prevCsvPath, 'utf8');
86
- const records = csvParse.parse(content, { columns: true, skip_empty_lines: true });
87
- for (const r of records) {
88
- if (r.canonicalId) previousSkillIds.add(r.canonicalId);
89
- }
90
- } catch (error) {
91
- await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
92
- }
93
- }
79
+ // Capture previously installed skill rows before they get overwritten
80
+ const preservedModules = originalConfig._preserveModules || [];
81
+ const previousSkillManifestRows = await this._readSkillManifestRows(paths.bmadDir);
82
+ const previousSkillIds = this._getPreviousSkillIdsForCleanup(previousSkillManifestRows, preservedModules);
94
83
 
95
84
  const allModules = config.modules || [];
96
85
 
97
- await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
86
+ await this._installAndConfigure(
87
+ config,
88
+ originalConfig,
89
+ paths,
90
+ allModules,
91
+ allModules,
92
+ addResult,
93
+ officialModules,
94
+ previousSkillManifestRows,
95
+ );
98
96
 
99
97
  await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
100
98
 
@@ -144,10 +142,11 @@ class Installer {
144
142
  * Remove modules that were previously installed but are no longer selected.
145
143
  * No confirmation — the user's module selection is the decision.
146
144
  */
147
- async _removeDeselectedModules(existingInstall, config, paths) {
145
+ async _removeDeselectedModules(existingInstall, config, paths, preservedModules = []) {
148
146
  const previouslyInstalled = new Set(existingInstall.moduleIds);
149
147
  const newlySelected = new Set(config.modules || []);
150
- const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core');
148
+ const preserved = new Set(preservedModules);
149
+ const toRemove = [...previouslyInstalled].filter((m) => !newlySelected.has(m) && m !== 'core' && !preserved.has(m));
151
150
 
152
151
  for (const moduleId of toRemove) {
153
152
  const modulePath = paths.moduleDir(moduleId);
@@ -212,7 +211,16 @@ class Installer {
212
211
  /**
213
212
  * Install modules, create directories, generate configs and manifests.
214
213
  */
215
- async _installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules) {
214
+ async _installAndConfigure(
215
+ config,
216
+ originalConfig,
217
+ paths,
218
+ officialModuleIds,
219
+ allModules,
220
+ addResult,
221
+ officialModules,
222
+ previousSkillManifestRows = [],
223
+ ) {
216
224
  const isQuickUpdate = config.isQuickUpdate();
217
225
  const moduleConfigs = officialModules.moduleConfigs;
218
226
 
@@ -291,25 +299,29 @@ class Installer {
291
299
 
292
300
  message('Generating manifests...');
293
301
  const manifestGen = new ManifestGenerator();
302
+ const preservedModules = originalConfig._preserveModules || [];
294
303
 
295
304
  const allModulesForManifest = config.isQuickUpdate()
296
305
  ? originalConfig._existingModules || allModules || []
297
- : originalConfig._preserveModules
298
- ? [...allModules, ...originalConfig._preserveModules]
306
+ : preservedModules.length > 0
307
+ ? [...allModules, ...preservedModules]
299
308
  : allModules || [];
300
309
 
301
310
  let modulesForCsvPreserve;
302
311
  if (config.isQuickUpdate()) {
303
312
  modulesForCsvPreserve = originalConfig._existingModules || allModules || [];
304
313
  } else {
305
- modulesForCsvPreserve = originalConfig._preserveModules ? [...allModules, ...originalConfig._preserveModules] : allModules;
314
+ modulesForCsvPreserve = preservedModules.length > 0 ? [...allModules, ...preservedModules] : allModules;
306
315
  }
307
316
 
317
+ await this._trackPreservedModuleFiles(paths.bmadDir, preservedModules);
318
+
308
319
  await manifestGen.generateManifests(paths.bmadDir, allModulesForManifest, [...this.installedFiles], {
309
320
  ides: config.ides || [],
310
321
  preservedModules: modulesForCsvPreserve,
311
322
  moduleConfigs,
312
323
  });
324
+ await this._appendPreservedSkillManifestRows(paths.bmadDir, previousSkillManifestRows, preservedModules);
313
325
 
314
326
  // Apply post-install --set TOML patches. Runs after writeCentralConfig
315
327
  // (inside generateManifests above) so the patch operates on the
@@ -411,6 +423,62 @@ class Installer {
411
423
  }
412
424
  }
413
425
 
426
+ async _readSkillManifestRows(bmadDir) {
427
+ const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
428
+ if (!(await fs.pathExists(csvPath))) return [];
429
+
430
+ try {
431
+ const csvParse = require('csv-parse/sync');
432
+ const content = await fs.readFile(csvPath, 'utf8');
433
+ return csvParse.parse(content, { columns: true, skip_empty_lines: true });
434
+ } catch (error) {
435
+ await prompts.log.warn(`Failed to parse skill-manifest.csv: ${error.message}`);
436
+ return [];
437
+ }
438
+ }
439
+
440
+ _getPreviousSkillIdsForCleanup(previousRows, preservedModules = []) {
441
+ const preservedModuleSet = new Set(preservedModules || []);
442
+ const ids = new Set();
443
+ for (const row of previousRows || []) {
444
+ if (row.canonicalId && !preservedModuleSet.has(row.module)) {
445
+ ids.add(row.canonicalId);
446
+ }
447
+ }
448
+ return ids;
449
+ }
450
+
451
+ async _appendPreservedSkillManifestRows(bmadDir, previousRows, preservedModules = []) {
452
+ if (!previousRows || previousRows.length === 0 || preservedModules.length === 0) return;
453
+
454
+ const preservedModuleSet = new Set(preservedModules);
455
+ const rowsToPreserve = previousRows.filter((row) => row.canonicalId && row.module && preservedModuleSet.has(row.module));
456
+ if (rowsToPreserve.length === 0) return;
457
+
458
+ const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
459
+ if (!(await fs.pathExists(csvPath))) return;
460
+
461
+ const currentRows = await this._readSkillManifestRows(bmadDir);
462
+ const activeIds = new Set(currentRows.map((row) => row.canonicalId).filter(Boolean));
463
+ const appendedRows = [];
464
+
465
+ for (const row of rowsToPreserve) {
466
+ if (activeIds.has(row.canonicalId)) continue;
467
+ activeIds.add(row.canonicalId);
468
+ appendedRows.push(
469
+ [row.canonicalId, row.name || row.canonicalId, row.description || '', row.module, row.path || '']
470
+ .map((field) => this.escapeCSVField(field))
471
+ .join(','),
472
+ );
473
+ }
474
+
475
+ if (appendedRows.length === 0) return;
476
+
477
+ const currentContent = await fs.readFile(csvPath, 'utf8');
478
+ const prefix = currentContent.endsWith('\n') ? currentContent : `${currentContent}\n`;
479
+ await fs.writeFile(csvPath, prefix + appendedRows.join('\n') + '\n', 'utf8');
480
+ }
481
+
414
482
  /**
415
483
  * Restore custom and modified files that were backed up before the update.
416
484
  * No-op for fresh installs (updateState is null).
@@ -597,6 +665,15 @@ class Installer {
597
665
  }
598
666
  }
599
667
 
668
+ async _trackPreservedModuleFiles(bmadDir, preservedModules = []) {
669
+ for (const moduleName of preservedModules) {
670
+ const modulePath = path.join(bmadDir, moduleName);
671
+ if (await fs.pathExists(modulePath)) {
672
+ await this._trackFilesRecursive(modulePath);
673
+ }
674
+ }
675
+ }
676
+
600
677
  /**
601
678
  * Install official (non-custom) modules.
602
679
  * @param {Object} config - Installation configuration
@@ -501,7 +501,7 @@ class ConfigDrivenIdeSetup {
501
501
 
502
502
  // Build removal set: previously installed skills + removals.txt entries
503
503
  let removalSet;
504
- if (options.previousSkillIds && options.previousSkillIds.size > 0) {
504
+ if (options.previousSkillIds) {
505
505
  // Install/update flow: use pre-captured skill IDs (before manifest was overwritten)
506
506
  removalSet = new Set(options.previousSkillIds);
507
507
  if (resolvedBmadDir) {
@@ -547,7 +547,7 @@ class ConfigDrivenIdeSetup {
547
547
  // previousSkillIds — full uninstall or per-IDE removal via
548
548
  // cleanupByList), don't spare anything; the IDE itself is going away,
549
549
  // so its pointers should go with it.
550
- const isInstallFlow = options.previousSkillIds && options.previousSkillIds.size > 0;
550
+ const isInstallFlow = !!options.previousSkillIds;
551
551
  const activeSkillIds = isInstallFlow ? await this._readActiveSkillIds(resolvedBmadDir) : new Set();
552
552
  const extension = this.installerConfig.commands_extension || '.md';
553
553
  await this.cleanupCommandPointers(
@@ -110,6 +110,44 @@ async function getModuleVersion(moduleCode, { repoUrl = null, registryDefault =
110
110
  * UI utilities for the installer
111
111
  */
112
112
  class UI {
113
+ async _retainUnavailableInstalledModules(selectedModules, installedModuleIds, bmadDir, options = {}) {
114
+ const { OfficialModules } = require('./modules/official-modules');
115
+ const officialCodes = new Set(['core']);
116
+
117
+ const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
118
+ for (const mod of builtInModules) {
119
+ officialCodes.add(mod.id);
120
+ }
121
+
122
+ const externalManager = new ExternalModuleManager();
123
+ const registryModules = await externalManager.listAvailable();
124
+ for (const mod of registryModules) {
125
+ officialCodes.add(mod.code);
126
+ }
127
+
128
+ const { CustomModuleManager } = require('./modules/custom-module-manager');
129
+ const customMgr = new CustomModuleManager();
130
+ const selectedSet = new Set(selectedModules);
131
+ const preserveModules = [];
132
+
133
+ for (const moduleId of installedModuleIds) {
134
+ if (moduleId === 'core') continue;
135
+ if (!selectedSet.has(moduleId) && !options.preserveUnselected) continue;
136
+ if (officialCodes.has(moduleId)) continue;
137
+
138
+ const customSource = await customMgr.findModuleSourceByCode(moduleId, { bmadDir });
139
+ if (!customSource) {
140
+ preserveModules.push(moduleId);
141
+ }
142
+ }
143
+
144
+ const preservedSet = new Set(preserveModules);
145
+ return {
146
+ selectedModules: selectedModules.filter((moduleId) => !preservedSet.has(moduleId)),
147
+ preserveModules,
148
+ };
149
+ }
150
+
113
151
  /**
114
152
  * Prompt for installation configuration
115
153
  * @param {Object} options - Command-line options from install command
@@ -273,6 +311,18 @@ class UI {
273
311
  selectedModules.unshift('core');
274
312
  }
275
313
 
314
+ const retainedModuleResult = await this._retainUnavailableInstalledModules(selectedModules, installedModuleIds, bmadDir, {
315
+ preserveUnselected: options.yes && !options.modules,
316
+ });
317
+ selectedModules = retainedModuleResult.selectedModules;
318
+ const preservedModules = retainedModuleResult.preserveModules;
319
+
320
+ if (preservedModules.length > 0) {
321
+ await prompts.log.warn(
322
+ `Retaining ${preservedModules.length} installed module(s) with no available source: ${preservedModules.join(', ')}`,
323
+ );
324
+ }
325
+
276
326
  // For existing installs, resolve per-module update decisions BEFORE
277
327
  // we clone anything. Reads the existing manifest's recorded channel
278
328
  // per module and prompts the user on available upgrades (patch/minor
@@ -317,6 +367,7 @@ class UI {
317
367
  setOverrides,
318
368
  skipPrompts: options.yes || false,
319
369
  channelOptions,
370
+ _preserveModules: preservedModules,
320
371
  };
321
372
  }
322
373
  }