bmad-method 6.7.0 → 6.7.1
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
|
@@ -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
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
:
|
|
298
|
-
? [...allModules, ...
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
package/tools/installer/ui.js
CHANGED
|
@@ -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
|
}
|