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/tools/installer/ui.js
CHANGED
|
@@ -2,7 +2,6 @@ const path = require('node:path');
|
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const { CLIUtils } = require('./cli-utils');
|
|
5
|
-
const { CustomHandler } = require('./custom-handler');
|
|
6
5
|
const { ExternalModuleManager } = require('./modules/external-manager');
|
|
7
6
|
const { getProjectRoot } = require('./project-root');
|
|
8
7
|
const prompts = require('./prompts');
|
|
@@ -48,19 +47,6 @@ function _extractMarketplaceVersion(data) {
|
|
|
48
47
|
return best;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// Separator class for visual grouping in select/multiselect prompts
|
|
52
|
-
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
|
53
|
-
class Separator {
|
|
54
|
-
constructor(text = '────────') {
|
|
55
|
-
this.line = text;
|
|
56
|
-
this.name = text;
|
|
57
|
-
}
|
|
58
|
-
type = 'separator';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Separator for choice lists (compatible interface)
|
|
62
|
-
const choiceUtils = { Separator };
|
|
63
|
-
|
|
64
50
|
/**
|
|
65
51
|
* UI utilities for the installer
|
|
66
52
|
*/
|
|
@@ -100,11 +86,6 @@ class UI {
|
|
|
100
86
|
// Check if there's an existing BMAD installation
|
|
101
87
|
const hasExistingInstall = await fs.pathExists(bmadDir);
|
|
102
88
|
|
|
103
|
-
let customContentConfig = { hasCustomContent: false };
|
|
104
|
-
if (!hasExistingInstall) {
|
|
105
|
-
customContentConfig._shouldAsk = true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
89
|
// Track action type (only set if there's an existing installation)
|
|
109
90
|
let actionType;
|
|
110
91
|
|
|
@@ -153,48 +134,9 @@ class UI {
|
|
|
153
134
|
|
|
154
135
|
// Handle quick update separately
|
|
155
136
|
if (actionType === 'quick-update') {
|
|
156
|
-
// Pass --custom-content through so installer can re-cache if cache is missing
|
|
157
|
-
let customContentForQuickUpdate = { hasCustomContent: false };
|
|
158
|
-
if (options.customContent) {
|
|
159
|
-
const paths = options.customContent
|
|
160
|
-
.split(',')
|
|
161
|
-
.map((p) => p.trim())
|
|
162
|
-
.filter(Boolean);
|
|
163
|
-
if (paths.length > 0) {
|
|
164
|
-
const customPaths = [];
|
|
165
|
-
const selectedModuleIds = [];
|
|
166
|
-
const sources = [];
|
|
167
|
-
for (const customPath of paths) {
|
|
168
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
169
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
170
|
-
if (validation) continue;
|
|
171
|
-
let moduleMeta;
|
|
172
|
-
try {
|
|
173
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
174
|
-
moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
|
|
175
|
-
} catch {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (!moduleMeta?.code) continue;
|
|
179
|
-
customPaths.push(expandedPath);
|
|
180
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
181
|
-
sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
|
|
182
|
-
}
|
|
183
|
-
if (customPaths.length > 0) {
|
|
184
|
-
customContentForQuickUpdate = {
|
|
185
|
-
hasCustomContent: true,
|
|
186
|
-
selected: true,
|
|
187
|
-
sources,
|
|
188
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
189
|
-
selectedModuleIds,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
137
|
return {
|
|
195
138
|
actionType: 'quick-update',
|
|
196
139
|
directory: confirmedDirectory,
|
|
197
|
-
customContent: customContentForQuickUpdate,
|
|
198
140
|
skipPrompts: options.yes || false,
|
|
199
141
|
};
|
|
200
142
|
}
|
|
@@ -225,120 +167,6 @@ class UI {
|
|
|
225
167
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
226
168
|
}
|
|
227
169
|
|
|
228
|
-
// After module selection, ask about custom modules
|
|
229
|
-
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
|
230
|
-
|
|
231
|
-
if (options.customContent) {
|
|
232
|
-
// Use custom content from command-line
|
|
233
|
-
const paths = options.customContent
|
|
234
|
-
.split(',')
|
|
235
|
-
.map((p) => p.trim())
|
|
236
|
-
.filter(Boolean);
|
|
237
|
-
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
|
|
238
|
-
|
|
239
|
-
// Build custom content config similar to promptCustomContentSource
|
|
240
|
-
const customPaths = [];
|
|
241
|
-
const selectedModuleIds = [];
|
|
242
|
-
const sources = [];
|
|
243
|
-
|
|
244
|
-
for (const customPath of paths) {
|
|
245
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
246
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
247
|
-
if (validation) {
|
|
248
|
-
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Read module metadata
|
|
253
|
-
let moduleMeta;
|
|
254
|
-
try {
|
|
255
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
256
|
-
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
|
|
257
|
-
const yaml = require('yaml');
|
|
258
|
-
moduleMeta = yaml.parse(moduleYaml);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (!moduleMeta) {
|
|
265
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (!moduleMeta.code) {
|
|
270
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
customPaths.push(expandedPath);
|
|
275
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
276
|
-
sources.push({
|
|
277
|
-
path: expandedPath,
|
|
278
|
-
id: moduleMeta.code,
|
|
279
|
-
name: moduleMeta.name || moduleMeta.code,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (customPaths.length > 0) {
|
|
284
|
-
customModuleResult = {
|
|
285
|
-
selectedCustomModules: selectedModuleIds,
|
|
286
|
-
customContentConfig: {
|
|
287
|
-
hasCustomContent: true,
|
|
288
|
-
selected: true,
|
|
289
|
-
sources,
|
|
290
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
291
|
-
selectedModuleIds: selectedModuleIds,
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
} else if (options.yes) {
|
|
296
|
-
// Non-interactive mode: preserve existing custom modules (matches default: false)
|
|
297
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
298
|
-
if (await fs.pathExists(cacheDir)) {
|
|
299
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
300
|
-
for (const entry of entries) {
|
|
301
|
-
if (entry.isDirectory()) {
|
|
302
|
-
customModuleResult.selectedCustomModules.push(entry.name);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
await prompts.log.info(
|
|
306
|
-
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
|
|
307
|
-
);
|
|
308
|
-
} else {
|
|
309
|
-
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
const changeCustomModules = await prompts.confirm({
|
|
313
|
-
message: 'Modify custom modules, agents, or workflows?',
|
|
314
|
-
default: false,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (changeCustomModules) {
|
|
318
|
-
customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
|
|
319
|
-
} else {
|
|
320
|
-
// Preserve existing custom modules if user doesn't want to modify them
|
|
321
|
-
const { Installer } = require('./core/installer');
|
|
322
|
-
const installer = new Installer();
|
|
323
|
-
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
|
|
324
|
-
|
|
325
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
326
|
-
if (await fs.pathExists(cacheDir)) {
|
|
327
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
328
|
-
for (const entry of entries) {
|
|
329
|
-
if (entry.isDirectory()) {
|
|
330
|
-
customModuleResult.selectedCustomModules.push(entry.name);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Merge any selected custom modules
|
|
338
|
-
if (customModuleResult.selectedCustomModules.length > 0) {
|
|
339
|
-
selectedModules.push(...customModuleResult.selectedCustomModules);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
170
|
// Ensure core is in the modules list
|
|
343
171
|
if (!selectedModules.includes('core')) {
|
|
344
172
|
selectedModules.unshift('core');
|
|
@@ -357,7 +185,6 @@ class UI {
|
|
|
357
185
|
skipIde: toolSelection.skipIde,
|
|
358
186
|
coreConfig: moduleConfigs.core || {},
|
|
359
187
|
moduleConfigs: moduleConfigs,
|
|
360
|
-
customContent: customModuleResult.customContentConfig,
|
|
361
188
|
skipPrompts: options.yes || false,
|
|
362
189
|
};
|
|
363
190
|
}
|
|
@@ -383,84 +210,6 @@ class UI {
|
|
|
383
210
|
selectedModules = await this.selectAllModules(installedModuleIds);
|
|
384
211
|
}
|
|
385
212
|
|
|
386
|
-
// Ask about custom content (local modules/agents/workflows)
|
|
387
|
-
if (options.customContent) {
|
|
388
|
-
// Use custom content from command-line
|
|
389
|
-
const paths = options.customContent
|
|
390
|
-
.split(',')
|
|
391
|
-
.map((p) => p.trim())
|
|
392
|
-
.filter(Boolean);
|
|
393
|
-
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
|
|
394
|
-
|
|
395
|
-
// Build custom content config similar to promptCustomContentSource
|
|
396
|
-
const customPaths = [];
|
|
397
|
-
const selectedModuleIds = [];
|
|
398
|
-
const sources = [];
|
|
399
|
-
|
|
400
|
-
for (const customPath of paths) {
|
|
401
|
-
const expandedPath = this.expandUserPath(customPath);
|
|
402
|
-
const validation = this.validateCustomContentPathSync(expandedPath);
|
|
403
|
-
if (validation) {
|
|
404
|
-
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Read module metadata
|
|
409
|
-
let moduleMeta;
|
|
410
|
-
try {
|
|
411
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
412
|
-
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
|
|
413
|
-
const yaml = require('yaml');
|
|
414
|
-
moduleMeta = yaml.parse(moduleYaml);
|
|
415
|
-
} catch (error) {
|
|
416
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!moduleMeta) {
|
|
421
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!moduleMeta.code) {
|
|
426
|
-
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
customPaths.push(expandedPath);
|
|
431
|
-
selectedModuleIds.push(moduleMeta.code);
|
|
432
|
-
sources.push({
|
|
433
|
-
path: expandedPath,
|
|
434
|
-
id: moduleMeta.code,
|
|
435
|
-
name: moduleMeta.name || moduleMeta.code,
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (customPaths.length > 0) {
|
|
440
|
-
customContentConfig = {
|
|
441
|
-
hasCustomContent: true,
|
|
442
|
-
selected: true,
|
|
443
|
-
sources,
|
|
444
|
-
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
|
|
445
|
-
selectedModuleIds: selectedModuleIds,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
} else if (!options.yes) {
|
|
449
|
-
const wantsCustomContent = await prompts.confirm({
|
|
450
|
-
message: 'Add custom modules, agents, or workflows from your computer?',
|
|
451
|
-
default: false,
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
if (wantsCustomContent) {
|
|
455
|
-
customContentConfig = await this.promptCustomContentSource();
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Add custom content modules if any were selected
|
|
460
|
-
if (customContentConfig && customContentConfig.selectedModuleIds) {
|
|
461
|
-
selectedModules.push(...customContentConfig.selectedModuleIds);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
213
|
// Ensure core is in the modules list
|
|
465
214
|
if (!selectedModules.includes('core')) {
|
|
466
215
|
selectedModules.unshift('core');
|
|
@@ -476,7 +225,6 @@ class UI {
|
|
|
476
225
|
skipIde: toolSelection.skipIde,
|
|
477
226
|
coreConfig: moduleConfigs.core || {},
|
|
478
227
|
moduleConfigs: moduleConfigs,
|
|
479
|
-
customContent: customContentConfig,
|
|
480
228
|
skipPrompts: options.yes || false,
|
|
481
229
|
};
|
|
482
230
|
}
|
|
@@ -814,90 +562,6 @@ class UI {
|
|
|
814
562
|
return configCollector.collectedConfig;
|
|
815
563
|
}
|
|
816
564
|
|
|
817
|
-
/**
|
|
818
|
-
* Get module choices for selection
|
|
819
|
-
* @param {Set} installedModuleIds - Currently installed module IDs
|
|
820
|
-
* @param {Object} customContentConfig - Custom content configuration
|
|
821
|
-
* @returns {Array} Module choices for prompt
|
|
822
|
-
*/
|
|
823
|
-
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
|
824
|
-
const color = await prompts.getColor();
|
|
825
|
-
const moduleChoices = [];
|
|
826
|
-
const isNewInstallation = installedModuleIds.size === 0;
|
|
827
|
-
|
|
828
|
-
const customContentItems = [];
|
|
829
|
-
|
|
830
|
-
// Add custom content items
|
|
831
|
-
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
|
|
832
|
-
// Existing installation - show from directory
|
|
833
|
-
const customHandler = new CustomHandler();
|
|
834
|
-
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
|
|
835
|
-
|
|
836
|
-
for (const customFile of customFiles) {
|
|
837
|
-
const customInfo = await customHandler.getCustomInfo(customFile);
|
|
838
|
-
if (customInfo) {
|
|
839
|
-
customContentItems.push({
|
|
840
|
-
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
|
|
841
|
-
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
|
|
842
|
-
checked: true, // Default to selected since user chose to provide custom content
|
|
843
|
-
path: customInfo.path, // Track path to avoid duplicates
|
|
844
|
-
hint: customInfo.description || undefined,
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Add official modules
|
|
851
|
-
const { OfficialModules } = require('./modules/official-modules');
|
|
852
|
-
const officialModules = new OfficialModules();
|
|
853
|
-
const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
|
|
854
|
-
|
|
855
|
-
// First, add all items to appropriate sections
|
|
856
|
-
const allCustomModules = [];
|
|
857
|
-
|
|
858
|
-
// Add custom content items from directory
|
|
859
|
-
allCustomModules.push(...customContentItems);
|
|
860
|
-
|
|
861
|
-
// Add custom modules from cache
|
|
862
|
-
for (const mod of customModulesFromCache) {
|
|
863
|
-
// Skip if this module is already in customContentItems (by path)
|
|
864
|
-
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
|
|
865
|
-
|
|
866
|
-
if (!isDuplicate) {
|
|
867
|
-
allCustomModules.push({
|
|
868
|
-
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
|
|
869
|
-
value: mod.id,
|
|
870
|
-
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
|
871
|
-
hint: mod.description || undefined,
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Add separators and modules in correct order
|
|
877
|
-
if (allCustomModules.length > 0) {
|
|
878
|
-
// Add separator for custom content, all custom modules, and official content separator
|
|
879
|
-
moduleChoices.push(
|
|
880
|
-
new choiceUtils.Separator('── Custom Content ──'),
|
|
881
|
-
...allCustomModules,
|
|
882
|
-
new choiceUtils.Separator('── Official Content ──'),
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Add official modules (only non-custom ones)
|
|
887
|
-
for (const mod of availableModules) {
|
|
888
|
-
if (!mod.isCustom) {
|
|
889
|
-
moduleChoices.push({
|
|
890
|
-
name: mod.name,
|
|
891
|
-
value: mod.id,
|
|
892
|
-
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
|
893
|
-
hint: mod.description || undefined,
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return moduleChoices;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
565
|
/**
|
|
902
566
|
* Select all modules (official + community) using grouped multiselect.
|
|
903
567
|
* Core is shown as locked but filtered from the result since it's always installed separately.
|
|
@@ -941,7 +605,7 @@ class UI {
|
|
|
941
605
|
// Local modules (BMM, BMB, etc.)
|
|
942
606
|
const localEntries = [];
|
|
943
607
|
for (const mod of localModules) {
|
|
944
|
-
if (
|
|
608
|
+
if (mod.id !== 'core') {
|
|
945
609
|
const entry = await buildModuleEntry(mod, mod.id, 'Local');
|
|
946
610
|
localEntries.push(entry);
|
|
947
611
|
if (entry.selected) {
|
|
@@ -1316,282 +980,6 @@ class UI {
|
|
|
1316
980
|
return existingInstall.ides;
|
|
1317
981
|
}
|
|
1318
982
|
|
|
1319
|
-
/**
|
|
1320
|
-
* Validate custom content path synchronously
|
|
1321
|
-
* @param {string} input - User input path
|
|
1322
|
-
* @returns {string|undefined} Error message or undefined if valid
|
|
1323
|
-
*/
|
|
1324
|
-
validateCustomContentPathSync(input) {
|
|
1325
|
-
// Allow empty input to cancel
|
|
1326
|
-
if (!input || input.trim() === '') {
|
|
1327
|
-
return; // Allow empty to exit
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
try {
|
|
1331
|
-
// Expand the path
|
|
1332
|
-
const expandedPath = this.expandUserPath(input.trim());
|
|
1333
|
-
|
|
1334
|
-
// Check if path exists
|
|
1335
|
-
if (!fs.pathExistsSync(expandedPath)) {
|
|
1336
|
-
return 'Path does not exist';
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Check if it's a directory
|
|
1340
|
-
const stat = fs.statSync(expandedPath);
|
|
1341
|
-
if (!stat.isDirectory()) {
|
|
1342
|
-
return 'Path must be a directory';
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// Check for module.yaml in the root
|
|
1346
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
1347
|
-
if (!fs.pathExistsSync(moduleYamlPath)) {
|
|
1348
|
-
return 'Directory must contain a module.yaml file in the root';
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Try to parse the module.yaml to get the module ID
|
|
1352
|
-
try {
|
|
1353
|
-
const yaml = require('yaml');
|
|
1354
|
-
const content = fs.readFileSync(moduleYamlPath, 'utf8');
|
|
1355
|
-
const moduleData = yaml.parse(content);
|
|
1356
|
-
if (!moduleData.code) {
|
|
1357
|
-
return 'module.yaml must contain a "code" field for the module ID';
|
|
1358
|
-
}
|
|
1359
|
-
} catch (error) {
|
|
1360
|
-
return 'Invalid module.yaml file: ' + error.message;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
return; // Valid
|
|
1364
|
-
} catch (error) {
|
|
1365
|
-
return 'Error validating path: ' + error.message;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
/**
|
|
1370
|
-
* Prompt user for custom content source location
|
|
1371
|
-
* @returns {Object} Custom content configuration
|
|
1372
|
-
*/
|
|
1373
|
-
async promptCustomContentSource() {
|
|
1374
|
-
const customContentConfig = { hasCustomContent: true, sources: [] };
|
|
1375
|
-
|
|
1376
|
-
// Keep asking for more sources until user is done
|
|
1377
|
-
while (true) {
|
|
1378
|
-
// First ask if user wants to add another module or continue
|
|
1379
|
-
if (customContentConfig.sources.length > 0) {
|
|
1380
|
-
const action = await prompts.select({
|
|
1381
|
-
message: 'Would you like to:',
|
|
1382
|
-
choices: [
|
|
1383
|
-
{ name: 'Add another custom module', value: 'add' },
|
|
1384
|
-
{ name: 'Continue with installation', value: 'continue' },
|
|
1385
|
-
],
|
|
1386
|
-
default: 'continue',
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
if (action === 'continue') {
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
let sourcePath;
|
|
1395
|
-
let isValid = false;
|
|
1396
|
-
|
|
1397
|
-
while (!isValid) {
|
|
1398
|
-
// Use sync validation because @clack/prompts doesn't support async validate
|
|
1399
|
-
const inputPath = await prompts.text({
|
|
1400
|
-
message: 'Path to custom module folder (press Enter to skip):',
|
|
1401
|
-
validate: (input) => this.validateCustomContentPathSync(input),
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
// If user pressed Enter without typing anything, exit the loop
|
|
1405
|
-
if (!inputPath || inputPath.trim() === '') {
|
|
1406
|
-
// If we have no modules yet, return false for no custom content
|
|
1407
|
-
if (customContentConfig.sources.length === 0) {
|
|
1408
|
-
return { hasCustomContent: false };
|
|
1409
|
-
}
|
|
1410
|
-
return customContentConfig;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
sourcePath = this.expandUserPath(inputPath);
|
|
1414
|
-
isValid = true;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// Read module.yaml to get module info
|
|
1418
|
-
const yaml = require('yaml');
|
|
1419
|
-
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
|
|
1420
|
-
const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
|
|
1421
|
-
const moduleData = yaml.parse(moduleContent);
|
|
1422
|
-
|
|
1423
|
-
// Add to sources
|
|
1424
|
-
customContentConfig.sources.push({
|
|
1425
|
-
path: sourcePath,
|
|
1426
|
-
id: moduleData.code,
|
|
1427
|
-
name: moduleData.name || moduleData.code,
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
|
-
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Ask if user wants to add these to the installation
|
|
1434
|
-
const shouldInstall = await prompts.confirm({
|
|
1435
|
-
message: `Install these ${customContentConfig.sources.length} custom modules?`,
|
|
1436
|
-
default: true,
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
if (shouldInstall) {
|
|
1440
|
-
customContentConfig.selected = true;
|
|
1441
|
-
// Store paths to module.yaml files, not directories
|
|
1442
|
-
customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
|
|
1443
|
-
// Also include module IDs for installation
|
|
1444
|
-
customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
return customContentConfig;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
/**
|
|
1451
|
-
* Handle custom modules in the modify flow
|
|
1452
|
-
* @param {string} directory - Installation directory
|
|
1453
|
-
* @param {Array} selectedModules - Currently selected modules
|
|
1454
|
-
* @returns {Object} Result with selected custom modules and custom content config
|
|
1455
|
-
*/
|
|
1456
|
-
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
|
1457
|
-
// Get existing installation to find custom modules
|
|
1458
|
-
const { existingInstall } = await this.getExistingInstallation(directory);
|
|
1459
|
-
|
|
1460
|
-
// Check if there are any custom modules in cache
|
|
1461
|
-
const { Installer } = require('./core/installer');
|
|
1462
|
-
const installer = new Installer();
|
|
1463
|
-
const { bmadDir } = await installer.findBmadDir(directory);
|
|
1464
|
-
|
|
1465
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
1466
|
-
const cachedCustomModules = [];
|
|
1467
|
-
|
|
1468
|
-
if (await fs.pathExists(cacheDir)) {
|
|
1469
|
-
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
1470
|
-
for (const entry of entries) {
|
|
1471
|
-
if (entry.isDirectory()) {
|
|
1472
|
-
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
|
|
1473
|
-
if (await fs.pathExists(moduleYamlPath)) {
|
|
1474
|
-
const yaml = require('yaml');
|
|
1475
|
-
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
|
1476
|
-
const moduleData = yaml.parse(content);
|
|
1477
|
-
|
|
1478
|
-
cachedCustomModules.push({
|
|
1479
|
-
id: entry.name,
|
|
1480
|
-
name: moduleData.name || entry.name,
|
|
1481
|
-
description: moduleData.description || 'Custom module from cache',
|
|
1482
|
-
checked: selectedModules.includes(entry.name),
|
|
1483
|
-
fromCache: true,
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const result = {
|
|
1491
|
-
selectedCustomModules: [],
|
|
1492
|
-
customContentConfig: { hasCustomContent: false },
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
// Ask user about custom modules
|
|
1496
|
-
await prompts.log.info('Custom Modules');
|
|
1497
|
-
if (cachedCustomModules.length > 0) {
|
|
1498
|
-
await prompts.log.message('Found custom modules in your installation:');
|
|
1499
|
-
} else {
|
|
1500
|
-
await prompts.log.message('No custom modules currently installed.');
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// Build choices dynamically based on whether we have existing modules
|
|
1504
|
-
const choices = [];
|
|
1505
|
-
if (cachedCustomModules.length > 0) {
|
|
1506
|
-
choices.push(
|
|
1507
|
-
{ name: 'Keep all existing custom modules', value: 'keep' },
|
|
1508
|
-
{ name: 'Select which custom modules to keep', value: 'select' },
|
|
1509
|
-
{ name: 'Add new custom modules', value: 'add' },
|
|
1510
|
-
{ name: 'Remove all custom modules', value: 'remove' },
|
|
1511
|
-
);
|
|
1512
|
-
} else {
|
|
1513
|
-
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
const customAction = await prompts.select({
|
|
1517
|
-
message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
|
|
1518
|
-
choices: choices,
|
|
1519
|
-
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
|
1520
|
-
});
|
|
1521
|
-
|
|
1522
|
-
switch (customAction) {
|
|
1523
|
-
case 'keep': {
|
|
1524
|
-
// Keep all existing custom modules
|
|
1525
|
-
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
|
1526
|
-
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
case 'select': {
|
|
1531
|
-
// Let user choose which to keep
|
|
1532
|
-
const selectChoices = cachedCustomModules.map((m) => ({
|
|
1533
|
-
name: `${m.name} (${m.id})`,
|
|
1534
|
-
value: m.id,
|
|
1535
|
-
checked: m.checked,
|
|
1536
|
-
}));
|
|
1537
|
-
|
|
1538
|
-
// Add "None / I changed my mind" option at the end
|
|
1539
|
-
const choicesWithSkip = [
|
|
1540
|
-
...selectChoices,
|
|
1541
|
-
{
|
|
1542
|
-
name: '⚠ None / I changed my mind - keep no custom modules',
|
|
1543
|
-
value: '__NONE__',
|
|
1544
|
-
checked: false,
|
|
1545
|
-
},
|
|
1546
|
-
];
|
|
1547
|
-
|
|
1548
|
-
const keepModules = await prompts.multiselect({
|
|
1549
|
-
message: 'Select custom modules to keep (use arrow keys, space to toggle):',
|
|
1550
|
-
choices: choicesWithSkip,
|
|
1551
|
-
required: true,
|
|
1552
|
-
});
|
|
1553
|
-
|
|
1554
|
-
// If user selected both "__NONE__" and other modules, honor the "None" choice
|
|
1555
|
-
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
|
|
1556
|
-
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
|
|
1557
|
-
result.selectedCustomModules = [];
|
|
1558
|
-
} else {
|
|
1559
|
-
// Filter out the special '__NONE__' value
|
|
1560
|
-
result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
|
|
1561
|
-
}
|
|
1562
|
-
break;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
case 'add': {
|
|
1566
|
-
// By default, keep existing modules when adding new ones
|
|
1567
|
-
// User chose "Add new" not "Replace", so we assume they want to keep existing
|
|
1568
|
-
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
|
1569
|
-
|
|
1570
|
-
// Then prompt for new ones (reuse existing method)
|
|
1571
|
-
const newCustomContent = await this.promptCustomContentSource();
|
|
1572
|
-
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
|
|
1573
|
-
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
|
|
1574
|
-
result.customContentConfig = newCustomContent;
|
|
1575
|
-
}
|
|
1576
|
-
break;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
case 'remove': {
|
|
1580
|
-
// Remove all custom modules
|
|
1581
|
-
await prompts.log.warn('All custom modules will be removed from the installation');
|
|
1582
|
-
break;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
case 'cancel': {
|
|
1586
|
-
// User cancelled - no custom modules
|
|
1587
|
-
await prompts.log.message('No custom modules will be added');
|
|
1588
|
-
break;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
return result;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
983
|
/**
|
|
1596
984
|
* Display module versions with update availability
|
|
1597
985
|
* @param {Array} modules - Array of module info objects with version info
|