bmad-method 6.3.1-next.3 → 6.3.1-next.4
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/cli-utils.js +0 -137
- package/tools/installer/core/manifest.js +0 -577
- package/tools/installer/ide/shared/path-utils.js +0 -145
- package/tools/installer/modules/custom-module-manager.js +0 -27
- package/tools/installer/modules/external-manager.js +0 -40
- package/tools/installer/modules/official-modules.js +2 -50
- package/tools/installer/modules/registry-client.js +0 -11
- package/tools/installer/prompts.js +0 -106
- package/tools/installer/ide/shared/module-injections.js +0 -136
package/package.json
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
const os = require('node:os');
|
|
3
1
|
const prompts = require('./prompts');
|
|
4
2
|
|
|
5
3
|
const CLIUtils = {
|
|
6
|
-
/**
|
|
7
|
-
* Get version from package.json
|
|
8
|
-
*/
|
|
9
|
-
getVersion() {
|
|
10
|
-
try {
|
|
11
|
-
const packageJson = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
12
|
-
return packageJson.version || 'Unknown';
|
|
13
|
-
} catch {
|
|
14
|
-
return 'Unknown';
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
|
|
18
4
|
/**
|
|
19
5
|
* Display BMAD logo and version using @clack intro + box
|
|
20
6
|
*/
|
|
@@ -52,37 +38,6 @@ const CLIUtils = {
|
|
|
52
38
|
});
|
|
53
39
|
},
|
|
54
40
|
|
|
55
|
-
/**
|
|
56
|
-
* Display section header
|
|
57
|
-
* @param {string} title - Section title
|
|
58
|
-
* @param {string} subtitle - Optional subtitle
|
|
59
|
-
*/
|
|
60
|
-
async displaySection(title, subtitle = null) {
|
|
61
|
-
await prompts.note(subtitle || '', title);
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Display info box
|
|
66
|
-
* @param {string|Array} content - Content to display
|
|
67
|
-
* @param {Object} options - Box options
|
|
68
|
-
*/
|
|
69
|
-
async displayBox(content, options = {}) {
|
|
70
|
-
let text = content;
|
|
71
|
-
if (Array.isArray(content)) {
|
|
72
|
-
text = content.join('\n\n');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const color = await prompts.getColor();
|
|
76
|
-
const borderColor = options.borderColor || 'cyan';
|
|
77
|
-
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
|
|
78
|
-
const formatBorder = colorMap[borderColor] || color.cyan;
|
|
79
|
-
|
|
80
|
-
await prompts.box(text, options.title, {
|
|
81
|
-
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
|
|
82
|
-
formatBorder,
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
|
|
86
41
|
/**
|
|
87
42
|
* Display module configuration header
|
|
88
43
|
* @param {string} moduleName - Module name (fallback if no custom header)
|
|
@@ -93,98 +48,6 @@ const CLIUtils = {
|
|
|
93
48
|
const title = header || `Configuring ${moduleName.toUpperCase()} Module`;
|
|
94
49
|
await prompts.note(subheader || '', title);
|
|
95
50
|
},
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Display module with no custom configuration
|
|
99
|
-
* @param {string} moduleName - Module name (fallback if no custom header)
|
|
100
|
-
* @param {string} header - Custom header from module.yaml
|
|
101
|
-
* @param {string} subheader - Custom subheader from module.yaml
|
|
102
|
-
*/
|
|
103
|
-
async displayModuleNoConfig(moduleName, header = null, subheader = null) {
|
|
104
|
-
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`;
|
|
105
|
-
await prompts.note(subheader || '', title);
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Display step indicator
|
|
110
|
-
* @param {number} current - Current step
|
|
111
|
-
* @param {number} total - Total steps
|
|
112
|
-
* @param {string} description - Step description
|
|
113
|
-
*/
|
|
114
|
-
async displayStep(current, total, description) {
|
|
115
|
-
const progress = `[${current}/${total}]`;
|
|
116
|
-
await prompts.log.step(`${progress} ${description}`);
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Display completion message
|
|
121
|
-
* @param {string} message - Completion message
|
|
122
|
-
*/
|
|
123
|
-
async displayComplete(message) {
|
|
124
|
-
const color = await prompts.getColor();
|
|
125
|
-
await prompts.box(`\u2728 ${message}`, 'Complete', {
|
|
126
|
-
rounded: true,
|
|
127
|
-
formatBorder: color.green,
|
|
128
|
-
});
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Display error message
|
|
133
|
-
* @param {string} message - Error message
|
|
134
|
-
*/
|
|
135
|
-
async displayError(message) {
|
|
136
|
-
const color = await prompts.getColor();
|
|
137
|
-
await prompts.box(`\u2717 ${message}`, 'Error', {
|
|
138
|
-
rounded: true,
|
|
139
|
-
formatBorder: color.red,
|
|
140
|
-
});
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Format list for display
|
|
145
|
-
* @param {Array} items - Items to display
|
|
146
|
-
* @param {string} prefix - Item prefix
|
|
147
|
-
*/
|
|
148
|
-
formatList(items, prefix = '\u2022') {
|
|
149
|
-
return items.map((item) => ` ${prefix} ${item}`).join('\n');
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Clear previous lines
|
|
154
|
-
* @param {number} lines - Number of lines to clear
|
|
155
|
-
*/
|
|
156
|
-
clearLines(lines) {
|
|
157
|
-
for (let i = 0; i < lines; i++) {
|
|
158
|
-
process.stdout.moveCursor(0, -1);
|
|
159
|
-
process.stdout.clearLine(1);
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Display module completion message
|
|
165
|
-
* @param {string} moduleName - Name of the completed module
|
|
166
|
-
* @param {boolean} clearScreen - Whether to clear the screen first (deprecated, always false now)
|
|
167
|
-
*/
|
|
168
|
-
displayModuleComplete(moduleName, clearScreen = false) {
|
|
169
|
-
// No longer clear screen or show boxes - just a simple completion message
|
|
170
|
-
// This is deprecated but kept for backwards compatibility
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Expand path with ~ expansion
|
|
175
|
-
* @param {string} inputPath - Path to expand
|
|
176
|
-
* @returns {string} Expanded path
|
|
177
|
-
*/
|
|
178
|
-
expandPath(inputPath) {
|
|
179
|
-
if (!inputPath) return inputPath;
|
|
180
|
-
|
|
181
|
-
// Expand ~ to home directory
|
|
182
|
-
if (inputPath.startsWith('~')) {
|
|
183
|
-
return path.join(os.homedir(), inputPath.slice(1));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return inputPath;
|
|
187
|
-
},
|
|
188
51
|
};
|
|
189
52
|
|
|
190
53
|
module.exports = { CLIUtils };
|
|
@@ -107,117 +107,6 @@ class Manifest {
|
|
|
107
107
|
return null;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
* Update existing manifest
|
|
112
|
-
* @param {string} bmadDir - Path to bmad directory
|
|
113
|
-
* @param {Object} updates - Fields to update
|
|
114
|
-
* @param {Array} installedFiles - Updated list of installed files
|
|
115
|
-
*/
|
|
116
|
-
async update(bmadDir, updates, installedFiles = null) {
|
|
117
|
-
const yaml = require('yaml');
|
|
118
|
-
const manifest = (await this._readRaw(bmadDir)) || {
|
|
119
|
-
installation: {},
|
|
120
|
-
modules: [],
|
|
121
|
-
ides: [],
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Handle module updates
|
|
125
|
-
if (updates.modules) {
|
|
126
|
-
// If modules is being updated, we need to preserve detailed module info
|
|
127
|
-
const existingDetailed = manifest.modules || [];
|
|
128
|
-
const incomingNames = updates.modules;
|
|
129
|
-
|
|
130
|
-
// Build updated modules array
|
|
131
|
-
const updatedModules = [];
|
|
132
|
-
for (const name of incomingNames) {
|
|
133
|
-
const existing = existingDetailed.find((m) => m.name === name);
|
|
134
|
-
if (existing) {
|
|
135
|
-
// Preserve existing details, update lastUpdated if this module is being updated
|
|
136
|
-
updatedModules.push({
|
|
137
|
-
...existing,
|
|
138
|
-
lastUpdated: new Date().toISOString(),
|
|
139
|
-
});
|
|
140
|
-
} else {
|
|
141
|
-
// New module - add with minimal details
|
|
142
|
-
updatedModules.push({
|
|
143
|
-
name,
|
|
144
|
-
version: null,
|
|
145
|
-
installDate: new Date().toISOString(),
|
|
146
|
-
lastUpdated: new Date().toISOString(),
|
|
147
|
-
source: 'unknown',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
manifest.modules = updatedModules;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Merge other updates
|
|
156
|
-
if (updates.version) {
|
|
157
|
-
manifest.installation.version = updates.version;
|
|
158
|
-
}
|
|
159
|
-
if (updates.installDate) {
|
|
160
|
-
manifest.installation.installDate = updates.installDate;
|
|
161
|
-
}
|
|
162
|
-
manifest.installation.lastUpdated = new Date().toISOString();
|
|
163
|
-
|
|
164
|
-
if (updates.ides) {
|
|
165
|
-
manifest.ides = updates.ides;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Handle per-module version updates
|
|
169
|
-
if (updates.moduleVersions) {
|
|
170
|
-
for (const [moduleName, versionInfo] of Object.entries(updates.moduleVersions)) {
|
|
171
|
-
const moduleIndex = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
172
|
-
if (moduleIndex !== -1) {
|
|
173
|
-
manifest.modules[moduleIndex] = {
|
|
174
|
-
...manifest.modules[moduleIndex],
|
|
175
|
-
...versionInfo,
|
|
176
|
-
lastUpdated: new Date().toISOString(),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Handle adding a new module with version info
|
|
183
|
-
if (updates.addModule) {
|
|
184
|
-
const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
|
|
185
|
-
const existing = manifest.modules.find((m) => m.name === name);
|
|
186
|
-
if (!existing) {
|
|
187
|
-
const entry = {
|
|
188
|
-
name,
|
|
189
|
-
version: version || null,
|
|
190
|
-
installDate: new Date().toISOString(),
|
|
191
|
-
lastUpdated: new Date().toISOString(),
|
|
192
|
-
source: source || 'external',
|
|
193
|
-
npmPackage: npmPackage || null,
|
|
194
|
-
repoUrl: repoUrl || null,
|
|
195
|
-
};
|
|
196
|
-
if (localPath) entry.localPath = localPath;
|
|
197
|
-
manifest.modules.push(entry);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
|
|
202
|
-
await fs.ensureDir(path.dirname(manifestPath));
|
|
203
|
-
|
|
204
|
-
// Clean the manifest data to remove any non-serializable values
|
|
205
|
-
const cleanManifestData = structuredClone(manifest);
|
|
206
|
-
|
|
207
|
-
const yamlContent = yaml.stringify(cleanManifestData, {
|
|
208
|
-
indent: 2,
|
|
209
|
-
lineWidth: 0,
|
|
210
|
-
sortKeys: false,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Ensure POSIX-compliant final newline
|
|
214
|
-
const content = yamlContent.endsWith('\n') ? yamlContent : yamlContent + '\n';
|
|
215
|
-
await fs.writeFile(manifestPath, content, 'utf8');
|
|
216
|
-
|
|
217
|
-
// Return the flattened format for compatibility
|
|
218
|
-
return this._flattenManifest(manifest);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
110
|
/**
|
|
222
111
|
* Read raw manifest data without flattening
|
|
223
112
|
* @param {string} bmadDir - Path to bmad directory
|
|
@@ -310,62 +199,6 @@ class Manifest {
|
|
|
310
199
|
await this._writeRaw(bmadDir, manifest);
|
|
311
200
|
}
|
|
312
201
|
|
|
313
|
-
/**
|
|
314
|
-
* Remove a module from the manifest
|
|
315
|
-
* @param {string} bmadDir - Path to bmad directory
|
|
316
|
-
* @param {string} moduleName - Module name to remove
|
|
317
|
-
*/
|
|
318
|
-
async removeModule(bmadDir, moduleName) {
|
|
319
|
-
const manifest = await this._readRaw(bmadDir);
|
|
320
|
-
if (!manifest || !manifest.modules) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
325
|
-
if (index !== -1) {
|
|
326
|
-
manifest.modules.splice(index, 1);
|
|
327
|
-
await this._writeRaw(bmadDir, manifest);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Update a single module's version info
|
|
333
|
-
* @param {string} bmadDir - Path to bmad directory
|
|
334
|
-
* @param {string} moduleName - Module name
|
|
335
|
-
* @param {Object} versionInfo - Version info to update
|
|
336
|
-
*/
|
|
337
|
-
async updateModuleVersion(bmadDir, moduleName, versionInfo) {
|
|
338
|
-
const manifest = await this._readRaw(bmadDir);
|
|
339
|
-
if (!manifest || !manifest.modules) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const index = manifest.modules.findIndex((m) => m.name === moduleName);
|
|
344
|
-
if (index !== -1) {
|
|
345
|
-
manifest.modules[index] = {
|
|
346
|
-
...manifest.modules[index],
|
|
347
|
-
...versionInfo,
|
|
348
|
-
lastUpdated: new Date().toISOString(),
|
|
349
|
-
};
|
|
350
|
-
await this._writeRaw(bmadDir, manifest);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Get version info for a specific module
|
|
356
|
-
* @param {string} bmadDir - Path to bmad directory
|
|
357
|
-
* @param {string} moduleName - Module name
|
|
358
|
-
* @returns {Object|null} Module version info or null
|
|
359
|
-
*/
|
|
360
|
-
async getModuleVersion(bmadDir, moduleName) {
|
|
361
|
-
const manifest = await this._readRaw(bmadDir);
|
|
362
|
-
if (!manifest || !manifest.modules) {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return manifest.modules.find((m) => m.name === moduleName) || null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
202
|
/**
|
|
370
203
|
* Get all modules with their version info
|
|
371
204
|
* @param {string} bmadDir - Path to bmad directory
|
|
@@ -403,27 +236,6 @@ class Manifest {
|
|
|
403
236
|
await fs.writeFile(manifestPath, content, 'utf8');
|
|
404
237
|
}
|
|
405
238
|
|
|
406
|
-
/**
|
|
407
|
-
* Add an IDE configuration to the manifest
|
|
408
|
-
* @param {string} bmadDir - Path to bmad directory
|
|
409
|
-
* @param {string} ideName - IDE name to add
|
|
410
|
-
*/
|
|
411
|
-
async addIde(bmadDir, ideName) {
|
|
412
|
-
const manifest = await this.read(bmadDir);
|
|
413
|
-
if (!manifest) {
|
|
414
|
-
throw new Error('No manifest found');
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (!manifest.ides) {
|
|
418
|
-
manifest.ides = [];
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (!manifest.ides.includes(ideName)) {
|
|
422
|
-
manifest.ides.push(ideName);
|
|
423
|
-
await this.update(bmadDir, { ides: manifest.ides });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
239
|
/**
|
|
428
240
|
* Calculate SHA256 hash of a file
|
|
429
241
|
* @param {string} filePath - Path to file
|
|
@@ -438,354 +250,6 @@ class Manifest {
|
|
|
438
250
|
}
|
|
439
251
|
}
|
|
440
252
|
|
|
441
|
-
/**
|
|
442
|
-
* Parse installed files to extract metadata
|
|
443
|
-
* @param {Array} installedFiles - List of installed file paths
|
|
444
|
-
* @param {string} bmadDir - Path to bmad directory for relative paths
|
|
445
|
-
* @returns {Array} Array of file metadata objects
|
|
446
|
-
*/
|
|
447
|
-
async parseInstalledFiles(installedFiles, bmadDir) {
|
|
448
|
-
const fileMetadata = [];
|
|
449
|
-
|
|
450
|
-
for (const filePath of installedFiles) {
|
|
451
|
-
const fileExt = path.extname(filePath).toLowerCase();
|
|
452
|
-
// Make path relative to parent of bmad directory, starting with 'bmad/'
|
|
453
|
-
const relativePath = 'bmad' + filePath.replace(bmadDir, '').replaceAll('\\', '/');
|
|
454
|
-
|
|
455
|
-
// Calculate file hash
|
|
456
|
-
const hash = await this.calculateFileHash(filePath);
|
|
457
|
-
|
|
458
|
-
// Handle markdown files - extract XML metadata if present
|
|
459
|
-
if (fileExt === '.md') {
|
|
460
|
-
try {
|
|
461
|
-
if (await fs.pathExists(filePath)) {
|
|
462
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
463
|
-
const metadata = this.extractXmlNodeAttributes(content, filePath, relativePath);
|
|
464
|
-
|
|
465
|
-
if (metadata) {
|
|
466
|
-
// Has XML metadata
|
|
467
|
-
metadata.hash = hash;
|
|
468
|
-
fileMetadata.push(metadata);
|
|
469
|
-
} else {
|
|
470
|
-
// No XML metadata - still track the file
|
|
471
|
-
fileMetadata.push({
|
|
472
|
-
file: relativePath,
|
|
473
|
-
type: 'md',
|
|
474
|
-
name: path.basename(filePath, fileExt),
|
|
475
|
-
title: null,
|
|
476
|
-
hash: hash,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
} catch (error) {
|
|
481
|
-
await prompts.log.warn(`Could not parse ${filePath}: ${error.message}`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
// Handle other file types (CSV, JSON, YAML, etc.)
|
|
485
|
-
else {
|
|
486
|
-
fileMetadata.push({
|
|
487
|
-
file: relativePath,
|
|
488
|
-
type: fileExt.slice(1), // Remove the dot
|
|
489
|
-
name: path.basename(filePath, fileExt),
|
|
490
|
-
title: null,
|
|
491
|
-
hash: hash,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return fileMetadata;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Extract XML node attributes from MD file content
|
|
501
|
-
* @param {string} content - File content
|
|
502
|
-
* @param {string} filePath - File path for context
|
|
503
|
-
* @param {string} relativePath - Relative path starting with 'bmad/'
|
|
504
|
-
* @returns {Object|null} Extracted metadata or null
|
|
505
|
-
*/
|
|
506
|
-
extractXmlNodeAttributes(content, filePath, relativePath) {
|
|
507
|
-
// Look for XML blocks in code fences
|
|
508
|
-
const xmlBlockMatch = content.match(/```xml\s*([\s\S]*?)```/);
|
|
509
|
-
if (!xmlBlockMatch) {
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const xmlContent = xmlBlockMatch[1];
|
|
514
|
-
|
|
515
|
-
// Extract root XML node (agent, task, template, etc.)
|
|
516
|
-
const rootNodeMatch = xmlContent.match(/<(\w+)([^>]*)>/);
|
|
517
|
-
if (!rootNodeMatch) {
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const nodeType = rootNodeMatch[1];
|
|
522
|
-
const attributes = rootNodeMatch[2];
|
|
523
|
-
|
|
524
|
-
// Extract name and title attributes (id not needed since we have path)
|
|
525
|
-
const nameMatch = attributes.match(/name="([^"]*)"/);
|
|
526
|
-
const titleMatch = attributes.match(/title="([^"]*)"/);
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
file: relativePath,
|
|
530
|
-
type: nodeType,
|
|
531
|
-
name: nameMatch ? nameMatch[1] : null,
|
|
532
|
-
title: titleMatch ? titleMatch[1] : null,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Generate CSV manifest content
|
|
538
|
-
* @param {Object} data - Manifest data
|
|
539
|
-
* @param {Array} fileMetadata - File metadata array
|
|
540
|
-
* @param {Object} moduleConfigs - Module configuration data
|
|
541
|
-
* @returns {string} CSV content
|
|
542
|
-
*/
|
|
543
|
-
generateManifestCsv(data, fileMetadata, moduleConfigs = {}) {
|
|
544
|
-
const timestamp = new Date().toISOString();
|
|
545
|
-
let csv = [];
|
|
546
|
-
|
|
547
|
-
// Header section
|
|
548
|
-
csv.push(
|
|
549
|
-
'# BMAD Manifest',
|
|
550
|
-
`# Generated: ${timestamp}`,
|
|
551
|
-
'',
|
|
552
|
-
'## Installation Info',
|
|
553
|
-
'Property,Value',
|
|
554
|
-
`Version,${data.version}`,
|
|
555
|
-
`InstallDate,${data.installDate || timestamp}`,
|
|
556
|
-
`LastUpdated,${data.lastUpdated || timestamp}`,
|
|
557
|
-
);
|
|
558
|
-
if (data.language) {
|
|
559
|
-
csv.push(`Language,${data.language}`);
|
|
560
|
-
}
|
|
561
|
-
csv.push('');
|
|
562
|
-
|
|
563
|
-
// Modules section
|
|
564
|
-
if (data.modules && data.modules.length > 0) {
|
|
565
|
-
csv.push('## Modules', 'Name,Version,ShortTitle');
|
|
566
|
-
for (const moduleName of data.modules) {
|
|
567
|
-
const config = moduleConfigs[moduleName] || {};
|
|
568
|
-
csv.push([moduleName, config.version || '', config['short-title'] || ''].map((v) => this.escapeCsv(v)).join(','));
|
|
569
|
-
}
|
|
570
|
-
csv.push('');
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// IDEs section
|
|
574
|
-
if (data.ides && data.ides.length > 0) {
|
|
575
|
-
csv.push('## IDEs', 'IDE');
|
|
576
|
-
for (const ide of data.ides) {
|
|
577
|
-
csv.push(this.escapeCsv(ide));
|
|
578
|
-
}
|
|
579
|
-
csv.push('');
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Files section - NO LONGER USED
|
|
583
|
-
// Files are now tracked in files-manifest.csv by ManifestGenerator
|
|
584
|
-
|
|
585
|
-
return csv.join('\n');
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Parse CSV manifest content back to object
|
|
590
|
-
* @param {string} csvContent - CSV content to parse
|
|
591
|
-
* @returns {Object} Parsed manifest data
|
|
592
|
-
*/
|
|
593
|
-
parseManifestCsv(csvContent) {
|
|
594
|
-
const result = {
|
|
595
|
-
modules: [],
|
|
596
|
-
ides: [],
|
|
597
|
-
files: [],
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
const lines = csvContent.split('\n');
|
|
601
|
-
let section = '';
|
|
602
|
-
|
|
603
|
-
for (const line_ of lines) {
|
|
604
|
-
const line = line_.trim();
|
|
605
|
-
|
|
606
|
-
// Skip empty lines and comments
|
|
607
|
-
if (!line || line.startsWith('#')) {
|
|
608
|
-
// Check for section headers
|
|
609
|
-
if (line.startsWith('## ')) {
|
|
610
|
-
section = line.slice(3).toLowerCase();
|
|
611
|
-
}
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Parse based on current section
|
|
616
|
-
switch (section) {
|
|
617
|
-
case 'installation info': {
|
|
618
|
-
// Skip header row
|
|
619
|
-
if (line === 'Property,Value') continue;
|
|
620
|
-
|
|
621
|
-
const [property, ...valueParts] = line.split(',');
|
|
622
|
-
const value = this.unescapeCsv(valueParts.join(','));
|
|
623
|
-
|
|
624
|
-
switch (property) {
|
|
625
|
-
// Path no longer stored in manifest
|
|
626
|
-
case 'Version': {
|
|
627
|
-
result.version = value;
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
case 'InstallDate': {
|
|
631
|
-
result.installDate = value;
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
case 'LastUpdated': {
|
|
635
|
-
result.lastUpdated = value;
|
|
636
|
-
break;
|
|
637
|
-
}
|
|
638
|
-
case 'Language': {
|
|
639
|
-
result.language = value;
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
case 'modules': {
|
|
647
|
-
// Skip header row
|
|
648
|
-
if (line === 'Name,Version,ShortTitle') continue;
|
|
649
|
-
|
|
650
|
-
const parts = this.parseCsvLine(line);
|
|
651
|
-
if (parts[0]) {
|
|
652
|
-
result.modules.push(parts[0]);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
case 'ides': {
|
|
658
|
-
// Skip header row
|
|
659
|
-
if (line === 'IDE') continue;
|
|
660
|
-
|
|
661
|
-
result.ides.push(this.unescapeCsv(line));
|
|
662
|
-
|
|
663
|
-
break;
|
|
664
|
-
}
|
|
665
|
-
case 'files': {
|
|
666
|
-
// Skip header rows (support both old and new format)
|
|
667
|
-
if (line === 'Type,Path,Name,Title' || line === 'Type,Path,Name,Title,Hash') continue;
|
|
668
|
-
|
|
669
|
-
const parts = this.parseCsvLine(line);
|
|
670
|
-
if (parts.length >= 2) {
|
|
671
|
-
result.files.push({
|
|
672
|
-
type: parts[0] || '',
|
|
673
|
-
file: parts[1] || '',
|
|
674
|
-
name: parts[2] || null,
|
|
675
|
-
title: parts[3] || null,
|
|
676
|
-
hash: parts[4] || null, // Hash column (may not exist in old manifests)
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
break;
|
|
681
|
-
}
|
|
682
|
-
// No default
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return result;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Parse a CSV line handling quotes and commas
|
|
691
|
-
* @param {string} line - CSV line to parse
|
|
692
|
-
* @returns {Array} Array of values
|
|
693
|
-
*/
|
|
694
|
-
parseCsvLine(line) {
|
|
695
|
-
const result = [];
|
|
696
|
-
let current = '';
|
|
697
|
-
let inQuotes = false;
|
|
698
|
-
|
|
699
|
-
for (let i = 0; i < line.length; i++) {
|
|
700
|
-
const char = line[i];
|
|
701
|
-
|
|
702
|
-
if (char === '"') {
|
|
703
|
-
if (inQuotes && line[i + 1] === '"') {
|
|
704
|
-
// Escaped quote
|
|
705
|
-
current += '"';
|
|
706
|
-
i++;
|
|
707
|
-
} else {
|
|
708
|
-
// Toggle quote state
|
|
709
|
-
inQuotes = !inQuotes;
|
|
710
|
-
}
|
|
711
|
-
} else if (char === ',' && !inQuotes) {
|
|
712
|
-
// Field separator
|
|
713
|
-
result.push(this.unescapeCsv(current));
|
|
714
|
-
current = '';
|
|
715
|
-
} else {
|
|
716
|
-
current += char;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Add the last field
|
|
721
|
-
result.push(this.unescapeCsv(current));
|
|
722
|
-
|
|
723
|
-
return result;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Escape CSV special characters
|
|
728
|
-
* @param {string} text - Text to escape
|
|
729
|
-
* @returns {string} Escaped text
|
|
730
|
-
*/
|
|
731
|
-
escapeCsv(text) {
|
|
732
|
-
if (!text) return '';
|
|
733
|
-
const str = String(text);
|
|
734
|
-
|
|
735
|
-
// If contains comma, newline, or quote, wrap in quotes and escape quotes
|
|
736
|
-
if (str.includes(',') || str.includes('\n') || str.includes('"')) {
|
|
737
|
-
return '"' + str.replaceAll('"', '""') + '"';
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
return str;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* Unescape CSV field
|
|
745
|
-
* @param {string} text - Text to unescape
|
|
746
|
-
* @returns {string} Unescaped text
|
|
747
|
-
*/
|
|
748
|
-
unescapeCsv(text) {
|
|
749
|
-
if (!text) return '';
|
|
750
|
-
|
|
751
|
-
// Remove surrounding quotes if present
|
|
752
|
-
if (text.startsWith('"') && text.endsWith('"')) {
|
|
753
|
-
text = text.slice(1, -1);
|
|
754
|
-
// Unescape doubled quotes
|
|
755
|
-
text = text.replaceAll('""', '"');
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return text;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Load module configuration files
|
|
763
|
-
* @param {Array} modules - List of module names
|
|
764
|
-
* @returns {Object} Module configurations indexed by name
|
|
765
|
-
*/
|
|
766
|
-
async loadModuleConfigs(modules) {
|
|
767
|
-
const configs = {};
|
|
768
|
-
|
|
769
|
-
for (const moduleName of modules) {
|
|
770
|
-
// Handle core module differently - it's in src/core-skills not src/modules/core
|
|
771
|
-
const configPath =
|
|
772
|
-
moduleName === 'core'
|
|
773
|
-
? path.join(process.cwd(), 'src', 'core-skills', 'config.yaml')
|
|
774
|
-
: path.join(process.cwd(), 'src', 'modules', moduleName, 'config.yaml');
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
if (await fs.pathExists(configPath)) {
|
|
778
|
-
const yaml = require('yaml');
|
|
779
|
-
const content = await fs.readFile(configPath, 'utf8');
|
|
780
|
-
configs[moduleName] = yaml.parse(content);
|
|
781
|
-
}
|
|
782
|
-
} catch (error) {
|
|
783
|
-
await prompts.log.warn(`Could not load config for module ${moduleName}: ${error.message}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
return configs;
|
|
788
|
-
}
|
|
789
253
|
/**
|
|
790
254
|
* Get module version info from source
|
|
791
255
|
* @param {string} moduleName - Module name/code
|
|
@@ -986,47 +450,6 @@ class Manifest {
|
|
|
986
450
|
|
|
987
451
|
return updates;
|
|
988
452
|
}
|
|
989
|
-
|
|
990
|
-
/**
|
|
991
|
-
* Compare two semantic versions
|
|
992
|
-
* @param {string} v1 - First version
|
|
993
|
-
* @param {string} v2 - Second version
|
|
994
|
-
* @returns {number} -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
|
995
|
-
*/
|
|
996
|
-
compareVersions(v1, v2) {
|
|
997
|
-
if (!v1 || !v2) return 0;
|
|
998
|
-
|
|
999
|
-
const normalize = (v) => {
|
|
1000
|
-
// Remove leading 'v' if present
|
|
1001
|
-
v = v.replace(/^v/, '');
|
|
1002
|
-
// Handle prerelease tags
|
|
1003
|
-
const parts = v.split('-');
|
|
1004
|
-
const main = parts[0].split('.');
|
|
1005
|
-
const prerelease = parts[1];
|
|
1006
|
-
return { main, prerelease };
|
|
1007
|
-
};
|
|
1008
|
-
|
|
1009
|
-
const n1 = normalize(v1);
|
|
1010
|
-
const n2 = normalize(v2);
|
|
1011
|
-
|
|
1012
|
-
// Compare main version parts
|
|
1013
|
-
for (let i = 0; i < 3; i++) {
|
|
1014
|
-
const num1 = parseInt(n1.main[i] || '0', 10);
|
|
1015
|
-
const num2 = parseInt(n2.main[i] || '0', 10);
|
|
1016
|
-
if (num1 !== num2) {
|
|
1017
|
-
return num1 < num2 ? -1 : 1;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// If main versions are equal, compare prerelease
|
|
1022
|
-
if (n1.prerelease && n2.prerelease) {
|
|
1023
|
-
return n1.prerelease < n2.prerelease ? -1 : n1.prerelease > n2.prerelease ? 1 : 0;
|
|
1024
|
-
}
|
|
1025
|
-
if (n1.prerelease) return -1; // Prerelease is older than stable
|
|
1026
|
-
if (n2.prerelease) return 1; // Stable is newer than prerelease
|
|
1027
|
-
|
|
1028
|
-
return 0;
|
|
1029
|
-
}
|
|
1030
453
|
}
|
|
1031
454
|
|
|
1032
455
|
module.exports = { Manifest };
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
* - standalone/agents/fred.md → bmad-agent-standalone-fred.md
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
// Type segments - agents are included in naming, others are filtered out
|
|
19
|
-
const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools'];
|
|
20
18
|
const AGENT_SEGMENT = 'agents';
|
|
21
19
|
|
|
22
20
|
// BMAD installation folder name - centralized constant for all installers
|
|
@@ -194,125 +192,6 @@ function parseDashName(filename) {
|
|
|
194
192
|
};
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
// ============================================================================
|
|
198
|
-
// LEGACY FUNCTIONS (underscore format) - kept for backward compatibility
|
|
199
|
-
// ============================================================================
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Convert hierarchical path to flat underscore-separated name (LEGACY)
|
|
203
|
-
* @deprecated Use toDashName instead
|
|
204
|
-
*/
|
|
205
|
-
function toUnderscoreName(module, type, name) {
|
|
206
|
-
const isAgent = type === AGENT_SEGMENT;
|
|
207
|
-
if (module === 'core') {
|
|
208
|
-
return isAgent ? `bmad_agent_${name}.md` : `bmad_${name}.md`;
|
|
209
|
-
}
|
|
210
|
-
if (module === 'standalone') {
|
|
211
|
-
return isAgent ? `bmad_agent_standalone_${name}.md` : `bmad_standalone_${name}.md`;
|
|
212
|
-
}
|
|
213
|
-
return isAgent ? `bmad_${module}_agent_${name}.md` : `bmad_${module}_${name}.md`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Convert relative path to flat underscore-separated name (LEGACY)
|
|
218
|
-
* @deprecated Use toDashPath instead
|
|
219
|
-
*/
|
|
220
|
-
function toUnderscorePath(relativePath) {
|
|
221
|
-
// Strip common file extensions (same as toDashPath for consistency)
|
|
222
|
-
const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, '');
|
|
223
|
-
const parts = withoutExt.split(/[/\\]/);
|
|
224
|
-
|
|
225
|
-
const module = parts[0];
|
|
226
|
-
const type = parts[1];
|
|
227
|
-
const name = parts.slice(2).join('_');
|
|
228
|
-
|
|
229
|
-
return toUnderscoreName(module, type, name);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Create custom agent underscore name (LEGACY)
|
|
234
|
-
* @deprecated Use customAgentDashName instead
|
|
235
|
-
*/
|
|
236
|
-
function customAgentUnderscoreName(agentName) {
|
|
237
|
-
return `bmad_custom_${agentName}.md`;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Check if a filename uses underscore format (LEGACY)
|
|
242
|
-
* @deprecated Use isDashFormat instead
|
|
243
|
-
*/
|
|
244
|
-
function isUnderscoreFormat(filename) {
|
|
245
|
-
return filename.startsWith('bmad_') && filename.includes('_');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Extract parts from an underscore-formatted filename (LEGACY)
|
|
250
|
-
* @deprecated Use parseDashName instead
|
|
251
|
-
*/
|
|
252
|
-
function parseUnderscoreName(filename) {
|
|
253
|
-
const withoutExt = filename.replace('.md', '');
|
|
254
|
-
const parts = withoutExt.split('_');
|
|
255
|
-
|
|
256
|
-
if (parts.length < 2 || parts[0] !== 'bmad') {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const agentIndex = parts.indexOf('agent');
|
|
261
|
-
|
|
262
|
-
if (agentIndex !== -1) {
|
|
263
|
-
if (agentIndex === 1) {
|
|
264
|
-
// bmad_agent_... - check for standalone
|
|
265
|
-
if (parts.length >= 4 && parts[2] === 'standalone') {
|
|
266
|
-
return {
|
|
267
|
-
prefix: parts[0],
|
|
268
|
-
module: 'standalone',
|
|
269
|
-
type: 'agents',
|
|
270
|
-
name: parts.slice(3).join('_'),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return {
|
|
274
|
-
prefix: parts[0],
|
|
275
|
-
module: 'core',
|
|
276
|
-
type: 'agents',
|
|
277
|
-
name: parts.slice(agentIndex + 1).join('_'),
|
|
278
|
-
};
|
|
279
|
-
} else {
|
|
280
|
-
return {
|
|
281
|
-
prefix: parts[0],
|
|
282
|
-
module: parts[1],
|
|
283
|
-
type: 'agents',
|
|
284
|
-
name: parts.slice(agentIndex + 1).join('_'),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (parts.length === 2) {
|
|
290
|
-
return {
|
|
291
|
-
prefix: parts[0],
|
|
292
|
-
module: 'core',
|
|
293
|
-
type: 'workflows',
|
|
294
|
-
name: parts[1],
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Check for standalone non-agent: bmad_standalone_name
|
|
299
|
-
if (parts[1] === 'standalone') {
|
|
300
|
-
return {
|
|
301
|
-
prefix: parts[0],
|
|
302
|
-
module: 'standalone',
|
|
303
|
-
type: 'workflows',
|
|
304
|
-
name: parts.slice(2).join('_'),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return {
|
|
309
|
-
prefix: parts[0],
|
|
310
|
-
module: parts[1],
|
|
311
|
-
type: 'workflows',
|
|
312
|
-
name: parts.slice(2).join('_'),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
195
|
/**
|
|
317
196
|
* Resolve the skill name for an artifact.
|
|
318
197
|
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
|
@@ -328,37 +207,13 @@ function resolveSkillName(artifact) {
|
|
|
328
207
|
return toDashPath(artifact.relativePath);
|
|
329
208
|
}
|
|
330
209
|
|
|
331
|
-
// Backward compatibility aliases (colon format was same as underscore)
|
|
332
|
-
const toColonName = toUnderscoreName;
|
|
333
|
-
const toColonPath = toUnderscorePath;
|
|
334
|
-
const customAgentColonName = customAgentUnderscoreName;
|
|
335
|
-
const isColonFormat = isUnderscoreFormat;
|
|
336
|
-
const parseColonName = parseUnderscoreName;
|
|
337
|
-
|
|
338
210
|
module.exports = {
|
|
339
|
-
// New standard (dash-based)
|
|
340
211
|
toDashName,
|
|
341
212
|
toDashPath,
|
|
342
213
|
resolveSkillName,
|
|
343
214
|
customAgentDashName,
|
|
344
215
|
isDashFormat,
|
|
345
216
|
parseDashName,
|
|
346
|
-
|
|
347
|
-
// Legacy (underscore-based) - kept for backward compatibility
|
|
348
|
-
toUnderscoreName,
|
|
349
|
-
toUnderscorePath,
|
|
350
|
-
customAgentUnderscoreName,
|
|
351
|
-
isUnderscoreFormat,
|
|
352
|
-
parseUnderscoreName,
|
|
353
|
-
|
|
354
|
-
// Backward compatibility aliases
|
|
355
|
-
toColonName,
|
|
356
|
-
toColonPath,
|
|
357
|
-
customAgentColonName,
|
|
358
|
-
isColonFormat,
|
|
359
|
-
parseColonName,
|
|
360
|
-
|
|
361
|
-
TYPE_SEGMENTS,
|
|
362
217
|
AGENT_SEGMENT,
|
|
363
218
|
BMAD_FOLDER_NAME,
|
|
364
219
|
};
|
|
@@ -155,33 +155,6 @@ class CustomModuleManager {
|
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/**
|
|
159
|
-
* @deprecated Use parseSource() instead. Kept for backward compatibility.
|
|
160
|
-
* Parse and validate a GitHub repository URL.
|
|
161
|
-
* @param {string} url - GitHub URL to validate
|
|
162
|
-
* @returns {Object} { owner, repo, isValid, error }
|
|
163
|
-
*/
|
|
164
|
-
validateGitHubUrl(url) {
|
|
165
|
-
if (!url || typeof url !== 'string') {
|
|
166
|
-
return { owner: null, repo: null, isValid: false, error: 'URL is required' };
|
|
167
|
-
}
|
|
168
|
-
const trimmed = url.trim();
|
|
169
|
-
|
|
170
|
-
// HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
|
|
171
|
-
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
172
|
-
if (httpsMatch) {
|
|
173
|
-
return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// SSH format: git@github.com:owner/repo[.git]
|
|
177
|
-
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
|
178
|
-
if (sshMatch) {
|
|
179
|
-
return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
158
|
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
|
186
159
|
|
|
187
160
|
/**
|
|
@@ -109,46 +109,6 @@ class ExternalModuleManager {
|
|
|
109
109
|
return modules.find((m) => m.code === code) || null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/**
|
|
113
|
-
* Get module info by key
|
|
114
|
-
* @param {string} key - The module key (e.g., 'bmad-creative-intelligence-suite')
|
|
115
|
-
* @returns {Object|null} Module info or null if not found
|
|
116
|
-
*/
|
|
117
|
-
async getModuleByKey(key) {
|
|
118
|
-
const modules = await this.listAvailable();
|
|
119
|
-
return modules.find((m) => m.key === key) || null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Check if a module code exists in external modules
|
|
124
|
-
* @param {string} code - The module code to check
|
|
125
|
-
* @returns {boolean} True if the module exists
|
|
126
|
-
*/
|
|
127
|
-
async hasModule(code) {
|
|
128
|
-
const module = await this.getModuleByCode(code);
|
|
129
|
-
return module !== null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get the URL for a module by code
|
|
134
|
-
* @param {string} code - The module code
|
|
135
|
-
* @returns {string|null} The URL or null if not found
|
|
136
|
-
*/
|
|
137
|
-
async getModuleUrl(code) {
|
|
138
|
-
const module = await this.getModuleByCode(code);
|
|
139
|
-
return module ? module.url : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Get the module definition path for a module by code
|
|
144
|
-
* @param {string} code - The module code
|
|
145
|
-
* @returns {string|null} The module definition path or null if not found
|
|
146
|
-
*/
|
|
147
|
-
async getModuleDefinition(code) {
|
|
148
|
-
const module = await this.getModuleByCode(code);
|
|
149
|
-
return module ? module.moduleDefinition : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
112
|
/**
|
|
153
113
|
* Get the cache directory for external modules
|
|
154
114
|
* @returns {string} Path to the external modules cache directory
|
|
@@ -12,6 +12,8 @@ class OfficialModules {
|
|
|
12
12
|
// Config collection state (merged from ConfigCollector)
|
|
13
13
|
this.collectedConfig = {};
|
|
14
14
|
this._existingConfig = null;
|
|
15
|
+
// Tracked during interactive config collection so {directory_name}
|
|
16
|
+
// placeholder defaults can be resolved in buildQuestion().
|
|
15
17
|
this.currentProjectDir = null;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -500,32 +502,6 @@ class OfficialModules {
|
|
|
500
502
|
}
|
|
501
503
|
}
|
|
502
504
|
|
|
503
|
-
/**
|
|
504
|
-
* Find all .md agent files recursively in a directory
|
|
505
|
-
* @param {string} dir - Directory to search
|
|
506
|
-
* @returns {Array} List of .md agent file paths
|
|
507
|
-
*/
|
|
508
|
-
async findAgentMdFiles(dir) {
|
|
509
|
-
const agentFiles = [];
|
|
510
|
-
|
|
511
|
-
async function searchDirectory(searchDir) {
|
|
512
|
-
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
|
513
|
-
|
|
514
|
-
for (const entry of entries) {
|
|
515
|
-
const fullPath = path.join(searchDir, entry.name);
|
|
516
|
-
|
|
517
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
518
|
-
agentFiles.push(fullPath);
|
|
519
|
-
} else if (entry.isDirectory()) {
|
|
520
|
-
await searchDirectory(fullPath);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
await searchDirectory(dir);
|
|
526
|
-
return agentFiles;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
505
|
/**
|
|
530
506
|
* Create directories declared in module.yaml's `directories` key
|
|
531
507
|
* This replaces the security-risky module installer pattern with declarative config
|
|
@@ -699,29 +675,6 @@ class OfficialModules {
|
|
|
699
675
|
return { createdDirs, movedDirs, createdWdsFolders };
|
|
700
676
|
}
|
|
701
677
|
|
|
702
|
-
/**
|
|
703
|
-
* Private: Process module configuration
|
|
704
|
-
* @param {string} modulePath - Path to installed module
|
|
705
|
-
* @param {string} moduleName - Module name
|
|
706
|
-
*/
|
|
707
|
-
async processModuleConfig(modulePath, moduleName) {
|
|
708
|
-
const configPath = path.join(modulePath, 'config.yaml');
|
|
709
|
-
|
|
710
|
-
if (await fs.pathExists(configPath)) {
|
|
711
|
-
try {
|
|
712
|
-
let configContent = await fs.readFile(configPath, 'utf8');
|
|
713
|
-
|
|
714
|
-
// Replace path placeholders
|
|
715
|
-
configContent = configContent.replaceAll('{project-root}', `bmad/${moduleName}`);
|
|
716
|
-
configContent = configContent.replaceAll('{module}', moduleName);
|
|
717
|
-
|
|
718
|
-
await fs.writeFile(configPath, configContent, 'utf8');
|
|
719
|
-
} catch (error) {
|
|
720
|
-
await prompts.log.warn(`Failed to process module config: ${error.message}`);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
678
|
/**
|
|
726
679
|
* Private: Sync module files (preserving user modifications)
|
|
727
680
|
* @param {string} sourcePath - Source module path
|
|
@@ -1091,7 +1044,6 @@ class OfficialModules {
|
|
|
1091
1044
|
*/
|
|
1092
1045
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
|
1093
1046
|
this.currentProjectDir = projectDir;
|
|
1094
|
-
|
|
1095
1047
|
// Load existing config if not already loaded
|
|
1096
1048
|
if (!this._existingConfig) {
|
|
1097
1049
|
await this.loadExistingConfig(projectDir);
|
|
@@ -50,17 +50,6 @@ class RegistryClient {
|
|
|
50
50
|
const content = await this.fetch(url, timeout);
|
|
51
51
|
return yaml.parse(content);
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Fetch a URL and parse the response as JSON.
|
|
56
|
-
* @param {string} url - URL to fetch
|
|
57
|
-
* @param {number} [timeout] - Timeout in ms
|
|
58
|
-
* @returns {Promise<Object>} Parsed JSON content
|
|
59
|
-
*/
|
|
60
|
-
async fetchJson(url, timeout) {
|
|
61
|
-
const content = await this.fetch(url, timeout);
|
|
62
|
-
return JSON.parse(content);
|
|
63
|
-
}
|
|
64
53
|
}
|
|
65
54
|
|
|
66
55
|
module.exports = { RegistryClient };
|
|
@@ -498,26 +498,6 @@ async function password(options) {
|
|
|
498
498
|
return result;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
/**
|
|
502
|
-
* Group multiple prompts together
|
|
503
|
-
* @param {Object} prompts - Object of prompt functions
|
|
504
|
-
* @param {Object} [options] - Group options
|
|
505
|
-
* @returns {Promise<Object>} Object with all answers
|
|
506
|
-
*/
|
|
507
|
-
async function group(prompts, options = {}) {
|
|
508
|
-
const clack = await getClack();
|
|
509
|
-
|
|
510
|
-
const result = await clack.group(prompts, {
|
|
511
|
-
onCancel: () => {
|
|
512
|
-
clack.cancel('Operation cancelled');
|
|
513
|
-
process.exit(0);
|
|
514
|
-
},
|
|
515
|
-
...options,
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
return result;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
501
|
/**
|
|
522
502
|
* Run tasks with spinner feedback
|
|
523
503
|
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
|
@@ -578,42 +558,6 @@ async function box(content, title, options) {
|
|
|
578
558
|
clack.box(content, title, options);
|
|
579
559
|
}
|
|
580
560
|
|
|
581
|
-
/**
|
|
582
|
-
* Create a progress bar for visualizing task completion
|
|
583
|
-
* @param {Object} [options] - Progress options (max, style, etc.)
|
|
584
|
-
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
|
|
585
|
-
*/
|
|
586
|
-
async function progress(options) {
|
|
587
|
-
const clack = await getClack();
|
|
588
|
-
return clack.progress(options);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Create a task log for displaying scrolling subprocess output
|
|
593
|
-
* @param {Object} options - TaskLog options (title, limit, retainLog)
|
|
594
|
-
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
|
|
595
|
-
*/
|
|
596
|
-
async function taskLog(options) {
|
|
597
|
-
const clack = await getClack();
|
|
598
|
-
return clack.taskLog(options);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* File system path prompt with autocomplete
|
|
603
|
-
* @param {Object} options - Path options
|
|
604
|
-
* @param {string} options.message - The prompt message
|
|
605
|
-
* @param {string} [options.initialValue] - Initial path value
|
|
606
|
-
* @param {boolean} [options.directory=false] - Only allow directories
|
|
607
|
-
* @param {Function} [options.validate] - Validation function
|
|
608
|
-
* @returns {Promise<string>} Selected path
|
|
609
|
-
*/
|
|
610
|
-
async function pathPrompt(options) {
|
|
611
|
-
const clack = await getClack();
|
|
612
|
-
const result = await clack.path(options);
|
|
613
|
-
await handleCancel(result);
|
|
614
|
-
return result;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
561
|
/**
|
|
618
562
|
* Autocomplete single-select prompt with type-ahead filtering
|
|
619
563
|
* @param {Object} options - Autocomplete options
|
|
@@ -631,50 +575,6 @@ async function autocomplete(options) {
|
|
|
631
575
|
return result;
|
|
632
576
|
}
|
|
633
577
|
|
|
634
|
-
/**
|
|
635
|
-
* Key-based instant selection prompt
|
|
636
|
-
* @param {Object} options - SelectKey options
|
|
637
|
-
* @param {string} options.message - The prompt message
|
|
638
|
-
* @param {Array} options.options - Array of choices [{value, label, hint?}]
|
|
639
|
-
* @returns {Promise<any>} Selected value
|
|
640
|
-
*/
|
|
641
|
-
async function selectKey(options) {
|
|
642
|
-
const clack = await getClack();
|
|
643
|
-
const result = await clack.selectKey(options);
|
|
644
|
-
await handleCancel(result);
|
|
645
|
-
return result;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Stream messages with dynamic content (for LLMs, generators, etc.)
|
|
650
|
-
*/
|
|
651
|
-
const stream = {
|
|
652
|
-
async info(generator) {
|
|
653
|
-
const clack = await getClack();
|
|
654
|
-
return clack.stream.info(generator);
|
|
655
|
-
},
|
|
656
|
-
async success(generator) {
|
|
657
|
-
const clack = await getClack();
|
|
658
|
-
return clack.stream.success(generator);
|
|
659
|
-
},
|
|
660
|
-
async step(generator) {
|
|
661
|
-
const clack = await getClack();
|
|
662
|
-
return clack.stream.step(generator);
|
|
663
|
-
},
|
|
664
|
-
async warn(generator) {
|
|
665
|
-
const clack = await getClack();
|
|
666
|
-
return clack.stream.warn(generator);
|
|
667
|
-
},
|
|
668
|
-
async error(generator) {
|
|
669
|
-
const clack = await getClack();
|
|
670
|
-
return clack.stream.error(generator);
|
|
671
|
-
},
|
|
672
|
-
async message(generator, options) {
|
|
673
|
-
const clack = await getClack();
|
|
674
|
-
return clack.stream.message(generator, options);
|
|
675
|
-
},
|
|
676
|
-
};
|
|
677
|
-
|
|
678
578
|
/**
|
|
679
579
|
* Get the color utility (picocolors instance from @clack/prompts)
|
|
680
580
|
* @returns {Promise<Object>} The color utility (picocolors)
|
|
@@ -790,20 +690,14 @@ module.exports = {
|
|
|
790
690
|
note,
|
|
791
691
|
box,
|
|
792
692
|
spinner,
|
|
793
|
-
progress,
|
|
794
|
-
taskLog,
|
|
795
693
|
select,
|
|
796
694
|
multiselect,
|
|
797
695
|
autocompleteMultiselect,
|
|
798
696
|
autocomplete,
|
|
799
|
-
selectKey,
|
|
800
697
|
confirm,
|
|
801
698
|
text,
|
|
802
|
-
path: pathPrompt,
|
|
803
699
|
password,
|
|
804
|
-
group,
|
|
805
700
|
tasks,
|
|
806
701
|
log,
|
|
807
|
-
stream,
|
|
808
702
|
prompt,
|
|
809
703
|
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const yaml = require('yaml');
|
|
4
|
-
const { glob } = require('glob');
|
|
5
|
-
const { getSourcePath } = require('../../project-root');
|
|
6
|
-
|
|
7
|
-
async function loadModuleInjectionConfig(handler, moduleName) {
|
|
8
|
-
const sourceModulesPath = getSourcePath('modules');
|
|
9
|
-
const handlerBaseDir = path.join(sourceModulesPath, moduleName, 'sub-modules', handler);
|
|
10
|
-
const configPath = path.join(handlerBaseDir, 'injections.yaml');
|
|
11
|
-
|
|
12
|
-
if (!(await fs.pathExists(configPath))) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const configContent = await fs.readFile(configPath, 'utf8');
|
|
17
|
-
const config = yaml.parse(configContent) || {};
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
config,
|
|
21
|
-
handlerBaseDir,
|
|
22
|
-
configPath,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function shouldApplyInjection(injection, subagentChoices) {
|
|
27
|
-
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (subagentChoices.install === 'all') {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (subagentChoices.install === 'selective') {
|
|
36
|
-
const selected = subagentChoices.selected || [];
|
|
37
|
-
|
|
38
|
-
if (injection.requires === 'any' && selected.length > 0) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (injection.requires) {
|
|
43
|
-
const required = `${injection.requires}.md`;
|
|
44
|
-
return selected.includes(required);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (injection.point) {
|
|
48
|
-
const selectedNames = selected.map((file) => file.replace('.md', ''));
|
|
49
|
-
return selectedNames.some((name) => injection.point.includes(name));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function filterAgentInstructions(content, selectedFiles) {
|
|
57
|
-
if (!selectedFiles || selectedFiles.length === 0) {
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const selectedAgents = selectedFiles.map((file) => file.replace('.md', ''));
|
|
62
|
-
const lines = content.split('\n');
|
|
63
|
-
const filteredLines = [];
|
|
64
|
-
|
|
65
|
-
for (const line of lines) {
|
|
66
|
-
if (line.includes('<llm') || line.includes('</llm>')) {
|
|
67
|
-
filteredLines.push(line);
|
|
68
|
-
} else if (line.includes('subagent')) {
|
|
69
|
-
let shouldInclude = false;
|
|
70
|
-
for (const agent of selectedAgents) {
|
|
71
|
-
if (line.includes(agent)) {
|
|
72
|
-
shouldInclude = true;
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (shouldInclude) {
|
|
78
|
-
filteredLines.push(line);
|
|
79
|
-
}
|
|
80
|
-
} else if (line.includes('When creating PRDs') || line.includes('ACTIVELY delegate')) {
|
|
81
|
-
filteredLines.push(line);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (filteredLines.length > 2) {
|
|
86
|
-
return filteredLines.join('\n');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoices) {
|
|
93
|
-
if (!subagentConfig || !subagentConfig.files) {
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!subagentChoices || subagentChoices.install === 'none') {
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let filesToCopy = subagentConfig.files;
|
|
102
|
-
|
|
103
|
-
if (subagentChoices.install === 'selective') {
|
|
104
|
-
filesToCopy = subagentChoices.selected || [];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const sourceDir = path.join(handlerBaseDir, subagentConfig.source || '');
|
|
108
|
-
const resolved = [];
|
|
109
|
-
|
|
110
|
-
for (const file of filesToCopy) {
|
|
111
|
-
// Use forward slashes for glob pattern (works on both Windows and Unix)
|
|
112
|
-
// Convert backslashes to forward slashes for glob compatibility
|
|
113
|
-
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
|
|
114
|
-
const pattern = `${normalizedSourceDir}/**/${file}`;
|
|
115
|
-
const matches = await glob(pattern);
|
|
116
|
-
|
|
117
|
-
if (matches.length > 0) {
|
|
118
|
-
const absolutePath = matches[0];
|
|
119
|
-
resolved.push({
|
|
120
|
-
file,
|
|
121
|
-
absolutePath,
|
|
122
|
-
relativePath: path.relative(sourceDir, absolutePath),
|
|
123
|
-
sourceDir,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return resolved;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
module.exports = {
|
|
132
|
-
loadModuleInjectionConfig,
|
|
133
|
-
shouldApplyInjection,
|
|
134
|
-
filterAgentInstructions,
|
|
135
|
-
resolveSubagentFiles,
|
|
136
|
-
};
|