bmad-method 6.2.3-next.25 → 6.2.3-next.26
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 +1 -1
- package/tools/installer/commands/install.js +0 -1
- package/tools/installer/core/existing-install.js +2 -8
- package/tools/installer/core/install-paths.js +0 -3
- package/tools/installer/core/installer.js +5 -396
- package/tools/installer/core/manifest-generator.js +0 -9
- package/tools/installer/core/manifest.js +1 -70
- package/tools/installer/modules/official-modules.js +14 -64
- package/tools/installer/ui.js +1 -613
- package/tools/installer/core/custom-module-cache.js +0 -260
- package/tools/installer/custom-handler.js +0 -112
- package/tools/installer/modules/custom-modules.js +0 -302
package/package.json
CHANGED
|
@@ -17,7 +17,6 @@ module.exports = {
|
|
|
17
17
|
'--tools <tools>',
|
|
18
18
|
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
|
|
19
19
|
],
|
|
20
|
-
['--custom-content <paths>', 'Comma-separated list of paths to custom modules/agents/workflows'],
|
|
21
20
|
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
|
22
21
|
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
|
23
22
|
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
|
@@ -10,14 +10,13 @@ const { Manifest } = require('./manifest');
|
|
|
10
10
|
class ExistingInstall {
|
|
11
11
|
#version;
|
|
12
12
|
|
|
13
|
-
constructor({ installed, version, hasCore, modules, ides
|
|
13
|
+
constructor({ installed, version, hasCore, modules, ides }) {
|
|
14
14
|
this.installed = installed;
|
|
15
15
|
this.#version = version;
|
|
16
16
|
this.hasCore = hasCore;
|
|
17
17
|
this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
|
|
18
18
|
this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
|
|
19
19
|
this.ides = Object.freeze([...ides]);
|
|
20
|
-
this.customModules = Object.freeze([...customModules]);
|
|
21
20
|
Object.freeze(this);
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -35,7 +34,6 @@ class ExistingInstall {
|
|
|
35
34
|
hasCore: false,
|
|
36
35
|
modules: [],
|
|
37
36
|
ides: [],
|
|
38
|
-
customModules: [],
|
|
39
37
|
});
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -53,15 +51,11 @@ class ExistingInstall {
|
|
|
53
51
|
let hasCore = false;
|
|
54
52
|
const modules = [];
|
|
55
53
|
let ides = [];
|
|
56
|
-
let customModules = [];
|
|
57
54
|
|
|
58
55
|
const manifest = new Manifest();
|
|
59
56
|
const manifestData = await manifest.read(bmadDir);
|
|
60
57
|
if (manifestData) {
|
|
61
58
|
version = manifestData.version;
|
|
62
|
-
if (manifestData.customModules) {
|
|
63
|
-
customModules = manifestData.customModules;
|
|
64
|
-
}
|
|
65
59
|
if (manifestData.ides) {
|
|
66
60
|
ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
|
|
67
61
|
}
|
|
@@ -120,7 +114,7 @@ class ExistingInstall {
|
|
|
120
114
|
return ExistingInstall.empty();
|
|
121
115
|
}
|
|
122
116
|
|
|
123
|
-
return new ExistingInstall({ installed, version, hasCore, modules, ides
|
|
117
|
+
return new ExistingInstall({ installed, version, hasCore, modules, ides });
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
@@ -20,14 +20,12 @@ class InstallPaths {
|
|
|
20
20
|
|
|
21
21
|
const configDir = path.join(bmadDir, '_config');
|
|
22
22
|
const agentsDir = path.join(configDir, 'agents');
|
|
23
|
-
const customCacheDir = path.join(configDir, 'custom');
|
|
24
23
|
const coreDir = path.join(bmadDir, 'core');
|
|
25
24
|
|
|
26
25
|
for (const [dir, label] of [
|
|
27
26
|
[bmadDir, 'bmad directory'],
|
|
28
27
|
[configDir, 'config directory'],
|
|
29
28
|
[agentsDir, 'agents config directory'],
|
|
30
|
-
[customCacheDir, 'custom modules cache'],
|
|
31
29
|
[coreDir, 'core module directory'],
|
|
32
30
|
]) {
|
|
33
31
|
await ensureWritableDir(dir, label);
|
|
@@ -40,7 +38,6 @@ class InstallPaths {
|
|
|
40
38
|
bmadDir,
|
|
41
39
|
configDir,
|
|
42
40
|
agentsDir,
|
|
43
|
-
customCacheDir,
|
|
44
41
|
coreDir,
|
|
45
42
|
isUpdate,
|
|
46
43
|
});
|
|
@@ -2,7 +2,6 @@ const path = require('node:path');
|
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const { Manifest } = require('./manifest');
|
|
4
4
|
const { OfficialModules } = require('../modules/official-modules');
|
|
5
|
-
const { CustomModules } = require('../modules/custom-modules');
|
|
6
5
|
const { IdeManager } = require('../ide/manager');
|
|
7
6
|
const { FileOps } = require('../file-ops');
|
|
8
7
|
const { Config } = require('./config');
|
|
@@ -19,7 +18,6 @@ class Installer {
|
|
|
19
18
|
constructor() {
|
|
20
19
|
this.externalModuleManager = new ExternalModuleManager();
|
|
21
20
|
this.manifest = new Manifest();
|
|
22
|
-
this.customModules = new CustomModules();
|
|
23
21
|
this.ideManager = new IdeManager();
|
|
24
22
|
this.fileOps = new FileOps();
|
|
25
23
|
this.installedFiles = new Set(); // Track all installed files
|
|
@@ -80,8 +78,6 @@ class Installer {
|
|
|
80
78
|
const officialModules = await OfficialModules.build(config, paths);
|
|
81
79
|
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
|
82
80
|
|
|
83
|
-
await this.customModules.discoverPaths(originalConfig, paths);
|
|
84
|
-
|
|
85
81
|
if (existingInstall.installed) {
|
|
86
82
|
await this._removeDeselectedModules(existingInstall, config, paths);
|
|
87
83
|
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
|
|
@@ -121,14 +117,9 @@ class Installer {
|
|
|
121
117
|
}
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Compute module lists: official = selected minus custom, all = both
|
|
127
|
-
const customModuleIds = new Set(this.customModules.paths.keys());
|
|
128
|
-
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
|
129
|
-
const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
|
|
120
|
+
const allModules = config.modules || [];
|
|
130
121
|
|
|
131
|
-
await this._installAndConfigure(config, originalConfig, paths,
|
|
122
|
+
await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
|
|
132
123
|
|
|
133
124
|
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
|
|
134
125
|
|
|
@@ -242,26 +233,6 @@ class Installer {
|
|
|
242
233
|
}
|
|
243
234
|
}
|
|
244
235
|
|
|
245
|
-
/**
|
|
246
|
-
* Cache custom modules into the local cache directory.
|
|
247
|
-
* Updates this.customModules.paths in place with cached locations.
|
|
248
|
-
*/
|
|
249
|
-
async _cacheCustomModules(paths, addResult) {
|
|
250
|
-
if (!this.customModules.paths || this.customModules.paths.size === 0) return;
|
|
251
|
-
|
|
252
|
-
const { CustomModuleCache } = require('./custom-module-cache');
|
|
253
|
-
const customCache = new CustomModuleCache(paths.bmadDir);
|
|
254
|
-
|
|
255
|
-
for (const [moduleId, sourcePath] of this.customModules.paths) {
|
|
256
|
-
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
|
|
257
|
-
sourcePath: sourcePath,
|
|
258
|
-
});
|
|
259
|
-
this.customModules.paths.set(moduleId, cachedInfo.cachePath);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
addResult('Custom modules cached', 'ok');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
236
|
/**
|
|
266
237
|
* Install modules, create directories, generate configs and manifests.
|
|
267
238
|
*/
|
|
@@ -284,11 +255,6 @@ class Installer {
|
|
|
284
255
|
installedModuleNames,
|
|
285
256
|
});
|
|
286
257
|
|
|
287
|
-
await this._installCustomModules(config, paths, addResult, officialModules, {
|
|
288
|
-
message,
|
|
289
|
-
installedModuleNames,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
258
|
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
|
293
259
|
},
|
|
294
260
|
});
|
|
@@ -515,48 +481,7 @@ class Installer {
|
|
|
515
481
|
}
|
|
516
482
|
|
|
517
483
|
/**
|
|
518
|
-
*
|
|
519
|
-
* that aren't already known from the manifest or external module list.
|
|
520
|
-
* @param {Object} paths - InstallPaths instance
|
|
521
|
-
*/
|
|
522
|
-
async _scanCachedCustomModules(paths) {
|
|
523
|
-
const cacheDir = paths.customCacheDir;
|
|
524
|
-
if (!(await fs.pathExists(cacheDir))) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
529
|
-
|
|
530
|
-
for (const cachedModule of cachedModules) {
|
|
531
|
-
const moduleId = cachedModule.name;
|
|
532
|
-
const cachedPath = path.join(cacheDir, moduleId);
|
|
533
|
-
|
|
534
|
-
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
535
|
-
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Skip if we already have this module from manifest
|
|
540
|
-
if (this.customModules.paths.has(moduleId)) {
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check if this is an external official module - skip cache for those
|
|
545
|
-
const isExternal = await this.externalModuleManager.hasModule(moduleId);
|
|
546
|
-
if (isExternal) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Check if this is actually a custom module (has module.yaml)
|
|
551
|
-
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
552
|
-
if (await fs.pathExists(moduleYamlPath)) {
|
|
553
|
-
this.customModules.paths.set(moduleId, cachedPath);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Common update preparation: detect files, preserve core config, scan cache, back up.
|
|
484
|
+
* Common update preparation: detect files, preserve core config, back up.
|
|
560
485
|
* @param {Object} paths - InstallPaths instance
|
|
561
486
|
* @param {Object} config - Clean config (may have coreConfig updated)
|
|
562
487
|
* @param {Object} existingInstall - Detection result
|
|
@@ -584,8 +509,6 @@ class Installer {
|
|
|
584
509
|
}
|
|
585
510
|
}
|
|
586
511
|
|
|
587
|
-
await this._scanCachedCustomModules(paths);
|
|
588
|
-
|
|
589
512
|
const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
|
|
590
513
|
|
|
591
514
|
return {
|
|
@@ -677,38 +600,6 @@ class Installer {
|
|
|
677
600
|
}
|
|
678
601
|
}
|
|
679
602
|
|
|
680
|
-
/**
|
|
681
|
-
* Install custom modules using CustomModules.install().
|
|
682
|
-
* Source paths come from this.customModules.paths (populated by discoverPaths).
|
|
683
|
-
*/
|
|
684
|
-
async _installCustomModules(config, paths, addResult, officialModules, ctx) {
|
|
685
|
-
const { message, installedModuleNames } = ctx;
|
|
686
|
-
const isQuickUpdate = config.isQuickUpdate();
|
|
687
|
-
|
|
688
|
-
for (const [moduleName, sourcePath] of this.customModules.paths) {
|
|
689
|
-
if (installedModuleNames.has(moduleName)) continue;
|
|
690
|
-
installedModuleNames.add(moduleName);
|
|
691
|
-
|
|
692
|
-
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
|
693
|
-
|
|
694
|
-
const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
|
|
695
|
-
const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
|
696
|
-
moduleConfig: collectedModuleConfig,
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// Generate runtime config.yaml with merged values
|
|
700
|
-
await this.generateModuleConfigs(paths.bmadDir, {
|
|
701
|
-
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Get display name from source module.yaml; version from marketplace.json
|
|
705
|
-
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
|
|
706
|
-
const displayName = moduleInfo?.name || moduleName;
|
|
707
|
-
const version = await this._getMarketplaceVersion(sourcePath);
|
|
708
|
-
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
603
|
/**
|
|
713
604
|
* Read files-manifest.csv
|
|
714
605
|
* @param {string} bmadDir - BMAD installation directory
|
|
@@ -1253,16 +1144,9 @@ class Installer {
|
|
|
1253
1144
|
const configuredIdes = existingInstall.ides;
|
|
1254
1145
|
const projectRoot = path.dirname(bmadDir);
|
|
1255
1146
|
|
|
1256
|
-
const customModuleSources = await this.customModules.assembleQuickUpdateSources(
|
|
1257
|
-
config,
|
|
1258
|
-
existingInstall,
|
|
1259
|
-
bmadDir,
|
|
1260
|
-
this.externalModuleManager,
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
1147
|
// Get available modules (what we have source for)
|
|
1264
1148
|
const availableModulesData = await new OfficialModules().listAvailable();
|
|
1265
|
-
const availableModules = [...availableModulesData.modules
|
|
1149
|
+
const availableModules = [...availableModulesData.modules];
|
|
1266
1150
|
|
|
1267
1151
|
// Add external official modules to available modules
|
|
1268
1152
|
const externalModules = await this.externalModuleManager.listAvailable();
|
|
@@ -1277,52 +1161,12 @@ class Installer {
|
|
|
1277
1161
|
}
|
|
1278
1162
|
}
|
|
1279
1163
|
|
|
1280
|
-
|
|
1281
|
-
for (const [moduleId, customModule] of customModuleSources) {
|
|
1282
|
-
const sourcePath = customModule.sourcePath;
|
|
1283
|
-
if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
|
|
1284
|
-
availableModules.push({
|
|
1285
|
-
id: moduleId,
|
|
1286
|
-
name: customModule.name || moduleId,
|
|
1287
|
-
path: sourcePath,
|
|
1288
|
-
isCustom: true,
|
|
1289
|
-
fromManifest: true,
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Handle missing custom module sources
|
|
1295
|
-
const customModuleResult = await this.handleMissingCustomSources(
|
|
1296
|
-
customModuleSources,
|
|
1297
|
-
bmadDir,
|
|
1298
|
-
projectRoot,
|
|
1299
|
-
'update',
|
|
1300
|
-
installedModules,
|
|
1301
|
-
config.skipPrompts || false,
|
|
1302
|
-
);
|
|
1303
|
-
|
|
1304
|
-
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
|
1305
|
-
|
|
1306
|
-
const customModulesFromManifest = validCustomModules.map((m) => ({
|
|
1307
|
-
...m,
|
|
1308
|
-
isCustom: true,
|
|
1309
|
-
hasUpdate: true,
|
|
1310
|
-
}));
|
|
1311
|
-
|
|
1312
|
-
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
|
1313
|
-
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
|
1164
|
+
const availableModuleIds = new Set(availableModules.map((m) => m.id));
|
|
1314
1165
|
|
|
1315
1166
|
// Only update modules that are BOTH installed AND available (we have source for)
|
|
1316
1167
|
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
|
|
1317
1168
|
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
|
|
1318
1169
|
|
|
1319
|
-
// Add custom modules that were kept without sources to the skipped modules
|
|
1320
|
-
for (const keptModule of keptModulesWithoutSources) {
|
|
1321
|
-
if (!skippedModules.includes(keptModule)) {
|
|
1322
|
-
skippedModules.push(keptModule);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
1170
|
if (skippedModules.length > 0) {
|
|
1327
1171
|
await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
|
|
1328
1172
|
}
|
|
@@ -1367,9 +1211,7 @@ class Installer {
|
|
|
1367
1211
|
actionType: 'install',
|
|
1368
1212
|
_quickUpdate: true,
|
|
1369
1213
|
_preserveModules: skippedModules,
|
|
1370
|
-
_customModuleSources: customModuleSources,
|
|
1371
1214
|
_existingModules: installedModules,
|
|
1372
|
-
customContent: config.customContent,
|
|
1373
1215
|
};
|
|
1374
1216
|
|
|
1375
1217
|
await this.install(installConfig);
|
|
@@ -1504,239 +1346,6 @@ class Installer {
|
|
|
1504
1346
|
return this._readOutputFolder(bmadDir);
|
|
1505
1347
|
}
|
|
1506
1348
|
|
|
1507
|
-
/**
|
|
1508
|
-
* Handle missing custom module sources interactively
|
|
1509
|
-
* @param {Map} customModuleSources - Map of custom module ID to info
|
|
1510
|
-
* @param {string} bmadDir - BMAD directory
|
|
1511
|
-
* @param {string} projectRoot - Project root directory
|
|
1512
|
-
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
|
1513
|
-
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
|
1514
|
-
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
|
|
1515
|
-
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
|
1516
|
-
*/
|
|
1517
|
-
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
|
|
1518
|
-
const validCustomModules = [];
|
|
1519
|
-
const keptModulesWithoutSources = []; // Track modules kept without sources
|
|
1520
|
-
const customModulesWithMissingSources = [];
|
|
1521
|
-
|
|
1522
|
-
// Check which sources exist
|
|
1523
|
-
for (const [moduleId, customInfo] of customModuleSources) {
|
|
1524
|
-
if (await fs.pathExists(customInfo.sourcePath)) {
|
|
1525
|
-
validCustomModules.push({
|
|
1526
|
-
id: moduleId,
|
|
1527
|
-
name: customInfo.name,
|
|
1528
|
-
path: customInfo.sourcePath,
|
|
1529
|
-
info: customInfo,
|
|
1530
|
-
});
|
|
1531
|
-
} else {
|
|
1532
|
-
// For cached modules that are missing, we just skip them without prompting
|
|
1533
|
-
if (customInfo.cached) {
|
|
1534
|
-
// Skip cached modules without prompting
|
|
1535
|
-
keptModulesWithoutSources.push({
|
|
1536
|
-
id: moduleId,
|
|
1537
|
-
name: customInfo.name,
|
|
1538
|
-
cached: true,
|
|
1539
|
-
});
|
|
1540
|
-
} else {
|
|
1541
|
-
customModulesWithMissingSources.push({
|
|
1542
|
-
id: moduleId,
|
|
1543
|
-
name: customInfo.name,
|
|
1544
|
-
sourcePath: customInfo.sourcePath,
|
|
1545
|
-
relativePath: customInfo.relativePath,
|
|
1546
|
-
info: customInfo,
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// If no missing sources, return immediately
|
|
1553
|
-
if (customModulesWithMissingSources.length === 0) {
|
|
1554
|
-
return {
|
|
1555
|
-
validCustomModules,
|
|
1556
|
-
keptModulesWithoutSources: [],
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Non-interactive mode: keep all modules with missing sources
|
|
1561
|
-
if (skipPrompts) {
|
|
1562
|
-
for (const missing of customModulesWithMissingSources) {
|
|
1563
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1564
|
-
}
|
|
1565
|
-
return { validCustomModules, keptModulesWithoutSources };
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
|
|
1569
|
-
|
|
1570
|
-
let keptCount = 0;
|
|
1571
|
-
let updatedCount = 0;
|
|
1572
|
-
let removedCount = 0;
|
|
1573
|
-
|
|
1574
|
-
for (const missing of customModulesWithMissingSources) {
|
|
1575
|
-
await prompts.log.message(
|
|
1576
|
-
`${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
|
|
1577
|
-
);
|
|
1578
|
-
|
|
1579
|
-
const choices = [
|
|
1580
|
-
{
|
|
1581
|
-
name: 'Keep installed (will not be processed)',
|
|
1582
|
-
value: 'keep',
|
|
1583
|
-
hint: 'Keep',
|
|
1584
|
-
},
|
|
1585
|
-
{
|
|
1586
|
-
name: 'Specify new source location',
|
|
1587
|
-
value: 'update',
|
|
1588
|
-
hint: 'Update',
|
|
1589
|
-
},
|
|
1590
|
-
];
|
|
1591
|
-
|
|
1592
|
-
// Only add remove option if not just compiling agents
|
|
1593
|
-
if (operation !== 'compile-agents') {
|
|
1594
|
-
choices.push({
|
|
1595
|
-
name: '⚠️ REMOVE module completely (destructive!)',
|
|
1596
|
-
value: 'remove',
|
|
1597
|
-
hint: 'Remove',
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const action = await prompts.select({
|
|
1602
|
-
message: `How would you like to handle "${missing.name}"?`,
|
|
1603
|
-
choices,
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
switch (action) {
|
|
1607
|
-
case 'update': {
|
|
1608
|
-
// Use sync validation because @clack/prompts doesn't support async validate
|
|
1609
|
-
const newSourcePath = await prompts.text({
|
|
1610
|
-
message: 'Enter the new path to the custom module:',
|
|
1611
|
-
default: missing.sourcePath,
|
|
1612
|
-
validate: (input) => {
|
|
1613
|
-
if (!input || input.trim() === '') {
|
|
1614
|
-
return 'Please enter a path';
|
|
1615
|
-
}
|
|
1616
|
-
const expandedPath = path.resolve(input.trim());
|
|
1617
|
-
if (!fs.pathExistsSync(expandedPath)) {
|
|
1618
|
-
return 'Path does not exist';
|
|
1619
|
-
}
|
|
1620
|
-
// Check if it looks like a valid module
|
|
1621
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
1622
|
-
const agentsPath = path.join(expandedPath, 'agents');
|
|
1623
|
-
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
1624
|
-
|
|
1625
|
-
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
|
1626
|
-
return 'Path does not appear to contain a valid custom module';
|
|
1627
|
-
}
|
|
1628
|
-
return; // clack expects undefined for valid input
|
|
1629
|
-
},
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// Defensive: handleCancel should have exited, but guard against symbol propagation
|
|
1633
|
-
if (typeof newSourcePath !== 'string') {
|
|
1634
|
-
keptCount++;
|
|
1635
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1636
|
-
continue;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// Update the source in manifest
|
|
1640
|
-
const resolvedPath = path.resolve(newSourcePath.trim());
|
|
1641
|
-
missing.info.sourcePath = resolvedPath;
|
|
1642
|
-
// Remove relativePath - we only store absolute sourcePath now
|
|
1643
|
-
delete missing.info.relativePath;
|
|
1644
|
-
await this.manifest.addCustomModule(bmadDir, missing.info);
|
|
1645
|
-
|
|
1646
|
-
validCustomModules.push({
|
|
1647
|
-
id: missing.id,
|
|
1648
|
-
name: missing.name,
|
|
1649
|
-
path: resolvedPath,
|
|
1650
|
-
info: missing.info,
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
updatedCount++;
|
|
1654
|
-
await prompts.log.success('Updated source location');
|
|
1655
|
-
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
case 'remove': {
|
|
1659
|
-
// Extra confirmation for destructive remove
|
|
1660
|
-
await prompts.log.error(
|
|
1661
|
-
`WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
|
|
1662
|
-
);
|
|
1663
|
-
|
|
1664
|
-
const confirmDelete = await prompts.confirm({
|
|
1665
|
-
message: 'Are you absolutely sure you want to delete this module?',
|
|
1666
|
-
default: false,
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
if (confirmDelete) {
|
|
1670
|
-
const typedConfirm = await prompts.text({
|
|
1671
|
-
message: 'Type "DELETE" to confirm permanent deletion:',
|
|
1672
|
-
validate: (input) => {
|
|
1673
|
-
if (input !== 'DELETE') {
|
|
1674
|
-
return 'You must type "DELETE" exactly to proceed';
|
|
1675
|
-
}
|
|
1676
|
-
return; // clack expects undefined for valid input
|
|
1677
|
-
},
|
|
1678
|
-
});
|
|
1679
|
-
|
|
1680
|
-
if (typedConfirm === 'DELETE') {
|
|
1681
|
-
// Remove the module from filesystem and manifest
|
|
1682
|
-
const modulePath = path.join(bmadDir, missing.id);
|
|
1683
|
-
if (await fs.pathExists(modulePath)) {
|
|
1684
|
-
const fsExtra = require('fs-extra');
|
|
1685
|
-
await fsExtra.remove(modulePath);
|
|
1686
|
-
await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
await this.manifest.removeModule(bmadDir, missing.id);
|
|
1690
|
-
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
|
1691
|
-
await prompts.log.warn('Removed from manifest');
|
|
1692
|
-
|
|
1693
|
-
// Also remove from installedModules list
|
|
1694
|
-
if (installedModules && installedModules.includes(missing.id)) {
|
|
1695
|
-
const index = installedModules.indexOf(missing.id);
|
|
1696
|
-
if (index !== -1) {
|
|
1697
|
-
installedModules.splice(index, 1);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
removedCount++;
|
|
1702
|
-
await prompts.log.error(`"${missing.name}" has been permanently removed`);
|
|
1703
|
-
} else {
|
|
1704
|
-
await prompts.log.message('Removal cancelled - module will be kept');
|
|
1705
|
-
keptCount++;
|
|
1706
|
-
}
|
|
1707
|
-
} else {
|
|
1708
|
-
await prompts.log.message('Removal cancelled - module will be kept');
|
|
1709
|
-
keptCount++;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
break;
|
|
1713
|
-
}
|
|
1714
|
-
case 'keep': {
|
|
1715
|
-
keptCount++;
|
|
1716
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1717
|
-
await prompts.log.message('Module will be kept as-is');
|
|
1718
|
-
|
|
1719
|
-
break;
|
|
1720
|
-
}
|
|
1721
|
-
// No default
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Show summary
|
|
1726
|
-
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
|
|
1727
|
-
let summary = 'Summary for custom modules with missing sources:';
|
|
1728
|
-
if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
|
|
1729
|
-
if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
|
|
1730
|
-
if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
|
|
1731
|
-
await prompts.log.message(summary);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
return {
|
|
1735
|
-
validCustomModules,
|
|
1736
|
-
keptModulesWithoutSources,
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
1349
|
/**
|
|
1741
1350
|
* Find the bmad installation directory in a project
|
|
1742
1351
|
* Always uses the standard _bmad folder name
|
|
@@ -375,8 +375,6 @@ class ManifestGenerator {
|
|
|
375
375
|
// Read existing manifest to preserve install date
|
|
376
376
|
let existingInstallDate = null;
|
|
377
377
|
const existingModulesMap = new Map();
|
|
378
|
-
let existingCustomModules = [];
|
|
379
|
-
|
|
380
378
|
if (await fs.pathExists(manifestPath)) {
|
|
381
379
|
try {
|
|
382
380
|
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
|
@@ -397,12 +395,6 @@ class ManifestGenerator {
|
|
|
397
395
|
}
|
|
398
396
|
}
|
|
399
397
|
}
|
|
400
|
-
|
|
401
|
-
if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) {
|
|
402
|
-
// We filter here so manifest regeneration preserves source metadata only for custom modules that
|
|
403
|
-
// are still installed. Without that, customModules can retain stale entries for modules that were removed.
|
|
404
|
-
existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id));
|
|
405
|
-
}
|
|
406
398
|
} catch {
|
|
407
399
|
// If we can't read existing manifest, continue with defaults
|
|
408
400
|
}
|
|
@@ -438,7 +430,6 @@ class ManifestGenerator {
|
|
|
438
430
|
lastUpdated: new Date().toISOString(),
|
|
439
431
|
},
|
|
440
432
|
modules: updatedModules,
|
|
441
|
-
customModules: existingCustomModules,
|
|
442
433
|
ides: this.selectedIdes,
|
|
443
434
|
};
|
|
444
435
|
|