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
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Module Source Cache
|
|
3
|
-
* Caches custom module sources under _config/custom/ to ensure they're never lost
|
|
4
|
-
* and can be checked into source control
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('fs-extra');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
const crypto = require('node:crypto');
|
|
10
|
-
const prompts = require('../prompts');
|
|
11
|
-
|
|
12
|
-
class CustomModuleCache {
|
|
13
|
-
constructor(bmadDir) {
|
|
14
|
-
this.bmadDir = bmadDir;
|
|
15
|
-
this.customCacheDir = path.join(bmadDir, '_config', 'custom');
|
|
16
|
-
this.manifestPath = path.join(this.customCacheDir, 'cache-manifest.yaml');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Ensure the custom cache directory exists
|
|
21
|
-
*/
|
|
22
|
-
async ensureCacheDir() {
|
|
23
|
-
await fs.ensureDir(this.customCacheDir);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get cache manifest
|
|
28
|
-
*/
|
|
29
|
-
async getCacheManifest() {
|
|
30
|
-
if (!(await fs.pathExists(this.manifestPath))) {
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const content = await fs.readFile(this.manifestPath, 'utf8');
|
|
35
|
-
const yaml = require('yaml');
|
|
36
|
-
return yaml.parse(content) || {};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Update cache manifest
|
|
41
|
-
*/
|
|
42
|
-
async updateCacheManifest(manifest) {
|
|
43
|
-
const yaml = require('yaml');
|
|
44
|
-
// Clean the manifest to remove any non-serializable values
|
|
45
|
-
const cleanManifest = structuredClone(manifest);
|
|
46
|
-
|
|
47
|
-
const content = yaml.stringify(cleanManifest, {
|
|
48
|
-
indent: 2,
|
|
49
|
-
lineWidth: 0,
|
|
50
|
-
sortKeys: false,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
await fs.writeFile(this.manifestPath, content);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Stream a file into the hash to avoid loading entire file into memory
|
|
58
|
-
*/
|
|
59
|
-
async hashFileStream(filePath, hash) {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
const stream = require('node:fs').createReadStream(filePath);
|
|
62
|
-
stream.on('data', (chunk) => hash.update(chunk));
|
|
63
|
-
stream.on('end', resolve);
|
|
64
|
-
stream.on('error', reject);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Calculate hash of a file or directory using streaming to minimize memory usage
|
|
70
|
-
*/
|
|
71
|
-
async calculateHash(sourcePath) {
|
|
72
|
-
const hash = crypto.createHash('sha256');
|
|
73
|
-
|
|
74
|
-
const isDir = (await fs.stat(sourcePath)).isDirectory();
|
|
75
|
-
|
|
76
|
-
if (isDir) {
|
|
77
|
-
// For directories, hash all files
|
|
78
|
-
const files = [];
|
|
79
|
-
async function collectFiles(dir) {
|
|
80
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
81
|
-
for (const entry of entries) {
|
|
82
|
-
if (entry.isFile()) {
|
|
83
|
-
files.push(path.join(dir, entry.name));
|
|
84
|
-
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
85
|
-
await collectFiles(path.join(dir, entry.name));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await collectFiles(sourcePath);
|
|
91
|
-
files.sort(); // Ensure consistent order
|
|
92
|
-
|
|
93
|
-
for (const file of files) {
|
|
94
|
-
const relativePath = path.relative(sourcePath, file);
|
|
95
|
-
// Hash the path first, then stream file contents
|
|
96
|
-
hash.update(relativePath + '|');
|
|
97
|
-
await this.hashFileStream(file, hash);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
// For single files, stream directly into hash
|
|
101
|
-
await this.hashFileStream(sourcePath, hash);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return hash.digest('hex');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Cache a custom module source
|
|
109
|
-
* @param {string} moduleId - Module ID
|
|
110
|
-
* @param {string} sourcePath - Original source path
|
|
111
|
-
* @param {Object} metadata - Additional metadata to store
|
|
112
|
-
* @returns {Object} Cached module info
|
|
113
|
-
*/
|
|
114
|
-
async cacheModule(moduleId, sourcePath, metadata = {}) {
|
|
115
|
-
await this.ensureCacheDir();
|
|
116
|
-
|
|
117
|
-
const cacheDir = path.join(this.customCacheDir, moduleId);
|
|
118
|
-
const cacheManifest = await this.getCacheManifest();
|
|
119
|
-
|
|
120
|
-
// Check if already cached and unchanged
|
|
121
|
-
if (cacheManifest[moduleId]) {
|
|
122
|
-
const cached = cacheManifest[moduleId];
|
|
123
|
-
if (cached.originalHash && cached.originalHash === (await this.calculateHash(sourcePath))) {
|
|
124
|
-
// Source unchanged, return existing cache info
|
|
125
|
-
return {
|
|
126
|
-
moduleId,
|
|
127
|
-
cachePath: cacheDir,
|
|
128
|
-
...cached,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Remove existing cache if it exists
|
|
134
|
-
if (await fs.pathExists(cacheDir)) {
|
|
135
|
-
await fs.remove(cacheDir);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Copy module to cache
|
|
139
|
-
await fs.copy(sourcePath, cacheDir, {
|
|
140
|
-
filter: (src) => {
|
|
141
|
-
const relative = path.relative(sourcePath, src);
|
|
142
|
-
// Skip node_modules, .git, and other common ignore patterns
|
|
143
|
-
return !relative.includes('node_modules') && !relative.startsWith('.git') && !relative.startsWith('.DS_Store');
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Calculate hash of the source
|
|
148
|
-
const sourceHash = await this.calculateHash(sourcePath);
|
|
149
|
-
const cacheHash = await this.calculateHash(cacheDir);
|
|
150
|
-
|
|
151
|
-
// Update manifest - don't store absolute paths for portability
|
|
152
|
-
// Clean metadata to remove absolute paths
|
|
153
|
-
const cleanMetadata = { ...metadata };
|
|
154
|
-
if (cleanMetadata.sourcePath) {
|
|
155
|
-
delete cleanMetadata.sourcePath;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
cacheManifest[moduleId] = {
|
|
159
|
-
originalHash: sourceHash,
|
|
160
|
-
cacheHash: cacheHash,
|
|
161
|
-
cachedAt: new Date().toISOString(),
|
|
162
|
-
...cleanMetadata,
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
await this.updateCacheManifest(cacheManifest);
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
moduleId,
|
|
169
|
-
cachePath: cacheDir,
|
|
170
|
-
...cacheManifest[moduleId],
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get cached module info
|
|
176
|
-
* @param {string} moduleId - Module ID
|
|
177
|
-
* @returns {Object|null} Cached module info or null
|
|
178
|
-
*/
|
|
179
|
-
async getCachedModule(moduleId) {
|
|
180
|
-
const cacheManifest = await this.getCacheManifest();
|
|
181
|
-
const cached = cacheManifest[moduleId];
|
|
182
|
-
|
|
183
|
-
if (!cached) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const cacheDir = path.join(this.customCacheDir, moduleId);
|
|
188
|
-
|
|
189
|
-
if (!(await fs.pathExists(cacheDir))) {
|
|
190
|
-
// Cache dir missing, remove from manifest
|
|
191
|
-
delete cacheManifest[moduleId];
|
|
192
|
-
await this.updateCacheManifest(cacheManifest);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Verify cache integrity
|
|
197
|
-
const currentCacheHash = await this.calculateHash(cacheDir);
|
|
198
|
-
if (currentCacheHash !== cached.cacheHash) {
|
|
199
|
-
await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
moduleId,
|
|
204
|
-
cachePath: cacheDir,
|
|
205
|
-
...cached,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get all cached modules
|
|
211
|
-
* @returns {Array} Array of cached module info
|
|
212
|
-
*/
|
|
213
|
-
async getAllCachedModules() {
|
|
214
|
-
const cacheManifest = await this.getCacheManifest();
|
|
215
|
-
const cached = [];
|
|
216
|
-
|
|
217
|
-
for (const [moduleId, info] of Object.entries(cacheManifest)) {
|
|
218
|
-
const cachedModule = await this.getCachedModule(moduleId);
|
|
219
|
-
if (cachedModule) {
|
|
220
|
-
cached.push(cachedModule);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return cached;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Remove a cached module
|
|
229
|
-
* @param {string} moduleId - Module ID to remove
|
|
230
|
-
*/
|
|
231
|
-
async removeCachedModule(moduleId) {
|
|
232
|
-
const cacheManifest = await this.getCacheManifest();
|
|
233
|
-
const cacheDir = path.join(this.customCacheDir, moduleId);
|
|
234
|
-
|
|
235
|
-
// Remove cache directory
|
|
236
|
-
if (await fs.pathExists(cacheDir)) {
|
|
237
|
-
await fs.remove(cacheDir);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Remove from manifest
|
|
241
|
-
delete cacheManifest[moduleId];
|
|
242
|
-
await this.updateCacheManifest(cacheManifest);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Sync cached modules with a list of module IDs
|
|
247
|
-
* @param {Array<string>} moduleIds - Module IDs to keep
|
|
248
|
-
*/
|
|
249
|
-
async syncCache(moduleIds) {
|
|
250
|
-
const cached = await this.getAllCachedModules();
|
|
251
|
-
|
|
252
|
-
for (const cachedModule of cached) {
|
|
253
|
-
if (!moduleIds.includes(cachedModule.moduleId)) {
|
|
254
|
-
await this.removeCachedModule(cachedModule.moduleId);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
module.exports = { CustomModuleCache };
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const yaml = require('yaml');
|
|
4
|
-
const prompts = require('./prompts');
|
|
5
|
-
/**
|
|
6
|
-
* Handler for custom content (custom.yaml)
|
|
7
|
-
* Discovers custom agents and workflows in the project
|
|
8
|
-
*/
|
|
9
|
-
class CustomHandler {
|
|
10
|
-
/**
|
|
11
|
-
* Find all custom.yaml files in the project
|
|
12
|
-
* @param {string} projectRoot - Project root directory
|
|
13
|
-
* @returns {Array} List of custom content paths
|
|
14
|
-
*/
|
|
15
|
-
async findCustomContent(projectRoot) {
|
|
16
|
-
const customPaths = [];
|
|
17
|
-
|
|
18
|
-
// Helper function to recursively scan directories
|
|
19
|
-
async function scanDirectory(dir, excludePaths = []) {
|
|
20
|
-
try {
|
|
21
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
22
|
-
|
|
23
|
-
for (const entry of entries) {
|
|
24
|
-
const fullPath = path.join(dir, entry.name);
|
|
25
|
-
|
|
26
|
-
// Skip hidden directories and common exclusions
|
|
27
|
-
if (
|
|
28
|
-
entry.name.startsWith('.') ||
|
|
29
|
-
entry.name === 'node_modules' ||
|
|
30
|
-
entry.name === 'dist' ||
|
|
31
|
-
entry.name === 'build' ||
|
|
32
|
-
entry.name === '.git' ||
|
|
33
|
-
entry.name === 'bmad'
|
|
34
|
-
) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Skip excluded paths
|
|
39
|
-
if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (entry.isDirectory()) {
|
|
44
|
-
// Recursively scan subdirectories
|
|
45
|
-
await scanDirectory(fullPath, excludePaths);
|
|
46
|
-
} else if (entry.name === 'custom.yaml') {
|
|
47
|
-
// Found a custom.yaml file
|
|
48
|
-
customPaths.push(fullPath);
|
|
49
|
-
} else if (
|
|
50
|
-
entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
|
|
51
|
-
// Skip if it's in src/modules (those are standard modules)
|
|
52
|
-
!fullPath.includes(path.join('src', 'modules'))
|
|
53
|
-
) {
|
|
54
|
-
customPaths.push(fullPath);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
// Ignore errors (e.g., permission denied)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Scan the entire project, but exclude source directories
|
|
63
|
-
await scanDirectory(projectRoot, [path.join(projectRoot, 'src'), path.join(projectRoot, 'tools'), path.join(projectRoot, 'test')]);
|
|
64
|
-
|
|
65
|
-
return customPaths;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get custom content info from a custom.yaml or module.yaml file
|
|
70
|
-
* @param {string} configPath - Path to config file
|
|
71
|
-
* @param {string} projectRoot - Project root directory for calculating relative paths
|
|
72
|
-
* @returns {Object|null} Custom content info
|
|
73
|
-
*/
|
|
74
|
-
async getCustomInfo(configPath, projectRoot = null) {
|
|
75
|
-
try {
|
|
76
|
-
const configContent = await fs.readFile(configPath, 'utf8');
|
|
77
|
-
|
|
78
|
-
// Try to parse YAML with error handling
|
|
79
|
-
let config;
|
|
80
|
-
try {
|
|
81
|
-
config = yaml.parse(configContent);
|
|
82
|
-
} catch (parseError) {
|
|
83
|
-
await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check if this is an module.yaml (module) or custom.yaml (custom content)
|
|
88
|
-
const isInstallConfig = configPath.endsWith('module.yaml');
|
|
89
|
-
const configDir = path.dirname(configPath);
|
|
90
|
-
|
|
91
|
-
// Use provided projectRoot or fall back to process.cwd()
|
|
92
|
-
const basePath = projectRoot || process.cwd();
|
|
93
|
-
const relativePath = path.relative(basePath, configDir);
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
id: config.code || 'unknown-code',
|
|
97
|
-
name: config.name,
|
|
98
|
-
description: config.description || '',
|
|
99
|
-
path: configDir,
|
|
100
|
-
relativePath: relativePath,
|
|
101
|
-
defaultSelected: config.default_selected === true,
|
|
102
|
-
config: config,
|
|
103
|
-
isInstallConfig: isInstallConfig, // Track which type this is
|
|
104
|
-
};
|
|
105
|
-
} catch (error) {
|
|
106
|
-
await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message);
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
module.exports = { CustomHandler };
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
const fs = require('fs-extra');
|
|
3
|
-
const yaml = require('yaml');
|
|
4
|
-
const { CustomHandler } = require('../custom-handler');
|
|
5
|
-
const { Manifest } = require('../core/manifest');
|
|
6
|
-
const prompts = require('../prompts');
|
|
7
|
-
|
|
8
|
-
class CustomModules {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.paths = new Map();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
has(moduleCode) {
|
|
14
|
-
return this.paths.has(moduleCode);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
get(moduleCode) {
|
|
18
|
-
return this.paths.get(moduleCode);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
set(moduleId, sourcePath) {
|
|
22
|
-
this.paths.set(moduleId, sourcePath);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Install a custom module from its source path.
|
|
27
|
-
* @param {string} moduleName - Module identifier
|
|
28
|
-
* @param {string} bmadDir - Target bmad directory
|
|
29
|
-
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
30
|
-
* @param {Object} options - Install options
|
|
31
|
-
* @param {Object} options.moduleConfig - Pre-collected module configuration
|
|
32
|
-
* @returns {Object} Install result
|
|
33
|
-
*/
|
|
34
|
-
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
|
|
35
|
-
const sourcePath = this.paths.get(moduleName);
|
|
36
|
-
if (!sourcePath) {
|
|
37
|
-
throw new Error(`No source path for custom module '${moduleName}'`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!(await fs.pathExists(sourcePath))) {
|
|
41
|
-
throw new Error(`Source for custom module '${moduleName}' not found at: ${sourcePath}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const targetPath = path.join(bmadDir, moduleName);
|
|
45
|
-
|
|
46
|
-
// Read custom.yaml and merge into module config
|
|
47
|
-
let moduleConfig = options.moduleConfig ? { ...options.moduleConfig } : {};
|
|
48
|
-
const customConfigPath = path.join(sourcePath, 'custom.yaml');
|
|
49
|
-
if (await fs.pathExists(customConfigPath)) {
|
|
50
|
-
try {
|
|
51
|
-
const content = await fs.readFile(customConfigPath, 'utf8');
|
|
52
|
-
const customConfig = yaml.parse(content);
|
|
53
|
-
if (customConfig) {
|
|
54
|
-
moduleConfig = { ...moduleConfig, ...customConfig };
|
|
55
|
-
}
|
|
56
|
-
} catch (error) {
|
|
57
|
-
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Remove existing installation
|
|
62
|
-
if (await fs.pathExists(targetPath)) {
|
|
63
|
-
await fs.remove(targetPath);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Copy files with filtering
|
|
67
|
-
await this._copyWithFiltering(sourcePath, targetPath, fileTrackingCallback);
|
|
68
|
-
|
|
69
|
-
// Add to manifest
|
|
70
|
-
const manifest = new Manifest();
|
|
71
|
-
const versionInfo = await manifest.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
72
|
-
await manifest.addModule(bmadDir, moduleName, {
|
|
73
|
-
version: versionInfo.version,
|
|
74
|
-
source: versionInfo.source,
|
|
75
|
-
npmPackage: versionInfo.npmPackage,
|
|
76
|
-
repoUrl: versionInfo.repoUrl,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
return { success: true, module: moduleName, path: targetPath, moduleConfig };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Copy module files, filtering out install-time-only artifacts.
|
|
84
|
-
* @param {string} sourcePath - Source module directory
|
|
85
|
-
* @param {string} targetPath - Target module directory
|
|
86
|
-
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
|
87
|
-
*/
|
|
88
|
-
async _copyWithFiltering(sourcePath, targetPath, fileTrackingCallback = null) {
|
|
89
|
-
const files = await this._getFileList(sourcePath);
|
|
90
|
-
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
if (file.startsWith('sub-modules/')) continue;
|
|
93
|
-
|
|
94
|
-
const isInSidecar = path
|
|
95
|
-
.dirname(file)
|
|
96
|
-
.split('/')
|
|
97
|
-
.some((dir) => dir.toLowerCase().endsWith('-sidecar'));
|
|
98
|
-
if (isInSidecar) continue;
|
|
99
|
-
|
|
100
|
-
if (file === 'module.yaml') continue;
|
|
101
|
-
if (file === 'config.yaml') continue;
|
|
102
|
-
|
|
103
|
-
const sourceFile = path.join(sourcePath, file);
|
|
104
|
-
const targetFile = path.join(targetPath, file);
|
|
105
|
-
|
|
106
|
-
// Skip web-only agents
|
|
107
|
-
if (file.startsWith('agents/') && file.endsWith('.md')) {
|
|
108
|
-
const content = await fs.readFile(sourceFile, 'utf8');
|
|
109
|
-
if (/<agent[^>]*\slocalskip="true"[^>]*>/.test(content)) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await fs.ensureDir(path.dirname(targetFile));
|
|
115
|
-
await fs.copy(sourceFile, targetFile, { overwrite: true });
|
|
116
|
-
|
|
117
|
-
if (fileTrackingCallback) {
|
|
118
|
-
fileTrackingCallback(targetFile);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Recursively list all files in a directory.
|
|
125
|
-
* @param {string} dir - Directory to scan
|
|
126
|
-
* @param {string} baseDir - Base directory for relative paths
|
|
127
|
-
* @returns {string[]} Relative file paths
|
|
128
|
-
*/
|
|
129
|
-
async _getFileList(dir, baseDir = dir) {
|
|
130
|
-
const files = [];
|
|
131
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
132
|
-
|
|
133
|
-
for (const entry of entries) {
|
|
134
|
-
const fullPath = path.join(dir, entry.name);
|
|
135
|
-
if (entry.isDirectory()) {
|
|
136
|
-
files.push(...(await this._getFileList(fullPath, baseDir)));
|
|
137
|
-
} else {
|
|
138
|
-
files.push(path.relative(baseDir, fullPath));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return files;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Discover custom module source paths from all available sources.
|
|
147
|
-
* @param {Object} config - Installation configuration
|
|
148
|
-
* @param {Object} paths - InstallPaths instance
|
|
149
|
-
* @returns {Map<string, string>} Map of module ID to source path
|
|
150
|
-
*/
|
|
151
|
-
async discoverPaths(config, paths) {
|
|
152
|
-
this.paths = new Map();
|
|
153
|
-
|
|
154
|
-
if (config._quickUpdate) {
|
|
155
|
-
if (config._customModuleSources) {
|
|
156
|
-
for (const [moduleId, customInfo] of config._customModuleSources) {
|
|
157
|
-
this.paths.set(moduleId, customInfo.sourcePath);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return this.paths;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// From UI: selectedFiles
|
|
164
|
-
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
|
|
165
|
-
const customHandler = new CustomHandler();
|
|
166
|
-
for (const customFile of config.customContent.selectedFiles) {
|
|
167
|
-
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
|
|
168
|
-
if (customInfo && customInfo.id) {
|
|
169
|
-
this.paths.set(customInfo.id, customInfo.path);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// From UI: sources
|
|
175
|
-
if (config.customContent && config.customContent.sources) {
|
|
176
|
-
for (const source of config.customContent.sources) {
|
|
177
|
-
this.paths.set(source.id, source.path);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// From UI: cachedModules
|
|
182
|
-
if (config.customContent && config.customContent.cachedModules) {
|
|
183
|
-
const selectedCachedIds = config.customContent.selectedCachedModules || [];
|
|
184
|
-
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
|
|
185
|
-
|
|
186
|
-
for (const cachedModule of config.customContent.cachedModules) {
|
|
187
|
-
if (cachedModule.id && cachedModule.cachePath && (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))) {
|
|
188
|
-
this.paths.set(cachedModule.id, cachedModule.cachePath);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return this.paths;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Assemble quick-update source candidates before install() hands them to discoverPaths().
|
|
198
|
-
* This exists because discoverPaths() consumes already-prepared quick-update sources,
|
|
199
|
-
* while quickUpdate() still has to build that source map from manifest, explicit inputs,
|
|
200
|
-
* and cache conventions.
|
|
201
|
-
* Precedence: manifest-backed paths, explicit sources override them, then cached modules.
|
|
202
|
-
* @param {Object} config - Quick update configuration
|
|
203
|
-
* @param {Object} existingInstall - Existing installation snapshot
|
|
204
|
-
* @param {string} bmadDir - BMAD directory
|
|
205
|
-
* @param {Object} externalModuleManager - External module manager
|
|
206
|
-
* @returns {Promise<Map<string, Object>>} Map of custom module ID to source info
|
|
207
|
-
*/
|
|
208
|
-
async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) {
|
|
209
|
-
const projectRoot = path.dirname(bmadDir);
|
|
210
|
-
const customModuleSources = new Map();
|
|
211
|
-
|
|
212
|
-
if (existingInstall.customModules) {
|
|
213
|
-
for (const customModule of existingInstall.customModules) {
|
|
214
|
-
// Skip if no ID - can't reliably track or re-cache without it
|
|
215
|
-
if (!customModule?.id) continue;
|
|
216
|
-
|
|
217
|
-
let sourcePath = customModule.sourcePath;
|
|
218
|
-
if (sourcePath && sourcePath.startsWith('_config')) {
|
|
219
|
-
// Paths are relative to BMAD dir, but we want absolute paths for install
|
|
220
|
-
sourcePath = path.join(bmadDir, sourcePath);
|
|
221
|
-
} else if (!sourcePath && customModule.relativePath) {
|
|
222
|
-
// Fall back to relativePath
|
|
223
|
-
sourcePath = path.resolve(projectRoot, customModule.relativePath);
|
|
224
|
-
} else if (sourcePath && !path.isAbsolute(sourcePath)) {
|
|
225
|
-
// If we have a sourcePath but it's not absolute, resolve it relative to project root
|
|
226
|
-
sourcePath = path.resolve(projectRoot, sourcePath);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// If we still don't have a valid source path, skip this module
|
|
230
|
-
if (!sourcePath || !(await fs.pathExists(sourcePath))) {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
customModuleSources.set(customModule.id, {
|
|
235
|
-
id: customModule.id,
|
|
236
|
-
name: customModule.name || customModule.id,
|
|
237
|
-
sourcePath,
|
|
238
|
-
relativePath: customModule.relativePath,
|
|
239
|
-
cached: false,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (config.customContent?.sources?.length > 0) {
|
|
245
|
-
for (const source of config.customContent.sources) {
|
|
246
|
-
if (source.id && source.path) {
|
|
247
|
-
customModuleSources.set(source.id, {
|
|
248
|
-
id: source.id,
|
|
249
|
-
name: source.name || source.id,
|
|
250
|
-
sourcePath: source.path,
|
|
251
|
-
cached: false, // From CLI, will be re-cached
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
|
258
|
-
if (!(await fs.pathExists(cacheDir))) {
|
|
259
|
-
return customModuleSources;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
263
|
-
for (const cachedModule of cachedModules) {
|
|
264
|
-
const moduleId = cachedModule.name;
|
|
265
|
-
const cachedPath = path.join(cacheDir, moduleId);
|
|
266
|
-
|
|
267
|
-
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
268
|
-
if (!(await fs.pathExists(cachedPath))) {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
if (!cachedModule.isDirectory()) {
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Skip if we already have this module from manifest
|
|
276
|
-
if (customModuleSources.has(moduleId)) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Check if this is an external official module - skip cache for those
|
|
281
|
-
const isExternal = await externalModuleManager.hasModule(moduleId);
|
|
282
|
-
if (isExternal) {
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Check if this is actually a custom module (has module.yaml)
|
|
287
|
-
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
288
|
-
if (await fs.pathExists(moduleYamlPath)) {
|
|
289
|
-
customModuleSources.set(moduleId, {
|
|
290
|
-
id: moduleId,
|
|
291
|
-
name: moduleId,
|
|
292
|
-
sourcePath: cachedPath,
|
|
293
|
-
cached: true,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return customModuleSources;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
module.exports = { CustomModules };
|